2025年8月30日 星期六

Python 學習筆記 : 股票爬蟲-個股每日本益比殖利率與股價淨值比

證交所提供完整且及時之股票資料, 大部分都可以使用 requests + BeautifulSoup 抓取, 經過清理後可用來進行量化分析 (但要注意爬取頻率不要太高, 否則會被反爬蟲程式鎖 IP 阻擋一段時間). 

本篇測試是閱讀劉承彥老師寫的 "Python股票演算法交易實務 147 個關鍵技巧詳解" (第二版, 博碩 2021, 此書已經絕版) 第二章技巧 30 的實測紀錄. 

本系列爬蟲測試筆記索引參考 :


證交所的個股日本益比, 殖利率及股價淨值比查詢網址如下 : 


在上方選擇資料年月, 輸入股票代號按 "查詢" 就會顯示該標的的全月之日資料 :





利用瀏覽器的開發者工具分析, 此網頁是透過下列網址從後端伺服器取得資料 :


注意, date 參數可傳入該月之任一日期, 不管填哪一日期都會傳回當月之每日資料 (JSON 格式字串), 例如 :

{"stat":"OK","date":"20250801","title":"114年08月 台積電           個股日本益比、殖利率及股價淨值比(以個股月查詢)","fields":["日期","殖利率(%)","股利年度","本益比","股價淨值比","財報年/季"],"data":[["114年08月01日","1.49",113,"22.58","6.48","114/1"],["114年08月04日","1.50",113,"22.48","6.45","114/1"],["114年08月05日","1.48",113,"22.78","6.53","114/1"],["114年08月06日","1.51",113,"22.28","6.39","114/1"],["114年08月07日","1.44",113,"23.37","6.70","114/1"],["114年08月08日","1.45",113,"23.27","6.68","114/1"],["114年08月11日","1.44",113,"23.37","6.70","114/1"],["114年08月12日","1.44",113,"23.37","6.70","114/1"],["114年08月13日","1.42",113,"23.77","6.82","114/1"],["114年08月14日","1.45",113,"20.87","6.65","114/2"],["114年08月15日","1.44",113,"20.96","6.68","114/2"],["114年08月18日","1.44",113,"20.96","6.68","114/2"],["114年08月19日","1.43",113,"21.05","6.71","114/2"],["114年08月20日","1.50",113,"20.16","6.43","114/2"],["114年08月21日","1.48",113,"20.43","6.51","114/2"],["114年08月22日","1.50",113,"20.16","6.43","114/2"],["114年08月25日","1.45",113,"20.79","6.62","114/2"],["114年08月26日","1.45",113,"20.87","6.65","114/2"],["114年08月27日","1.43",113,"21.14","6.74","114/2"],["114年08月28日","1.47",113,"20.61","6.57","114/2"],["114年08月29日","1.47",113,"20.61","6.57","114/2"]],"total":21}

這就是用來渲染上面網頁所需的資料. 這個網址格式可寫成如下 f  字串 :

f'https://www.twse.com.tw/exchangeReport/BWIBBU?response=json&date={date}&stockNo={stock_no}'

這個 JSON 資料可以用 json 套件轉成 Python 字典方便後續處理 (例如轉成 DataFrame). 首先匯入 requests 與 json 套件 : 

>>> import requests, json  

定義日期與股票待號變數 :

>>> date='20250801'  
>>> stock_no='2330'   

然後將這兩個參數傳入資料取得網址的 f 字串中 : 

>>> url=f'https://www.twse.com.tw/exchangeReport/BWIBBU?response=json&date={date}&stockNo={stock_no}'   
>>> url  
'https://www.twse.com.tw/exchangeReport/BWIBBU?response=json&date=20250801&stockNo=2330'

這樣便可以用 requests.get() 取得資料了 :

>>> res=requests.get(url)   

爬取的資料是 JSON 格式的字串, 放在回應物件的 text 屬性中, 可以用 json.loads() 函式將此字串轉成 Python 字典, 用法參考 :


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

