2024年6月1日 星期六

Python 學習筆記 : 網頁爬蟲實戰 (十) 高科大圖書館爬蟲

完成市圖爬蟲後再接再厲, 繼續來寫母校高科大圖書館爬蟲. 高科大是我的第一母校, 距離也很近 (比第二母校中山大學近多了), 而且三校合併之後校友借書量也增為三倍, 真是太佛心了. 這次的爬蟲任務有二 :
  • 每天自動續借
  • 將被預約書籍用 Line 訊息通知
目的是為了省去常常要登入續借的麻煩 (懶人最愛自動化), 若借閱中書被預約要加速讀完趕快拿去還. 

本系列之前的筆記參考 : 



一. 檢視目標網頁 :  

母校圖書館登入網址如下 : 





校友須按下方 "其他讀者" 進去登入網頁 : 




輸入帳號與密碼後按底下的 "登入" 鈕就會進入個人書房頁面 : 




按右上角的姓名, 於彈出的選單中按 "我的借閱" 會列出借閱書目 :



 
按右上角的 "全部借閱" 會更新到期日為今日 : 




如果都沒被預約順利續借完成, 則書目列表最上面會顯示 "所有借閱資料已成功續借"; 否則會顯示 "部分書籍續借失敗". 因目前都借閱成功, 故先進行自動續借部分的測試但不要佈署, 因為每天自動續借會無法進行下一步測試. 

使用 Chrome 擴充套件 Quick Javascript Switch 關閉 Javascript 發現整個網站從登入頁面開始都是透過 Javascript 產生的動態網頁, 因此無法使用 requests 套件來抓, 必須使用 Selenium 才行. 


二. 使用 Selenium 擷取網頁 :  

先使用 LG 筆電的 Firefox 測試 (因我的 Chrome 版本太新無法與 Selenium 匹配), 完成後再改為 Chromium 版佈署至 Mapleboard 的 Ubuntu Mate 上. 

首先在 Chrome 上按 F12 開啟開發者工具視窗, 然後連線上面的圖書館登入網址, 在右邊開發者工具視窗的 Element 頁籤上點一下, 按 Ctrl + F 後在底下輸入框搜尋 "其他讀者" 即可找到此網頁載入之 HTML 碼位置 : 




可見這個 "其他讀者" 項目是放在一個自訂標籤 "md-list-item" 中, 改用 md-list-item 搜尋可知整個網頁中有兩個這樣的標籤, "其他讀者" 是第二個, 而第一個則是上面的 "教職員工生". 




先匯入 Selenium 相關套件模組 :

>>> import selenium   
>>> from selenium import webdriver   
>>> from selenium.webdriver.common.by import By    

紀錄版本資訊 :

>>> selenium.__version__    
'4.11.2' 
>>> webdriver.__version__     
'4.11.2'  

建立 WebDriver 物件並連線目標網站 :

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

使用標籤名稱搜尋 md-list-item 元素 : 

>>> md_list=browser.find_elements(By.TAG_NAME, 'md-list-item')   
>>> len(md_list)   
2
>>> type(md_list[1])   
<class 'selenium.webdriver.remote.webelement.WebElement'>   

可見此網頁中確實有兩個 md-list-item 標籤, 我們的目標是其中的第二個 WebElement 物件, 只要呼叫其 click() 方法即可進入 "其他讀者" 的登入頁面 : 

>>> md_list[1].click() 

這時在開發者工具的 Element 頁籤搜尋 "讀者證號" 即可找到登入頁面中填寫帳號密碼的兩個輸入框, 這兩個 input-text 元素的 id 分別為 LoginUserName 與 LoginPassword : 




可以用 id 來取得這兩個輸入框的 WebElement 物件並呼叫 send_key() 填入帳密 : 

>>> login_user_name=browser.find_element(By.ID, 'LoginUserName')   
>>> login_user_name.send_keys('MyID')  
>>> login_password=browser.find_element(By.ID, 'LoginPassword')   
>>> login_password.send_keys('MyPassword')   

