2024年6月25日 星期二

momo 購買勳風循環扇 (立扇, 電扇)

由於鄉下老家二樓的洗手間西曬夏天非常悶熱, 打算買一個電扇散熱, 我上週已從祖堂接電到門外的插座, 今日上 momo 買了一組電扇, 周末帶回去鄉下 : 

 


目前有 momo 幣 600 多元, 這次使用 200 元, 實付 799 元, 有 momo 幣回饋買東西真爽, 這次活動登記又送 100 元哩, 呵呵. 



2024年6月24日 星期一

露天購買碳化竹蓆

因為這個夏天的周末我都會在鄉下老家頂樓用行軍床露營, 但行軍床的床面布料不甚透氣, 所以上露天買了如下竹蓆用來鋪在上面 : 





全家取貨付款免運 $428 元.

2024 年第 25 周記事

時序來到年中了, 夏至的節氣讓這幾天熱爆, 周末在鄉下書房睡根本像烤爐那樣, 即使電扇猛吹還是汗如雨下, 只好將行軍床與蚊帳搬到三樓樓頂露營, 這裡清風徐徐, 以天地為棟宇, 仰頭還可以看到白雲 (上半夜) 與星斗 (下半夜) 哩 :





但是不知是否周六下午喝咖啡, 晚上又喝奶茶的關係, 平常五分鐘入眠的我居然失眠到兩點都沒睡著, 無奈只好下樓回房間睡, 這時也沒上半夜這麼悶熱了, 不知不覺入夢鄉. 下周末我還是要在頂樓露營 (如果沒下雨的話). 

這兩周岳父的酪梨在採收, 水某連兩周都回去鄉下幫忙採, 我也是吃了兩周的酪梨早餐. 為了控制血壓我最近改變飲食與烹飪法, 早餐是番茄淋亞麻仁油+水煮蛋+酪梨, 中餐照舊訂公司便當, 晚餐則改為水煮綠花椰 (秋葵, 茄子, ...) + 亞麻仁油/雞胸肉等, 或買五菜飯, 偶而才買一次正中排骨. 基本上就是一天只有中餐有半碗白米飯, 希望一年半內從 70kg 減至 55kg, 亦即目標是一個月要降 1kg 左右. 

2024年6月23日 星期日

市圖還書 4 本 (App Inventor)

本周市圖還了 4 本書 : 
借來都沒時間玩, 因有附光碟, 怕不小心遺失會很麻煩, 先還回去等要用時再借. 

TMS320F28335 開發板

今天在露天看到這塊 DSP 開發板 :


TMS32F28335 是德儀開發的 32 位元 DSP 晶片, 內建 150 MIPS 的 FPU, 提供 12 位元 16 通道 ADC, 具有突出的低功號高效能特性, 廣泛應用於家用生活電子產品, 規格參考 : 


此晶片甚至有出版商出書介紹其用法 :





雖然 IO 介面一應俱全, 但獨獨缺少網路介面. 

2024年6月22日 星期六

Python 學習筆記 : 網頁爬蟲實戰 (十三) 富時中國 A50 期貨指數

昨晚回到鄉下, 早上去晉德針灸回來後繼續來玩爬蟲 (希望能在六月底結束這場已耗時近三個月的爬蟲戰爭), 今天要爬的對象是從下面陳會安老師的這本書看到的範例 : 


此範例是要擷取新加坡交易所 (SGX) 的富時中國 A50 指數期貨報價資訊, 該指數由英國富時公司編製, 目標是追蹤中國滬深交易所市值最大的 50 家 A 股公司的市場表現. 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

新加坡交易所 (SGX) 的富時中國 A50 指數期貨報價資訊網址如下 : 





可見目標資料是以表格方式呈現, 但檢視原始碼會發現裡面一個 table 元素也沒有, 網頁應該是 Javascript 動態產生 :




這可用 Quick Javascript Switcher 來確認, 當關閉 Javascript 功能時整個表格會消失不見, 可見此網頁確實是透過 Javascript 動態產生, 此種網頁無法直接用 requests 套件來擷取 (因為網頁中沒有實體資料, 是網頁載入後才繪製的). 不過, 透過分析 HTTP 請求流程, 有機會找到含有目標資料的未公開 API, 通常是 JSON (主流) 或 XML (較少見了) 格式之資料. 

在 Chrome 按 F12 開啟開發人員工具視窗並切換到 "Network/XHR" 頁籤, 然後重新整理目標網頁, 左下方視窗繪出現瀏覽器所提出的 HTTP 請求, 逐一點選並觀察右下方視窗的 Preview 內容, 可發現其中 CN?order=asc&orderby=... 這個請求的 Preview 內容就是繪製網頁表格的資料來源 (通常是檔案大小較大那個 URL) : 




也可以切到 Response 頁籤觀察回應訊息來確認 : 




切到 Header 頁籤可知此請求使用 HTTP GET 方法 :




在 CN?order=asc&orderby=... 請求上按滑鼠右鍵點選 Copy/Copy URL 即可複製此請求之完整網址, 將其貼到瀏覽器網址列 : 


事實上最後面兩個參數 session 與 t 不給也是可以的 : 


這就是隱藏在背後未公開的 API, 與上一篇測試的景氣對策信號網頁處理方式類似, 只要從 HTTP 請求流程中找出目標資料的 API 網址, 接下來就可以用 requests 套件來擷取它了, 但要注意的是發出 HTTP 請求的方法, 務必依照真人操作瀏覽器所用的方法, 例如此例用的是 GET 方法, 而前一篇景氣對策信號網頁用的是 POST 方法, 方法用錯可能無法擷取到資料, 因為不是每個伺服器都會同時實作資源的 GET 與 POST 方法. 

由於是使用 GET 方法, 可以將上面網址直接貼到瀏覽器網址列來瀏覽此 JSON 資料 :




現在新版瀏覽器都有一個 "美化排版" 的貼心功能, 勾選它就會將 JSON 格式資料以鍵值對方式整齊排列以增進可閱讀性. 由上可知, 目標資料都放在 data 這個鍵裡面. 


二. 使用 requests 擷取目標資料 (JSON) :  

先匯入會用到的套件模組 :

>>> import requests 
>>> import json  
>>> from pprint import pprint  

用 GET 方法擷取目標資料並且用 json.loads() 將回應的 JSON 字串轉成 Python 字典 :

>>> url='https://api.sgx.com/derivatives/v1.0/contract-code/CN?order=asc&orderby=delivery-month&category=futures'    
>>> res=requests.get(url)    # 注意此例必須用 get()
>>> res.encoding    
'utf-8'
>>> data=json.loads(res.text)   

