2022年7月18日 星期一

Python 學習筆記 : 從台灣 Yahoo 股市擷取即時股價資訊

周末將 Yahoo Finance 盯盤程式上線, 今天 09:00 開盤卻發現數字不動, 等到 09:20 左右才開始更新, 為何會有時間差呢? 奇怪. 而台灣 Yahoo 股市頁面則準時跳動, 雖然並沒有要隨時盯盤面, 但有時間差總是不完美, 所以決定要更改爬蟲目標為台灣 Yahoo 股市, 網址例如 :


首先檢視原始碼搜尋股價位置 :





可見股價資訊被包在一個 span 標籤中 :

<span class="Fz(32px) Fw(b) Lh(1) Mend(16px) D(f) Ai(c) C($c-trend-up)">496.5</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 標籤用 ▲  表示漲跌 (但網頁原始碼中無法顯示出來) :

<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>

所以同樣用 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 標籤中 :

<span class="Jc(fe) Fz(20px) Lh(1.2) Fw(b) D(f) Ai(c) C($c-trend-up)">(0.61%)</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) 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><span class="Fz(12px) C($c-icon)">連漲連跌</span>

可見這段趨勢說明是放在一個 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 即是如此 : 

<span class="Fz(16px) C($c-link-text) Mb(4px)"><div class="D(f)">連2平 (<span class="D(f) Ai(c)">0.00%</span>)</div></span><span class="Fz(12px) C($c-icon)">連漲連跌</span>

這樣 select() 會傳回一個空串列 : 

>>> tag=soup.select('span[class*="Fz(16px) Mb(4px)"]')    
>>> tag   
[]

所以這部分需要做特別處理, 只有當 Tag 串列不為空時才取趨勢摘要以避免錯誤 : 

>>> if tag:   
        trend_note=tag[0].text.split()[0]   
    else:   
        trend_note=''    

修改上面盯盤程式加入此趨勢摘要後的完整程式碼如下 : 


測試 2 : 擷取台灣 Yahoo 股市即時股價 (加入趨勢摘要) [看原始碼]

# yahoo_twstock_price_2.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})'
    # 擷取趨勢摘要
    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漲']




1 則留言 :

匿名 提到...

https://djinfo.cathaysec.com.tw/z/GetStkRTDataJSON.djjson?B=2330