2024年5月31日 星期五

領樹苗

上週大樓主委給了我一封養工處來函, 管委會之前向市政府工務局申請了 50 株植栽樹苗已獲得批准, 要我找時間去楠梓苗圃領回來. 仔細看公文請領時間都是上班時間 (08:30~17:30), 我五點下班去也來不及, 而且中午休息時間也不能領. 所幸公司彈性上班時間放寬到 09:30, 所以只要提早在 08:30 前到苗圃, 一領完馬上回家放好樹苗去上班應該還趕得上 09:30 刷卡. 

昨晚睡前查詢苗圃地址為楠梓區長興街 87 號, 地圖顯示離中山高楠梓交流道很近, 但由於我已經很久沒走一高了, 近年大中路匝道也變動很多, 要怎麼上國一北不太清楚, 於是問整天走南闖北的峰大師, 他說走民族路轉大中路, 然後走內側前行過了榮總大門就會看到國一北上標誌, 順著指示走即可. 但我會錯意, 以為是到鼎中路那邊再切內車道, 所以轉入大中路後是靠外側, 到鼎中路時才發現無法切進內車道, 只好順車流往仁武平面道方向走, 然後右轉進小巷子後繞到鼎中路轉回大中路, 這樣就折騰了 10 幾分鐘. 

上了國一後直走下楠梓交流道右轉興楠路第一個紅綠燈左轉常德路, 經過兩個紅路燈右轉長興街就到了. 走進去出示公文後, 年輕的妹妹叫我去苗圃任選 50 株即可. 我挑了 30 株矮仙丹與 20 株七里香, 然後將車子從後門開進來抬上車後返家. 我看時間有點不夠就請同事幫我請半小時. 如果不是來時耽擱了, 應該就不用請假了. 

聽那位小妹說才知道, 原來美位高雄市民憑身分證就可到苗圃領三株樹苗, 但必須間隔 3 個月才能再次領, 亦即每張身分證一年可領 12 株, 真不錯, 我以前都不知道. 

2024年5月28日 星期二

Python 學習筆記 : 網頁爬蟲實戰 (九) 市立圖書館個人書房借書資訊 (下)

在前一篇測試中已在 Mapleboard 的 Ubuntu Mate 上成功地利用 Firefox 與 Chromium 執行 Selenium 爬蟲程式, 經觀察 Chromium 的執行速度比 Firefox 快一倍, 因此正式上線時會使用 Chromium. 本篇是此任務的結尾, 目標是將擷取到的市圖 "我的書房" 網頁中的借閱與預約書籍資訊做進一步處理, 萃取出下列資料後用 Line Notify 推播到 "市圖借書" 群組 : 
  • 今日到期書目與續借次數
  • 明日到期書目與續借次數
  • 逾期書目與續借次數
  • 近 7 日到期書目與續借次數
  • 預約書目與順位 
本系列之前的筆記參考 : 



六. 借閱與預約資訊處理 :      

先來計算借閱書目是否逾期, 這可以利用到期日與今日的 datetime 物件相減的結果來判斷, 到期日減今日如果值為 0 表示此書為今日到期, 負數表示逾期, -1 表示已逾期一天, -2 為逾期兩天 ... 如果是正數表示尚未到期. 這個計算要用到 Python 內建的 datetime 套件中的兩個模組 : datetime 與 timedelta :

>>> from datetime import datetime, timedelta   

datetime 模組常用函式如下 :


 datetime 模組常用函式 說明
 now()  傳回目前日期時間之 datetime 物件
 today() 傳回今日之 datetime 物件


datetime 物件的常用方法如下 :


 datetime 物件常用方法 說明
 strftime(格式字串)  將 datetime 物件依格式字串 (例如 '%Y%m%d') 轉成字串
 strptime(日期時間字串, 格式字串) 將日期時間字串轉成 datetime 物件


先使用一個借閱紀錄範例來測試 : 

>>> borrow_books=[{'book_name': 'PyTorch深度學習攻略 : 核心開發者親授!', 
               'due_date': '2024-05-27', 'due_times': '2'}, 
              {'book_name': '一行指令學Python : 用機器學習掌握人工智慧',
               'due_date': '2024-05-28', 'due_times': '1'},
              {'book_name': '用Python學AIoT智慧聯網',
               'due_date': '2024-05-29', 'due_times': '0'}]

以其中第一本書的到期日為例 :

>>> due_date=borrow_books[0]['due_date']   
>>> due_date   
'2024-05-27'   

呼叫 datetime.strptime() 函式將此格式之日期字串轉成 datetime 物件 :

>>> due_date=datetime.strptime(due_date,  '%Y-%m-%d')     
>>> due_date   
datetime.datetime(2024, 5, 27, 0, 0)

呼叫 datetime.today() 函式會傳回今日 (現在) 的 datetime 物件 : 

>>> datetime.today()   
datetime.datetime(2024, 5, 28, 15, 3, 21, 200197)   

但計算日期差不需要時間部分, 只要呼叫 strptime() 方法並傳入格式字串 '%Y-%m-%d' 就可去除時間部分, 傳回日期字串 : 

>>> today_str=datetime.today().strftime('%Y-%m-%d')   
>>> today_str  
'2024-05-28'  

但要做日期相減須轉回 datetime 物件 :

>>> today=datetime.strptime(today_str, "%Y-%m-%d")   
>>> today   
datetime.datetime(2024, 5, 28, 0, 0)  

用 due_date 去減 today 即可得到一個表示日期差的 timedelta 物件 : 

>>> due_date-today   
datetime.timedelta(days=-1)

若要取得日期差之整數值可利用其 days 屬性 :

>>> (due_date-today).days   
-1

到期日與今日差為 -1 表示已經逾期一天了. 參考 :


寫一個迴圈來走訪上面的借閱紀錄範例 : 

>>> for book in borrow_books:   
    book_name=book['book_name']   
    due_date=book['due_date']   
    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:   
        print(f'{book_name} 已逾期 {abs(delta)} 天')   
    elif delta == 0:     
        print(f'{book_name} 今日到期')   
    elif delta == 1:   
        print(f'{book_name} 明日到期')  
        
結果如下 : 

PyTorch深度學習攻略 : 核心開發者親授! 已逾期 1 天
一行指令學Python : 用機器學習掌握人工智慧 今日到期
用Python學AIoT智慧聯網 明日到期