用 pprint.pprint() 來檢視 data 鍵之值 (很長略去) : 

>>> pprint(data['data'])   
[{'aggregate-total-volume': 337725.0,
  'base-date': '20240621',
  'best-ask-price': 1206500.0,
  'best-ask-price-abs': 12065.0,
  'best-ask-price-adj': 12065.0,
  'best-ask-quantity': 5.0,
  'best-bid-price': 1205100.0,
  'best-bid-price-abs': 12051.0,
  'best-bid-price-adj': 12051.0,
  'best-bid-quantity': 9.0,
  'category': 'futures',
  'change': -13100.0,
  'change-abs': 131.0,
  'change-adj': -131.0,
  'change-percentage': -1.0753570842226234,
  'contract-code': 'CN',
  'contract-name': 'SGX FTSE China A50 Index Futures',
  'current-trading-session': '0',
  'daily-settlement-price': 1205100.0,
  'daily-settlement-price-abs': 12051.0,
  'daily-settlement-price-adj': 12051.0,
  'default-dsp': '2',
  'delivery-month': '2024-06',
  'derivative-type': 'equityindex',
  'first-trading-date': '2023-06-29',
  'it': 'cnfc',
  'last-trade-price': 1205100.0,
  'last-traded-price-abs': 12051.0,
  'last-traded-price-adj': 12051.0,
  'last-trading-date': '2024-06-26',
  'last-update-time': '2024-06-22 05:07:18.0',
  'open-interest': 963882.0,
  'outright-traded-volume': 305652.0,
  'p': 'X',
  'preliminary-settlement-price': 1218200.0,
  'preliminary-settlement-price-abs': 12182.0,
  'preliminary-settlement-price-adj': 12182.0,

...(略)...

  'session-close': '-',
  'session-close-abs': None,
  'session-close-adj': None,
  'session-open': None,
  'session-open-abs': None,
  'session-open-adj': None,
  'session-traded-high': None,
  'session-traded-high-abs': None,
  'session-traded-high-adj': None,
  'session-traded-low': None,
  'session-traded-low-abs': None,
  'session-traded-low-adj': None,
  'spread-volume': 0.0,
  'strike-price': None,
  'symbol': 'CNH25',
  'total-volume': 0.0,
  'updated-time': 1719007206472,
  'v': '',
  'volume-session': '1',
  'volume-trade': 0.0}]

由於目標資料較大, 我們可將 data 字典存入 json 檔案 :

>>> with open('sgx-ftse-a50.json', 'w', encoding='utf-8') as f:      
    json.dump(data, f)      

完整程式碼如下 :

import requests 
import json

url='https://api.sgx.com/derivatives/v1.0/contract-code/' +\
    'CN?order=asc&orderby=delivery-month&category=futures'    
res=requests.get(url)
data=json.loads(res.text)     # 亦可用 data=res.json()
with open('sgx-ftse-a50.json', 'w', encoding='utf-8') as f:      
    json.dump(data, f)
print('擷取結果儲存於 ./sgx-ftse-a50.json 檔案')

執行結果如下 :

>>> %Run sgx-ftse-a50-test-1.py  
擷取結果儲存於 ./sgx-ftse-a50.json 檔案


三. 擷取關鍵欄位存成 csv 檔 :  

從上面取得的目標資料可知, data['data'] 本身也是一個字典, 它包含相當多的鍵 (欄位), 其中對交易者較關鍵者如下 :
  • last-update-time: 最近更新時間
  • last-trading-date: 最近到期日
  • symbol: 代號
  • current-trading-session: 盤別
  • change-abs: 漲跌
  • change-percentage: 漲跌幅
  • session-open-abs: 開盤價
  • session-traded-high-abs: 最高價
  • session-traded-low-abs: 最低價
  • last-traded-price-abs: 收盤價
  • daily-settlement-price-abs: 結算價
  • total-volume: 成交量
  • open-interest: 未平倉量
下列程式碼從 data['data'] 中擷取這些關鍵欄位並存成 csv 檔 :

import requests 
import csv

url='https://api.sgx.com/derivatives/v1.0/contract-code/' +\
    'CN?order=asc&orderby=delivery-month&category=futures'    
res=requests.get(url)
data=res.json()
csv_file='sgx-ftse-a50-2.csv'
with open(csv_file, 'w+', newline='', encoding='utf-8') as f:   # newline='' 避免 Excel 空列   
    writer=csv.writer(f)
    header=['最近更新時間',
            '最近到期日',
            '代號',
            '盤別',
            '漲跌',
            '漲跌幅',
            '開盤價',
            '最高價',
            '最低價',
            '收盤價',
            '結算價',
            '成交量',
            '未平倉量']
    writer.writerow(header)    # 寫入標頭列
    for item in data['data']:     # 遍歷所有項目
        row=[]     # 儲存每列資料用
        row.append(item['last-update-time'])
        row.append(item['last-trading-date'])
        row.append(item['symbol'])
        if item['current-trading-session']=='0':   # 改換顯示
            row.append('T')      # 當日交易日
        else:
            row.append('T+')    # 次交易日
        row.append(item['change-abs'])
        row.append(item['change-percentage'])
        row.append(item['session-open-abs'])
        row.append(item['session-traded-high-abs'])
        row.append(item['change-percentage'])
        row.append(item['session-traded-low-abs'])
        row.append(item['last-traded-price-abs'])
        row.append(item['daily-settlement-price-abs'])
        row.append(item['total-volume'])
        row.append(item['open-interest'])
        writer.writerow(row)     # 寫入列
    print(f'擷取結果儲存於 ./{csv_file} 檔案')
    
執行結果如下 :

>>> %Run sgx-ftse-a50-test-2.py  
擷取結果儲存於 ./sgx-ftse-a50-2.csv 檔案  

用純文字編輯器開啟此 csv 檔 :




因 Excel 預設是 ANSI 編碼, 若直接用 Excel 開啟此 UTF-8 編碼的 csv 檔, 則中文會變成亂碼 :



 
解決之道是先用記事本開啟此 csv 檔, 然後另存新檔 (可用不同檔名, 例如後面加 BOM, 以免覆蓋原檔), 並選擇編碼格式為 "使用 BOM 之 UTF-8" 而非單純之 "UTF-8" :




這樣用 Excel 開啟 sgx-ftse-a50-2-BOM.csv 時就不會出現亂碼了 :





2024年6月21日 星期五

Python 學習筆記 : 網頁爬蟲實戰 (十二) 國發會景氣對策信號

抓完集保資料後再接再厲, 今天改抓國發會的景氣對策信號網頁, 本篇測試參考下面這本書中的範例 (我是想趕快看完把書還掉) : 


