2025年4月11日 星期五

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

在前一篇測試中我們使用 Telegram 的 HTTP API 傳送圖片到聊天室, 本篇改用 python-telegram-bot 套件的 Bot 物件來實作相同的功能. 本系列之前的文章參考 :


我的 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')   

# load_secrets_py
from dotenv import load_dotenv  
import os  

load_dotenv()    
token=os.environ.get('TELEGRAM_TOKEN')   
chat_id=os.environ.get('TELEGRAM_ID') 

參考 :


本篇直接從傳送圖片的函式入手, 關於 Bot 物件用法參考第二篇測試, .  


7. 利用  Bot 物件傳送圖片 :     

用 Bot 物件傳送圖片比起用 HTTP API 來說在程式碼結構上相對地簡單, 但是要注意的是, python-telegram-bot 套件從 v20 版開始採用 async (非同步) 架構, 所有 Bot 物件的方法都需要在 async 函式中使用 await 呼叫才行, 如果使用舊版的同步式呼叫函式, 雖然程式可以執行, 但實際上並不會傳送圖片. 


(1). 傳送單一圖片 :  

首先來改寫前一篇測試中傳送一個網路圖片的 telegram_web_image() 函式, 其 Bot 物件版的寫法如下 :

import asyncio
from telegram import Bot

async def send_photo_async(token, chat_id, photo_url, caption=None, parse_mode='Markdown'):
    bot=Bot(token=token)
    await bot.send_photo(
        chat_id=chat_id,
        photo=photo_url,
        caption=caption,
        parse_mode=parse_mode
        )
    print("Message sent successfully.")
    return True

if __name__ == '__main__':
    token='YOUR TOKEN'
    chat_id='YOUR CHAT ID'
    photo_url='URL OF PHOTO'
    caption='THE CAPTION'
    asyncio.run(send_photo_async(token, chat_id, photo_url, caption))

此函式定義的最前面要加上 async 表示為非同步, 呼叫 Bot 物件方法時前面要冠上 await 等待回應, 測試如下 :

>>> import asyncio     
>>> from telegram import Bot   
>>> async def send_photo_async(token, chat_id, photo_url, caption=None, parse_mode='Markdown'):
    bot=Bot(token=token)
    await bot.send_photo(
        chat_id=chat_id,
        photo=photo_url,
        caption=caption,
        parse_mode=parse_mode
        )
    print("Message sent successfully.")
    return True

呼叫 asyncio.run() 來執行這個非同步函式 (已利用 dotenv 載入 token 與 chat_id) : 

>>> asyncio.run(send_photo_async(token, chat_id, photo, caption))   
Message(caption='Lena Söderberg', caption_entities=(MessageEntity(length=14, offset=0, type=<MessageEntityType.BOLD>),), channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 2, 20, 44, 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=122, photo=(PhotoSize(file_id='AgACAgQAAxkDAANvZ_f6r1maJjJS-WDvkJQ-Py3JzbcAApKtMRtu9CVR606iM07xqbsBAAMCAANzAAM2BA', file_size=1964, file_unique_id='AQADkq0xG270JVF4', height=90, width=90), PhotoSize(file_id='AgACAgQAAxkDAANvZ_f6r1maJjJS-WDvkJQ-Py3JzbcAApKtMRtu9CVR606iM07xqbsBAAMCAANtAAM2BA', file_size=21503, file_unique_id='AQADkq0xG270JVFy', height=300, width=300)), supergroup_chat_created=False)
Message sent successfully.
True

這時檢查 Telegram App 果然有收到這張圖片 : 




如果要傳送本機圖片, 需要將開啟的檔案用 telegram.InputFile 類別包起來再傳給 send_photo() 方法的 photo 參數, 程式碼如下 : 

import asyncio
from telegram import Bot, InputFile   

async def telegram_photo_async(token, chat_id, photo_path, caption=None, parse_mode='Markdown'):
    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=parse_mode
                )
        print("Message sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending photo: {e}")
        return False