因為今日是 2024-05-28, 第一本書到期日為 2024-05-27 故顯示 '逾期 1 天'; 第二本書到期日為 2024-05-28 故顯示 '今日到期'; 第三本書到期日為 2024-05-29 故顯示 '明日到期'. 


七. 用 Line Notify 推播借閱與預約資訊 :    

前面的測試已完全擷取到個人書房借閱與預約資訊, 接下來要將這些訊息透過 Line Notify 推播到手機 Line 聊天室. 首先在手機 Line 中建立一個個人群組 "市圖借書預約資訊" (也可以推播到個人 Line 帳號, 但那樣會與其他個人聊天訊息混在一起) :




輸入群組名稱後按右上角的 "建立" 鈕即可 :




可以加入所有想獲得此推播訊息的人進入群組, 但最重要的是要將 Line Notify 加進來, 這樣程式發送的訊息才會被送進此群組. 按右上方的三條槓按鈕進入群組的設定頁面 :




按 "邀請" 鈕後, 在下列頁面中點選 Line Notify 邀請它進來群組內 :




可見 Line Notify 已加入此群組 :




然後登入 Line Notify 網站 :


按右上角帳號名稱會出現下拉式選單, 點選 "個人頁面" :




將個人頁面拉到最底下, 按 "發行權仗" 鈕 :




在上面輸入權仗名稱 "市圖借書預約資訊", 選擇要接收要接收訊息之聊天室為與權仗同名的 "市圖借書預約資訊" 群組, 然後按底下的 "發行" 鈕就會獲得一個權仗 :




先按左邊的複製鈕將權杖複製到純文字檔中儲存才能關閉此視窗 :




這樣就可以來撰寫 Line Notify 程式碼了 :

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
import re
from datetime import datetime
import time
import requests

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_books(account, password):
    try:
        # 登入我的書房
        options=Options()
        options.add_argument("--headless")
        driverpath='geckodriver.exe'
        browser=webdriver.Firefox(options=options)
        browser.implicitly_wait(20)
        browser.get("https://webpacx.ksml.edu.tw/personal/")
        loginid=browser.find_element(By.ID, "loginid")
        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')
        browser.implicitly_wait(20)
        login_btn.click()
        # 擷取借閱紀錄
        div_redblock=browser.find_element(By.CLASS_NAME, 'redblock')
        browser.implicitly_wait(20)
        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()
            pattern=r'\d{4}-\d{2}-\d{2}'
            due_date=book.find_element(By.XPATH, './ul[4]/li[2]').text
            item['due_date']=re.findall(pattern, 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]   
            borrow_books.append(item)
        browser.back() # 回上一頁
        # 擷取預約紀錄
        div_blueblock=browser.find_element(By.CLASS_NAME, 'blueblock')
        browser.implicitly_wait(20)
        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
            item['sequence']=re.findall(r'\d+', sequence)[0]
            reserve_books.append(item)
        browser.close()
        return (borrow_books, reserve_books)        
    except Exception as e:
        print(e)
        return None, None
    
if __name__ == '__main__':
    start=time.time()
    token='7CLpVmFpNihuN6GB0bQcc5M1nOhpAtony1966QFMgzz'   # 範例權仗
    users=[['family87', '123456'],
           ['amy08123', '123456'],
           ['kelly19', '123456'],
           ['peter120', '123456'],
           ['shinping92', '123456'],
           ['daddy587', '123456']]      # 範例帳號
    for user in users:   # 走訪每個帳號
        account=user[0]
        password=user[1]
        borrow_books, reserve_books=get_books(account, password)
        if borrow_books: 
            borrow=[]
            for book in borrow_books:
                book_name=book['book_name']   
                due_times=book['due_times']
                due_date=book['due_date']
                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)} 天, ' +\
                         '續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 0:        # 今日到期之書
                    msg=f'▶ {book_name} (今日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 1:        # 明日到期之書
                    msg=f'▶ {book_name} (明日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif 1 < delta < 7:    # 2~7 日內到期之書
                    msg=f'▶ {book_name} ({book["due_date"]} 到期, '\
                        f'續借次數 {due_times})'
                    borrow.append(msg)                    
            if len(borrow) != 0:   # 有 7 日內到期之書
                borrow.insert(0, f'\n❖ {account} 的借閱 :')
                msg='\n'.join(borrow)
                code=line_notify(msg, token)
                if code==200:
                    print('Line 訊息發送成功!')
                else:
                    print(f'Line 訊息發送失敗! (code={code})')
        if reserve_books:     # 有預約書才處理
            reserve=[]      # 儲存處理後的預約書
            i=1
            for book in reserve_books:  # 走訪每本預約書
                book_name=book['book_name']
                sequence=book['sequence']
                this_year=str(datetime.today().year)  # 取得今年西元年
                if sequence == this_year:
                    msg=f'{i}. {book_name} (已到館)'
                else:
                    msg=f'{i}. {book_name} (順位 {sequence})'
                reserve.append(msg)
                i += 1
            if len(reserve) != 0:
                reserve.insert(0, f'\n❖ {account} 的預約 :')
                msg='\n'.join(reserve)
                code=line_notify(msg, token)
                if code==200:
                    print('Line 訊息發送成功!')
                else:
                    print(f'Line 訊息發送失敗! (code={code})')
    end=time.time()
    print(f'執行時間:{end-start}')

此處的 token 變數即上面為了傳訊息到 "市圖借書預約資訊" 群組所申請的權仗 (此為範例而已), users 是記錄借書帳密的二維串列, 第一個元素是帳號, 第二個是密碼, 使用迴圈走訪此串列, 依序用各使用者之帳密呼叫 get_books() 去擷取市圖 "我的書房" 的借閱與預約資訊, 它會傳回兩個字典 : borrow_books 與 reserve_books, 分別用來儲存借閱資訊與預約資訊, 無資料則傳回 None, None. 傳回資訊經過處理後串成字串呼叫 line_notify() 推播至群組, 結果如下 :




八. 在 Mapleboard 上佈署爬蟲程式 :    

由於我的兩塊 Pi 3 的桌面怪怪的, Putty 遠端存取 OK, 但用 VNC 做遠端開發因程式集選單要很久才會打開, 所以我這次把爬蟲改為佈署到 Mapleboard 上用 Chomium 的 WebDriver 來跑 Selenium 爬蟲, 最後設定 Chrontab 定時於每天下午 4 點執行, 作法與在樹莓派幾乎相同 (預設使用者目錄為 /home/使用者, 樹莓派是 /home/pi). 

