2026年4月20日 星期一

HDD/SSD 健康狀態檢測軟體 CrystalDiskInfo

昨天成功地用 chdisk 救回 WD 5TB 硬碟後, 用 CrystalDiskInfo (CDI) 檢查了硬碟健康狀況為優良, 詢問 Gemini 得知它不僅能檢測硬碟, 還能檢測 SDD, 目前幾乎所有電腦都改用 SDD, 這種儲存跟汽車電瓶一樣, 接近壽命終點時說掛就掛, 而且內容就不回來, 所以要經常查看 SDD 健康狀態, 低於 50% 壽命時就要考慮更換或提高備份頻率, 以免後悔不及. 

以下是 Gemini 對 CDI 的說明與建議 :

===========================================================

對現代電腦來說,用 CDI 監測 SSD 的健康狀況甚至比 HDD 更重要,因為 SSD 有明確的**「寫入壽命限制」**。

不過,看 SSD 的報告時,重點指標與你剛才看那顆 5TB HDD 的邏輯不太一樣:

1. SSD 檢測的黃金指標:剩餘壽命

在 SSD 的報告中,最上方大大的「健康狀態」通常會直接顯示一個百分比,例如 「良好 98%」

  • 這是什麼: 這是根據 SSD 廠商定義的 TBW (Total Bytes Written,總寫入量) 估算出來的。

  • 怎麼看: * 100% - 90%: 像新的一樣。

    • 低於 50%: 雖然還能用,但建議開始尋找下一顆硬碟。

    • 低於 10% 或 0%: 隨時可能進入「唯讀模式」或直接掛掉,必須立刻更換。

2. 重點關注項目(對比 HDD)

打開 CDI 看你的 SSD 時,請盯住這兩項:

  • 03 剩餘壽命 (Percentage Used): 顯示已消耗的設計壽命。

  • F1 總計主機寫入量 (Total Host Writes): 這告訴你這顆 SSD 自出廠以來到底寫入了多少資料(通常以 GB 或 TB 為單位)。對於開發者來說,如果你頻繁編譯大型專案或跑大量 Log,這個數字會跳很快。

  • 01 介面 CRC 錯誤計數: 如果這個數值不是 0,通常代表你的 SATA 線或是 M.2 插槽接觸不良,而不是 SSD 本身壞了。


3. SSD 與 HDD 在 CDI 中的差異

項目HDD (傳統硬碟)SSD (固態硬碟)
主要風險物理壞軌、機械磨損閃存顆粒寫入次數耗盡
關鍵指標05 (重定位磁區)、C5 (等候重定)03 (剩餘壽命百分比)
溫度建議 50°C 以下較高,NVMe 飆到 60-70°C 算常見
異常徵兆異音、讀取極慢、chkdsk 噴錯誤突然偵測不到、檔案消失、藍屏

4. 檢查建議

切換到你的 C 槽(通常是 SSD) 看一下:

  1. 檢查百分比: 確認你的系統碟還有多少壽命。

  2. 檢查傳輸模式: 確認它是否跑在 PCIe 4.0 x4 或是你主機板支援的最高速度(如果你是用高效能 NVMe SSD)。

特別提醒:

SSD 如果顯示「黃燈(警告)」,通常不像 HDD 那樣還能透過 chkdsk 救回「邏輯損壞」。SSD 一旦顆粒老化出現錯誤,通常是不可逆的,**「備份」**是唯一的救贖


下面是我的 LG Gram 筆電兩顆 SSD 檢測結果 : 

一. C 碟 : 三星 1TB Samsung PM9A1 (MZVL21T0HCLR)




Gemini 的 SSD 健康檢查報告分析:

1. 核心健康指標

  • 健康狀態:良好 98%

    • 對於已經使用 5323 小時(約 7 個多月不間斷運作)的系統碟來說,只消耗了 2% 的壽命,這代表它的耐用度非常高。依照這個速度,這顆 SSD 要達到壽命臨界值可能還需要十年以上。

  • SSD 總寫入量:29,768 GB (約 29 TB)

    • 這反映了你作為軟體工程師的工作量。頻繁的編譯、下載與檔案操作都會累積寫入量。29 TB 雖然聽起來很多,但這顆 SSD 的設計寫入壽命通常在 600 TBW 以上,目前僅用了不到 5%。

2. 值得注意的細節

  • 傳輸模式:PCIe 4.0 x4 | PCIe 4.0 x4

    • 這代表你的硬碟正確地運行在最高頻寬下,沒有因為插槽或設定問題被降速。

  • 溫度:39°C

    • 對於 NVMe SSD 來說,這個溫度非常低且理想。這說明 LG Gram 的內部散熱設計(或這顆 Samsung SSD 的發熱控制)做得很好,在高負載運作時也不容易發生因過熱而降速(Thermal Throttling)的情況。

  • 不安全關機次數:33 次 (0x21)

    • 這通常發生在電腦當機、強制長按電源鍵關機,或是電池完全沒電自動關機時。雖然次數不多,但 SSD 對斷電比 HDD 敏感,建議盡量避免在檔案寫入時強制關機。


3. 對於這顆 SSD 的維護建議

注意, SSD 維護上做法與 HDD 完全相反:

  1. 絕對不要執行「磁碟重組」: SSD 內部有自己的「磨損均衡 (Wear Leveling)」機制,會自動安排資料存放位置以延長壽命。手動重組只會白白浪費寫入次數,對速度毫無幫助。

  2. 確保開啟 TRIM 指令: Windows 10/11 預設會開啟。它會告訴 SSD 哪些區塊的資料已刪除可以回收。

  3. 保留 15% 以上空間: 雖然 1TB 很大,但 SSD 如果塞得太滿(超過 90%),寫入放大效應會增加,導致壽命消耗變快且速度變慢。建議 C 碟隨時保留約 150GB 左右的空閒空間。

  4. 定期用 CDI 檢查「03 可用備用 (Available Spare)」: 如果這個數值(目前是 100%)開始下降,才代表 SSD 真的開始出現損壞的記憶體顆粒。

總結: 這是一顆頂級的系統碟,目前正處於巔峰狀態,無需任何特殊整理。你那顆 WD 5TB HDD 比較需要關注「整理」,而這顆 Samsung SSD 只要維持正常使用、不要塞太滿即可。


二. D 碟 WD Blue SN570 (1TB) : 




Gemini 的 SSD 健康檢查報告分析:

這顆 WD Blue SN570 簡直就是「新車剛過磨合期」的狀態!雖然你是後來擴充的,但從數據上看,這顆硬碟幾乎沒怎麼被「操」到。

以下是針對這顆擴充碟的分析:

1. 核心健康指標

  • 健康狀態:良好 100%

    • 這非常罕見,代表這顆 SSD 的設計壽命完全還沒被消耗。

  • 已用時間總計:82 小時

    • 這真的非常新!對比你 C 碟的 5323 小時,這顆 D 碟基本上才剛開始運作不久。

  • SSD 總寫入量:137 GB

    • 這個寫入量極低。以 SN570 1TB 版本高達 600 TBW 的耐用度來看,你目前只用了 $0.02\%$ 的壽命。