本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

國發會的景氣對策信號位於官網首頁下方 : 


將官網首頁往下拉到 "重要指標" 項目即可看到景氣對策信號與分數圖表 :




不過觀察原始碼可知, 首頁的這張圖只是單純的一張 png 圖檔而已 :




要按圖表左側的 "查詢系統" 進入下列網址才能找到資料 :






這是一個 Javascript 動態圖表, 當滑鼠在圖上移動時會顯示該位置的資料值. 使用 Quick Javascript Switcher 檢查, 關掉 Javascript 就會讓線圖消失, 確認此網頁乃是 Javascript 動態繪製產生, 這種網頁無法直接用 requests 套件來擷取, 但有可能透過分析其 HTTP 動態發現隱藏未公開的 API. 

在 Chrome 按 F12 開啟開發人員工具視窗後重新載入目標網頁, 然後切到 "Network/XHR" 頁籤會發現左側有一個 lightscore 的 HTTP 請求, 點選它會在右方的 Preview 或 Response 出現 JSON 資料, 這就是繪製此圖所需的資料 :




在 lightscore 上按滑鼠右鍵點選 Copy/Copy URL 可得到此資源的 URL :


但是如果直接連線此網址會得到如下回應 :




這是因為通過瀏覽器網址列連線使用的是 HTTP GET 方法, 而真人操作瀏覽器從首頁連過去使用的是 POST 方法, 將開發者工具切到 Headers 頁籤即知 :




二. 使用 requests 擷取目標資料 (JSON) :  

因為目標資料是 JSON 字串, 因此除了 requests 外還要匯入 json 模組 : 

>>> import requests 
>>> import json  
>>> url='https://index.ndc.gov.tw/n/json/lightscore'   
>>> res=requests.post(url)  
>>> res.encoding  
'utf-8'  

然後呼叫 json 模組的 loads() 函式將 JSON 字串轉換成字典 :

>>> data=json.loads(res.text)    

如果要用漂亮的格式列印此字典, 可呼叫 pprint.pprint() 函式 : 

>>> from pprint import pprint   
>>> pprint(data)   
{'lang': {'highcharts_coincident': '同時指標不含趨勢指數',
          'highcharts_coincident_legend': '同時指標不含趨勢指數',
          'highcharts_downloadJPEG': '下載jpg',
          'highcharts_downloadPDF': '下載pdf',
          'highcharts_downloadPNG': '下載png',
          'highcharts_downloadSVG': '下載svg',
          'highcharts_lagged': '落後指標不含趨勢指數',
          'highcharts_lagged_legend': '落後指標不含趨勢指數',
          'highcharts_leading': '領先指標不含趨勢指數',
          'highcharts_leading_legend': '領先指標不含趨勢指數',
          'highcharts_lightscore': '景氣對策信號及分數',
          'highcharts_printChart': '列印',
          'highcharts_resetZoom': '重設縮放',
          'highcharts_resetZoomTitle': '重設原比例',
          'highcharts_score': '分數',
          'ndc_spec_essence': '實質'},
 'line': [{'x': '202305', 'y': 12},
          {'x': '202306', 'y': 13},
          {'x': '202307', 'y': 15},
          {'x': '202308', 'y': 15},
          {'x': '202309', 'y': 17},
          {'x': '202310', 'y': 16},
          {'x': '202311', 'y': 20},
          {'x': '202312', 'y': 22},
          {'x': '202401', 'y': 27},
          {'x': '202402', 'y': 29},
          {'x': '202403', 'y': 31},
          {'x': '202404', 'y': 35}],
 'next': '2024-06-27 16:00',
 'right': {'10': {'code': 'SR0037',
                  'd': [{'m': '202403', 'n': 3}, {'m': '202404', 'n': 5}],
                  'lang': '機械及電機設備進口值',
                  'sort': 7,
                  'trans': 'default'},
           '11': {'code': 'SR0057',
                  'd': [{'m': '202403', 'n': 3}, {'m': '202404', 'n': 5}],
                  'lang': '批發、零售及餐飲業營業額',
                  'sort': 8,
                  'trans': 'default'},
           '3': {'code': 'SR0031',
                 'd': [{'m': '202403', 'n': 2}, {'m': '202404', 'n': 2}],
                 'lang': '貨幣總計數 M1B',
                 'sort': 0,
                 'trans': 'default'},
           '4': {'code': 'SR0033',
                 'd': [{'m': '202403', 'n': 5}, {'m': '202404', 'n': 5}],
                 'lang': '股價指數',
                 'sort': 1,
                 'trans': 'default'},
           '5': {'code': 'SR0034',
                 'd': [{'m': '202403', 'n': 4}, {'m': '202404', 'n': 5}],
                 'lang': '工業生產指數',
                 'sort': 2,
                 'trans': 'default'},
           '6': {'code': 'SR0055',
                 'd': [{'m': '202403', 'n': 3}, {'m': '202404', 'n': 4}],
                 'lang': '製造業銷售量指數',
                 'sort': 3,
                 'trans': 'default'},
           '7': {'code': 'SR0056',
                 'd': [{'m': '202403', 'n': 3}, {'m': '202404', 'n': 3}],
                 'lang': '製造業營業氣候測驗點',
                 'sort': 4,
                 'trans': 'up'},
           '8': {'code': 'SR0035',
                 'd': [{'m': '202403', 'n': 3}, {'m': '202404', 'n': 3}],
                 'lang': '工業及服務業加班工時',
                 'sort': 5,
                 'trans': 'default'},
           '9': {'code': 'SR0036',
                 'd': [{'m': '202403', 'n': 5}, {'m': '202404', 'n': 3}],
                 'lang': '海關出口值',
                 'sort': 6,
                 'trans': 'default'}}}

其中 line 屬性的 (x, y) 座標就是過去 12 個月 (x) 的景氣分數 (y). 關於 pprint 用法參考 :


呼叫 json 模組的 dump() 函式則可以將結果字典寫入 ,json 檔 :

>>> with open('monitoring_indicators.json', 'w', encoding='utf-8') as f:   
    json.dump(data, f)   


關於 json 模組用法參考 :


可見這種 Javascript 生成的網頁只要能找出隱藏的 API, 其實也很好擷取, 也毋須動用 Selenium,  而且這種隱藏的 API 網頁通常沒有防爬機制, 像這個景氣對策信號網頁, 它甚至都不檢查 User-Agent, 直接 POST 即可取得.

2024年6月20日 星期四

Python 學習筆記 : 網頁爬蟲實戰 (十一) 集保戶股權分散表

