顯示具有 Google 標籤的文章。 顯示所有文章
顯示具有 Google 標籤的文章。 顯示所有文章

2026年7月5日 星期日

Google Antigravity 學習筆記索引

谷歌在 6/18 進行產品線更新, 推出 Antigravity CLI 平台 (agy) 給個人用戶使用 (免費版與 Google AI Pro 以及 Google AI Ultra 方案訂戶), 原本的 Gemini CLI 則保留給購買 Google Cloud 企業方案, 或是使用付費企業級 API 密鑰的用戶使用. Antigravity CLI 功能比 Gemini CLI 更強大, 可以選擇 Gemini/Claude/GPT 模型來實作專案. 

以下是 agy 的學習筆記索引 :



~ 進行中 ~

2025年8月29日 星期五

在 Gmail 建立標籤與篩選郵件的方法

自從在 Mapleboard 安裝 Fail2Ban 封鎖意圖破解密碼入侵的駭客後, 我的 Gmail 就被 Fail2Ban 寄來的封鎖回報信件淹沒, 讓我要找重要信件感到很麻煩 : 




避免此困擾的方法是在 Gmail 新增一個 Fail2Ban 標籤, 然後建立篩選條件, 只要主旨含有 'Fail2Ban' 就打上 Fail2Ban 標籤, 這樣以後一收到符合條件之信件就會直接丟到 Fail2Ban 標籤, 不會顯示在收件夾了. 

首先按 Gmail 左方導覽列 "標籤" 右邊的 "+" 鈕新增一個標籤 :





輸入新標籤名稱 Fail2Ban 後按 "建立" :



這樣在標籤項目下就會出現 Fail2Ban 這個新標籤了 :




接下來要設定篩選條件, 按 Gamil 右上角的設定齒輪, 按 "查看所有設定" :




按 "建立新篩選器" :




在 "包含字詞" 欄位輸入 Fail2Ban 後按右下角的 "繼續" : 





勾選 "略過收件夾 (將其封存)" 後按右下角的 "建立篩選器" :




這樣便完成設定了 :




這樣之後收到的信就不會出現在收件夾, 直接丟到 Fail2Ban 標籤了.


2025-09-01 補充 :

設定後這幾天發現 Gmail 竟然沒收到任何封鎖通知信, 但查詢 Fail2Ban 封鎖狀態正常, 檢查上面設定篩選器有問題, 沒有勾選 "套用標籤", 因為勾選 "忽略收件夾" 所以根本不會進來, 修改如下 :





拿掉了 "忽略收件夾" 後果然就收到信了. 

2025年7月21日 星期一

Python 學習筆記 : 用 Google SMTP 伺服器傳送 Email

這兩天在 Mapleboard 上安裝 postfix 與 mailutils 工具包成功地從命令列透過 Google SMTP 郵件伺服器傳送 Email, 其實也可以用 Python 的 smtplib 套件來做, 我之前在鄉下那台樹莓派 Pi 3 主機也是用這方法將光世代網路的外網 IP 寄到我的 Hinet 信箱以便能遠端連線, 參考 :


今天我將其中 Python 程式 reportip3.py 的寄信功能寫成一個 send_mail() 函式方便應用程式呼叫.


1. 傳送 ASCII 信件 : 

如果信件內容是英數字等 ASCII 編碼的字元, 可用下列函式 send_mail() 來傳送 : 

# smtp_mail_test_1.py
import smtplib

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body=''):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    '''content=(
        f"Subject:{subject}\n"
        f"From:{from_addr}\n"
        f"To:{', '.join(to_addr)}\n"
        f"Cc:{', '.join(cc_addr)}\n"
        f"{body}"
        )'''
    content=(
        "Subject:{subject}\n"
        "From:{from_addr}\n"
        "To:{to_addr}\n"
        "Cc:{cc_addr}\n"
        "{body}"
        ).format(
            subject=subject,
            from_addr=from_addr,
            to_addr=', '.join(to_addr),
            cc_addr=', '.join(cc_addr),
            body=body
            )    
    all_recipients=to_addr + cc_addr  # 合併收件人和副本收件人
    status=smtp.sendmail(from_addr, all_recipients, content)
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject=''                   # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']      # 副本收件人(無設為 None)
body='您好, 這是測試信'                # 信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body)

此處因為我的 Pi 3 安裝的 Python 是 3.5 版不支援 f 字串, 因此改用 format() 來塞變數到字串中. 如果是 Python 3.6+ 版就可以使用 f 字串了. 此程式是從我的 Gmail 信箱傳送 Email 到我的 Hinet 與公司信箱 (副本), 結果兩個信箱都有成功地收到郵件 : 




注意, 程式中的 password 並不是 Google 密碼, 而是啟用 Google 二階段驗證之後, 到下列網址產生的 Google 應用程式密碼 : 


在 "應用程式名稱" 框內輸入可辨識的名稱, 按右下角的 "建立" 鈕即可 : 





將產生的密碼複製下來, 去除中間的分隔用的空白字元後即可使用, 但最好先儲存到文字檔中備查, 因為它只顯示一次, 若沒記下來, 下次要用時須重新建立. 


2. 傳送非 ASCII 信件 : 

不過上面的函式只能用來傳送 ASCII 字元, 如果主旨與內容含有非 ASCII 字元例如中文, 執行時會出現 "UnicodeEncodeError: 'ascii' codec can't encode characters" 的錯誤訊息. 

解決辦法是用 Python 內建的 email 模組中的 email.mime.text.MIMEText 類別來處理 Unicode, 此外, 為了能寄出帶有 PDF與圖檔等附件的信件, 還需要 email.mime.multipart.MIMEMultipart 來打包郵件內容, 完整程式碼如下 :

