今天在讀 "最強 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> <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), 填入 "資料年度" (民國年), "資料類型" 欄勾選 "股東會相關資料", "資料細節" 欄勾選 "股東會年報" :
按 "查詢" 鈕會載入一個相同網址之第二個網頁, 查詢結果以一個表格呈現 :
可見此表單內有四個隱藏元素, 其傳遞之變數名稱為 step, kind, co_id, 與 filename, 它們會被表單 fm2 傳遞給伺服器 :
這時會傳回第三個網頁顯示要下載的 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 的檔案 :
沒有留言 :
張貼留言