這幾天上完 PyTorch 課終於可以繼續玩 Python 爬蟲, 今天要爬的對象是集保結算所 (TDCC) 的集保戶股權分散表, 這是一個頗有挑戰性的網頁, 我以前用 PHP 爬這張表時還苦惱了一陣子哩! 但此網頁已經改版, 舊版為 big5 編碼, 且提交欄位中有一個怪碼 (那時就是卡在這怪碼). 參考 :


新版網頁則改為 utf-8 編碼, 且增加了隨機生成的查詢權杖, 使得 requests 爬蟲無法使用. 

本篇測試參考下面這本書 :


不過因為集保網頁已經改版, 網址也不同, 因此該書只能做為參考而已. 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

新版台股集保結算所網址如下 : 





輸入證券代號或名稱按查詢, 結果會顯示在網頁底下的表格中 : 




<div class="table-frame securities-overview m-t-20"> <div class="table-responsive" style="max-height:100%;"> <table class="table"> <thead> <tr> <th>序</th> <th>持股/單位數分級</th> <th>人數</th> <th>股數/單位數</th> <th>占集保庫存數比例 (%)</th> </tr> </thead> <tr> <td align="center" colspan="5"><span class="font" style="color: #ff0000;">查無此資料</span></td> </tr> </table> </div> </div>


使用 Quick Javascript Switcher 檢查可知此網頁並非由 Javascript 動態產生, 而是由上方的表單提交後回應了新的網頁 (不是 Ajax). 雖然看似靜態網頁, 但是這並不表示此網頁之內容用 requests 就能輕易擷取, 主要原因是此網頁的查詢表單中含有隨機產生的隱藏欄位值所致, 如果提交時沒有返回此隨機值, 將被視為是爬蟲行為而無法取得預期之回應. 

上方表單的 HTML 碼如下 : 

<form action="/portal/zh/smWeb/qryStock" method="post" name="form1" id="form1" > <input type="hidden" name="SYNCHRONIZER_TOKEN" value="f4e9fe7a-c5a1-41c5-a1f5-e937786064b6" id="SYNCHRONIZER_TOKEN" /> <input type="hidden" name="SYNCHRONIZER_URI" value="/portal/zh/smWeb/qryStock" id="SYNCHRONIZER_URI" /> <input type="hidden" name="method" value="submit" id="method" /> <input type="hidden" name="firDate" value="20240614" id="firDate" /> <table width="100%" style="max-width: 700px;margin: auto;"> <tr> <td width="150"> <label for="scaDate">資料日期</label> </td> <td width="550"> <select name="scaDate" id="scaDate"> <option value="20240614" selected>20240614</option> <option value="20240607" >20240607</option> <option value="20240531" >20240531</option> <option value="20240524" >20240524</option> <option value="20240517" >20240517</option> <option value="20240510" >20240510</option> <option value="20240503" >20240503</option> <option value="20240426" >20240426</option> <option value="20240419" >20240419</option> <option value="20240412" >20240412</option> <option value="20240403" >20240403</option> <option value="20240329" >20240329</option> <option value="20240322" >20240322</option> <option value="20240315" >20240315</option> <option value="20240308" >20240308</option> <option value="20240301" >20240301</option> <option value="20240223" >20240223</option> <option value="20240217" >20240217</option> <option value="20240207" >20240207</option> <option value="20240202" >20240202</option> <option value="20240126" >20240126</option> <option value="20240119" >20240119</option> <option value="20240112" >20240112</option> <option value="20240105" >20240105</option> <option value="20231229" >20231229</option> <option value="20231222" >20231222</option> <option value="20231215" >20231215</option> <option value="20231208" >20231208</option> <option value="20231201" >20231201</option> <option value="20231124" >20231124</option> <option value="20231117" >20231117</option> <option value="20231110" >20231110</option> <option value="20231103" >20231103</option> <option value="20231027" >20231027</option> <option value="20231020" >20231020</option> <option value="20231013" >20231013</option> <option value="20231006" >20231006</option> <option value="20230928" >20230928</option> <option value="20230923" >20230923</option> <option value="20230915" >20230915</option> <option value="20230908" >20230908</option> <option value="20230901" >20230901</option> <option value="20230825" >20230825</option> <option value="20230818" >20230818</option> <option value="20230811" >20230811</option> <option value="20230804" >20230804</option> <option value="20230728" >20230728</option> <option value="20230721" >20230721</option> <option value="20230714" >20230714</option> <option value="20230707" >20230707</option> <option value="20230630" >20230630</option> <option value="20230621" >20230621</option> </select> </td> </tr> <tr> <td> <input type="radio" name="sqlMethod" id="sqlStockNo" value="StockNo" checked> <label for="sqlStockNo">證券代號</label> </td> <td> <label for="StockNo" class="show-for-sr">請輸入證券代號</label> <input type="text" name="stockNo" id="StockNo" value="2330" placeholder="請輸入證券代號"/> </td> </tr> <tr> <td> <input type="radio" name="sqlMethod" id="sqlStockName" value="StockName" > <label for="sqlStockName">證券名稱</label> </td> <td> <label for="StockName" class="show-for-sr">請輸入證券名稱</label> <input type="text" name="stockName" id="StockName" value="" placeholder="請輸入證券名稱"> </td> </tr> <tr> <td colspan="2"><input type="submit" value="查詢"></td> </tr> </table> </form>

查詢後會傳回新網頁並被賦予隱藏欄位S YNCHRONIZER_TOKEN 新的值 (用來防爬), 查詢結果顯示在下方的表格內, 此表格預設是空的 :

<table class="table"> <thead> <tr> <th>序</th> <th>持股/單位數分級</th> <th>人數</th> <th>股數/單位數</th> <th>占集保庫存數比例 (%)</th> </tr> </thead> <tr> <td align="center" colspan="5"><span class="font" style="color: #ff0000;">查無此資料</span></td> </tr> </table>

如果有查詢到資料, 伺服器會回應結果到下方的表格內 (以 20240614 為例) :