# smtp_mail_test_2.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body=''):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart()
    content['Subject']=subject
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文
    content.attach(MIMEText(body, 'plain', 'utf-8'))
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                   # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']      # 副本收件人(無設為 None)
body='您好, 這是測試信'                # 信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body)

結果如下 :




如果只是傳送純文字的中文 Email, 那麼這個程式就堪用了. 但是如果傳送 HTML 格式的內容, 例如將 body 改為如下粗體 :

body='<b>您好, 這是測試信</b>' 

則它會以 HTML 碼純文字傳送, 結果如下 :




3. 傳送網頁格式信件 :    

如果郵件內容是 HTML 網頁碼, 則在呼叫建構式 MIMEText() 時要傳入 mode 參數為 'html' :

MIMEText(body, 'html', 'utf-8')

修改 send_mail() 函式添加一個 mode 參數來控制內容模式為 'plain' 或 'html' :

# smtp_mail_test_3.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, body='', mode='html'):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart()
    content['Subject']=subject
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文
    if mode == 'html':
        content.attach(MIMEText(body, 'html', 'utf-8'))
    else:
        content.attach(MIMEText(body, 'plain', 'utf-8'))
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

# 使用範例
account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                         # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']          # 副本收件人(無設為 None)
body='<b><i>您好, 這是測試信<i><b>'   # 信件內容
mode='html'                        # 內容模式 'plain'/'html'
send_mail(account, password, subject, from_addr, to_addr, cc_addr, body, mode)

此處信件內容為粗體斜體的中文字, 結果如下 :




如果將 mode 改成純文字 mode='html' 就沒有網頁效果了 : 




4. 傳送 Text+HTML雙模格式信件 :   

上面的 send_mail() 函式呼叫時要用 mode 參數決定內容格式為純文字 (plain) 或網頁 (html) 格式, 純文字格式可以在所有郵件客戶端顯示; 網頁格式較美觀, 但有些郵件軟體不支援, 這時可能會直接顯示原始 HTML 碼, 空白或純文字亂碼等. 

解決辦法是採用 multipart/alternative 內容格式, 也就是同時傳送純文字內容 text_body 與網頁格式內容 html_body, 當收信端收到這種 multipart/alternative 格式信件時, 若支援 HTML 就會顯示網頁格式之內容 html_body; 反之則顯示純文字格式內容 text_body. 

修改 send_mail() 函式加入 text_body 與 html_body 參數並取消 mode 參數 :

# smtp_mail_test_4.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=''):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart()
    content['Subject']=subject
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文 (雙模)
    content.attach(MIMEText(text_body, 'plain', 'utf-8'))  # 純文字
    if html_body:
        content.attach(MIMEText(html_body, 'html', 'utf-8'))  # 網頁格式
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

# 使用範例
account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                         # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']          # 副本收件人(無設為 None)
text_body='您好, 這是測試信'         # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>'   # 網頁格式信件內容
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body)

結果如下 :




5. 傳送有附檔之信件 :    

傳送附檔需要 open() 函式以二進位模式讀取附檔, 用 email.mime.application.MIMEApplication 類別傳給 MIMEApplication() 後加入信件本體中, 修改 send_mail() 函式添加 attachments 參數如下 : 

# smtp_mail_test_5.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication   
import os   

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=None, attachments=None):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    if attachments is None:  # 可變資料不宜做參數
        attachments=[]       # 應付迴圈需求
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart()
    content['Subject']=subject
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文 (雙模)
    if not text_body and not html_body:  # text_body 與 html_body 均未傳
        text_body='(No message content)'   # 預設內容
    content.attach(MIMEText(text_body, 'plain', 'utf-8'))  # 純文字
    if html_body:
        content.attach(MIMEText(html_body, 'html', 'utf-8'))  # 網頁格式
    # 加入附件
    for filepath in attachments:
        if os.path.isfile(filepath):
            filename=os.path.basename(filepath)
            with open(filepath, 'rb') as f:
                part=MIMEApplication(f.read(), Name=filename)  
                part.add_header('Content-Disposition', 'attachment', filename=filename)
                content.attach(part)
        else:
            print(f'找不到附件:{filepath}')                
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

# 使用範例
account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                         # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']          # 副本收件人(無設為 None)
text_body='您好, 這是測試信'         # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>'   # 網頁格式信件內容
attachments=['cat1.jpg', 'cat2.jpg']  # 附檔
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body, attachments)

藍色字的是為了傳送附檔所增加的部分, 結果如下 :




6. 處理主旨的 Unicode 與 Text+HTML 雙模問題 :    

在上面的測試中主旨含有中文 Unicode 都能順利傳送且在收信端正常顯示中文, 其實這些信件並不符合 RFC 標準格式 (因為 RFC 標頭只允許 ASCII 字元), 但因為目前多數的郵件客戶端會試圖去猜測編碼方式, 通常會自動用 UTF-8 來解釋非 ASCII 字元, 所以大都能正常顯示信件內容. 其實比較保險, 符合 RFC 規範的做法是用 email.header.Header 類別來處理主旨的 Unicode 問題, 只要將主旨內容傳給 Header() 建構式即可. 

其次, 我將上面的函式提交給 AI 檢查, 它建議雙模應該採取兩層結構, 把 text/plain 和 text/html 包在 MIMEMultipart('alternative') 中, 再讓它作為內容的一部分附加到 MIMEMultipart('mixed') 外層, 讓彼此在 multipart/alternative 中互為備案 (fallback 機制), 這樣才算是符合 RFC 標準, 否則部分收信端可能只會讀第一個文字段落而忽略 HTML 部分. 

