2025年4月17日 星期四

Python 學習筆記 : 用 Telegram 傳送股價訊息

今天終於把 Telegram Bot API 我用得到的主要功能都測完了, 終於可以用這成果來改寫我前年寫的股價爬蟲程式了, 自從 LINE Notify 於 4/1 終止服務後它也就跟著停擺了, 只好轉換平台到 Telegram. 關於 Telegram Bot API 用法參考下列索引 :


爬蟲程式都是在樹莓派 Pi 3 與 Mapleboard 的 Linux 主機上透過 crontab 定時週期性執行, 所以首先要在 Pi 3 上安裝 python-telegram-bot 套件 : 

pip3 install python-telegram-bot  
  
pi@raspberrypi:~ $ pip3 install python-telegram-bot    
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting python-telegram-bot
  Downloading https://files.pythonhosted.org/packages/86/ea/52fc452521483e7e31138d4d58e29b00ca3de07085bff11a823a41d56e03/python_telegram_bot-20.3-py3-none-any.whl (545kB)
Collecting httpx~=0.24.0 (from python-telegram-bot)
  Downloading https://files.pythonhosted.org/packages/ec/91/e41f64f03d2a13aee7e8c819d82ee3aa7cdc484d18c0ae859742597d5aa0/httpx-0.24.1-py3-none-any.whl (75kB)
 Requirement already satisfied: sniffio in ./.local/lib/python3.7/site-packages (from httpx~=0.24.0->python-telegram-bot) (1.3.1)
Requirement already satisfied: idna in /usr/lib/python3/dist-packages (from httpx~=0.24.0->python-telegram-bot) (2.6)
Requirement already satisfied: certifi in ./.local/lib/python3.7/site-packages (from httpx~=0.24.0->python-telegram-bot) (2024.2.2)
Collecting httpcore<0.18.0,>=0.15.0 (from httpx~=0.24.0->python-telegram-bot)
  Downloading https://files.pythonhosted.org/packages/94/2c/2bde7ff8dd2064395555220cbf7cba79991172bf5315a07eb3ac7688d9f1/httpcore-0.17.3-py3-none-any.whl (74kB)
Requirement already satisfied: h11<0.15,>=0.13 in ./.local/lib/python3.7/site-packages (from httpcore<0.18.0,>=0.15.0->httpx~=0.24.0->python-telegram-bot) (0.14.0)
Collecting anyio<5.0,>=3.0 (from httpcore<0.18.0,>=0.15.0->httpx~=0.24.0->python-telegram-bot)
  Downloading https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl (80kB)
Requirement already satisfied: typing-extensions; python_version < "3.8" in ./.local/lib/python3.7/site-packages (from h11<0.15,>=0.13->httpcore<0.18.0,>=0.15.0->httpx~=0.24.0->python-telegram-bot) (4.7.1)
Requirement already satisfied: exceptiongroup; python_version < "3.11" in ./.local/lib/python3.7/site-packages (from anyio<5.0,>=3.0->httpcore<0.18.0,>=0.15.0->httpx~=0.24.0->python-telegram-bot) (1.2.1)
anyio 3.7.1 has requirement idna>=2.8, but you'll have idna 2.6 which is incompatible.
Installing collected packages: anyio, httpcore, httpx, python-telegram-bot
Successfully installed anyio-3.7.1 httpcore-0.17.3 httpx-0.24.1 python-telegram-bot-20.3

也可以用下列指令查詢 telegram 套件版本 :

pip list | grep telegram  

pi@raspberrypi:~ $ pip3 list | grep telegram   
python-telegram-bot 20.3    

v20.3 表示必須使用非同步方式呼叫 Bot 物件的方法. 

接下來改寫股價爬蟲程式, 用 Telegram 取代 LINE Notify, 原程式參考 :


將文中的 yahoo_twstock_monitor_table.py 改寫為下面的 yahoo_twstock_monitor_table_2.py : 

# yahoo_twstock_monitor_table_2.py
import time
import random
import requests
from bs4 import BeautifulSoup
import matplotlib.pyplot as plt
from datetime import datetime
import asyncio
from telegram import Bot, InputFile

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.find('h1', class_='C($c-link-text) Fw(b) Fz(24px) Mend(8px)')
    name=tag.text    
    # 擷取股價
    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=tag[0].text.split()[0]
    else:
        trend=''
    return price, change_price, change_quote, trend, name

async def telegram_send_text(text):
    bot=Bot(token=token)
    try:
        await bot.send_message(
            chat_id=chat_id,
            text=text,
            parse_mode='Markdown'
            )
        return True
    except Exception as e:
        print(f'Error sending text: {e}')
        return False