2. 性能與規格分析

  • 傳輸模式:PCIe 3.0 x4

    • 這是正確的。SN570 本身就是 PCIe 3.0 的規格,所以它跑在 3.0 x4 是發揮全速。雖然你的筆電插槽可能支援到 4.0(如 C 碟那顆),但這顆 D 碟會穩定運行在 3.0 的最高速。

  • 溫度:33°C

    • 比 C 碟還要低,這是一個非常涼爽的操作溫度,對電子零件的長壽非常有幫助。


3. 為什麼這顆硬碟特別適合當「資料存放區」?

SN570 是屬於「無快取 (DRAM-less)」但性能平衡的 SSD。這類硬碟的特性是:

  • 省電: 對於 LG Gram 這種強調續航的筆電非常友善。

  • 發熱低: 很適合放在筆電的第二個擴充槽。

  • 穩定: 非常適合作為存放專案代碼、文件或照片的副碟。

4. 給 Tony 的維護建議

這顆硬碟目前完全不需要任何維護。但既然它是 D 碟,我建議你可以利用它來分擔 C 碟的壓力

  • 存放開發專案: 既然你的工作涉及 Python 和金融資料分析(kbar 庫等),可以把這些專案放在 D 碟。

  • 虛擬機或 Docker: 如果有使用這些工具,將它們的映像檔放在 D 碟,可以避免 C 碟過快消耗壽命,且這顆 SN570 的速度對於這類應用綽綽有餘。

總結

你這台 LG Gram 的硬碟配置非常健康:

  1. C 碟 (Samsung): 強大的系統碟,負責作業系統與軟體運行。

  2. D 碟 (WD Blue): 極新的擴充碟,適合存放長期資料與專案。


WD 5TB 隨身硬碟無法讀取問題

週六下午在上緯育的 SSD (OpenSec) 線上課程時, 插在 USB 的 WD 5TB 隨身硬碟突然擱登兩聲離線, 我以為是碰到插頭接觸不良而已. 週日晚上要複製檔案時接上 USB, 有讀到驅動 G 碟, 但無法讀取檔案目錄結構 : 




週日晚上回到高雄後, 拿另外一條 USB 線連接筆電還是一樣, 於是問 Gemini 該怎麼辦, 它建議我先以管理員身分開啟命令提示字元視窗, 然後用 chdisk 檢查修復檔案目錄連結 : 

C:\Users\tony1>chkdsk G: /f   
檔案系統的類型是 NTFS。
磁碟區標籤為 Elements。

階段 1: 正在檢查基本檔案系統結構...
  已經處理 249088 個檔案記錄。
檔案驗證完成。
階段期間 (檔案記錄驗證): 22.14 秒。
  已經處理 47 個大型檔案記錄。
階段期間 (孤立檔案記錄修復): 0.52 毫秒。
  已經處理 0 個損毀的檔案記錄。
階段期間 (不正確檔案記錄檢查): 0.98 毫秒。

階段 2: 正在檢查檔案名稱連結...
正在修正檔案 5 中索引 $I30 的錯誤。
CHKDSK 在檔案 5 之索引 $I30 的點陣圖中發現可用空間被標示為已配置。
正在為檔案 5 中的索引 $I30 進行排序。
  已經處理 2386 個重新分析記錄。
  已經處理 276974 個索引項目。
索引驗證完成。
階段期間 (索引驗證): 3.86 分鐘。
CHKDSK 正在掃描未編製索引的檔案以重新連線到其原始目錄。
正在將孤立的檔案 $MFT (0) 復原成目錄檔案 5。
正在將孤立的檔案 $MFTMirr (1) 復原成目錄檔案 5。
正在將孤立的檔案 $LogFile (2) 復原成目錄檔案 5。
正在將孤立的檔案 $Volume (3) 復原成目錄檔案 5。
正在將孤立的檔案 $AttrDef (4) 復原成目錄檔案 5。
正在將孤立的檔案 . (5) 復原成目錄檔案 5。
正在將孤立的檔案 $Bitmap (6) 復原成目錄檔案 5。
正在將孤立的檔案 $Boot (7) 復原成目錄檔案 5。
正在將孤立的檔案 $BadClus (8) 復原成目錄檔案 5。
正在將孤立的檔案 $Secure (9) 復原成目錄檔案 5。
正在略過關於復原孤立項的後續訊息。
  已經掃描 51 個未編製索引的檔案。
  51 個未編製索引的檔案已復原到原始目錄。
階段期間 (孤立重新連接): 2.44 秒。
  0 個未編製索引的檔案已復原到 [Lost and Found]。
階段期間 (孤兒復原丟失與發現): 36.93 毫秒。
  已經處理 2386 個重新分析記錄。
階段期間 (重新剖析點與物件識別碼驗證): 14.32 毫秒。

階段 3: 正在檢查安全性描述元...
安全性描述元驗證完成。
階段期間 (安全性描述項驗證): 50.83 毫秒。
  已經處理 13943 個資料檔案。
階段期間 (資料屬性驗證): 1.80 毫秒。
正在修正主檔案表格 (MFT) 鏡像的錯誤。
正在修正磁碟區點陣圖的錯誤。

Windows 已修正檔案系統。
不需要進一步的動作。

磁碟空間總計    4769272 MB。
234806 個檔案共    4371444 MB。
13945 個索引共      71928 KB。
損壞的磁區共          0 KB。
系統使用空間     464123 KB。
記錄檔共佔用      65536 KB。
磁碟可用空間  406840116 KB。

每個配置單位有       4096 個位元組。
磁碟上共有 1220933887 個配置單位。
磁碟上有  101710029 個可用的配置單位。
總持續時間: 4.28 分鐘 (256882 毫秒)。

----------------------------------------

我把以上結果貼給 Gemini 評估, 結果如下 : 

-----------------------------------------------------------
這代表你的 5TB 硬碟硬體暫時看起來是健康的,這次純粹是軟體層面的「索引混亂」。

從你的 Log 來看,這次修復了幾個關鍵問題:

1. 修復了什麼?

  • 索引點陣圖 (Bitmap) 錯誤:這就像是圖書館的目錄說某個位置是空的,但實際上那裡放了書。chkdsk 重新校正了這些空間分配。

  • 孤立檔案 (Orphaned Files) 復原:這部分最關鍵。你的 $MFT(主檔案表格)等系統檔案一度失去了連結,導致 Windows 看不到目錄。chkdsk 把這些「走丟」的系統檔案重新接回了目錄樹(檔案 5)。

  • MFT 鏡像修正:主檔案表格的備份也被同步修正了,確保下次讀取穩定。


2. 後續建議 (SOP)

