2024年8月3日 星期六

Python 學習筆記 : 網頁爬蟲實戰 (十七) 下載證交所股東會年報 pdf 檔

今天在讀 "最強 AI 投資分析" 這本書的第七章時發現原來可以利用 ChatGPT 強大的文本分析能力幫我們解讀股東會年報, 本篇任務就是寫個爬蟲程式下載股東會年報 pdf 檔, 以利後續交給 ChatGPT 解讀.

本系列之前的筆記參考 : 



一. 檢視目標網站 : 

證交所股東會年度報告書事投資人評估公司經營狀況的重要管道, 主要包含企業的營運, 財務, 募資, 與面臨之風險等資訊, 是投資人評估長期投資價值的重要文件, 每年五月份左右會以 pdf 檔形式發布於證交所的公開資訊觀測站供投資人下載閱覽. 

證交所上市公司年報網站網址為 : 





首先使用 Quick Javascript Switcher 擴充套件檢測, 發現整個網頁在關閉 Javascript 功能後大致都還在, 只有 "季別/月份" 與 "資料細節說明" 這兩個下拉式選單裡的選項會消失, 檢視原始碼發現這兩組選項值都是由 Javascript 靜態生成而非透過 Ajax 動態, 查詢年報基本上是用表單提交 HTTP 請求達成, 因此只要用 requests + BeautifulSoup 即可擷取要下載的 pdf 檔. 

檢視原始碼複製其中的表單, 用 HTML formatter 格式化後的 HTML 碼如下 :


<form action='/server-java/t57sb01' id="fm" method='post' name='fm'> <input name='id' type='hidden' value=''><input name='key' type='hidden' value=''><input name='step' type='hidden'> <table border='0'> <tr> <td></td> </tr> <tr> <td>公司(證券)代號</td> <td colspan='3'> <input maxlength='10' name='co_id' size='10' type='text'> <a href='javascript:next("00");'><font class='AS21' size='2'>以市場別/產業別查詢代號</font></a>&nbsp;&nbsp;&nbsp; <a href='javascript:qry();'><font class='AS21' size='2'>以公司簡稱查詢</font></a> </td> </tr> <tr> <td colspan='4'><font color='red' size='-1'>若欲查詢證券商者,請輸入000+證券商代號前3碼。<br> 例:欲查詢1180仁信證券之資料,則請於公司(證券)代號欄位中輸入000118</font> </td> </tr> <tr> <td>資料年度</td> <td><input maxlength='3' name='year' size='3' type='text'></td> <td>季別 / 月份</td> <td> <select name='seamon' onchange=''> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> <option value=''>     </option> </select> </td> </tr> <tr> <td>資料類型</td> <td> <select name='mtype' onchange='reload_dtype();reload_sm();'> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> </select> </td> <td>資料細節說明</td> <td> <select name='dtype' onchange=''> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> <option value=''>           </option> </select> </td> </tr> <tr> <td align='center' colspan='4'> <a href='javascript:next("1");'><img alt='查詢' border='0' src='/image/t57sf09.gif'></a> <a href='/Reader_TW.exe'><font class='AS21' size='2'>DYNADOC WDL Viewer下載</font></a> </td> </tr> </table> <br> </form>

雖然 "季別/月份" 與 "資料細節說明" 這兩個下拉式選單的選項是 Javascript 生成, 但 "季別/月份" 的選項在此用不到, "資料細節說明" 的選項值已知要選 "股東會年報", 因此只要能捕捉到表單提交時所傳送之所有參數, 就能使用 requests 套件模擬人工查詢之動作. 

在 Chrome 瀏覽器按 F12 開啟開發人員工具視窗以便觀察 HTTP 請求訊息. 在 "公司/(證券)代號" 欄輸入公司名稱 (例如台積電) 或證券代號 (例如 2330), 填入 "資料年度" (民國年), "資料類型" 欄勾選 "股東會相關資料", "資料細節" 欄勾選 "股東會年報" :




按 "查詢" 鈕會載入一個相同網址之第二個網頁, 查詢結果以一個表格呈現 :




但點按 "電子檔案" 欄位中的 pdf 檔超連結並不是直接下載檔案, 而是會載入第三個相同網址網頁. 檢視開發人員工具視窗的 Element 頁籤可知, 第二個網頁中的查詢結果表格其實是放在一個 name="fm2" 的表單中, 其請求方法為 POST :




可見此表單內有四個隱藏元素, 其傳遞之變數名稱為 step, kind, co_id, 與 filename, 它們會被表單 fm2 傳遞給伺服器 :




"電子檔案" 欄位中的超連結被按下時會呼叫 readfile2() 函數來提交表單, 切換至 Network 的 Payload 頁籤可看到 POST 請求的全部變數, 可見實際上會傳送 8 個變數, 其中 id, key, 與 seamon 三個變數在下載股東會年報 pdf 任務中用不到為空字串, 實際上要指定的只有 co_id (股票代號) 與 year (年度) 這兩個變數而已, 對於查詢股東會年報, step='1', mtype='F', dtype='F04': 