if __name__ == '__main__':
    token='YOUR TOKEN'
    chat_id='YOUR CHAT ID'
    photo_path='PHOTO PATH'  # 本機圖片檔案
    caption='HE CAPTION'
    asyncio.run(telegram_local_image_async(token, chat_id, photo_path, caption))

測試結果 :

>>> import asyncio    
>>> from telegram import Bot, InputFile   
>>> async def telegram_photo_async(token, chat_id, photo_path, caption=None, parse_mode='Markdown'):
    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=parse_mode
                )
        print("Message sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending photo: {e}")
        return False

呼叫 asyncio.run() 來執行這個非同步函式 (已利用 dotenv 載入 token 與 chat_id) : 

>>> asyncio.run(telegram_photo_async(token, chat_id, photo_path, caption))    
Message(caption='可愛的貓咪 1', caption_entities=(MessageEntity(length=7, offset=0, type=<MessageEntityType.BOLD>),), channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 7, 24, 25, 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=129, photo=(PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADcwADNgQ', file_size=1394, file_unique_id='AQADgMIxGwVuuFd4', height=60, width=90), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADbQADNgQ', file_size=18479, file_unique_id='AQADgMIxGwVuuFdy', height=213, width=320), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADeAADNgQ', file_size=94085, file_unique_id='AQADgMIxGwVuuFd9', height=533, width=800), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADeQADNgQ', file_size=241570, file_unique_id='AQADgMIxGwVuuFd-', height=853, width=1280), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADdwADNgQ', file_size=540510, file_unique_id='AQADgMIxGwVuuFd8', height=1500, width=2250)), supergroup_chat_created=False)

App 顯示收到該圖片 :




(2). 傳送多張圖片 :

使用 Bot 物件傳送多張圖片時, 不論是網路圖片還是本機圖片, 都是使用 telegram. InputMediaPhoto 類別來包裝這些媒體, 差別只是本機圖片需要開啟檔案再打包, 網路圖片直接將網址打包而已. 

傳送多張網路圖片的程式碼如下 :

import asyncio
from telegram import Bot, InputMediaPhoto  

async def telegram_images_async(token, chat_id, photo_urls, captions=None):
    bot=Bot(token=token)
    try:        
        captions=captions or []  # 預設空串列表避免 NoneType 問題
        media_group=[]  # 儲存 InputMediaPhoto 物件的串列
        for i, url in enumerate(photo_urls):  # 取出索引與圖片網址
            media=InputMediaPhoto(   # 建立 InputMediaPhoto 物件
                media=url,  
                caption=captions[i] if i < len(captions) else None,
                parse_mode='Markdown'
                )
            media_group.append(media)
        # 傳送 Media Group (多圖片)
        await bot.send_media_group(chat_id=chat_id, media=media_group)
        print("Media group sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending media group: {e}")
        return False

if __name__ == '__main__':
    token='YOUR TOKEN'
    chat_id='YOUR CHAT ID'
    photos=['https://yaohuang1966.github.io/images/cat.jpg',
            'https://yaohuang1966.github.io/images/orchid.jpg'] 
    captions=['可愛的小咪', '蘭花']
    asyncio.run(telegram_images_async(token, chat_id, photos, captions))

此函式使用空串列 media_group 來儲存用 InputMediaPhoto 打包後之圖片媒體, 然後在呼叫 Bot 物件的 send_media_group() 方法時將此串列傳給 media 參數, 

我準備了兩張圖片放在 GitHub :


測試如下 : 