雖然現在讀得到了,但身為開發者,我們都知道「事出必有因」。為了確保這顆 5TB 隨身硬碟之後不再搞事,建議你做以下幾件事:

  • 立刻檢查 S.M.A.R.T. 資訊: 雖然 chkdsk 顯示 0 壞軌,但那是「邏輯層面」。建議用 CrystalDiskInfo 看一下 05 (重新分配磁區數)C5 (等候重映射的磁區數) 是否為 0。如果這兩個數值不是 0,代表硬碟物理壽命快到頭了,趕快趁現在備份。

  • 養成「安全移除」的習慣: 這次的錯誤(索引與點陣圖不符)最常發生在資料寫入中途拔掉 USB,或是 USB 供電不穩(例如插在前端 USB 孔或 Hub 上)。下次拔除前,務必點選右下角的「安全移除硬體」。

  • 停用「快速啟動」或檢查寫入快取: 如果你常用這顆硬碟,可以在「裝置管理員」>「磁碟機」>「WD Elements」內容中,確認「原則」設定。如果追求安全,可以選擇「快速移除」(停用寫入快取)。


3. 關於資料安全

這顆硬碟已經 4.3TB 滿載(剩下約 400GB),這對 5TB 硬碟來說負擔不小,且檔案系統毀損過一次後,穩定性會略微下降。

建議下一步: 既然現在恢復了,趁這幾天趕快把裡面最不可取代的資料(例如你的 e-book 草稿、kbar 原始碼等)同步到雲端或另一顆實體硬碟上。

-----------------------------------------------------

做完 chdisk 果然就恢復正常了, 還好能修復連結, 否則裡面去年以來的上課資料毀壞那就虧大了. 接下來我按照其建議去下載安裝 CrystalDiskInfo 來檢測硬碟狀況, 此軟體是日本人開發的, 體積很小直接到微軟 Store 下載安裝 (網路上的免安裝版我用 VirusTotal 掃描幾乎都有夾帶木馬) :


檢測結果如下 : 




貼給 Gemini 分析結果如下 : 

這份健康報告非常漂亮,這顆 WD 5TB 隨身硬碟目前的硬體狀態非常健康

以下是針對這張截圖的幾個核心分析:

1. 關鍵健康指標 (核心數據)

  • 健康狀態:良好。這是最直觀的結果。

  • 05 (重定位磁區計數): 原始值為 0。這代表硬碟沒有任何物理壞軌被替換掉,碟盤表面狀況良好。

  • C5 (等候重定的磁區計數): 原始值為 0。這代表沒有任何疑似壞軌在排隊。

  • C6 (無法校正的磁區計數): 原始值為 0。再次確認磁區資料讀寫穩定。

2. 使用記錄

  • 已用時間總計:3931 小時。這顆硬碟大約已經服役了 5 個多月(如果是 24 小時不關機計算),對於一顆隨身硬碟來說,正處於「壯年期」。

  • 已用次數總計:578 次。平均每次開機使用約 6.8 小時。

  • 轉速:4800 RPM。這是典型的 2.5 吋節能/高容量硬碟規格,發熱量會比較低,目前的 37°C 非常理想。


3. 為什麼會發生「檔案目錄毀損」?

既然硬體沒問題,那為什麼會噴錯誤?最可能的兇手有三個:

  1. 異常拔除: 在 Windows 還在寫入快取資料時,就直接拔掉 USB 線,導致 NTFS 的 MFT (Master File Table) 沒寫完。

  2. 供電瞬間不穩: 5TB 的碟盤密度很高,對電流波動敏感。如果插在不穩的 USB 插座,讀寫中途掉電也會導致邏輯錯誤。

  3. 磁碟已滿的副作用: 你的硬碟已經用了 4.3TB 以上,當空間極度剩餘不足時,NTFS 的碎片整理或元數據更新會變得很頻繁,這時候若發生中斷,毀損機率會變高。

4. 維護建議

  • 繼續使用: 沒問題,它不是快掛了,只是「軟體感冒」剛修好。

  • 安全退出: 之後請務必點選 Windows 右下角的「安全移除硬體」。

  • 避免前端插槽: 建議優先插在電腦機殼後方的 USB 孔(或供電足夠的電競筆電插孔),穩定電壓是 5TB 硬碟的長壽關鍵。

------------------------------------------------

這顆硬碟就回來了真是好佳在啊! 趕緊開啟 MIS 桌機, 把內容複製到 18TB 大硬碟裡備份. 這顆也快裝滿了, 也準備要換新的一顆來用了. 

2026年4月19日 星期日

2026 年第 15 周記事

又過了一周了, 四月下旬天氣變得熱起來, 今年的芒果樹結果不多, 菜園的只有兩棵樹有結果, 另外路邊的只有零星結一些, 我今天下午拿去年剩餘的袋子去套袋, 只完成菜園的那兩棵樹, 路邊的等下周再套. 套袋時在儲水池旁的芒果樹上發現一個鳥巢, 裡面有孵化的雛鳥兩隻 :




我傳給 Gemini 辨認, 說是白頭翁的雛鳥 :




這幾天回頭整理我的 5 台 Linux 主機, 其中三台樹莓派是我的主要爬蟲主機, 前陣子不明原因當機, 直到前幾天才有時間重灌, 順便把爬蟲程式升版. 接下來要做的有下列三件 :
  • Serverless 平台加入 API Key 
  • 高雄光世代固定 IP & 全屋通路由器設定
  • Mapleboard 外接 USB 儲存設定
  • 鄉下的 Pi 3 主機升版為 Bulleye
鄉下的 Pi 3 主機目前跑的是 Stretch, 這根本是老古董了, 今天已備份 SD 卡內資料, 準備買一張 32 GB 卡來升版為 Bulleye. 

2026年4月18日 星期六

Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v13

將母校圖書館爬蟲升版後打鐵趁熱, 今天順便將此次優化 Selenium 爬蟲的技巧也套用在市圖爬蟲程式上, 可同時於 Pi 400, Pi 3B, 與 Pi 3A+ 上執行. 

本系列全部測試文章索引參考 :


新版 v13 程式碼如下 :

# ksml_lib_13.py
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import re
from datetime import datetime
import time
import requests
import sys
from dotenv import dotenv_values
import os
import socket

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