搜尋 "登入" 可以找到登入按鈕 button 元素 :



 
但卻沒有 name 或 id 屬性來取得此 button 元素, 只有 class 屬性可用來識別它, 搜尋其中的第一個樣式類別 button-large 發現可找到兩個, 不過其中第一個是 icon-button-class 樣式, 所以其實 button-large 樣式只有一個, 只要用 CLASS_NAME 搜尋就可以找到它 : 

>>> login_btn=browser.find_element(By.CLASS_NAME, 'button-large')    
>>> login_btn.get_attribute('type')       # 確認此為 Submit 按鈕
'submit'

最後呼叫其 click() 方法登入 :

>>> login_btn.click()   

成功登入網站後, 按右上方的登入者姓名會彈出一張選單, 按其中的 "我的借閱" 即可得到目標網頁, 但首先是要找到此登入者姓名按鈕, 在開發者工具的 Element 頁籤搜尋登入者姓名即可找到一個有 user-button 樣式類別的按鈕 : 




搜尋 user-button 只有找到一個, 因此具有此樣式類別的元素只有一個, 可以用 By.CLASS_NAME 來定位它, 然後呼叫其 click() 方法來顯示下拉式選單 : 

>>> user_btn=browser.find_element(By.CLASS_NAME, 'user-button')   
>>> user_btn.click()    

到這一步時瀏覽器右上角就出現下拉式選單了 : 




接下來要在此下拉式選單網頁中取得 "我的借閱" 按鈕的 WebElement 物件. 在開發者工具的 Element 頁籤搜尋 "我的借閱" 可以找到此 button 元素, 它同樣是沒有 name 或 id 屬性 :




嘗試用它的第一個樣式類別 md-button 去搜尋 (其實它具有多個樣式類別 md-button md-primoExplore-theme md-ink-ripple), 發現有多達 20 個元素使用了此 class 名稱 (我的借閱是其中第 15 個), 雖然也可以用索引 [14] 來定位它, 但這次想改用 XPATH, 用滑鼠左鍵點一下此按鈕, 然後按滑鼠右鍵依序選 "Copy -> Copy Full XPath" :




把它放在一個變數中, 然後呼叫 find_element() 用 XPATH 來定位 "我的借閱" 按鈕, 最後呼叫此按鈕 WebElement 物件之 click() 方法即可得到目標網頁 : 

>>> xpath='/html/body/div[3]/md-menu-content/md-menu-item[3]/button'    
>>> my_borrow=browser.find_element(By.XPATH, xpath)   
>>> my_borrow.click()     
 



只要按右上角的 "全部續借" 鈕就達成 1/2 任務了, 但要先取得此按鈕之 WebElement 物件, 在開發者工具的 Element 頁籤搜尋 "全部續借" 可以找到 4 個 "全部續借" 按鈕, 我們的目標是其中第 3 個不要找錯了 :




同樣用上面的方法複製此按鈕的 XPATH :




儲存至 xpath 變數 :

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

取得 "全部續借" 按鈕之 WebElement 物件 : 

>>> all_borrow=browser.find_element(By.XPATH, xpath)   
>>> all_borrow.get_attribute('class')       # 確認是否取得物件
'button-link md-button md-primoExplore-theme'     

最後呼叫 click() :

>>> all_borrow.click() 

這樣就取得目標網頁了, 如果借閱的書籍無人預約, 那麼全部書籍都會續借成功, 會在書目列表最上面顯示 "所有借閱資料已成功續借" : 




如果借閱書籍中有被預約, 則該書無法續借, 這時會顯示 "只有部分借閱資料已成功續借" : 




我們必須擷取這個續借結果, 如果是 "所有借閱資料已成功續借", 則爬蟲就完成任務了; 但若是 "只有部分借閱資料已成功續借", 則必須看看哪幾本書被預約, 將其到書名與到期日透過 Line Notify 推播出來. 

