最近在練習 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 標籤內容如下 :
找到路標之後就可以開始來撰寫與測試爬蟲程式了.
首先匯入 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 標籤裡面 :
但是如果只用這個 class 屬性去用 find() 搜尋, 將會找到錯誤的資料, 因為具有 class="C($positiveColor)" 的 span 標籤不只一個, 事實上從上圖可知, 此 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 版.
沒有留言:
張貼留言