def get_books(account, password):
    browser=None
    result=(None, None)  # 預設回傳值   
    try:
        # 登入我的書房
        # 設定一個在 SD 卡上的暫存目錄 (for Trixie)
        chrome_tmp_path=os.path.expanduser('~/chrome_tmp')
        if not os.path.exists(chrome_tmp_path):
            os.makedirs(chrome_tmp_path)        
        options=Options()
        options.add_argument("--headless=new") # 新版無頭擬真瀏覽器
        options.add_argument("--no-sandbox") # Trixie 必加
        options.add_argument("--disable-dev-shm-usage") # 避免擠爆 /dev/shm
        options.add_argument('--disable-gpu') # 避免 GPU 驅動崩潰
        # 強迫使用 SD 卡空間 (特別是 Trixie 必須)
        options.add_argument(f'--user-data-dir={chrome_tmp_path}')
        # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)
        options.add_argument('--disk-cache-size=104857600')
        options.binary_location='/usr/bin/chromium'       
        service=Service('/usr/bin/chromedriver')
        browser=webdriver.Chrome(service=service, options=options)
        browser.implicitly_wait(60)
        browser.set_window_size(1920, 1080)        
        # 載入網頁
        browser.get('https://webpacx.ksml.edu.tw/personal/')
        loginid=browser.find_element(By.ID, 'logxinid')
        loginid.send_keys(account)
        pincode=browser.find_element(By.ID, 'pincode')
        pincode.send_keys(password)
        div_btn_grp=browser.find_element(By.CLASS_NAME, 'btn_grp')
        login_btn=div_btn_grp.find_element(By.TAG_NAME, 'input')
        login_btn.click()
        # 擷取借閱紀錄
        div_redblock=browser.find_element(By.CLASS_NAME, 'redblock')
        div_redblock.click()
        books=browser.find_elements(By.CLASS_NAME, 'bookdata')
        borrow_books=[]
        for book in books:
            item=dict()
            book_name=book.find_element(By.XPATH, './h2/a').text    
            item['book_name']=book_name.replace('/', '').strip()
            book_site=book.find_element(By.XPATH, './ul[3]/li[1]').text
            reg=r'典藏地:(\S+)'
            item['book_site']=re.findall(reg, book_site)[0]
            reg=r'\d{4}-\d{2}-\d{2}'
            due_date=book.find_element(By.XPATH, './ul[4]/li[2]').text
            item['due_date']=re.findall(reg, due_date)[0] 
            due_times=book.find_element(By.XPATH, './ul[5]/li[1]').text
            item['due_times']=re.findall(r'\d{1}', due_times)[0]
            try: 
                state=book.find_element(By.XPATH, './ul[6]/li[1]').text
            except:
                state=''
            finally:
                if '有人預約' in state:
                    item['state']=', 有人預約'
                else:
                    item['state']=''
            borrow_books.append(item)
        print('擷取借閱紀錄 ... OK')
        browser.back() # 回上一頁
        # 擷取預約紀錄
        div_blueblock=browser.find_element(By.CLASS_NAME, 'blueblock')
        div_blueblock.click()
        books=browser.find_elements(By.CLASS_NAME, 'bookdata')
        reserve_books=[]
        for book in books:
            item=dict()
            book_name=book.find_element(By.XPATH, './h2/a').text    
            item['book_name']=book_name.replace('/', '').strip()
            sequence=book.find_element(By.XPATH, './ul[7]/li[1]').text
            if '預約待取' in sequence:  # 已到館
                item['ready_for_pickup']=True
                reg=r'\d{4}-\d{2}-\d{2}'
                item['expiration']=re.findall(reg, sequence)[0]
                item['sequence']='0'
            else: # 預約中
                item['ready_for_pickup']=False
                item['expiration']=''
                item['sequence']=re.findall(r'\d+', sequence)[0]
            reserve_books.append(item)
        print('擷取預約紀錄 ... OK')
        result=(borrow_books, reserve_books)        
    except Exception as e:
        print(f'發生錯誤 : {e}')
    finally:
        if browser:
            try:
                browser.quit()  # 釋放記憶體
                print('資源已釋放')
            except:
                pass            
    return result   
    
if __name__ == '__main__':
    start=time.time()
    config=dotenv_values('.env')
    TELEGRAM_TOKEN=config.get('TELEGRAM_TOKEN')
    TELEGRAM_ID=config.get('TELEGRAM_ID')
    #print(TELEGRAM_TOKEN)
    #print(TELEGRAM_ID)
    host_name=socket.gethostname()
    print(f'主機 : {host_name}')    
    if len(sys.argv) != 3:
        print(f'用法: {sys.argv[0]} 帳號 密碼')
        sys.exit(1)
    # 取得傳入的帳密參數
    account=sys.argv[1]
    password=sys.argv[2]
    # 呼叫 get_books() 取得借書與預約書        
    borrow_books, reserve_books=get_books(account, password)
    b_msg=''  # 借書資訊字串初始值
    r_msg=''  # 預約資訊字串初始值
    # 處理借書 
    if borrow_books: 
        borrow=[]
        for book in borrow_books:
            book_name=book['book_name']
            book_site=book['book_site'] 
            due_times=book['due_times']
            due_date=book['due_date']
            state=book['state']
            due_date=datetime.strptime(due_date, '%Y-%m-%d') # 到期日   
            today_str=datetime.today().strftime('%Y-%m-%d')   
            today=datetime.strptime(today_str, "%Y-%m-%d")   
            delta=(due_date-today).days  # 計算離到期日還有幾天
            if delta < 0:  # 負數=已逾期
                msg=f'🅧 {book_name} (逾期 {abs(delta)} 天{state}, {book_site})'
                borrow.append(msg)
            elif delta == 0:  # 0=今天到期
                msg=f'⓿ {book_name} (今日到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif delta == 1:  # 1=明天到期 
                msg=f'❶ {book_name} (明日到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif delta == 2:  # 2=後天到期 
                msg=f'❷ {book_name} (後天到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif 2 < delta < 8:  # 3 天以上一周內到期
                msg=f'✦ {book_name} ({book["due_date"]} 到期, '\
                    f'續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
        # 製作借書到期摘要字串 
        if len(borrow) != 0:
            borrow.insert(0, f'\n❖ {account} 的借閱 :')
            b_msg='\n'.join(borrow)  # 更新借書資訊字串
        print('產生借書到期摘要 ... OK')
    # 處理預約書
    if reserve_books:
        reserve=[]
        i=0
        j=['①', '②', '③', '④', '⑤']
        k=['❶', '❷', '❸', '❹', '❺']
        # 預約狀態
        for book in reserve_books:
            book_name=book['book_name']
            sequence=book['sequence']
            ready_for_pickup=book['ready_for_pickup'] # 已到館
            expiration=book['expiration']  # 取書截止日
            if ready_for_pickup:
                msg=f'{k[i]} {book_name} (已到館, 保留期限 {expiration})'
            else:
                msg=f'{j[i]} {book_name} (順位 {sequence})'
            reserve.append(msg)
            i += 1
        # 製作預約書摘要字串    
        if len(reserve) != 0:
            reserve.insert(0, f'\n❖ {account} 的預約 :')
            r_msg='\n'.join(reserve)  # 更新資訊字串
    print('產生預約書摘要 ... OK')
    if b_msg or r_msg:  # 任一不為空字串就更新資料表
        url="https://serverless-5e6i.onrender.com/function/update_ksml_books"
        payload={
            "account": account,
            "borrow_books": b_msg,   
            "reserve_books": r_msg
            }
        res=requests.post(url, json=payload)
        print(res.json())        
    end=time.time()
    print(f'執行時間:{end-start}')

此次改版也修正了 try-except-finally 結構, 把 browser.close() 改成 browser.quit(), 前者雖然會把視窗關掉, 但背景的 chromedriver 可能還在跑繼續吃 RAM, 導致背景殘留了一堆 chromedriver 的殭屍進程. 此優化版程式在 Pi 3B, 3A+ 與 Pi 400 均可順利執行. 

執行結果如下 : 