<table class="table"> <thead> <tr> <th>序</th> <th>持股/單位數分級</th> <th>人數</th> <th>股數/單位數</th> <th>占集保庫存數比例 (%)</th> </tr> </thead> <tr> <td align="center">1</td> <td align="center">1-999</td> <td align="right">683,556</td> <td align="right">116,764,637</td> <td align="right">0.45</td> </tr> <tr> <td align="center">2</td> <td align="center">1,000-5,000</td> <td align="right">303,738</td> <td align="right">583,434,524</td> <td align="right">2.24</td> </tr> <tr> <td align="center">3</td> <td align="center">5,001-10,000</td> <td align="right">37,867</td> <td align="right">274,725,852</td> <td align="right">1.05</td> </tr> <tr> <td align="center">4</td> <td align="center">10,001-15,000</td> <td align="right">13,075</td> <td align="right">161,124,768</td> <td align="right">0.62</td> </tr> <tr> <td align="center">5</td> <td align="center">15,001-20,000</td> <td align="right">6,327</td> <td align="right">112,107,192</td> <td align="right">0.43</td> </tr> <tr> <td align="center">6</td> <td align="center">20,001-30,000</td> <td align="right">6,290</td> <td align="right">154,365,938</td> <td align="right">0.59</td> </tr> <tr> <td align="center">7</td> <td align="center">30,001-40,000</td> <td align="right">2,983</td> <td align="right">103,758,273</td> <td align="right">0.40</td> </tr> <tr> <td align="center">8</td> <td align="center">40,001-50,000</td> <td align="right">1,817</td> <td align="right">82,021,153</td> <td align="right">0.31</td> </tr> <tr> <td align="center">9</td> <td align="center">50,001-100,000</td> <td align="right">3,531</td> <td align="right">247,479,322</td> <td align="right">0.95</td> </tr> <tr> <td align="center">10</td> <td align="center">100,001-200,000</td> <td align="right">1,835</td> <td align="right">256,834,978</td> <td align="right">0.99</td> </tr> <tr> <td align="center">11</td> <td align="center">200,001-400,000</td> <td align="right">1,194</td> <td align="right">333,768,664</td> <td align="right">1.28</td> </tr> <tr> <td align="center">12</td> <td align="center">400,001-600,000</td> <td align="right">500</td> <td align="right">244,870,947</td> <td align="right">0.94</td> </tr> <tr> <td align="center">13</td> <td align="center">600,001-800,000</td> <td align="right">309</td> <td align="right">213,676,720</td> <td align="right">0.82</td> </tr> <tr> <td align="center">14</td> <td align="center">800,001-1,000,000</td> <td align="right">207</td> <td align="right">184,908,461</td> <td align="right">0.71</td> </tr> <tr> <td align="center">15</td> <td align="center">1,000,001以上</td> <td align="right">1,568</td> <td align="right">22,862,231,563</td> <td align="right">88.16</td> </tr> <tr> <td align="center">16</td> <td align="center">差異數調整(說明4)</td> <td align="right"></td> <td align="right">-2,000</td> <td align="right">-0.00</td> </tr> <tr> <td align="center">17</td> <td align="center">合 計</td> <td align="right">1,064,797</td> <td align="right">25,932,070,992</td> <td align="right">100.00</td> </tr> </table>

以上 HTML 碼其實是利用下列網站美化過的 : 


進一步觀察原始碼中的表單結構, 可知當按下查詢鈕時會向後端程式 /portal/zh/smWeb/qryStock 以 POST 方法傳遞如下變數 :
  • method: 查詢方法 submit 
  • firDate: 最近發布資料的日期
  • scaDate : 資料日期 (格式 YYYYmmdd)
  • StockNo : 證券代號 (例如 : 2330)
  • StockName : 證券名稱 (例如 : 台積電)
  • sqlMethod : 查詢方式 (證券代號 StockNo 或證券名稱 StockName)
  • SYNCHRONIZER_URI : 網址 /portal/zh/smWeb/qryStock
  • SYNCHRONIZER_TOKEN: 臨時編配的查詢權杖 (每次不同)
這裡面有四個是隱藏欄位, 其中 SYNCHRONIZER_TOKEN 就是隨機產生的查詢權杖, 每次載入此網頁都會得到不同的查詢權杖, 在權杖有效期限內再次查詢時後端伺服器會認為這是真人操作瀏覽器行為而給予回應, 但使用 requests 的爬蟲程式則因為無法預知此隨機值而查詢失敗, 因此 requests 在此派不上用場, 必須使用 Selenium 來模擬真人操作瀏覽器才行. 

在 Chrome 按 F12 開啟開發者工具視窗, 重新輸入表單後按查詢, 然後切到開發者工具視窗的 Network/Doc 頁籤, 左邊會看到 qryStock 這個請求, 這就是按查詢鈕後瀏覽器送出的 POST 請求 :




切到 FormData 頁籤就可以看到送出的表單變數, 可見除了我們勾選填入的資料外, 表單提交時還送出了幾個隱藏欄位, 其中的 SYNCHRONIZER_TOKEN 就是後端為了防爬而隨機產生的查詢權杖 :




切到 Response 頁籤則可看到伺服器的回應內容 :




接下來用 requests 套件來確認它真的無法達成任務 (即使標頭有帶 User-Agent 與 Referer 也是枉然, 集保網站主要是靠隱藏欄位中的隨機權杖來防爬的). 


二. 用 requests 擷取目標網頁 :  

先匯入套件與模組 : 

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

為了逼真些, HTTP 標頭除了 User-Agent 外還帶了 Refer 屬性, 其值可在手動操作時的請求標頭中找到 : 

>>> ua=UserAgent()  
>>> headers={'User-Agent': ua.random,  
         'Referer': 'https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'}  

接著定義 POST 要傳遞之參數, 此處 SYNCHRONIZER_TOKEN 取自上一次手動操作後的值 :

>>> data={'method': 'submit',   
      'firDate': '20240614',  
      'scaDate': '20240614',
      'sqlMethod': 'StockNo',
      'stockNo': '2330',
      'stockName': '',
      'SYNCHRONIZER_URI': '/portal/zh/smWeb/qryStock',
      'SYNCHRONIZER_TOKEN': 'f4e9fe7a-c5a1-41c5-a1f5-e937786064b6'}

最後用 POST 方法提交表單, 並將回應網頁存入檔案中 : 

>>> url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'   
>>> res=requests.post(url, headers=headers, data=data)   
>>> with open('tdcc.htm', "w", encoding='utf-8') as f:      
    f.write(res.text)   

開啟 tdcc.htm 可知網站回應 "查無資料" :

<table class="table"> <thead> <tr> <th>序</th> <th>持股/單位數分級</th> <th>人數</th> <th>股數/單位數</th> <th>占集保庫存數比例 (%)</th> </tr> </thead> <tr> <td align="center" colspan="5"><span class="font" style="color: #ff0000;">查無此資料</span></td> </tr> </table>

研判應該是隱藏欄位 SYNCHRONIZER_TOKEN 不符所致. 


三. 用 Selenium 擷取目標網頁 :  

