2025年12月7日 星期日

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

上週社區網路停業後, 佈署在高雄家 Pi 3 主機上的爬蟲全部停擺, 週三 (10/3) 中華電信來安裝光世代 300M 後網路可用了, 但 Pi 3 因為之前使用 RJ45 網路線連網, 必須重新設定才能運作, 為了能讓爬蟲恢復運轉我先把母校圖書館借書與預約爬蟲程式移到 Pi 400 上跑, 但出現執行錯誤, 詢問 AI 才知在樹莓派 Trixie OS 上 Selenium 調用 Chromium webdriver 的方式已經不同 :


1. 改用新套件 : 

在 Debian 12 (Bookworm) 之後的版本, chromium-browser 與 chromium-chromedriver 套件已被移除, 要改用 chromium 與 chromium-driver 套件, 所以要先安裝這兩個套件 :

sudo apt update
sudo apt install -y chromium chromium-driver

pi@pi3aplus:~ $ sudo apt install -y chromium chromium-driver      
Upgrading:                       
  chromium  chromium-common  chromium-l10n  chromium-sandbox

Installing:
  chromium-driver

Summary:
  Upgrading: 4, Installing: 1, Removing: 0, Not Upgrading: 83
  Download size: 171 MB
  Space needed: 19.5 MB / 43.7 GB available

下載:1 http://archive.raspberrypi.com/debian trixie/main arm64 chromium-l10n all 1:142.0.7444.175-1~deb13u1+rpt1 [19.4 MB]
下載:2 http://archive.raspberrypi.com/debian trixie/main arm64 chromium arm64 1:142.0.7444.175-1~deb13u1+rpt1 [106 MB]
下載:3 http://archive.raspberrypi.com/debian trixie/main arm64 chromium-common arm64 1:142.0.7444.175-1~deb13u1+rpt1 [36.0 MB]
下載:4 http://archive.raspberrypi.com/debian trixie/main arm64 chromium-sandbox arm64 1:142.0.7444.175-1~deb13u1+rpt1 [113 kB]
下載:5 http://archive.raspberrypi.com/debian trixie/main arm64 chromium-driver arm64 1:142.0.7444.175-1~deb13u1+rpt1 [8,756 kB]
取得 171 MB 用了 52s (3,260 kB/s)                                              
apt-listchanges: 讀取改變紀錄中...
(讀取資料庫 ... 目前共安裝了 125708 個檔案和目錄。)
正在準備解包 .../chromium-l10n_1%3a142.0.7444.175-1~deb13u1+rpt1_all.deb……
Unpacking chromium-l10n (1:142.0.7444.175-1~deb13u1+rpt1) over (1:142.0.7444.162
-1~deb13u1+rpt1) ...
正在準備解包 .../chromium_1%3a142.0.7444.175-1~deb13u1+rpt1_arm64.deb……
Unpacking chromium (1:142.0.7444.175-1~deb13u1+rpt1) over (1:142.0.7444.162-1~de
b13u1+rpt1) ...
正在準備解包 .../chromium-common_1%3a142.0.7444.175-1~deb13u1+rpt1_arm64.deb……
Unpacking chromium-common (1:142.0.7444.175-1~deb13u1+rpt1) over (1:142.0.7444.1
62-1~deb13u1+rpt1) ...
正在準備解包 .../chromium-sandbox_1%3a142.0.7444.175-1~deb13u1+rpt1_arm64.deb……
Unpacking chromium-sandbox (1:142.0.7444.175-1~deb13u1+rpt1) over (1:142.0.7444.
162-1~deb13u1+rpt1) ...
選取了原先未選的套件 chromium-driver。
正在準備解包 .../chromium-driver_1%3a142.0.7444.175-1~deb13u1+rpt1_arm64.deb……
Unpacking chromium-driver (1:142.0.7444.175-1~deb13u1+rpt1) ...
設定 chromium-sandbox (1:142.0.7444.175-1~deb13u1+rpt1) ...
設定 chromium-common (1:142.0.7444.175-1~deb13u1+rpt1) ...
設定 chromium (1:142.0.7444.175-1~deb13u1+rpt1) ...
設定 chromium-driver (1:142.0.7444.175-1~deb13u1+rpt1) ...
設定 chromium-l10n (1:142.0.7444.175-1~deb13u1+rpt1) ...
執行 desktop-file-utils (0.28-1) 的觸發程式……
執行 hicolor-icon-theme (0.18-2) 的觸發程式……
執行 gnome-menus (3.36.0-3) 的觸發程式……
執行 libc-bin (2.41-12+rpt1) 的觸發程式……
執行 man-db (2.13.1-1) 的觸發程式……
執行 mailcap (3.74) 的觸發程式……
pi@raspberrypi:~ $ which chromium
which chromedriver
/usr/bin/chromium
/usr/bin/chromedriver

安裝好後用 which 指令查詢 :

pi@pi3aplus:~ $ which chromium  
/usr/bin/chromium
pi@pi3aplus:~ $ which chromedriver   
/usr/bin/chromedriver


2. 改用新寫法 : 

匯入套件 :

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

設定無頭存取選項物件 & 建立 Chorme 瀏覽器物件 :

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)


3. 爬蟲程式改版 : 

依照上面新寫法將爬蟲程式改為 v10 版 :

# nkust_lib_10.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

async def telegram_send_text(text):
    bot=Bot(token=token)
    try:
        await bot.send_message(
            chat_id=chat_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('我的帳號')  
        login_password=browser.find_element(By.ID, 'LoginPassword')   
        login_password.send_keys('我的密碼')
        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()
    msg=get_nkust_lib()
    token='我的Telegram Token'
    chat_id='Telegram聊天室 ID'  
    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@pi3aplus:~ $ source ~/myenv313/bin/activate   
(myenv313) pi@pi3aplus:~ $ python nkust_lib_10.py   
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
搜尋被預約書籍 ... OK
訊息傳送成功!

2025-12-07 13:16:35
被預約的書:
 1. 持續買進 : 資料科學家的投資終極解答, 存錢及致富的實證方法 / 尼克.馬朱利(Nick Maggiulli)著 ; 李芳齡譯 (c.3) 到期日: 12/31/2025
執行時間:573.5429089069366

花了快 10 分鐘, 好久 !




4. 設定 crontab : 

先用 chmod 將爬蟲程式添加可執行權限 :

pi@pi3aplus:~ $ chmod +x nkust_lib_10.py   
pi@pi3aplus:~ $ ls -ls nkust_lib_10.py  
8 -rwxrwxr-x 1 pi pi 6039 12月  7 10:43 nkust_lib_10.py

編輯 crontab, 設定於 06:00, 12:00, 與 16:00 執行此程式 :

pi@pi3aplus:~ $ crontab -e   
crontab: installing new crontab
pi@pi3aplus:~ $ crontab -l  
*/3 * * * * curl -s https://serverless-fdof.onrender.com/function/hello
*/3 * * * * curl -s https://serverless-5e6i.onrender.com/function/hello
0 6,12,16 * * * /home/pi/myenv313/bin/python /home/pi/nkust_lib_10.py

沒有留言 :