關於在 Mapleboard 上跑 Selenium 的測試參考 :


由於在 Mapleboard 跑 Chromium 速度比 Firefox 快, 所以將上面在 Win11 + Firefox 上跑的程式依據下面兩篇改寫為 Chromium 版 :

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

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_books(account, password):
    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(20)
        browser.get("https://webpacx.ksml.edu.tw/personal/")
        loginid=browser.find_element(By.ID, "loginid")
        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')
        browser.implicitly_wait(20)
        login_btn.click()
        # 擷取借閱紀錄
        div_redblock=browser.find_element(By.CLASS_NAME, 'redblock')
        browser.implicitly_wait(20)
        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()
            pattern=r'\d{4}-\d{2}-\d{2}'
            due_date=book.find_element(By.XPATH, './ul[4]/li[2]').text
            item['due_date']=re.findall(pattern, 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]   
            borrow_books.append(item)
        browser.back() # 回上一頁
        # 擷取預約紀錄
        div_blueblock=browser.find_element(By.CLASS_NAME, 'blueblock')
        browser.implicitly_wait(20)
        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
            item['sequence']=re.findall(r'\d+', sequence)[0]
            reserve_books.append(item)
        browser.close()
        return (borrow_books, reserve_books)        
    except Exception as e:
        print(e)
        return None, None
    
if __name__ == '__main__':
    start=time.time()
    token='7CLpVmFpNihuN6GB0bQcc5M1nOhpAtony1966QFMgzz'   # 範例權仗
    users=[['family87', '123456'],
           ['amy08123', '123456'],
           ['kelly19', '123456'],
           ['peter120', '123456'],
           ['shinping92', '123456'],
           ['daddy587', '123456']]      # 範例帳號
    for user in users:
        account=user[0]
        password=user[1]
        borrow_books, reserve_books=get_books(account, password)
        if borrow_books:   # 有借書, 不是傳回 None
            borrow=[]    # 儲存處理過的借書資訊
            for book in borrow_books:    # 走訪借書串列
                book_name=book['book_name']   
                due_times=book['due_times']
                due_date=book['due_date']
                due_date=datetime.strptime(due_date, '%Y-%m-%d')    # 轉成 datetime 物件
                today_str=datetime.today().strftime('%Y-%m-%d')    # 轉成日期字串去除時間
                today=datetime.strptime(today_str, "%Y-%m-%d")    # 轉回 datetime 物件
                delta=(due_date-today).days   # 計算逾期天數
                if delta < 0:   # 借書逾期了
                    msg=f'▶ {book_name} (逾期 {abs(delta)} 天, ' +\
                         '續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 0:   # 今日到期
                    msg=f'▶ {book_name} (今日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 1:    # 明日到期
                    msg=f'▶ {book_name} (明日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif 1 < delta < 7:   # 2~7 日內到期
                    msg=f'▶ {book_name} ({book["due_date"]} 到期, '\
                        f'續借次數 {due_times})'
                    borrow.append(msg)                    
            if len(borrow) != 0:    # 有處理過的借書資料
                borrow.insert(0, f'\n❖ {account} 的借閱 :')
                msg='\n'.join(borrow)
                code=line_notify(msg, token)
                if code==200:
                    print('Line 訊息發送成功!')
                else:
                    print(f'Line 訊息發送失敗! (code={code})')
        if reserve_books:    # 有預約書, 不是傳回 None
            reserve=[]    # 儲存處理過的預約資訊
            i=1   # 計數器
            for book in reserve_books:    # 走訪預約書串列
                book_name=book['book_name']
                sequence=book['sequence']
                this_year=str(datetime.today().year)   # 今年年分
                if sequence == this_year:   # 預約書到館 sequence 欄會抓到今年年分
                    msg=f'{i}. {book_name} (已到館)'
                else:
                    msg=f'{i}. {book_name} (順位 {sequence})'
                reserve.append(msg)
                i += 1
            if len(reserve) != 0:   # 有處理過之預約資料
                reserve.insert(0, f'\n❖ {account} 的預約 :')
                msg='\n'.join(reserve)
                code=line_notify(msg, token)
                if code==200:
                    print('Line 訊息發送成功!')
                else:
                    print(f'Line 訊息發送失敗! (code={code})')
    end=time.time()
    print(f'執行時間:{end-start}')

此檔案存放在 /home/tony1966/python 下, 這要用在編輯 Crontab 時要用到 :

tony1966@LX2438:~/python$ pwd   
/home/tony1966/python  

首先要用 chmod 指令將此程式檔改為可執行 :

tony1966@LX2438:~/python$ sudo chmod +x ksml_books_7.py  
tony1966@LX2438:~/python$ ls -ls ksml_books_7.py   
8 -rwxrwxr-x 1 tony1966 tony1966 5845 May 29 23:38 ksml_books_7.py     

先手動下指令測試看看 :

tony1966@LX2438:~/python$ /usr/bin/python3 /home/tony1966/python/ksml_books_7.py
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
Line 訊息發送成功!
執行時間:161.23561120033264

看起來沒問題, 然後用 crontab -e 指令編輯 Cron table :

tony1966@LX2438:~/python$ crontab -e    
no crontab for tony1966 - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        <---- easiest   (選 1 用 nano 編輯 Crontab)
  2. /usr/bin/vim.tiny
  3. /usr/bin/code
  4. /bin/ed

Choose 1-4 [1]: 1

這會開啟 Nano 來編輯 Cron table, 在最底下輸入 :

0 16 * * * /usr/bin/python3 /home/tony1966/python/ksml_books_7.py    

這表示要電腦在每天下午 4 點執行 ksml_books_7.py, 關於 Crontab 用法參考 : 


然後按 Ctrl + O, 按 Enter 存檔後按 Ctrl + X 跳出 Nano. 

忙了一周終於把長久以來想做的事給完成了! 了了一樁心願, 而且透過此爬蟲任務也深刻地理解 Selenium 強大的功能, 也學會了 XPATH 用法, 哎呀, 真是一舉多得啊! 


2024-06-05 補充 :

今天參考下面網站的網頁圖示對送出訊息樣式 (主要是項目的標示) 做了一些修改 : 

https://tw.piliapp.com/symbol/  (網頁特殊符號)