>>> import asyncio   
>>> from telegram import Bot, InputMediaPhoto   
>>> async def telegram_images_async(token, chat_id, photo_urls, captions=None):
    bot=Bot(token=token)
    try:        
        captions=captions or []  # 預設空串列表避免 NoneType 問題
        media_group=[]  # 儲存 InputMediaPhoto 物件的串列
        for i, url in enumerate(photo_urls):  # 取出索引與圖片網址
            media=InputMediaPhoto(   # 建立 InputMediaPhoto 物件
                media=url,   # 將網址指定給 media 參數
                caption=captions[i] if i < len(captions) else None,
                parse_mode='Markdown'
                )
            media_group.append(media)
        # 傳送 Media Group (多圖片)
        await bot.send_media_group(chat_id=chat_id, media=media_group)
        print("Media group sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending media group: {e}")
        return False

此處在呼叫 InputMediaPhoto() 建構式時將圖片網址只配給 media 字串建立 InputMediaPhoto 物件, 並將這打包後的圖片媒體物件放入 media_group 串列中儲存, 在呼叫 Bot 物件的 send_media_group() 方法時將媒體串列傳給 media 參數即可將這些圖片傳送出去. 

定義圖片網址與標題串列 :

>>> photos=['https://yaohuang1966.github.io/images/cat.jpg',
        'https://yaohuang1966.github.io/images/orchid.jpg']   
>>> captions=['可愛的小咪', '蘭花']    

呼叫 asyncio.run() 來執行這個非同步函式 (已利用 dotenv 載入 token 與 chat_id) : 

>>> asyncio.run(telegram_images_async(token, chat_id, photos, captions))  
(Message(caption='可愛的小咪', channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室 ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 11, 30, 54, 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, media_group_id='13954968437357869', message_id=137, photo=(PhotoSize(file_id='AgACAgQAAxUHZ_ZdsT7IDVJhtUCxnXNFh_cudJgAAhW4MRtSC7VT0zHgt9QirCMBAAMCAANzAAM2BA', file_size=1120, file_unique_id='AQADFbgxG1ILtVN4', height=57, width=90), PhotoSize(file_id='AgACAgQAAxUHZ_ZdsT7IDVJhtUCxnXNFh_cudJgAAhW4MRtSC7VT0zHgt9QirCMBAAMCAANtAAM2BA', file_size=10157, file_unique_id='AQADFbgxG1ILtVNy', height=204, width=320)), supergroup_chat_created=False), Message(caption='蘭花', channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室 ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 11, 30, 54, 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, media_group_id='13954968437357869', message_id=138, photo=(PhotoSize(file_id='AgACAgQAAxUHZ_Zgmtac22EDmiPs9y7K1GvmInkAAsS3MRs0HLRTjJ5CHHFKag8BAAMCAANzAAM2BA', file_size=1396, file_unique_id='AQADxLcxGzQctFN4', height=44, width=90), PhotoSize(file_id='AgACAgQAAxUHZ_Zgmtac22EDmiPs9y7K1GvmInkAAsS3MRs0HLRTjJ5CHHFKag8BAAMCAANtAAM2BA', file_size=19517, file_unique_id='AQADxLcxGzQctFNy', height=156, width=320), PhotoSize(file_id='AgACAgQAAxUHZ_Zgmtac22EDmiPs9y7K1GvmInkAAsS3MRs0HLRTjJ5CHHFKag8BAAMCAAN4AAM2BA', file_size=48815, file_unique_id='AQADxLcxGzQctFN9', height=292, width=600)), supergroup_chat_created=False))

檢視 App 果然收到這兩張圖片 :




傳送本機多個圖片同樣也是使用 InputMediaPhoto 類別來打包要上傳的圖檔, 與網路圖片不同之處是本機圖檔需先用 open() 開檔, 並將檔案參考傳給 InputMediaPhoto() 建構式的 media 參數建立物件並放進媒體串列中儲存, 然後在呼叫 Bot 物件的 send_media_group() 方法時將此 InputMediaPhoto 物件串列傳給 media 參數即可, 程式碼如下 : 

# telegram_send_local_photos_async1.py
import asyncio
from telegram import Bot, InputMediaPhoto   