這時會傳回第三個網頁顯示要下載的 pdf 檔超連結 :




檢視 Network 的 Payload 頁籤可知此 POST 請求所傳遞之變數有 4 個 : step='9', kind='F', co_id='2330', 與電子檔案名稱 filename, 其實就是表單 name='fm2' 中的那 4 個隱藏欄位之值 : 




可知

這才是真正的 pdf 檔案下載頁面, 從 Element 頁籤可知超連結 a 元素的 href 是一個相對之 URL 路徑檔, 直接點按會以 GET 方式提出請求下載 pdf 檔 :


 

點按超連結或滑鼠移到上面按右鍵選另存檔案後即可開啟 pdf :




以上便是手動下載年報的操作過程與目標網頁分析, 接下來要用 requests 與 BeautifulSoup 套件以 Python 程式下載 pdf 檔.


二. 使用 requests + BeautifulSoup 下載年報 pdf 檔 : 

首先匯入 requests 與 BeautifulSoup 套件 :

>>> import requests   
>>> from bs4 import BeautifulSoup   

由上面分析可知需提交兩次 POST 請求與一次 GET 請求才能下載股東會年報之 pdf 檔, 因此必須用 requests 向目標網站連續送出 HTTP 請求. 

首先是第一次 POST 請求, 由上面分析可知它必須傳遞 8 個變數, 但其中只有 co_id (股票代號) 與 year (年度) 需要指定, mtype 固定為 'F', dtype 固定為 'F04', step 固定為 '1', 其餘 3 個 : id, key, 與 seamon 均為空字串 :

以查詢台積電 112 年度股東會年報為例, 要傳送的參數可用下列字典表示 :

>>> data={'id': '',
      'key': '',
      'step': '1',
      'co_id': '2330',
      'year': '112',
      'seamon': '',
      'mtype': 'F',
      'dtype': 'F04'}    
>>> data     
{'id': '', 'key': '', 'step': '1', 'co_id': '2330', 'year': '112', 'seamon': '', 'mtype': 'F', 'dtype': 'F04'}   

定義目標網站網址 : 

>>> url='https://doc.twse.com.tw/server-java/t57sb01'    

呼叫 requests.post() 並傳入 url 與 data 參數提出 POST 請求, 這會傳回一個代表回應網頁的 Response 物件, 將其 :

>>> res=requests.post(url, data=data)   
>>> soup=BeautifulSoup(res.text, 'lxml')      

由於回應網頁 (第二個網頁) 中只有一個超連結, 故只要用 find('a') 即可找到 a 元素 : 

>>> link=soup.find('a')    
>>> link   
<a href='javascript:readfile2("F","2330","2022_2330_20230606F04.pdf");'>2022_2330_20230606F04.pdf</a>
>>> filename=link.text   
>>> filename   
'2022_2330_20230606F04.pdf'   

這個 pdf 檔名會被設定在 name='fm2' 表單 name='filename' 的隱藏欄位傳遞給伺服器, 因此接下來要用 step, kind, co_id, 與 filename 這四個參數向目標網站發出第二個 POST 請求 : 

>>> data={'step': '9',
      'kind': 'F',
      'co_id': '2330',
      'filename': filename}    
>>> data     
{'step': '9', 'kind': 'F', 'co_id': '2330', 'filename': '2022_2330_20230606F04.pdf'}
>>> res=requests.post(url, data=data)   
>>> soup=BeautifulSoup(res.text, 'lxml')     
>>> link=soup.find('a')   
>>> link   
<a href="/pdf/2022_2330_20230606F04_20240803_233245.pdf">2022_2330_20230606F04.pdf</a>

第三個網頁就沒有表單了, 它回應的超連結直接在 href 屬性中給出 pdf 檔的路徑檔名 :

>>> href=link.get('href')   
>>> href   
'/pdf/2022_2330_20230606F04_20240803_233245.pdf'    

注意, 這是一個相對於目標網址 url 根目錄 https://doc.twse.com.tw 的檔案路徑, 因此第三次的 HTTP 請求可用此 href 值串在目標網址 url 後面對伺服器以 GET 方法提出, 因為請求的資源並非網頁, 而是 pdf 檔, 因此須將取得之回應 byte 內容 (即 Response 物件之 content 屬性值) 以二進位格式 ('rb') 存成 pdf 檔 : 

>>> pdf_url='https://doc.twse.com.tw' + href   
>>> pdf_url      
'https://doc.twse.com.tw/pdf/2022_2330_20230606F04_20240803_233245.pdf'
>>> res=requests.get(pdf_url)   
>>> with open(filename, 'wb') as f:    
    f.write(res.content)    
    
9826260

檢視目前工作目錄下果然出現一個檔名為 2022_2330_20230606F04.pdf 的檔案 :




沒有留言 :