2025年4月8日 星期二

Python 學習筆記 : 用 Telegram Bot 取代 LINE Notify (三)

因為我的股票爬蟲程式需要傳送圖片到群組, 所以今天先來測試如何用 Telegram Bot API 傳送圖片, 然後再來修改爬蟲程式. 本系列之前的文章參考 : 


我的 Telegram Bot API 權杖與聊天室識別碼都存放在環境變數檔 .env 裡面, 然後用 dotenv 套件載入後用 os 模組讀取出來放在 token 與 chat_id 變數裡 : 

>>> from dotenv import load_dotenv   
>>> load_dotenv()    
True 
>>> import os    
>>> token=os.environ.get('TELEGRAM_TOKEN')   
>>> chat_id=os.environ.get('TELEGRAM_ID')   

參考 :


本篇旨在測試如何使用 HTTP POST 方法將圖片檔案傳送到聊天室裡. 


6. 利用 HTTP 的 POST 方法傳送圖片 :    

發出 HTTP 請求最好用的是 requests 模組, 先匯入 requests :

>>> import requests 

接下來定義網址變數, Telegram Bot API 傳送圖片的 HTTP 網址格式如下 :  

https://api.telegram.org/bot{token}/sendPhoto

>>> url=f'https://api.telegram.org/bot{token}/sendPhoto'    
>>> url    
'https://api.telegram.org/bot<我的權杖>/sendPhoto' 

其中嵌入變數 token 為 Telegram Bot 的權杖. 

首先來傳送網路上的圖片, 例如下面這張 Wiki 上的 Lenna 照片 :





定義圖片網址變數 :

>>> photo = 'https://upload.wikimedia.org/wikipedia/zh/thumb/3/34/Lenna.jpg/300px-Lenna.jpg'    

然後定義 HTTP 請求要攜帶的變數字典 data :

>>> data={'chat_id': chat_id, 'photo': photo}    

其中 chat_id 鍵為聊天室識別碼, photo 鍵為圖片網址, 這樣就可以呼叫 requests 的 post() 函式並傳入 url 與 data 參數提出 POST 請求 : 

>>> r=requests.post(url, data=data)    

傳回值為一個儲存 JSON 格式回應字串的 Response 物件, 呼叫其 json() 方法會將 JSON 字串轉成字典型態傳回 : 

>>> r.json()  
{'ok': True, 'result': {'message_id': 28, 'from': {'id': 7938146214, 'is_bot': True, 'first_name': 'twstock', 'username': 'twstock168_bot'}, 'chat': {'id': <聊天室識別碼>, 'first_name': '<我的名字>', 'username': '<我的 username>', 'type': 'private'}, 'date': 1744102311, 'photo': [{'file_id': 'AgACAgQAAxkDAAMcZ_Tjp_hAlv-3qWVBwQ-jkZQHwv0AApKtMRtu9CVR606iM07xqbsBAAMCAANzAAM2BA', 'file_unique_id': 'AQADkq0xG270JVF4', 'file_size': 1964, 'width': 90, 'height': 90}, {'file_id': 'AgACAgQAAxkDAAMcZ_Tjp_hAlv-3qWVBwQ-jkZQHwv0AApKtMRtu9CVR606iM07xqbsBAAMCAANtAAM2BA', 'file_unique_id': 'AQADkq0xG270JVFy', 'file_size': 21503, 'width': 300, 'height': 300}]}}

可見此字典紀錄了此次傳送之圖檔的識別碼, 尺寸, 檔案大小等資訊. 

這時去查看 Telegram App 就會看到所傳送的圖檔了 :




data 字典也可以用 caption 鍵設定圖片標題, 例如 :

>>> data={'chat_id': chat_id, 'photo': photo, 'caption': 'Lenna'}   
>>> r=requests.post(url, data=data)   

結果如下 :




可見圖片標題是放在底下. 

好站 : Wolke Who Code With AI 的 YT 頻道

今天在找 LINE Notify 替代方案時找到 Wolke 的 YT 頻道 :


裡面有許多與 AI 相關的教學, 值得找時間來看看. 

樹莓派 Pi 3 主機改用 Telegram 傳送外網與內網 IP

我有一台 Pi 3 主機放在鄉下老家, 主要是在跑一堆爬蟲程式與做 IoT 物聯網 Gateway 用, 因我有在它的無線基地台防火牆打洞允許 SSH 從遠端連入, 但必須知道目前光世代浮動的外網 IP, 所以之前寫了一個 Python 程式 reportip4.py 透過 crontab 每小時執行一次, 然後將結果經由 LINE Notify 傳送到 LINE 聊天室, 參考 :


可惜免費的 LINE Notify 已於 4/1 結束服務, 所以我春假連假時研究了一下, 決定改用 Telegram Bot API 來傳訊息, 參考 :


今天把 reportip4.py 改成如下的 Telegram 版 :

# reportip5.py
import socket
import requests

def telegram_msg(token, chat_id, text):
    url='https://api.telegram.org/bot{}/sendMessage'.format(token)
    data={'chat_id': chat_id, 'text': text}
    try:
        with requests.post(url, data=data, timeout=10) as r:
            r.raise_for_status()  # 若 status_code != 2xx 會拋出例外
            result=r.json()
            if result.get('ok'):
                print('Message sent successfully.')
                return True
            else:
                print('Telegram API Error: {}'.format(result))
                return False
    except requests.exceptions.RequestException as e:
        print('Request error: {}'.format(e))
        return False

def private_ip(host_name='google.org'):
    remote_ip=socket.gethostbyname(host_name)
    cs=socket.socket()
    cs.connect((remote_ip, 80))
    return cs.getsockname()[0]

def public_ip():
    hostname='https://api.ipify.org'
    return requests.get(hostname).text

if __name__ == '__main__':
    msg='\n外網: {}\n內網: {}'.format(public_ip(), private_ip())
    token='Telegram 權杖'  
    chat_id='Telegram 聊天室識別碼'  
    result=telegram_msg(token, chat_id, msg)
    if result:
        print('Telegram 訊息發送成功!')
    else:
        print('Telegram 訊息發送失敗!')

