2024年5月18日 星期六

好站 : SQLite

這兩天寫爬蟲有用到 SQLite, 除了複習自己寫的筆記外, 還找到下面這篇很棒的文章 :


此文從安裝 DB Browser for SQLite 建立資料庫, 到新增/修改/刪除資料表,  CRUD 作業之 SQL 語法, 資料庫匯出匯入, 到資料表欄位說明等都整理得非常簡明扼要, 是臨時要 SQLite 用時可以迅速找到解答的好地方. 

露天購買太陽能 LED 燈

今天逛露天賣場看到下面這款太陽能 LED 燈似乎不錯用, 可以裝在陽台晚上自動亮燈 :





萊爾富免運 169. 先買一個試試看, 好用再加購.

露天購買 K10 Max 四軸遙控飛機

今天又買了一個新玩具, 前天在露天看到下面這款 K10 Max 四軸遙控, 覺得是這價位中飛控做得還不錯的一款入門練習機, 賣家原本最多只有配雙電池, 經我要求增加一個單購電池選項才下標購買 (一顆 200 元) : 






這款 K10 Max 在 Aliexpress 也有賣, 但它只配單電池 :


雖然這種 1000 多塊的玩具型 4 軸不用期待太高, 但可以拿來當練習機熟悉四軸控制. 以下是在 YT 找到的一些介紹與測試影片 :














2024年5月17日 星期五

好用的 SQLite 管理軟體 SQLite Expert Personal

SQLite 是一款小而美的單檔資料庫軟體, 今天找到一款好用的 SQLite 資料庫管理軟體 SQLite Expert Personal 來操作資料庫, 網址如下 : 


官網提供商用收費的 SQLite Expert Professional 試用版 (30 日到期) 與免費的 Personal 版 (功能較少但無到期日), 安裝 Personal 版就夠用了 : 




關於付費之專業版與免費版功能差異比較參考 :



1. 安裝軟體 : 

執行下載之檔案進行安裝 : 








安裝完畢執行此軟體 : 




2. 新增資料庫 : 

按左上角的按鈕即可新增資料庫 :





3. 新增資料表 : 

開啟資料庫後, 按上面中間的 "New Table" 鈕 :




輸入資料表名稱 : 





切換到第二個頁籤 "Columns", 按右上角的 "Add" 鈕 :




於彈出視窗中設定欄位資料後按 OK, 先輸入 date, stock_id, 與 stock_name 這三個欄位 :







按右下角的 "Apply" 鈕即將此三欄位加入資料表中 : 



Python 學習筆記 : 網頁爬蟲實戰 (八) 台股每日盤後資訊網頁

今天繼續來玩 Python 爬蟲, 本篇要爬的對象是證交所台股每日盤後資訊網頁, 十幾年前我用 PHP 爬這張網頁時覺得處理起來很麻煩 (那時的網頁是將全部上市股票擠在一張網頁, 用字串處理技巧硬剖), 現在改用 Python 相對簡單多了. 

本系列之前的筆記參考 : 


本篇測試部分作法參考了下面這本書中的範例加以改寫 : 



一. 檢視目標網頁 :  

證交所台股每日盤後資訊網頁之網址如下 :





不過這個網頁只是大盤統計資料而已, 我們要的標的網頁必須勾選上面的 "分類" 選項, 勾選 "全部 (不含權證, 牛熊證, 可展延牛熊證)" 後按查詢才會看到 : 




但得到的網頁包含了 8 張表格, 我們的目標資訊是最底下的那張表格 :




由於查詢後網址不變, 可見網頁中的這 8 張表格是透過 Ajax/XHR 產生的, 這可以用 Chrome 擴充功能 Quick Javascript Switcher 關掉 Javascript 功能印證 (關掉就看不到這些表格), 所以可以利用 Chrome 開發者功能的 Networking/XHR 功能來觀察後端提供表格內容的網址, 可取得證交所不公開的 API 來直接擷取資料 (通常是 JSON 檔). 

首先在瀏覽器上按 F12 打開開發者視窗, 然後重新整理上面台股每日盤後資訊網頁, 開發者工具視窗點選 "Networking" 與 "Fetch/XHR", 點擊左邊的回應網址, 看看右邊 Response 頁籤中是否有我們要的目標資料 (通常是檔案較大的那個) :




找到目標後在左邊回應網址上按滑鼠右鍵選 "Copy/Copy URL" 將網址複製下來 :

https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20240513&type=ALLBUT0999&response=json&_=1715650102878

這就是證交所不公開的 API, 透過此 URL 即可直接取得我們想要的資料了 (完全用不到 BeautifulSoup). 網址最後面的參數 _ 可有可無, 即使去掉也可以順利取得資料 (JSON) :


把此網址貼到 Chrome 就會看到傳回的 JSON 字串 :




勾選左上角的 "美化排版" 即可將 JSON 排列整齊以利閱讀 :



