2022年7月15日 星期五

Python 學習筆記 : 從美國 Yahoo Finance 擷取即時股價資訊

最近在練習 Python 網路爬蟲, 前陣子抓過聚財網的每日收盤價用來製作股市看板, 然後利用 Line Notify 推播訊息, 讓沒時間盯盤的人至少能被動地得知關心的股票之今日市況. 今天要練習的科目是從美國 Yahoo Finance 網頁抓取即時股價, 如果價格跌幅超過 2% 就即時發出告警訊息. 

美國 Yahoo Finance 網址如下 :





在搜尋框中輸入股票代號, 例如 2330.TW (台股後面要加 .TW) 即可 : 




可見網頁上會列出查詢當下的股價與漲跌情形 (價格與幅度), 本篇爬蟲程式練習的目標就是要利用 requests 與 BeautifulSoup 套件來擷取其中的股價與漲跌資訊. 

查詢後網址列會顯示此網頁的網址 :

https://finance.yahoo.com/quote/2330.TW?p=2330.TW&.tsrc=fin-srch

最後面 & 帶領的參數可有可無, 所以基本上網址格式就用最短的即可, 格式如下 :

url='https://finance.yahoo.com/quote/{stock_id}?p={stock_id}'

其中 stock_id 為要代入之股票代號. 


1. 擷取股價資訊 : 

首先在所查詢的股價網頁上按滑鼠右鍵, 點選 "檢視網頁原始碼", 然後在開啟的原始碼頁面中按 Ctrl+F 搜尋目前股價 "492.50" : 




可見股價是放在一個自訂標籤 fin-streamer 內, 這時拉曳原始碼頁面的水平卷軸往前, 就可以找到 fin-streamer 標籤的頭 : 




可見這個 fin-streamer 標籤內有一個樣式類別屬性 class="Fw(b) Fz(36px) Mb(-4px) D(ib)", 這是重要的路標資訊 (爬蟲最常用的路標就是 id 與 class 屬性), 可以拿來給 BeautifulSoup 定位目標之用, 完整的 fin-streamer 標籤內容如下 : 

<fin-streamer class="Fw(b) Fz(36px) Mb(-4px) D(ib)" data-symbol="2330.TW" data-test="qsp-price" data-field="regularMarketPrice" data-trend="none" data-pricehint="2" value="492.5" active="">492.50</fin-streamer>

找到路標之後就可以開始來撰寫與測試爬蟲程式了. 

首先匯入 requests 與 BeautifulSoup :

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

然後定義要爬的網址 url, 這是要傳給 requests.get() 當參數的 :

>>> url='https://finance.yahoo.com/quote/2330.TW?p=2330.TW' 

除了 url 外, 還要傳 HTTP 標頭字典給 requests.get() 的 headers 參數, 標頭中最重要的是 User-Agent 鍵 (使用者代理), 用來標示提出請求的用戶端是甚麼應用程式, 必須偽裝成瀏覽器送出請求才不會被對方主機拒絕, 以 Chrome 瀏覽器為例其標頭字典如下 :

>>> headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/102.0.0.0 Safari/537.36'}   

此處因為標頭字串太長了, 所以用長字串連接符 \ 分列串接 (注意, \ 後面要直接跳行, 不可以有空格). 這樣就可以將 url 與 headers 傳入 requests.get() 來提出請求, 然後將回應物件的文字內容傳給 BeautifulSoup 去解析 HTML 結構 :  

>>> r=requests.get(url, headers=headers)       # 傳回 Response 物件
>>> soup=BeautifulSoup(r.text, 'lxml')            # 傳回 BeautifulSoup 物件

然後呼叫 BeautifulSoup 物件的 find() 方法指定 class 樣式去搜尋 fin-streamer 標籤, 找到後會傳回一個 Tag 物件, 其 text 屬性即為該標籤之文字內容, 即所要抓的股價 : 

>>> price=soup.find('fin-streamer', {'class': 'Fw(b) Fz(36px) Mb(-4px) D(ib)'})    
>>> price.text     
'492.50'

茲將上述程式碼撰寫成函式 yahoo_stock_crawler(), 完整程式碼如下 : 


測試 1 : 擷取美國 Yahoo Finance 即時股價 [看原始碼]

# yahoo_finance_stock_price_1.py
import requests
from bs4 import BeautifulSoup

def yahoo_stock_crawler(stock):
    headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/102.0.0.0 Safari/537.36'}
    url=f'https://finance.yahoo.com/quote/{stock}?p={stock}'
    r=requests.get(url, headers=headers)看原始碼
    soup=BeautifulSoup(r.text, 'lxml')
    price=soup.find('fin-streamer', {'class': 'Fw(b) Fz(36px) Mb(-4px) D(ib)'})
    return float(price.text)

price=yahoo_stock_crawler('0050.TW')
print(price)


2. 擷取股價漲跌幅 :

接著要擷取股價後面的漲跌幅資訊, 用目前的 +3.68% 在網頁原始碼頁面中搜尋, 可知此資料被包裹在一個具有 class="C($positiveColor)" 樣式屬性的 span 標籤裡面 :