在 Element 中搜尋 "所有借閱資料已成功續借" 或 "只有部分借閱資料已成功續借" 可以發現續借結果放在一個自訂標籤 prm-alert-bar 裡面的 span 元素內 :




此 span 元素的 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

但此處我們要嘗試另一種元素定位方式, 利用 span 元素的上層節點 prm-alert-bar 的 WebElement 物件來搜尋 span 元素, 在 Element 中搜尋 "prm-alert-bar" 發現網頁中共有兩個 prm-alert-bar 元素, 我們的目標元素位於其中的第一個 :




因此只要呼叫 find_element() 就可以找到它 :   

>>> prm_alert_bar=browser.find_element('tag name', 'prm-alert-bar')    
>>> prm_alert_bar.get_attribute('ng-if')     # 確認有取得 prm-alert-bar 元素
'!$ctrl.isLoadingLoans'   

然後用 prm_alert_bar 這元素的 WebElement 物件去搜尋裡面的 span 元素會找到 3 個, 其中索引 0 是包覆索引 1, 而索引 2 則是 "解除" 超連結 :

>>> alert_spans=prm_alert_bar.find_elements('tag name', 'span')    
>>> len(alert_spans)     
3
>>> alert_spans[0].text   
'只有部分借閱資料已成功續借。'
>>> alert_spans[1].text   
'只有部分借閱資料已成功續借。'
>>> alert_spans[2].text   
'解除'

所以呼叫 find_element() 就可以了 :

>>> alert_span=prm_alert_bar.find_element('tag name', 'span')    
>>> alert_span.text      
'只有部分借閱資料已成功續借。'

當然直接用 XPATH 亦可 :

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

上面操作的完整程式碼如下 :

from selenium import webdriver   
from selenium.webdriver.common.by import By

browser=webdriver.Firefox()       
url='https://nkust.primo.exlibrisgroup.com/discovery/login?'  +\
    'vid=886NKUST_INST:86NKUST&lang=zh-tw'   
browser.get(url)   
browser.implicitly_wait(20)
# 按其他讀者
md_list=browser.find_elements(By.TAG_NAME, 'md-list-item')
md_list[1].click()
# 登入系統
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()
# 按名字顯現選單
user_btn=browser.find_element(By.CLASS_NAME, 'user-button')   
user_btn.click()
# 按我的借閱鈕
xpath='/html/body/div[3]/md-menu-content/md-menu-item[3]/button'    
my_borrow=browser.find_element(By.XPATH, xpath)   
my_borrow.click()
# 按全部續借
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)   
all_borrow.click()
# 檢查續借結果
#prm_alert_bar=browser.find_element('tag name', 'prm-alert-bar')
#alert_span=prm_alert_bar.find_element('tag name', 'span')
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:
    print('所有借閱資料已成功續借')
else:
    print('只有部分借閱資料已成功續借')
    # waiting to continue ...
browser.close()


事實上所有的元素定位都可以完全使用 XPATH, 即使要定位之元素有 id 屬性或惟一的 name 或 class 名稱, 都可以用 XPATH 取代. 

2024-06-11 補充 : 

由於借閱書目以分頁方式呈現, 每頁 10 本書, 如果超過 10 本會在底下出現 "載入更多結果" 按鈕, 而被預約的書會列在所有書目的最後面, 故要取得被預約的書必須按此按鈕到最後一個分頁 :




我最多只能借 30 本, 通常都是借好借滿, 因此要按兩次到第三分頁才能知道哪些書被預約了. 在 Element 中搜尋 "載入更多結果" 可以找到這個按鈕的 HTML 碼 : 




可見它具有 class="button-confirm" 樣式類別, 且搜尋 "button-confirm" 只出現一個 :




當進入 "我的借閱" 第一頁時可以用 By.CLASS_NAME 來取得此按鈕 :  到最後一頁時即無此按鈕, 因此可以用 try except 來捕捉是否有此按鈕, 如果沒有表示已到最後一頁. 

>>> load_more=browser.find_elements(By.CLASS_NAME, 'button-confirm')   
>>> len(load_more)    
1