pi@kaopi3:~ $ python ksml_lib_12.py faxxxxxx 123456   
擷取借閱紀錄 ... OK
擷取預約紀錄 ... OK
產生借書到期摘要 ... OK
產生預約書摘要 ... OK
{'message': 'faxxxxxx 的資料已更新', 'status': 'success'}
執行時間:667.9564106464386

pi@pi3aplus:~ $ python ksml_lib_12.py faxxxxxx 123456
主機 : pi3aplus
擷取借閱紀錄 ... OK
擷取預約紀錄 ... OK
資源已釋放
產生借書到期摘要 ... OK
產生預約書摘要 ... OK
{'message': 'faxxxxxx 的資料已更新', 'status': 'success'}
執行時間:675.6979095935822

(myenv313) pi@pi400:~ $ python ksml_lib_12.py faxxxxxx 123456
主機 : pi400
擷取借閱紀錄 ... OK
擷取預約紀錄 ... OK
資源已釋放
產生借書到期摘要 ... OK
產生預約書摘要 ... OK
{'message': 'faxxxxxx 的資料已更新', 'status': 'success'}
執行時間:573.2406423091888

可見 Pi 3B 與 3A+ 速度差不多, 但 Pi 400 就快了 100 秒. 

注意, 此處呼叫的後端端點有兩個, 都是建置在 Render 平台上的 serverless 服務, 目前我有兩個端點, 分配如下 :
  • kaopi3 : https://serverless-5e6i.onrender.com/function/send_books_messages
  • pi3aplus : https://serverless-fdof.onrender.com/function/send_books_messages
  • pi400 : https://serverless-fdof.onrender.com/function/send_books_messages (備用)
擷取並傳送借書資訊的程式 get_ksml_books_messages.py 也是要設定對應端點, 例如 kaopi3 :

# get_ksml_books_messages.py
import requests
import socket

host_name=socket.gethostname()
print(f'主機 : {host_name}')
params={'crawler': f'{host_name}'}
url='https://serverless-5e6i.onrender.com/function/send_books_messages'
res=requests.get(url, params=params)
print(res)

此爬蟲程式會呼叫 serverless 平台上的 send_books_messages.py 函式讀取 serverless.db 上記錄的借書與預約資訊, 並送出 Telegram 訊息. send_books_messages.py 函式內容如下 :

# send_ksml_books_messages.py
import asyncio
import sqlite3
from telegram import Bot

async def telegram_send_text(token, chat_id, text):
    """非同步傳送 Telegram 訊息"""
    try:
        bot=Bot(token=token)
        await bot.send_message(chat_id=chat_id, text=text)
        return True
    except Exception as e:
        print(f"傳送失敗: {e}")
        return False

def main(request, **kwargs):
    DB_PATH='./serverless.db'
    config=kwargs.get('config', {})
    telegram_token=config.get('TELEGRAM_TOKEN')
    telegram_id=config.get('TELEGRAM_ID')
    if not telegram_token or not telegram_id:
        return '未設定 TELEGRAM_TOKEN 或 TELEGRAM_ID'
    try:  # 連線資料庫
        conn=sqlite3.connect(DB_PATH)
        cur=conn.cursor()
        cur.execute("SELECT borrow_books, reserve_books FROM ksml_books;")
        rows=cur.fetchall()
        conn.close()
    except Exception as e:
        return f'資料庫讀取失敗: {e}'
    if not rows:
        return '沒有任何資料可傳送'
    # 傳送訊息
    success_count=0
    fail_count=0
    for borrow_books, reserve_books in rows:
        for msg in [borrow_books, reserve_books]:
            if msg and msg.strip():
                ok=asyncio.run(telegram_send_text(telegram_token, telegram_id, msg))
                if ok:
                    success_count += 1
                else:
                    fail_count += 1
    return f'傳送完成:成功 {success_count} 筆,失敗 {fail_count} 筆'

市圖爬蟲架構較複雜, 得畫一張圖來備忘才行. 

Python 學習筆記 : 母校圖書館借書與預約爬蟲程式改版 v11

之前版本的爬蟲我都把帳號密碼金鑰等等都寫在程式碼裡, 這實在不是好的做法, 今天將爬蟲進行改版, 將帳密金鑰都存放在隱藏檔 .env 中, 於程式裡利用 dotenv 從 .env 檔讀取出來. 關於 dotenv 用法參考 :


本系列全部文章索引參考 :


先將目前的第 10 版爬蟲複製一份到第 11 版 :

pi@kaopi3:~ $ cp nkust_lib_10.py nkust_lib_11.py    

用 nano 編輯程式碼 :

pi@kaopi3:~ $ nano nkust_lib_11.py   

修改為如下 : 

# nkust_lib_11.py
from selenium import webdriver   
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import time
import requests
from datetime import datetime
import asyncio
from telegram import Bot
import re
from dotenv import dotenv_values

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

def get_nkust_lib():
    browser=None
    msg="無法取得資料"  # ✅ 防止未賦值
    try:
        options=Options()
        options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.binary_location="/usr/bin/chromium"
        service=Service("/usr/bin/chromedriver")
        browser=webdriver.Chrome(service=service, options=options)
        browser.implicitly_wait(60)
        browser.set_window_size(1920, 1080)

        url=(
            'https://nkust.primo.exlibrisgroup.com/discovery/login?'
            'vid=886NKUST_INST:86NKUST&lang=zh-tw'
        )
        browser.get(url)

        # 按其他讀者
        md_list=browser.find_elements(By.TAG_NAME, 'md-list-item')
        if len(md_list) > 1:
            md_list[1].click()
            print('按其他讀者 ... OK')

        # 登入系統
        login_user_name=browser.find_element(By.ID, 'LoginUserName')   
        login_user_name.send_keys(NKUST_LIB_ID)  
        login_password=browser.find_element(By.ID, 'LoginPassword')   
        login_password.send_keys(NKUST_LIB_PWD)
        login_btn=browser.find_element(By.CLASS_NAME, 'button-large')
        login_btn.click()
        print('登入系統 ... OK')

        # 按名字顯現選單
        user_btn=browser.find_element(By.CLASS_NAME, 'user-button')
        actions=ActionChains(browser)
        actions.move_to_element(user_btn)
        actions.click(user_btn)
        actions.perform()
        print('按名字顯現選單 ... OK')

        # 按我的借閱鈕
        xpath='/html/body/div[3]/md-menu-content/md-menu-item[3]/button'    
        my_borrow=browser.find_element(By.XPATH, xpath)   
        actions.move_to_element(my_borrow)
        actions.click(my_borrow)
        actions.perform()
        print('按我的借閱 ... OK')

        # 按全部續借
        xpath=(
            '/html/body/primo-explore/div/prm-account/md-content'
            '/div[2]/prm-account-overview/md-content/md-tabs/'
            'md-tabs-content-wrapper/md-tab-content[2]/div/'
            'div/prm-loans/div[1]/div[2]/div[2]/button'
        )
        all_borrow=browser.find_element(By.XPATH, xpath)
        actions.move_to_element(all_borrow)
        actions.click(all_borrow)
        actions.perform()
        print('按全部續借 ... OK')

        # 檢查續借結果
        xpath=(
            '/html/body/primo-explore/div/prm-account/md-content'
            '/div[2]/prm-account-overview/md-content/md-tabs'
            '/md-tabs-content-wrapper/md-tab-content[2]/div/div'
            '/prm-loans/div[2]/prm-alert-bar/div/div/span'
        )
        alert_span=browser.find_element(By.XPATH, xpath)
        if '所有借閱資料已成功續借' in alert_span.text:
            msg='❖ 所有借閱資料已成功續借'
        else:
            msg='❖ 只有部分借閱資料已成功續借'

            # 檢查是否有 "載入更多結果" 按鈕
            for i in range(3):  # 最多 3 頁
                load_more=browser.find_elements(By.CLASS_NAME, 'button-confirm')
                if not load_more:
                    break
                load_more[0].click()
                time.sleep(2)

            # 抓取所有借閱書目
            loan_items=browser.find_elements(By.TAG_NAME, 'md-list-item')
            unrenew_books=[]

            for item in loan_items:
                try:
                    # ✅ 1. 必須有書名才是書
                    title_elem=item.find_elements(By.CSS_SELECTOR, "h3 a")
                    if not title_elem:
                        continue
                    title=title_elem[0].text.strip()
                    item_text=item.text.strip()

                    # ✅ 2. 取得到期日
                    due_text=''
                    due_elem=item.find_elements(By.XPATH, './/p[@data-qa="automation_mlc_record_date"]')
                    if due_elem:
                        m=re.search(r'\d{2}/\d{2}/\d{4}', due_elem[0].text)
                        if m:
                            due_text=m.group(0)

                    # ✅ 3. 只收真正「被預約 / 無法續借」
                    if any(k in item_text for k in ["被預約", "無法續借", "recall"]):
                        unrenew_books.append((title, due_text))
                except Exception:
                    continue
            if unrenew_books:
                msg="被預約的書:\n" + "\n".join(
                    [f"{i+1:>2}. {t[0]} 到期日: {t[1]}" for i, t in enumerate(unrenew_books)]
                )
            else:
                msg="全部書籍皆已續借"
            print('搜尋被預約書籍 ... OK')

    except Exception as e:
        print(e)
    finally:
        if browser:
            browser.quit()  # ✅ 防呆
        return msg  # ✅ msg 一定有值