由於這台 Pi 的 Python 版本為 3.5, 不支援 f 字串, 所以變數嵌入字串改用字串的 format() 方法, 用 nano 編輯好 reportip5.py 後用 chmod 將其改為可執行, 測試 OK : 

pi@raspberrypi:~ $ nano reportip5.py    
pi@raspberrypi:~ $ ls reportip5.py -ls    
4 -rw-r--r-- 1 pi pi 1334  4月  7 21:53 reportip5.py  
pi@raspberrypi:~ $ sudo chmod +x /home/pi/reportip5.py    
pi@raspberrypi:~ $ ls reportip5.py -ls 
4 -rwxr-xr-x 1 pi pi 1334  4月  7 21:53 reportip5.py  
pi@raspberrypi:~ $ python3 reportip5.py   
Message sent successfully.
Telegram 訊息發送成功!

檢查 Telegram App 果然有收到 IP 訊息 :




接下來修改 crontab, 將原先執行的 reportip4.py 改為 reportip5.py : 

pi@raspberrypi:~ $ sudo crontab -e   
crontab: installing new crontab
pi@raspberrypi:~ $ sudo crontab -l   
0 * * * * sudo /usr/local/bin/checkwifi.sh
0 * * * * /usr/bin/python3 /home/pi/reportip5.py

經過一個晚上, 早上醒來檢視 Telegram 每小時整點都有正常收到訊息 :




哈哈, Telegram 還不錯用!

2025 年第 14 周記事

本周只上三天班就放清明連假了, 好爽! 因菁菁週三晚上要回家, 所以下班就去楠梓載她, 順便將她工作室淘汰的工作椅先放車上, 周四早上載她回楠梓時順路載回鄉下. 周五太陽火熱, 先將椅子用吸塵器吸乾淨, 然後用濕毛巾擦過後放在曬穀場曝曬一整天, 傍晚搬進客廳當沙發用, 坐起來很舒服, 難怪菁菁的客戶一坐下來就很容易睡著, 接睫毛就方便多了. 

上週在 momo 回購的兩個收納櫃載回鄉下後週四下午我就組裝好了, 一個放在廚房, 將置物櫃上亂亂的物品一一整理放進收納櫃, 廚房終於清爽多了. 另一個還沒想好要放哪, 可能放電腦間用來收納我的木工與五金工具. 

本周內訓收穫很多, 三月份請到益師傅來講課, 週三介紹的 Felo.ai 讓我眼睛為之一亮, 它可以將英文影片轉成中文, 這對苦於英文聽力不佳的人來說是一大福音, 我有打算購買單機終生使用版, 約 1 千多塊. 現在 AI 工具零瑯滿目, 每天都有新玩意出現, 學也學不完. 

四天的連假好像一下子就過去了, 這四天裡最駭人的莫過於川普的關稅解放日, 世界股市應聲倒地, 我們被課了 34% 的稅, 但因為連假休市, 累積到周一發作, 一開盤掉 1000 多點, 許多股票直接跌停, 指數瞬間掉了 2000 多點, 我的 0056 也一開盤就 31.66 到收盤, 進場小買三張, 相信未來幾天會續跌. 台積電也是 848 一路到底, 我試著掛 10 股零股, 哈哈, 居然成交! 以前掛過很多次從未成交過, 總算成為台積電股東囉! 

2025年4月7日 星期一

momo買書三本(Stable Diffusion + Gemini)

向市圖借的 "AI 繪圖夢工廠 (旗標)" 本周即將到期, 我上週照書上說明成功地把杰克艾米立的 Stable Diffusion 懶人包安裝好, 讓我信心大振, 但這本書非常熱門, 現在預約要一年後才會輪到, 乾脆去 momo 買一本算了. 後來發現杰克艾米立就有出一本 Stable Diffusion 專書, 那就一起買唄! 順便帶了一本 Google Gemini 的書 : 





總價 2110, 應付 1660, 用掉 momo 幣 234 元, 實付 1432 元 (約 68 折), 花 momo 幣好爽哈哈. 

Python 學習筆記 : 用 Telegram Bot 取代 LINE Notify (二)

在前一篇測試中我們已能順利用 HTTP 請求呼叫 Telegram Bot API 傳送訊息到指定的 Telegram 聊天室, 本篇要改用一個第三方套件 python-telegram-bot 來簡化這個程序. 本系列之前的文章參考 :


以下測試要用到 Telegram Bot API 的權杖 (token) 與聊天室識別碼 (chat_id), 我們先用 dotenv 套件從 .env 中讀取 token 與 chat_id 變數備用 :

>>> from dotenv import load_dotenv   
>>> load_dotenv()    
True 
>>> import os    
>>> token=os.environ.get('TELEGRAM_TOKEN')   
>>> chat_id=os.environ.get('TELEGRAM_ID')   

參考 :



4. 安裝 python-telegram-bot 套件 : 

首先用 pip 安裝 python-telegram-bot 套件 : 

D:\python\test>pip install python-telegram-bot   
Collecting python-telegram-bot
  Downloading python_telegram_bot-22.0-py3-none-any.whl.metadata (17 kB)
Collecting httpx~=0.27 (from python-telegram-bot)
  Downloading httpx-0.28.1-py3-none-any.whl.metadata (7.1 kB)
Requirement already satisfied: anyio in c:\users\tony1\appdata\roaming\python\python310\site-packages (from httpx~=0.27->python-telegram-bot) (3.7.1)
Requirement already satisfied: certifi in c:\users\tony1\appdata\roaming\python\python310\site-packages (from httpx~=0.27->python-telegram-bot) (2023.7.22)
Collecting httpcore==1.* (from httpx~=0.27->python-telegram-bot)
  Downloading httpcore-1.0.7-py3-none-any.whl.metadata (21 kB)