async def telegram_send_photo(photo_path, caption=None):
    bot=Bot(token=token)
    try:
        with open(photo_path, 'rb') as f:
            photo=InputFile(f, filename=photo_path)     # 用 InputFile 包裝本機檔案
            await bot.send_photo(
                chat_id=chat_id,
                photo=photo,
                caption=caption,
                parse_mode='Markdown'
                )
        return True
    except Exception as e:
        print(f'Error sending photo: {e}')
        return False

plt.rcParams["font.family"]=["Microsoft JhengHei"]
plt.figure(tight_layout=True)
plt.axis('off')
columns=["代號", "成交價", "漲跌幅"]
stocks=['0050', '0056', '2330', '2303', '2412', '00894', '00878']
data=[]
msg=['股價趨勢']
for stock in stocks:
    price, change_price, change_quote, trend, name=yahoo_twstock_crawler(stock)
    row=[stock, f'{price} ({change_price})', change_quote[1:-1]]
    print(row, name, trend)
    data.append(row)
    msg.append(f'{stock}({name}): {trend}')
    time.sleep(random.randint(1, 5))
table=plt.table(cellText=data, colLabels=columns, loc="center",
                 colWidths=[0.25, 0.5, 0.25], colColours=['yellow']*3)
table.auto_set_font_size(False)
table.set_fontsize(25)
for key, cell in table.get_celld().items():
    cell.set_linewidth(0.5)
    cell.set_height(0.13) 
    if key[0] != 0:
        if key[0]%2 == 0:                  # 判斷是否為偶數列
            cell.set_facecolor('cyan')
        else:
            cell.set_facecolor('white') 
image='yahoo_twstock_monitor.jpg'
plt.savefig(image)
token='Telegram Bot API Token'
chat_id='Telegram Chat ID'
now_str=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
caption=f'目前股價 {now_str}'
if asyncio.run(telegram_send_photo(image, caption)):
    print('圖片傳送成功!')
else:
    print('圖片傳送失敗!')
text='\n'.join(msg)
if asyncio.run(telegram_send_text(text)):
    print('訊息傳送成功!')
else:
    print('訊息傳送失敗!')    

此程式使用 Bot 物件的 send_message() 與 send_photo() 方法來傳送圖片與文字訊息, 注意, v.20 版以上的 telegram 套件必須使用非同步方式呼叫 Bot 物件的方法. 測試結果如下 : 

>>> %Run yahoo_twstock_monitor_table_2.py   
['0050', '162.35 (-3.00)', '-1.81%'] 元大台灣50 漲→跌
['0056', '32.85 (-0.29)', '-0.88%'] 元大高股息 連4漲→跌
['2330', '855.0 (-22.00)', '-2.51%'] 台積電 漲→跌
['2303', '44.95 (+0.25)', '+0.56%'] 聯電 連5漲
['2412', '128.5 (-0.50)', '-0.39%'] 中華電 漲→跌
['00894', '16.37 (-0.38)', '-2.27%'] 中信小資高價30 連4漲→跌
['00878', '19.99 (-0.18)', '-0.89%'] 國泰永續高股息 連4漲→跌
圖片傳送成功!
訊息傳送成功!

查看 Telegram App 有收到一則圖片訊息與一則文字訊息 : 




將此程式上傳至 Pi 3 後要先用 chmod 修改為可執行 (x 屬性) :

pi@raspberrypi:~ $ sudo chmod +x /home/pi/yahoo_twstock_monitor_table_2.py   
pi@raspberrypi:~ $ ls -l yahoo_twstock_monitor_table_2.py    
-rwxr-xr-x 1 pi pi 4267  4月 17 00:54 yahoo_twstock_monitor_table_2.py

然後修改 crontab, 將原本要執行的程式改為上面這個 yahoo_twstock_monitor_table_2.py 即可定時週期性地收到報價訊息 :

pi@raspberrypi:~ $ crontab -e   
crontab: installing new crontab   
pi@raspberrypi:~ $ crontab -l   
0 16 * * 1-5 /usr/bin/python3 /home/pi/twstock_dashboard_update.py
*/31 9-13 * * 1-5 /usr/bin/python3 /home/pi/yahoo_twstock_monitor_table_2.py
0 8,18 * * * /usr/bin/python3 /home/pi/btc_eth_prices_line_notify.py
1 12,17 * * * /usr/bin/python3 /home/pi/technews_2.py
0 9 * * * /usr/bin/python3 /home/pi/books.com.tw_66.py
0 13 * * * /usr/bin/python3 /home/pi/ksml_books_8.py

關於 crontab 用法參考 :


沒有留言 :