if __name__ == '__main__':
    start=time.time()
    config=dotenv_values('.env')
    NKUST_LIB_ID=config.get('NKUST_LIB_ID')
    NKUST_LIB_PWD=config.get('NKUST_LIB_PWD')
    TELEGRAM_TOKEN=config.get('TELEGRAM_TOKEN')
    TELEGRAM_ID=config.get('TELEGRAM_ID')
    #print(NKUST_LIB_ID)
    #print(NKUST_LIB_PWD)
    #print(TELEGRAM_TOKEN)
    #print(TELEGRAM_ID)
    msg=get_nkust_lib()
    if msg:
        now=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        msg='\n' + now + '\n' + msg
        if asyncio.run(telegram_send_text(msg)):
            print('訊息傳送成功!')
        else:
            print('訊息傳送失敗!')
    print(msg)
    end=time.time()
    print(f'執行時間:{end-start}')

黃底高亮者為修改或添加的部分, 執行結果如下 :

pi@kaopi3:~ $ python nkust_lib_11.py   
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
訊息傳送成功!

2026-04-18 00:37:52
❖ 所有借閱資料已成功續借
執行時間:81.74577593803406


2026-04-18 補充 : 

早上又對程式碼進行局部微調, 版本號仍維持 v11, 主要修改如下 :

1. 添加主機名稱訊息 : 
利用 socket.gethostname() 即可得到主機名稱. 這樣收到 Telegram 才知道是哪台主機的爬蟲完成的.

2. 使用 SD 卡儲存 Chrome/Chromium 暫存資料 : 
由於 Pi 400 的 Trixie 仍然開發演進中, 最新版本改用 Chrome 而非 Chromium, 跑 Selenium 爬蟲時使用 DRAM 來暫存資料, 可能導致 DRAM 塞滿而使瀏覽器閃退, 於是改為強制將暫存資料放在 chrome_tmp 資料夾下, 瀏覽器設定修改為 : 

        # 設定一個在 SD 卡上的暫存目錄 (for Trixie)
        chrome_tmp_path=os.path.expanduser('~/chrome_tmp')
        if not os.path.exists(chrome_tmp_path):
            os.makedirs(chrome_tmp_path)        
        options=Options()
        options.add_argument("--headless=new") # 新版無頭擬真瀏覽器
        options.add_argument("--no-sandbox") # Trixie 必加
        options.add_argument("--disable-dev-shm-usage") # 避免擠爆 /dev/shm
        options.add_argument('--disable-gpu') # 避免 GPU 驅動崩潰
        # 強迫使用 SD 卡空間 (特別是 Trixie 必須)
        options.add_argument(f'--user-data-dir={chrome_tmp_path}')
        # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)
        options.add_argument('--disk-cache-size=104857600') 

以上設定雖然是針對 Trixie, 但在 Bulleye (Pi 3 & Pi 3A+) 上跑也是可以的. 完整程式碼如下 :

# nkust_lib_11.py
from selenium import webdriver   
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
import time
import requests
from datetime import datetime
import asyncio
from telegram import Bot
import re
from dotenv import dotenv_values
import os
import socket

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