有找到 "載入更多結果" 按鈕表示還有其他借閱書籍, 按此按鈕進入第二頁 : 

>>> load_more[0].click()    

再次搜尋  "載入更多結果" 按鈕發現還有, 表示有第三頁, 繼續按 : 

>>> load_more=browser.find_elements(By.CLASS_NAME, 'button-confirm')   
>>> len(load_more)   
1
>>> load_more[0].click()  

進入到第三頁時就找不到此按鈕了, 表示此為最後一頁 : 

>>> load_more=browser.find_elements(By.CLASS_NAME, 'button-confirm')    
>>> len(load_more)   
0

那些被預約的書就列在此頁表格的最底下幾列 : 




因為目前沒有任何借閱書被預約, 因此只能先假設最後一本書被預約, 暫時只取出這本書的書名回傳, 等實際出現被預約書時再來修改. 

在 Chrome 開發者工具之 Element 視窗搜尋最後一本書的書名 "Python資料可視化" 發現此表格的每一本書的資訊都是放在自訂標籤 md-list-item 內 :




書名可從其內之 h3 或 a 標籤取得, 但 a 元素裡面的資訊最完整 (包含是否被預約), 因此應從 a 元素中擷取, 書名就在 a 元素的 aria-label 屬性裡 : 




在最後一頁 (第三頁) 搜尋 "<a" 發現在滿借 30 本書情況下總共有 38 的 a 元素, 書目資料是從第 9 個 a 元素開始, 可見在最後一頁搜尋 a 元素時實際上是搜尋全部借閱書籍, 而非只有第三頁那 10 本書. 




但實際測試卻只找到 37 個 a 元素, 而且第一本書 "Python for Excel" 不見了, 第一本書是在第 11 個 a 元素 (索引 10), 不知道這跟用 "<a" 在 Element 中搜尋差在哪裡 : 

>>> a_links=browser.find_elements(By.TAG_NAME, 'a')   
>>> len(a_links)       
37
>>> a_links[9].get_attribute('aria-label')    
'在新分頁開啟RefWorks'
>>> a_links[10].get_attribute('aria-label')   
'自然語言處理最佳實務 : 全面建構真正的NLP系統 / Sowmya Vajjala等著 ; 賴屹民譯 ,在新視窗中開啟'

可以在 for 迴圈中用 enumerate() 掃描這些 a 元素 :

>>> for idx, a_link in enumerate(a_links):   
    print(f"{idx} : {a_link.get_attribute('aria-label')}")    
    