# smtp_mail_test_6.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.header import Header  
import os

def send_mail(account, password, subject, from_addr, to_addr, cc_addr=None, text_body='', html_body=None, attachments=None):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    if attachments is None:  # 可變資料不宜做參數
        attachments=[]       # 應付迴圈需求
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart('mixed')  # 外層 mixed
    content['Subject']=Header(subject, 'utf-8')   # 處理主旨 Unicode
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文 (雙模)    
    alt_part=MIMEMultipart('alternative')  # 內層:alternative(純文字 + HTML)
    alt_part.attach(MIMEText(text_body or '(No message content)', 'plain', 'utf-8'))
    if html_body:
        alt_part.attach(MIMEText(html_body, 'html', 'utf-8'))
    content.attach(alt_part)  # 將內層加入外層
    # 加入附件
    for filepath in attachments:
        if os.path.isfile(filepath):
            filename=os.path.basename(filepath)
            with open(filepath, 'rb') as f:
                part=MIMEApplication(f.read(), Name=filename)
                part.add_header('Content-Disposition', 'attachment', filename=filename)
                content.attach(part)
        else:
            print(f'找不到附件:{filepath}')                
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

# 使用範例
account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                         # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']          # 副本收件人(無設為 None)
text_body='您好, 這是測試信👋'       # 純文字信件內容
html_body='<b><i>您好, 這是測試信<i><b>🌈'   # 網頁格式信件內容
attachments=['cat1.jpg']  # 附檔
send_mail(account, password, subject, from_addr, to_addr, cc_addr, text_body, html_body, attachments)

黃底色為新增或修改的部分, 因為已經處理了 Unicode, 所以不論中文或 emoji 圖示均可傳送, 結果如下 :




7. 在 HTML 內容中內嵌圖片 :    

上面範例是透過附件檔案方式傳送圖片, 使用夾帶附件的方式可用來傳送任何檔案, 但如果想看到附件內容須點擊開啟附件檔. 如果想在信件內容中直接顯示圖片, 則必須以 HTML 格式用 img 標籤內嵌圖片來傳送信件, 且須使用 email.mime.image,MIMEImage 類別來包裝 open() 讀取的圖片 byte 資料後加入信件標頭中, 將 send_mail() 函式改寫如下 :

# smtp_mail_test_7.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from email.header import Header
from email.mime.image import MIMEImage
import os

def send_mail(account,
              password,
              subject,
              from_addr,
              to_addr,
              cc_addr=None,
              text_body='',
              html_body=None,
              attachments=None,
              inline_images=None):
    if cc_addr is None:
        cc_addr=[]  # 預設空串列
    if attachments is None:  # 可變資料不宜做參數
        attachments=[]       # 應付迴圈需求
    if inline_images is None:  # 無內嵌圖片
        inline_images={}        
    # 登入 Google SMTP 伺服器
    smtp=smtplib.SMTP('smtp.gmail.com', 587)
    smtp.ehlo()
    smtp.starttls()  # 啟動 TLS 安全傳輸
    smtp.login(account, password)  # 登入 Google SMTP 伺服器
    # 建立郵件內容
    content=MIMEMultipart('mixed')  # 外層 mixed
    content['Subject']=Header(subject, 'utf-8')  # 處理主旨 Unicode
    content['From']=from_addr
    content['To']=', '.join(to_addr)
    content['Cc']=', '.join(cc_addr)
    # 添加郵件正文 (雙模)    
    alt_part=MIMEMultipart('alternative')  # 內層:alternative(純文字 + HTML)
    alt_part.attach(MIMEText(text_body or '(No message content)', 'plain', 'utf-8'))
    # 若含有 inline 圖片使用 multipart/related 包住 HTML
    if html_body:
        related_part=MIMEMultipart('related')
        related_part.attach(MIMEText(html_body, 'html', 'utf-8'))
        for cid, img_path in inline_images.items():
            if os.path.isfile(img_path):
                with open(img_path, 'rb') as f:
                    img=MIMEImage(f.read())
                    img.add_header('Content-ID', f'<{cid}>')
                    img.add_header('Content-Disposition', 'inline', filename=os.path.basename(img_path))
                    related_part.attach(img)
            else:
                print(f'找不到內嵌圖片:{img_path}')
        alt_part.attach(related_part)  # 將 related 加入 alt
    content.attach(alt_part)  # 將 alt 加入信件內容
    # 加入附件
    for filepath in attachments:
        if os.path.isfile(filepath):
            filename=os.path.basename(filepath)
            with open(filepath, 'rb') as f:
                part=MIMEApplication(f.read(), Name=filename)
                part.add_header('Content-Disposition', 'attachment', filename=filename)
                content.attach(part)
        else:
            print(f'找不到附件:{filepath}')                
    # 合併收件人和副本收件人
    all_recipients=to_addr + cc_addr
    status=smtp.sendmail(from_addr, all_recipients, content.as_string())
    if status == {}:
        print('郵件傳送成功!')
    else:
        print('郵件傳送失敗!')
    smtp.quit()

# 使用範例
account='mygmail@gmail.com'     # Gmail 帳號
password='azfkqbjbftodjucd'         # Google 應用程式密碼 (這是樣本)
subject='郵件測試'                         # 主旨
from_addr='mygmail@gmail.com'   # 寄件人
to_addr=['myhinet@ms5.hinet.net']  # 收件人
cc_addr=['tony@xxx.com.tw']          # 副本收件人(無設為 None)
text_body='您好, 這是測試信👋'       # 純文字信件內容
html_body='''
<html>
  <body>
    <h3>您好!</h3>
    <p>這是內嵌圖片的測試:</p>
    <img src="cid:cat1" width="300">
  </body>
</html>
'''
attachments=None  # 無附檔
inline_images={"cat1": "cat1.jpg"}  # 內嵌圖片 (鍵與 img 之 src 對應)
send_mail(
    account,
    password,
    subject,
    from_addr,
    to_addr, 
    cc_addr,
    text_body,
    html_body,
    attachments,
    inline_images
    )