<span class="C($positiveColor)">(+3.68%)</span>




但是如果只用這個 class 屬性去用 find() 搜尋, 將會找到錯誤的資料, 因為具有 class="C($positiveColor)" 的 span 標籤不只一個, 事實上從上圖可知, 此 span 其實外面還有一個自訂標籤 fin-streamer 包著它, 拉動水平卷軸往前就可看到全貌 : 

<fin-streamer class="Fw(500) Pstart(8px) Fz(24px)" data-symbol="2330.TW" data-field="regularMarketChangePercent" data-trend="txt" data-pricehint="2" data-template="({fmt})" value="0.036842104" active=""><span class="C($positiveColor)">(+3.68%)</span></fin-streamer>

但是具有 class="Fw(500) Pstart(8px) Fz(24px)" 樣式屬性的 fin-streamer 標籤卻有兩個, 第一個就是漲跌價格 (+17.50), 第二個才是漲跌幅 (+3.68%). 如果用 find() 去搜尋有此 class 的 fin-streamer 標籤會找到第一個, 即漲跌價格的 Tag 物件, 例如 :

>>> change=soup.find('fin-streamer', {'class': 'Fw(500) Pstart(8px) Fz(24px)'})    
>>> change   
<fin-streamer active="" class="Fw(500) Pstart(8px) Fz(24px)" data-field="regularMarketChange" data-pricehint="2" data-symbol="2330.TW" data-test="qsp-price-change" data-trend="txt" value="17.5"><span class="C($positiveColor)">+17.50</span></fin-streamer>

但如果是用 soup.findAll() 去搜尋, 就會找到兩個 Tag 物件. 例如 :

>>> changes=soup.findAll('fin-streamer', {'class': 'Fw(500) Pstart(8px) Fz(24px)'}) 
>>> changes   
[<fin-streamer active="" class="Fw(500) Pstart(8px) Fz(24px)" data-field="regularMarketChange" data-pricehint="2" data-symbol="2330.TW" data-test="qsp-price-change" data-trend="txt" value="17.5"><span class="C($positiveColor)">+17.50</span></fin-streamer>, <fin-streamer active="" class="Fw(500) Pstart(8px) Fz(24px)" data-field="regularMarketChangePercent" data-pricehint="2" data-symbol="2330.TW" data-template="({fmt})" data-trend="txt" value="0.036842104"><span class="C($positiveColor)">(+3.68%)</span></fin-streamer>]

我本來只想抓漲跌幅就好, 既然要用 findAll() 抓的話, 乾脆就全都要好了 (已經不是小孩子了, 呵呵), 首先處理漲跌價, 上面的 findAll() 傳回兩個 Tag 元素之串列, 每一個 fin-streamer 的 Tag 裡面包著一個 span 標籤, 因此只要分別用 find() 去搜尋 span 標籤即可取得漲跌價與漲跌幅 :

>>> change_price=changes[0].find('span')    
>>> change_price    
<span class="C($positiveColor)">+17.50</span>
>>> change_price.text    
'+17.50'   
>>> change_quote=changes[1].find('span')   
>>> change_quote       
<span class="C($positiveColor)">(+3.68%)</span>    
>>> change_quote.text   
'(+3.68%)'

茲修改上面範例, 增加擷取漲跌價與漲跌幅資訊, 完整程式碼如下 : 


測試 2 : 擷取美國 Yahoo Finance 即時股價與漲跌價&漲跌幅 [看原始碼]

# yahoo_finance_stock_price_2.py
import requests
from bs4 import BeautifulSoup

def yahoo_stock_crawler(stock):
    headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/102.0.0.0 Safari/537.36'}
    url=f'https://finance.yahoo.com/quote/{stock}?p={stock}'
    r=requests.get(url, headers=headers)
    soup=BeautifulSoup(r.text, 'lxml')
    price=soup.find('fin-streamer', {'class': 'Fw(b) Fz(36px) Mb(-4px) D(ib)'})
    changes=soup.findAll('fin-streamer', {'class': 'Fw(500) Pstart(8px) Fz(24px)'})
    change_price=changes[0].find('span').text
    change_quote=changes[1].find('span').text
    return float(price.text), change_price, change_quote

price, change_price, change_quote=yahoo_stock_crawler('2330.TW')
print(f'{price} {change_price} {change_quote}')

執行結果如下 :

>>> %Run yahoo_finance_stock_price_2.py    
492.5 +17.50 (+3.68%)


3. 撰寫盯盤程式 :

有了上面基礎後就可以來撰寫一個盯盤程式, 基本構想是將要盯的股票代號放在串列中, 然後用迴圈來走訪這些股票, 呼叫上面的爬蟲程式取得即時股價與漲跌幅, 若超過指定震盪幅度 (例如 2%) 就用 Line Notify 發出告警通知. 