def get_nkust_lib():
    browser=None
    msg="無法取得資料"  # ✅ 防止未賦值
    try:
        # 設定一個在 SD 卡上的暫存目錄 (for Trixie)
        chrome_tmp_path=os.path.expanduser('~/chrome_tmp')
        if not os.path.exists(chrome_tmp_path):
            os.makedirs(chrome_tmp_path)        
        options=Options()
        options.add_argument("--headless=new") # 新版無頭擬真瀏覽器
        options.add_argument("--no-sandbox") # Trixie 必加
        options.add_argument("--disable-dev-shm-usage") # 避免擠爆 /dev/shm
        options.add_argument('--disable-gpu') # 避免 GPU 驅動崩潰
        # 強迫使用 SD 卡空間 (特別是 Trixie 必須)
        options.add_argument(f'--user-data-dir={chrome_tmp_path}')
        # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)
        options.add_argument('--disk-cache-size=104857600') 
        options.binary_location="/usr/bin/chromium"
        service=Service("/usr/bin/chromedriver")
        browser=webdriver.Chrome(service=service, options=options)
        browser.implicitly_wait(60)
        browser.set_window_size(1920, 1080)

        url=(
            'https://nkust.primo.exlibrisgroup.com/discovery/login?'
            'vid=886NKUST_INST:86NKUST&lang=zh-tw'
        )
        browser.get(url)

        # 按其他讀者
        md_list=browser.find_elements(By.TAG_NAME, 'md-list-item')
        if len(md_list) > 1:
            md_list[1].click()
            print('按其他讀者 ... OK')

        # 登入系統
        login_user_name=browser.find_element(By.ID, 'LoginUserName')   
        login_user_name.send_keys(NKUST_LIB_ID)  
        login_password=browser.find_element(By.ID, 'LoginPassword')   
        login_password.send_keys(NKUST_LIB_PWD)
        login_btn=browser.find_element(By.CLASS_NAME, 'button-large')
        login_btn.click()
        print('登入系統 ... OK')

        # 按名字顯現選單
        user_btn=browser.find_element(By.CLASS_NAME, 'user-button')
        actions=ActionChains(browser)
        actions.move_to_element(user_btn)
        actions.click(user_btn)
        actions.perform()
        print('按名字顯現選單 ... OK')

        # 按我的借閱鈕
        xpath='/html/body/div[3]/md-menu-content/md-menu-item[3]/button'    
        my_borrow=browser.find_element(By.XPATH, xpath)   
        actions.move_to_element(my_borrow)
        actions.click(my_borrow)
        actions.perform()
        print('按我的借閱 ... OK')

        # 按全部續借
        xpath=(
            '/html/body/primo-explore/div/prm-account/md-content'
            '/div[2]/prm-account-overview/md-content/md-tabs/'
            'md-tabs-content-wrapper/md-tab-content[2]/div/'
            'div/prm-loans/div[1]/div[2]/div[2]/button'
        )
        all_borrow=browser.find_element(By.XPATH, xpath)
        actions.move_to_element(all_borrow)
        actions.click(all_borrow)
        actions.perform()
        print('按全部續借 ... OK')

        # 檢查續借結果
        xpath=(
            '/html/body/primo-explore/div/prm-account/md-content'
            '/div[2]/prm-account-overview/md-content/md-tabs'
            '/md-tabs-content-wrapper/md-tab-content[2]/div/div'
            '/prm-loans/div[2]/prm-alert-bar/div/div/span'
        )
        alert_span=browser.find_element(By.XPATH, xpath)
        if '所有借閱資料已成功續借' in alert_span.text:
            msg='❖ 所有借閱資料已成功續借'
        else:
            msg='❖ 只有部分借閱資料已成功續借'

            # 檢查是否有 "載入更多結果" 按鈕
            for i in range(3):  # 最多 3 頁
                load_more=browser.find_elements(By.CLASS_NAME, 'button-confirm')
                if not load_more:
                    break
                load_more[0].click()
                time.sleep(2)

            # 抓取所有借閱書目
            loan_items=browser.find_elements(By.TAG_NAME, 'md-list-item')
            unrenew_books=[]

            for item in loan_items:
                try:
                    # ✅ 1. 必須有書名才是書
                    title_elem=item.find_elements(By.CSS_SELECTOR, "h3 a")
                    if not title_elem:
                        continue
                    title=title_elem[0].text.strip()
                    item_text=item.text.strip()

                    # ✅ 2. 取得到期日
                    due_text=''
                    due_elem=item.find_elements(By.XPATH, './/p[@data-qa="automation_mlc_record_date"]')
                    if due_elem:
                        m=re.search(r'\d{2}/\d{2}/\d{4}', due_elem[0].text)
                        if m:
                            due_text=m.group(0)

                    # ✅ 3. 只收真正「被預約 / 無法續借」
                    if any(k in item_text for k in ["被預約", "無法續借", "recall"]):
                        unrenew_books.append((title, due_text))
                except Exception:
                    continue
            if unrenew_books:
                msg="被預約的書:\n" + "\n".join(
                    [f"{i+1:>2}. {t[0]} 到期日: {t[1]}" for i, t in enumerate(unrenew_books)]
                )
            else:
                msg="全部書籍皆已續借"
            print('搜尋被預約書籍 ... OK')

    except Exception as e:
        print(e)
    finally:
        if browser:
            browser.quit()  # ✅ 防呆
        return msg  # ✅ msg 一定有值

if __name__ == '__main__':
    start=time.time()
    config=dotenv_values('.env')
    NKUST_LIB_ID=config.get('NKUST_LIB_ID')
    NKUST_LIB_PWD=config.get('NKUST_LIB_PWD')
    TELEGRAM_TOKEN=config.get('TELEGRAM_TOKEN')
    TELEGRAM_ID=config.get('TELEGRAM_ID')
    #print(NKUST_LIB_ID)
    #print(NKUST_LIB_PWD)
    #print(TELEGRAM_TOKEN)
    #print(TELEGRAM_ID)
    host_name=socket.gethostname()
    print(f'主機 : {host_name}')
    msg=get_nkust_lib()
    if msg:
        now=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        msg='\n' + now + '\n' + msg + '\n' + f'(host_name)'
        if asyncio.run(telegram_send_text(msg)):
            print('訊息傳送成功!')
        else:
            print('訊息傳送失敗!')
    print(msg)
    end=time.time()
    print(f'執行時間:{end-start}')

執行結果 : 

pi@kaopi3:~ $ python nkust_lib_11.py
主機 : kaopi3
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
搜尋被預約書籍 ... OK
訊息傳送成功!

2026-04-18 11:14:19
全部書籍皆已續借
(kaopi3)
執行時間:358.6611738204956

2026年4月17日 星期五

Google Gemini API 學習索引

過去兩年來串接大語言模型 API 我最常用的是 OpenAI, 這必須要先綁信用卡購買隨用即付額度才能使用; 而 Google Gemini 則不需要, 免費仔目前 gemini-2.5-flash 享有的請求限制為 10 次/分, 25 萬 token/分, 250 次/日, 對於新手非常慷慨. 最近打算對 Gemini API 做較全面的測試, 先把過去的測試文章整理程如下索引以利查考 :


~ 進行中 ~

Google Gemini API 學習筆記 : 新舊 API 呼叫方法整理

Google 在 2024 年底推出新版的 SDK 套件 google-genai 來取代舊版的 google-generativeai, 目前我的 LG Gram 筆電的虛擬環境安裝的最新 langchain-core 就是依賴於 google-genai, 但昨天在 Pi 3A+ 的 Bulleye 上測試發現無法安裝新版的 google-genai, 只能用舊版的 google-generativeai, 所以我將呼叫原生 Gemini API 時新舊兩種用法整理如下備查. 參考 :



1. 舊版 Gemin API (google-generativeai) 用法 :

安裝 :

pip install google-generativeai 

匯入 :

import google.generativeai as genai 

設定金鑰 :

genai.configure(api_key=api_key)

建立模型 :

model=genai.GenerativeModel('gemini-2.5-flash') 

提問 :

reply=model.generate_content('你是誰?')  

取得回覆 : 

print(reply.text)


2. 新版 Gemin API (google-genai) 用法 :

安裝 :

pip install google-genai 

匯入 :

from google import genai 

建立 Client 物件 (設定金鑰) :

client=genai.Client(api_key=api_key)

建立模型 & 提問 :

reply=client.models.generate_content(
    model='gemini-2.5-flash', 
    contents='你是誰?'
    )

取得回覆 : 

print(reply.text)

樹莓派學習筆記 : 在 Bulleye 上安裝 google-generativeai 套件