此處藍色與黃色為改寫或增加的部分, 結果如下 :




可見圖片是直接內嵌在信件內容中.

最後這一版 send_mail() 就是集大成的版本了 (Unicode+純文字網頁雙模+可傳附件檔+可內嵌圖片), 我們可以同時夾帶附件檔與內嵌圖片. 但如果信件內容只是英數字而已的話, 其實用最上面那個範例中的簡易版 send_mail() 即可. 

2025年3月25日 星期二

Python 學習筆記 : 使用 Custom Search JSON API 取得谷歌搜尋結果

這幾天註冊了 SerpAPI 帳戶使用其 API Key 來取得 Google 搜尋結果, 但它的免費帳戶一個月只能呼叫 100 次, 平均一天只能搜尋 33 次, 這似乎是太少了, 參考 :


今天在 "ChatGPT 開發手冊 Turbo x Vision" 這本書中找到 Custom Search JSON API, 它對免費帳戶提供每天 100 次呼叫服務, 是 SerpAPI 的 3 倍, 這就佛心多了, 以下紀錄註冊免費帳戶與取得 API Key 程序以及如何用它來取得 Google 搜尋資料. 


1. 啟用谷歌 Custon Search JSON API 功能 :     

Google Custom Search JSON API 是 Google 提供的一個 REST API, 可以讓開發者透過程式存取 Google 搜尋結果 (傳回格式為 JSON), 並可根據特定的自訂搜尋引擎來篩選內容. 只要有 Google 帳號即可申請使用, 免費版每天可搜尋 100 次, 付費版 5 美元可搜尋 1000 次, 不論免費或付費, 每次搜尋最多傳回 10 筆結果 (可利用 start 參數指定傳回筆數). 

首先瀏覽器要登入 Google 帳號, 然後前往下列網址 : 


按 "搜尋引擎 ID" 項目說明中的 "程式化搜尋引擎控制台" 超連結 :  




按右上角的 "新增" 鈕新增一個自訂搜尋引擎 :




在 "為你的搜尋引擎命名" 欄中輸入搜尋引擎名稱 (自訂), 並於 "要搜尋甚麼" 欄位中勾選 "搜尋整個網路", 勾選我不是機器人後按 "建立" 鈕 : 




這樣便建立了一個搜尋引擎, 我們可以複製 HTML 碼內嵌到自己的網頁中使用 : 




按 "自訂" 鈕在下列頁面中按 "搜尋引擎 ID" 欄位右邊的複製鈕將此 ID 複製儲存到文字檔備用 : 




接下來要取得此搜尋引擎 API 的金鑰, 這時回到 "程式化搜尋引擎控制台" 網頁, 按 "API 金鑰" 欄位中的 "取得金鑰" 鈕 : 



 

在彈出的 "Enable Custom Search API" 視窗中點選 "+ Create a new project" 新增一個 GCP 專案 : 




在 "Enter a new project name" 欄位中輸入專案名稱 (只可用英數字與 - 字元), 這裡用上面的搜尋引擎相同名稱 LLM-test 後按 "NEXT" 鈕 :




這樣便完成 API Key 的啟用了, 按 "SHOW KEY" 鈕即可顯示金鑰 : 




按右邊複製鈕將 API Key 複製到文字檔中儲存備用 : 




有了自訂搜尋引擎 ID 與其 API Key 便可以用 HTTP GET 方法來取得谷歌搜尋結果了 (注意, 只能用 GET 方法, 不提供 POST 方法). 


2. 使用 Custon Search JSON API 取得谷歌搜尋結果 :       

透過 HTTP GET 方法取得谷歌自訂引擎搜尋結果可以使用內建的 urllib 或第三方爬蟲套件 requests, 以下測試使用 requests :

>>> import requests    

關於 requests 用法參考 :


向自訂的谷歌搜尋引擎提出請求的網址格式如下 : 

https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}

也可以用 gl 參數指定地理位置 (台灣是 tw) 與 lr 參數指定語言 (繁體中文是 lang_zh-TW) : 

https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}&gl=tw&lr=lang_zh_TW

其中 query 是要搜尋的關鍵字, api_key 是金鑰, cx 是自訂搜尋引擎的 ID, NUM 是傳回的搜尋結果筆數 (前 NUM 筆), 此兩參數都使用 ISO 國家 (3166) 與語言標碼表 (639), 參考 :


定義變數如下 :

>>> cx='我的搜尋引擎 ID'   
>>> api_key='我的 API Key'     
>>> num=3   
>>> query='2024台灣總統大選是誰當選?'    

製作 HTTP GET 請求網址並將其傳入 requests.get() 函式 : 

>>> url=f'https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}'   
>>> r=requests.get(url)    

傳回值是攜帶 JSON 格式搜尋結果的 Response 物件, 呼叫其 json() 方法會轉成字典物件 :  

>>> data=r.json()    
>>> type(data)   
<class 'dict'>

呼叫 keys() 方法檢視字典的鍵 : 

>>> data.keys()  
dict_keys(['kind', 'url', 'queries', 'context', 'searchInformation', 'items'])

搜尋結果是放 items 鍵裡面, 其值為一串列 : 

>>> items=data['items']    
>>> type(items)   
<class 'list'>

檢視串列的第一個元素即可看到第一筆搜尋結果是一個字典 : 