async def telegram_images_async(token, chat_id, photo_paths, captions=None):
    bot=Bot(token=token)
    try:        
        captions=captions or []  # 預設空串列表避免 NoneType 問題
        media_group=[]  # 儲存 InputMediaPhoto 物件的串列
        files=[]  # 儲存檔案參考的串列
        for i, path in enumerate(photo_paths):  # 取出索引與圖片路徑
            f=open(path, 'rb')  # 開啟圖檔
            files.append(f)  # 暫存檔案參考以免被關閉
            media=InputMediaPhoto(   # 建立 InputMediaPhoto 物件
                media=f,   # 傳入檔案參考
                caption=captions[i] if i < len(captions) else None,
                parse_mode='Markdown'
                )
            media_group.append(media)
        # 傳送 Media Group (多圖片)
        await bot.send_media_group(chat_id=chat_id, media=media_group)
        print("Media group sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending media group: {e}")
        return False
    finally:  # 關閉所有檔案        
        for f in files:  
            f.close()

if __name__ == '__main__':
    token='YOUR TOKEN'
    chat_id='YOUR CHAT ID'
    photos=['cat1.jpg', 'cat2.jpg', 'cat3.jpg']
    captions=['貓咪 1', '貓咪 2', '貓咪 3']
    asyncio.run(telegram_images_async(token, chat_id, photos, captions))

測試結果 :

>>> import asyncio
>>> from telegram import Bot, InputMediaPhoto  
>>> async def telegram_images_async(token, chat_id, photo_paths, captions=None):
    bot=Bot(token=token)
    try:        
        captions=captions or []  # 預設空串列表避免 NoneType 問題
        media_group=[]  # 儲存 InputMediaPhoto 物件的串列
        files=[]  # 儲存檔案參考的串列
        for i, path in enumerate(photo_paths):  # 取出索引與圖片路徑
            f=open(path, 'rb')  # 開啟圖檔
            files.append(f)  # 暫存檔案參考以免被關閉
            media=InputMediaPhoto(   # 建立 InputMediaPhoto 物件
                media=f,  
                caption=captions[i] if i < len(captions) else None,
                parse_mode='Markdown'
                )
            media_group.append(media)
        # 傳送 Media Group (多圖片)
        await bot.send_media_group(chat_id=chat_id, media=media_group)
        print("Media group sent successfully.")
        return True
    except Exception as e:
        print(f"Error sending media group: {e}")
        return False
    finally:  # 關閉所有檔案        
        for f in files:  
            f.close()

定義要傳送的本機圖片檔名路徑與標題 : 

>>> photos=['cat1.jpg', 'cat2.jpg', 'cat3.jpg']   
>>> captions=['貓咪 1', '貓咪 2', '貓咪 3']    

呼叫 asyncio.run() 來執行這個非同步函式 (已利用 dotenv 載入 token 與 chat_id) : 