可以用 rich 套件的 print() 以結構化形式顯示字典內容 : 

>>> from rich import print as pprint   
>>> pprint(dic_obj)    
{
    'stat': 'OK',
    'date': '20250801',
    'title': '114年08月 台積電           
個股日本益比、殖利率及股價淨值比(以個股月查詢)',
    'fields': [
        '日期',
        '殖利率(%)',
        '股利年度',
        '本益比',
        '股價淨值比',
        '財報年/季'
    ],
    'data': [
        ['114年08月01日', '1.49', 113, '22.58', '6.48', '114/1'],
        ['114年08月04日', '1.50', 113, '22.48', '6.45', '114/1'],
        ['114年08月05日', '1.48', 113, '22.78', '6.53', '114/1'],
        ['114年08月06日', '1.51', 113, '22.28', '6.39', '114/1'],
        ['114年08月07日', '1.44', 113, '23.37', '6.70', '114/1'],
        ['114年08月08日', '1.45', 113, '23.27', '6.68', '114/1'],
        ['114年08月11日', '1.44', 113, '23.37', '6.70', '114/1'],
        ['114年08月12日', '1.44', 113, '23.37', '6.70', '114/1'],
        ['114年08月13日', '1.42', 113, '23.77', '6.82', '114/1'],
        ['114年08月14日', '1.45', 113, '20.87', '6.65', '114/2'],
        ['114年08月15日', '1.44', 113, '20.96', '6.68', '114/2'],
        ['114年08月18日', '1.44', 113, '20.96', '6.68', '114/2'],
        ['114年08月19日', '1.43', 113, '21.05', '6.71', '114/2'],
        ['114年08月20日', '1.50', 113, '20.16', '6.43', '114/2'],
        ['114年08月21日', '1.48', 113, '20.43', '6.51', '114/2'],
        ['114年08月22日', '1.50', 113, '20.16', '6.43', '114/2'],
        ['114年08月25日', '1.45', 113, '20.79', '6.62', '114/2'],
        ['114年08月26日', '1.45', 113, '20.87', '6.65', '114/2'],
        ['114年08月27日', '1.43', 113, '21.14', '6.74', '114/2'],
        ['114年08月28日', '1.47', 113, '20.61', '6.57', '114/2'],
        ['114年08月29日', '1.47', 113, '20.61', '6.57', '114/2']
    ],
    'total': 21
}

可見目標資料放在 data 屬性中 : 

>>> pprint(dic_obj['data'])  
[
    ['114年08月01日', '1.49', 113, '22.58', '6.48', '114/1'],
    ['114年08月04日', '1.50', 113, '22.48', '6.45', '114/1'],
    ['114年08月05日', '1.48', 113, '22.78', '6.53', '114/1'],
    ['114年08月06日', '1.51', 113, '22.28', '6.39', '114/1'],
    ['114年08月07日', '1.44', 113, '23.37', '6.70', '114/1'],
    ['114年08月08日', '1.45', 113, '23.27', '6.68', '114/1'],
    ['114年08月11日', '1.44', 113, '23.37', '6.70', '114/1'],
    ['114年08月12日', '1.44', 113, '23.37', '6.70', '114/1'],
    ['114年08月13日', '1.42', 113, '23.77', '6.82', '114/1'],
    ['114年08月14日', '1.45', 113, '20.87', '6.65', '114/2'],
    ['114年08月15日', '1.44', 113, '20.96', '6.68', '114/2'],
    ['114年08月18日', '1.44', 113, '20.96', '6.68', '114/2'],
    ['114年08月19日', '1.43', 113, '21.05', '6.71', '114/2'],
    ['114年08月20日', '1.50', 113, '20.16', '6.43', '114/2'],
    ['114年08月21日', '1.48', 113, '20.43', '6.51', '114/2'],
    ['114年08月22日', '1.50', 113, '20.16', '6.43', '114/2'],
    ['114年08月25日', '1.45', 113, '20.79', '6.62', '114/2'],
    ['114年08月26日', '1.45', 113, '20.87', '6.65', '114/2'],
    ['114年08月27日', '1.43', 113, '21.14', '6.74', '114/2'],
    ['114年08月28日', '1.47', 113, '20.61', '6.57', '114/2'],
    ['114年08月29日', '1.47', 113, '20.61', '6.57', '114/2']
]

