周末將 Yahoo Finance 盯盤程式上線, 今天 09:00 開盤卻發現數字不動, 等到 09:20 左右才開始更新, 為何會有時間差呢? 奇怪. 而台灣 Yahoo 股市頁面則準時跳動, 雖然並沒有要隨時盯盤面, 但有時間差總是不完美, 所以決定要更改爬蟲目標為台灣 Yahoo 股市, 網址例如 :
首先檢視原始碼搜尋股價位置 :
可見股價資訊被包在一個 span 標籤中 :
但是要注意, 此 class 的結尾 C($c-trend-up) 是用來將價格標為紅色 (表示上漲), 如果平盤就沒有這個樣式, 如果股價下跌就會以 C($c-trend-down) 結尾, 價格就會是黑色, 所以這樣就無法用 find() 以確定的 class 來抓股價.
但不管漲跌還是平盤, class 的前面都會有 'Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c)' (也是唯一的), 所以可以用 CSS 選擇器 class*= 來抓, 參考 :
例如 :
>>> import requests
>>> from bs4 import BeautifulSoup
>>> url=f'https://tw.stock.yahoo.com/quote/2330'
>>> 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'}
>>> r=requests.get(url, headers=headers)
>>> soup=BeautifulSoup(r.text, 'lxml')
>>> tag=soup.select('span[class*="Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c)"]')
>>> tag
[<span class="Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c) C($c-trend-up)">496.5</span>]
>>> tag[0].text
'496.5'
因為 *= 是模糊比對, 所以結果即使只有一個也是放在串列中傳回, 必須用索引 [0] 來取得第一個元素. 漲跌價也是放在 span 標籤裡面, 其 class 也是唯一的, 但與上面的股價一樣也是在結尾的地方用 C($c-trend-up) 或 C($c-trend-down) 標示漲跌價的顏色 (平盤則無), 而且內層還有一個 span 標籤用 ▲ ▼ 表示漲跌 (但網頁原始碼中無法顯示出來) :
所以同樣用 select() 以 class*= 選擇器進行模糊比對 :
>>> tag=soup.select('span[class*="Fz(20px) Fw(b) Lh(1.2) Mend(4px) D(f) Ai(c)"]')
>>> tag
[<span class="Fz(20px) Fw(b) Lh(1.2) Mend(4px) D(f) Ai(c) C($c-trend-up)"><span class="Mend(4px) Bds(s)" style="border-color:transparent transparent #ff333a transparent;border-width:0 6.5px 9px 6.5px"></span>3.0</span>]
>>> tag[0]
<span class="Fz(20px) Fw(b) Lh(1.2) Mend(4px) D(f) Ai(c) C($c-trend-up)"><span class="Mend(4px) Bds(s)" style="border-color:transparent transparent #ff333a transparent;border-width:0 6.5px 9px 6.5px"></span>3.0</span>
>>> tag[0].text
'3.0'
由於它採用上下三角形標示漲跌, 而不是像 Yahoo Finance 那樣直接將 +- 冠在漲跌價前面, 要如何取得是漲是跌資訊呢? 具體而言, 就是想複製出 Yahoo Finance 那樣 +3.0 的效果, 作法是從 Tag 物件的 attrs 著手, 此屬性之值為一個字典, 裡面儲存該標籤的所有屬性 :
>>> tag[0].attrs
{'class': ['Fz(20px)', 'Fw(b)', 'Lh(1.2)', 'Mend(4px)', 'D(f)', 'Ai(c)', 'C($c-trend-up)']}
這個 span 標籤只有一個 class 屬性, 所以可以用 ['class'] 取得其值 (型態為串列) :
>>> tag[0].attrs['class']
['Fz(20px)', 'Fw(b)', 'Lh(1.2)', 'Mend(4px)', 'D(f)', 'Ai(c)', 'C($c-trend-up)']
如果串列中包含 'C($c-trend-up)' 就在漲跌價前面冠上 '+'; 如果串列中包含 'C($c-trend-down)' 就在漲跌價前面冠上 '-', 例如 :
>>> if 'C($c-trend-up)' in tag[0].attrs['class']:
change='+' + tag[0].text
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change='-' + tag[0].text
else:
change=tag[0].text
>>> change
'+3.0'
接下來是抓漲跌幅, 這比較簡單, 它也是放在 span 標籤中 :
同樣地, 它也是用 'C($c-trend-up)' 與 'C($c-trend-down)' 樣式來標示漲跌而不是正負號, 處理方法與上面漲跌價類似, 也是用 select() 以 class*= 選擇器去做模糊比對 :
>>> tag=soup.select('span[class*="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c)"]')
>>> tag
[<span class="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c) C($c-trend-up)">(0.61%)</span>]
>>> tag[0]
<span class="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c) C($c-trend-up)">(0.61%)</span>
>>> tag[0].text
'(0.61%)'
>>> tag[0].attrs
{'class': ['Jc(fe)', 'Fz(20px)', 'Lh(1.2)', 'Fw(b)', 'D(f)', 'Ai(c)', 'C($c-trend-up)']}
>>> tag[0].attrs['class']
['Jc(fe)', 'Fz(20px)', 'Lh(1.2)', 'Fw(b)', 'D(f)', 'Ai(c)', 'C($c-trend-up)']
所以複製出像 Yahoo Finance 那樣用 (+0.61%) 表示的作法也是類似, 但漲跌幅外面有用括號包住, 需要先取出括號內的百分比數據, 這可以用切片 [1:-1] 輕易取得 :
>>> change_quote=tag[0].text[1:-1]
>>> change_quote
'0.61%'
這樣就可以
>>> if 'C($c-trend-up)' in tag[0].attrs['class']:
change_quote=f'(+{change_quote})'
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change_quote=f'(-{change_quote})'
else:
change_quote=f'({change_quote})'
>>> change_quote
'(+0.61%)'
這樣就完成了, 完整程式碼如下 :
測試 1 : 擷取台灣 Yahoo 股市即時股價 [看原始碼]
# yahoo_twstock_price_1.py
import re
import time
import random
import requests
from bs4 import BeautifulSoup
def yahoo_twstock_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://tw.stock.yahoo.com/quote/{stock}'
r=requests.get(url, headers=headers)
soup=BeautifulSoup(r.text, 'lxml')
# 擷取股價
tag=soup.select('span[class*="Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c)"]')
price=float(tag[0].text)
# 擷取漲跌價
tag=soup.select('span[class*="Fz(20px) Fw(b) Lh(1.2) Mend(4px) D(f) Ai(c)"]')
if 'C($c-trend-up)' in tag[0].attrs['class']:
change_price='+' + tag[0].text
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change_price='-' + tag[0].text
else:
change_price=tag[0].text
# 擷取漲跌幅
tag=soup.select('span[class*="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c)"]')
change_quote=tag[0].text[1:-1]
if 'C($c-trend-up)' in tag[0].attrs['class']:
change_quote=f'(+{change_quote})'
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change_quote=f'(-{change_quote})'
else:
change_quote=f'({change_quote})'
return price, 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:
price, change_price, change_quote=yahoo_twstock_crawler(stock)
alert_msg=f'{stock} {price} {change_price} {change_quote}'
print(alert_msg)
change_quote2=float(re.search(r'\d+\.\d+', change_quote).group())
if change_quote2 > 2 or change_quote2 < -2:
alert_msg_list.append(alert_msg)
time.sleep(random.randint(1, 5))
print(alert_msg_list)
if len(alert_msg_list) > 1:
msg='\n'.join(alert_msg_list)
token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'
notify(msg, token)
結果如下 :
>>> %Run yahoo_twstock_monitor.py
0050 114.2 +0.50 (+0.44%)
0056 27.48 +0.39 (+1.44%)
2330 495.5 +3.0 (+0.61%)
2303 40.0 +1.05 (+2.70%)
2412 123.0 0.0 (0.00%)
00894 11.56 +0.15 (+1.31%)
['\n注意! 盤中漲跌幅超過 2% :', '2303 40.0 +1.05 (+2.70%)']
Bingo!
其實仔細看台灣雅虎股市網頁, 會發現它還有一個 Yahoo Finance 沒有的資訊, 那就是在 "加入自選股" 按鈕下方簡短的最近股價走勢摘要, 例如 '連三漲' :
原始碼如下 :
可見這段趨勢說明是放在一個 span 標籤中, 其 class 以 'Fz(16px) Mb(4px)' 開頭, 同樣會根據漲跌情形在 class 屬性結尾處添加 'C($c-trend-up)' 或 'C($c-trend-down)' 樣式, 後面還有一些附屬資訊例如連續累積漲幅等, 直接用 select() 以 class*= 選擇器去做模糊比對即可抓到整組資料 :
>>> tag=soup.select('span[class*="Fz(16px) Mb(4px)"]')
>>> tag
[<span class="Fz(16px) Mb(4px) C($c-trend-up)"><div class="D(f)">連3漲 (<span class="D(f) Ai(c)"><span class="Mend(4px) Bds(s)" style="border-color:transparent transparent #ff333a transparent;border-width:0 5px 7px 5px"></span>9.57%</span>)</div></span>]
>>> tag[0]
<span class="Fz(16px) Mb(4px) C($c-trend-up)"><div class="D(f)">連3漲 (<span class="D(f) Ai(c)"><span class="Mend(4px) Bds(s)" style="border-color:transparent transparent #ff333a transparent;border-width:0 5px 7px 5px"></span>9.57%</span>)</div></span>
>>> tag[0].text
'連3漲 (9.57%)'
>>> tag[0].text.split()[0] # 只取前面的摘要, 避免資訊過多
'連3漲'
可見 text 屬性會把內層的 HTML 碼全部略掉只剩下資料內容 (9.57%) 而已, 如果要在漲跌幅數據前面加上 + 或 - 號就要像上面那樣判斷 class 屬性值是否有 'C($c-trend-up)' 或 'C($c-trend-down)' 樣式, 雖然也不會很麻煩, 但這樣其實資訊太多了, 乾脆只取前面的中文摘要就好, 這只要用 split() 取出索引 0 即可.
不過經測試發現如果趨勢摘要是平盤例如 '連3平' 或 '漲→平' 的時候, class 屬性值的順序居然不同, 'Mb(4px)' 被放到最後面, 中間插入 'C($c-link-text)' 樣式, 例如今日 2412 即是如此 :
這樣 select() 會傳回一個空串列 :
>>> tag=soup.select('span[class*="Fz(16px) Mb(4px)"]')
>>> tag
[]
所以這部分需要做特別處理, 只有當 Tag 串列不為空時才取趨勢摘要以避免錯誤 :
>>> if tag:
trend_note=tag[0].text.split()[0]
else:
trend_note=''
修改上面盯盤程式加入此趨勢摘要後的完整程式碼如下 :
import re
import time
import random
import requests
from bs4 import BeautifulSoup
def yahoo_twstock_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://tw.stock.yahoo.com/quote/{stock}'
r=requests.get(url, headers=headers)
soup=BeautifulSoup(r.text, 'lxml')
# 擷取股價
tag=soup.select('span[class*="Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c)"]')
price=float(tag[0].text)
# 擷取漲跌價
tag=soup.select('span[class*="Fz(20px) Fw(b) Lh(1.2) Mend(4px) D(f) Ai(c)"]')
if 'C($c-trend-up)' in tag[0].attrs['class']:
change_price='+' + tag[0].text
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change_price='-' + tag[0].text
else:
change_price=tag[0].text
# 擷取漲跌幅
tag=soup.select('span[class*="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c)"]')
change_quote=tag[0].text[1:-1]
if 'C($c-trend-up)' in tag[0].attrs['class']:
change_quote=f'(+{change_quote})'
elif 'C($c-trend-down)' in tag[0].attrs['class']:
change_quote=f'(-{change_quote})'
else:
change_quote=f'({change_quote})'
# 擷取趨勢摘要
tag=soup.select('span[class*="Fz(16px) Mb(4px)"]')
if tag:
trend_note=tag[0].text.split()[0]
else:
trend_note=''
return price, change_price, change_quote, trend_note
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:
price, change_price, change_quote, trend_note=yahoo_twstock_crawler(stock)
alert_msg=f'{stock} {price} {change_price} {change_quote} {trend_note}'
print(alert_msg)
change_quote2=float(re.search(r'\d+\.\d+', change_quote).group())
if change_quote2 > 2 or change_quote2 < -2:
alert_msg_list.append(alert_msg)
time.sleep(random.randint(1, 5))
print(alert_msg_list)
if len(alert_msg_list) > 1:
msg='\n'.join(alert_msg_list)
token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'
notify(msg, token)
結果如下 :
>>> %Run yahoo_twstock_price_2.py
0050 114.2 +0.50 (+0.44%) 連4漲
0056 27.48 +0.39 (+1.44%) 跌→漲
2330 495.5 +3.0 (+0.61%) 連4漲
2303 40.0 +1.05 (+2.70%) 連4漲
2412 123.0 0.0 (0.00%)
00894 11.56 +0.15 (+1.31%) 連4漲
['\n注意! 盤中漲跌幅超過 2% :', '2303 40.0 +1.05 (+2.70%) 連4漲']
https://djinfo.cathaysec.com.tw/z/GetStkRTDataJSON.djjson?B=2330
回覆刪除