程式修改部分為借閱之 delta :

                if delta < 0:
                    msg=f'🅧 {book_name} (逾期 {abs(delta)} 天, ' +\
                         '續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 0:
                    msg=f'❶ {book_name} (今日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 1:   
                    msg=f'❷ {book_name} (明日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 2:   
                    msg=f'❸ {book_name} (後天到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif 2 < delta < 8:   
                    msg=f'▶ {book_name} ({book["due_date"]} 到期, '\
                        f'續借次數 {due_times})'
                    borrow.append(msg)

❶ 表示當日到期;  ❷ 表示明日到期; 新增 ❸ 為後天到期, 其餘至第七日才使用 ▶ 符號. 

預約書項目符號修改部分 : 

            i=0
            j=['①', '②', '③', '④', '⑤']
            for book in reserve_books:
                book_name=book['book_name']
                sequence=book['sequence']
                this_year=str(datetime.today().year)
                if sequence == this_year:
                    msg=f'{j[i]} {book_name} (已到館)'
                else:
                    msg=f'{j[i]} {book_name} (順位 {sequence})'
                reserve.append(msg)
                i += 1

此處增加一個 j 串列來儲存預約書數量, i 初始值從 1 改為 0. 





2024-06-06 補充 :

我覺得上面修改的項目符號有點不合理, 借閱書當日到期應該顯示 ⓿ 才對, 因為已經不足一天, 明日到期才是 ❶. 另外, 到館之預約書改用黑底數字標示, 到期日 3 日以上的標示改用 ✦ 符號, 程式碼再度修改如下, 借閱書部分 :

                if delta < 0:
                    msg=f'🅧 {book_name} (逾期 {abs(delta)} 天, ' +\
                         '續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 0:
                    msg=f'⓿ {book_name} (今日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 1:   
                    msg=f'❶ {book_name} (明日到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif delta == 2:   
                    msg=f'❷ {book_name} (後天到期, 續借次數 {due_times})'
                    borrow.append(msg)
                elif 2 < delta < 8:   
                    msg=f'✦ {book_name} ({book["due_date"]} 到期, '\
                        f'續借次數 {due_times})'
                    borrow.append(msg)  

🅧 表示已逾期;  表示當日到期; ❶ 表示明日到期; ❷ 為後天到期; 其餘至第七日才使用  符號. 

預約書顯示部分 :

            i=0
            j=['①', '②', '③', '④', '⑤']
            k=['❶', '❷', '❸', '❹', '❺']
            for book in reserve_books:
                book_name=book['book_name']
                sequence=book['sequence']
                this_year=str(datetime.today().year)
                if sequence == this_year:
                    msg=f'{k[i]} {book_name} (已到館)'
                else:
                    msg=f'{j[i]} {book_name} (順位 {sequence})'
                reserve.append(msg)
                i += 1

此處新增一個 k 串列儲存黑底的 1~5, 當預約書為已到館狀態就用 k 串列顯示, 否則用 j 串列, 結果如下 : 





經過以上修改後的外觀總算讓我滿意了.


2024-06-16 補充 :

由於發現一些小問題, 所以今天對程式做了一些修補, 一是當借書逾期時 due_times 欄位變成無值, 直接顯示變數名稱 {due_times}; 二是已到館的預約書只顯示 "已到館", 如果能加上取書的保留期限那就更完美了, 三是刪除了多餘的 implicitly_wait() 呼叫, 此方法只要在 WebDriver 物件建立後呼叫一次即可.




逾期的書一定要先還, 因此已續借次數沒有意義, 解決辦法就是直接拿掉逾期書的 {due_times} 輸出, 只顯示已逾期幾天 :

                if delta < 0:
                    msg=f'🅧 {book_name} (逾期 {abs(delta)} 天)'
                    borrow.append(msg)

至於新增已到館預約書之保留期限日期, 我修改了判斷預約書是否為已到館之邏輯, 之前是用 sequnce 欄位, 因為預約中書籍的順位 sequence 的 XPATH 為 "./ul[7]/li[1]", 此位置在已到館書籍則為 "流通狀態", 裡面包著的 span 元素其內容就是保留期限 :





已到館的書順位就是 0, 此資訊不重要, 因此可以利用 XPATH="./ul[7]/li[1]" 這個原本是 sequence 順位的欄位來判別是否已到館, 如果 span 元素內容含有 "預約待取" 字樣表示已到館, 那麼就用正規式擷取後面的保留期限, 程式碼修改如下 : 

            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'    # 預約順位不重要設為 0
            else: # 預約中
                item['ready_for_pickup']=False   # 預約中
                item['expiration']=''    # 預約中保留期限日期無意義設為 ''
                item['sequence']=re.findall(r'\d+', sequence)[0]  # 取出目前順位
            reserve_books.append(item)

此處 item 字典新增 ready_to_pickup 與 expiration 欄位, 前者值為 True/False 分別表示 "已到館"/預約中", 後者則用來儲存已到館書的保留期限. 

主程式中處理預約書的部分修改為 :

            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

經過這樣調整後程式的邏輯就更明確了, 結果也符合要求 :




佈署於 Mapleboard 的程式碼也隨之更新 (帳號密碼權杖等均為虛構) : 


2024年5月27日 星期一

高科大還書 1 本 : AI 繪圖夢工廠

今天下班順路去母校圖書館還下面這本書 (有人預約) : 



Source : 天瓏


此書主要介紹 Stable Diffusion, Leonardo.ai 與 Midjourney 這三款 AI 生圖工具, Midjourney 以前有免費方案時我曾玩過, 須透過 Discord 進去, 坦白說我對 Discord 令人眼花撩亂的介面不太喜歡, 取消免費方案後我更不會去用 Midjourney 了. 反觀 Leonardo.ai 就很佛心, 至今仍提供免費生圖方案, 免費用戶每天會獲得 150 個免費 token, 生一張圖依尺寸大小不同會用掉 1~4 個 token, 亦即至少每天可讓免費用戶產生近 40 張大尺寸圖, 不夠再付費購買 token, 這實在很棒. 至於開放原始碼的 Stable Diffusion 本身就是免費的 AI 生圖模型, 只要有符合條件的硬體 (獨立顯卡 + 足夠 DRAM) 就可以在本機自行生圖, 或者購買 Colab 運算單元在雲端跑 WebUI 生圖. 

以下是我看完第三章 Stable Diffusion 的筆記 :


