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 爬蟲的缺點就是跑不快啊!

沒有留言:

張貼留言