2025年12月13日 星期六

在 Windows 上安裝原生版 Claude Code

最近與明賢表弟在 LINE 上聊到 Vibe coding, 他今年用 Claude Code 開發軟體的經驗是, 以後程式員都得放棄自行 coding 的想法, 因為 AI 寫程式的能力太強, 軟體工程師都應轉型為具有系統思維的 PM. 這讓我對 Claude Code 突然有了興趣, 在 momo 買了市面上僅有的兩本書來一探究竟 : 


今天先把 Claude Code 開發環境架起來, 主要參考旗標這本書, 在 LG Gram 筆電上透過 Scoop 軟體從網路下載 PowerShell 腳本程式安裝原生版 Cloude Code, 步驟如下 : 


1. 設定 PowerShell 視窗腳本執行原則 (Execution Policy) : 

首先在 Windows 搜尋 powershell 並直接開啟此視窗程式 (不要用系統管理員), 然後複製貼上下列指令按 Enter 執行, 這樣才能執行從網路下載具有數位簽章的 PowerShell 腳本程式 Scoop :

PS C:\Users\tony1> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser  
PS C:\Users\tony1>




書上說會出現 Yes/No 詢問提示, 但我的沒有, 這可能是 -Scope CurrentUser 的關係, 這表示變更範圍只影響目前使用者, 不會影響到系統上的其他使用者或整個電腦, 所以 PowerShell 有時會省略提示, 直接接受對當前使用者設定的修改. 


2. 安裝套件管理工具 Scoop : 

貼上下列指令從網路下載 Scoop 腳本執行安裝 :

PS C:\Users\tony1> Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression   
Initializing...
Downloading...
Extracting...
Creating shim...
Adding ~\scoop\shims to your path.
Scoop was installed successfully!
Type 'scoop help' for instructions.


3. 安裝 Git 與設定 git-bash 環境變數 : 

使用 Scoop 來安裝 Git : 

PS C:\Users\tony1> scoop install git   
Scoop uses Git to update itself. Run 'scoop install git' and try again.
Installing '7zip' (25.01) [64bit] from 'main' bucket
7z2501-x64.msi (1.9 MB) [====================================================] 100%
Checking hash of 7z2501-x64.msi ... ok.
Extracting 7z2501-x64.msi ... done.
Linking ~\scoop\apps\7zip\current => ~\scoop\apps\7zip\25.01
Creating shim for '7z'.
Creating shim for '7zFM'.
Making C:\Users\tony1\scoop\shims\7zfm.exe a GUI binary.
Creating shim for '7zG'.
Making C:\Users\tony1\scoop\shims\7zg.exe a GUI binary.
Creating shortcut for 7-Zip (7zFM.exe)
Persisting Codecs
Persisting Formats
Running post_install script...done.
'7zip' (25.01) was installed successfully!
Notes
-----
Add 7-Zip as a context menu option by running:
reg import "C:\Users\tony1\scoop\apps\7zip\current\install-context.reg"
Installing 'git' (2.52.0) [64bit] from 'main' bucket
PortableGit-2.52.0-64-bit.7z.exe (57.4 MB) [=================================] 100%
Checking hash of PortableGit-2.52.0-64-bit.7z.exe ... ok.
Extracting PortableGit-2.52.0-64-bit.7z.exe ... done.
Linking ~\scoop\apps\git\current => ~\scoop\apps\git\2.52.0
Creating shim for 'sh'.
Creating shim for 'bash'.
Creating shim for 'git'.
Creating shim for 'gitk'.
Making C:\Users\tony1\scoop\shims\gitk.exe a GUI binary.
Creating shim for 'git-gui'.
Making C:\Users\tony1\scoop\shims\git-gui.exe a GUI binary.
Creating shim for 'scalar'.
Creating shim for 'tig'.
Creating shim for 'git-bash'.
Making C:\Users\tony1\scoop\shims\git-bash.exe a GUI binary.
Creating shortcut for Git Bash (git-bash.exe)
Creating shortcut for Git GUI (git-gui.exe)
Running post_install script...done.
'git' (2.52.0) was installed successfully!
Notes
-----
Set Git Credential Manager Core by running: "git config --global credential.helper
manager"

To add context menu entries, run
'C:\Users\tony1\scoop\apps\git\current\install-context.reg'

To create file-associations for .git* and .sh files, run
'C:\Users\tony1\scoop\apps\git\current\install-file-associations.reg'

注意, 此處安裝 Git 並非是要用到版本控制, 而是要利用 Git 所附的 git-bash 來執行 Claude Code. 用 where.exe 查詢 bash 程式位置 (若有多個要找路徑中含有 scoop 者) : 

PS C:\Users\tony1> where.exe bash
C:\Users\tony1\scoop\shims\bash.exe
PS C:\Users\tony1>

先將 git-bash 路徑複製下來, 按搜尋鈕輸入 "環境變數" :




新增使用者變數 CLAUDE_CODE_GIT_BASH_PATH :




貼上前面複製的 git-bash 路徑 : 




按確定完成設定 :




關閉 Power Shell 視窗重開, 這樣新增的環境變數才會生效. 以上設定是因為 Claude Code 固定會到環境變數 CLAUDE_CODE_GIT_BASH_PATH 去找 git-bash 路徑之故. 


4. 安裝原生版 Claude Code : 

在重新開啟的 Power Shell 視窗輸入下列指令安裝 Claude Code  :

PS C:\Users\tony1> irm https://claude.ai/install.ps1 | iex   
Setting up Claude Code...

√ Claude Code successfully installed!

  Version: 2.0.65

  Location: C:\Users\tony1\.local\bin\claude.exe


  Next: Run claude --help to get started

‼ Setup notes:
  • Native installation exists but C:\Users\tony1\.local\bin is not in your PATH. Add it by opening: System Properties →
   Environment Variables → Edit User PATH → New → Add the path above. Then restart your terminal.


✅ Installation complete!


這樣便安裝好 Claude Code 了, 在 Setup notes 中提醒要將 Claude Code 的執行檔路徑加入使用者環境變數 Path 中, 這樣以後才能在任何路徑下執行 Claude Code, 設定方式與上面設定環境變數 CLAUDE_CODE_GIT_BASH_PATH 相同, 點選使用者變數 Path 按編輯鈕 :




按新增鈕 : 




貼上 Claude Code 程式路徑 C:\Users\tony1\.local\bin 後按確定即可 :




以上便完成 Claude Code 安裝與設定了.

關閉 Power Shell 視窗重開, 下 claude 指令即可啟動 Claude Code : 

PS C:\Users\tony1> claude 




按上下鍵選擇一個終端機樣式, 直接按 Enter 使用預設 Dark mode 即可. 接下來會進入登入頁面並要求選擇一個付費方案 (Pro/Max), 這部分先 hold 一下, 後續要使用時再來設定. 接下來先用 scoop 安裝其他會用到的軟體. 

2025年12月11日 星期四

在樹莓派 Pi 400 上執行 Streamlit 應用程式

我的 Pi 400 上的共用 Python 虛擬環境 myenv313 已配置完成, 基本上用來測試與佈署機器學習與量化投資 App. 但 Streamlit 不能在此環境中安裝, 因為它依賴的底層 C/C++ 擴充程式會破壞其他套件 (例如 Pandas), 必須在獨立的虛擬環境中安裝. 


1. 建立一個虛擬環境 streamlit_venv 安裝 Streamlit :

pi@raspberrypi:~ $ python -m venv streamlit_venv   
pi@raspberrypi:~ $ source ~/streamlit_venv/bin/activate   
(streamlit_venv) pi@raspberrypi:~ $ pip install streamlit --no-cache-dir   
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
  Downloading cachetools-6.2.2-py3-none-any.whl.metadata (5.6 kB)