>>> items[0]   
{'kind': 'customsearch#result', 'title': '2024年11月5日總統大選結果公布時間表| SF.gov', 'htmlTitle': '<b>2024</b>年11月5日<b>總統大選</b>結果公布時間表| SF.gov', 'link': 'https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w', 'displayLink': 'www.sf.gov', 'snippet': 'Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所\xa0...', 'htmlSnippet': 'Nov 5, 2024 <b>...</b> 投票站關閉後,選務處將會公布四份初步結果報告: ... 於<b>選舉</b>之夜公布的所有<b>選舉</b>結果均為初步性的,在往後的日子,<b>當選</b>務處點算數以萬計的選票後結果將會有所&nbsp;...', 'formattedUrl': 'https://www.sf.gov/.../november-5-2024-presidential-general-election-result...', 'htmlFormattedUrl': 'https://www.sf.gov/.../november-5-<b>2024</b>-presidential-general-election-result...', 'pagemap': {'metatags': [{'next-head-count': '3', 'viewport': 'width=device-width'}]}}

我們關心的資訊放在下列三個鍵裡面 :
  • title : 網頁標題
  • link : 網頁網址
  • snippet : 網頁摘要
可以用迴圈迭代 items 串列元素印出這三個鍵的內容, 此處改用字典的 get() 方法, 並指定預設傳回值為空串列 [] :

>>> for item in data.get('items', []):
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n') 
  
標題: 2024年11月5日總統大選結果公布時間表| SF.gov
描述: Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所 ...
網址: https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w

標題: 2024年中华民国总统选举- 维基百科,自由的百科全书
描述: 选举结果,赖清德、萧美琴当选中华民国第16任总统、副总统。本次是继2000年后再度未有任一候选人得票率过半的总统选举,亦是自总统直选以来,首度 ...
網址: https://zh.wikipedia.org/zh-cn/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89

標題: 台湾大选:一文读懂民进党赖清德为何获胜以及选后的各种看点- BBC ...
描述: Jan 14, 2024 ... 2024年台湾总统选举结果揭晓,民进党候选人赖清德及萧美琴成功当选新一任总统及副总统,总得票数达558万,得票率为40.05%。此役民进党打破台湾政坛执政党 ...
網址: https://www.bbc.com/zhongwen/simp/chinese-news-67973916

可見若未指定位置與語言, 傳回值可能出現簡體中文結果. 

另外一個做法是把 GET 的參數放在一個字典中, 然後在呼叫 requests.get() 方法時將其傳遞給 params 參數 : 

>>> params={
    'q': query,
    'key': api_key,
    'cx': cx,
    'lr': 'lang_zh-TW',  
    'gl': 'tw',          
    'num': 3            
    }  

這時 url 就不需要攜帶參數了 : 

>>> url='https://www.googleapis.com/customsearch/v1'   
>>> r=requests.get(url, params=params)    
>>> data=r.json()    
>>> for item in data.get('items', []):
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n')  
  
標題: 2024年11月5日總統大選結果公布時間表| SF.gov
描述: Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所 ...
網址: https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w

標題: 2024年中華民國總統選舉- 維基百科,自由的百科全書
描述: 選舉結果,賴清德、蕭美琴當選中華民國第16任總統、副總統。本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度 ...
網址: https://zh.wikipedia.org/zh-hant/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89

標題: 台灣大選2024:賴清德當選總統民進黨未能控制立法院- BBC News ...
描述: Jan 13, 2024 ... 1月13日,台灣舉行2024年總統選舉與立法委員選舉,執政黨民進黨候選人賴清德以超過558萬票勝選。
網址: https://www.bbc.com/zhongwen/trad/chinese-news-67971619

因為有指定 gl 與 lr 參數, 所以傳回的搜尋結果都是繁體中文了. 

為了簡化查詢過程, 我將上述程式碼寫成如下之函式 :

>>> def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
    params={
        'q': query,
        'key': api_key,
        'cx': cx,
        'gl': gl,  
        'lr': lr,          
        'num': num          
        }
    url='https://www.googleapis.com/customsearch/v1'
    r=requests.get(url, params=params)
    data=r.json()
    return data.get('items', [])

此函式預設傳回前 3 筆台灣繁體中文之搜尋結果, 使用前先匯入 requests 套件 : 

>>> import requests     

然後定義引擎 ID, 金鑰, 與關鍵字等參數值, 並將它們傳入 search_google() 即可, 它會傳回一個包含 title, link, snippet 等鍵之字典串列 : 

>>> cx='我的搜尋引擎 ID'   
>>> api_key='我的 API Key'     
>>> query='2024 台灣金曲歌王是誰?'    
>>> for item in search_google(query, cx, api_key):    
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n')
    
標題: 2024金曲獎得獎名單完整公佈!MC HotDog奪歌王,歌后由孫盛希抱回
描述: Jun 30, 2024 ... 拿下最佳作詞人的MC HotDog熱狗再度成功拿下金曲歌王,適逢女兒生日,上台時他表示:「今天剛好是我女兒的生日,祝你生日快樂。我本來跟他講說,如果我沒有得獎 ...
網址: https://www.marieclaire.com.tw/entertainment/music/80150/the-35th-golden-melody-awards-winners

標題: 【金曲獎2024】金曲35完整得獎名單!MC HotDog熱狗奪歌王 ...
描述: Jun 29, 2024 ... 2024年「第35屆金曲獎」於6月29日,在台北小巨蛋盛大登場!本屆除了主持組合、表演嘉賓讓人相當期待,得獎名單也相當精采,角逐歌王歌后寶座的唱匠包括: ...
網址: https://www.elle.com/tw/entertainment/music/g61459193/gma-2024-35-award/