0 : 跳轉到主功能表
1 : 跳轉到我的帳戶總覽
2 : None
3 : None
4 : 圖書館,在新視窗中開啟
5 : 使用說明,在新視窗中開啟
6 : 全部館藏
7 : 期刊查詢
8 : 我的收藏
9 : 在新分頁開啟RefWorks
10 : 自然語言處理最佳實務 : 全面建構真正的NLP系統 / Sowmya Vajjala等著 ; 賴屹民譯 ,在新視窗中開啟
11 : Python3.x機器學習基礎與應用特訓教材 : 軟體設計領域 / 林英志編著 (c.2) ,在新視窗中開啟
12 : PyTorch深度學習攻略 : 核心開發者親授! / Eli Stevens, Luca Antiga, Thomas Viehmann著 ; 黃駿譯 ,在新視窗中開啟
13 : Python資料分析 : 用pandas、numpy和ipython做資料分析 / Wes McKinney原著 ; 張靜雯譯 (c.2) ,在新視窗中開啟
14 : 駕馭Chat GPT 4 : 探索Azure OpenAI與Cognitive Service for Language開發實踐(使用.NET與Node.js) / 柯克(Ko Ko), 陳葵懋(Ian Chen), Ryan Chung著 ,在新視窗中開啟
15 : 一行指令學 Python : 用機器學習掌握人工智慧 / 徐聖訓編著 ,在新視窗中開啟
16 : Python實戰聖經 : 用簡單強大的模組套件完成最強應用 = Python development bible / 文淵閣工作室編著 ,在新視窗中開啟
17 : Python從初學到生活應用超實務 : 讓Python幫你處理日常生活與工作中繁瑣重複的工作 / 陳會安著 (c.3) ,在新視窗中開啟
18 : Deep Learning . 3 . 用Python進行深度學習框架的開發實作 / 斎藤康毅著 ; 吳嘉芳譯 ,在新視窗中開啟
19 : 超圖解Python程式設計入門 = Illustrated Python programmingguide / 趙英傑作 ,在新視窗中開啟
20 : 東京大學資料科學家養成全書 : 使用Python動手學習資料分析 / 塚本邦尊, 山田典一, 大澤文孝著 ; 莊永裕譯 (c.2) ,在新視窗中開啟
21 : Deep Learning 2 : 用Python進行自然語言處理的基礎理論實作 / 斎藤康毅著 ; 吳嘉芳譯 ,在新視窗中開啟
22 : 金融AI : 人工智慧的金融應用 / Yves Hilpisch著 ; 陳仁和譯 ,在新視窗中開啟
23 : PyTorch 自然語言處理 : 以深度學習建立語言應用程式 / Delip Rao, Brian McMahan著 ; 楊尊一譯 ,在新視窗中開啟
24 : 深度學習的16堂課 : CNN.RNN.GAN.DQN.DRL 看得懂、學得會、做得出! / Jon Krohn, Grant Beyleveld, Aglaé Bassens作 ; 黃駿, 哈雷譯 (c.2) ,在新視窗中開啟
25 : 用Excel學Python資料分析 / 張俊紅著 ,在新視窗中開啟
26 : 偏不讓你抓 : 最強Python爬蟲vs反爬蟲大戰實錄 / 韋世東作 ,在新視窗中開啟
27 : 金融人才×機器學習聯手出擊 : 專為FinTech領域打造的機器學習指南 / Jannes Klaas著 ; 彭勝陽譯 (c.2) ,在新視窗中開啟
28 : Python網頁框架超集合 : 在Django、Tornado、Flask、Twisted全面應用 / 劉長龍作 ,在新視窗中開啟
29 : 人工智慧Python基礎課 : 用Python分析了解你的資料 / 陳會安作 ,在新視窗中開啟
30 : 全格局使用PyTorch : 基礎篇 / 深度學習和圖神經網路. 李金洪著 ,在新視窗中開啟
31 : Python資料科學與機器學習 : 從入門到實作必備攻略 / Frank Kane著 ; 陳光欣譯 ,在新視窗中開啟
32 : 區塊鏈生存指南 : 帶你用Python寫出區塊鏈! / 李耕銘著 ,在新視窗中開啟
33 : 全格局使用PyTorch : 實戰篇 / 深度學習和圖神經網路. 李金洪著 ,在新視窗中開啟
34 : 人工智慧 : 概念應用與管理 = Artificial intelligence : concept application and management / 林東清著 ,在新視窗中開啟
35 : 必學!Python 資料科學.機器學習最強套件 : NumPy Pandas Matplotlib OpenCV scikit-learn tf.Keras / 石川聡彦作 ; 劉金讓譯 (c.3) ,在新視窗中開啟
36 : Python資料可視化攻略 / 小久保奈都彌著 ; 許郁文譯 ,在新視窗中開啟

不過也沒差, 反正目前要擷取的是最後一本書的書名 (假設只被預約一本), 所以只要取出索引 [-1] 那本即可 : 

>>> a_links[36].get_attribute('aria-label')  
'Python資料可視化攻略 / 小久保奈都彌著 ; 許郁文譯 ,在新視窗中開啟'
>>> a_links[-1].get_attribute('aria-label')    
'Python資料可視化攻略 / 小久保奈都彌著 ; 許郁文譯 ,在新視窗中開啟'

測試發現在最後一頁搜尋全部借閱書籍時排序不會每次一樣, 因此抓出來的最後一本書是哪一本也不固定, 但如果有書被預約, 則一定放在最後面. 