Collecting click<9,>=7.0 (from streamlit)
  Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting numpy<3,>=1.23 (from streamlit)
  Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (62 kB)
Collecting packaging>=20 (from streamlit)
  Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting pandas<3,>=1.4.0 (from streamlit)
  Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.metadata (91 kB)
Collecting pillow<13,>=7.1.0 (from streamlit)
  Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (8.8 kB)
Collecting protobuf<7,>=3.20 (from streamlit)
  Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl.metadata (593 bytes)
Collecting pyarrow>=7.0 (from streamlit)
  Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl.metadata (3.2 kB)
Collecting requests<3,>=2.27 (from streamlit)
  Downloading requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting tenacity<10,>=8.1.0 (from streamlit)
  Downloading tenacity-9.1.2-py3-none-any.whl.metadata (1.2 kB)
Collecting toml<2,>=0.10.1 (from streamlit)
  Downloading toml-0.10.2-py2.py3-none-any.whl.metadata (7.1 kB)
Collecting typing-extensions<5,>=4.4.0 (from streamlit)
  Downloading typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
Collecting watchdog<7,>=2.1.5 (from streamlit)
  Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl.metadata (44 kB)
Collecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit)
  Downloading gitpython-3.1.45-py3-none-any.whl.metadata (13 kB)
Collecting pydeck<1,>=0.8.0b4 (from streamlit)
  Downloading pydeck-0.9.1-py2.py3-none-any.whl.metadata (4.1 kB)
Collecting tornado!=6.5.0,<7,>=6.0.3 (from streamlit)
  Downloading tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (2.8 kB)
Collecting jinja2 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting jsonschema>=3.0 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema-4.25.1-py3-none-any.whl.metadata (7.6 kB)
Collecting narwhals>=1.27.1 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading narwhals-2.13.0-py3-none-any.whl.metadata (12 kB)
Collecting gitdb<5,>=4.0.1 (from gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading gitdb-4.0.12-py3-none-any.whl.metadata (1.2 kB)
Collecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading smmap-5.0.2-py3-none-any.whl.metadata (4.3 kB)
Collecting python-dateutil>=2.8.2 (from pandas<3,>=1.4.0->streamlit)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting pytz>=2020.1 (from pandas<3,>=1.4.0->streamlit)
  Downloading pytz-2025.2-py2.py3-none-any.whl.metadata (22 kB)
Collecting tzdata>=2022.7 (from pandas<3,>=1.4.0->streamlit)
  Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB)
Collecting charset_normalizer<4,>=2 (from requests<3,>=2.27->streamlit)
  Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (37 kB)
Collecting idna<4,>=2.5 (from requests<3,>=2.27->streamlit)
  Downloading idna-3.11-py3-none-any.whl.metadata (8.4 kB)
Collecting urllib3<3,>=1.21.1 (from requests<3,>=2.27->streamlit)
  Downloading urllib3-2.6.1-py3-none-any.whl.metadata (6.6 kB)
Collecting certifi>=2017.4.17 (from requests<3,>=2.27->streamlit)
  Downloading certifi-2025.11.12-py3-none-any.whl.metadata (2.5 kB)