標題: 【2024 金曲獎】金曲35 完整得獎名單:MC HotDog、孫盛希封歌王 ...
描述: Jun 29, 2024 ... ... 歌手」封歌王、歌后,但真正的大贏家則是一舉拿下「最佳樂團獎」、「最佳華語專輯獎」、「年度專輯獎」的草東沒有派對!現在,馬上來看金曲35 的完整 ...
網址: https://www.gq.com.tw/article/%E5%BE%97%E7%8D%8E%E5%90%8D%E5%96%AE-2024%E9%87%91%E6%9B%B2%E7%8D%8E-%E9%87%91%E6%9B%B235

Custom Search JSON API 每天可以免費呼叫 100 次, 這是 SerpAPI 的 3 倍, 所以以後就改用這個吧! SerpAPI 就當備用好了. 

2025年3月22日 星期六

Python 學習筆記 : 使用 SerpAPI 取得 Google 搜尋結果

今天因為測試 OpenAI API 需要用到 Google 搜尋來詢問與時事相關問題時, 發現之前使用的 googlesearch-python 套件沒有傳回任何結果 : 

>>> from googlesearch import search   
>>> keywords='2024台灣總統大選是誰當選?'   
>>> items=search(keywords, advanced=True, num_results=3)      
>>> for item in items:  
  print(f'標題:{item.title}')
  print(f'標題:{item.description}')
  print(f'標題:{item.url}\n')
  
執行結果是空白, 詢問 ChatGPT 答覆是此套件可能已經無法使用, 建議使用 SerpAPI 來取得谷歌搜尋結果, 但必須先註冊帳號取得 API Key 才能使用. 


1. 註冊 SerpAPI 帳號 : 

到 SerpAPI 官網按右上角的 "Register" 註冊帳號, 免費帳戶每個月可搜尋 100 次 (平均一天只能使用 33 次, 好像有點小氣) :





可以用 Google 或 GitHub 帳號快速註冊, 但我習慣用 Hinet 郵件註冊, 填好姓名, Email 與設定密碼後按底下的 Sign Up 鈕, 它會寄一封驗證信到信箱, 去信箱收信並按下信中的確認超連結 : 




這時會開啟電話驗證頁面, 在底下 Verification code 欄位中先點前面的國籍選擇台灣, 然後輸入自己的手機號碼, 按 Send Code 鈕 : 




然後收取手機簡訊, 將 6 碼驗證碼貼到 Verification code 欄後按 Verify 鈕 :




按底下的 Subscribe 鈕即完成註冊並顯示 Dashboard 頁面 :





往下拉到 Your private API Key 就可以看到使用 API 所需的金鑰, 按右邊的按鈕複製到文字檔中保存備用 : 




有了 API Key 接下來要安裝 API 的套件. 


2. 安裝 google-search-results 套件 :   

用 pip 安裝 google-search-results 套件 : 

D:\python\test>pip install google-search-results   
Collecting google-search-results
  Downloading google_search_results-2.4.2.tar.gz (18 kB)
  Preparing metadata (setup.py) ... done
Requirement already satisfied: requests in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-search-results) (2.31.0)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests->google-search-results) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests->google-search-results) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from requests->google-search-results) (1.26.19)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests->google-search-results) (2023.7.22)
Building wheels for collected packages: google-search-results
  Building wheel for google-search-results (setup.py) ... done
  Created wheel for google-search-results: filename=google_search_results-2.4.2-py3-none-any.whl size=32077 sha256=6675aa38f27d33c50a5ce0f9af5d634abe719b015fe63b2326a5a926094321fe
  Stored in directory: c:\users\tony1\appdata\local\pip\cache\wheels\d3\b2\c3\03302d12bb44a2cdff3c9371f31b72c0c4e84b8d2285eeac53
Successfully built google-search-results
Installing collected packages: google-search-results
Successfully installed google-search-results-2.4.2

雖然安裝的套件名稱是, 但匯入使用時卻要用 serpapi 這個套件名稱 : 

>>> import serpapi  

用 dir() 檢視套件內容 : 

>>> dir(serpapi)   
['AppleAppStoreSearch', 'BaiduSearch', 'BingSearch', 'DuckDuckGoSearch', 'EbaySearch', 'GoogleScholarSearch', 'GoogleSearch', 'HomeDepotSearch', 'NaverSearch', 'SerpApiClient', 'WalmartSearch', 'YahooSearch', 'YandexSearch', 'YoutubeSearch', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'apple_app_store_search', 'baidu_search', 'bing_search', 'constant', 'duck_duck_go_search', 'ebay_search', 'google_scholar_search', 'google_search', 'home_depot_search', 'naver_search', 'pagination', 'serp_api_client', 'serp_api_client_exception', 'walmart_search', 'yahoo_search', 'yandex_search', 'youtube_search'] 

可見此 API 套件支援非常多搜尋引擎, 除了 Google 外還可以搜尋 DuckDuckGo, Bin, Yahoo 等, 但本篇只會用到其中的 GoogleSearch 類別 :

>>> type(serpapi.GoogleSearch)   
<class 'type'>


3. 用 GoogleSearch 物件搜尋 :   

首先從 serpapi 匯入 GoogleSearch 類別 : 

>>> from serpapi import GoogleSearch    

然後呼叫其建構式 GoogleSearch() 並傳入一個參數字典 params 來建立一個 GoogleSearch 物件 : 

GoogleSearch(params)  

此 params 參數字典的常用鍵值如下表所示 : 