昨天成功重灌 Pi 3A+ 的 Bulleye 後嘗試安裝 langchain-core, 結果因為版本衝突不順利, 且就算安裝成功, 由於 Pi 3A+ 只有 512MB DRAM, 跑 langchain 太沉重了只好放棄, 改為安裝原生 SDK. 安裝 openai 套件成功且可順利匯入 :

pi@pi3aplus:~ $ pip install openai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting openai
...(略)...
Installing collected packages: openai
Successfully installed openai-2.32.0
pi@pi3aplus:~ $ python   
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from openai import OpenAI   
>>> exit()  

安裝 Gemini 舊版 API 的 google-generativeai 套件 : 

pi@pi3aplus:~ $ pip install google-generativeai   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai
  Downloading google_generativeai-0.8.6-py3-none-any.whl (155 kB)
...(略)...
Successfully installed google-ai-generativelanguage-0.6.15 google-api-python-client-2.194.0 google-auth-httplib2-0.3.1 google-generativeai-0.8.6 grpcio-1.80.0 grpcio-status-1.71.2 httplib2-0.31.2 protobuf-5.29.6 uritemplate-4.2.0

但用 import google.generativeai as genai 匯入時會出現 grpcio 相關錯誤, 原因也是版本衝突問題, AI 建議改用下列安裝指令鎖住版本 : 

pi@pi3aplus:~ $ pip install "google-generativeai==0.3.1" \
            "google-ai-generativelanguage==0.4.0" \
            "grpcio==1.54.2" \
            "grpcio-status==1.54.2" \
            "protobuf==4.25.3" \
            --force-reinstall --no-cache-dir   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting google-generativeai==0.3.1
  Downloading google_generativeai-0.3.1-py3-none-any.whl (146 kB)
...(略)...
Successfully installed certifi-2026.2.25 cffi-2.0.0 charset-normalizer-3.4.7 cryptography-46.0.7 google-ai-generativelanguage-0.4.0 google-api-core-2.29.0 google-auth-2.49.2 google-generativeai-0.3.1 googleapis-common-protos-1.73.0 grpcio-1.54.2 grpcio-status-1.54.2 idna-3.11 proto-plus-1.27.1 protobuf-4.25.3 pyasn1-0.6.3 pyasn1-modules-0.4.2 pycparser-2.23 requests-2.32.5 tqdm-4.67.3 typing-extensions-4.15.0 urllib3-2.6.3

這樣匯入時就只報出無關緊要的 Warning 了 (這些警告只是提醒 : Python 3.9 太舊了, 以後可能不支援而已) : 

pi@pi3aplus:~ $ python  
Python 3.9.2 (default, Jan 24 2026, 09:41:14) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.generativeai as genai   
/home/pi/.local/lib/python3.9/site-packages/google/api_core/_python_version_support.py:246: FutureWarning: You are using a non-supported Python version (3.9.2). Google will not post any further updates to google.api_core supporting this Python version. Please upgrade to the latest Python version, or at least Python 3.10, and then update google.api_core.
  warnings.warn(message, FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/auth/__init__.py:54: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)
/home/pi/.local/lib/python3.9/site-packages/google/oauth2/__init__.py:40: FutureWarning: You are using a Python version 3.9 past its end of life. Google will update google-auth with critical bug fixes on a best-effort basis, but not with any other fixes or features. Please upgrade your Python version, and then update google-auth.
  warnings.warn(eol_message.format("3.9"), FutureWarning)

如果不想看到這些警告, 可以在程式開頭用下列程式碼隱藏 :

import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

如果想永久隱藏, 就要用 nano ~/.bashrc 去修改設定檔, 在最底下加上 :

export PYTHONWARNINGS="ignore::FutureWarning"

用下列程式測試可正確載入 API :

import grpc
print(f"GRPC 版本: {grpc.__version__}") # 應該要是 1.54.2
import google.generativeai as genai
print("Gemini SDK 成功載入!")

>>> import google.generativeai as genai    
>>> print(genai.__version__)   
0.3.1
>>> try:
...     print("SDK 載入成功,準備測試屬性...")
...     model = genai.GenerativeModel('gemini-pro')
...     print("模型物件建立成功!")
... except Exception as e:
...     print(f"執行出錯: {e}")
... 
SDK 載入成功,準備測試屬性...
模型物件建立成功!

我把 Gemini API 金鑰放在環境變數檔 .env 裡的 GEMINI_API_KEY 中利用 dotenv 套件讀取後實際呼叫 Gemini API 測試 OK :

>>> import google.generativeai as genai  
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')     
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> genai.configure(api_key=gemini_api_key)    
>>> model=genai.GenerativeModel('gemini-2.5-flash')   
>>> reply=model.generate_content('你是誰?')    
>>> print(reply.text)    
我是一個大型語言模型,由 Google 訓練。
我沒有名字、沒有身體,也沒有個人情感或意識。我的目的是回答你的問題、提供資訊、進行對話,並在各種任務上提供幫助。

下面是 OpenAI API 的測試 :

>>> from openai import OpenAI   
>>> from dotenv import dotenv_values 
>>> config=dotenv_values('.env')
>>> openai_api_key=config.get('OPENAI_API_KEY')    
>>> client=OpenAI(api_key=openai_api_key)   
>>> reply=client.chat.completions.create(   
...     messages=[   
...         {"role": "user",
...          "content": "你是誰?",
...         }],
...     model="gpt-3.5-turbo",
...     )
>>> print(reply.choices[0].message.content)
我是一個AI人工智能助手,可以與你進行對話、回答問題和提供信息。有什麼我可以幫助你的嗎?

樹莓派學習筆記 : 消除 PATH 環境變數警告的方法

高雄家的 Pi 3 A+ 主機在三月下旬我去日本時不明原因當機 (應該是 8GB TF 卡掛了), 它負責的爬蟲停擺, 回來後一直沒時間修復, 昨晚找出一片 32GB TF 卡重新燒錄 Bulleye 後已重新上線. 但在更新套件時出現一個警告 : 

pi@pi3aplus:~ $ pip install --upgrade requests urllib3   
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Requirement already satisfied: requests in /usr/lib/python3/dist-packages (2.25.1)
Collecting requests
... (略)...
Installing collected packages: urllib3, charset-normalizer, requests
  WARNING: The script normalizer is installed in '/home/pi/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed charset-normalizer-3.4.7 requests-2.32.5 urllib3-2.6.3

此警告意思是套件有安裝可執行腳本 (此處為 normalizer), 但該腳本所在目錄 /home/pi/.local/bin 沒有在 PATH 裡, 所以若在終端機直接打指令 (normalizer) 時會找不到, 出現 command not found 錯誤. 如果根本不用 CLI 工具執行此套件可忽略此警告, 但以後 pip install 時仍可能出現, 為了不礙眼, 乾脆將 /home/pi/.local/bin 加入 PATH 裡一勞永逸, 做法如下 :

編輯 .bashrc : 

pi@pi3aplus:~ $ nano ~/.bashrc   

在最底下加入此目錄 : 

export PATH="$HOME/.local/bin:$PATH"




重新執行 .bashrc 即可 :

pi@pi3aplus:~ $ source ~/.bashrc