>>> asyncio.run(telegram_images_async(token, chat_id, photos, captions))   
(Message(caption='貓咪 1', channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室 ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 9, 13, 20, 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, media_group_id='13954902400686829', message_id=132, photo=(PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADcwADNgQ', file_size=1394, file_unique_id='AQADgMIxGwVuuFd4', height=60, width=90), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADbQADNgQ', file_size=18479, file_unique_id='AQADgMIxGwVuuFdy', height=213, width=320), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADeAADNgQ', file_size=94085, file_unique_id='AQADgMIxGwVuuFd9', height=533, width=800), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADeQADNgQ', file_size=241570, file_unique_id='AQADgMIxGwVuuFd-', height=853, width=1280), PhotoSize(file_id='AgACAgUAAxUHZ_eKuzGsDrmHcOe8XMClagTnAAHHAAKAwjEbBW64V_EWQQJaJNhaAQADAgADdwADNgQ', file_size=540510, file_unique_id='AQADgMIxGwVuuFd8', height=1500, width=2250)), supergroup_chat_created=False), Message(caption='貓咪 2', channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室 ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 9, 13, 20, 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, media_group_id='13954902400686829', message_id=133, photo=(PhotoSize(file_id='AgACAgUAAxUHZ_eKvAi3dvXSZMjm4zc5H0ucywUAAoLCMRsFbrhXGCZ9_AoDXBMBAAMCAANzAAM2BA', file_size=1366, file_unique_id='AQADgsIxGwVuuFd4', height=60, width=90), PhotoSize(file_id='AgACAgUAAxUHZ_eKvAi3dvXSZMjm4zc5H0ucywUAAoLCMRsFbrhXGCZ9_AoDXBMBAAMCAANtAAM2BA', file_size=20757, file_unique_id='AQADgsIxGwVuuFdy', height=213, width=320), PhotoSize(file_id='AgACAgUAAxUHZ_eKvAi3dvXSZMjm4zc5H0ucywUAAoLCMRsFbrhXGCZ9_AoDXBMBAAMCAAN4AAM2BA', file_size=82754, file_unique_id='AQADgsIxGwVuuFd9', height=533, width=800), PhotoSize(file_id='AgACAgUAAxUHZ_eKvAi3dvXSZMjm4zc5H0ucywUAAoLCMRsFbrhXGCZ9_AoDXBMBAAMCAAN5AAM2BA', file_size=185302, file_unique_id='AQADgsIxGwVuuFd-', height=853, width=1280), PhotoSize(file_id='AgACAgUAAxUHZ_eKvAi3dvXSZMjm4zc5H0ucywUAAoLCMRsFbrhXGCZ9_AoDXBMBAAMCAAN3AAM2BA', file_size=719975, file_unique_id='AQADgsIxGwVuuFd8', height=1707, width=2560)), supergroup_chat_created=False), Message(caption='貓咪 3', channel_chat_created=False, chat=Chat(first_name='<我的名字>', id=<聊天室 ID>, type=<ChatType.PRIVATE>, username='<我的 username>'), date=datetime.datetime(2025, 4, 11, 9, 13, 20, 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, media_group_id='13954902400686829', message_id=134, photo=(PhotoSize(file_id='AgACAgUAAxUHZ_eKu6BwUtic_8Lj7XblPfxa9lMAAoHCMRsFbrhXkvOMxCCzQocBAAMCAANzAAM2BA', file_size=1042, file_unique_id='AQADgcIxGwVuuFd4', height=90, width=80), PhotoSize(file_id='AgACAgUAAxUHZ_eKu6BwUtic_8Lj7XblPfxa9lMAAoHCMRsFbrhXkvOMxCCzQocBAAMCAANtAAM2BA', file_size=12276, file_unique_id='AQADgcIxGwVuuFdy', height=320, width=285), PhotoSize(file_id='AgACAgUAAxUHZ_eKu6BwUtic_8Lj7XblPfxa9lMAAoHCMRsFbrhXkvOMxCCzQocBAAMCAAN4AAM2BA', file_size=68931, file_unique_id='AQADgcIxGwVuuFd9', height=800, width=712), PhotoSize(file_id='AgACAgUAAxUHZ_eKu6BwUtic_8Lj7XblPfxa9lMAAoHCMRsFbrhXkvOMxCCzQocBAAMCAAN3AAM2BA', file_size=173001, file_unique_id='AQADgcIxGwVuuFd8', height=1500, width=1336), PhotoSize(file_id='AgACAgUAAxUHZ_eKu6BwUtic_8Lj7XblPfxa9lMAAoHCMRsFbrhXkvOMxCCzQocBAAMCAAN5AAM2BA', file_size=182085, file_unique_id='AQADgcIxGwVuuFd-', height=1280, width=1140)), supergroup_chat_created=False))
True

檢查 App 果然收到了三張圖片 : 



沒有留言 :