參數名稱 說明
q 搜尋關鍵字, 例如 "2024台灣總統大選"
num 設定結果數量 (最多 100), 例如 num=10 取得 10 筆結果
start 設定從第幾筆開始 (用於分頁), 例如 start=10 從第 11 筆開始
gl 設定國家代碼, 例如 "tw" (台灣), "us" (美國)
hl 設定語言,例如 "zh-TW" (繁體中文), "en" (英文)
safe 是否啟用安全搜尋, 例如 "active" (開啟), "off" (關閉)
api_key SerpAPI 提供的 API Key, 例如 "你的 API Key"


例如搜尋 2024 台灣總統當選人 : 

>>> params={
    'q': '2024台灣總統大選是誰當選?',
    'api_key': '我的 API Key', 
    'num': 3,
    'gl': 'tw',
    'hl': zh-tw'
    }

將其傳入 GoogleSearch() 建立一個 GoogleSearch 物件 : 

>>> search=GoogleSearch(params)   
>>> type(search)   
<class 'serpapi.google_search.GoogleSearch'> 

用 dir() 檢視 GoogleSearch 物件成員 :

>>> dir(search)   
['BACKEND', 'SERP_API_KEY', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'construct_url', 'engine', 'get_account', 'get_dict', 'get_dictionary', 'get_html', 'get_json', 'get_location', 'get_object', 'get_raw_json', 'get_response', 'get_results', 'get_search_archive', 'make_pyobj', 'pagination', 'params_dict', 'timeout']

可以呼叫所提供的 get_xxx() 方法取得搜尋結果, 例如呼叫 get_dict() 會取得字典型態之結果 : 

>>> results=search.get_dict()   
>>> type(results)   
<class 'dict'>

用 keys() 方法檢視此字典之鍵 :

>>> results.keys()   
dict_keys(['search_metadata', 'search_parameters', 'search_information', 'organic_results', 'related_searches', 'pagination', 'serpapi_pagination'])

搜尋結果主要是放在 organic_results 這個鍵裡面, 此鍵主要儲存 Google 搜尋主要的自然搜尋結果 (即非廣告的結果) : 

>>> results['organic_results']     
[{'position': 1, 'title': '第16任總統副總統選舉', 'link': 'https://db.cec.gov.tw/ElecTable/Election/ElecTickets?dataType=tickets&typeId=ELC&subjectId=P0&legisId=00&themeId=4d83db17c1707e3defae5dc4d4e9c800&dataLevel=C&prvCode=00&cityCode=000&areaCode=00&deptCode=000&liCode=0000', 'redirect_link': 'https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://db.cec.gov.tw/ElecTable/Election/ElecTickets%3FdataType%3Dtickets%26typeId%3DELC%26subjectId%3DP0%26legisId%3D00%26themeId%3D4d83db17c1707e3defae5dc4d4e9c800%26dataLevel%3DC%26prvCode%3D00%26cityCode%3D000%26areaCode%3D00%26deptCode%3D000%26liCode%3D0000&ved=2ahUKEwjBq-zWyKGMAxXtSzABHd6WD6oQFnoECB4QAQ', 'displayed_link': 'https://db.cec.gov.tw › ElecTable › Election › ElecTickets', 'favicon': 'https://serpapi.com/searches/67e0b597a87cda1dbcb7f08f/images/f9b7843ebcee3773ad58dd96752619e182d6d5ca2488e6e49be838c999102248.png', 'snippet': '第16任總統副總統選舉 ; 金門縣. 柯文哲. /吳欣盈. 賴清德. /蕭美琴. 侯友宜. /趙少康. 1. 2. 3. 13,038. 4,569. 28,005. 28.58%. 10.02%. 61.40% ; 基隆市. 柯文哲. /吳欣盈.', 'snippet_highlighted_words': ['總統選舉'], 'source': '選舉資料庫'}, {'position': 2, 'title': '2024年中華民國總統選舉', 'link': 'https://zh.wikipedia.org/zh-tw/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89', 'redirect_link': 'https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://zh.wikipedia.org/zh-tw/2024%25E5%25B9%25B4%25E4%25B8%25AD%25E8%258F%25AF%25E6%25B0%2591%25E5%259C%258B%25E7%25B8%25BD%25E7%25B5%25B1%25E9%2581%25B8%25E8%2588%2589&ved=2ahUKEwjBq-zWyKGMAxXtSzABHd6WD6oQFnoECCAQAQ', 'displayed_link': 'https://zh.wikipedia.org › zh-tw › 2024年中華民國總統...', 'favicon': 'https://serpapi.com/searches/67e0b597a87cda1dbcb7f08f/images/f9b7843ebcee3773ad58dd96752619e16639a9f527bc1f87bab180ae788ac04d.png', 'snippet': '本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度由同一政黨連續三次獲勝。蔡英文八年執政雖成功交棒,但維持執政地位的民進黨則在同日 ...', 'snippet_highlighted_words': ['總統選舉'], 'source': '维基百科'}, {'position': 3, 'title': '2024 總統大選即時開票', 'link': 'https://event.gvm.com.tw/2024presidential_election/votingresults_president.html', 'redirect_link': 'https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://event.gvm.com.tw/2024presidential_election/votingresults_president.html&ved=2ahUKEwjBq-zWyKGMAxXtSzABHd6WD6oQFnoECCIQAQ', 'displayed_link': 'https://event.gvm.com.tw › votingresults_president', 'favicon': 'https://serpapi.com/searches/67e0b597a87cda1dbcb7f08f/images/f9b7843ebcee3773ad58dd96752619e162d8bff986619777e960b531be99ef7f.png', 'snippet': '2024總統大選在即,在賴清德、侯友宜、柯文哲競逐的局勢下,最終誰會拿下總統大位?台灣會迎來新一次的政黨輪替嗎?《遠見》統合各候選人政見,針對選戰熱門關鍵字、候選人 ...', 'snippet_highlighted_words': ['2024總統大選', '台灣'], 'source': '遠見雜誌'}]