觀察上面的網頁原始碼, 隱藏欄位 firDate 紀錄的是最近一次發布資料的日期, 實際佈署爬蟲時我們只要取得 firDate 日期, 然後去點選下拉式選單 scaDate, 因單選圓鈕 sqlMethod 預設就是證券代號 stockNo 不用點選, 故只要在證券代號輸入框 stockNo 欄輸入股票代號, 再按下查詢鈕即可 :

>>> from selenium import webdriver   
>>> driver=webdriver.Firefox()    
>>> driver.implicitly_wait(20)     
>>> url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
>>> driver.get(url)    

這樣便載入目標網頁了, 首先取得隱藏欄位 firDtae (最近發布資料之日期) :

>>> firDate=driver.find_element('name', 'firDate')   
>>> firDate.get_attribute('value')     
'20240614'    

接著取得日期下拉式選單 select 元素之物件, 然後搜尋其下之全部 option 物件 : 

>>> scaDate_select=driver.find_element('id', 'scaDate')  
>>> scaDate_options=scaDate_select.find_elements('tag name', 'option')    
>>> len(scaDate_options)   
52

可見資料庫可讓我們查詢過去 52 周 (一年) 的股權分散資訊 (每周更新一次), 選項中的第一個就是最新的資料, 預設已被勾選 :

>>> scaDate_options[0].text   
'20240614'
>>> scaDate_options[0].is_selected()   
True
>>> scaDate_options[0].get_attribute('value')    
'20240614' 

如果要抓一年前的資料, 可以呼叫倒數第一個選項物件的 click() 方法即可 :

>>> scaDate_options[-1].click()  
>>> scaDate_options[-1].is_selected()  
True   
>>> scaDate_options[-1].get_attribute('value')    
'20230621'  

因我們要抓最新的資料, 所以勾選第一個選項以恢復預設情況 :

>>> scaDate_options[0].click()  
>>> scaDate_options[0].is_selected()   
True
>>> scaDate_options[0].get_attribute('value')   
'20240614'

接著要在證券代號 stockNo 欄輸入股票代號, 例如 2330 :

>>> stockNo_text=driver.find_element('name', 'stockNo')  
>>> stockNo_text.send_keys('2330')  

最後是取得表單底下的 "查詢" 鈕物件並呼叫其 click() 方法, 由於此按鈕沒有 name, id, 與 class 以資定位, 故使用 XPATH :
 
>>> submit_btn=driver.find_element('xpath', '//*[@id="form1"]/table/tbody/tr[4]/td/input')    
>>> submit_btn.click()   

這樣就能得到目標網頁了 : 




取得回應之目標網頁後, 即可擷取表格中的資料了.  此表格有帶一個 class='table' 樣式類別, 搜尋原始碼可知此屬性為唯一, 因此可用 class name 來定位此表格 : 

>>> table=driver.find_element('class name', 'table')     

在 table 物件中搜尋其子代的所有列 (tr 元素), 然後針對每一列 (tr) 搜尋其下的所有 td 元素來讀取其內容, 所以需要雙層迴圈來迭代 :  

>>> trs=table.find_elements('tag name', 'tr')   # 取得 table 物件
>>> for tr in trs:    # 拜訪所有列 (tr) 物件
    tds=tr.find_elements('tag name', 'td')      # 取得該列所有欄 (td) 物件
    i=0      # 判斷第幾欄用
    for td in tds:     # 拜訪所有欄  
        if i in (1, 2, 3):      # 第 2, 3, 4 欄整數可能有千位逗號
            text=td.text.replace(',', '')     # 去除可能有的千位逗號
        else:     # 其他欄位不改
            text=td.text   
        print(text, end=',')      
        i += 1     # 欄數增量
    print('\n')     

最後關閉瀏覽器 :

>>> driver.close()   

結果如下 : 

1,1-999,772171,124019060,0.47,

2,1000-5000,311651,595985812,2.29,

3,5001-10000,38009,275747903,1.06,

4,10001-15000,13252,163217708,0.62,

5,15001-20000,6368,112848611,0.43,

6,20001-30000,6319,155212065,0.59,

7,30001-40000,2996,104055410,0.40,

8,40001-50000,1831,82623496,0.31,

9,50001-100000,3584,250913869,0.96,

10,100001-200000,1850,257729419,0.99,

11,200001-400000,1237,346853028,1.33,

12,400001-600000,527,258437049,0.99,

13,600001-800000,321,222776994,0.85,

14,800001-1000000,211,189786240,0.73,

15,1000001以上,1557,22794838328,87.89,

16,差異數調整(說明4),,-14000,-0.00,

17,合 計,1161884,25935030992,100.00,

這樣就完成目標網頁之擷取了. 注意, 根據上面的網頁原始碼可知, 回應表格中表頭的 tr 是放在 thead 底下, 它裡面沒有 td 元素 (表頭是放在 th 元素內), 因此第一列會是空列. 

上面測試的完整程式碼如下 :

# tdcc_test_2.py
from selenium import webdriver

driver=webdriver.Firefox()    
driver.implicitly_wait(20)     
url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
driver.get(url)
# 點下拉式選單第一個選項
scaDate_select=driver.find_element('id', 'scaDate')  
scaDate_options=scaDate_select.find_elements('tag name', 'option')    
scaDate_options[0].text
scaDate_options[0].click()
# 輸入股票代號
stockNo_text=driver.find_element('name', 'stockNo')  
stockNo_text.send_keys('2330')
# 按查詢鈕
xpath='//*[@id="form1"]/table/tbody/tr[4]/td/input'
submit_btn=driver.find_element('xpath', xpath)    
submit_btn.click()  

table=driver.find_element('class name', 'table')
trs=table.find_elements('tag name', 'tr')
for tr in trs:
    tds=tr.find_elements('tag name', 'td')
    i=0
    for td in tds:
        if i in (1, 2, 3):
            text=td.text.replace(',', '')
        else:
            text=td.text
        print(text, end=',')
        i += 1
    print('\n')
driver.close()


四. 將 Selenium 爬蟲寫成函式 :  

由於集保所只提供查詢過去一年 52 周個股的股權分散表, 因此若要取得這 52 張資料表需要先擷取這些資料的發布日期, 也就是下拉式選單的 52 個選項的 value 屬性值, 以下是用 Firefox 無頭模式執行的 scaDates 爬蟲 :

# tdcc_test_3.py
from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.firefox.options import Options
import time

def get_scadate():
    options=Options()
    options.add_argument("--headless")
    driver=webdriver.Firefox(options=options)    
    driver.implicitly_wait(20)     
    url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
    driver.get(url)
    scaDates=[]
    select=driver.find_element('id', 'scaDate')  
    options=select.find_elements('tag name', 'option')
    dates=[option.text for option in options]
    driver.close()
    return dates