而欄位則是放在 fields 屬性中 : 

>>> dic_obj['fields']   
['日期', '殖利率(%)', '股利年度', '本益比', '股價淨值比', '財報年/季']

可以呼叫 open() 函式將此字典存入檔案 :

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

如果要讀回成字典則可呼叫 json.load() :

>>> with open('twse_per_yield_pbr.json', 'r', encoding='utf-8') as f:   
    dic_obj=json.load(f)     

但通常會存入資料庫, 存取較方便. 

以上測試之完整程式碼如下 :

# twse_per_yield_pbr.py
import requests
import json
from fake_useragent import UserAgent

def get_per_yield_pbr(date, stock_no):
    url=f'https://www.twse.com.tw/exchangeReport/BWIBBU?response=json&date={date}&stockNo={stock_no}'
    ua=UserAgent()
    headers={'User-Agent': ua.random}
    res=requests.get(url, headers=headers)
    if res.status_code == requests.codes.ok:
        data=json.loads(res.text)
        return data
    else:
        print(f'Error: {res.status_code}, Response: {res.text}')
        return False

if __name__ == '__main__':
    date=input('請輸入日期 (格式 YYYYMMDD) :')
    stock_no=input('請輸入股票代號 (例如 2330) :')
    data=get_per_yield_pbr(date, stock_no)
    if data:
        print(data['fields'])
        print(data['data'])
    else:
        print('爬取網頁失敗')

執行結果如下 :

>> %Run twse_per_yield_pbr.py   
請輸入日期 (格式 YYYYMMDD) :20250801
請輸入股票代號 (例如 2330) :2330
['日期', '殖利率(%)', '股利年度', '本益比', '股價淨值比', '財報年/季']
[['114年08月01日', '1.49', 113, '22.58', '6.48', '114/1'], ['114年08月04日', '1.50', 113, '22.48', '6.45', '114/1'], ['114年08月05日', '1.48', 113, '22.78', '6.53', '114/1'], ['114年08月06日', '1.51', 113, '22.28', '6.39', '114/1'], ['114年08月07日', '1.44', 113, '23.37', '6.70', '114/1'], ['114年08月08日', '1.45', 113, '23.27', '6.68', '114/1'], ['114年08月11日', '1.44', 113, '23.37', '6.70', '114/1'], ['114年08月12日', '1.44', 113, '23.37', '6.70', '114/1'], ['114年08月13日', '1.42', 113, '23.77', '6.82', '114/1'], ['114年08月14日', '1.45', 113, '20.87', '6.65', '114/2'], ['114年08月15日', '1.44', 113, '20.96', '6.68', '114/2'], ['114年08月18日', '1.44', 113, '20.96', '6.68', '114/2'], ['114年08月19日', '1.43', 113, '21.05', '6.71', '114/2'], ['114年08月20日', '1.50', 113, '20.16', '6.43', '114/2'], ['114年08月21日', '1.48', 113, '20.43', '6.51', '114/2'], ['114年08月22日', '1.50', 113, '20.16', '6.43', '114/2'], ['114年08月25日', '1.45', 113, '20.79', '6.62', '114/2'], ['114年08月26日', '1.45', 113, '20.87', '6.65', '114/2'], ['114年08月27日', '1.43', 113, '21.14', '6.74', '114/2'], ['114年08月28日', '1.47', 113, '20.61', '6.57', '114/2'], ['114年08月29日', '1.47', 113, '20.61', '6.57', '114/2']]

此程式使用第三方套件 fake_useragent 產生 HTTP 標頭的 User-Agent 用來偽裝成一般瀏覽器, 避免被證交所伺服器反爬蟲程式阻擋, 用法參考 :


沒有留言 :