以上單行指令測試都沒問題, 但寫成單一程式執行時卻不是很穩定, 有時可順利執行完, 有時出現錯誤, 通常在點名字要叫出選單按 "我的借閱" 時出現如下例外訊息 :

"Message: Element <button class="md-button md-primoExplore-theme md-ink-ripple" type="button"> is not clickable at point (986,197) because another element <md-backdrop class="md-menu-backdrop md-click-catcher ng-animate ng-enter md-primoExplore-theme ng-enter-active"> obscures it"

似乎是呼叫 click() 時滑鼠位置是在不可按的地方, 查詢找到下面這篇文章 :


此文提出幾種解決方法, 例如先呼叫 maximize_window() 方法將瀏覽器視窗放到最大, 但測試發現還是不穩定, 其次使用隱式等待, 但這也早就已經在用了, 最後是使用滑鼠動作鏈 ActionChains 類別, 經測試有效, 但對於  "載入更多結果" 按鈕卻無效, 可能是書目表格長度會影響該按鈕之座標所致. 




修改後的程式碼如下 : 

# nkust_lib_3.py
from selenium import webdriver   
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains

browser=webdriver.Firefox()       
url='https://nkust.primo.exlibrisgroup.com/discovery/login?'  +\
    'vid=886NKUST_INST:86NKUST&lang=zh-tw'   
browser.get(url)   
browser.implicitly_wait(20)
# 按其他讀者
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:
    print('所有借閱資料已成功續借')
else:
    print('只有部分借閱資料已成功續借')
    # 檢查是否有 "載入更多結果" 按鈕
    for i in range(3): # 最多 3 頁
        load_more=browser.find_elements('class name','button-confirm')
        if len(load_more)==0: # 最後一頁: 不用再按
            break
        else: # 不是最後一頁: 繼續按 "載入更多結果" 按鈕
            load_more[0].click() # 按 "載入更多結果" 至下一頁
    # 搜尋全部借閱書籍
    a_links=browser.find_elements(By.TAG_NAME, 'a')
    last_book=a_links[-1].get_attribute('aria-label')   # 最後一本書 (暫用)
    print(last_book)
browser.close()

執行結果如下 :

>>> %Run nkust_lib_3.py   
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
只有部分借閱資料已成功續借
金融AI : 人工智慧的金融應用 / Yves Hilpisch著 ; 陳仁和譯 ,在新視窗中開啟   

書名後面固定都會有 "在新視窗中開啟", 這是 aria-label 的屬性值 中本來就有的, 雖然可以用 replace() 去除, 其實直接用 a 元素物件的 text 屬性值即可 :

last_book=a_links[-1].text
 

三. 寫成爬蟲函式 :  

將上面的測試程式改寫為如下的函式 get_ukas :

# nkust_lib_4.py
from selenium import webdriver   
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
import time

def get_nkust_lib():
    try:
        browser=webdriver.Firefox()           
        browser.implicitly_wait(30)
        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')
        #xpath='/html/body/primo-explore/div/prm-explore-main/div' +\
              #'/prm-topbar/div/prm-user-area-expandable/md-menu/button'
        #user_btn=browser.find_element(By.XPATH, xpath)
        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('class name','button-confirm')
                if len(load_more)==0: # 最後一頁
                    break
                else:
                    load_more[0].click() # 按 "載入更多結果" 至下一頁
            # 找尋借閱書籍
            a_links=browser.find_elements(By.TAG_NAME, 'a')
            last_book=a_links[-1].text
            msg += '\n' + last_book
    except Exception as e:
        print(e)
    finally:
        browser.close()
        return msg

if __name__ == '__main__':
    start=time.time()
    msg=get_nkust_lib()
    print(msg)
    end=time.time()
    print(f'執行時間:{end-start}')

執行結果如下 :

>>> %Run nkust_lib_4.py    
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
只有部分借閱資料已成功續借
Python3.x機器學習基礎與應用特訓教材 : 軟體設計領域 / 林英志編著 (c.2)
執行時間:39.66444277763367