1. Stable Difussion 最主要的兩個功能 :
  • text2img : 文生圖
    在 Prompt 區輸入區輸入圖片的正向與負向提示詞按 "Generate" 鈕即可.
  • img2text : 圖生圖
    拖曳或上傳圖片後按 按 "Generate" 鈕即可, 也可以佐以正負向提示詞. 
2. 文生圖 (text2img) : 

負向提示詞用來排除不要的畫風, 物件或結構, 不論正向或負向, 排在越前面的提示詞影響力越大. 提示詞之間要用半形逗號隔開, 也可以用 + 號連接短提示詞, 或用 | 串接提示詞來表示循環繪製效果. 空格與換行在提示詞中無作用, 不會影響提示詞權重. 例如 :

8k portrait, beautiful cyborg, brown hair, intricate, elegant

若將 brown hair 改成 brown hair | yellow hair 會有棕黃相間的效果 : 

8k portrait, beautiful cyborg, brown hair | yellow hair, intricate, elegant

如果想要改變提示詞權重可以這麼做 : 
  • 在提示詞後面串接 ":數值" 來改變其權重. 
  • 將提示詞用小括號 () 括起來, 這會提升其權重為 1.1 倍.
  • 將提示詞用中括號 [] 括起來, 這會降低其權重為 0.9 倍
若將 beautiful cyborg 與 elegant 用雙重小括號括起來會強化美麗機器人與優雅這兩個特徵 : 

8k portrait, ((beautiful cyborg)), brown hair | yellow hair, intricate, ((elegant))

若加上國家或區域名稱則會生成具有地區特色的圖像, 例如 : 

8k portrait, ((beautiful cyborg)), brown hair | yellow hair, intricate, ((elegant)), Taiwan

另外為了避免生出例如六個手指的人類, 通常會給予 ((disfigured)) 的負向提示詞. 

注意, 提示詞不論文生圖或圖生圖都可以用. 

文生圖功能區選項欄位 :
  • Sampling method (取樣方法) :
    此選項影響圖像風格, 可用選項例如 Euler a, LMS, DPM2 a, DPM fast, DPM++2S a Karras, DPM++2M Karras 等.
  • Sampling steps (取樣步數) :
    步數越大圖像品質越精緻, 但生圖時間會拉長, 一般設在 20~30 即可. 
  • Restore face (臉部修正) :
    雖然會增加算圖時間, 但此項最好要勾選, 避免生成的人臉有扭曲歪斜情況. 
  • Hires, fix (高品質圖像) :
    勾選後會生成較細緻的圖片 (但也增加生圖時間). 如果生出一張滿意的圖片, 可以設定 Seed 欄的隨機種子固定風格後勾選此項重新生成更精細的圖像. 
  • Width, Height (圖像尺寸) :
    預設生成圖片尺寸為 512*512 px, 可以視需要自行調整.
  • Tilting (拼接) :
    勾選此項可以生成像磁磚那樣重複拼接的圖像. 
  • Batch size (輸出張數), Batch count (輸出批次) :
    Batch size 是設定每次生成幾張圖 (建議設 2 或 4), 而 Batch count 則是設定算幾次 (建議設為 1).
  • CFG (縮放因子) : 
    用來設定提示詞對生成之圖的影響力, 值越大生成之圖片越符合提示詞之描述, 值越小則模型加入的隨機性越大. 
  • Seed (隨機種子) :
    預設 -1 表示不固定隨機種子, 則每次生圖的方式會因為隨機性而差異很大, 可設為任意正整數, 例如經典的 42, 這樣相同的提示詞每次生圖方式就會固定. 欄位右邊的骰子按鈕可以讓隨機種子回復預設的 -1 (完全隨機), 按最右邊的循環按鈕則會將隨機種子固定為目前的設定值. 

3. 圖生圖 (img2img) : 

圖生圖是以上傳的圖片為基礎擴增原圖風格來產生新圖像. 

圖生圖功能區選項設定 : 
  • Width (寬度), Height (高度) :
    可設定圖片縮放的尺寸. 
  • Resize mode (縮放模式) :
    Just resize (直接縮放) : 依寬高設定縮放, 景物基本不變
    Crop and resize (剪裁縮放) : 剪裁後等比例依寬高設定縮放, 景物可能被刪減
    Resize and fill (填充) : 將原尺寸與寬高設定不足之處填充, 景物會增加
    Just resize-latent upscale (調整大小) : 與 Just resize 類似, 但隨機性較高
  • Denoising strength (重繪幅度) : 
    用來設定模型圖生圖依樣畫葫蘆程度, 值越低越像上傳之圖片, 值越高則模型加入隻創意越多. 
        
4. Stable Diffusion 模型風格 : 

因 Stable Diffusion 為開源模型, 許多人會將不同風格之微調後模型上傳到 Civitai 網站 :


比較常用的 Stable Diffusion 風格模型 : 
  • Realistic Vision (擅長擬真的虛擬人物)
  • ChilloutMix (擅長亞洲臉孔女性)
  • Deliberate (擅長虛實混和的多樣化藝術風格)
  • DreamShaper (擅長美版藝術圖像)
  • Rev Animated (擅長美版藝術圖像, 但添加更多光影與層次細節)
  • MeinaMix (擅長日版動漫風格)
  • OrangeMix (擅長光感柔和之日版風格)

5. PNG Info :

 可以將 Stable Diffusion 生成的圖像上傳到 PNG Info 頁籤反查所使用之提示詞與參數. 

書中範例可在登入旗標網站後下載 :


2024 年第 21 周記事

本周仍然在忙著寫爬蟲與整理筆記, 這幾天寫市立圖書館爬蟲讓我收穫頗豐, 對 Selenium 的掌握度大幅上升, 幾乎可以降伏任何 Javascript 生成的動態網頁, 只是抓取速度沒有靜態網頁那麼快, 其實能抓到資料完成任務就行, 速度不是問題. 原先打算要開展的 PyTorch 學習因為爬蟲而遞延了, 我以為要搞定爬蟲只是一兩個禮拜, 現在覺得沒這麼簡單, 還有 Scrapy 要學, 看起來不啟動多執行緒不行了. 