可見 organic_results 鍵的值是一個字典串列, 其元素為每一筆搜尋結果, 此處因為指定 num=3, 所以總共有三筆 : 

>>> type(results['organic_results'])  
<class 'list'>
>>> len(results['organic_results'])      
3

串列中的每個字典的鍵與說明如下表所示 :


搜尋結果字典的鍵名 說明
position 該結果在 Google 搜尋結果中的排名 (位置編號)
title 搜尋結果的標題, 通常與網頁標題相符
link 該搜尋結果的網址 (URL)
redirect_link 如果有 Google 重定向, 這裡會包含重定向後的 URL
displayed_link 在 Google 搜尋結果中顯示的網址 (可能與原始 `link` 不完全相同, 例如省略 `https://` 或 `www.`)
favicon 該網站的圖示 (favicon) 圖片 URL
snippet 該搜尋結果的簡短摘要, 通常來自該網頁的 `meta description` 或 Google 提取的內容
snippet_highlighted_words 在 `snippet` 中被 Google 強調的關鍵字列表 (可能是搜尋字詞的變體或相關詞)
source 如果結果來自特定的新聞來源或平台, 這裡會顯示來源名稱


也可以呼叫 results 字典的 get() 方法並傳入 'organic_results', 並指定一個空串列作為預設的回傳值, 迭代回傳之字典串列並透過上表中的鍵名即可取得標題, 摘要, 與網址等 : 

>>> for result in results.get('organic_results', []):   
    print(f'標題: {result["title"]}')
    print(f'描述: {result["snippet"]}')
    print(f'網址: {result["link"]}\n')   
  
標題: 第16任總統副總統選舉
描述: 第16任總統副總統選舉 ; 金門縣. 柯文哲. /吳欣盈. 賴清德. /蕭美琴. 侯友宜. /趙少康. 1. 2. 3. 13,038. 4,569. 28,005. 28.58%. 10.02%. 61.40% ; 基隆市. 柯文哲. /吳欣盈.
網址: https://db.cec.gov.tw/ElecTable/Election/ElecTickets?dataType=tickets&typeId=ELC&subjectId=P0&legisId=00&themeId=4d83db17c1707e3defae5dc4d4e9c800&dataLevel=C&prvCode=00&cityCode=000&areaCode=00&deptCode=000&liCode=0000

標題: 2024年中華民國總統選舉
描述: 本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度由同一政黨連續三次獲勝。蔡英文八年執政雖成功交棒,但維持執政地位的民進黨則在同日 ...
網址: https://zh.wikipedia.org/zh-tw/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89

標題: 2024 總統大選即時開票
描述: 2024總統大選在即,在賴清德、侯友宜、柯文哲競逐的局勢下,最終誰會拿下總統大位?台灣會迎來新一次的政黨輪替嗎?《遠見》統合各候選人政見,針對選戰熱門關鍵字、候選人 ...
網址: https://event.gvm.com.tw/2024presidential_election/votingresults_president.html

由於通常都是搜尋繁體中文資料, 因此我寫了如下的 search_google() 來簡化 API 的調用 : 

>>> def search_google(query, serpapi_key, num=3, gl='tw', hl='zh-tw'):
    params={
        'q': query,
        'api_key': serpapi_key,
        'num': num,
        'gl': gl,
        'hl': hl
        }
    search=GoogleSearch(params)
    results=search.get_dict()
    return results.get('organic_results', [])

此函式已預設指定傳回最前面 3 筆繁體中文結果, 呼叫時只要傳入要搜尋的關鍵字 query 與 API Key 即可, 若有搜尋到資料會傳回一個包含 title (標題), snippet (描述), 與 url (網址) 等鍵的字典串列, 若沒有搜尋結果就傳回空字串, 例如 :

>>> results=search_google(query, serpapi_key)   
>>> type(results)  
<class 'list'>  
>>> len(results)  
3
>>> type(results[0])   
<class 'dict'>

可見傳回了包含 3 筆搜尋結果字典的串列, 用迴圈迭代此串列元素並顯示 title, snippet, 與 url 這三個鍵之值 : 

>>> for result in results:   
    print(f'標題: {result["title"]}')
    print(f'描述: {result["snippet"]}')
    print(f'網址: {result["link"]}\n')  
  
標題: 2024年中華民國總統選舉
描述: 本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度由同一政黨連續三次獲勝。蔡英文八年執政雖成功交棒,但維持執政地位的民進黨則在同日 ...
網址: https://zh.wikipedia.org/zh-tw/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89

標題: 第16任總統副總統選舉
描述: 第16任總統副總統選舉 ; 金門縣. 柯文哲. /吳欣盈. 賴清德. /蕭美琴. 侯友宜. /趙少康. 1. 2. 3. 13,038. 4,569. 28,005. 28.58%. 10.02%. 61.40% ; 基隆市. 柯文哲. /吳欣盈.
網址: https://db.cec.gov.tw/ElecTable/Election/ElecTickets?dataType=tickets&typeId=ELC&subjectId=P0&legisId=00&themeId=4d83db17c1707e3defae5dc4d4e9c800&dataLevel=C&prvCode=00&cityCode=000&areaCode=00&deptCode=000&liCode=0000

標題: 【Data Reporter】35張圖表,帶你看2024大選關鍵結果
描述: 2024年1月15日 —
網址: https://www.twreporter.org/a/2024-election-results-chart

結果與上面是一樣的. 

在 SerpAPI 儀錶板網頁右上角會記錄已呼叫 API 的次數, 免費帳戶每月可呼叫 100 次 :