2026年4月18日 星期六

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年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   

2026年4月16日 星期四

好站 : 高見龍老師的部落格

三月時內訓課程請到六角學院的高見龍老師來講 SDD 規格驅動開發, 讓我覺得獲益匪淺, 其實課程內容的精髓大部份都可以在其部落格找到 :


部落格中與 SDD 規格驅動開發有關的是這幾篇 :


其中 Spectra 是高老師為 OpenSpec 開發的 GUI 工具, 可以讓 OpenSpec 用起來更簡單更直覺, 剛好我最近正在學 OpenSpec 用得上 (雖然我只開發小專案, 但江湖在走, SDD 還是要有). 

我去年參加公司的 Hahow 企業版線上課程時就上了不少高老師的課 (Python, Javascript, 網頁前端), 不管是口條或內容都非常棒, 雖說 AI 時代學習這些技能似乎不那麼重要了, 但有基礎的人與菜菜小白終究是不同的. 

2026年4月15日 星期三

好站 : 工程師下班有約

最近上了幾堂林鼎淵老師的 Vibe coding 內訓課程, 覺得他是實力厚實備課非常認真的老師, 這從等候程式執行的幾分鐘, 他都有準備小知識與開發經驗談來墊檔與分享可見一斑. 他經營的 YT 頻道有非常多實用與最新的 AI 技術分享影片 (尤其是 Vibe coding), 值得記下來有空時學習 : 


2026年4月14日 星期二

(補記) 購買 Accupass AI 實戰工作坊

月初 (大約 4/6) 在 Accupass 買了一堂 Vibe coding 線上課程忘了記錄下來, 今天整理截圖才發現, 4/18 (六) 13:30~16:30 上課, 馬上列入行事曆以免錯過 :




我在 Hahow 也還有四門課買了尚未啟用學習, 第二季要來完成進度了. 

如何尋找靜音中的手機

前天 (週日) 爸坐阿泉伯的車去愛心會開會, 手機掉在車上未發覺, 回到家才發現手機不見了, 我透過定位發現手機在阿泉伯家, 但去他家找了三遍都沒找到, 撥電話都沒聽到響鈴, 我懷疑爸是否誤將手機關靜音, 於是問 Gemini 如何在手機靜音狀態下讓它發出聲音? 方法如下 :
  1. 在電腦或其他行動裝置的瀏覽器網址 google.com/android/find 
  2. 登入要尋找的手機的 Google 帳號
  3. 在左側面板或選單中點擊 「播放音效」(Play Sound)
我開啟自己的手機瀏覽器, 連線 google.com/android/find, 登入爸的 Google 帳戶後, 點選 "安全性與登入" :




點選尋找遺失的裝置 : 




點選要找尋的手機 OPPO A38 :




按 "播放音效" 就會持續播放 5 分鐘的響鈴了 :





最後是在阿泉伯媳婦提醒下在車子副駕椅子下找到手機, 雖然這個方法並未建功 (在車內就算響鈴除非走近也聽不到), 但如果要在家裡某個角落找尋亂放的手機就能派上用場了. 

2026年4月12日 星期日

2026 年第 14 周記事

時光飛逝, 思緒還駐留在大阪城門口初綻的櫻花, 怎麼一轉眼已來到四月中旬矣, 三周前還要穿長袖冬衣, 現在已換季來到夏天了, 三月真是一個天氣多變的月份. 

本周回頭抓了一下 LangChain 與 LLM 串接的進度, 其實只是想對借來的書有所交代而已, 目前重點還是 Vibe coding, 以及較專業些的 SDD, 這周末都在補課, 之前購買的線上課程在 3/28 就開課了, 但那時在忙掃墓, 所以沒跟上. 上完課覺得, 我其實都只開發小型專案而已, SDD 對我而言似乎大砲打小鳥. 不過或許以後用得著, 就學起來吧.  

今天早上爸去參加愛心會理監事開會, 中午回來吃過飯後發現手機不見了, 我用 Google 地圖定位發現手機在阿泉伯家, 但我騎車下去找, 現場撥電話也沒聽到響鈴, 猜測可能掉水裡, 但周邊水溝來回也沒找著. 傍晚忙完芭樂套袋, 騎機車再次去阿泉伯家, 剛好遇見他媳婦阿勤, 我說好奇怪, 明明定位在你家, 為何找不到? 她說會不會掉在車裡, 因為早上爸是搭阿泉伯車去開會, 打開車門果然掉在椅子下, 難怪撥電話都聽不到響鈴. 幸好找到了, 不然得買新手機, 且還要跑中華電信換 SIM 卡.