Requirement already satisfied: idna in c:\users\tony1\appdata\roaming\python\python310\site-packages (from httpx~=0.27->python-telegram-bot) (3.4)
Requirement already satisfied: h11<0.15,>=0.13 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from httpcore==1.*->httpx~=0.27->python-telegram-bot) (0.14.0)
Requirement already satisfied: sniffio>=1.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from anyio->httpx~=0.27->python-telegram-bot) (1.3.0)
Requirement already satisfied: exceptiongroup in c:\users\tony1\appdata\roaming\python\python310\site-packages (from anyio->httpx~=0.27->python-telegram-bot) (1.1.3)
Downloading python_telegram_bot-22.0-py3-none-any.whl (673 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 673.5/673.5 kB 3.3 MB/s eta 0:00:00
Downloading httpx-0.28.1-py3-none-any.whl (73 kB)
Downloading httpcore-1.0.7-py3-none-any.whl (78 kB)
Installing collected packages: httpcore, httpx, python-telegram-bot
  Attempting uninstall: httpcore
    Found existing installation: httpcore 0.17.3
    Uninstalling httpcore-0.17.3:
      Successfully uninstalled httpcore-0.17.3
  Attempting uninstall: httpx
    Found existing installation: httpx 0.24.1
    Uninstalling httpx-0.24.1:
      Successfully uninstalled httpx-0.24.1
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
reflex 0.2.8 requires fastapi<0.97.0,>=0.96.0, but you have fastapi 0.115.5 which is incompatible.
reflex 0.2.8 requires httpx<0.25.0,>=0.24.0, but you have httpx 0.28.1 which is incompatible.
reflex 0.2.8 requires pydantic<2.0.0,>=1.10.2, but you have pydantic 2.5.3 which is incompatible.
reflex 0.2.8 requires python-multipart<0.0.6,>=0.0.5, but you have python-multipart 0.0.12 which is incompatible.
reflex 0.2.8 requires typer==0.4.2, but you have typer 0.13.1 which is incompatible.
Successfully installed httpcore-1.0.7 httpx-0.28.1 python-telegram-bot-22.0

這樣就完成安裝了, 版本為 v22. 


5. 利用 Bot 物件的 send_message() 傳送訊息 : 

雖然安裝時的套件名稱是 python-telegram-bot (這是 PyPi 上的 distribution name), 實際使用時則是要匯入 telegram 這個套件名稱 : 

>>> import telegram   

先用 dir() 檢視套件內容 : 

>>> dir(telegram)  
['AffiliateInfo', 'Animation', 'Audio', 'BackgroundFill', 'BackgroundFillFreeformGradient', 'BackgroundFillGradient', 'BackgroundFillSolid', 'BackgroundType', 'BackgroundTypeChatTheme', 'BackgroundTypeFill', 'BackgroundTypePattern', 'BackgroundTypeWallpaper', 'Birthdate', 'Bot', 'BotCommand', 'BotCommandScope', 'BotCommandScopeAllChatAdministrators', 'BotCommandScopeAllGroupChats', 'BotCommandScopeAllPrivateChats', 'BotCommandScopeChat', 'BotCommandScopeChatAdministrators', 'BotCommandScopeChatMember', 'BotCommandScopeDefault', 'BotDescription', 'BotName', 'BotShortDescription', 'BusinessConnection', 'BusinessIntro', 'BusinessLocation', 'BusinessMessagesDeleted', 'BusinessOpeningHours', 'BusinessOpeningHoursInterval', 'CallbackGame', 'CallbackQuery', 'Chat', 'ChatAdministratorRights', 'ChatBackground', 'ChatBoost', 'ChatBoostAdded', 'ChatBoostRemoved', 'ChatBoostSource', 'ChatBoostSourceGiftCode', 'ChatBoostSourceGiveaway', 'ChatBoostSourcePremium', 'ChatBoostUpdated', 'ChatFullInfo', 'ChatInviteLink', 'ChatJoinRequest', 'ChatLocation', 'ChatMember', 'ChatMemberAdministrator', 'ChatMemberBanned', 'ChatMemberLeft', 'ChatMemberMember', 'ChatMemberOwner', 'ChatMemberRestricted', 'ChatMemberUpdated', 'ChatPermissions', 'ChatPhoto', 'ChatShared', 'ChosenInlineResult', 'Contact', 'CopyTextButton', 'Credentials', 'DataCredentials', 'Dice', 'Document', 'EncryptedCredentials', 'EncryptedPassportElement', 'ExternalReplyInfo', 'File', 'FileCredentials', 'ForceReply', 'ForumTopic', 'ForumTopicClosed', 'ForumTopicCreated', 'ForumTopicEdited', 'ForumTopicReopened', 'Game', 'GameHighScore', 'GeneralForumTopicHidden', 'GeneralForumTopicUnhidden', 'Gift', 'Gifts', 'Giveaway', 'GiveawayCompleted', 'GiveawayCreated', 'GiveawayWinners', 'IdDocumentData', 'InaccessibleMessage', 'InlineKeyboardButton', 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio', 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif', 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto', 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo', 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument', 'InlineQueryResultGame', 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', 'InlineQueryResultVoice', 'InlineQueryResultsButton', 'InputContactMessageContent', 'InputFile', 'InputInvoiceMessageContent', 'InputLocationMessageContent', 'InputMedia', 'InputMediaAnimation', 'InputMediaAudio', 'InputMediaDocument', 'InputMediaPhoto', 'InputMediaVideo', 'InputMessageContent', 'InputPaidMedia', 'InputPaidMediaPhoto', 'InputPaidMediaVideo', 'InputPollOption', 'InputSticker', 'InputTextMessageContent', 'InputVenueMessageContent', 'Invoice', 'KeyboardButton', 'KeyboardButtonPollType', 'KeyboardButtonRequestChat', 'KeyboardButtonRequestUsers', 'LabeledPrice', 'LinkPreviewOptions', 'Location', 'LoginUrl', 'MaskPosition', 'MaybeInaccessibleMessage', 'MenuButton', 'MenuButtonCommands', 'MenuButtonDefault', 'MenuButtonWebApp', 'Message', 'MessageAutoDeleteTimerChanged', 'MessageEntity', 'MessageId', 'MessageOrigin', 'MessageOriginChannel', 'MessageOriginChat', 'MessageOriginHiddenUser', 'MessageOriginUser', 'MessageReactionCountUpdated', 'MessageReactionUpdated', 'OrderInfo', 'PaidMedia', 'PaidMediaInfo', 'PaidMediaPhoto', 'PaidMediaPreview', 'PaidMediaPurchased', 'PaidMediaVideo', 'PassportData', 'PassportElementError', 'PassportElementErrorDataField', 'PassportElementErrorFile', 'PassportElementErrorFiles', 'PassportElementErrorFrontSide', 'PassportElementErrorReverseSide', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'PassportFile', 'PersonalDetails', 'PhotoSize', 'Poll', 'PollAnswer', 'PollOption', 'PreCheckoutQuery', 'PreparedInlineMessage', 'ProximityAlertTriggered', 'ReactionCount', 'ReactionType', 'ReactionTypeCustomEmoji', 'ReactionTypeEmoji', 'ReactionTypePaid', 'RefundedPayment', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', 'ReplyParameters', 'ResidentialAddress', 'RevenueWithdrawalState', 'RevenueWithdrawalStateFailed', 'RevenueWithdrawalStatePending', 'RevenueWithdrawalStateSucceeded', 'SecureData', 'SecureValue', 'SentWebAppMessage', 'SharedUser', 'ShippingAddress', 'ShippingOption', 'ShippingQuery', 'StarTransaction', 'StarTransactions', 'Sticker', 'StickerSet', 'Story', 'SuccessfulPayment', 'SwitchInlineQueryChosenChat', 'TelegramObject', 'TextQuote', 'TransactionPartner', 'TransactionPartnerAffiliateProgram', 'TransactionPartnerChat', 'TransactionPartnerFragment', 'TransactionPartnerOther', 'TransactionPartnerTelegramAds', 'TransactionPartnerTelegramApi', 'TransactionPartnerUser', 'Update', 'User', 'UserChatBoosts', 'UserProfilePhotos', 'UsersShared', 'Venue', 'Video', 'VideoChatEnded', 'VideoChatParticipantsInvited', 'VideoChatScheduled', 'VideoChatStarted', 'VideoNote', 'Voice', 'WebAppData', 'WebAppInfo', 'WebhookInfo', 'WriteAccessAllowed', '__all__', '__annotations__', '__author__', '__bot_api_version__', '__bot_api_version_info__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '__version_info__', '_birthdate', '_bot', '_botcommand', '_botcommandscope', '_botdescription', '_botname', '_business', '_callbackquery', '_chat', '_chatadministratorrights', '_chatbackground', '_chatboost', '_chatfullinfo', '_chatinvitelink', '_chatjoinrequest', '_chatlocation', '_chatmember', '_chatmemberupdated', '_chatpermissions', '_choseninlineresult', '_copytextbutton', '_dice', '_files', '_forcereply', '_forumtopic', '_games', '_gifts', '_giveaway', '_inline', '_keyboardbutton', '_keyboardbuttonpolltype', '_keyboardbuttonrequest', '_linkpreviewoptions', '_loginurl', '_menubutton', '_message', '_messageautodeletetimerchanged', '_messageentity', '_messageid', '_messageorigin', '_messagereactionupdated', '_paidmedia', '_passport', '_payment', '_poll', '_proximityalerttriggered', '_reaction', '_reply', '_replykeyboardmarkup', '_replykeyboardremove', '_sentwebappmessage', '_shared', '_story', '_switchinlinequerychosenchat', '_telegramobject', '_update', '_user', '_userprofilephotos', '_utils', '_version', '_videochat', '_webappdata', '_webappinfo', '_webhookinfo', '_writeaccessallowed', 'constants', 'error', 'helpers', 'request', 'warnings']

可見 telegram 套件定義了非常多類別與模組, 其中的 Bot 類別是這個套件的核心, 所以通常只要匯入這個類別即可 : 

>>> type(telegram.Bot)   
<class 'abc.ABCMeta'> 
>>> from telegram import Bot

呼叫這個 Bot 類別的建構式並傳入 token 可以建立一個 Telegram Bot 物件 : 

>>> bot=Bot(token)   
>>> type(bot)    
<class 'telegram._bot.Bot'>  

Bot 物件提供豐富的方法可用來控制機器人做出各種動作, 例如發出訊息, 傳送照片, 編輯訊息等等, 常用的方法如下表 : 


 Bot 物件常用方法  說明
 send_message(chat_id, text)  傳送文字訊息給某個使用者或群組
 send_photo(chat_id, photo, caption=None)  傳送照片, 並可附加說明文字
 send_document(chat_id, document)  傳送文件 (例如 PDF, ZIP 等)
 send_video(chat_id, video)  傳送影片
 send_audio(chat_id, audio)  傳送音訊檔案
 get_me()  取得目前 bot 的基本資訊
 get_updates()  拉取使用者互動 (polling 模式)
 set_webhook(url)  設定 webhook URL 以接收事件
 delete_message(chat_id, message_id)  刪除指定訊息
 edit_message_text(...)  編輯已發送的文字訊息


注意, 這些方法都有駝峰式的別名, 例如 send_message() 的駝峰式別名為 sendMessage(), 呼叫哪一個都可以. 其次, v20 版以後 Bot 物件的所有方法都改用非同步的 async 函式實作, 必須用 await 來執行它, 如果直接呼叫 send_message() 或 sendMessage() 是不會傳送訊息的, 只會傳回一個 coroutine 物件而已 :

>>> bot.send_message(chat_id, text)   
<coroutine object Bot.send_message at 0x00000252D5938190>

呼叫 Bot 物件的 send_message() 前要先建立一個 Application 物件, 此類別位於 telegram.ext 模組內, 呼叫 Application.builder().token(token).build() 函式即可建立一個 Application 物件 : 

>>> from telegram.ext import Application   
>>> app=Application.builder().token(token).build()   
>>> type(app)   
<class 'telegram.ext._application.Application'>   

然後定義一個 async 函式利用 await 來呼叫 Bot 物件的方法, 並透過 asyncio.run() 來呼叫此函式才會真正執行 Bot 物件的方法 : 

>>> async def telegram_text():  
    await app.bot.send_message(chat_id=chat_id, text=text)  

然後匯入 asyncio 模組, 呼叫其 run() 函式來執行非同步函式 :

>>> import asyncio     
>>> asyncio.run(telegram_text())     
Message(channel_chat_created=False, chat=Chat(first_name='<MyName>', id=<MyChatID>, type=<ChatType.PRIVATE>, username='yhhuang1966'), date=datetime.datetime(2025, 4, 7, 7, 27, 47, tzinfo=datetime.timezone.utc), delete_chat_photo=False, from_user=User(first_name='twstock', id=7938146214, is_bot=True, username='twstock168_bot'), group_chat_created=False, message_id=9, supergroup_chat_created=False, text='川普解放日關稅來襲, 今日台股上演大逃殺')

這時 Telegram App 就會收到這則訊息了 : 




以上測試之完整程式碼如下 :

# telegram_send_text_2.py
from telegram import Bot
from telegram.ext import Application
import asyncio

async def telegram_text(chat_id, text):  
    await app.bot.send_message(chat_id=chat_id, text=text)
    
token='我的 Telegram 權杖'
chat_id='我的聊天室識別碼'
text='2025-04-07 台股收盤 19232 點, 跌 2065 點, 跌幅 9.7%'
app=Application.builder().token(token).build()
asyncio.run(telegram_text(chat_id, text))

此例 token 與 chat_id 為全域變數, 其實不需要傳入 telegram_text() 內, 但為了讓其他函式呼叫還是傳入參數較為完整. 執行結果如下 : 



2025年4月6日 星期日

Python 學習筆記 : 用 Telegram Bot 取代 LINE Notify (一)

由於 LINE Notify 服務已於 2025-04-01 終止, 必須為我的眾多爬蟲找尋替代方案, 比較之後覺得還是改用免費的 Telegram 好了, 實作方式也比 LINE 簡單得多, 作法參考 :


Telgram Bot 的優點摘要整理如下 :
  • 免費且無流量限制, 提供完整的通知服務
  • Telegram Bot 設定步驟簡單, 使用 HTTP 呼叫 API 即可傳遞訊息
  • 支援文字, 圖片, 影片, 文件等多媒體格式訊息
  • 支持自定義的訊息格式, 按鈕回應和群組通知, 可應用於即時提醒, 客服回應, 排程通知等
雖然 Telegram 是免費, 但為了防止濫用仍有如下用量之限制 :
  • 每個 Bot 每秒最多只能發出 30 個訊息
  • 每個 Bot 每分鐘最多只能發出 20 個訊息給每個聊天室 (包含群組和頻道)
  • 每個 Bot 每小時最多只能發出 1000 個訊息給每個聊天室 (包含群組和頻道)
  • 每個 Bot 每天最多只能發出 50000 個訊息
參考書籍 :


其他 Telegram Bot 教學 :



1. 安裝 Telegram App : 

建立 Telgram 機器人之前要先安裝 Telgram App, 它就像 LINE 一樣是接收/傳送/顯示即時訊息的 UI 介面. 在手機開啟 Google Play 或 App Store 搜尋 Telegram 後安裝 App (注意, 此 App 沒有中文介面, 用預設的英文介面就好) : 




開啟 Telegram App :




接下來會傳簡訊驗證手機號碼, 按 Continue 繼續 :




要求撥打電話給你, 按 "允許" : 




選擇國家 Taiwan, 輸入手機號碼 :




要求允許讀取通話紀錄以便自動讀取驗證簡訊, 按 Continue : 




輸入姓名按右下角的右箭頭 : 




允許 Telegram 傳送通知 :




這樣就完成 App 的安裝了 : 




按左上角的三條槓點選 Contacts :




按 Continue 允許 Telegram 讀取通訊錄 :




按允許 : 




這樣通訊錄上已有 Telegram 帳戶的人就會出現在 Telegram 聯絡人列表上了 :




可以按右下角的 + 鈕新增連絡人. 


2. 建立 Telegram Bot 取得 API token 與 Chat ID : 

建立 Telgram 機器人先安裝 Telgram App, 到 Google Play 或 App Store 搜尋 Telegram 後安裝 (此 App 沒有中文介面, 用預設的英文介面就好), 完成後執行此 App, 按右上角的搜尋鈕 :




輸入 BatFather 會出現一堆 BatFather 名稱的帳號, 找有藍勾勾且 users 數超過 275 萬的那個才是正宗機器人之父, 其他全部是假的不要點 (如果找不到可以只輸入部分例如 BotFa) :




點選 BatFather 後進入其聊天室, 按底下的 START 鈕 :




在最底下輸入 /newbot 指令 (Telegram 的指令都是用 / 開始) 後按右邊發送鈕建立機器人 :





這時他會問你這個機器人要取甚麼名字 (name), 在底下輸入名稱後按右邊發送鈕 :




接下來要為機器人取一個使用者名稱 (username), 在底下輸入名稱後按右邊發送鈕 :




結果 twstock_bot 這個 username 已經被別人用了, 所以我改用 twstock168_bot :




收到如下回應表示 username 已設定完成, 回應裡面的 HTTP API 就是呼叫 Telegram API 時必須提供的權杖 (token), 請將其複製到文字檔中儲存備用 : 




接下來還必須取得聊天室的識別碼 (ID), 先回到 App 首頁, 按右上方的搜尋鈕, 輸入 @userinfobot  搜尋 userinfobot 這個系統機器人, 注意, 必須是如下圖所示的 userinfobot@userinfobot 這個才是正宗, 其他都是假的 (不要亂點, 以免帳號被盜) :



進入 userinfobot 機器人聊天室後按底下的 START 鈕, 它就會傳回你的聊天室 ID : 





請將此 ID 複製貼到文字檔案中儲存備用, 例如我是將其與其他 API Key 或 Token 一起放在目前工作目錄下的純文字檔 .env 裡面的 TELEGRAM_TOKEN 與 TELEGRAM_ID 變數中 : 




然後用 dotenv 套件將其讀進環境變數中, 然後用 os 模組讀取環境變數 : 

>>> from dotenv import load_dotenv   
>>> load_dotenv()    
True 
>>> import os    
>>> token=os.environ.get('TELEGRAM_TOKEN')   
>>> chat_id=os.environ.get('TELEGRAM_ID')     

接下來只要在呼叫 Telegram API 時將 token 與 chat_id 傳給伺服器, 認證成功就可以將訊息傳出去了. 


3. 利用 HTTP 的 GET/POST 方法傳送訊息 : 

Telegram Bot API 是一個 RESTful API, 可以透過 HTTP 的 GET 或 POST 方法與 Telegram 的伺服器互動, 其介面簡潔易用, 非常適合專案開發, IoT 應用, 以及伺服器監控等用途. 

使用 GET 方法時變數是全部放在網址中傳遞, 網址格式如下 :

https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={text} 

可見 Telegram API 的參數有兩個 (網址中 ? 後面的就是攜帶的變數) : chat_id (聊天室識別碼) 與 text (要傳的訊息). 使用 requests 的 get() 函式時也可以將參數放在字典中, 然後將其傳給 params 變數. 

使用 POST 方法時變數 (chat_id 與 text 訊息) 是放在 body 中傳遞, 網址格式如下 :

https://api.telegram.org/bot{token}/sendMessage

變數則放在字典中 :

data={
    'chat_id': chat_id,
    'text': text
    }

然後在呼叫 post() 函式時將此字典傳給 data 參數即可 (注意是 data 不是 params). 

首先匯入 requests 模組 :呼叫 Telegram Bot API 來傳送即時訊息 :

>>> import requests

定義要傳送的訊息 text 並且將它與 token 以及 chat_id 一起嵌入 API 的網址變數 url 中 :

>>> text='Hello! 你好'  
>>> url=f'https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}&text={text}'    

這裡用 f 字串嵌入上面用 dotenv 載入的環境變數 token (Telegram 權杖) 與聊天室識別碼 chat_id, 輸出 url 內容如下 :

>>> url   
'https://api.telegram.org/bot<權杖>/sendMessage?chat_id=<聊天室識別碼>&text=Hello! 你好'

然後呼叫 requests 的 get() 函式向 Telegram 伺服器提出 GET 請求就會將訊息 text 傳送到聊天室了, 它會傳回一個類 JSON 格式的 Response 物件, 呼叫其 json() 方法可以轉成字典 :

>>> r=requests.get(url)   
>>> type(r)   
<class 'requests.models.Response'>
>>> data=r.json()     
>>> data   
{'ok': True, 'result': {'message_id': 4, 'from': {'id': 7938146214, 'is_bot': True, 'first_name': 'twstock', 'username': 'twstock168_bot'}, 'chat': {'id': <MyChatID>, 'first_name': '77960', 'type': 'private'}, 'date': 1743950717, 'text': 'Hello! 你好'}}
>>> type(data)   
<class 'dict'>

可見傳回值就是與這次傳送相關的資訊, 其中 from 屬性的 id 就是權杖的編號. 這時去查看 Telegram 會發現收到機器人轉發的訊息 : 




也可以將變數放在一個字典中, 然後在呼叫 requests.get() 時將字典傳給 params 參數 :

>>> params={'chat_id': chat_id, 'text': text}     
>>> params    
{'chat_id': '<MyChatID>', 'text': 'Hello! 你好'}

這時網址就不用攜帶變數了 : 

>>> url=f'https://api.telegram.org/bot{token}/sendMessage'    
>>> r=requests.get(url, params=params)   
>>> r.json()   
{'ok': True, 'result': {'message_id': 5, 'from': {'id': 7938146214, 'is_bot': True, 'first_name': 'twstock', 'username': 'twstock168_bot'}, 'chat': {'id': <MyChatID>, 'first_name': '77960', 'type': 'private'}, 'date': 1743951580, 'text': 'Hello! 你好'}}

這樣就收到了一則相同的訊息 :




也可以用 POST 方法傳送訊息, 這時變數字典要用 data 傳送 (不是 params) :

>>> data={'chat_id': chat_id, 'text': text}  
>>> data  
{'chat_id': '<MyChatID>', 'text': 'Hello! 你好'}  

這個 data 變數字典與上面的 params 內容是完全相同的, 只是為了遷就 post() 的參數名稱改為 data 而已. 網址與上面無變數的一樣 : 

>>> url=f'https://api.telegram.org/bot{token}/sendMessage'   
>>> r=requests.post(url, data=data)    
>>> r.json()   
{'ok': True, 'result': {'message_id': 6, 'from': {'id': 7938146214, 'is_bot': True, 'first_name': 'twstock', 'username': 'twstock168_bot'}, 'chat': {'id': <MyChatID>, 'first_name': '77960', 'type': 'private'}, 'date': 1743952234, 'text': 'Hello! 你好'}}

這時 Telegram App 再次收到同樣的訊息 :




我將上面的程序寫成如下的函式 :

import requests

def telegram_msg(token, chat_id, text):
    url=f'https://api.telegram.org/bot{token}/sendMessage'
    data={'chat_id': chat_id, 'text': text}
    try:
        with requests.post(url, data=data, timeout=10) as r:
            r.raise_for_status()  # 若 status_code != 2xx 會拋出例外
            result=r.json()
            if result.get('ok'):
                print('Message sent successfully.')
                return True
            else:
                print(f'Telegram API Error: {result}')
                return False
    except requests.exceptions.RequestException as e:
        print(f'Request error: {e}')
        return False

此函式加入了例外捕捉並使用 with 語法, 這樣當 with 區塊結束時就會自動關閉 Response 物件了, 測試結果 OK :

>>> text='你是在說哈囉嗎?'   
>>> telegram_msg(token, chat_id, text)     
Message sent successfully.
True

檢視 App 確實有收到訊息 : 



2025年4月5日 星期六

關於 LINE Notify 終止服務之替代方案

LINE Notify 已經在 4/1 終止服務了, 我之前寫的爬蟲程式 (圖書館借書預約, 台股市況, 樹莓派 Pi 3 主機 IP, 太陽能儲能系統監控, ...) 都是使用 LINE Notify 推播服務的, 目前都已停擺了. 趁清明連假最後兩天抽空檢視了之前在臉書或網路上看到而記錄下來的替代方案, 歸納起來有如下作法 :
  • 改用推播訊息完全免費的聊天 App : Slack/Telegram, 改平台如果是自用我覺得無妨, 但如果是群組就變成成員都要一起轉移陣地, 有切換上的不習慣.
  • 改用 LINE Messaging API, 但每月免費 200 則, 超過須付費, 每月 800 元. 
個人應用部分例如圖書館借書預約爬蟲因每天 2 則, 可以改寫為 LINE Bot 免費使用; 股票追蹤機器人訊息量大目前較傾向改用 Slack/Telegram, 屆時也只好叫群組成員改換平台. 


2025-04-07 補充 :

Telegram Bot 傳送即時訊息方法參考 :



2025-04-08 補充 :

今天找到下面這個 YT 教學, 展示如何立用 Google App Script 當後臺來透過 LINE Messaging API 傳送訊息 :




紫微斗數學習筆記 : 排命盤

我從學生時代就開始看紫微斗數了, 但就只是看看而已, 並沒有做系統化研究, 更別說整理筆記了. 最近從市圖借到下面這本大耕老師寫的斗數書, 看著看著就興起寫筆記念頭了 :


以前排命盤要翻書, 根據出生時辰與性別在 12 宮位中填入主星, 副星, 以及四化等元素, 排一張命盤要花上數小時, 手動排命盤可參考下面紫微研究苑的 12 篇文章 :


這 12 篇鉅細靡遺地描述排命盤的方法, 想要手工排盤就不用再翻書了. 

本系列之前的文章參考 :



1. 十二宮位 : 

一個命盤的基本結構如下 : 


姓名 :    
陽曆生辰 :
農曆生辰 :
生肖 :
命宮 :    身宮 :
命主 :    身主 :


周圍的 12 格代表 12 個宮位, 分別用 12 地支表示 (地支宮位), 右下角是地支的最後一個 (亥), 其左邊一格是起始宮位, 從子宮開始順時針繞一圈至右下角的亥宮結束. 注意, 地支在 12 個宮位的位置是固定的, 所有的命盤右下角都是亥宮. 

手工排盤第一步是先找出命宮位置, 先從寅宮 (左下角宮位, 代表正月) 開始順時針數到出生月份, 再從那裏逆時針數到出生時辰之宮位即是命宮所在. 12 個月份與地支的對映關係如下表 : 


月份 正月 二月 三月 四月 五月 六月 七月 八月 九月 十月 冬月 臘月
地支


以一個模擬的男性命主生辰 1965 年 11 月 19 日丑時為例, 從左下角寅宮 (一月) 順時針走 11 格會到達子宮, 從這裡由地支的子開始逆時針往回走到生時的丑時, 也就是往回走一格到右下角的亥宮即為命宮所在 :




找出命宮後就可以按順時鐘順序排出其他的 11 個宮位 :

命宮 > 父母宮 > 福德宮 > 田宅宮 > 官祿宮 > 僕役宮(朋友宮) > 遷移宮 > 疾厄宮 > 財帛宮 > 子女宮 > 夫妻宮 > 兄弟宮 (順時鐘)

以上面的命主為例, 12 宮位就定位出來了 : 




因為生辰與性別不同, 每個人的 12 宮位位置也就不同 (命盤相同的機率以人口數據專家推算在千萬分之一以下), 即使命盤完全一樣的雙胞胎, 命運仍會因為後天的選擇與心態而有所不同. 


2. 排盤軟體 : 

12 宮位確定之後, 接下來就是安主星副星與四化, 這些都有既定規則推演. 但其實網路上有非常多的電腦排盤軟體, 不用這麼累手工排盤, 例如 : 


進入 AI 時代後我們可以借助 ChatGPT 等 AI 工具來輔助學習, 不過 AI 生成的數據需要查尋 Google 或書籍來驗證資訊正確性, 跟寫程式一樣, AI 解盤結果不見得正確, 例如給 ChatGPT 農曆生辰讓它排命盤, 結果它換算出來的陽曆生辰居然是錯的, 命盤排錯了當然是滿盤皆錯. 

依據生辰與性別排出來的命盤稱為本命盤 (又稱原局), 可以從主星與副星在 12 宮位的位置, 以及觀察四化與格局來判斷一個人的先天命格, 性格特質, 人生潛能等基本命運格局, 本命盤是固定不變的. 

此外還有所謂運限盤, 這是隨時間變化的命盤, 包含大限盤 (每 10 年一大限), 小限盤 (每年一小限), 與流年盤 (每年命宮位置隨年而變) 三種, 它們是配合本命盤根據不同的時間流轉 (如年齡) 所推導出來的, 用來觀察不同人生階段的運勢波動與時運吉凶. 運限盤宮位的排法是根據命宮起點與五行局, 年齡或年度來推算. 


3. 陰陽五行與 60 花甲子 : 

以上面模擬的男性命主為例, 命盤最中央除生辰外還有一些推演出來的資訊 : 




首先看出生年天干的陰陽屬性, 陰男表示命主為男性, 其出生年的天干為乙, 屬於陰性的乙, 丁, 己, 辛, 癸這五者之一 (出生年天干若為甲, 丙, 戊, 庚, 壬則為陽性). 

農曆的序年採用 10 天干與 12 地支前後配對共有 60 個組合, 稱為 60 花甲子, 前面為天干, 其陰陽五行屬性如下表 (天干無生肖屬性) :


天干
五行
陰陽


60 花甲子的後面是 12 地支, 其陰陽五行屬性與生肖配對如下表 :


地支
五行
陰陽
生肖


下表即 60 花甲子組合, 從甲子年開始, 接著乙丑, 丙寅, 丁卯, .... 直到癸亥共 60 年一個循環, 然後再回到甲子年展開新的輪迴 :


天干地支的一個循環 (六十甲子)
甲子 乙丑 丙寅 丁卯 戊辰 己巳 庚午 辛未 壬申 癸酉
甲戌 乙亥 丙子 丁丑 戊寅 己卯 庚辰 辛巳 壬午 癸未
甲申 乙酉 丙戌 丁亥 戊子 己丑 庚寅 辛卯 壬辰 癸巳
甲午 乙未 丙申 丁酉 戊戌 己亥 庚子 辛丑 壬寅 癸卯
甲辰 乙巳 丙午 丁未 戊申 己酉 庚戌 辛亥 壬子 癸丑
甲寅 乙卯 丙辰 丁巳 戊午 己未 庚申 辛酉 壬戌 癸亥


參考 :


其實生年天干與地支都可以依據西元年經過簡單的計算規則來推算, 算法如下 :
  • 生年天干 :
    可由西元年尾數來判斷, 尾數 1 為辛, 尾數 2 為壬, 尾數 3 為癸, 尾數 4 為甲, 尾數 5 為乙, 尾數 6 為丙, 尾數 7 為丁, 尾數 8 為戊, 尾數 9 為己, 尾數 10 為庚. 
  • 生年地支 :
    計算規則為西元年減 4 再除以 12 取餘數, 0 為子, 1 為丑, 2 為寅, 3 為卯, 4 為辰, 5 為巳, 6 為午, 7 為未, 8 為申, 9 為酉, 10 為戌, 11 為氦. 
以命主 1965 年生為例, 尾數為 5 故天干為乙; 1965 減 4 為 1961, 除以 12 得餘數 5, 故生年地支為巳, 查萬年曆 1965 年確實為乙巳年, 天干為乙屬陰, 故為陰男. 


4. 五行局 : 

五行局是由五行 (金木水火土) 加上局數 (2~6, 起運年齡) 組成的, 一共有 5 個 : 
  • 水二局 : 兩歲起運
  • 木三局 : 三歲起運
  • 金四局 : 四歲起運
  • 土五局 : 五歲起運
  • 火六局 : 六歲起運 
命盤中的五行局是由生年天干及命宮地支決定的, 如下表所示 :


生年干 / 命宮支 子丑 寅卯 辰巳 午未 申酉 戌亥
甲己 水二局 火六局 木三局土五局 金四局 火六局
乙庚 火六局 土五局 金四局 木三局 水二局 土五局
丙辛 土五局 木三局 水二局 金四局 火六局 木三局
丁壬 木三局 金四局 火六局 水二局 土五局 金四局
戊癸 金四局 水二局 土五局 火六局 木三局 水二局


以上面的模擬命盤為例, 命主生年為乙巳年, 生年天干為乙, 命宮位置為亥宮, 地支為亥, 由上表可知其五行局為土五局, 即五歲起運, 大限由此開始每十年一個階段. 

五行局是用來確定命宮星曜與大限流年排布的關鍵, 命格的五行局會影響命盤的結構, 例如安紫微星時就是利用五行局與出生日來決定的, 如下表所示 : 


五行局 / 日期 123 456 789 101112 131415
水二局
木三局
金四局
土五局
火六局

五行局 / 日期 161718 192021 222324 252627 282930
水二局
木三局
金四局
土五局
火六局


以上面模擬命主為土五局生日為 19 日為例, 其紫微星位於辰宮. 接下來就可以逆時針排布紫微系的六顆主星 : 

紫微星 > 天機星 > 跳一格 > 太陽星 > 武曲星 > 天同星 > 跳二格 > 廉貞星

接下來是安天府星, 其位置在紫微星的 45 度斜對角, 若紫微在右上角的申宮或左下角的寅宮, 則天府與紫微同宮. 以上面模擬命主紫微星位於辰宮為例, 則其天府星在子宮. 安好天府星後即可按順時針方向排布天府星系的其他七顆星, 順序如下 : 

天府星 > 太陰星 > 貪狼星 > 巨門星 > 天相星 > 天梁星 > 七殺星 > 跳三格 > 破軍星

這樣 14 顆主星的位置就排出來了 :




以上是五行局對本命盤結構的影響, 此外五行局的局數則決定幾歲開始起運, 例如土五局就是五歲開始起運, 這也同時決定每個大限的時間範圍, 例如土五局第一個大限就是 5~14 歲, 第二個大限是 15~24 歲 ... 依此類推. 同理, 水二局的人兩歲就起大運了. 


5. 身宮 : 

命宮代表一個人的天賦, 本性與命運架構, 而身宮代表一個人後天的行為模式, 發展方向與應對環境的方式, 也可以說是想要自我實現的目標, 會在成年具有獨立思考能力後顯現, 參考 :


身宮的找法參考 :


即從左下角的寅宮開始 (寅月為正月), 順時針走到出生月的宮位, 以該宮位為子時繼續順時針走到出生時辰即為身宮所在. 

以上面命主生辰 11 月 19 日丑時為例, 從寅宮順時針走 11 個月到達子宮, 以此為子時走到生時丑時到達丑宮, 故身宮為丑宮, 該命主的丑宮為福德宮, 因此也可以說身宮在福德宮. 身宮在福德宮的人, 重視內在感受, 內心世界豐富, 喜歡思考, 對人生有比較多哲學與靈性層面的想法, 喜歡獨處或享受安靜空間, 常透過沉思冥想或閱讀來充實自己, 是個用心靈在過日子的人.