if __name__ == '__main__':
    start=time.time()
    scadates=get_scadate()
    print(f'scaDates : \n{scadates}')
    end=time.time()
    print(f'執行時間:{end-start}')

執行結果如下 :

>>> %Run tdcc_test_3.py   
scaDates : 
['20240614', '20240607', '20240531', '20240524', '20240517', '20240510', '20240503', '20240426', '20240419', '20240412', '20240403', '20240329', '20240322', '20240315', '20240308', '20240301', '20240223', '20240217', '20240207', '20240202', '20240126', '20240119', '20240112', '20240105', '20231229', '20231222', '20231215', '20231208', '20231201', '20231124', '20231117', '20231110', '20231103', '20231027', '20231020', '20231013', '20231006', '20230928', '20230923', '20230915', '20230908', '20230901', '20230825', '20230818', '20230811', '20230804', '20230728', '20230721', '20230714', '20230707', '20230630', '20230621']
執行時間:19.174318075180054

其次我們將上面查詢股權分散表的程式改寫為如下的函式 craw_tdcc() :

# tdcc_test_4.py 
from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.firefox.options import Options
import time
import csv

def craw_tdcc(date, stock_no):
    options=Options()
    options.add_argument("--headless")             # 無頭模式
    driver=webdriver.Firefox(options=options)
    #driver=webdriver.Firefox()
    driver.implicitly_wait(20)     
    url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
    driver.get(url)
    select=Select(driver.find_element('id', 'scaDate'))   # 建立 Select 物件
    select.select_by_value(date)     # 選取 value 屬性值為 date 之選項
    stockNo_text=driver.find_element('name', 'stockNo')  
    stockNo_text.send_keys(stock_no)
    xpath='//*[@id="form1"]/table/tbody/tr[4]/td/input'
    submit_btn=driver.find_element('xpath', xpath)    
    submit_btn.click()
    data=[]
    table=driver.find_element('class name', 'table')
    trs=table.find_elements('tag name', 'tr')
    for tr in trs:
        tds=tr.find_elements('tag name', 'td')
        i=0
        row=[]
        for td in tds:
            if i in (1, 2, 3):
                col=td.text.replace(',', '')    # 去除可能的千位逗號
            else:
                col=td.text
            row.append(col)
            i += 1
        data.append(row)
    driver.close()
    del data[0]       # 刪除表頭空列
    return data

if __name__ == '__main__':
    start=time.time()
    date='20240614'
    stock_no='2330'
    data=craw_tdcc(date, stock_no)
    print(data)
    with open(f'{stock_no}_{date}.csv', 'w', newline='') as f:  # 避免 Excel 空行
        writer=csv.writer(f)
        writer.writerows(data)
    end=time.time()
    print(f'執行時間:{end-start}')

此處使用了 selenium.webdriver.support.select.Select 類別來處理 select 元素選項的選取操作, 這是透過呼叫 Select 物件的 select_by_value() 來達成的. craw_tdcc() 函式會將擷取到的回應表格資料放在一個二維串列中傳回, 由於回應表格中的表頭無 td 元素, 因此第一列是空列, 可用 del data[0] 去除, 在主程式中會將此二維串列寫入 csv 檔中. 注意, 由於在 Windows 中以 Excel 讀取 csv 檔會出現多跳一行問題, 開啟 csv 檔時務必添加 newline='' 參數. 

執行結果如下 :

>>> %Run tdcc_test_4.py   
[['1', '1-999', '772171', '124019060', '0.47'], ['2', '1000-5000', '311651', '595985812', '2.29'], ['3', '5001-10000', '38009', '275747903', '1.06'], ['4', '10001-15000', '13252', '163217708', '0.62'], ['5', '15001-20000', '6368', '112848611', '0.43'], ['6', '20001-30000', '6319', '155212065', '0.59'], ['7', '30001-40000', '2996', '104055410', '0.40'], ['8', '40001-50000', '1831', '82623496', '0.31'], ['9', '50001-100000', '3584', '250913869', '0.96'], ['10', '100001-200000', '1850', '257729419', '0.99'], ['11', '200001-400000', '1237', '346853028', '1.33'], ['12', '400001-600000', '527', '258437049', '0.99'], ['13', '600001-800000', '321', '222776994', '0.85'], ['14', '800001-1000000', '211', '189786240', '0.73'], ['15', '1000001以上', '1557', '22794838328', '87.89'], ['16', '差異數調整(說明4)', '', '-14000', '-0.00'], ['17', '合\u3000計', '1161884', '25935030992', '100.00']]
執行時間:36.20839500427246 

開啟儲存之 csv 檔預設會開啟 Excel : 




實際應用上會將結果存入資料庫以利進一步運算. 

如果要一次下載多個日期的資料, 程式可改為如下 (但會花比較久時間) :

# tdcc_test_5.py
from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.firefox.options import Options
import time
import csv

def get_scadate():
    options=Options()
    options.add_argument("--headless")
    driver=webdriver.Firefox(options=options)    
    driver.implicitly_wait(20)     
    url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
    driver.get(url)
    scaDates=[]
    select=driver.find_element('id', 'scaDate')  
    options=select.find_elements('tag name', 'option')
    dates=[option.text for option in options]
    driver.close()
    return dates

def craw_tdcc(date, stock_no):
    options=Options()
    options.add_argument("--headless")
    driver=webdriver.Firefox(options=options)
    driver.implicitly_wait(20)     
    url='https://www.tdcc.com.tw/portal/zh/smWeb/qryStock'      
    driver.get(url)
    select=Select(driver.find_element('id', 'scaDate'))  
    select.select_by_value(date)
    stockNo_text=driver.find_element('name', 'stockNo')  
    stockNo_text.send_keys(stock_no)
    xpath='//*[@id="form1"]/table/tbody/tr[4]/td/input'
    submit_btn=driver.find_element('xpath', xpath)    
    submit_btn.click()
    data=[]
    table=driver.find_element('class name', 'table')
    trs=table.find_elements('tag name', 'tr')
    for tr in trs:
        tds=tr.find_elements('tag name', 'td')
        i=0
        row=[]
        for td in tds:
            if i in (1, 2, 3):
                col=td.text.replace(',', '')
            else:
                col=td.text
            row.append(col)
            i += 1
        data.append(row)
    driver.close()
    del data[0]
    return data

if __name__ == '__main__':
    start=time.time()
    dates=get_scadate()
    print(f'scaDates : \n{dates}')
    stocks=['2330']
    for stock in stocks:
        for date in dates[:5]:   # 擷取近 5 周資料
            data=craw_tdcc(date, stock)
            print(data)
            with open(f'{stock}_{date}.csv', 'w', newline='') as f:
                writer=csv.writer(f)
                writer.writerows(data)
    end=time.time()
    print(f'執行時間:{end-start}')