這周由於小舅與婷婷協助載爸到隔壁鄉的晉德中醫做針灸, 爸的腳傷已恢復得差不多, 僵硬的小腿肚也回復鬆軟, 走路也不再疼痛, 上下樓梯可以雙腳使力了. 婷婷是上周帶小孩來玩聽我詢問小舅開中醫診所的同學, 我說想載爸去那邊針灸看看, 她說自己的拇指也因為長期打電腦無法自由彎曲想去看, 說可以載爸一起去, 去了 4 趟即有明顯改善, 我覺得在筋骨這方面還是中醫比較在行.

鄉下家的無線話機前兩周就無法使用, 測試發現市線路問題, 但連續兩個周末都事情忙沒時間查線, 昨天下午忙完廚房著手查線, 發現線路是走室內到客廳酒櫥後面, 正要移動酒櫥查看, 剛好婷婷與小朋友們來了, 查線工作只好推遲到下周. 但下個周日要跟爸去獅形頂給五穀爺請, 我看本周最好周五下班就回鄉下才有足夠時間完成線路修復任務. 

上週載回鄉下的幾株老的矮仙丹種在大門口的馬路邊, 週六下午回到鄉下發現大都乾掉, 只有兩株存活, 趕緊澆水看看能否救活乾掉的那幾株. 大樓主委種剩下給我的約 8 盆矮仙丹幼苗我打算種植在大門口波蘿蜜樹下的平台缺口上, 但要先做點木工前置作業圍出一個長方形才能放泥土進去, 列入下周任務之一. 大樓申請的 50 株幼苗核准公文下來了, 主委叫我去楠梓的苗圃領回, 但因艾維尼颱風這幾天會下雨, 可能週四或周五較適當. 

鄉下家的小白上週五離家後至今未回來, 依照過往經驗應該是不會回來了 (公貓大都在一年後會離開). 它每天回來吃完飼料就會下芭樂田裡, 在一棵它專有的芭樂樹下睡覺, 睡飽了就越過馬路, 順著田埂往下走出去找女朋友, 這是它每天的日常. 當然也有可能遇到危險或吃到毒藥 GG 了, 但我寧願相信是找到另一家主人了. 他們四兄妹如今只剩小灰而已, 她的三隻貓兒子已經會吃乾糧了, 但仍躲在庫房裡, 小灰還沒帶出來走動. 

2024年5月26日 星期日

Colab 免付費方案不允許執行 Stable Diffusion

我向母校圖書館借閱的 "AI 繪圖夢工廠" 的第三章是介紹如何在 Colab 上執行 Satble Diffusion, 一直想找時間來實測一下, 但最近都在忙著寫爬蟲沒空. 由於此書即將到期, 所以晚上趕緊抽空玩看看. Satble Diffusion 是開放原始碼風格多樣的 AI 生圖模型, 可以下載模型後以自有資料集進行訓練或微調. 

在本機跑 Satble Diffusion 的最低硬體條件要求 8GB 以上 DRAM 與 NVIDIA 4GB 以上獨立顯示卡. 書中建議使用 Google Colab 來跑, 有一位高手 Camenduru 在 GitHub 上分享他於 Colab 上跑 Stable Diffusion 的 WebUI 專案, 提供了很多風格的運算程式碼連結, 點擊即可連至 Colab 執行. 不過書中提到 Google 已開始限制免費用戶算圖用量, 生成 1~2 張圖就會斷線, 但我實測結果則是, Colab 現在根本就不讓免費帳戶跑這種耗費大量算力的運算, 我才執行不到 5 分鐘即被強制斷線. 

Camenduru 的 Colab Stable Diffusion 專案網址如下 :


往下拉至 Colab 部分, 搜尋 Deliberate 這個風格 : 



點擊中間的穩定版超連結就會連結至 Colab : 




按左上角的 Play 鍵執行程式碼會出現一個警告視窗, 按 "仍要繼續" 鈕 : 




出現第二個警告視窗, 按 "仍要執行" 鈕 : 




這樣就會開始執行程式碼了, 但不到 5 分鐘就被強制斷線 (Colab 會偵測) : 




重新連線試了好多次都是如此, 只好死心了. 作者建議付費購買 Colab Pro 算力, Pay as You Go 方案 10 美金可獲得 100 個運算單元, 約可連續算圖 50~60 小時, 參考 :


好用的線上 HTML 碼格式化工具 : HTML Formatter

最近在測試 Python 爬蟲程式時發現一個好用的線上工具 : 


使用爬蟲工具如 BeautifulSoup 或 Selenium 擷取網頁時會抽絲剝繭一步步往目標資料前進, 通常會先觀察 response.text 或 response.page_content 結構來找尋目標資料位置, 但前端網頁設計師將網頁整整齊齊地寫 (他們才不會考慮寫爬蟲的人的感受), 檢視 HTML 原始碼時會發現大都亂糟糟地擠在一起 (應該是用壓縮器去除空格與跳行以縮小檔案大小), 這時就可以把原始碼貼到這個網站, 按下 "Format HTML" 鈕就會在底下看到格式化後整整齊齊的 HTML 碼 :




將結果複製到 HTML 編輯器就能好好地觀察結構了. 

好站 : Linux 壓縮與打包指令

這兩天在安裝 WebDriver 時需要解壓縮 gz 檔, 但 Linux 指令多如牛毛記不牢, 上網搜尋找到下面這篇教學馬上派上用場 : 


有空要來整理一下我收集的 Linux 常用指令集, 不過通常搜尋更快. 

露天購買手機腰包

上個月爸的小米 POCO 手機電池膨脹無法開機, 在 momo 買了 OPPO A38 代替, 但回鄉下發現手機腰包也該換了, 今天上露天買了這款 :





XXL 的兩個全家免運 298 元. 

2024年5月25日 星期六

Mapleboard MP510-50 測試 (二十) : 安裝 Chromium WebDriver

中午完成在 Mapleboard 上使用 Selenium 操控 Firefox 瀏覽器的測試並成功執行爬蟲程式後, 下午回到鄉下老家檢查 Mapleboard 的 Ubuntu Mate 有預載 Chromium 瀏覽器 (故不須再安裝), 晚飯後便來研究如何用 Selenium 操控 Chromium 瀏覽器. 


1. 安裝 Chromium 瀏覽器 :

如果 Ubuntu 沒有 Chromium 的話, 可用下列指令安裝 : 

sudo apt-get install chromium-browser   

因 Mapleboard 的 Ubuntu Mate 出廠就有 Chromium, 所以這一步跳過, 


2. 安裝 Chromium 驅動程式 :  

