2025年11月12日 星期三

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

上回改版為 v8 時沒有順便處理被預約書的到期日, 收到 Telegram 訊息時只知道哪些書被預約了, 還是要登入網站去查哪一天須還, 挺麻煩的, 今天花了點時間補抓到期日資訊, 改版為 v9. 

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


到期日資訊列在借閱書籍列表的第三欄 :




到期日被包裹在一個 P 元素裡面, 可用 data-qa="automation_mlc_record_date" 屬性定位, 其 XPATH 為 .//p[@data-qa="automation_mlc_record_date"] :

<p data-qa="automation_mlc_record_date" class="normal-text" ...>
  <span translate="nui.loan.due">到期</span>:
  12/01/2025, 23:59
</p>




只要用正規表達式 r'\d{2}/\d{2}/\d{4}' 就可以將 P 元素中符合日期格式的內容抓出來, 所以需要匯入標準庫中的 re 模組. 完整程式碼如下 :

# nkust_lib_9.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():
    try:
        options=Options()
        options.add_argument("--headless")
        driverpath='/usr/lib/chromium-browser/chromedriver'
        service=Service(driverpath)
        browser=webdriver.Chrome(options=options, service=service)
        browser.implicitly_wait(60)
        browser.maximize_window()
        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')
        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)  # 等待 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_element(By.CSS_SELECTOR, "h3 a")
                    title=title_elem.text.strip()
                    # 2. 取得整個 item 的文字
                    item_text=item.text.strip()
                    # 3. 嘗試抓到期日
                    try:
                        due_elem=item.find_element(By.XPATH, './/p[@data-qa="automation_mlc_record_date"]')
                        due_text=due_elem.text.strip()
                        # 擷取出「12/01/2025」格式之到期日
                        m=re.search(r'\d{2}/\d{2}/\d{4}', due_text)
                        due_date=m.group(0) if m else ''
                    except Exception:
                        due_date=''                  
                    # 4. 判斷借閱狀態是否包含 "已續借"
                    if "已續借" not in item_text:
                        unrenew_books.append((title, due_date))
                    #print(f"[DEBUG] {title} -> {item_text} / 到期日: {due_date}")
                except Exception as e:        
                    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:
        browser.close()
        return msg

if __name__ == '__main__':
    start=time.time()
    msg=get_nkust_lib()
    token='Telegram 權杖'
    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}')

主要的異動部分為抓到期日的藍色字部分 (每一本借閱書籍都會抓), 在步驟四判斷若該書被預約, 則將書名與到期日組成 tuple 放入 unrenew_books 串列裡, 因為從原本的書名字串改成 tuple, 所以組成 msg 字串時 t 是一個 tuple, 要用 t[0] 取出書名, 用 t[1] 取出到期日, 執行結果如下 :

pi@raspberrypi:~ $ python3 nkust_lib_9.py       
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK

2025-11-11 17:03:32
被預約的書:
 1. Python原力爆擊 : OpenAI / Gemini / AWS / Ollama生成式AI應用新手指南 / 柯克(Ko Ko)等著 (c.3) 到期日: 12/02/2025
 2. AI應用程式開發 : 活用ChatGPT與LLM技術開發實作 / Olivier Caelen, Marie-Alice Blete著 ; 藍子軒譯 到期日: 12/01/2025
 3. Python金融市場賺大錢聖經 : 寫出你的專屬指標 / 張峮瑋作 到期日: 11/15/2025
執行時間:354.4999508857727

收到的 Telegram 訊息 :




最後將 v9 程式添加可執行權限, 並更改 crontab 中定時執行的爬蟲程式為 v9 : 

pi@raspberrypi:~ $ chmod +x nkust_lib_9.py
pi@raspberrypi:~ $ ls -ls nkust_lib_9.py  
8 -rwxr-xr-x 1 pi pi 6148 11月 12 08:31 nkust_lib_9.py
pi@raspberrypi:~ $ crontab -e   

0 6,16 * * * /usr/bin/python3 /home/pi/nkust_lib_9.py

crontab: installing new crontab

終於弄成自己想要的樣子啦, 收工. 

沒有留言 :