此處 stocks 串列只有一檔股票, 而且日期也只取最近 5 周的資料, 執行結果如下 :

>>> %Run tdcc_test_5.py   
scaDates : 
['20240614', '20240607', '20240531', '20240524', '20240517', '20240510', '20240503', '20240426', '20240419', '20240412', '20240403', '20240329', '20240322', '20240315', '20240308', '20240301', '20240223', '20240217', '20240207', '20240202', '20240126', '20240119', '20240112', '20240105', '20231229', '20231222', '20231215', '20231208', '20231201', '20231124', '20231117', '20231110', '20231103', '20231027', '20231020', '20231013', '20231006', '20230928', '20230923', '20230915', '20230908', '20230901', '20230825', '20230818', '20230811', '20230804', '20230728', '20230721', '20230714', '20230707', '20230630']
[['1', '1-999', '772171', '124019060', '0.47'], ['2', '1000-5000', '311651', '595985812', '2.29'], ['3', '5001-10000', '38009', '275747903', '1.06'], ['4', '10001-15000', '13252', '163217708', '0.62'], ['5', '15001-20000', '6368', '112848611', '0.43'], ['6', '20001-30000', '6319', '155212065', '0.59'], ['7', '30001-40000', '2996', '104055410', '0.40'], ['8', '40001-50000', '1831', '82623496', '0.31'], ['9', '50001-100000', '3584', '250913869', '0.96'], ['10', '100001-200000', '1850', '257729419', '0.99'], ['11', '200001-400000', '1237', '346853028', '1.33'], ['12', '400001-600000', '527', '258437049', '0.99'], ['13', '600001-800000', '321', '222776994', '0.85'], ['14', '800001-1000000', '211', '189786240', '0.73'], ['15', '1000001以上', '1557', '22794838328', '87.89'], ['16', '差異數調整(說明4)', '', '-14000', '-0.00'], ['17', '合\u3000計', '1161884', '25935030992', '100.00']]
[['1', '1-999', '754140', '122931202', '0.47'], ['2', '1000-5000', '308948', '592075408', '2.28'], ['3', '5001-10000', '37952', '275356134', '1.06'], ['4', '10001-15000', '13186', '162406756', '0.62'], ['5', '15001-20000', '6367', '112831866', '0.43'], ['6', '20001-30000', '6298', '154603168', '0.59'], ['7', '30001-40000', '2990', '103812000', '0.40'], ['8', '40001-50000', '1822', '82306729', '0.31'], ['9', '50001-100000', '3577', '250416577', '0.96'], ['10', '100001-200000', '1853', '258937961', '0.99'], ['11', '200001-400000', '1222', '342883922', '1.32'], ['12', '400001-600000', '519', '254611205', '0.98'], ['13', '600001-800000', '328', '226461894', '0.87'], ['14', '800001-1000000', '213', '191294827', '0.73'], ['15', '1000001以上', '1559', '22804106338', '87.92'], ['16', '差異數調整(說明4)', '', '-4995', '-0.00'], ['17', '合\u3000計', '1140974', '25935030992', '100.00']]
[['1', '1-999', '744965', '122341201', '0.47'], ['2', '1000-5000', '310675', '595417580', '2.29'], ['3', '5001-10000', '38016', '275712661', '1.06'], ['4', '10001-15000', '13281', '163677511', '0.63'], ['5', '15001-20000', '6360', '112691588', '0.43'], ['6', '20001-30000', '6309', '154968971', '0.59'], ['7', '30001-40000', '2981', '103450488', '0.39'], ['8', '40001-50000', '1822', '82237435', '0.31'], ['9', '50001-100000', '3585', '251238321', '0.96'], ['10', '100001-200000', '1844', '258191786', '0.99'], ['11', '200001-400000', '1214', '340093536', '1.31'], ['12', '400001-600000', '522', '256521760', '0.98'], ['13', '600001-800000', '321', '221574342', '0.85'], ['14', '800001-1000000', '209', '187553987', '0.72'], ['15', '1000001以上', '1564', '22809359825', '87.94'], ['16', '合\u3000計', '1133668', '25935030992', '100.00']]
[['1', '1-999', '721905', '119306320', '0.46'], ['2', '1000-5000', '299429', '575311651', '2.21'], ['3', '5001-10000', '37269', '270201915', '1.04'], ['4', '10001-15000', '13002', '160269696', '0.61'], ['5', '15001-20000', '6319', '112051439', '0.43'], ['6', '20001-30000', '6216', '152676847', '0.58'], ['7', '30001-40000', '2940', '102064749', '0.39'], ['8', '40001-50000', '1807', '81539300', '0.31'], ['9', '50001-100000', '3561', '249429358', '0.96'], ['10', '100001-200000', '1846', '258535811', '0.99'], ['11', '200001-400000', '1207', '338705791', '1.30'], ['12', '400001-600000', '513', '251552176', '0.96'], ['13', '600001-800000', '323', '223169542', '0.86'], ['14', '800001-1000000', '199', '177764044', '0.68'], ['15', '1000001以上', '1570', '22862454353', '88.15'], ['16', '差異數調整(說明4)', '', '-2000', '-0.00'], ['17', '合\u3000計', '1098106', '25935030992', '100.00']]
[['1', '1-999', '727299', '120373098', '0.46'], ['2', '1000-5000', '304071', '583632160', '2.25'], ['3', '5001-10000', '37735', '273432276', '1.05'], ['4', '10001-15000', '13156', '162212934', '0.62'], ['5', '15001-20000', '6348', '112607949', '0.43'], ['6', '20001-30000', '6251', '153470264', '0.59'], ['7', '30001-40000', '2971', '103100395', '0.39'], ['8', '40001-50000', '1817', '81941764', '0.31'], ['9', '50001-100000', '3566', '249917563', '0.96'], ['10', '100001-200000', '1840', '257392245', '0.99'], ['11', '200001-400000', '1224', '343468505', '1.32'], ['12', '400001-600000', '510', '250674895', '0.96'], ['13', '600001-800000', '316', '219144168', '0.84'], ['14', '800001-1000000', '204', '182103579', '0.70'], ['15', '1000001以上', '1571', '22841647197', '88.07'], ['16', '差異數調整(說明4)', '', '-88000', '-0.00'], ['17', '合\u3000計', '1108879', '25935030992', '100.00']]
執行時間:232.52468371391296

這樣就花了近 4 分鐘時間, Selenium 爬蟲的缺點就是跑不快啊!