四. 用 Line Notify 推播續借資訊 :   

設定 Line Notify 的方法參考下面這篇 : 


由於最終是要佈署在 Mapleboard 上用 Chromium 以無頭方式執行, 程式修改如下 : 

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

def line_notify(msg, token):
    url="https://notify-api.line.me/api/notify"
    headers={"Authorization": "Bearer " + token,
             "Content-Type": "application/x-www-form-urlencoded"
             }
    payload={"message": msg}
    r=requests.post(url, headers=headers, params=payload)
    return r.status_code

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(30)
        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('class name','button-confirm')
                if len(load_more)==0: # 最後一頁
                    break
                else:
                    load_more[0].click() # 按 "載入更多結果" 至下一頁
            # 找尋借閱書籍
            a_links=browser.find_elements(By.TAG_NAME, 'a')
            last_book=a_links[-1].text
            msg += '\n' + '❶ ' + last_book
    except Exception as e:
        print(e)
    finally:
        browser.close()
        return msg

if __name__ == '__main__':
    start=time.time()
    msg=get_nkust_lib()
    token='7CLpVmFpNihuN6GB0bQcc5M1nOhpAtony1966QFMgzz'   # 範例權仗
    if msg:
        now=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        msg='\n' + now + '\n' + msg
        code=line_notify(msg, token)
        if code==200:
            print('Line 訊息發送成功!')
        else:
            print(f'Line 訊息發送失敗! (code={code})')
    print(msg)
    end=time.time()
    print(f'執行時間:{end-start}')

存檔後用 chmod 更改為可執行檔 : 

tony1966@LX2438:~/python$ sudo chmod +x nkust_lib_6.py    
[sudo] tony1966 的密碼:  
tony1966@LX2438:~/python$ ls -ls   
總用量 36
0 lrwxrwxrwx 1 tony1966 tony1966   29 May 25 13:56 geckodriver -> /snap/bin/firefox.geckodriver
4 -rw-rw-r-- 1 tony1966 tony1966 2849 May 26 00:36 ksml_books_1.py
4 -rw-rw-r-- 1 tony1966 tony1966 3090 May 26 00:31 ksml_books_2.py
4 -rw-rw-r-- 1 tony1966 tony1966 2870 May 26 00:19 ksml_books_3.py
4 -rw-rw-r-- 1 tony1966 tony1966 3095 May 26 14:12 ksml_books_4.py
4 -rw-rw-r-- 1 tony1966 tony1966 3095 May 28 20:43 ksml_books_5.py
8 -rwxrwxr-x 1 tony1966 tony1966 6106 Jun  6 19:06 ksml_books_7.py
8 -rwxrwxr-x 1 tony1966 tony1966 4433 Jun 12 23:01 nkust_lib_6.py

先手動執行 :

tony1966@LX2438:~/python$ /usr/bin/python3 /home/tony1966/python/nkust_lib_6.py   
按其他讀者 ... OK
登入系統 ... OK
按名字顯現選單 ... OK
按我的借閱 ... OK
按全部續借 ... OK
Line 訊息發送成功!

2024-06-12 23:06:07
❖ 只有部分借閱資料已成功續借
❶ 用Excel學Python資料分析 / 張俊紅著
執行時間:25.12677836418152

結果如下 :




用 crontab -e 修改 Cron table 加入此程式之定時執行設定 :

tony1966@LX2438:~/python$ crontab -e
tony1966@LX2438:~/python$ crontab -l   
0 6,16 * * * /usr/bin/python3 /home/tony1966/python/ksml_books_7.py
0 6,16 * * * /usr/bin/python3 /home/tony1966/python/nkust_lib_6.py

參考 :



2024-09-10 補充 :

這兩天終於有書被預約要還, 但似乎書目不對, Line 通知的訊息如下 : 




但我登入去查看是這兩本 : 




就是上回尚未未收尾部分, 要看看是哪裡有錯. 

沒有留言 :