Ubuntu 的安裝路徑是 /usr/lib/chromium-browser/chromedriver, 所以先檢查一下 :

tony1966@LX2438:~/python$ ls -ls /usr/lib/chromium-browser/chromedriver   
ls: 無法存取 '/usr/lib/chromium-browser/chromedriver': 沒有此一檔案或目錄

可見 Ubuntu Mate 尚未安裝驅動程式, 安裝指令如下 : 
 
sudo apt-get install chromium-chromedriver 

tony1966@LX2438:~/python$ sudo apt-get install chromium-chromedriver 
[sudo] tony1966 的密碼: 
正在讀取套件清單... 完成
正在重建相依關係... 完成  
正在讀取狀態資料... 完成  
下列【新】套件將會被安裝:
  chromium-chromedriver
升級 0 個,新安裝 1 個,移除 0 個,有 7 個未被升級。
需要下載 2,308 B 的套件檔。
此操作完成之後,會多佔用 77.8 kB 的磁碟空間。
下載:1 http://ports.ubuntu.com jammy-updates/universe arm64 chromium-chromedriver arm64 1:85.0.4183.83-0ubuntu2.22.04.1 [2,308 B]
取得 2,308 B 用了 1s (2,111 B/s)                  
選取了原先未選的套件 chromium-chromedriver。
(讀取資料庫 ... 目前共安裝了 289968 個檔案和目錄。)
正在準備解包 .../chromium-chromedriver_1%3a85.0.4183.83-0ubuntu2.22.04.1_arm64.deb……
解開 chromium-chromedriver (1:85.0.4183.83-0ubuntu2.22.04.1) 中...
設定 chromium-chromedriver (1:85.0.4183.83-0ubuntu2.22.04.1) ...

再次檢查安裝路徑就可以看到驅動程式了 :

tony1966@LX2438:~/python$ ls -ls /usr/lib/chromium-browser/chromedriver   
0 lrwxrwxrwx 1 root root 22 Oct 18  2022 /usr/lib/chromium-browser/chromedriver -> ../../bin/chromedriver  


3. 使用 Selenium 控制 Chromium 瀏覽器 :    

安裝好 Chromium 驅動程式就可以來測試以 Selenium 控制瀏覽器, 因為 Selenium 是 v4.21.0, 驅動程式的路徑要用 Servive 物件來做, 語法程序與前一篇 Firefox 的差不多 : 

首先匯入套件模組 : 

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

其中 Options 類別只有在要設定選項時 (例如無頭模式) 才需要匯入. 

接著定義驅動程式路徑變數, 然後把它傳給 Service 類別的建構式以建立 Service 物件, 最後呼叫 webdriver.Chrome() 並傳入 Service 物件即可建立 WebDriver 物件, 同時會開啟一個 Chromium 視窗 (WebDriver 代表此瀏覽器) :

driverpath='/usr/lib/chromium-browser/chromedriver'
service=Service(driverpath)
browser=webdriver.Chrome(service=service)

如果要用無頭模式, 則要先呼叫 Options() 建構式來建立一個 Options 物件, 然後呼叫其 add_argument() 方法傳入 '--headless', 最後在呼叫 webdriver.Chrome() 傳入 Options 與 Service 物件即可 :

options=Options() 
options.add_argument("--headless") 
browser=webdriver.Chrome(options=options, service=service)

注意, options 必須做為第一參數. 例如 : 

>>> from selenium import webdriver  
>>> from selenium.webdriver.chrome.service import Service    
>>> driverpath='/usr/lib/chromium-browser/chromedriver'   
>>> service=Service(driverpath)   
>>> browser=webdriver.Chrome(service=service)  
>>> browser.get('https://tw.yahoo.com')   

結果如下 : 




呼叫 WebDriver 物件的 close() 方法即可關閉此 Chromium 瀏覽器 : 

>>> browser.close()   

參考 :



2024-07-06 補充 :

今天整理部落格文章發現, 原來我之前寫過一篇如何在樹莓派安裝 Web Driver 的文章 :


雖然 Ubuntu 與樹莓派有些不同, 還是可以作為參考. 

Mapleboard MP510-50 測試 (十九) : 安裝 Selenium 與 Firefox WebDriver

昨天搞定忙了三天的市立圖書館爬蟲程式, 因為打算要部署在樹莓派, 想說先到樹莓派上測試一下 Firfox/Chronium 的 Web Driver 是否管用, 雖然今年三月曾在鄉下的 Pi 3 上安裝測試過 Chronium 的 Web Driver 確認可用, 但這次在高雄這塊 Pi 上卻不行, 甚至連線回去鄉下的 Pi 3 居然無法載入 Web Driver, 明明測試 OK 後甚麼都沒動, 居然兩個月之後 GG 了. 從昨晚到今天早上不斷尋找解決辦法都徒勞無功, 最後決定放棄. 參考 : 


轉頭想起已經兩三個月沒摸的 Mapleboard, 它的效能比 Pi 3 好太多 (64 位元 CPU + 4GB DRAM), 而且在 Ubuntu 22.04 上直接安裝最新的 Selenium 4 跟 Windows 一樣, 或許直接用 Firefox 就可以不用改程式直接跑圖書館爬蟲程式了. 但過程也沒想像中順利, 還是遇到 Firefox Web Driver (gecko) 版本與路徑問題, 所幸有在網路上找到解決方案順利完成 Firefox 驅動程式載入問題, 過程紀錄如下 : 


1. 安裝 Selenium : 

這部分沒有問題, 安裝到最新版的 Selenium v4.21.0 : 