{
    "tables": [
        {
            "title": "113年05月13日 價格指數(臺灣證券交易所)",
            "fields": [
                "指數",
                "收盤指數",
                "漲跌(+/-)",
                "漲跌點數",
                "漲跌百分比(%)",
                "特殊處理註記"
            ],
            "data": [
                [
                    "寶島股價指數",
                    "23,695.45",
                    "<p style ='color:red'>+</p>",
                    "150.48",
                    "0.64",
                    ""
                ],
                [
                    "發行量加權股價指數",
                    "20,857.71",
                    "<p style ='color:red'>+</p>",
                    "148.87",
                    "0.72",
                    ""
                ],
                [
                    "臺灣公司治理100指數",
                    "12,043.95",
                    "<p style ='color:red'>+</p>",
                    "122.85",
                    "1.03",
                    ""
                ],
                ... (略) ...
            ]
        },
        {
            "title": "113年05月13日 每日收盤行情(全部(不含權證、牛熊證))",
            "fields": [
                "證券代號",
                "證券名稱",
                "成交股數",
                "成交筆數",
                "成交金額",
                "開盤價",
                "最高價",
                "最低價",
                "收盤價",
                "漲跌(+/-)",
                "漲跌價差",
                "最後揭示買價",
                "最後揭示買量",
                "最後揭示賣價",
                "最後揭示賣量",
                "本益比"
            ],
            "data": [
                [
                    "0050",
                    "元大台灣50",
                    "9,642,900",
                    "12,460",
                    "1,576,521,176",
                    "163.35",
                    "163.90",
                    "162.95",
                    "163.30",
                    "<p style= color:red>+</p>",
                    "1.65",
                    "163.25",
                    "58",
                    "163.30",
                    "14",
                    "0.00"
                ],
                ... (略) ...


API 網址可以用 f 字串來表示 :

url=f'https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date={data}&type=ALLBUT0999&response=json'

只要傳入日期 date (格式 YYYYMMDD) 即可取得指定日期之盤後資訊. 


二. 擷取目標網頁內容 :  

找到資料來源 URL 後就可以用 requests 套件來抓資料, 先匯入要用到的模組 : 

>>> import requests   
>>> from fake_useragent import UserAgent    
>>> import csv  
>>> import json   

建立偽裝用的 User Agent 標頭字典 :

>>> ua=UserAgent()  
>>> headers={'User-Agent': ua.random}    

關於 fake_useragent 套件用法參考 :


將網址與標頭傳入 requests.get() :

>>> url='https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20240513&type=ALLBUT0999&response=json&_=1715650102878'    
>>> res=requests.get(url, headers=headers)   

將回應內文 res.text 傳給 json.loads() 把 JSON 字串轉為字典 :

>>> data=json.loads(res.text)    
>>> type(data)   
<class 'dict'>   

呼叫 keys() 查詢字典有哪些鍵 : 

>>> data.keys()  
dict_keys(['tables', 'params', 'stat', 'date'])

對照上面瀏覽器顯示的 JSON 資料, 可知表格內容放在 tables 屬性中, 查詢共有 10 個 table :

>>> len(data['tables'])  
10

不過最後一個為空表格, 只有前 9 個有資料 : 

>>> data['tables'][0]['title']   
'113年05月13日 價格指數(臺灣證券交易所)'
>>> data['tables'][1]['title']   
'價格指數(跨市場)'
>>> data['tables'][2]['title']   
'價格指數(臺灣指數公司)'
>>> data['tables'][3]['title']       
'報酬指數(臺灣證券交易所)'
>>> data['tables'][4]['title']    
'報酬指數(跨市場)'
>>> data['tables'][6]['title']     
'113年05月13日 大盤統計資訊'
>>> data['tables'][7]['title']     
'漲跌證券數合計'
>>> data['tables'][8]['title']     
'113年05月13日 每日收盤行情(全部(不含權證、牛熊證))'
>>> data['tables'][9]     
{}

可見目標資料放在第 9 個表格 ( tables 屬性值為串列, 目標資料放在其索引 8 的一個字典的 data 屬性值裡) :

        {
            "title": "113年05月13日 每日收盤行情(全部(不含權證、牛熊證))",
            "fields": [
                "證券代號",
                "證券名稱",
                "成交股數",
                "成交筆數",
                "成交金額",
                "開盤價",
                "最高價",
                "最低價",
                "收盤價",
                "漲跌(+/-)",
                "漲跌價差",
                "最後揭示買價",
                "最後揭示買量",
                "最後揭示賣價",
                "最後揭示賣量",
                "本益比"
            ],
            "data": [
                [
                    "0050",
                    "元大台灣50",
                    "9,642,900",
                    "12,460",
                    "1,576,521,176",
                    "163.35",
                    "163.90",
                    "162.95",
                    "163.30",
                    "<p style= color:red>+</p>",
                    "1.65",
                    "163.25",
                    "58",
                    "163.30",
                    "14",
                    "0.00"
                ],
                [
                    "0051",
                    "元大中型100",


因此用 data['tables'][8]['data'] 便能取得全部上市股票盤後資訊, 先用 len() 看看有幾筆 : 

>>> len(data['tables'][8]['data'])    
1236

可見共有 1236 支股票, 檢視其中幾筆 : 

>>> data['tables'][8]['data'][0]    
['0050', '元大台灣50', '9,642,900', '12,460', '1,576,521,176', '163.35', '163.90', '162.95', '163.30', '<p style= color:red>+</p>', '1.65', '163.25', '58', '163.30', '14', '0.00']
>>> data['tables'][8]['data'][1]   
['0051', '元大中型100', '53,991', '230', '4,253,963', '79.00', '79.00', '78.55', '78.95', '<p style= color:red>+</p>', '0.20', '78.80', '1', '79.00', '6', '0.00'] 
>>> data['tables'][8]['data'][2]   
['0052', '富邦科技', '476,629', '683', '75,336,734', '158.00', '158.75', '157.50', '157.90', '<p style= color:red>+</p>', '1.20', '157.90', '17', '158.00', '5', '0.00']

前面都是 ETF 基金, 到索引 191 才是個股 : 

>>> data['tables'][8]['data'][191]    
['1101', '台泥', '30,344,896', '14,455', '1,021,662,057', '33.60', '33.85', '33.45', '33.70', '<p style= color:red>+</p>', '0.40', '33.70', '158', '33.75', '183', '32.72']
>>> data['tables'][8]['data'][200]    
['1203', '味王', '1,071', '77', '50,950', '--', '--', '--', '--', '<p> </p>', '0.00', '48.95', '1', '49.55', '1', '23.48']

可以用迴圈走訪 data['tables'][8]['data'] 串列來取得全部上市證券盤後資料並使用 csv 套件將其存入 .csv 檔或存入資料庫. 但在此之前須先進行資料清洗, 處理表格各欄位之資料型態 : 


1. 去除漲跌價差欄位 :

第 9 欄位的漲跌價差是一個 HTML 字串, 此欄位可透過前後日收盤價計算而得故可移除 :

>>> data['tables'][8]['data'][191][9]    
'<p style= color:red>+</p>'

這可以透過串列切片來處理.


2. 添加日期欄位 :

如果要將每日擷取到的盤後資料存入資料庫, 則必須添加一個日期欄位來區別, 我們可以從 data['title'] 屬性值中取得日期 :

"title": "113年05月13日 每日收盤行情(全部(不含權證、牛熊證))"

注意, 日期資訊是民國年月日, 與後面的 "每日收盤行情... " 之間有個空格, 先用 split() 將日期資訊萃取出來 : 

>>> date_str=data['tables'][8]['title'].split(' ')[0]      
>>> date_str     
'113年05月13日'

在利用正規表示法把年月日都替換成 '-' :

>>> import re   
>>> date_str=re.sub(r"\D", "-", date_str)     
>>> date_str      
'113-05-13-'   

用字串切片去除結尾的 '-' 字元 :

>>> date_str=date_str[:-1]   
>>> date_str    
'113-05-13'  

接下來將日期字串用 split() 拆開後把民國年加上 1911 再組回去即可 :

>>> y, m, d=date_str.split('-')   
>>> date_str=str(int(y) + 1911) + '-' + m + '-' + d   
>>> date_str    
'2024-05-13'


3. 將數字字串轉成數值型態 (int/float) :

由上可知, data['tables'][8]['data'] 串列中各欄位之值都是字串, 為了爾後進行計算, 除了第二欄位 "證券名稱" 外都要轉為數值, 浮點數可傳給 float() 轉換, 整數可傳給 int() 轉換. 

例如大立光的價格資訊中有小數部分, 所以需要用 float() 轉成浮點數, 但整數部分有逗點必須先去除 : 

>>> data['tables'][8]['data'][725]    
['3008', '大立光', '426,653', '1,576', '951,013,700', '2,235.00', '2,250.00', '2,215.00', '2,240.00', '<p style= color:red>+</p>', '10.00', '2,235.00', '1', '2,240.00', '6', '14.43']   

不過在進行數字字串轉數值時會遇到一個問題, 若股票當日成交股數 (第三欄位) 為 0, 則欄位 6, 7, 8, 9 的開高低收欄位會是 '--' 而非數值字串 :  

>>> data['tables'][8]['data'][165]  
['01002T', '土銀國泰R1', '0', '0', '0', '--', '--', '--', '--', '<p> </p>', '0.00', '17.15', '1', '17.19', '1', '0.00']   

因此對於成交股數為 0 的股票就跳過去不予處理

這樣就可以走訪 data['tables'][8]['data'] 串列之元素來進行轉換了, 以下為了縮短指涉長度, 用一個新變數 stocks 來指向二維串列 data['tables'][8]['data'] :

>>> stocks=data['tables'][8]['data']     
>>> stocks[0]    
['0050', '元大台灣50', '9,642,900', '12,460', '1,576,521,176', '163.35', '163.90', '162.95', '163.30', '<p style= color:red>+</p>', '1.65', '163.25', '58', '163.30', '14', '0.00']

原始欄位共 16 個, 如果刪除第 9 欄位 (索引 8) "漲跌(+/-)", 然後在最前面插入日期欄位, 則一刪一增後仍然是 16 欄, 結果會變成如下 : 

>>> del stocks[0][9]  
>>> stocks[0].insert(0, date_str)   
>>> stocks[0]    
['2024-05-13', '0050', '元大台灣50', '9,642,900', '12,460', '1,576,521,176', '163.35', '163.90', '162.95', '163.30', '1.65', '163.25', '58', '163.30', '14', '0.00']

為了能在做欄位型態轉換時能知道此欄應該轉成甚麼型態, 定義一個變數 col_type 來做對映一刪一增後的欄位型態分布 : 

>>> col_types=['date', 'text', 'text', 'int', 'int', 'int', 'float', 'float', 'float', 'float', 'float', 'float', 'int', 'float', 'int', 'float']  
>>> col_types   
['date', 'text', 'text', 'int', 'int', 'int', 'float', 'float', 'float', 'float', 'float', 'float', 'int', 'float', 'int', 'float']

然後定義一個函式來處理欄位型態轉換 : 
 
>>> def col_trans(col, col_type):     
    if col_type in ['int', 'float']:         # 欄位型態是 int/float 才轉換
        col=col.strip().replace(',', '')    # 先將左右空格與千位逗號去除
        try :                                         # int() 與 float() 轉型失敗會丟出例外
            if col_type == 'int':             # 整數用 int() 轉換
                return int(col)
            else:
                return float(col)              # 浮點數用 float() 轉換
        except:
            return -1                              # 例外傳回 -1
    else:
        return col                                 # 非 int, float 不轉換傳回本身

接下來以 stocks[0] 台灣 50 為例測試此函式是否能正確進行資料清洗, 只要以串列生成式用一個指令就能完成 : 

[col_trans(col, col_type[idx]) for idx, col in enumerate(stocks[0])]  

此處我們利用 enumerate() 函式來取得 stocks[0] 串列的元素 col 與其索引 idx, 因為要從 col_types 串列中取得各欄位所對映之型態必須用到索引 : 

>>> stocks[0]   
['2024-05-13', '0051', '元大中型100', '53,991', '230', '4,253,963', '79.00', '79.00', '78.55', '78.95', '0.20', '78.80', '1', '79.00', '6', '0.00']
>>> [col_trans(col, col_type[idx]) for idx, col in enumerate(stocks[0])]   
['2024-05-13', '0051', '元大中型100', 53991, 230, 4253963, 79.0, 79.0, 78.55, 78.95, 0.2, 78.8, 1, 79.0, 6, 0.0]

結果顯示所有 int/float 型態的欄位都被正確地轉換了. 

我們可以將每支股票清洗後的串列依序存入一個空串列中, 結果就是一個二維串列, 然後用 csv 套件將此二維串列寫入 .csv 檔案, 完整程式碼如下 : 

import requests   
from fake_useragent import UserAgent    
import csv  
import json
import re

def col_trans(col, col_type):   # 資料清洗與轉換
    if col_type in ['int', 'float']:  # 只轉換 int/float 類型資料
        col=col.replace(',', '')       # 去除整數部分的千位逗號
        try :
            if col_type == 'int':
                return int(col)      # 轉成 int
            else:
                return float(col)   # 轉成 float
        except:
            return -1
    else:
        return col    # 非 int/float 類型不處理直接傳回

ua=UserAgent()  
headers={'User-Agent': ua.random}    
url='https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20240513&type=ALLBUT0999&response=json&_=1715650102878'    
res=requests.get(url, headers=headers)
data=json.loads(res.text)
if 'tables' in data:  # 非交易日 data 字典中無 tables 屬性
    date_str=data['tables'][8]['title'].split(' ')[0]
    date_str=re.sub(r"\D", "-", date_str)
    date_str=date_str[:-1]
    y, m, d=date_str.split('-')
    date_str=str(int(y) + 1911) + '-' + m + '-' + d    # 民國轉西元
    stocks=data['tables'][8]['data']    # 盤後資訊在第 9 張表
    col_types=['date', 'text', 'text', 'int', 'int', 'int',
               'float', 'float', 'float', 'float', 'float', 'float', 
               'int', 'float', 'int', 'float']    # 欄位型態對映
    results=[]   # 儲存個股清洗後的結果
    for stock in stocks:   # 走訪每支股票
        if stock[2] != '0':
            del stock[9]      # 刪除第 10 欄 '漲跌+-'
            stock.insert(0, date_str)    # 交易日期插在最前面 (第 1 欄)
            results.append([col_trans(col, col_types[idx])
             for idx, col in enumerate(stock)])   # 資料清洗與轉換
    with open('twse_after_trading.csv', 'w', newline='') as f:    # 結果寫入 csv 檔
        writer=csv.writer(f)  
        writer.writerows(results)
        print('寫入檔案完成')
else:
    print('非交易日無盤後資料')
      
注意, 由於非交易日無盤後交易資料, 證交所網頁會傳回 JSON 字串 {'stat': '很抱歉,沒有符合條件的資料!'}, 故 data 字典中會找不到 tables 屬性, 故程式中使用 if 'tables' in data 來檢查是否維交易日, 是的話才進行截取與清理作業. 最後呼叫 csv 寫入物件 Writer 的 writerows() 方法將二維串列寫入檔案, 參考 :


結果如下 : 

2024-05-13,0050,元大台灣50,9642900,12460,1576521176,163.35,163.9,162.95,163.3,1.65,163.25,58,163.3,14,0.0
2024-05-13,0051,元大中型100,53991,230,4253963,79.0,79.0,78.55,78.95,0.2,78.8,1,79.0,6,0.0
2024-05-13,0052,富邦科技,476629,683,75336734,158.0,158.75,157.5,157.9,1.2,157.9,17,158.0,5,0.0
2024-05-13,0053,元大電子,18050,104,1586656,87.85,88.2,87.75,88.0,1.2,87.9,1,88.15,14,0.0
2024-05-13,0055,元大MSCI金融,512466,448,13619756,26.65,26.68,26.5,26.53,0.09,26.52,11,26.53,18,0.0
2024-05-13,0056,元大高股息,16994138,12450,663441474,39.07,39.15,38.93,39.04,0.0,39.03,8,39.04,105,0.0
2024-05-13,0057,富邦摩台,12177,14,1472245,120.9,121.0,120.75,120.75,1.65,120.85,25,121.15,15,0.0
... (略) ...
2024-05-13,9940,信義,321271,748,10599015,33.15,33.15,32.85,32.95,0.2,32.95,1,33.0,17,13.79
2024-05-13,9941,裕融,904683,1580,130613753,144.5,145.5,143.0,145.5,0.5,145.0,1,145.5,59,16.29
2024-05-13,9941A,裕融甲特,18867,23,961069,50.9,51.0,50.9,51.0,0.1,50.9,3,51.1,4,0.0
2024-05-13,9942,茂順,81343,171,10499679,130.0,130.0,128.5,129.5,0.5,129.0,4,129.5,2,10.99
2024-05-13,9943,好樂迪,488748,487,44056473,91.0,91.0,89.0,90.7,0.3,90.6,16,90.7,9,15.37
2024-05-13,9944,新麗,1017075,538,21658756,21.15,22.0,20.85,21.55,0.4,21.5,8,21.55,5,215.5
2024-05-13,9945,潤泰新,45390604,23639,1940083275,40.95,43.95,40.9,43.9,3.4,43.9,53,43.95,357,16.08
2024-05-13,9946,三發地產,2558212,1500,95295621,36.7,38.2,35.95,38.0,1.1,37.9,1,38.0,131,100.0
2024-05-13,9955,佳龍,1380053,1160,41882995,31.5,31.65,29.75,29.75,0.65,29.75,28,29.8,14,0.0
2024-05-13,9958,世紀鋼,12639860,9709,2989728306,230.0,241.0,229.5,236.0,11.5,235.5,61,236.0,15,53.15




可見已正確擷取盤後行情資訊.  


三. 寫成爬蟲函式 :

上面的程式中目標網頁 URL 是固定的, 要擷取特定日期時必須去修改 URL 字串, 我們可以將其改寫為函式, 只要傳入日期字串 (格式為 'YYmmdd') 就傳回一個二為串列, 所以需要匯入 datetime 套件來製作日期字串, 呼叫 datetime.datetime.today() 或 datetime.datetime.now() 函式都會傳回一個 datetime.datetime 物件, 然後呼叫其 strftime() 方法並傳入 '%Y%m%d' 格式字串即可 : 

>>> from datetime import datetime 
>>> datetime.today().strftime('%Y%m%d')    
'20240517'
>>> datetime.now().strftime('%Y%m%d')    
'20240517'

完整的程式碼如下 : 

# craw_twse_after_trading.py
import requests   
from fake_useragent import UserAgent    
import csv  
import json
import re
from datetime import datetime

def col_trans(col, col_type):
    if col_type in ['int', 'float']:
        col=col.replace(',', '')
        try :
            if col_type == 'int':
                return int(col)
            else:
                return float(col)
        except:
            return -1
    else:
        return col

def get_twse_after_trade(the_date):
    ua=UserAgent()  
    headers={'User-Agent': ua.random}    
    url='https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=' +\
        the_date + '&type=ALLBUT0999&response=json&_=1715650102878'    
    res=requests.get(url, headers=headers)
    data=json.loads(res.text)
    if 'tables' in data:
        date_str=data['tables'][8]['title'].split(' ')[0]
        date_str=re.sub(r"\D", "-", date_str)
        date_str=date_str[:-1]
        y, m, d=date_str.split('-')
        date_str=str(int(y) + 1911) + '-' + m + '-' + d
        stocks=data['tables'][8]['data']
        col_types=['date', 'text', 'text', 'int', 'int', 'int',
                   'float', 'float', 'float', 'float', 'float', 'float', 
                   'int', 'float', 'int', 'float']
        results=[]
        for stock in stocks:
            if stock[2] != '0':
                del stock[9]
                stock.insert(0, date_str)
                results.append([col_trans(col, col_types[idx])
                 for idx, col in enumerate(stock)])            
        return results
    else:
        return None

if __name__ == '__main__':
    the_date=datetime.now().strftime('%Y%m%d')
    results=get_twse_after_trade(the_date)
    with open('twse_after_trading.csv', 'w', newline='') as f:   
        writer=csv.writer(f)  
        writer.writerows(results)
        
只要更改 the_date 字串就可以擷取指定日期之盤後資料了. 


四. 將結果存入 SQLite 資料庫 :

在上面的測試中我們將爬蟲的擷取結果存入 csv 檔, 如果存入資料庫會讓存取更方便, 例如用 Flask/Django 建構一個網站來瀏覽或查詢盤後資料時. 此處使用小而美的單檔資料庫 SQLite, 關於 SQLite 用法參考 :


首先用 SQL Manager for SQLite 軟體建立一個資料庫 tw_stocks, 然後建立一個包含下列 16 個欄位的資料表 twse_after_trade : 
  1. date : 交易日, DATETEXT(10)
  2. stock_id : 證券代號, TEXT(10)
  3. stock_name : 證券名稱, TEXT(20)
  4. volumn : 成交股數 (成交量), INTEGER
  5. transactions : 成交筆數, INTEGER
  6. turnover : 成交金額, INTEGER
  7. open : 開盤價, REAL
  8. high : 最高價, REAL
  9. low : 最低價, REAL
  10. close : 收盤價, REAL
  11. spread : 漲跌價差, REAL
  12. last_best_bid_price : 最後揭示買價, REAL
  13. last_best_bid_volumn : 最後揭示買量, INTEGER
  14. last_best_ask_price : 最後揭示賣價, REAL
  15. last_best_ask_volumn : 最後揭示賣量, INTEGER
  16. PER : 本益比, REAL



關於 SQL Manager for SQLite 軟體用法參考 :


也可以先用 SQL Manager for SQLite 建立空白資料庫 tw_stocks 後, 用 SQL 指令建立 twse_after_trade 資料表 :

CREATE TABLE [twse_after_trade](
  [date] DATETEXT(10) NOT NULL, 
  [stock_id] TEXT(10) NOT NULL, 
  [stock_name] TEXT(20) NOT NULL, 
  [volumn] INTEGER, 
  [transactions] INTEGER, 
  [turnover] INTEGER, 
  [open] REAL, 
  [high] REAL, 
  [low] REAL, 
  [close] REAL, 
  [spread] REAL, 
  [last_best_bid_price] REAL, 
  [last_best_bid_volumn] INTEGER, 
  [last_best_ask_price] REAL, 
  [last_best_ask_volumn] INTEGER, 
  [PER] REAL)

建立 twse_after_trade 資料表後先用第一筆資料來測試看看資料表能否正常寫入, 先匯入 sqlite3 套件並建立資料庫連線 : 

>>> import sqlite3   
>>> conn=sqlite3.connect("tw_stocks.sqlite")   

製作寫入資料表的 SQL 指令後呼叫 execute() 執行, 接著呼叫 commit() 回存資料庫 :

>>> SQL="INSERT INTO twse_after_trade(date, stock_id, stock_name, " +\
    "volumn, transactions, turnover, open, high, low, " +\
    "close, spread, last_best_bid_price, last_best_bid_volumn, " +\
    "last_best_ask_price, last_best_ask_volumn, PER) " +\
    "VALUES('2024-05-17', '0050', '元大台灣50', 4652000, 2229, " +\ 
    "777580350, 167.55, 167.7, 166.7, 167.25, 0.2, 167.2, -1, " +\
    "167.25, -1, 0.0)" 
>>> conn.execute(SQL)   
<sqlite3.Cursor object at 0x0000016933A33B40>
>>> conn.commit()  

這時將 SQL Manager for SQLite 切到第二個頁籤 Browse Data 就可看到寫入之資料 :




用 SELECT 指令確認可將資料讀出來 : 

>>> SQL="SELECT * FROM twse_after_trade"   
>>> cursor=conn.execute(SQL)   
>>> print(cursor.fetchone())   
('2024-05-17', '0050', '元大台灣50', 4652000, 2229, 777580350, 167.55, 167.7, 166.7, 167.25, 0.2, 167.2, -1, 167.25, -1, 0.0)  

這樣就可以將上面的程式修改為儲存到 SQLite 版了, 完整程式碼如下 :

# craw_twse_after_trading_sqlite.py
import requests   
from fake_useragent import UserAgent    
import csv  
import json
import re
from datetime import datetime
import sqlite3

def col_trans(col, col_type):
    if col_type in ['int', 'float']:
        col=col.replace(',', '')
        try :
            if col_type == 'int':
                return int(col)
            else:
                return float(col)
        except:
            return -1
    else:
        return col

def get_twse_after_trade(the_date):
    ua=UserAgent()  
    headers={'User-Agent': ua.random}    
    url='https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=' +\
        the_date + '&type=ALLBUT0999&response=json&_=1715650102878'    
    res=requests.get(url, headers=headers)
    data=json.loads(res.text)
    if 'tables' in data:
        date_str=data['tables'][8]['title'].split(' ')[0]
        date_str=re.sub(r"\D", "-", date_str)
        date_str=date_str[:-1]
        y, m, d=date_str.split('-')
        date_str=str(int(y) + 1911) + '-' + m + '-' + d
        stocks=data['tables'][8]['data']
        col_types=['date', 'text', 'text', 'int', 'int', 'int',
                   'float', 'float', 'float', 'float', 'float', 'float', 
                   'int', 'float', 'int', 'float']
        results=[]
        for stock in stocks:
            if stock[2] != '0':
                del stock[9]
                stock.insert(0, date_str)
                results.append([col_trans(col, col_types[idx])
                 for idx, col in enumerate(stock)])            
        return results
    else:
        return None

if __name__ == '__main__':
    the_date=datetime.now().strftime('%Y%m%d')
    stocks=get_twse_after_trade(the_date)
    if stocks:
        conn=sqlite3.connect("tw_stocks.sqlite")
        for stock in stocks:
            stock=[str(i) for i in stock]
            values="VALUES('" + "','".join(stock) + "')"
            SQL="INSERT INTO twse_after_trade(date, stock_id, " +\
                "stock_name, volumn, transactions, turnover, " +\
                "open, high, low, close, spread, " +\
                "last_best_bid_price, last_best_bid_volumn, " +\
                "last_best_ask_price, last_best_ask_volumn, " +\
                "PER) " + values
            conn.execute(SQL)
        conn.commit()
        conn.close()
    else:
        print('非交易日')

由於在前面清理資料時將數值字串資料轉換成 int 與 float, 一般 SQL 指令中, 數值資料不加括號, 字串資料要加括號, 而 SQLite 的 SQL 指令不管數值或字串一律加括號也可以 (它應該是照欄位類型自動轉換, 這樣好處理), 所以此處用串列生成式先將 stock 的元素都轉成字串, 然後串成 values 字串以組成 SQL 的 INSERT 指令, 結果如下 :




不過我以前用 PHP 寫的系統是各股有自己的資料表, 擷取到資料後是分別寫入各股的資料表, 而不是像這樣全部存在一個資料表. 

也可以把資料庫檔上傳到線上 SQLite 網站 SQLite Viewer 進行瀏覽與操作 :




關於 SQLite Viewer 用法參考 :


感覺 SQLite Viewer 字大介面簡潔又可輸出 CSV 檔, 就是機敏資料不好上傳.

2024年5月16日 星期四

Aliexpress 購買 P530 遙控飛機 (仿 F22)

我去年初在 Aliexpress 購買了一架固定翼遙控飛機 (G1 fighter), 價格為 1080 元, 但卻因為加購的 3 電池組在中國海關被退回, 賣家建議退訂退費改訂雙電池組的, 但後來一忙就沒再買了. 這幾天谷歌廣告跳出遙控飛機廣告, 讓我又想起這件事, 於是上 Aliexpress 逛逛, 找到下面這架覺得很不錯的飛機, 但這次我選附兩顆電池保麗龍包裝的套餐 : 





結帳以美元計價 US$ 25.13 元 (使用 DBS Eco 卡) :





產品描述 :

Features:

-Desined according to F22 fighter real plane,with cool appearance.
-2PCS 816 coreless motors combination,stronger driving power,
-EPP composite material, high flexibility, excellent recovery,and not easy to break
-Easy operation,suitable for beginners and intermediate flight game players
-Apply to indoor and outdoor flight, longer remote control distance.Flying range up to 200m
-Rudder volume size adjustble,and you can do all kinds of difficult movements accurately,such as going forward, up/down, fine tuning, spiral moxement,loops,etc.

Specification:

Mode:
P530 Version With LED
Wingspan: 450mm
Fuselage length: 320mm
Hight: 125mm
Flight time: about 20 minutes
Charging time: 40~60minutes
Frequency: 2.4GHz
Transmitter battery: 2AA Battery (not included)
Airplane battery: 3.7V 500mah Li-Po Battery
Number of drivering motor:
2*816 coreless motors combination
Control distance: 200m
Flying weight: 75g
Dimention: 320mm*450mm*125mm(W*L*H)
Suitble for: age 14+
Package size: BOX:41*9.5*35CM(L*W*H)
Foam:41*34*9CM(L*W*H)
Package weight: Foam:350g/BOX:555g

Note:Transmitter is mode2(Left hand throttle)

Package list:

1*P530 Airplane
1*2.4G Transmitter(Mode2)
1*3.7V 500mah Li-Po Battery
2*Spare propeller (White represents the right propeller, black represents the left propeller)
1*Landing gear
1*USB Charger
2*Propeller protection iron hook
1*Spare propeller switching iron plate
1*Screwdriver
1*English manual


2024-05-18 補充 :

今日追蹤遞送流程, 貨品已展開城際長途遞送 (LINE HAUL) :



在 Aliexpress 買東西一定要追蹤, 以免遞送錯誤別人完成簽收你卻沒收到, 耽誤了辦理退費 refund時機 (記得超過時限會自動結案). 

2024年5月15日 星期三

momo 購買小米戶外攝影機 CW300 記憶卡

上週在鄉下老家大門安裝了小米戶外攝影機 CW300, 但尚未插記憶卡, 此款攝影機最高支援 256GB SD 卡, 等級最好是 C10 以上, 錄影解析度預設是 2K, 256GB 大約可錄兩週, 128GB 約可錄 1 周, 64GB 大約 3 天, 參考 :


今天上 momo 買一片 256GB 卡, 另外買 32GB 卡 2 片給小米 1080P 用 :





使用 momo 幣 350 元, 實付 445 元. 

2024年5月14日 星期二

Python 學習筆記 : 網頁爬蟲實戰 (七) 台股上市櫃公司清單網頁

本篇要爬的對象是證交所的上市櫃公司列表, 這是台股上千支股票的完整清單, 只要有新股完成掛牌上市或舊股申請下市, 這張表格隨即更新. 爬這張表的目的主要是要追蹤上市櫃公司的動態變化, 如果想要掃描有潛在獲利的投資標的時, 手上必須隨時有最新的市場全貌, 所以這張表也是建立自有量化投資資料庫的根本. 



一. 檢視目標網頁 : 

證交所上市公司清單的網頁名稱為 "本國上市證券國際證券辨識號碼一覽表", 網址如下 :





而上櫃公司清單的網頁名稱則為 "本國上櫃證券國際證券辨識號碼一覽表", 網址如下 :





這兩個網頁都是靜態網頁, 因此非常容易擷取, 從外觀來看兩個網頁結構是一樣的, 但還須檢視原始碼才能確定. 我在 2017 年時曾經使用 PHP 撰寫爬蟲程式去爬這兩個網頁, 多年來網頁格式都沒變 (連編碼格式都還是 MS950, 只有內容變而已), 參考 :


不過在瀏覽器中檢視原始碼時可能會因為占用記憶體過多而無法顯示 (因為網頁內容很長), 解決辦法是先用 requests 套件抓下來後存成 .htm 檔, 然後用文字編輯軟體如記事本開啟來觀察. 雖然這兩個網頁沒有防爬機制, 但我們還是習慣在提出請求時送出攜帶模擬瀏覽器 User-Agent 的 headers 參數, 參考 :


首先匯入 requests 與 fake_useragent 套件 :

>>> import requests    
>>> from fake_useragent import UserAgent    
>>> ua=UserAgent()         # 建立 FakeUserAgent 物件 
>>> headers={'User-Agent': ua.random}    # 使用隨機 User-Agent

先來處理上市公司部分 : 

>>> url='https://isin.twse.com.tw/isin/C_public.jsp?strMode=2'    
>>> res=requests.get(url, headers=headers)      # 檔案很大下載時間約 10 秒

接下來檢視網頁編碼, 可知格式為 MS950 (即 big5) : 

>>> res.encoding   
'MS950'
>>> len(res.text)    
8974741

內容高達 9 百萬個字元, 難怪用瀏覽器檢視原始碼時都會載入失敗, 所以先將它存成 .htm 檔案再用編輯器開啟 : 

>>> with open('twse_exchange_list.htm', "w", encoding='utf-8') as f:    
    f.write(res.text)    
    print("儲存網頁檔 OK")    
    
8974741
儲存網頁檔 OK

注意, 此處我們在存檔時同時也將編碼格式改成 utf-8. 

用文字編輯器開啟網頁檔來檢視原始碼 : 




可見上市公司內容是放在一個 class='h4' 的 table 元素裡. 

接下來處理上櫃公司清單網頁 :

>>> url='https://isin.twse.com.tw/isin/C_public.jsp?strMode=4'   
>>> res=requests.get(url, headers=headers)    
>>> res.encoding   
'MS950'   
>>> len(res.text)   
2778216   
>>> with open('twse_counter_list.htm', "w", encoding='utf-8') as f:      
    f.write(res.text)      
    print("儲存網頁檔 OK")  
    
2778216
儲存網頁檔 OK

雖然上櫃公司較上市少, 但此檔案也有 277 萬字元 (約 2.9MB), 用文字編輯器開啟 : 




可見網頁原始碼結構與上市公司清單完全一樣, 公司內容同樣是放在一個 class='h4' 的表格中, 因此擷取內容的處理方式相同. 我將這兩個檔案放在 GitHub :



二. 擷取目標網頁內容 : 

由於這兩個網頁都是靜態網頁, 所以只要用 BeautifulSoup 就可以將內容抓出來放到資料庫, 此處為了簡單計, 將它們以 csv 檔存檔, 以後要進一步放進資料庫也方便. 先以上市公司為例擷取, 之後將流程寫成同時處理上市與上櫃的函式 : 

匯入套件模組 : 

>>> import requests    
>>> from fake_useragent import UserAgent 
>>> from bs4 import BeautifulSoup   

以上市公司為例擷取網頁內容 : 
   
>>> ua=UserAgent()         # 建立 FakeUserAgent 物件 
>>> headers={'User-Agent': ua.random}     
>>> url='https://isin.twse.com.tw/isin/C_public.jsp?strMode=2'     # 上市公司清單
>>> res=requests.get(url, headers=headers)  
>>> soup=BeautifulSoup(res.text, 'lxml')   

呼叫 find() 方法找尋 class 屬性值為 'h4' 的 table 元素 :

>>> table_tag=soup.find('table', class_='h4')   # 傳回 Tag 物件

呼叫 find_all() 尋找 table 底下所有的 tr 元素 (列), 傳回 Tag 物件串列 :

>>> tr_tags=table_tag.find_all('tr')    # 在 table 元素內尋找所有 tr 元素
>>> len(tr_tags)    # 列數
39450

可見此表格有 39450 列, 檢視前 3 列, 第一列為欄位名稱 : 

>>> tr_tags[0]   
<tr align="center"><td bgcolor="#D5FFD5">有價證券代號及名稱 </td><td bgcolor="#D5FFD5">國際證券辨識號碼(ISIN Code)</td><td bgcolor="#D5FFD5">上市日</td><td bgcolor="#D5FFD5">市場別</td><td bgcolor="#D5FFD5">產業別</td><td bgcolor="#D5FFD5">CFICode</td><td bgcolor="#D5FFD5">備註</td></tr>

第二列只有單欄的 "股票" :

>>> tr_tags[1]  
<tr><td bgcolor="#FAFAD2" colspan="7"><b> 股票 <b> </b></b></td></tr>

第三列開始才是公司資料 :

>>> tr_tags[2]   
<tr><td bgcolor="#FAFAD2">1101 台泥</td><td bgcolor="#FAFAD2">TW0001101004</td><td bgcolor="#FAFAD2">1962/02/09</td><td bgcolor="#FAFAD2">上市</td><td bgcolor="#FAFAD2">水泥工業</td><td bgcolor="#FAFAD2">ESVUFR</td><td bgcolor="#FAFAD2"></td></tr>

在這種公司資料列中搜尋 td 元素, 會傳回內部所有 td 元素的 Tag 物件串列 :

>>> td_tags=tr_tags[2].find_all('td')    
>>> td_tags
[<td bgcolor="#FAFAD2">1101 台泥</td>, <td bgcolor="#FAFAD2">TW0001101004</td>, <td bgcolor="#FAFAD2">1962/02/09</td>, <td bgcolor="#FAFAD2">上市</td>, <td bgcolor="#FAFAD2">水泥工業</td>, <td bgcolor="#FAFAD2">ESVUFR</td>, <td bgcolor="#FAFAD2"></td>]

正常會有 7 個欄位 : 

>>> len(td_tags)   
7

檢視這 7 個欄位 :

>>> td_tags[0].text     # 有價證券代號及名稱
'1101\u3000台泥'
>>> td_tags[1].text     # 國際證券辨識號碼(ISIN Code)
'TW0001101004'
>>> td_tags[2].text     # 上市日
'1962/02/09'
>>> td_tags[3].text     # 市場別
'上市'
>>> td_tags[4].text     # 產業別
'水泥工業'
>>> td_tags[5].text     # CFICode
'ESVUFR'
>>> td_tags[6].text     # 備註

因為表格是二維資料, 先定義一個空的二維串列來儲存上市股票清單 :

>>> data=[]  

然後在 table 元素內搜尋所有的 tr 元素 :

>>> tr_tags=table_tag.find_all('tr')  

接著就可以用兩層迴圈來取得表格儲存格裡的資料, 第一層掃描列, 第二層掃描欄 : 

>>> for tr_tag in tr_tags:            # 掃描列 (row)
    td_tags=tr_tag.find_all('td')   # 取得該列所有 td 元素之 Tag 物件串列
    if len(td_tags) == 7:                 # 剔除欄位數不是 7 的列
        data.append([td_tag.text for td_tag in td_tags])   # 將 td 的內文串列存入 data
    
>>> len(data)    
39441

上面我們在內層迴圈中只抓欄位數為 7 的列放入 data 串列, 因此過濾後只剩下 39441 列, 比上面原始表格的 39450 列少了 9 列. 檢視 data 串列前幾個元素 :

>>> data[0]     # 欄位標題
['有價證券代號及名稱 ', '國際證券辨識號碼(ISIN Code)', '上市日', '市場別', '產業別', 'CFICode', '備註']

>>> data[0]   
['有價證券代號及名稱 ', '國際證券辨識號碼(ISIN Code)', '上市日', '市場別', '產業別', 'CFICode', '備註']
>>> data[1]   
['1101\u3000台泥', 'TW0001101004', '1962/02/09', '上市', '水泥工業', 'ESVUFR', '']
>>> data[2]  
['1102\u3000亞泥', 'TW0001102002', '1962/06/08', '上市', '水泥工業', 'ESVUFR', '']
>>> data[3]   
['1103\u3000嘉泥', 'TW0001103000', '1969/11/14', '上市', '水泥工業', 'ESVUFR', '']

可見表格中的上市公司清單都已存入 data 串列中了, 但是第一欄的公司代號與名稱之間有一個 '\u3000' 字元, 這是空格的 unicode 碼, 我們希望新增一欄來放公司名稱, 第一欄只放公司代號, 以後轉入資料庫後在搜尋上較好處理, 方法是在用一個迴圈來掃描列, 然後將第一欄位以 '\u3000' 為界進行拆分, 前者仍放回第一欄位, 後者插入到新增的第二欄位 :

>>> for row in data[1:]:                             # 從第 2 列開始 (跳過欄位名稱列)
    stock_name=row[0].split('\u3000')[1]   # 先將拆出的證券名稱暫存
    row[0]=row[0].split('\u3000')[0]            # 將拆出的證券名稱覆蓋第一欄
    row.insert(1, stock_name)                      # 在第 2 欄位置插入拆出之證券名稱

檢視改變後的結果 :

>>> data[1]  
['1101', '台泥', 'TW0001101004', '1962/02/09', '上市', '水泥工業', 'ESVUFR', '']
>>> data[2]   
['1102', '亞泥', 'TW0001102002', '1962/06/08', '上市', '水泥工業', 'ESVUFR', '']
>>> data[3]   
['1103', '嘉泥', 'TW0001103000', '1969/11/14', '上市', '水泥工業', 'ESVUFR', '']
>>> len(data[3])      # 從 7 欄變 8 欄了
8

可見證券代號與名稱已經順利拆成兩欄了. 

另外第一列的欄位標題的第一欄也要拆開 (以利於輸出到 csv 檔) :

>>> data[0][0]='有價證券代號'              # 第一欄為證券代號
>>> data[0].insert(1, '有價證券名稱')    # 插入第二欄為證券名稱
>>> data[0]    
['有價證券代號', '有價證券名稱', '國際證券辨識號碼(ISIN Code)', '上市日', '市場別', '產業別', 'CFICode', '備註']   
>>> len(data[0])     # 從 7 欄變 8 欄了
8

可以使用 csv 套件將 data 串列寫到 .csv 檔案中 :

>>> with open('twse_exchange_list.csv', 'w', newline='') as f:  
    writer=csv.writer(f)     # 建立 writer 物件
    for row in data:            # 將資料逐列寫入檔案
        chars=writer.writerow(row)   # 將列寫入檔案傳回寫入字元數

注意, 呼叫 open() 時需傳入 newline='', 否則用 Excel 開啟時每列之間會多出一個空白列. 也可以用 writerows() 一次將整個二維串列 data 寫入 .csv 檔 :

>>> with open('twse_exchange_list.csv', 'w', newline='') as f:   
    writer=csv.writer(f)  
    writer.writerows(data)  

開啟輸出檔 twse_exchange_list.csv 結果如下 :

有價證券代號,有價證券名稱,國際證券辨識號碼(ISIN Code),上市日,市場別,產業別,CFICode,備註
1101,台泥,TW0001101004,1962/02/09,上市,水泥工業,ESVUFR,
1102,亞泥,TW0001102002,1962/06/08,上市,水泥工業,ESVUFR,
1103,嘉泥,TW0001103000,1969/11/14,上市,水泥工業,ESVUFR,
1104,環泥,TW0001104008,1971/02/01,上市,水泥工業,ESVUFR,
1108,幸福,TW0001108009,1990/06/06,上市,水泥工業,ESVUFR,
1109,信大,TW0001109007,1991/12/05,上市,水泥工業,ESVUFR,
1110,東泥,TW0001110005,1994/10/22,上市,水泥工業,ESVUFR,
1201,味全,TW0001201002,1962/02/09,上市,食品工業,ESVUFR,
... (略) ...
01114S,111中租賃B,TW00001114S1,2022/04/15,上市,,DAFUFR,
01001T,土銀富邦R1,TW00001001T8,2005/03/10,上市,,CBCIXU,
01002T,土銀國泰R1,TW00001002T6,2005/10/03,上市,,CBCIXU,
01004T,土銀富邦R2,TW00001004T2,2006/04/13,上市,,CBCIXU,
01007T,兆豐國泰R2,TW00001007T5,2006/10/13,上市,,CBCIXU,
01009T,王道圓滿R1,TW00001009T1,2018/06/21,上市,,CBCIXU,
01010T,京城樂富R1,TW00001010T9,2018/12/05,上市,,CBCIXU,




以上完成了上市公司清單擷取, 由於上櫃公司清單網頁與上市一樣, 只是網址不同, 因此只要更改 url 即可, 演算法都是一樣的 : 

>>> url='https://isin.twse.com.tw/isin/C_public.jsp?strMode=4'     # 上櫃公司清單
>>> res=requests.get(url, headers=headers)  
>>> soup=BeautifulSoup(res.text, 'lxml')   
>>> table_tag=soup.find('table', class_='h4')   # 傳回 Tag 物件
>>> data=[]  
>>> tr_tags=table_tag.find_all('tr') 
>>> for tr_tag in tr_tags:            # 掃描列 (row)
    td_tags=tr_tag.find_all('td')   # 取得該列所有 td 元素之 Tag 物件串列
    if len(td_tags) == 7:                 # 剔除欄位數不是 7 的列
        data.append([td_tag.text for td_tag in td_tags])   # 將 td 的內文串列存入 data
>>> for row in data[1:]:                             # 從第 2 列開始 (跳過欄位名稱列)
    stock_name=row[0].split('\u3000')[1]   # 先將拆出的證券名稱暫存
    row[0]=row[0].split('\u3000')[0]            # 將拆出的證券名稱覆蓋第一欄
    row.insert(1, stock_name)                      # 在第 2 欄位置插入拆出之證券名稱
>>> with open('twse_counter_list.csv', 'w', newline='') as f:      
    writer=csv.writer(f)    
    writer.writerows(data)  

這樣便完成上櫃公司清單的擷取了, 開啟 twse_counter_list.csv 檢視內容 :

有價證券代號及名稱 ,國際證券辨識號碼(ISIN Code),上市日,市場別,產業別,CFICode,備註
70000U,譜瑞群益39售07,TW24Z70000U2,2024/03/12,上櫃,,RWSCPE,
70001U,宣德國票3A售01,TW24Z70001U0,2024/03/12,上櫃,,RWSCPE,
70002U,碩禾國票3A售01,TW24Z70002U8,2024/03/12,上櫃,,RWSCPE,
70003U,福華凱基3B售01,TW24Z70003U6,2024/03/13,上櫃,,RWSCPE,
70004U,鈊象元富39售07,TW24Z70004U4,2024/03/13,上櫃,,RWSCPE,
70005U,新普統一3C售02,TW24Z70005U1,2024/03/13,上櫃,,RWSCPE,
70006U,環球晶群益39售02,TW24Z70006U9,2024/03/13,上櫃,,RWSCPE,
70007U,宏捷科國票3A售03,TW24Z70007U7,2024/03/13,上櫃,,RWSCPE,
70008U,智通國票3A售02,TW24Z70008U5,2024/03/13,上櫃,,RWSCPE,
70009U,鈊象國票3A售01,TW24Z70009U3,2024/03/13,上櫃,,RWSCPE,
... (略) ...
9962,有益,TW0009962001,2006/07/10,上櫃,鋼鐵工業,ESVUFR,
8349A,恒耀甲特,TW0008349A03,2020/02/24,上櫃,,EPNRAR,
01014S,93中信貸a,TW00001014S3,2004/08/10,上櫃,,DAVUBR,
01015S,93中信貸b,TW00001015S0,2004/08/10,上櫃,,DAVUBR,
01016S,93中信貸c,TW00001016S8,2004/08/10,上櫃,,DAVUBR,
01017S,93中信貸d,TW00001017S6,2004/08/10,上櫃,,DAVUBR,
01111S,081中租賃A,TW00001111S7,2019/12/11,上櫃,,DAFUFR,
01112S,081中租賃B,TW00001112S5,2019/12/11,上櫃,,DAFUFR,




四. 寫成爬蟲程式 : 

將上面的測試程序寫成如下之爬蟲程式 get_twse_stocks_list.py :

# get_twse_stocks_list.py
import requests    
from fake_useragent import UserAgent 
from bs4 import BeautifulSoup
import csv

ua=UserAgent()          
headers={'User-Agent': ua.random}
markets={'exchange': '2', 'counter': 4}
for market in markets:
    try:
        m=markets[market]
        url=f'https://isin.twse.com.tw/isin/C_public.jsp?strMode={m}'
        res=requests.get(url, headers=headers)
        soup=BeautifulSoup(res.text, 'lxml')
        table_tag=soup.find('table', class_='h4')
        data=[]
        tr_tags=table_tag.find_all('tr')
        for tr_tag in tr_tags:
            td_tags=tr_tag.find_all('td')
            if len(td_tags) == 7:
                data.append([td_tag.text for td_tag in td_tags])
        for row in data[1:]:
            stock_name=row[0].split('\u3000')[1]
            row[0]=row[0].split('\u3000')[0]
            row.insert(1, stock_name)
        file=f'twse_{market}_list.csv'
        with open(file, 'w', newline='') as f:
            writer=csv.writer(f)  
            writer.writerows(data)
    except Exception as e:
        print(e)

測試結果 OK, 在工作目錄下產生 twse_exchange_list.csv 與 twse_counter_list.csv 兩個檔案.