將上面的 yahoo_finance_stock_price_2.py 複製為 yahoo_finance_stock_price_3.py 後進行功能增修, 新增匯入 re, time, 與 random 這三個模組, re 是用來擷取漲跌幅數據之用, 因為從漲跌幅字串 '(+3.68%)' 中取出浮點數 3.68 對正規表達式而言是輕而易舉之事. random 與 time 是用來偽裝, 為了避免被鎖定, 每個迴圈之間的爬蟲間隔要隨機且不要太緊湊, 所以要用到 random.randomint() 與 time.sleep() 來偽裝成無規律的請求. 完整程式碼如下 : 


測試 3 : 追蹤即時股價並在漲跌幅超過 2% 時發出 Line Notify 告警 [看原始碼]

import re
import time
import random
import requests
from bs4 import BeautifulSoup

def yahoo_stock_crawler(stock):
    headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
                           AppleWebKit/537.36 (KHTML, like Gecko) \
                           Chrome/102.0.0.0 Safari/537.36'}
    url=f'https://finance.yahoo.com/quote/{stock}?p={stock}'
    r=requests.get(url, headers=headers)
    soup=BeautifulSoup(r.text, 'lxml')
    price=soup.find('fin-streamer', {'class': 'Fw(b) Fz(36px) Mb(-4px) D(ib)'})
    changes=soup.findAll('fin-streamer', {'class': 'Fw(500) Pstart(8px) Fz(24px)'})
    change_price=changes[0].find('span').text
    change_quote=changes[1].find('span').text
    return float(price.text), change_price, change_quote

def notify(msg, token):
    url="https://notify-api.line.me/api/notify"
    headers={"Authorization": "Bearer " + token}
    payload={"message": msg}
    r=requests.post(url, headers=headers, params=payload)
    return "訊息發送成功!"

stocks=['0050', '0056', '2330', '2303', '2412', '00894']   # 要盯的股票列表
alert_msg_list=['\n注意! 盤中漲跌幅超過 2% :']    # 預設警訊
for stock in stocks:
    id=stock + '.TW'    # 後面補上 '.TW'
    price, change_price, change_quote=yahoo_stock_crawler(id)   # 擷取股價與漲跌幅
    alert_msg=f'{stock} {price} {change_price} {change_quote}'   # 告警字串
    print(alert_msg)
    change_quote2=float(re.search("\d+\.\d+", change_quote).group())   # 取出數字
    if change_quote2 > 2 or change_quote2 < -2:   # 漲跌 2% 以上
        alert_msg_list.append(alert_msg)    # 紀錄告警字串
    time.sleep(random.randint(1, 5))     # 隨機間隔 1~5 秒再抓下一支
print(alert_msg_list)
if len(alert_msg_list) > 1:   # =1 表示只有預設警訊: 不推播
    msg='\n'.join(alert_msg_list)     # 串接超過 2% 的各股警訊
    token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'
    notify(msg, token)
   
此處使用正規式 "\d+\.\d+" 從漲跌幅字串 (+3.68%) 中取出浮點數 3.68, re.search() 會傳回一個 Match 物件, 呼叫此物件的 group() 方法即可取得匹配的字串, 最後用 float() 將其轉成浮點數. 關於正規表達式用法參考 :


執行結果如下 : 

>>> %Run yahoo_finance_stock_price_3.py
0050 115.5 +1.50 (+1.32%)
0056 27.09 -0.09 (-0.33%)
2330 492.5 +17.50 (+3.68%)   
2303 38.95 +0.15 (+0.39%)
2412 123.0 0.00 (0.00%)
00894 11.41 +0.23 (+2.06%)    
['\n注意! 盤中漲跌幅超過 2% :', '2330 492.5 +17.50 (+3.68%)', '00894 11.41 +0.23 (+2.06%)']

手機收到的 Line 推播 :




關於 Line Notify 用法參考 :



4. 佈署到樹莓派 :

最後是將上面的程式 (我改名為 yahoo_finance_twstock_monitor.py) 用 WinSCP 傳送到樹莓派, 用 chmod 指令將此程式更改為可執行 : 

pi@raspberrypi:~ $ sudo chmod +x /home/pi/yahoo_finance_twstock_monitor.py
pi@raspberrypi:~ $ ls yahoo_finance_twstock_monitor.py -ls
4 -rwxr-xr-x 1 pi pi 1719  7月 16 10:55 yahoo_finance_twstock_monitor.py

然後用 crontab -e 編輯 Crontab 加入如下指令 : 

*/5 9-14 * * 1-5 /usr/bin/python3 /home/pi/yahoo_finance_twstock_monitor.py  

其中*/5 表示每 5 分鐘執行一次, 9-14 表示從早上 9 點到下午 2 點, 1-5 表示周一到周五. 

pi@raspberrypi:~ $ crontab -e   
crontab: installing new crontab  
pi@raspberrypi:~ $ crontab -l   
0 16-17 * * 1-5 /usr/bin/python3 /home/pi/twstock_dashboard_update.py
*/5 9-14 * * 1-5 /usr/bin/python3 /home/pi/yahoo_finance_twstock_monitor.py

關於 Crontab 用法參考 :



2022-07-16 補充 :

其實也可以爬台灣的 Yahoo 股市, 其網址較短不需要加 '.TW', 例如 :


有空時再把上面程式修改為台灣 Yahoo 版. 

沒有留言 :