tony1966@LX2438:~$ pip3 install selenium    
Defaulting to user installation because normal site-packages is not writeable
Collecting selenium
  Downloading selenium-4.21.0-py3-none-any.whl (9.5 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.5/9.5 MB 5.0 MB/s eta 0:00:00
Collecting certifi>=2021.10.8
  Downloading certifi-2024.2.2-py3-none-any.whl (163 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 163.8/163.8 KB 5.5 MB/s eta 0:00:00
Requirement already satisfied: urllib3[socks]<3,>=1.26 in /usr/lib/python3/dist-packages (from selenium) (1.26.5)
Collecting typing_extensions>=4.9.0
  Downloading typing_extensions-4.12.0-py3-none-any.whl (37 kB)
Collecting trio~=0.17
  Downloading trio-0.25.1-py3-none-any.whl (467 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 467.7/467.7 KB 5.2 MB/s eta 0:00:00
Collecting trio-websocket~=0.9
  Downloading trio_websocket-0.11.1-py3-none-any.whl (17 kB)
Collecting attrs>=23.2.0
  Downloading attrs-23.2.0-py3-none-any.whl (60 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60.8/60.8 KB 3.5 MB/s eta 0:00:00
Requirement already satisfied: idna in /usr/lib/python3/dist-packages (from trio~=0.17->selenium) (3.3)
Collecting sortedcontainers
  Downloading sortedcontainers-2.4.0-py2.py3-none-any.whl (29 kB)
Collecting outcome
  Downloading outcome-1.3.0.post0-py2.py3-none-any.whl (10 kB)
Collecting sniffio>=1.3.0
  Downloading sniffio-1.3.1-py3-none-any.whl (10 kB)
Collecting exceptiongroup
  Downloading exceptiongroup-1.2.1-py3-none-any.whl (16 kB)
Collecting wsproto>=0.14
  Downloading wsproto-1.2.0-py3-none-any.whl (24 kB)
Collecting PySocks!=1.5.7,<2.0,>=1.5.6
  Downloading PySocks-1.7.1-py3-none-any.whl (16 kB)
Collecting h11<1,>=0.9.0
  Downloading h11-0.14.0-py3-none-any.whl (58 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58.3/58.3 KB 2.8 MB/s eta 0:00:00
Installing collected packages: sortedcontainers, typing_extensions, sniffio, PySocks, h11, exceptiongroup, certifi, attrs, wsproto, outcome, trio, trio-websocket, selenium
Successfully installed PySocks-1.7.1 attrs-23.2.0 certifi-2024.2.2 exceptiongroup-1.2.1 h11-0.14.0 outcome-1.3.0.post0 selenium-4.21.0 sniffio-1.3.1 sortedcontainers-2.4.0 trio-0.25.1 trio-websocket-0.11.1 typing_extensions-4.12.0 wsproto-1.2.0

檢查版本 : 

tony1966@LX2438:~$ python3   
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import selenium    
>>> selenium.__version__     
'4.21.0'


2. 下載 Firefox Web Driver : (此步驟似乎是多餘的) 

我原先參考之前的筆記 :


與下面這篇教學 :


開啟 Mapleboard 的 Firefox 連線 Firefox 驅動程式下載網址 :


往下拉到 "Assets" 這區塊, 要選擇最上面 linux-aarch-64 的那個下載 : 




我第一次是下載 linux-64 那個, 但呼叫 webdriver() 時卻出現下面兩個錯誤訊息 :

"selenium.common.exceptions.WebDriverException: Message: Unsupported platform/architecture combination: linux/aarch64"

"OSError: [Errno 8] Exec format error: '/usr/local/bin/geckodriver'"

經搜尋才知道這錯誤是因為安裝了錯誤的驅動程式版本所致, Mapleboard 的 Linux 是 aarch 版本的才對, 改下載 linux-aarch-64 的那個才決此問題, 參考 :


"You have most probably installed a version of geckodriver that is meant for a different OS/platform! get the correct version from https://github.com/mozilla/geckodriver/releases and replace the one you have"

這過程中也順便學到一個查詢系統與 CPU 是 32/64 位元的 Linux 指令 :

getconf LONG_BIT 

tony1966@LX2438:~$ getconf LONG_BIT   
64

參考 : 


驅動程式下載後直接在 Desktop 操作解壓縮, 或在終端機使用 tar -xvf 指令將 .gz 檔解開, 得到一個 geckodriver 檔案, 然後將它移動到 /usr/local/bin 底下, 並且用 chmod 指令將其變更為可執行檔  : 

tony1966@LX2438:~/python$ sudo mv geckodriver /usr/local/bin/  
[sudo] tony1966 的密碼: 
tony1966@LX2438:~/python$ cd /usr/local/bin   
tony1966@LX2438:/usr/local/bin$ sudo chmod +x geckodriver    

結果還是無效, 出現如下錯誤 : 

raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: Process unexpectedly closed with status 1


3. 使用 Firefox 內建的 geckodriver 驅動程式 : 

最後找到這篇 : 





原來 Mapleboard 上安裝的 Firefox 是 snap 版 (Ubuntu 22.04 預載), 已經內建了 Firefox 的驅動程式 geckodriver, 只要用 ln 指令連結它即可, 先用 whereis 指令尋找 firefox.geckodriver 路徑 : 

tony1966@LX2438:~/python$ whereis firefox.geckodriver    
firefox.geckodriver: /snap/bin/firefox.geckodriver  '

然後用 ln 將其連結至 geckodriver : 

tony1966@LX2438:~/python$ ln -s /snap/bin/firefox.geckodriver geckodriver    

這樣就會在目前工作目錄 python 下建立一個 geckodriver 連結 :




但必須用 Selenium 4 新的連結驅動程式的作法, 使用 Service 類別來建立 Service 物件 :

>>> from selenium import webdriver   
>>> from selenium.webdriver.chrome.service import Service    
>>> driverpath = './geckodriver'      # 指定驅動程式連結的路徑 
>>> firefox_service = Service(driverpath)      
>>> browser=webdriver.Firefox(service=firefox_service)   # 傳入 Service 物件載入驅動程式  
>>> browser.get('https://tw.yahoo.com')     


結果如下 :




終於搞定了! 

順便記一下插曲 : 雖然上面呼叫 Firefox() 不再馬上跳出錯誤, 看起來正在啟動 Firefox 時, 卻突然出現 "Profile missing" 視窗 : 



我參考下面這篇官方教學 : 


在 Mapleboard 手動開啟 Firefox, 然後在網址列輸入 about:profiles 按 Enter 就會跳出下面視窗 :




甚麼都不用動, 使用 default profile 開啟 Firefox, 直接按右下角的 Start Firefox 鈕, 再次執行 webdriver.Firefox() 就不會再抱怨找不到 profile 而順利開啟瀏覽器. 


2024-05-25 補充 :

我回到 Windows 用上面的 Service 物件發現會出現錯誤, 似乎 Windows 上直接呼叫 webdriver.Firefox() 即可, 如要無頭模式就傳入 options=options. 

另外, 在下面這篇文章中提到使用 Phantom.js 可以避開 WebDriver 問題 :





有空可以來測試看看.