Collecting MarkupSafe>=2.0 (from jinja2->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
Collecting attrs>=22.2.0 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading attrs-25.4.0-py3-none-any.whl.metadata (10 kB)
Collecting jsonschema-specifications>=2023.03.6 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB)
Collecting referencing>=0.28.4 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB)
Collecting rpds-py>=0.7.1 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (4.1 kB)
Collecting six>=1.5 (from python-dateutil>=2.8.2->pandas<3,>=1.4.0->streamlit)
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading streamlit-1.52.1-py3-none-any.whl (9.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.0/9.0 MB 4.8 MB/s eta 0:00:00
Downloading altair-6.0.0-py3-none-any.whl (795 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 795.4/795.4 kB 5.4 MB/s eta 0:00:00
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading cachetools-6.2.2-py3-none-any.whl (11 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading gitpython-3.1.45-py3-none-any.whl (208 kB)
Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)
Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (14.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2/14.2 MB 4.4 MB/s eta 0:00:00
Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (11.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.7/11.7 MB 4.9 MB/s eta 0:00:00
Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (6.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 4.0 MB/s eta 0:00:00
Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl (324 kB)
Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 4.4 MB/s eta 0:00:00
Downloading requests-2.32.5-py3-none-any.whl (64 kB)
Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (147 kB)
Downloading idna-3.11-py3-none-any.whl (71 kB)
Downloading smmap-5.0.2-py3-none-any.whl (24 kB)
Downloading tenacity-9.1.2-py3-none-any.whl (28 kB)
Downloading toml-0.10.2-py2.py3-none-any.whl (16 kB)
Downloading tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (445 kB)
Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB)
Downloading urllib3-2.6.1-py3-none-any.whl (131 kB)
Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl (79 kB)
Downloading certifi-2025.11.12-py3-none-any.whl (159 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading jsonschema-4.25.1-py3-none-any.whl (90 kB)
Downloading attrs-25.4.0-py3-none-any.whl (67 kB)
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.0/45.0 MB 4.6 MB/s eta 0:00:00
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kB)
Downloading referencing-0.37.0-py3-none-any.whl (26 kB)
Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (389 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
Installing collected packages: pytz, watchdog, urllib3, tzdata, typing-extensions, tornado, toml, tenacity, smmap, six, rpds-py, pyarrow, protobuf, pillow, packaging, numpy, narwhals, MarkupSafe, idna, click, charset_normalizer, certifi, cachetools, blinker, attrs, requests, referencing, python-dateutil, jinja2, gitdb, pydeck, pandas, jsonschema-specifications, gitpython, jsonschema, altair, streamlit
Successfully installed MarkupSafe-3.0.3 altair-6.0.0 attrs-25.4.0 blinker-1.9.0 cachetools-6.2.2 certifi-2025.11.12 charset_normalizer-3.4.4 click-8.3.1 gitdb-4.0.12 gitpython-3.1.45 idna-3.11 jinja2-3.1.6 jsonschema-4.25.1 jsonschema-specifications-2025.9.1 narwhals-2.13.0 numpy-2.3.5 packaging-25.0 pandas-2.3.3 pillow-12.0.0 protobuf-6.33.2 pyarrow-22.0.0 pydeck-0.9.1 python-dateutil-2.9.0.post0 pytz-2025.2 referencing-0.37.0 requests-2.32.5 rpds-py-0.30.0 six-1.17.0 smmap-5.0.2 streamlit-1.52.1 tenacity-9.1.2 toml-0.10.2 tornado-6.5.3 typing-extensions-4.15.0 tzdata-2025.2 urllib3-2.6.1 watchdog-6.0.0

Pi 400 有 4GB 記憶體, 跑起來就是快. 檢視 Streamlit 版本 :

(streamlit_venv) pi@raspberrypi:~ $ python   
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import streamlit as st   
>>> st.__version__   
'1.52.1'

檢視虛擬目錄下的套件 :

(streamlit_venv) pi@raspberrypi:~ $ pip list  
Package                   Version
------------------------- -----------
altair                    6.0.0
attrs                     25.4.0
blinker                   1.9.0
cachetools                6.2.2
certifi                   2025.11.12
charset-normalizer        3.4.4
click                     8.3.1
gitdb                     4.0.12
GitPython                 3.1.45
idna                      3.11
Jinja2                    3.1.6
jsonschema                4.25.1
jsonschema-specifications 2025.9.1
MarkupSafe                3.0.3
narwhals                  2.13.0
numpy                     2.3.5
packaging                 25.0
pandas                    2.3.3
pillow                    12.0.0
pip                       25.1.1
protobuf                  6.33.2
pyarrow                   22.0.0
pydeck                    0.9.1
python-dateutil           2.9.0.post0
pytz                      2025.2
referencing               0.37.0
requests                  2.32.5
rpds-py                   0.30.0
six                       1.17.0
smmap                     5.0.2
streamlit                 1.52.1
tenacity                  9.1.2
toml                      0.10.2
tornado                   6.5.3
typing_extensions         4.15.0
tzdata                    2025.2
urllib3                   2.6.1
watchdog                  6.0.0

依賴的大套件包括 Numpy, Pandas, 與 Pyarrow. 


2. 本機測試 Streamlit app :

我從王進德老師的 "Raspberry Pi 5 + AI創新實踐 : 電腦視覺與人工智慧應用指南" 書中找到一個範例來測試: 


將此程式取名 interactive_ui.py 存在 streamlit_app 資料夾下 :

# interactive_ui.py 
import streamlit as st
import datetime
from time import sleep

st.title("互動元件")

# 加入單行文字輸入框
st.subheader("text input")
name=st.text_input("Enter you name:")
st.write(f"Hello, {name}")

# 加入多行文字輸入框
message=st.text_area("Enter your message:")
st.write("Your message:")
st.write(message)

# 加入數值輸入框
st.subheader("number input")
age=st.number_input("Enter your age:", min_value=0, max_value=120)
st.write(f"Your age is {age}")

# 加入按鈕
st.subheader("Button")
if st.button('Click Me'):
    st.write("Button clicked!")

# 加入單選鈕
st.subheader("Radio and checkbox")
gender = st.radio("Select your gender:", ["Male", "Female", "Other"], 
horizontal=True)
st.write(f"You selected {gender}")

# 加入複選框
agree= st.checkbox("I agree to the terms and conditions")
if agree:
    st.write("Thank you for agreeing!")

# 加入下拉式選單
st.subheader("select box")
fruit = st.selectbox("Your favorite fruit:", ["Apple", "Banana", "Cherry"])
st.write(f"You selected {fruit}")

# 加入可複選的下拉式選單
options = st.multiselect("Your favorite colors:", ["Red", "Green", "Blue"])
st.write(f"You selected {options}")

# 加入滑桿
st.subheader("slider")
value = st.slider("Select a value:", 0, 100, 50)
st.write(f"Selected value: {value}")

# 加入值範圍滑桿
range_value = st.slider("Select a range of value:", 0, 100, (20, 80))
st.write(f"Selected range: {range_value}")

# 加入日期選擇器
st.subheader("date and time")
date = st.date_input("Select a date:", datetime.datetime.now())
st.write(f"Selected data: {date}")

# 加入時間選擇器
time = st.time_input("Select a time:", datetime.time(12,30))
st.write(f"Selected time: {time}")

# 加入進度條
st.subheader("progress and spinner")
progress_bar=st.progress(0)
for value in range(101):
    progress_bar.progress(value)
    sleep(0.1)
st.write("Done")

# 加入旋轉指示器
with st.spinner("等待中..."):
    sleep(10)
st.success('Done')

Streamlit 程式不可在 Thonny 中直接執行, 必須在終端機用 streamlit run 執行 :

(streamlit_venv) pi@raspberrypi:~/streamlit_app $ streamlit run interactive_ui.py   

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.50.74:8501

它會自動開啟 Chromium 瀏覽器展示結果網頁, 但同時也出現了一個顯示 "開啟你的設定檔時發生錯誤, 部分功能無法使用" 的視窗 : 




問 AI 得知原因很多, 對 Pi 400 最可能的原因是 Chromium 設定檔損毀, 通常是非正常關機或 Chromium 跑一半被 kill 掉造成, 解決辦法很簡單, 就是刪除目前的 Chromium 設定檔, 然後重開瀏覽器即可 :

mv ~/.config/chromium ~/.config/chromium_backup

重新執行上面的 interactive_ui.py 就會開啟 Chromium, 果然就解決了. 

關於 Streamlit 用法參考 :



3. 用 ngrok 讓外部可存取 Streamlit app  :

用 wget 指令下載 64 位元版的 ngrok : 

pi@raspberrypi:~ $ wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm64.tgz   
--2025-12-11 16:15:07--  https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-arm64.tgz
正在查找主機 bin.equinox.io (bin.equinox.io)... 35.71.179.82, 13.248.244.96, 99.83.220.108, ...
正在連接 bin.equinox.io (bin.equinox.io)|35.71.179.82|:443... 連上了。
已送出 HTTP 要求,正在等候回應... 200 OK
長度: 10292787 (9.8M) [application/octet-stream]
儲存到:「ngrok-v3-stable-linux-arm64.tgz」

ngrok-v3-stable-lin 100%[===================>]   9.82M   736KB/s  於 19s       

2025-12-11 16:15:27 (518 KB/s) - 已儲存 「ngrok-v3-stable-linux-arm64.tgz」 [10292787/10292787]

用 tar 解壓縮, 這會得到一個單一的執行檔 ngrok : 

pi@raspberrypi:~ $ tar -xvf ngrok-v3-stable-linux-arm64.tgz   
ngrok  

用 mv 指令將 ngrok 執行檔移動到 PATH 目錄 : 

pi@raspberrypi:~ $ sudo mv ngrok /usr/local/bin/   

檢視 ngrok 版本 : 

pi@raspberrypi:~ $ ngrok --version   
ngrok version 3.34.0

然後註冊 & 登入 ngrok 官網取得 authtoken (認證金鑰), 由於 ngrok 免費帳戶只能用在一個連線, 所以此處我改用 Gmail 註冊了第二個帳戶 (注意, 不是直接用 Gmail 帳戶) :


按 Copy 複製金鑰 : 




加入 ngrok authtoken (認證金鑰) :

pi@raspberrypi:~ $ ngrok config add-authtoken  1n4Ebyjktony1966PyZVSQ1nKz8Mn_tony1966At62W7RGhsx1L
Authtoken saved to configuration file: /home/pi/.config/ngrok/ngrok.yml  

金鑰會存入 .config/ngrok/ngrok.yml 檔案中. 

然後執行下列指令即可得到一個對外得 https 網址 :

ngrok http 本機埠號

pi@raspberrypi:~ $ ngrok http 8501 




可見 ngrok 會將上面的本機區網網址 http://192.168.50.74:8501 映射到公網網址 https://02433506593c.ngrok-free.app/ :




這樣就可以從公網存取這個本機網站了. 

好書 : Raspberry Pi 5 + AI創新實踐 : 電腦視覺與人工智慧應用指南

最近從母校借到王進德老師寫的樹莓派新書 : 


此書詳盡地介紹 Pi 5 從燒錄 OS 到各種視覺應用 (含 Webcam 與 Picamerra, 以及 OpenCV, MediaPipe 等), 最吸引我注意的是書末有 Streamlit + ChatGPT 應用. 

書中範例檔案可在 GitHub 下載 :


修復 Kolin 捕蚊燈

上週因為中華電信要來家裡安裝光世代網路, 我先將客廳預定放置小烏龜的角落清理一番, 順便將鋼琴上故障多時的 Kolin 捕蚊燈拿下來檢修, 原以為只是換根燈管就可以搞定, 沒想到興沖沖地從小北買回一支 T8BL 燈管 ($79 元), 裝上去居然不會亮, 拿回去小北換另一支還是不亮, 研判燈管沒問題, 於是拿起子拆開捕蚊燈, 發現電路板上有一個玻璃殼的元件, 谷歌搜尋找到下面這篇 :


原來這個玻璃元件就是一般日光燈用的啟動器, 我照此文方法, 用鱷魚夾短路此元件, 果然就順利點亮燈管了 : 




於是我又跑了一趟小北買了一個 10W 的啟動器, 用尖嘴鉗拆開蓋子, 裡面果然就是一模一樣的玻璃元件與一個並聯的電容器 : 




電容啟用不到可以剪掉, 然後用烙鐵將捕蚊燈控制板上的舊啟動器, 將新的啟動元件焊上去 (不分極性), 上電測試果然順利點亮捕蚊燈 : 




哈哈, 原本想花千元買過新的, 結果只用了不到百元就搞定了. 

2025年12月10日 星期三

在樹莓派 P3 A+ 虛擬環境下安裝 Streamlit 的空間不足問題

我的樹莓派 Pi 3A+ 主機由於記憶體僅 512MB, 最近將作業系統升級為 Trixie Lite 後因為 Selenium 與 Chronium 都會吃較多記憶體, 使得執行 Selenium 爬蟲程式時常會用光記憶體而導致爬取失敗, 看來只能用在執行靜態網頁爬蟲程式了. 但這樣一來又覺得沒有充分利用到這台主機, 想說 Lite 版適合當小型伺服器, 那就拿來跑 Streamlit 網站應用程式吧! 

首先建立一個虛擬環境 stremlit_venv, 然後在裡面安裝 Streamlit, 但是執行到一半, 到下載相依的 pyarrow 套件時就出現 OSError 28 錯誤而失敗 : 

pi@pi3aplus:~ $ python -m venv streamlit_venv     
pi@pi3aplus:~ $ source ~/streamlit_venv/bin/activate    
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit   
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
... (略) ...
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Using cached pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
ERROR: Could not install packages due to an OSError: [Errno 28] 裝置上已無多餘空間

先用 df -h 檢查 SD 卡空間, 還有超大的 18GB 空間 : 

(streamlit_venv) pi@pi3aplus:~ $ df -h   
檔案系統        容量  已用  可用 已用% 掛載點
udev             74M     0   74M    0% /dev
tmpfs            84M  9.0M   75M   11% /run
/dev/mmcblk0p2   29G   11G   18G   38% /
tmpfs           512M     0  512M    0% /dev/shm
tmpfs           5.0M   12K  5.0M    1% /run/lock
tmpfs           1.0M     0  1.0M    0% /run/credentials/systemd-journald.service
tmpfs           209M  138M   71M   67% /tmp
/dev/mmcblk0p1  510M   66M  445M   13% /boot/firmware
tmpfs           1.0M     0  1.0M    0% /run/credentials/getty@tty1.service
tmpfs            42M   16K   42M    1% /run/user/1000

再用 df -i 檢查 inode 使用情形, 發現 inode 只用了 14%, 顯然不是 inode 不足 :

(streamlit_venv) pi@pi3aplus:~ $ df -i   
檔案系統        Inodes  I已用   I可用 I已用% 掛載點
udev             18807    467   18340     3% /dev
tmpfs            53266    827   52439     2% /run
/dev/mmcblk0p2 1890720 259286 1631434    14% /
tmpfs            53266      1   53265     1% /dev/shm
tmpfs            53266      5   53261     1% /run/lock
tmpfs             1024      1    1023     1% /run/credentials/systemd-journald.service
tmpfs          1048576   1673 1046903     1% /tmp
/dev/mmcblk0p1       0      0       0      - /boot/firmware
tmpfs             1024      1    1023     1% /run/credentials/getty@tty1.service
tmpfs            10653     41   10612     1% /run/user/1000

從 SD 卡還有 18GB, inode 使用率僅 14% 可知問題不是 inode 或 SD 卡容量不足, 而是記憶體的 /tmp 不足所致. 通常 /tmp 預設只切 DRAM (512MB) 的一半約 200MB, 但 Streamlit 的最大依賴套件 pyarrow-22.0.0 檔案高達 45.0 MB, 實際需求空間是檔案大小 2~3 倍, 安裝 意即安裝其 wheel 約需要 120–150 MB 的 /tmp 空間, 但目前 /tmp 只剩 71 MB 所以直接觸發 Errno 28. 

Gemini 的建議是在檔案系統中建立一個資料夾做為儲存 pip 下載安裝所需檔案過程中要用到的暫存空間, 然後將環境變數 TMPDIR 指向此資料夾以取代預設的 /tmp 記憶體, 且在用 pip install 安裝時加上 --no-cache-dir 參數, 安裝完即丟棄檔案 :

(streamlit_venv) pi@pi3aplus:~ $ mkdir ~/pip_tmp   
(streamlit_venv) pi@pi3aplus:~ $ TMPDIR=~/pip_tmp    
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit --no-cache-dir     
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
... () ...
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━ 20.2/45.0 MB 9.0 MB/s eta 0:00:03ERROR: Could not install packages due to an OSError: [Errno 28] 裝置上已無多餘空間

   ━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━ 20.2/45.0 MB 9.0 MB/s eta 0:00:03

雖然設定了 TMPDIR, 但 pip 在下載檔案時仍然會先佔用 DRAM 的 /tmp 而失敗, 我轉而詢問 ChatGPT, 它建議除了 TMPDIR 外, 還有 TMP 與 TEMP 這兩個環境變數也要設定為指向下載暫存資料夾 (此處改用 disk_tmp, 上面那個 pip_tmp 可以刪除) : 

(streamlit_venv) pi@pi3aplus:~ $ mkdir -p ~/disk_tmp  
(streamlit_venv) pi@pi3aplus:~ $ TMPDIR=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ TMP=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ TEMP=~/disk_tmp 
(streamlit_venv) pi@pi3aplus:~ $ pip install streamlit --no-cache-dir 
Collecting streamlit
  Downloading streamlit-1.52.1-py3-none-any.whl.metadata (9.8 kB)
Collecting altair!=5.4.0,!=5.4.1,<7,>=4.0 (from streamlit)
  Downloading altair-6.0.0-py3-none-any.whl.metadata (11 kB)
Collecting blinker<2,>=1.5.0 (from streamlit)
  Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting cachetools<7,>=4.0 (from streamlit)
  Downloading cachetools-6.2.2-py3-none-any.whl.metadata (5.6 kB)
Collecting click<9,>=7.0 (from streamlit)
  Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting numpy<3,>=1.23 (from streamlit)
  Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (62 kB)
Collecting packaging>=20 (from streamlit)
  Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB)
Collecting pandas<3,>=1.4.0 (from streamlit)
  Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl.metadata (91 kB)
Collecting pillow<13,>=7.1.0 (from streamlit)
  Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl.metadata (8.8 kB)
Collecting protobuf<7,>=3.20 (from streamlit)
  Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl.metadata (593 bytes)
Collecting pyarrow>=7.0 (from streamlit)
  Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl.metadata (3.2 kB)
Collecting requests<3,>=2.27 (from streamlit)
  Downloading requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting tenacity<10,>=8.1.0 (from streamlit)
  Downloading tenacity-9.1.2-py3-none-any.whl.metadata (1.2 kB)
Collecting toml<2,>=0.10.1 (from streamlit)
  Downloading toml-0.10.2-py2.py3-none-any.whl.metadata (7.1 kB)
Collecting typing-extensions<5,>=4.4.0 (from streamlit)
  Downloading typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB)
Collecting watchdog<7,>=2.1.5 (from streamlit)
  Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl.metadata (44 kB)
Collecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit)
  Downloading gitpython-3.1.45-py3-none-any.whl.metadata (13 kB)
Collecting pydeck<1,>=0.8.0b4 (from streamlit)
  Downloading pydeck-0.9.1-py2.py3-none-any.whl.metadata (4.1 kB)
Collecting tornado!=6.5.0,<7,>=6.0.3 (from streamlit)
  Downloading tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (2.8 kB)
Collecting jinja2 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting jsonschema>=3.0 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema-4.25.1-py3-none-any.whl.metadata (7.6 kB)
Collecting narwhals>=1.27.1 (from altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading narwhals-2.13.0-py3-none-any.whl.metadata (12 kB)
Collecting gitdb<5,>=4.0.1 (from gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading gitdb-4.0.12-py3-none-any.whl.metadata (1.2 kB)
Collecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython!=3.1.19,<4,>=3.0.7->streamlit)
  Downloading smmap-5.0.2-py3-none-any.whl.metadata (4.3 kB)
Collecting python-dateutil>=2.8.2 (from pandas<3,>=1.4.0->streamlit)
  Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl.metadata (8.4 kB)
Collecting pytz>=2020.1 (from pandas<3,>=1.4.0->streamlit)
  Downloading pytz-2025.2-py2.py3-none-any.whl.metadata (22 kB)
Collecting tzdata>=2022.7 (from pandas<3,>=1.4.0->streamlit)
  Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB)
Collecting charset_normalizer<4,>=2 (from requests<3,>=2.27->streamlit)
  Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (37 kB)
Collecting idna<4,>=2.5 (from requests<3,>=2.27->streamlit)
  Downloading idna-3.11-py3-none-any.whl.metadata (8.4 kB)
Collecting urllib3<3,>=1.21.1 (from requests<3,>=2.27->streamlit)
  Downloading urllib3-2.6.1-py3-none-any.whl.metadata (6.6 kB)
Collecting certifi>=2017.4.17 (from requests<3,>=2.27->streamlit)
  Downloading certifi-2025.11.12-py3-none-any.whl.metadata (2.5 kB)
Collecting MarkupSafe>=2.0 (from jinja2->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
Collecting attrs>=22.2.0 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading attrs-25.4.0-py3-none-any.whl.metadata (10 kB)
Collecting jsonschema-specifications>=2023.03.6 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl.metadata (2.9 kB)
Collecting referencing>=0.28.4 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading referencing-0.37.0-py3-none-any.whl.metadata (2.8 kB)
Collecting rpds-py>=0.7.1 (from jsonschema>=3.0->altair!=5.4.0,!=5.4.1,<7,>=4.0->streamlit)
  Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.metadata (4.1 kB)
Collecting six>=1.5 (from python-dateutil>=2.8.2->pandas<3,>=1.4.0->streamlit)
  Downloading six-1.17.0-py2.py3-none-any.whl.metadata (1.7 kB)
Downloading streamlit-1.52.1-py3-none-any.whl (9.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.0/9.0 MB 9.4 MB/s eta 0:00:00
Downloading altair-6.0.0-py3-none-any.whl (795 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 795.4/795.4 kB 11.4 MB/s eta 0:00:00
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading cachetools-6.2.2-py3-none-any.whl (11 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading gitpython-3.1.45-py3-none-any.whl (208 kB)
Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)
Downloading numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (14.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2/14.2 MB 9.6 MB/s eta 0:00:00
Downloading pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl (11.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.7/11.7 MB 7.2 MB/s eta 0:00:00
Downloading pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (6.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 10.1 MB/s eta 0:00:00
Downloading protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl (324 kB)
Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 10.1 MB/s eta 0:00:00
Downloading requests-2.32.5-py3-none-any.whl (64 kB)
Downloading charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (147 kB)
Downloading idna-3.11-py3-none-any.whl (71 kB)
Downloading smmap-5.0.2-py3-none-any.whl (24 kB)
Downloading tenacity-9.1.2-py3-none-any.whl (28 kB)
Downloading toml-0.10.2-py2.py3-none-any.whl (16 kB)
Downloading tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (444 kB)
Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB)
Downloading urllib3-2.6.1-py3-none-any.whl (131 kB)
Downloading watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl (79 kB)
Downloading certifi-2025.11.12-py3-none-any.whl (159 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading jsonschema-4.25.1-py3-none-any.whl (90 kB)
Downloading attrs-25.4.0-py3-none-any.whl (67 kB)
Downloading jsonschema_specifications-2025.9.1-py3-none-any.whl (18 kB)
Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
Downloading narwhals-2.13.0-py3-none-any.whl (426 kB)
Downloading packaging-25.0-py3-none-any.whl (66 kB)
Downloading pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl (45.0 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.0/45.0 MB 9.5 MB/s eta 0:00:00
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kB)
Downloading referencing-0.37.0-py3-none-any.whl (26 kB)
Downloading rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (389 kB)
Downloading six-1.17.0-py2.py3-none-any.whl (11 kB)
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
Installing collected packages: pytz, watchdog, urllib3, tzdata, typing-extensions, tornado, toml, tenacity, smmap, six, rpds-py, pyarrow, protobuf, pillow, packaging, numpy, narwhals, MarkupSafe, idna, click, charset_normalizer, certifi, cachetools, blinker, attrs, requests, referencing, python-dateutil, jinja2, gitdb, pydeck, pandas, jsonschema-specifications, gitpython, jsonschema, altair, streamlit
Successfully installed MarkupSafe-3.0.3 altair-6.0.0 attrs-25.4.0 blinker-1.9.0 cachetools-6.2.2 certifi-2025.11.12 charset_normalizer-3.4.4 click-8.3.1 gitdb-4.0.12 gitpython-3.1.45 idna-3.11 jinja2-3.1.6 jsonschema-4.25.1 jsonschema-specifications-2025.9.1 narwhals-2.13.0 numpy-2.3.5 packaging-25.0 pandas-2.3.3 pillow-12.0.0 protobuf-6.33.2 pyarrow-22.0.0 pydeck-0.9.1 python-dateutil-2.9.0.post0 pytz-2025.2 referencing-0.37.0 requests-2.32.5 rpds-py-0.30.0 six-1.17.0 smmap-5.0.2 streamlit-1.52.1 tenacity-9.1.2 toml-0.10.2 tornado-6.5.2 typing-extensions-4.15.0 tzdata-2025.2 urllib3-2.6.1 watchdog-6.0.0
(streamlit_venv) pi@pi3aplus:~ $

哈哈! 果然成功了! 

原來使用 pip install 套件時並非只有 pip 在工作, 雖然 pip 會遵守規範將下載之檔案暫存在 TMPDIR, 但第三方依賴套件例如 pyarrow 或 C++ 擴展程式可能會用到編譯器或建構腳本, 但依賴套件的編譯器或腳本不一定會遵循, 它們可能使用 TMP 或 TEMP 環境變數, 當這些依賴程式找不到 TMP 或 TEMP 就會 fallback 到 DRAM 的 /tmp, 結果就可能因空間不足而失敗, 所以最妥當的方法就是將 TMP 與 TEMP 與 TMPDIR 一樣指向暫存資料夾, 這樣就幾乎把大部分跑回 /tmp 的可能性都堵死. 這個設定其實就是欺騙 pip, 讓它誤以為擁有 18GB 的可用暫存空間. 

測試是否可載入 streamlit : 

(streamlit_venv) pi@pi3aplus:~ $ python  
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import streamlit   
>>> streamlit.__version__   
'1.52.1'

但上面的設定只是暫時的, 當終端機關閉後就消失了. 對於 Pi 3A+ 這種只有 512MB 記憶體的主機而言, 以後每次安裝大一點的套件還是會遇到同樣問題. 如果要徹底解決, 就必須修改 Shell 啟動檔案 ~/.bashrc, 用 export 指令設定這三個環境變數指向暫存資料夾 ~/disk_tmp :

用 nano 開啟Shell 啟動檔 ~/.bashrc : 

pi@pi3aplus:~ $ nano ~/.bashrc    

把下列三個 export 指令寫在檔尾 (較安全) : 

export TMPDIR=~/disk_tmp
export TMP=~/disk_tmp
export TEMP=~/disk_tmp

按 Ctrl+O 存檔後按 Ctrl+X 退出 nano, 用 source 指令叫 Shell 重新讀取啟動檔 :

pi@pi3aplus:~ $ source ~/.bashrc   

用 echo 指令驗證此三個環境變數是否有設定進去 : 

pi@pi3aplus:~ $ echo $TMPDIR  
/home/pi/disk_tmp
pi@pi3aplus:~ $ echo $TMP  
/home/pi/disk_tmp
pi@pi3aplus:~ $ echo $TEMP  
/home/pi/disk_tmp

這樣以後每次開啟終端機進虛擬環境用 pip 安裝套件時就不會再遇到 OSError: [Errno 28] 暫存區記憶體空間不足的錯誤了. 

關於 Python 的 venv 虛擬環境用法

這陣子我手上的樹莓派陸續升版為最新的 Trixie OS, 不允許直接在系統 Python 環境用 pip 安裝第三方套件 (自上一版 Bookworm 開始遵循 PEP 668 規範), 必須建立虛擬環境後在裡面安裝, 所以之後會很常使用 venv 指令. 

最常用的三個 venv 指令如下 (myvenv 是自訂的虛擬環境資料夾名稱) :
  • 建立虛擬環境 : python -m venv myvenv 
  • 進入虛擬環境 : surce myvenv/bin/activate
  • 退出虛擬環境 : deactivate
今天在 "Raspberry Pi 5 + AI創新實踐 : 電腦視覺與人工智慧應用指南" 這本書最後一章看到它在 python -m venv 指令後面使用了我沒看過的 --system-site-packages 參數, 詢問 Gemini 才知道這是為了在虛擬環境中使用系統 Python 中已經安裝的套件 (例如某些大型或難以編譯的套件如 NumPy 或特定的資料庫驅動等), 使用此參數可在虛擬環境中也能使用它們而毋須重新安裝. 但若在虛擬環境中安裝了與系統同名的套件, 則虛擬環境中的版本會優先使用.

如果建立虛擬環境時使用了 --system-site-packages 參數, 則在此虛擬環境下輸入 pip list 命令會輸出兩類套件混在一起顯示 : 
  • 虛擬環境內用 pip install 手動安裝的套件
  • 系統 Python 的所有套件
如果要查看虛擬環境下安裝的套件, 可以用 pip list --local 指令. 

用 --system-site-packages 建立的虛擬環境在用 pip freeze > requirements.txt 指令進行打包或依賴管理時會有不純淨和混亂的問題, pip freeze 只會列出在虛擬環境中手動安裝的套件, 不會將透過 --system-site-packages 繼承的系統級套件包含在 requirements.txt 中, 當將此專案部署到另一個環境 (例如新的伺服器或同事的電腦) 並嘗試使用 pip install -r requirements.txt 安裝依賴時, 專案會缺少所有必要的系統套件而無法運行, 這也是為什麼在絕大多數的專案中並不建議使用這個參數的原因. 

結論 : 不要用 --system-site-packages 參數建立虛擬環境, 這種虛擬環境不純淨與不可移植, 專案無法保證在其他地方運行結果一致. 

venv 的可用參數整理如下表 : 


參數 說明 範例用途
--clear 如果指定的環境目錄已經存在,則在創建新環境之前會**刪除**現有目錄的內容。 當您想徹底重建一個現有的虛擬環境時使用。
--upgrade-deps 確保環境中的基礎依賴,如 **pip**、**setuptools** 和 **wheel**,被升級到最新的版本。 推薦使用,以確保您使用最新的套件管理工具。
-h, --help 顯示 venv 模組的說明訊息和所有可用參數。 用於查找完整的參數列表。
--copies 強制使用 **複製** 而不是符號連結來安裝環境中的檔案。 在某些檔案系統或作業系統上,符號連結可能會有問題時使用。
--system-site-packages 允許虛擬環境訪問系統安裝的套件。 在需要共用大型系統套件時使用。
--symlinks (預設行為)盡可能使用**符號連結**來安裝環境中的檔案,以節省空間。 如果您使用了 --copies,可以用此選項切換回預設行為。
--prompt <PROMPT> 為虛擬環境指定一個**不同的前綴**,該前綴會顯示在終端機提示符號中,而不是使用目錄名稱。 讓您更容易識別當前激活的是哪個環境,例如 --prompt my-project-dev


如果要在建立虛擬環境時 pip 為最新版, 可以加 --upgrade-deps 參數 :

python -m venv --upgrade-deps myvenv 

例如 :

pi@pi3aplus:~ $ python -m venv streamlit_venv  
pi@pi3aplus:~ $ cd streamlit_venv   
pi@pi3aplus:~/streamlit_venv $ ls -ls   
總用量 16
4 drwxrwxr-x 2 pi pi 4096 12月 10 14:47 bin
4 drwxrwxr-x 3 pi pi 4096 12月 10 14:46 include
4 drwxrwxr-x 3 pi pi 4096 12月 10 14:46 lib
0 lrwxrwxrwx 1 pi pi    3 12月 10 14:46 lib64 -> lib
4 -rw-rw-r-- 1 pi pi  161 12月 10 14:46 pyvenv.cfg

啟動與退出虛擬環境的 activate/deactivate 等指令都放在 bin 底下. 

pi@pi3aplus:~ $ source ~/streamlit_venv/bin/activate    
(streamlit_venv) pi@pi3aplus:~ $ pip list   
Package Version
------- -------
pip     25.1.1
(streamlit_venv) pi@pi3aplus:~ $

可見純淨的虛擬環境剛建立時只有 pip 一個套件而已. 

Pi 3 主機升版為 Raspberry Pi OS Trixie (32-bit Desktop)

高雄家的 Pi 3 主機因為社區網路停止服務後停擺, 申裝光世代後又因為桌面異常無法更改網路設定, 只好改燒錄新版 OS Trixie, 這回我改用官方燒錄程式 Imager, 參考 :


開機後檢查 TF 卡檔案系統 :

pi@KAO-Pi3:~ $ df -h   
Filesystem      Size  Used Avail Use% Mounted on
udev            323M     0  323M   0% /dev
tmpfs           185M  9.0M  176M   5% /run
/dev/mmcblk0p2   29G  5.3G   22G  20% /
tmpfs           461M  8.0K  461M   1% /dev/shm
tmpfs           5.0M   12K  5.0M   1% /run/lock
tmpfs           1.0M     0  1.0M   0% /run/credentials/systemd-journald.service
tmpfs           461M  171M  291M  37% /tmp
/dev/mmcblk0p1  510M  106M  405M  21% /boot/firmware
tmpfs           1.0M     0  1.0M   0% /run/credentials/getty@tty1.service
tmpfs           1.0M     0  1.0M   0% /run/credentials/serial-getty@ttyS0.service
tmpfs            93M   64K   93M   1% /run/user/1000

用掉 5.3GB, 還有 22GB 可用. 

首先安裝 VNC 遠端桌面, 參考 :


接著安裝 Anydesk, 因 OS 是 32 位元故要安裝 armhf 版的 anydesk, 只有舊的 6.3.0 版可下載 :




但安裝時出現錯誤 :




詢問 AI 得知 Anydesk 目前僅支援 64 位元 OS (似乎已放棄 32 位元), 所以只好放棄安裝. 

Trixie 內建 Python 3.13 版環境 : 

pi@KAO-Pi3:~ $ python   
Python 3.13.5 (main, Jun 25 2025, 18:55:22) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

然後是建立 Python 共用虛擬環境來安裝常用套件, 參考 : 


注意, 不要在此共用虛擬環境安裝 streamlit, 它依賴的底層 C 程式會破壞 pandas 等套件之運作, 要用單獨的虛擬環境安裝 streamlit. 

pi@KAO-Pi3:~ $ python -m venv myenv313   
pi@KAO-Pi3:~ $ ls -ls   
total 36
4 drwxrwxr-x 2 pi pi 4096 Dec  4 22:56 Desktop
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Documents
4 drwxr-xr-x 2 pi pi 4096 Dec  9 07:05 Downloads
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Music
4 drwxrwxr-x 5 pi pi 4096 Dec  9 10:37 myenv313  
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Pictures
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Public
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Templates
4 drwxr-xr-x 2 pi pi 4096 Dec  4 22:56 Videos

這樣便建立了一個虛擬環境資料夾 myenv313, 用下列指令進入虛擬環境 : 

pi@KAO-Pi3:~ $ source myenv313/bin/activate    
(myenv313) pi@KAO-Pi3:~ $

接下來即可在此虛擬環境用 pip 安裝套件了. 


1. 資料科學基礎套件 : 

pip install numpy==2.2.6
pip install pandas
pip install scipy
pip install matplotlib
pip install seaborn
pip install bokeh
pip install plotly
pip install scikit-learn

註 : 因 pandas-ta 0.4.71b 依賴 numpy 2.2.6, 故先行指定此版本. 


2. 深度學習框架 : 

pip install torch torchvision torchaudio


3. Web UI : 

pip install django
pip install gradio

註 : streamlit 依賴許多 C extension, 與樹莓派 Trixie 的 Python 3.13 部分不相容, 會導致 Python 執行環境崩潰, 勿安裝. 


4. 爬蟲 : 

pip install html5lib
pip install selenium
pip install scrapy


5. 量化投資 : 

pip install yfinance
pip install twstock
pip install mplfinance
pip install ta
pip install kbar
pip install backtrader
pip install pyfolio-reloaded


6. Bot 套件 : 

pip install line-bot-sdk
pip install python-telegram-bot


7. LLM 套件 : 

pip install openai
pip install google-generativeai


momo 買書兩本 (Claude Code 與程式人的量化投資)

前天逛明儀時發現剛上市的一本量化投資好書 :


此書有介紹 backtrader 套件, 本來想買現書回家一賭為快, 但考慮到還有 4000 元 momo 幣, 只好忍住回家上 momo 找, 但它要 12/10 才上架, 所以今天一過 12 點就立馬放入購物車, 連同胡嘉璽老師的 Claude Code 書一起買 :





79 折優惠後 1327 元, 用掉 momo 幣 327 元, 實付 1000 元 (約 6 折). 另外還登記到 100 元 momo 幣與 B 群維他命 :



2025年12月9日 星期二

Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v12 (於 Pi 400)

完成母校圖書館爬蟲程式改版移植到 Pi 3 A+ (Trixie OS) 後, 下午繼續移植市圖爬蟲程式, 主要是必須改用新的 chromium 與 chromium-driver 套件 : 

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

寫法也要用新的方式 :

        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)

參考 :


套件安裝在前一篇均已完成, 直接近虛擬環境, 編輯程式與執行 :

pi@pi3aplus:~ $ source ~/myenv313/bin/activate   
(myenv313) pi@pi3aplus:~ $ nano ksml_lib_12.py   

輸入新版程式 :

# ksml_lib_12.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
import sys

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_books(account, password):
    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.get('https://webpacx.ksml.edu.tw/personal/')
        loginid=browser.find_element(By.ID, 'logxinid')
        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')
        login_btn.click()
        # 擷取借閱紀錄
        div_redblock=browser.find_element(By.CLASS_NAME, 'redblock')
        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()
            book_site=book.find_element(By.XPATH, './ul[3]/li[1]').text
            reg=r'典藏地:(\S+)'
            item['book_site']=re.findall(reg, book_site)[0]
            reg=r'\d{4}-\d{2}-\d{2}'
            due_date=book.find_element(By.XPATH, './ul[4]/li[2]').text
            item['due_date']=re.findall(reg, 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]
            try: 
                state=book.find_element(By.XPATH, './ul[6]/li[1]').text
            except:
                state=''
            finally:
                if '有人預約' in state:
                    item['state']=', 有人預約'
                else:
                    item['state']=''
            borrow_books.append(item)
        print('擷取借閱紀錄 ... OK')
        browser.back() # 回上一頁
        # 擷取預約紀錄
        div_blueblock=browser.find_element(By.CLASS_NAME, 'blueblock')
        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
            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'
            else: # 預約中
                item['ready_for_pickup']=False
                item['expiration']=''
                item['sequence']=re.findall(r'\d+', sequence)[0]
            reserve_books.append(item)
        browser.close()
        print('擷取預約紀錄 ... OK')
        return (borrow_books, reserve_books)        
    except Exception as e:
        print(e)
        return None, None
    
if __name__ == '__main__':
    start=time.time()
    token='我的 Telgram 權杖'
    chat_id='聊天室 ID'
    if len(sys.argv) != 3:
        print('用法: python3 ksml_personal_12.py 帳號 密碼')
        sys.exit(1)
    # 取得傳入的帳密參數
    account=sys.argv[1]
    password=sys.argv[2]
    # 呼叫 get_books() 取得借書與預約書        
    borrow_books, reserve_books=get_books(account, password)
    b_msg=''  # 借書資訊字串初始值
    r_msg=''  # 預約資訊字串初始值
    # 處理借書 
    if borrow_books: 
        borrow=[]
        for book in borrow_books:
            book_name=book['book_name']
            book_site=book['book_site'] 
            due_times=book['due_times']
            due_date=book['due_date']
            state=book['state']
            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)} 天{state}, {book_site})'
                borrow.append(msg)
            elif delta == 0:  # 0=今天到期
                msg=f'⓿ {book_name} (今日到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif delta == 1:  # 1=明天到期 
                msg=f'❶ {book_name} (明日到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif delta == 2:  # 2=後天到期 
                msg=f'❷ {book_name} (後天到期, 續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
            elif 2 < delta < 8:  # 3 天以上一周內到期
                msg=f'✦ {book_name} ({book["due_date"]} 到期, '\
                    f'續借次數 {due_times}{state}, {book_site})'
                borrow.append(msg)
        # 製作借書到期摘要字串 
        if len(borrow) != 0:
            borrow.insert(0, f'\n❖ {account} 的借閱 :')
            b_msg='\n'.join(borrow)  # 更新借書資訊字串
        print('產生借書到期摘要 ... OK')
    # 處理預約書
    if reserve_books:
        reserve=[]
        i=0
        j=['①', '②', '③', '④', '⑤']
        k=['❶', '❷', '❸', '❹', '❺']
        # 預約狀態
        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
        # 製作預約書摘要字串    
        if len(reserve) != 0:
            reserve.insert(0, f'\n❖ {account} 的預約 :')
            r_msg='\n'.join(reserve)  # 更新資訊字串
    print('產生預約書摘要 ... OK')
    if b_msg or r_msg:  # 任一不為空字串就更新資料表
        url="https://serverless-fdof.onrender.com/function/update_ksml_books"
        payload={
            "account": account,
            "borrow_books": b_msg,   
            "reserve_books": r_msg
            }
        res=requests.post(url, json=payload)
        print(res.json())        
    end=time.time()
    print(f'執行時間:{end-start}')        

此處爬蟲結果放在字典中向佈署於 render.com 的 serverless 平台上的 update_ksml_books 函式提出 POST 請求, 它會將爬蟲訊息儲存在該平台的 SQLite 資料庫 serverless.db 上 :

# update_ksml_books.py
import sqlite3
from datetime import datetime, timedelta

def main(request, **kwargs):
    """
    POST 請求範例:
    {
        "account": "tony",
        "borrow_books": "書名1 (到期日 2025-10-22); 書名2 ...",
        "reserve_books": "書名A (已到館); 書名B (順位 2) ..."
    }
    """
    DB_PATH='./serverless.db'
    try:  # 從 POST 請求 body 中解析 JSON 格式資料並轉成 Python 字典
        data=request.get_json(force=True)
    except Exception as e:
        return {"status": "error", "message": f"解析 JSON 失敗: {str(e)}"}
    # 從字典中取得參數值
    account=data.get('account')
    borrow_books=data.get('borrow_books', '')
    reserve_books=data.get('reserve_books', '')
    # 檢查主鍵 account 
    if not account:
        return {"status": "error", "message": "缺少帳號資訊"}
    try:  # 更新 ksml_books 資料表
        conn=sqlite3.connect(DB_PATH)
        cur=conn.cursor()
        # 建立 ksml_books 資料表 (若不存在)
        cur.execute("""
            CREATE TABLE IF NOT EXISTS ksml_books (
                account TEXT PRIMARY KEY,
                borrow_books TEXT,
                reserve_books TEXT,
                updated_at TEXT
            )
        """)
        # 統一用 UTC 現在時間 + 8 取得台灣目前時間
        utc_now=datetime.utcnow()
        taiwan_now=utc_now + timedelta(hours=8)
        now_str=taiwan_now.strftime('%Y-%m-%d %H:%M:%S')
        # 使用 INSERT OR REPLACE 寫入紀錄,如果帳號已存在就更新
        cur.execute("""
            INSERT OR REPLACE INTO ksml_books (account, borrow_books, reserve_books, updated_at)
            VALUES (?, ?, ?, ?)
        """, (account, borrow_books, reserve_books, now_str))
        conn.commit()
        conn.close()
        return {"status": "success", "message": f"{account} 的資料已更新"}
    except Exception as e:
        return {"status": "error", "message": str(e)}

執行結果 :

(myenv313) pi@pi3aplus:~ $ python ksml_lib_12.py xxxx118 xxxx27   
擷取借閱紀錄 ... OK
擷取預約紀錄 ... OK
產生借書到期摘要 ... OK
產生預約書摘要 ... OK
{'message': 'xxxx118 的資料已更新', 'status': 'success'}
執行時間:348.5533776283264

但我設定 crontab 去跑爬蟲無結果, 手動測試發現不穩定, 常會出現錯誤 : 

pi@pi3aplus:~ $ /home/pi/myenv313/bin/python /home/pi/ksml_lib_12.py xxxx119 xxxx16
Message: session not created
from chrome not reachable; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#sessionnotcreatedexception
Stacktrace:
#0 0x005593e0d070 <unknown>
#1 0x0055938df220 <unknown>
#2 0x0055938cf970 <unknown>
#3 0x00559391894c <unknown>
#4 0x005593915500 <unknown>
#5 0x005593911090 <unknown>
#6 0x005593952ee4 <unknown>
#7 0x00559395286c <unknown>
#8 0x00559391c1f0 <unknown>
#9 0x005593dd8c2c <unknown>
#10 0x005593ddbc84 <unknown>
#11 0x005593ddb878 <unknown>
#12 0x005593dc40a0 <unknown>
#13 0x005593ddc2e0 <unknown>
#14 0x005593daf2a0 <unknown>
#15 0x005593df9fc0 <unknown>
#16 0x005593dfa1b0 <unknown>
#17 0x005593e0bd24 <unknown>
#18 0x007fa1115f74 <unknown>
#19 0x007fa117de88 <unknown>

產生預約書摘要 ... OK
執行時間:94.88470888137817

問 AI 好像是新版 Chromium 更吃記憶體, 新版 Selenium 對記憶體要求也較高, 即使是用 headless 也是容易讓 Chrome 物件建不起來. 

我依照 AI 建議將 swap 記憶體加大為 2GB, shm 從 64MB 擴大為 512MB 也沒用 :

pi@pi3aplus:~ $ swapon --show  
NAME       TYPE      SIZE   USED PRIO
/dev/zram0 partition 416M 352.1M  100
pi@pi3aplus:~ $ sudo swapoff -a
強制結束
pi@pi3aplus:~ $ sudo fallocate -l 2G /swapfile   
pi@pi3aplus:~ $ sudo chmod 600 /swapfile   
pi@pi3aplus:~ $ sudo mkswap /swapfile   
Setting up swapspace version 1, size = 2 GiB (2147479552 bytes)
no label, UUID=c0c0c57c-3115-418c-9c24-b3571a36a773
pi@pi3aplus:~ $ sudo swapon /swapfile 
pi@pi3aplus:~ $ grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab/swapfile none swap sw 0 0
pi@pi3aplus:~ $ sudo mount -o remount,size=512M /dev/shm   
mount: (hint) your fstab has been modified, but systemd still uses
       the old version; use 'systemctl daemon-reload' to reload.
pi@pi3aplus:~ $ free -h   
               total        used        free      shared  buff/cache   available
Mem:           416Mi       269Mi        83Mi        24Mi       138Mi       146Mi
Swap:          2.4Gi       333Mi       2.1Gi
pi@pi3aplus:~ $ df -h /dev/shm   
檔案系統        容量  已用  可用 已用% 掛載點
tmpfs           512M     0  512M    0% /dev/shm
pi@pi3aplus:~ $ swapon --show  
NAME       TYPE      SIZE   USED PRIO
/dev/zram0 partition 416M 353.5M  100
/swapfile  file        2G     0B   -2

但同樣程式在 Pi 400 卻可順利運行 (而且速度也快很多) :

(myenv313) pi@raspberrypi:~ $ python ksml_lib_12.py xxxx119 xxxx16  
擷取借閱紀錄 ... OK
擷取預約紀錄 ... OK
產生借書到期摘要 ... OK
產生預約書摘要 ... OK
{'message': 'xxxx119 的資料已更新', 'status': 'success'}
執行時間:20.821362495422363

所以 Pi 3 要嘛退回 Buster, 要嘛維持目前 Trixie 但只做 Selenium 以外的爬蟲用途了. 

在 serverless 平台上還有一個程式 send_ksml_books_messages.py 負責取出 serverless.db 的 ksml_books 資料表全部內容取出傳送到 Telegram, 程式如下 : 

# send_ksml_books_messages.py
import asyncio
import sqlite3
from telegram import Bot

async def telegram_send_text(token, chat_id, text):
    """非同步傳送 Telegram 訊息"""
    try:
        bot=Bot(token=token)
        await bot.send_message(chat_id=chat_id, text=text)
        return True
    except Exception as e:
        print(f"傳送失敗: {e}")
        return False

def main(request, **kwargs):
    DB_PATH='./serverless.db'
    config=kwargs.get('config', {})
    telegram_token=config.get('TELEGRAM_TOKEN')
    telegram_id=config.get('TELEGRAM_ID')
    if not telegram_token or not telegram_id:
        return '未設定 TELEGRAM_TOKEN 或 TELEGRAM_ID'
    try:  # 連線資料庫
        conn=sqlite3.connect(DB_PATH)
        cur=conn.cursor()
        cur.execute("SELECT borrow_books, reserve_books FROM ksml_books;")
        rows=cur.fetchall()
        conn.close()
    except Exception as e:
        return f'資料庫讀取失敗: {e}'
    if not rows:
        return '沒有任何資料可傳送'
    # 傳送訊息
    success_count=0
    fail_count=0
    for borrow_books, reserve_books in rows:
        for msg in [borrow_books, reserve_books]:
            if msg and msg.strip():
                ok=asyncio.run(telegram_send_text(telegram_token, telegram_id, msg))
                if ok:
                    success_count += 1
                else:
                    fail_count += 1
    return f'傳送完成:成功 {success_count} 筆,失敗 {fail_count} 筆'

只要在本地樹莓派 crontab 定期向 serverless 的 send_ksml_books_messages.py 提出一個 GET 請求就會觸發它送出 Telegram 訊息了, 例如下面這個在 Pi 3 A+ 上的  get_ksml_books_messages.py :

#  get_ksml_books_messages.py
import requests

url="https://serverless-fdof.onrender.com/function/send_ksml_books_messages"
res=requests.get(url)
print(res)

這模式雖然較複雜, 但未來在製作 Telegram 聊天機器人時會較方便, 參考 :