顯示具有 水井三寶 標籤的文章。 顯示所有文章
顯示具有 水井三寶 標籤的文章。 顯示所有文章

2026年9月28日 星期一

從程式基礎到智慧應用:Python 程式實習課程

從程式基礎到智慧應用:Python 程式實習課程

Python 程式實習以「看懂問題、設計流程、撰寫程式、測試除錯、整合應用」為學習主線。學生從變數、條件判斷、迴圈與資料結構開始,逐步運用函式、模組、檔案與資料處理,再把程式連接到 Raspberry Pi、Hub 8735 Ultra 或既有雲端應用,完成能說明、能測試、能展示的實作成果。

一、Python 程式實習學習架構

課程重點在 Python 程式設計能力。硬體與雲端服務是學生應用程式的情境:先學會如何表達資料、控制程式流程及拆解任務,再運用 Python 與裝置或服務交換資料。


Python 程式實習學習架構:由程式基本功出發,逐步完成裝置串接與整合專題。

二、從基礎語法建立程式思考

初學程式時,先練習將生活中的需求拆成明確步驟,再把步驟寫成 Python 指令。變數與資料型別用來保存資訊,輸入與輸出讓程式能與使用者互動,運算式則負責計算及比較。

學習內容程式觀念練習例子
變數與資料型別整數、浮點數、字串、布林值;為資料選擇合適型別。保存感測數值、商品名稱或是否需要提醒等資訊。
輸入、輸出與運算讀取輸入、顯示結果、使用運算式處理數值。輸入資料後,計算平均值或產生簡短摘要。
條件判斷使用 if、elif、else 依條件選擇程式分支。依照設定條件顯示不同狀態或建議。
重複執行使用 for 或 while 處理重複任務。逐筆整理資料、重複讀取輸入或持續檢查狀態。

三、使用資料結構整理資訊

實際應用通常需要處理一批資料。學生可以運用字串處理文字,使用串列保存多筆項目,使用字典描述具有欄位名稱的資料,並透過檔案讀寫保存程式輸入或輸出。

# 用字典整理一筆場域資料
reading = {
    "site": "水井村",
    "temperature": 27.5,
    "status": "正常"
}

print(reading["site"], reading["temperature"])

教師可提供去識別化或模擬資料,讓學生練習查找、分類、統計與轉換。透過清楚的資料結構,程式更容易閱讀、測試,也更方便後續串接服務。

四、以函式、模組與除錯提升程式品質

當程式逐漸變長,學生需要學習把重複工作整理成函式,使用參數傳入資料、以回傳值交付結果,並透過模組重用已寫好的功能。例外處理則協助程式面對輸入錯誤、檔案不存在或服務暫時無法連線等情況。

  • 函式:把一項清楚的工作包裝起來,讓主程式更容易理解。
  • 模組:匯入標準函式庫或課程提供的程式模組,擴充功能。
  • 例外處理:辨識可能發生的錯誤,提供適當訊息或後續處理。
  • 測試與除錯:以小資料測試功能,閱讀錯誤訊息,逐步定位問題。

五、讓 Python 處理資料並連接服務

掌握程式基本能力後,學生可練習讀取教師提供或允許使用的資料,進行清理、轉換及摘要。進一步可認識 JSON 等常見資料格式,以及 HTTP/API 請求的基本概念,理解程式如何向服務提出請求並處理回應。課程實作應使用已核准的測試服務與介面,並遵守資料來源、授權與個資保護規範。

應用階段Python 任務學習重點
資料蒐集讀取輸入資料、檔案或授權的服務回應。辨識資料來源與欄位。
資料處理整理文字及數值、篩選紀錄、計算統計或產生摘要。資料型別、串列、字典、函式與程式流程。
資料交換依介面規格將資料轉為 JSON,處理服務回傳結果。資料格式、請求與回應、錯誤處理。
結果呈現顯示狀態、產生簡易報表或提供查詢結果。將程式輸出連結到使用者需求。

六、從 PC 延伸到 Raspberry Pi 與場域應用

PC 適合撰寫及測試 Python 程式;Raspberry Pi 可用於裝置端實作。課程可先在 PC 完成資料處理,再依設備介面與課程環境,透過 USB 進行 PC 與 Pi 的資料交換,或使用 Wi-Fi 連接 Hub 8735 Ultra/雲端應用。

若在 Raspberry Pi 上使用 MicroPython,學生可比較它與一般 Python 的使用情境。兩者語法有相通之處,但可用的函式庫、硬體介面與執行環境可能不同,因此程式要依實際裝置及課程提供的範例調整。USB 資料交換也須依設備採用的介面與驅動設定操作。

理解問題→設計流程→撰寫 Python→測試除錯→整合應用

七、以智慧生活情境發展專題

學生可從既有系統選擇一種應用情境,聚焦完成一項範圍適中的程式任務。系統串接依已開放的操作介面及資料權限進行;若無法直接連接正式服務,也可使用教師提供的模擬資料完成相同程式概念練習。

應用情境Python 專題方向
智慧養殖:水井村 USR整理模擬或授權的養殖資料,計算摘要、顯示狀態或產生提醒。
智慧工藝:水井三寶建立工藝資料清單,實作分類、查詢或互動介紹。
智慧商店:雲林國際慢食協會整理商品資訊,製作搜尋、分類或簡易商品問答原型。
智慧學習:元長樂齡中心製作簡易問答或互動練習,呈現適合學習者使用的內容。

八、建議的學習成果與評量方式

期末可採個人或小組專題,讓學生完成一個能清楚展示的 Python 應用原型。評量除了結果,也可檢視學生如何拆解問題、安排程式結構、處理例外及驗證輸出。

  • Python 程式碼及簡要流程圖。
  • 測試資料、測試結果與除錯紀錄。
  • PC 與 Raspberry Pi 的資料交換或服務串接展示。
  • 一份專題說明,交代使用情境、資料來源、程式功能與限制。

建議評量面向:程式概念正確性、程式可讀性、測試與除錯歷程、資料使用合宜性,以及應用成果是否回應所選情境。

結語:讓程式成為解決問題的工具

Python 程式實習的學習成果,不只是記住語法,而是能把需求轉成清楚的程式流程,選擇合適的資料結構,運用函式整理功能,並透過測試與除錯逐步完成作品。當程式再與 Raspberry Pi、Hub 8735 Ultra 或雲端服務連結,學生便能把程式能力應用到智慧養殖、智慧工藝、智慧商店與智慧學習等真實情境。

教學支援:實作題目、設備介面與場域案例可透過智慧生活科技專業社群交流,並依教室設備及既有系統開放的介面調整。

#Python程式設計 #程式實習 #RaspberryPi #MicroPython #資料處理 #智慧生活

從一台電腦走進網際網路:網際網路應用課程的知識與實務

從一台電腦走進網際網路:網際網路應用課程的知識與實務

網際網路每天都在傳送訊息、影像與感測資料。學生如何看見資料離開電腦後,經過哪些網路設備與服務,最後抵達雲端應用?本課程以 PC+Raspberry Pi+Hub 8735 Ultra/雲端應用為實作平台,帶學生從網路設定、封包傳輸與連線診斷,逐步走到智慧生活場域應用。

一、課程平台:讓資料傳輸看得見

學生使用 PC 與 Raspberry Pi 練習網路與裝置通訊;再透過 Wi-Fi,將資料送往 Hub 8735 Ultra 或已建置完成的雲端應用。課程的核心不是只操作設備,而是理解資料如何形成、如何傳遞,以及服務端如何回應。



網際網路應用課程學習架構:從網路設定與封包傳送,延伸到雲端服務與智慧場域。

圖中的大型網際網路雲端代表課程的學習核心。學生從 PC 端操作網路診斷工具,透過 USB 與 Raspberry Pi 交換資料,再讓 Pi 經 Wi-Fi 進入網路,最後連接 Hub 或雲端服務。箭頭分別表示資料傳輸,以及 DNS 查詢、API 請求等服務互動。

二、學生可以學到哪些網際網路知識?

學習主題重要概念實作練習
網路連線與架構用戶端、伺服器、區域網路、網際網路、閘道器與雲端服務的關係。繪製 PC、Raspberry Pi、Wi-Fi 路由器、Hub/雲端服務的連線圖。
IP 位址與 DNSIP 位址用來辨識網路端點;DNS 將網域名稱解析為 IP 位址。使用 ipconfig 檢視網路設定,使用 nslookup 查詢網域名稱。
封包與路由資料會切分成封包,經過不同網路節點轉送到目的地;路徑與延遲可能改變。使用 ping 測試連線,使用 tracert 觀察路由節點與回應時間。
TCP 與 UDP兩種常見傳輸層協定,各有不同的傳輸特性與適用情境。在 PC 與 Raspberry Pi 間傳送文字、指令或狀態資料,觀察傳輸與回覆。
Wi-Fi 與物聯網無線網路讓裝置連入區域網路,並透過閘道器連接外部服務。設定 Raspberry Pi 網路連線,確認裝置可到達指定服務。
雲端服務與 API服務端接收請求、處理資料並回傳結果;API 是應用程式與服務交換資料的介面。依課程提供的介面或測試環境,送出資料並檢查回應與錯誤訊息。
網路資料應用網頁與服務提供可整理的資料,但擷取及使用必須遵守來源規範與授權。使用教師提供或允許使用的資料,練習爬蟲、整理資料與簡單查詢。

三、從工具操作到網路診斷

網路診斷工具可以讓抽象的網路概念變成可觀察的結果。學生先查看本機網路設定,再測試連線、追蹤路徑及查詢網域名稱,練習根據輸出推測問題可能出現在電腦設定、DNS、路由或遠端服務。

  • ipconfig:檢視本機 IP、子網路遮罩、預設閘道與 DNS 等資訊。
  • ping:測試目的端是否可回應,觀察往返時間與封包遺失情形。
  • tracert:查看封包到目的端途中經過的路由節點。
  • nslookup:向 DNS 查詢網域名稱對應的網路位址。

實作時可安排「連不上服務」的診斷任務:學生先整理現象,再選擇適當指令測試,最後以證據說明問題與處理方式。這能建立有步驟的除錯習慣,而不是只反覆重開設備。

四、實作資料如何從 PC 抵達雲端?

課程可依下列流程安排,讓學生逐段完成並驗證:

① 設定網路→② 傳送資料→③ 診斷路徑→④ 串接服務→⑤ 完成應用
  1. 認識 PC 的網路身分:取得 IP、閘道與 DNS 資訊,畫出目前設備所在的網路環境。
  2. 建立 PC 與 Raspberry Pi 資料交換:依教室設備介面,以 USB 連線傳送指令與回覆。實際使用的連線方式、驅動程式與介面設定,依課堂設備配置進行。
  3. 測試網路與路由:使用診斷工具檢查端點可達性,並追蹤資料前往目標服務的路徑。
  4. 讓 Raspberry Pi 透過 Wi-Fi 連線:確認裝置已連上指定網路,並能與目標端點交換資料。
  5. 連接 Hub 或雲端應用:依教師提供的服務介面或操作方式送出資料、查看回應,完成小型應用展示。

五、連結已建置完成的智慧生活場域

課程可使用已完成的系統作為學習情境,讓學生練習思考網路技術如何支持實際服務。依系統開放的介面與資料權限,教師可安排觀察、模擬資料傳送或小型串接專題。

應用場域可討論的網路應用問題
智慧養殖:水井村 USR感測資料如何由場域端送出?遠端使用者如何取得環境資訊?
智慧工藝:水井三寶如何透過網路呈現工藝內容、導覽資訊或互動服務?
智慧商店:雲林國際慢食協會商品與店家資訊如何整理、查詢並提供給使用者?
智慧學習:元長樂齡中心如何透過網路支援學習內容、互動活動或服務資訊?

進階活動可以加入爬蟲或 LLM Agent:先以授權資料建立可查詢的內容,再設計能回答場域問題的資訊小幫手。學生同時練習檢查資料來源、辨識回答依據及保護個人資料。

六、建議的學生學習成果

期末可採分組實作,每組選擇一種智慧生活情境,完成下列成果:

  • 一張清楚標示 PC、Raspberry Pi、網路設備、Hub/雲端服務的系統架構圖。
  • 一份網路設定及連線診斷紀錄,說明使用哪些工具、觀察到什麼結果。
  • 一段可展示的裝置資料交換或服務請求流程。
  • 一個連結場域需求的應用原型,並說明資料來源、傳輸路徑與服務回應。

評量可著重概念理解、實作正確性、診斷過程與成果說明。學生不只展示「程式有跑」,也要能說明資料從哪裡來、經過哪些網路環節,以及遇到問題時如何定位原因。

結語:讀懂網路中的資料旅程

透過 PC、Raspberry Pi、Hub 8735 Ultra 與既有雲端應用,學生能把 IP、DNS、TCP/UDP、封包路由、Wi-Fi 與雲端服務連成一條可實作的學習路徑。從設定網路、傳送資料、診斷連線,到完成智慧場域應用,網際網路不再只是課本上的名詞,而是可以觀察、測試與應用的技術。

教學支援:課程技術與場域實作可透過智慧生活科技專業社群交流,並依 Hub 8735 Ultra、雲端服務及教室設備實際開放的介面安排操作。

#網際網路應用 #TCP #UDP #RaspberryPi #網路診斷 #物聯網 #智慧生活

2026年9月4日 星期五

雲端網路的核心概念:從智慧養殖到 AI 互動導覽

雲端網路的核心概念:從智慧養殖到 AI 互動導覽

當我們使用手機看影片、上傳照片、網路購物或遠端控制家電時,背後都離不開「雲端網路」。 雲端網路不只是把資料放到遠端伺服器,更是透過網路把人、設備、資料、服務與人工智慧連結起來, 讓系統能夠即時互動、彈性擴充與集中管理。

一、什麼是雲端網路?

雲端網路是指透過網際網路,使用遠端資料中心提供的運算、儲存、資料庫、應用程式及網路服務。 使用者不必自己購買大量伺服器或網路設備,只要透過電腦、手機、感測器或機器人連上網路, 就能使用雲端上的資源。

一句話理解:
雲端網路就是把「運算、儲存、網路與智慧服務」放到雲端,讓各地的使用者與設備可以隨時連線使用。

二、雲端網路的核心概念

  1. 虛擬化(Virtualization)
    將一台實體伺服器、硬碟或網路設備,切分成多個可獨立使用的虛擬資源,例如虛擬機器、虛擬硬碟與虛擬網路。
  2. 軟體定義網路(SDN, Software Defined Networking)
    將網路設備的管理與決策集中到軟體控制器。管理者可以用軟體調整資料傳輸路徑、頻寬與安全規則, 讓網路管理更有彈性。
  3. 網路功能虛擬化(NFV, Network Function Virtualization)
    原本要使用專用硬體才能完成的路由器、防火牆、負載平衡器等功能,改以軟體方式在一般伺服器上執行, 可降低成本,也更容易擴充。
  4. 基礎設施即代碼(IaC, Infrastructure as Code)
    用程式或設定檔建立伺服器、資料庫、網路與防火牆規則。這樣可以重複部署、保留版本紀錄, 並減少人工設定造成的錯誤。
  5. 彈性擴充與負載平衡
    當使用者增加時,雲端可以自動增加伺服器資源;當流量下降時,再減少資源以節省成本。 負載平衡則把大量使用者的請求分配到多台伺服器。
  6. 資安、身分驗證與資料保護
    雲端系統會利用帳號權限、防火牆、加密、VPN、網路分區與備份機制, 保護資料與設備,避免未授權使用者存取。

三、雲端網路在生活與產業上的應用

領域 應用例子 雲端網路的功能
智慧家庭 遠端控制冷氣、攝影機、門鎖與掃地機器人 讓家電與手機透過雲端交換資料與控制指令。
串流影音 YouTube、Netflix、線上直播與雲端遊戲 利用雲端伺服器、CDN 與負載平衡,提供大量使用者穩定服務。
線上學習 Google Classroom、Moodle、視訊課程 集中提供教材、作業、測驗、影片與學習紀錄。
電子商務 網路商店、行動支付、物流追蹤 整合商品、訂單、庫存、金流與配送資訊。
智慧製造 機台監控、品質檢測、預測維護 收集設備資料,透過雲端 AI 分析異常與預估故障。
智慧農漁業 水質、土壤、氣象、影像與設備控制 讓感測器即時上傳資料,並由手機或電腦遠端監控。

四、三個智慧生活與地方創生應用案例

案例一:水井村智慧養殖

在養殖現場,可利用溶氧、水溫、pH 值等感測器蒐集魚塭資料,再透過 HUB8735、 Wi-Fi、4G/5G 或 LoRa 網路傳送到雲端平台。養殖戶只要打開手機,就能即時查看水質狀況。

當溶氧過低或水溫異常時,雲端平台可以自動發出告警通知,並進一步連動打氣機或抽水設備。 若累積足夠資料,還能運用 AI 分析水質變化,協助養殖戶及早處理問題。

核心價值:讓養殖管理從「靠經驗巡池」走向「即時監測、遠端管理與智慧決策」。

案例二:水井三寶智慧互動導覽平台

水井村可將烏龜、白馬與姻緣花等地方文化故事,建置成雲端智慧導覽平台。 遊客掃描 QR Code、使用手機網頁或操作互動螢幕後,即可取得故事、照片、語音、影片與地圖資訊。

平台也可結合 AI 導覽 Agent。當遊客詢問「姻緣樹有多久歷史?」、 「下一個景點怎麼走?」或「水井三寶代表什麼意義?」時, AI Agent 可根據地方知識庫即時回答,成為不受時間限制的數位導覽員。

核心價值:讓地方文化能被數位保存、持續更新,並以互動方式傳遞給更多遊客與下一代。

案例三:Q-Robot AI 結合 AI Agent

Q-Robot 可結合麥克風、喇叭、螢幕、按鈕、燈光與感測器,成為能與人互動的智慧機器人。 當孩童、長者或遊客向 Q-Robot 說話或按下按鈕時,Q-Robot 可透過雲端網路連接 AI Agent 與知識庫, 理解問題後,再以語音、文字、燈光或動作回應。

例如,Q-Robot 可以扮演下列角色:

  • 地方文化小導遊:介紹水井三寶與村庄故事。
  • 兒童學習夥伴:帶領孩子完成程式設計、環保、食農與 AI 體驗任務。
  • 長者陪伴助手:以較慢的語速聊天、播放懷舊故事,提醒社區活動。
  • 活動任務引導員:依照參與者的回答與活動進度,安排下一個互動關卡。
核心價值:讓機器人不只是播放指令,而是能連接地方知識、理解問題並提供個人化互動的 AI 夥伴。

五、三個案例的比較

案例 主要對象 雲端網路的角色 AI 的角色
水井村智慧養殖 養殖戶 傳送感測資料、遠端控制設備 分析水質與異常預警
水井三寶智慧互動導覽 遊客、社區居民 提供故事、地圖、影音與使用紀錄 回答地方文化與導覽問題
Q-Robot AI Agent 兒童、長者、學習者 連接機器人、知識庫與後台服務 理解提問、規劃任務、產生個人化回應

六、結語

雲端網路的價值,不只是把資料存放在遠端,而是把現場設備、使用者、資料平台與 AI 智慧服務連接起來。 從水井村智慧養殖的水質監測,到水井三寶的文化導覽,再到 Q-Robot 的 AI 互動陪伴, 都說明了雲端網路可以成為地方創生、智慧教育與永續生活的重要基礎。

善用雲端網路,讓地方的故事、產業與學習,連結成更智慧、更永續的未來。

2026年8月31日 星期一

Python一下:用jieba整理水井村地方故事與關鍵詞

《Python一下:從風土資料到智慧生活》第 32 篇

用jieba整理水井村地方故事與關鍵詞

讓「水井三寶、姻緣花、柴井車、智慧養殖」不再被切散,也不讓演算法取代社區自己的說法。

CH14 斷詞處理jieba地方故事
學習目標
完成本篇後,你能解釋中文斷詞的必要性,使用jieba精確模式處理繁體中文,建立水井村自訂詞典與停用詞,統計詞頻、搜尋上下文、擷取TF-IDF關鍵詞,建立可人工審查的結果檔,並說明口述故事的同意、去識別、台語轉寫及地方詮釋限制。

一、電腦為何不知道「姻緣花」是一個詞?

英文常以空白分隔單字,中文句子通常沒有天然空白。「水井三寶包含烏龜、白馬與姻緣花」若逐字處理,就無法直接知道「水井三寶」和「姻緣花」是地方概念。斷詞的工作,就是提出一種詞語邊界。

原文可能結果問題
水井三寶水井/三寶地方品牌詞被拆開
姻緣花姻緣/花地方文化名稱失去完整性
魚菜共生淨水魚菜/共生/淨水可能需要保留「魚菜共生」
HUB 8735 Ultra英數與空白混合需另外設計技術詞正規化
斷詞沒有唯一答案:適合搜尋的切法、適合關鍵詞統計的切法,以及社區認同的地方詞,可能不同。結果必須依任務評估,不能把工具輸出當成地方語言的標準答案。

二、安裝jieba並記錄維護風險

import subprocess
import sys


subprocess.run(
    [sys.executable, "-m", "pip", "install", "jieba"],
    check=True,
)

截至本篇核對時,PyPI顯示的最新版仍是2020年發布的0.42.1。它適合教學與既有專案評估,但正式採用前應重新檢查維護狀態、Python相容性、授權、安全與替代工具,不能因為安裝成功就直接長期部署。

三、第一個繁體中文斷詞

import jieba


text = "水井三寶包含烏龜、白馬與姻緣花。"
tokens = list(jieba.cut(text, cut_all=False, HMM=True))

print(tokens)
print("/".join(tokens))

cut_all=False是精確模式,較適合一般文字分析;HMM可嘗試辨識未登錄詞,但不保證地方詞一定正確。

四、三種模式用途不同

sentence = "水井村推動智慧養殖與魚菜共生淨水。"

precise = list(jieba.cut(sentence, cut_all=False))
full = list(jieba.cut(sentence, cut_all=True))
search = list(jieba.cut_for_search(sentence))

print("精確模式:", precise)
print("全模式:", full)
print("搜尋模式:", search)
模式適合限制
精確模式詞頻、一般分析仍可能切錯地方詞
全模式觀察可能詞語重疊多,不能直接計算一般詞頻
搜尋引擎模式建立搜尋索引長詞會再次切分,詞數不可和精確模式混比

五、使用獨立Tokenizer,避免全域設定互相干擾

import jieba


tokenizer = jieba.Tokenizer()
for word in [
    "水井三寶",
    "姻緣花",
    "柴井車",
    "魚菜共生",
    "智慧養殖",
    "工作蝦",
]:
    tokenizer.add_word(word, freq=100000)

text = "水井三寶與柴井車承載地方記憶。"
print(list(tokenizer.cut(text, cut_all=False)))

在多人專案或測試中,獨立Tokenizer比修改全域 jieba.dt更容易控制。詞頻100000只是確保教學示例優先成詞,不代表真實語料頻率。

六、繁體中文詞典與地方詞典分工

jieba專案提供較大的 extra_dict/dict.txt.big,可作為繁體中文候選詞典;實際專案應把經審核的詞典檔納入版本管理,記錄來源、授權與雜湊,不在程式執行時臨時從不明網址下載。

from pathlib import Path
import jieba


def build_tokenizer(base_dictionary=None, user_dictionary=None):
    tokenizer = jieba.Tokenizer(
        dictionary=str(base_dictionary) if base_dictionary else None
    )
    if user_dictionary:
        path = Path(user_dictionary)
        if not path.is_file():
            raise FileNotFoundError(path)
        tokenizer.load_userdict(str(path))
    return tokenizer


# 專案備妥詞典後再啟用:
# tokenizer = build_tokenizer(
#     "data/dict.txt.big", "data/shuijing_words.txt"
# )

七、自訂詞典格式

自訂詞典每行為「詞語 詞頻 詞性」,詞頻與詞性可省略,檔案使用UTF-8。建議由社區與課程團隊共同審查:

水井三寶 100000 nz
姻緣花 100000 nz
柴井車 100000 nz
工作蝦 100000 nz
魚菜共生 100000 nz
智慧養殖 100000 nz
風頭水尾 100000 nz

nz只是常見自訂專名標記,詞性標註並非本篇主要目標。地方詞典還應另有說明表,記錄詞義、別稱、使用者、例句、審核者與版本日期。

八、先做最小文字清理

import re
import unicodedata


def normalize_text(text):
    text = unicodedata.normalize("NFKC", str(text))
    text = text.replace("\u3000", " ")
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()


sample = "水井村 推動  智慧養殖。"
print(normalize_text(sample))

NFKC會統一部分全形與相容字元,但也可能改變原始書寫形式。口述史原稿應另外保存;分析使用的是衍生副本,並記錄清理規則。

九、停用詞:刪掉常見詞也可能刪掉地方語氣

STOPWORDS = {
    "的", "了", "在", "和", "與", "是",
    "有", "也", "就", "我們", "一個",
}


def meaningful_tokens(text, tokenizer, stopwords=STOPWORDS):
    for token in tokenizer.cut(normalize_text(text)):
        token = token.strip()
        if not token:
            continue
        if token in stopwords:
            continue
        if not re.search(r"[\u3400-\u9fffA-Za-z0-9]", token):
            continue
        yield token


story = "我們在水井村用智慧養殖守護工作蝦。"
print(list(meaningful_tokens(story, tokenizer)))

停用詞必須依任務制定。長者口述中的「阮、咱、彼時、做伙」可能承載語氣、群體關係與台語特色,不應直接套用一般中文停用詞刪除。

十、用Counter統計地方詞頻

from collections import Counter


stories = [
    "水井村推動智慧養殖,也用魚菜共生淨水。",
    "水井三寶包含烏龜、白馬與姻緣花。",
    "姻緣花承載水井村的地方記憶與祝福。",
]

counter = Counter()
for story in stories:
    counter.update(meaningful_tokens(story, tokenizer))

for word, count in counter.most_common(10):
    print(word, count)

詞頻高表示在這批語料出現多,不等於對社區最重要。一次只被長者說出的罕見詞,可能反而是珍貴文化線索。

十一、同一詞在幾篇故事出現?

document_frequency = Counter()

for story in stories:
    unique_words = set(
        meaningful_tokens(story, tokenizer)
    )
    document_frequency.update(unique_words)

for word, documents in document_frequency.most_common(10):
    print(word, "出現在", documents, "篇")

詞頻計算總出現次數;文件頻率計算出現於幾篇文件。兩者回答不同問題,不能混稱「熱門程度」。

十二、顯示關鍵詞所在原句

def find_contexts(stories, keyword):
    results = []
    for index, story in enumerate(stories, start=1):
        sentences = re.split(r"(?<=[。!?])", story)
        for sentence in sentences:
            sentence = sentence.strip()
            if keyword in sentence:
                results.append({
                    "story": index,
                    "sentence": sentence,
                })
    return results


for item in find_contexts(stories, "姻緣花"):
    print(item)

詞頻表應能回到原句,才能讓社區夥伴確認用法與意義。公開結果若含可識別人物或私密經驗,仍需授權與去識別。

十三、TF-IDF擷取關鍵詞

import jieba.analyse


corpus_text = "\n".join(stories)
keywords = jieba.analyse.extract_tags(
    corpus_text,
    topK=8,
    withWeight=True,
)

for word, weight in keywords:
    print(word, round(weight, 4))

TF-IDF權重不是機率,也不是社區重要性分數。jieba預設IDF來自其內建語料,不一定代表臺灣農漁村或水井村;地方分析可建立經審查的領域語料與IDF,但需要足夠文件及清楚方法。

十四、讓TF-IDF使用自訂Tokenizer的地方詞

jieba.analyse預設使用全域斷詞器,可能和前面的獨立Tokenizer設定不同。簡單教材可在分析前把地方詞加入全域詞典,但要在單一初始化函式集中管理:

DOMAIN_WORDS = [
    "水井三寶", "姻緣花", "柴井車",
    "魚菜共生", "智慧養殖", "工作蝦",
]


def configure_global_jieba():
    for word in DOMAIN_WORDS:
        jieba.add_word(word, freq=100000, tag="nz")


configure_global_jieba()
keywords = jieba.analyse.extract_tags(
    "\n".join(stories), topK=8, withWeight=True
)
print(keywords)

測試要先重新初始化或固定執行順序,避免全域狀態讓測試互相污染。大型系統可評估更易注入Tokenizer的分析設計。

十五、比較詞頻與TF-IDF,不急著選「唯一答案」

frequency_top = [
    word for word, count in counter.most_common(8)
]
tfidf_top = [word for word, weight in keywords]

comparison = {
    "both": sorted(set(frequency_top) & set(tfidf_top)),
    "frequency_only": [
        word for word in frequency_top if word not in tfidf_top
    ],
    "tfidf_only": [
        word for word in tfidf_top if word not in frequency_top
    ],
}

print(comparison)

共同出現的詞可優先檢視;只在一邊出現的詞也不能直接丟掉。最終主題命名應回到原句、訪談脈絡與社區共同討論。

十六、為地方詞典建立回歸測試

CASES = {
    "水井三寶包含姻緣花": {"水井三寶", "姻緣花"},
    "魚菜共生協助淨水": {"魚菜共生"},
    "智慧養殖記錄工作蝦": {"智慧養殖", "工作蝦"},
}


def test_domain_dictionary(tokenizer):
    failures = []
    for sentence, expected in CASES.items():
        actual = set(tokenizer.cut(sentence, cut_all=False))
        missing = expected - actual
        if missing:
            failures.append({
                "sentence": sentence,
                "missing": sorted(missing),
                "actual": sorted(actual),
            })
    return failures


failures = test_domain_dictionary(tokenizer)
assert not failures, failures
print("地方詞典測試通過")

每次更新jieba、基礎詞典或地方詞典都執行測試,避免原本正確的地方詞突然被拆開。

十七、輸出可審查的JSON,而非只有詞雲

import json
from datetime import datetime, timezone


report = {
    "created_at": datetime.now(timezone.utc).isoformat(),
    "method": "jieba精確模式+自訂地方詞+詞頻與TF-IDF",
    "documents": len(stories),
    "top_frequency": counter.most_common(10),
    "top_tfidf": [
        {"word": word, "weight": weight}
        for word, weight in keywords
    ],
    "review_status": "待社區人工審查",
}

print(json.dumps(report, ensure_ascii=False, indent=2))

詞雲視覺吸引人,但會省略方法、分母與上下文。研究或USR成果應保留版本、語料範圍、清理規則、詞典、停用詞、程式與人工審查紀錄。

十八、台語口述故事要多一層語言尊重

階段應注意
語音轉文字台語辨識可能誤轉人名、地名、農產與料理
轉寫選擇漢字、台羅或混合書寫需保留原則與原始錄音
jieba斷詞主要針對中文文字,不等於台語語言分析工具
修訂由說話者或熟悉地方語言的人確認,不擅自「改成標準中文」
公開確認用途、範圍、署名/匿名、撤回及保存期限

十九、個資去識別不能只靠正規表示式

import re


def flag_possible_sensitive_text(text):
    patterns = {
        "phone": r"(?<!\d)09\d{8}(?!\d)",
        "email": r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}",
        "address_hint": r"(?:路|街|巷|弄|號)",
    }
    return {
        name: bool(re.search(pattern, text))
        for name, pattern in patterns.items()
    }


print(flag_possible_sensitive_text(
    "這是一段不含聯絡方式的示範故事。"
))

這只能標記部分可能風險,無法辨識暱稱、親屬關係、罕見事件或組合資訊。去識別必須人工審查;即使移除姓名,故事仍可能讓社區成員辨識當事人。

二十、課堂挑戰

挑戰A|基礎:以5段匿名地方故事建立詞頻表,列出有效詞數、文件頻率及每個前10詞的原句。
挑戰B|進階:建立20個水井村地方詞的詞典與至少10個回歸測試案例,記錄新增前後斷詞差異。
挑戰C|USR場域:請長者或社區夥伴檢視演算法選出的關鍵詞,標記「認同、需改名、不宜公開、演算法漏掉」,比較機器結果與地方觀點。

二十一、用AI協助整理,但不能代替受訪者確認

請擔任地方故事斷詞與關鍵詞分析助教。
語料來自已同意教學分析並完成去識別的社區故事。
請先檢查繁體中文、台語轉寫、地方專名、同義詞、停用詞與個資風險,
再協助比較詞頻、文件頻率與TF-IDF結果。
不得把演算法關鍵詞稱為「社區最重要的文化」,
不得擅自改寫長者原意,也不要要求提供真實姓名或未授權逐字稿。
請輸出待人工確認清單、原句索引與地方詞典修訂建議。

二十二、延伸閱讀

Python一下:用Pillow打造水井三寶地方圖卡處理工具

《Python一下:從風土資料到智慧生活》第 30 篇

用Pillow打造水井三寶地方圖卡處理工具

讓烏龜、白馬與姻緣花的照片,變成尺寸一致、文字清楚、可追溯來源的地方圖卡。

CH14 圖片處理Pillow水井三寶
學習目標
完成本篇後,你能安全開啟與驗證圖片、依EXIF修正方向、比較thumbnail與fit、加入繁體中文標題及半透明浮水印、正確輸出JPEG/PNG、批次產生圖卡與處理紀錄,並說明肖像權、著作權、位置資訊及AI生成標示。

一、地方圖卡不是「把字貼上去」而已

同一張水井三寶照片可能來自手機、相機、社區長者或AI創作,具有不同方向、尺寸、色彩模式、檔案格式及使用權。圖卡工具至少要回答四件事:

問題程式處理USR責任
圖片能否安全讀取?限制大小、辨識格式、捕捉錯誤不接受不明來源大檔直接進系統
版型是否一致?轉向、縮放、裁切與色彩模式不扭曲人物、工藝或地方物件
文字是否清楚?字型、文字框、對比與安全邊界名稱與故事先由社區確認
能否合法公開?輸出時移除不必要metadata確認授權、肖像同意及AI標示

二、安裝Pillow:安裝名稱與匯入名稱不同

import subprocess
import sys


subprocess.run(
    [sys.executable, "-m", "pip", "install", "Pillow"],
    check=True,
)

pip安裝名稱是Pillow,程式匯入名稱仍是PIL。請在專案虛擬環境安裝,並把實際驗證版本記錄在依賴檔中。

三、讀取圖片基本資訊

from pathlib import Path
from PIL import Image


source = Path("images/turtle.jpg")

with Image.open(source) as image:
    print("格式:", image.format)
    print("尺寸:", image.size)
    print("色彩模式:", image.mode)
    print("影格數:", getattr(image, "n_frames", 1))

Image.open()會延遲讀取像素,因此要使用with確保檔案關閉。不要只依副檔名判定內容;Pillow會由檔案內容辨識格式。

四、不可信圖片先驗證,再重新開啟

import warnings
from pathlib import Path
from PIL import Image, UnidentifiedImageError


MAX_FILE_BYTES = 15 * 1024 * 1024
ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP"}


def validate_image(path):
    path = Path(path)
    if not path.is_file():
        raise FileNotFoundError(path)
    if path.stat().st_size > MAX_FILE_BYTES:
        raise ValueError("檔案超過15 MB限制")

    with warnings.catch_warnings():
        warnings.simplefilter("error", Image.DecompressionBombWarning)
        try:
            with Image.open(path) as image:
                detected_format = image.format
                if detected_format not in ALLOWED_FORMATS:
                    raise ValueError("不支援的圖片格式")
                image.verify()
        except UnidentifiedImageError as error:
            raise ValueError("無法辨識圖片") from error

    return detected_format

verify()檢查後必須重新開啟圖片,不能繼續用同一物件處理像素。檔案大小與像素量是兩種不同風險;Pillow會對異常巨大像素數提出decompression bomb警告或錯誤,不應為方便直接關閉保護。

五、手機照片方向錯誤:套用EXIF Orientation

from PIL import Image, ImageOps


def open_oriented_rgb(path):
    validate_image(path)
    with Image.open(path) as image:
        oriented = ImageOps.exif_transpose(image)
        return oriented.convert("RGB")


image = open_oriented_rgb("images/turtle.jpg")
print(image.size, image.mode)

ImageOps.exif_transpose()依EXIF方向旋轉或鏡射像素,並處理方向資訊。若略過這一步,手機上直立的照片在輸出後可能橫躺。

六、thumbnail:完整保留、放進指定範圍

from pathlib import Path
from PIL import Image


image = open_oriented_rgb("images/turtle.jpg")
thumbnail = image.copy()
thumbnail.thumbnail(
    (800, 800),
    resample=Image.Resampling.LANCZOS,
)
Path("output").mkdir(parents=True, exist_ok=True)
thumbnail.save("output/turtle_thumbnail.jpg", quality=88)

print(thumbnail.size)

thumbnail()會原地修改圖片,保持長寬比並限制在指定框內,通常不會放大小圖。它適合完整保留照片,但輸出寬高不一定完全一致。

七、ImageOps.fit:裁成一致的社群圖卡

from PIL import Image, ImageOps


image = open_oriented_rgb("images/white_horse.jpg")
card_photo = ImageOps.fit(
    image,
    (1200, 800),
    method=Image.Resampling.LANCZOS,
    centering=(0.5, 0.45),
)
card_photo.save("output/white_horse_1200x800.jpg", quality=90)

print(card_photo.size)

fit()會等比例縮放後裁切,得到固定尺寸。centering可以調整保留重心;人物、樹木或工藝品不能一律置中,最好提供裁切預覽讓社區夥伴確認。

八、不要用resize硬拉成固定尺寸

from PIL import ImageOps, Image


def make_card_background(image, size=(1200, 1200), mode="crop"):
    if mode == "crop":
        return ImageOps.fit(
            image, size, method=Image.Resampling.LANCZOS
        )
    if mode == "contain":
        return ImageOps.pad(
            image,
            size,
            method=Image.Resampling.LANCZOS,
            color=(245, 240, 224),
        )
    raise ValueError("mode必須是crop或contain")

crop填滿版面但會裁掉邊緣;contain完整保留但可能出現留白。直接 resize((1200,1200))會改變比例,使姻緣花、烏龜或人物變形。

九、繁體中文字型要由專案明確提供

不同電腦的中文字型位置不同。最穩定做法是在取得合法再散布授權後,將指定字型作為專案資產,或由部署環境透過設定提供路徑。

import os
from pathlib import Path
from PIL import ImageFont


def load_font(size):
    font_path = Path(os.environ["SHUIJING_FONT_PATH"])
    if not font_path.is_file():
        raise FileNotFoundError("找不到指定中文字型")
    return ImageFont.truetype(str(font_path), size=size)


title_font = load_font(64)

不要假設Arial、微軟正黑體或Noto一定存在;也不要未確認授權就把系統字型複製到公開專案。

十、使用textbbox量字,再畫文字底板

from PIL import ImageDraw


def draw_title(image, title, font, margin=48):
    draw = ImageDraw.Draw(image, "RGBA")
    box = draw.textbbox((0, 0), title, font=font)
    text_width = box[2] - box[0]
    text_height = box[3] - box[1]

    x = margin
    y = image.height - text_height - margin * 2
    panel = (
        x - 20,
        y - 16,
        min(x + text_width + 20, image.width - margin),
        y + text_height + 20,
    )
    draw.rounded_rectangle(
        panel, radius=18, fill=(13, 66, 55, 205)
    )
    draw.text(
        (x, y), title, font=font,
        fill=(255, 255, 255, 255),
        stroke_width=1,
        stroke_fill=(0, 0, 0, 180),
    )


card = make_card_background(
    open_oriented_rgb("images/marriage_flower.jpg")
)
draw_title(card, "水井三寶|姻緣花", load_font(64))

textbbox()可先取得文字範圍,再決定底板大小。長標題仍可能超出畫面,下一節加入自動縮小。

十一、讓長標題自動縮小

from PIL import ImageDraw


def fit_font(draw, text, max_width, start=72, minimum=28):
    for size in range(start, minimum - 1, -2):
        font = load_font(size)
        left, top, right, bottom = draw.textbbox(
            (0, 0), text, font=font
        )
        if right - left <= max_width:
            return font
    raise ValueError("標題太長,請編輯文字或改用多行版型")

自動縮小仍應設最低可讀字級。若再縮就看不清楚,應改寫標題或設計多行文字,而不是讓字擠成一條。

十二、加入半透明來源標示

from PIL import Image, ImageDraw


def add_credit(image, text, font):
    base = image.convert("RGBA")
    layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(layer)
    box = draw.textbbox((0, 0), text, font=font)
    width = box[2] - box[0]
    height = box[3] - box[1]
    position = (
        base.width - width - 30,
        24,
    )
    draw.text(
        position, text, font=font,
        fill=(255, 255, 255, 190),
        stroke_width=2,
        stroke_fill=(0, 0, 0, 150),
    )
    return Image.alpha_composite(base, layer)


credited = add_credit(
    card, "影像來源:經社區授權", load_font(28)
)

浮水印不能取代授權。文字要依真實來源填寫;AI生成或經AI大幅修改的圖片,也應依使用情境清楚標示。

十三、輸出JPEG或PNG前先處理色彩模式

from pathlib import Path


def save_for_web(image, destination):
    destination = Path(destination)
    destination.parent.mkdir(parents=True, exist_ok=True)
    suffix = destination.suffix.lower()

    if suffix in {".jpg", ".jpeg"}:
        image.convert("RGB").save(
            destination,
            format="JPEG",
            quality=88,
            optimize=True,
        )
    elif suffix == ".png":
        image.convert("RGBA").save(
            destination,
            format="PNG",
            optimize=True,
        )
    else:
        raise ValueError("輸出格式只允許JPEG或PNG")


save_for_web(credited, "output/marriage_flower_card.jpg")

JPEG不支援透明度,RGBA直接存JPEG會出錯;PNG適合透明圖示,但照片可能較大。重新建立輸出檔可避免原照片中的GPS等EXIF跟著公開,但若你主動傳入exif或其他metadata則另當別論。

十四、完成單張水井三寶圖卡函式

from PIL import ImageDraw


def create_local_card(
    source, destination, title, credit,
    size=(1200, 1200), crop_mode="crop"
):
    image = open_oriented_rgb(source)
    card = make_card_background(image, size, crop_mode)

    draw = ImageDraw.Draw(card, "RGBA")
    title_font = fit_font(
        draw, title, max_width=size[0] - 120
    )
    draw_title(card, title, title_font, margin=48)
    card = add_credit(card, credit, load_font(28))
    save_for_web(card, destination)

    return {
        "source": str(source),
        "destination": str(destination),
        "title": title,
        "size": size,
    }


result = create_local_card(
    "images/turtle.jpg",
    "output/turtle_card.jpg",
    "水井三寶|烏龜",
    "影像來源:經社區授權",
)
print(result)

十五、批次處理三張圖卡

JOBS = [
    {
        "source": "images/turtle.jpg",
        "destination": "output/turtle_card.jpg",
        "title": "水井三寶|烏龜",
    },
    {
        "source": "images/white_horse.jpg",
        "destination": "output/white_horse_card.jpg",
        "title": "水井三寶|白馬",
    },
    {
        "source": "images/marriage_flower.jpg",
        "destination": "output/marriage_flower_card.jpg",
        "title": "水井三寶|姻緣花",
    },
]

records = []
for job in JOBS:
    try:
        record = create_local_card(
            **job,
            credit="影像來源:經社區授權",
        )
        record["status"] = "success"
    except Exception as error:
        record = {
            "source": job["source"],
            "status": "failed",
            "error": type(error).__name__,
        }
    records.append(record)

for record in records:
    print(record)

批次工具不應因一張壞圖讓所有工作中止,但錯誤紀錄不要包含個資、秘密路徑或完整內部例外內容。

十六、用雜湊與尺寸驗證輸出

import hashlib
from pathlib import Path
from PIL import Image


def inspect_output(path, expected_size=(1200, 1200)):
    path = Path(path)
    digest = hashlib.sha256(path.read_bytes()).hexdigest()
    with Image.open(path) as image:
        image.load()
        if image.size != expected_size:
            raise ValueError("輸出尺寸不正確")
        return {
            "path": str(path),
            "format": image.format,
            "mode": image.mode,
            "size": image.size,
            "bytes": path.stat().st_size,
            "sha256": digest,
        }


print(inspect_output("output/turtle_card.jpg"))

雜湊可協助確認檔案是否變更,不能證明圖片內容正確。仍要人工檢查裁切、文字、對比、地方名稱與來源標示。

十七、發布前的人文與倫理檢查

檢查發布前要確認
著作權攝影者、插畫者或權利人是否同意使用及改作?
肖像與隱私可辨識人物是否同意?是否含門牌、車牌或位置資訊?
地方詮釋「水井三寶」名稱與故事是否經社區夥伴確認?
AI透明AI生成、補圖或大幅修改是否清楚標示?
可近用性網站是否提供替代文字,而非只把文字畫進圖片?
原圖要另外保管:批次程式輸出到獨立資料夾,不覆蓋社區提供的原始照片。若未取得公開同意,處理完成也不代表可以上網發布。

十八、課堂挑戰

挑戰A|基礎:用同一張照片分別產生thumbnail、crop與contain三種版本,說明各自適合的情境。
挑戰B|進階:讓長標題自動換成兩行,確保不低於32px,並為文字底板保留左右安全距離。
挑戰C|USR場域:和社區夥伴共同建立圖卡metadata表,記錄作品名、來源、授權範圍、人物同意、AI使用、審核者及下架日期。

十九、用AI協助檢查,但不要交出未授權照片

請擔任Python Pillow程式碼審查助教。
我要把社區授權照片製作成1200×1200地方圖卡。
請檢查:不可信圖片驗證、解壓縮炸彈、EXIF方向、
等比例縮放、裁切重心、中文字型授權、文字可讀性、
JPEG/PNG模式、metadata移除、錯誤處理與不覆蓋原圖。
不要要求我上傳未取得同意的人像或含GPS的原始照片。
請提供最小測試案例與人工審查清單。

二十、延伸閱讀

Python一下:從XML讀懂智慧設備設定與地方導覽資料

《Python一下:從風土資料到智慧生活》第 23 篇

從XML讀懂智慧設備設定與地方導覽資料

從樹狀標記中找出水井三寶多語內容與設備參數,同時守住格式、來源及操作安全。

CH12 XMLElementTree設定驗證
學習目標
完成本篇後,你能辨認XML元素、屬性、文字與階層,使用 ElementTree 解析、搜尋、建立及輸出XML,處理命名空間與解析錯誤,並把設備設定轉成「待人工確認」的安全資料。

一、XML與JSON有何不同?

兩者都能表示結構化資料。JSON常見於網頁API;XML在設備設定、標準文件、RSS及舊系統交換格式中仍很常見。

特性JSONXML
主要結構物件、陣列、基本型別元素樹、屬性、文字
資料型別數字、布林、null等讀出時多半先是字串
重複節點通常使用陣列重複同名元素
命名空間沒有內建機制可避免不同詞彙名稱衝突
設備安全原則:XML格式正確不代表參數安全。教學程式只讀取、驗證並產生預覽;不可把外部XML直接送往水泵、曝氣機或其他設備。實際套用須確認來源、版本、簽章或完整性、場域規則及人工授權。

二、看懂XML結構

<guide version="1.0">
  <item id="white-horse" language="zh-Hant">
    <title>白馬</title>
    <status>待社區確認</status>
  </item>
  <item id="turtle" language="zh-Hant">
    <title>烏龜</title>
    <status>待社區確認</status>
  </item>
</guide>

guide 是根元素;item 是子元素;id 與 language 是屬性;title 元素內的「白馬」是文字內容。XML必須只有一個根元素,開始與結束標籤要正確巢狀。

三、從字串解析XML

import xml.etree.ElementTree as ET

xml_text = """
<guide version="1.0">
  <item id="marriage-flower" language="zh-Hant">
    <title>姻緣花</title>
    <status>待社區確認</status>
  </item>
</guide>
"""

root = ET.fromstring(xml_text)
print("根元素:", root.tag)
print("版本:", root.get("version"))

fromstring() 回傳根元素。XML屬性可用 get() 取得,找不到時回傳 None。

四、搜尋元素與讀取文字

for item in root.findall("item"):
    item_id = item.get("id")
    language = item.get("language")
    title = item.findtext("title", default="").strip()
    status = item.findtext("status", default="").strip()

    print(item_id, language, title, status)
方法用途
find()尋找第一個符合的子元素
findall()取得所有符合元素
findtext()直接取得第一個符合元素的文字
iter()走訪目前元素下所有指定節點

五、從檔案安全解析

import xml.etree.ElementTree as ET
from pathlib import Path


def load_xml(path, max_bytes=2_000_000):
    path = Path(path)
    if not path.is_file():
        raise FileNotFoundError(f"找不到XML:{path}")
    if path.stat().st_size > max_bytes:
        raise ValueError("XML超過教學程式允許大小")

    try:
        tree = ET.parse(path)
    except ET.ParseError as error:
        raise ValueError(f"XML格式錯誤:{error}") from error
    return tree

檔案大小限制可降低意外資源消耗,但不是完整的XML安全防護。若要解析來源不可信的XML,應採用專為防禦XML攻擊設計的函式庫(如defusedxml)及環境資源限制。

六、把導覽元素轉成字典

def parse_guide_item(item):
    item_id = (item.get("id") or "").strip()
    language = (item.get("language") or "").strip()
    title = item.findtext("title", default="").strip()
    status = item.findtext("status", default="").strip()

    missing = [
        name
        for name, value in {
            "id": item_id,
            "language": language,
            "title": title,
            "status": status,
        }.items()
        if not value
    ]
    if missing:
        raise ValueError("缺少內容:" + "、".join(missing))

    return {
        "id": item_id,
        "language": language,
        "title": title,
        "status": status,
    }

XML沒有自動幫你驗證業務規則。讀取後仍須確認必要屬性、空白文字、重複ID及可接受的語言代碼。

七、批次處理並隔離錯誤

def parse_guide(root):
    records = []
    errors = []
    used_ids = set()

    for index, item in enumerate(root.findall("item"), start=1):
        try:
            record = parse_guide_item(item)
            if record["id"] in used_ids:
                raise ValueError(f"重複id:{record['id']}")
            used_ids.add(record["id"])
            records.append(record)
        except ValueError as error:
            errors.append({"序號": index, "原因": str(error)})

    return records, errors

八、解析智慧設備設定

device_xml = """
<device-config version="1.0" apply="preview-only">
  <device id="TEMP-01" type="temperature">
    <enabled>true</enabled>
    <sample-seconds>60</sample-seconds>
  </device>
</device-config>
"""

config_root = ET.fromstring(device_xml)
device = config_root.find("device")

device_id = device.get("id")
device_type = device.get("type")
enabled_text = device.findtext("enabled", default="").strip().lower()
sample_seconds = int(
    device.findtext("sample-seconds", default="")
)

print(device_id, device_type, enabled_text, sample_seconds)

XML文字須自行轉成 int 或布林值。不要使用 bool("false"),因為非空字串會得到 True。

九、嚴格轉換布林與整數

def parse_boolean(text, field_name):
    normalized = str(text).strip().lower()
    if normalized == "true":
        return True
    if normalized == "false":
        return False
    raise ValueError(f"{field_name}只能是true或false")


def parse_positive_int(text, field_name, maximum):
    value = int(str(text).strip())
    if not 1 <= value <= maximum:
        raise ValueError(
            f"{field_name}必須介於1到{maximum}"
        )
    return value


enabled = parse_boolean(enabled_text, "enabled")
interval = parse_positive_int(
    sample_seconds, "sample-seconds", 86400
)
print(enabled, interval)
上限只是教學防線:86400秒並非真實設備管理建議。實際採樣週期與控制參數要由設備規格、場域人員與專業需求共同決定。

十、命名空間不是多餘的括號

XML命名空間避免不同標準使用相同標籤時衝突。ElementTree搜尋時要提供前綴對照。

import xml.etree.ElementTree as ET

xml_text = """
<g:guide xmlns:g="https://example.edu/guide">
  <g:item id="white-horse">
    <g:title>白馬</g:title>
  </g:item>
</g:guide>
"""

root = ET.fromstring(xml_text)
ns = {"g": "https://example.edu/guide"}

for item in root.findall("g:item", ns):
    title = item.findtext("g:title", default="", namespaces=ns)
    print(item.get("id"), title)

範例網址只是命名空間識別字,不代表瀏覽器一定能開啟該網址。

十一、建立XML導覽檔

import xml.etree.ElementTree as ET
from pathlib import Path


def build_guide_xml(records):
    root = ET.Element("guide", {"version": "1.0"})
    for record in records:
        item = ET.SubElement(
            root,
            "item",
            {
                "id": record["id"],
                "language": record["language"],
            },
        )
        ET.SubElement(item, "title").text = record["title"]
        ET.SubElement(item, "status").text = record["status"]

    tree = ET.ElementTree(root)
    ET.indent(tree, space="  ")
    return tree


records = [
    {"id": "white-horse", "language": "zh-Hant",
     "title": "白馬", "status": "待社區確認"},
]
tree = build_guide_xml(records)

由ElementTree建立元素時,特殊字元會被正確跳脫;不要用字串拼接XML,否則文字中的 &、< 等符號容易破壞格式。

十二、不覆蓋地輸出XML

from pathlib import Path


def write_xml_exclusive(tree, path):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    xml_bytes = ET.tostring(
        tree.getroot(),
        encoding="utf-8",
        xml_declaration=True,
    )
    with path.open("xb") as file:
        file.write(xml_bytes)
    return path


# write_xml_exclusive(tree, "導覽資料/水井三寶_v1.xml")

十三、設定變更比較

def compare_device_config(old_config, new_config):
    changes = {}
    keys = sorted(old_config.keys() | new_config.keys())

    for key in keys:
        old_value = old_config.get(key)
        new_value = new_config.get(key)
        if old_value != new_value:
            changes[key] = {
                "舊值": old_value,
                "新值": new_value,
            }
    return changes

變更報告應先交由設備負責人確認,再經測試環境驗證;不可由XML解析程式直接套用到正式場域。

十四、常見錯誤檢查表

現象原因修正
ParseError標籤未關閉或巢狀錯誤檢查錯誤位置與原始來源
find找不到元素路徑錯誤或有命名空間確認階層並提供ns對照
false被轉成True使用bool("false")明確比對true/false
中文輸出成亂碼編碼不一致輸出UTF-8並寫入XML宣告
設定檔直接控制設備缺少驗證與核准層只產生預覽與變更報告

十五、USR實作挑戰

挑戰A|水井三寶多語XML
為白馬、烏龜與姻緣花建立華語、台語文字節點,加入確認狀態與授權欄位,找出缺漏及重複ID。
挑戰B|設備設定預覽
解析三個匿名設備設定,比較新舊版本,輸出變更報告,但不連接或操作任何真實設備。
挑戰C|錯誤XML測試
準備缺標籤、缺屬性、錯誤布林、過大整數與命名空間版本,確認程式能提供清楚錯誤。

十六、與生成式AI協作

請擔任Python XML與智慧設備設定安全助教。
請檢查我的ElementTree程式:
1. 驗證根元素、版本、必要屬性與文字;
2. 數字與布林必須嚴格轉型;
3. 支援XML命名空間;
4. 不以字串拼接XML;
5. 輸出UTF-8且不得覆蓋舊檔;
6. 不可信XML須限制大小並說明安全解析方案;
7. 設定只能產生預覽和變更報告,不可直接控制設備。
請提供正常與故障測試資料。
本篇小結
XML是一棵帶有元素、屬性與文字的樹。解析只是第一步;真正可靠的流程還要驗證結構、型別、命名空間、來源與版本,設備設定更必須經人工核准。

Python一下:照片與語音不是文字——認識二進位檔與雜湊驗證

《Python一下:從風土資料到智慧生活》第 19 篇

照片與語音不是文字——認識二進位檔與雜湊驗證

安全保存水井三寶影像、長者口述錄音及設備原始檔,並確認備份前後一個位元組都沒有改變。

CH10 二進位檔bytesSHA-256
學習目標
完成本篇後,你能分辨文字與二進位資料,使用 rb、wb、xb 讀寫位元組,以分塊方式處理大型媒體檔,使用Base64進行文字化傳輸,並用SHA-256驗證檔案完整性。

一、為什麼照片不能用UTF-8讀?

文字檔需要編碼把位元組解讀成字元;JPEG、PNG、MP3、WAV等檔案則有自己的二進位格式。若以 encoding="utf-8" 開啟照片,Python會嘗試把影像位元組解碼為文字,通常產生 UnicodeDecodeError。

資料Python型別常用模式
逐字稿、CSV、JSONstrr、w,指定編碼
照片、錄音、PDF、韌體bytesrb、wb,不指定編碼
資料權利提醒:照片與錄音可能包含肖像、聲音、住家環境、位置及個人故事。能讀取或複製檔案不等於取得公開、訓練AI或再利用權利。應保存同意範圍、撤回方式及公開版本,原始媒體需限制存取。

二、認識bytes

text = "水井三寶"
data = text.encode("utf-8")

print(type(text), len(text))
print(type(data), len(data))
print(data)
print(data.decode("utf-8"))

bytes 是0到255整數組成的不可變序列。文字可經編碼變成位元組,但照片位元組本身不應任意 decode() 成文字。

三、讀取二進位檔的前幾個位元組

from pathlib import Path


def read_header(path, size=16):
    path = Path(path)
    with path.open("rb") as file:
        return file.read(size)


# 實際使用時換成資料副本
# header = read_header("照片/水井三寶.jpg")
# print(header)
# print(header.hex())

許多檔案格式有特徵位元組,但只檢查檔頭不能證明整份檔案安全或內容正確。副檔名與檔頭都只能作為初步判斷。

四、建立教學用二進位檔

from pathlib import Path

path = Path("教學資料.bin")
sample = bytes([0, 1, 2, 127, 128, 254, 255])

with path.open("xb") as file:
    file.write(sample)

with path.open("rb") as file:
    restored = file.read()

print(restored)
print(list(restored))

xb 表示排他建立二進位檔;目標已存在時會拒絕覆蓋,適合保護重要資料。

五、分塊讀取大型照片或錄音

使用 read() 一次讀完整段錄音可能占用大量記憶體。分塊處理能固定每次使用量。

from pathlib import Path


def count_bytes(path, chunk_size=64 * 1024):
    total = 0
    with Path(path).open("rb") as file:
        while chunk := file.read(chunk_size):
            total += len(chunk)
    return total


print(count_bytes("教學資料.bin"))

六、安全複製:目標存在就停止

from pathlib import Path


def copy_binary(source, target, chunk_size=1024 * 1024):
    source = Path(source)
    target = Path(target)
    if not source.is_file():
        raise FileNotFoundError(f"來源不存在:{source}")

    target.parent.mkdir(parents=True, exist_ok=True)
    with source.open("rb") as input_file:
        with target.open("xb") as output_file:
            while chunk := input_file.read(chunk_size):
                output_file.write(chunk)
    return target


# copy_binary("原始媒體/訪談.wav", "備份/訪談.wav")
避免半成品:若複製途中失敗,目標可能只寫入一部分。正式流程可先寫入暫存檔,驗證雜湊後再更名;失敗時保留錯誤紀錄並人工處理。

七、用SHA-256建立檔案指紋

import hashlib
from pathlib import Path


def sha256_file(path, chunk_size=1024 * 1024):
    digest = hashlib.sha256()
    with Path(path).open("rb") as file:
        while chunk := file.read(chunk_size):
            digest.update(chunk)
    return digest.hexdigest()


print(sha256_file("教學資料.bin"))

相同位元組會產生相同摘要;只要內容改變,摘要通常就不同。雜湊適合驗證完整性,但不能從摘要還原檔案。

八、複製後驗證是否一致

def verify_same_file(source, target):
    source_hash = sha256_file(source)
    target_hash = sha256_file(target)
    return {
        "來源SHA256": source_hash,
        "目標SHA256": target_hash,
        "內容一致": source_hash == target_hash,
    }


# result = verify_same_file("原始媒體/訪談.wav", "備份/訪談.wav")
# print(result)
雜湊不是備份:摘要只能幫助發現差異,不能救回遺失檔案。真正備份至少要有獨立副本、權限管理、定期驗證與還原演練。

九、Base64:把位元組轉成文字

import base64

original = b"USR binary demo"
encoded = base64.b64encode(original)
decoded = base64.b64decode(encoded, validate=True)

print(encoded.decode("ascii"))
print(decoded)
print(original == decoded)

Base64常用於JSON、電子郵件或文字通道傳輸二進位內容,但體積會增加,而且不是加密。任何拿到內容的人都能解碼,不能用來保護訪談錄音或設備憑證。

十、建立二進位檔清冊

from pathlib import Path


def binary_inventory(folder):
    allowed = {".jpg", ".jpeg", ".png", ".wav", ".mp3", ".pdf"}
    records = []

    for path in sorted(Path(folder).rglob("*")):
        if path.is_file() and path.suffix.lower() in allowed:
            records.append({
                "相對路徑": str(path.relative_to(folder)),
                "大小": path.stat().st_size,
                "SHA256": sha256_file(path),
            })
    return records


# for record in binary_inventory("教學媒體"):
#     print(record)

清冊公開前要檢查檔名是否含姓名、地址或活動參與者資訊。SHA-256摘要本身通常不透露內容,但可用於比對已知檔案,仍應依資料政策管理。

十一、驗證備份清冊

def compare_inventories(source_records, backup_records):
    source_map = {
        record["相對路徑"]: record["SHA256"]
        for record in source_records
    }
    backup_map = {
        record["相對路徑"]: record["SHA256"]
        for record in backup_records
    }

    all_paths = sorted(source_map.keys() | backup_map.keys())
    return [
        {
            "相對路徑": path,
            "狀態": (
                "一致"
                if source_map.get(path) == backup_map.get(path)
                else "缺少或內容不同"
            ),
        }
        for path in all_paths
    ]

十二、不要用雜湊保存密碼

本篇SHA-256用於檔案完整性,不適合直接保存使用者密碼。密碼需要專用、帶鹽且刻意緩慢的演算法與成熟框架;也不要自己發明加密方式。

十三、常見錯誤檢查表

現象原因修正
照片出現UnicodeDecodeError以文字模式讀取改用rb,不指定encoding
大型錄音占滿記憶體一次read全部內容使用固定大小分塊
備份覆蓋原檔使用wb且未檢查使用xb並分開來源與目標
檔案大小相同就判斷一致大小不能代表內容比較SHA-256
把Base64當加密誤解其用途視為編碼,不存敏感資料

十四、USR實作挑戰

挑戰A|媒體清冊
對已授權的教學副本建立相對路徑、大小與SHA-256清冊,檢查檔名是否洩露個資。
挑戰B|備份驗證
複製三個小型教學檔案到獨立資料夾,比對雜湊;再修改副本一個位元組,觀察結果。
挑戰C|授權分級
為原始錄音、內部研究版、講述者確認版與公開剪輯版設計不同資料夾與存取規則。

十五、與生成式AI協作

請擔任Python二進位檔與USR媒體保存助教。
請檢查我的照片與錄音備份程式:
1. 使用rb與xb,不得覆蓋原始媒體;
2. 大檔案必須分塊處理;
3. 複製後以SHA-256驗證;
4. Base64只能視為編碼,不是加密;
5. 清冊不得洩露不必要個資;
6. 能存取檔案不代表取得公開或AI訓練授權。
請先列出風險,再提供可測試函式。
本篇小結
二進位檔要用位元組模式處理,大檔案要分塊,重要副本要拒絕覆蓋並用雜湊驗證。技術確保內容完整,授權與權限制度則確保使用正當。

Python一下:從水井三寶到多語導覽——ASCII、Unicode與中文字串

《Python一下:從風土資料到智慧生活》第 14 篇

從水井三寶到多語導覽——ASCII、Unicode與中文字串

讓白馬、烏龜、姻緣花與台語地方故事,跨過裝置、檔案和網頁仍能正確顯示。

CH9 進階字串Unicode多語導覽
學習目標
完成本篇後,你能分辨字元、碼位、字串與位元組;理解 ASCII、Unicode 與 UTF-8 的關係;使用 ord()、chr()、encode()、decode();處理中文字串、正規化與常見亂碼。

一、電腦怎麼認識「姻緣花」?

人看到的是文字,電腦儲存的是數字與位元組。為了讓不同裝置對同一個字有共同理解,需要三個層次:

層次意義例子
字元 character人理解的文字單位「花」
Unicode 碼位字元的通用編號U+82B1
編碼 encoding把碼位轉成實際位元組的規則UTF-8
地方語言提醒:台語文字可能使用漢字、臺羅、聲調符號或混合形式。程式能保存字形,卻不能替社區決定哪種寫法最能表達語意;公開導覽前仍須由講述者與地方語言使用者確認。

二、ASCII:早期的基本字元表

ASCII 主要涵蓋英文字母、數字、標點與控制字元,無法直接表示「水井村」等中文字。

print(ord("A"))
print(chr(65))
print(ord("1"))

text = "USR"
for character in text:
    print(character, ord(character))

ord() 取得單一字元的 Unicode 碼位整數;chr() 則把整數轉回字元。ASCII 字元同時也是 Unicode 的一部分。

三、Unicode 不等於 UTF-8

Unicode 是字元及碼位的共同標準;UTF-8、UTF-16 則是把碼位轉成位元組的編碼方式。Python 3 的 str 表示 Unicode 文字,bytes 表示已編碼的位元組。

name = "姻緣花"

print(type(name))
for character in name:
    print(character, f"U+{ord(character):04X}")

四、encode 與 decode:文字和位元組之間的橋

story = "水井三寶:白馬、烏龜、姻緣花"

utf8_data = story.encode("utf-8")
restored_story = utf8_data.decode("utf-8")

print(utf8_data)
print(restored_story)
print(story == restored_story)

記住方向:文字 encode 成 bytes;bytes decode 回文字。兩端必須對同一批位元組使用相符編碼。

五、字數與位元組數為何不同?

texts = ["ABC", "水井村", "水井村🐢"]

for text in texts:
    encoded = text.encode("utf-8")
    print(
        text,
        "字串長度:", len(text),
        "UTF-8 位元組數:", len(encoded),
    )

len(str) 計算 Python 字串中的碼位數量;len(bytes) 計算位元組。資料庫欄位、網路封包或設備緩衝區若限制位元組,不能只用中文字數估算。

六、中文字串也能切片與搜尋

guide = "白馬、烏龜、姻緣花"

items = guide.split("、")
print(items)
print("烏龜" in guide)
print(guide.replace("、", "|"))

for number, item in enumerate(items, start=1):
    print(f"{number}. {item}")

Python 的字串操作對中文同樣適用。但使用者看到的一個圖形,可能由多個 Unicode 碼位組成,因此「畫面上的字數」不一定永遠等於 len()。

七、看起來一樣,電腦卻判斷不同

帶重音字母可以是一個預組字元,也可以由基本字母加組合符號構成。兩者畫面相似,但直接比較可能不同。

import unicodedata

text_a = "Tâi-gí"
text_b = "Ta\u0302i-gi\u0301"

print(text_a, text_b)
print(text_a == text_b)

normalized_a = unicodedata.normalize("NFC", text_a)
normalized_b = unicodedata.normalize("NFC", text_b)
print(normalized_a == normalized_b)

搜尋、比對與去除重複資料前,可統一使用 NFC。正規化改變的是字元表示方式,不會自動修正拼字或翻譯。

八、清理訪談轉文字資料

語音轉文字結果常混入全形空白、換行與前後空白。先做最小幅度清理,並保留原始訪談版本。

import unicodedata


def clean_transcript(text):
    text = unicodedata.normalize("NFC", text)
    text = text.replace("\u3000", " ")
    lines = [line.strip() for line in text.splitlines()]
    return "\n".join(line for line in lines if line)


raw_text = " 阿嬤講:姻緣花開矣。\n\n  這是庄頭的記持。 "
print(clean_transcript(raw_text))
口述歷史倫理:不要因為「清理文字」而改寫長輩原意。建議同時保存原始錄音、逐字稿、修訂稿、講述者確認狀態及公開授權範圍。

九、讀寫 UTF-8 文字檔

開啟文字檔時明確指定 encoding="utf-8",可降低不同作業系統間的差異。

from pathlib import Path

path = Path("shuijing_guide.txt")
story = "水井三寶:白馬、烏龜、姻緣花\n"

path.write_text(story, encoding="utf-8")
loaded_story = path.read_text(encoding="utf-8")

print(loaded_story)

若來源是 Windows 軟體匯出的 UTF-8 BOM 檔案,可視情況以 utf-8-sig 讀取;但應先確認來源格式,不要輪流猜編碼直到「看似正常」。

十、亂碼是怎麼發生的?

亂碼通常不是文字「壞掉」,而是寫入與讀取使用不同編碼。以下故意用錯誤方式示範:

original = "水井村"
utf8_bytes = original.encode("utf-8")

wrong_text = utf8_bytes.decode("latin-1")
print(wrong_text)

recovered = wrong_text.encode("latin-1").decode("utf-8")
print(recovered)

這個還原方法只適用於特定「UTF-8 位元組被誤當 Latin-1」的情況。真實資料若已被多次轉碼或遺失位元組,可能無法完全恢復,應優先找回原始檔。

十一、不要隨便忽略解碼錯誤

data = b"guide:\xff\xfe"

try:
    text = data.decode("utf-8")
except UnicodeDecodeError as error:
    print("無法以 UTF-8 解碼:", error)
    text = None

print(text)

errors="ignore" 會直接丟掉無法解碼的內容,可能讓地方名稱或訪談語句悄悄缺字。資料保存工作應記錄錯誤並回查來源;只有明確了解後果時才使用替代策略。

十二、完成多語導覽資料清理工具

import unicodedata


def normalize_text(value):
    if not isinstance(value, str):
        raise TypeError("導覽文字必須是字串")
    value = unicodedata.normalize("NFC", value)
    return " ".join(value.split())


def clean_guide_record(record):
    required = ("主題", "語言", "內容")
    missing = [key for key in required if key not in record]
    if missing:
        raise KeyError("、".join(missing))

    return {
        "主題": normalize_text(record["主題"]),
        "語言": normalize_text(record["語言"]),
        "內容": normalize_text(record["內容"]),
    }


records = [
    {"主題": "白馬", "語言": "華語", "內容": " 水井村地方故事 "},
    {"主題": "姻緣花", "語言": "台語", "內容": "庄頭的記持"},
]

for record in records:
    print(clean_guide_record(record))

十三、常見問題檢查表

現象可能原因建議
中文字變成問號或亂碼編碼不一致確認原始編碼及讀寫設定
相同臺羅詞搜尋不到組合字元表示不同比對前統一 NFC
上傳設備後文字被截斷把字數誤當位元組數以指定編碼後的 bytes 計算
檔案開頭多出奇怪符號UTF-8 BOM確認後使用 utf-8-sig
忽略錯誤後地方名稱缺字使用 errors="ignore"保留錯誤並回查原始資料

十四、USR 實作挑戰

挑戰 A|水井三寶碼位卡
列出「白馬、烏龜、姻緣花」每個字元的 Unicode 碼位與 UTF-8 位元組數,觀察標點與中文字的差異。
挑戰 B|台語逐字稿正規化
對已獲同意的教學逐字稿進行 NFC 與空白清理,逐行比較原始版和清理版,請講述者確認沒有改變原意。
挑戰 C|跨裝置測試
將 UTF-8 導覽檔分別在 Thonny、Colab、手機與網頁開啟,記錄顯示差異與解決方式。

十五、與生成式 AI 協作

請擔任 Python Unicode 與地方語言資料助教。
請檢查我的多語導覽文字處理程式:
1. 分辨 str 與 bytes 的使用是否正確;
2. 檔案讀寫必須明確指定 UTF-8;
3. 搜尋比對前使用 NFC 正規化;
4. 不可用 errors="ignore" 默默刪字;
5. 保留原始逐字稿,不可擅自改寫地方語意;
6. 說明如何測試中文、臺羅與表情符號。
請先指出風險,再提供修正版。
本篇小結
Unicode 為字元建立共同編號,UTF-8 則把這些編號轉成位元組。只要清楚區分 str 與 bytes、統一編碼並保留原始資料,多語地方故事就能更可靠地跨平台流動。

2026年8月30日 星期日

Python一下:讓水井三寶開口說話——input、print與型別轉換

Python一下|第2篇

讓水井三寶開口說話——input、print與型別轉換

《從風土資料到智慧生活的程式設計》——讓程式從單向展示,變成會聽、會回答的地方導覽員。

本篇學習目標
  • 使用print()輸出文字、變數及多筆資料。
  • 使用input()接收使用者輸入。
  • 理解input()取得的資料一律是字串。
  • 使用int()、float()與str()進行型別轉換。
  • 完成一個「水井三寶互動導覽」小程式。

一、從會顯示文字,到會和人互動

上一篇,我們在Python、Thonny或Google Colab中執行第一支程式。當時程式只會把預先寫好的內容顯示出來。這一次,我們要讓程式先詢問使用者,再依照回答呈現結果。

這種「輸入—處理—輸出」的流程,是智慧導覽、問卷、資料登錄、感測資料處理及AI系統的共同基礎。

輸入 Input

接收使用者輸入的姓名、答案或數值。

處理 Process

儲存、轉換、計算或判斷取得的資料。

輸出 Output

把處理結果顯示給使用者。

二、使用print()讓Python說話

print()是Python最常用的輸出函式,可以顯示文字、數字及變數內容。

範例1:輸出文字

print("歡迎來到雲林縣口湖鄉水井村!")
print("今天要介紹水井三寶。")
歡迎來到雲林縣口湖鄉水井村! 今天要介紹水井三寶。

範例2:同時輸出文字與變數

place = "水井村"
treasure = "姻緣花"

print("地方:", place)
print("地方特色:", treasure)

逗號可以讓print()一次輸出多個項目;Python通常會自動在項目之間加入空白。

範例3:使用f-string排出自然句子

place = "水井村"
treasure = "白馬"

print(f"歡迎來到{place},今天要認識的是{treasure}。")
f-string:在字串前加上f,就能把大括號中的變數放進句子。這種寫法清楚、好讀,後續教材會經常使用。

print()常用參數

參數作用範例
sep指定多個輸出項目之間的分隔符號print("白馬", "烏龜", "姻緣花", sep="、")
end指定輸出結束後使用的字元,預設為換行print("水井村", end="→")
print("白馬", "烏龜", "姻緣花", sep="、")
print("地方故事", end=" → ")
print("數位導覽")

三、使用input()讓Python聽見回答

input()會顯示提示文字,暫停程式並等待使用者輸入;按下Enter後,輸入內容才會交給程式。

name = input("請問你叫什麼名字?")
print(f"{name},歡迎你來認識水井村!")
操作提醒:在Thonny執行時,請到下方Shell輸入答案;在Google Colab執行時,輸入框會出現在程式碼儲存格下方。

把輸入內容存入變數

等號左邊的name是變數,負責記住使用者輸入的內容:

favorite = input("水井三寶中,你最想認識哪一個?")
print(f"你選擇了:{favorite}")

四、input()取得的內容都是字串

即使使用者輸入28,input()取得的仍是文字"28",而不是可以直接計算的整數。

temperature = input("請輸入目前水溫:")

print(temperature)
print(type(temperature))

type()可用來查看資料型別,執行結果會顯示<class 'str'>。

為什麼沒有轉換就不能正確計算?

temperature = input("請輸入目前水溫:")
tomorrow = temperature + 1

上例會產生錯誤,因為字串不能直接和整數相加。必須先把輸入內容轉成數值。

五、int()、float()與str()型別轉換

函式轉換結果適合資料範例
int()整數人數、數量、次數int("25")變成25
float()浮點數水溫、重量、價格、溶氧float("28.5")變成28.5
str()字串把數值轉成可串接的文字str(28.5)變成"28.5"

範例:計算參訪總人數

students = int(input("請輸入學生人數:"))
teachers = int(input("請輸入教師人數:"))
total = students + teachers

print(f"本次USR參訪共有{total}人。")

範例:輸入智慧養殖水溫

water_temperature = float(input("請輸入養殖池水溫:"))
adjusted_temperature = water_temperature + 0.5

print(f"目前水溫為{water_temperature:.1f}°C。")
print(f"模擬升高0.5°C後為{adjusted_temperature:.1f}°C。")

:.1f表示以小數點後一位顯示浮點數。

六、完成「水井三寶互動導覽」

現在把輸入、變數、型別轉換與輸出組合成一支完整程式:

print("=== 水井三寶互動導覽 ===")

name = input("請輸入你的名字:")
age = int(input("請輸入你的年齡:"))
favorite = input("白馬、烏龜、姻緣花,你最想認識哪一個?")

print()
print(f"{name},歡迎來到水井村!")
print(f"你今年{age}歲,最想認識的是{favorite}。")
print("地方故事因為有人聆聽,才能繼續流傳。")
=== 水井三寶互動導覽 === 請輸入你的名字:小敏 請輸入你的年齡:20 白馬、烏龜、姻緣花,你最想認識哪一個?姻緣花 小敏,歡迎來到水井村! 你今年20歲,最想認識的是姻緣花。 地方故事因為有人聆聽,才能繼續流傳。

七、常見錯誤與除錯

錯誤情況原因修正方式
SyntaxError引號、括號或冒號未成對檢查中英文標點及左右括號
NameError變數拼字不同,或尚未建立確認大小寫及變數名稱完全一致
TypeError字串與數字直接相加先使用int()、float()或str()轉換
ValueError把「二十八」或空白轉為數值數值欄位輸入阿拉伯數字;進階時加入例外處理
程式一直等待input()正在等待使用者輸入在Shell或Colab輸入框輸入資料並按Enter
資料倫理提醒:課堂練習不要要求學生輸入身分證號、電話、住址等不必要個資。USR場域蒐集長者或居民資料前,應先說明用途、取得同意並妥善保存。

八、漸進式練習

基礎練習:地方名片

輸入社區名稱、代表農產與地方特色,再輸出一張文字名片。

community = input("社區名稱:")
product = input("代表農產或漁產:")
feature = input("地方特色:")

print("------ 地方名片 ------")
print(f"社區:{community}")
print(f"農漁產:{product}")
print(f"特色:{feature}")

進階練習:養殖用電估算

輸入設備功率與每天運轉時數,估算一天用電量:

power = float(input("設備功率(kW):"))
hours = float(input("每天運轉時數:"))
energy = power * hours

print(f"每日估計用電量:{energy:.2f} kWh")

USR挑戰:長者故事基本資料

設計一支程式,依序詢問:

  1. 故事主題。
  2. 發生地點。
  3. 最懷念的味道、聲音或情景。
  4. 是否同意將整理後的故事公開展示。

最後以清楚的格式輸出。請不要增加受訪者沒有說過的內容,也不要蒐集與作品無關的個人資料。

九、學習檢核

  1. print()與input()分別負責什麼工作?
  2. 為什麼input()輸入的28不能直接加上1?
  3. 人數、水溫與姓名分別適合轉成哪一種資料型別?
  4. print(a, b, sep="、")中的sep有什麼作用?
  5. USR場域進行互動輸入時,為什麼要注意個資與知情同意?

十、AI協作提示詞

請擔任Python初學者助教。
我要設計一支「地方文化互動介紹」程式,
目前只學過print、input、變數、int、float、str與f-string。

請先問我想介紹的社區、農漁產或地方故事,
再依我提供的內容產生程式。
不要使用if、for、函式或尚未學過的語法,
也不要增加我沒有提供的人物與地方資料。
最後逐行說明程式。

十一、本篇小結

print()讓程式把資訊傳達給人,input()讓程式聽見人的回答,型別轉換則讓輸入的文字能正確參與計算。三者組合起來,Python就不再只是顯示固定內容,而能成為簡單的地方導覽與資料登錄工具。

程式會說話,也要先學會聆聽;科技進入USR場域,也應從理解地方與尊重居民開始。

2026年8月26日 星期三

[水井村USR] 從遠端控制走向自主維運:異常告警、雲端診斷與安全維護模式實測

水井三寶故事機|V1.0-14

從遠端控制走向自主維運:異常告警、雲端診斷與安全維護模式實測

HUB 8735 Ultra+JQ6500+Django,不只會播放地方故事,更能自行回報健康、接受安全維護命令,並在展場端保留必要的實體停止能力。

100設備健康分數
GOOD健康等級
-27 dBmWi-Fi RSSI
0離線待送事件

前一版 V1.0-13 已完成安全遠端命令、執行結果回報與逾時保護;V1.0-14 再向真正的展場自主維運前進。這次測試不是只確認「雲端按鈕能不能動」,而是驗證設備能否回答三個更重要的問題:設備現在健康嗎?需要維護時能否安全鎖定?維護結束後能否立即恢復服務?

一、V1.0-14增加了什麼?

功能用途展場價值
設備健康分數依網路、播放器、事件佇列、復原次數與告警計算健康狀態管理者不必到現場逐台檢查
異常告警將故障或維護狀態同步到Django儀表板快速辨認需要處理的設備
RUN_DIAGNOSTIC由雲端要求設備產生完整診斷快照遠端取得韌體、RSSI、按鈕、BUSY及JQ6500狀態
MAINTENANCE_ON進入維護模式並阻擋一般播放與音量命令避免維修時被觀眾誤觸啟動
MAINTENANCE_OFF解除維護鎖定,恢復故事播放服務完成維護後不必重新燒錄程式
復原頻率限制限制單位時間內的播放器自動復原次數防止故障設備陷入無限重置

二、雲端儀表板已成為展場維運中心

Django儀表板同時呈現展覽互動成果與設備健康資訊。管理者可以看見故事啟動、完整播放率、設備在線狀態、健康分數、健康等級、告警數、復原層級、最新診斷,以及最近一筆遠端命令結果。


圖:SHUIJING-002在線,健康分數100、等級GOOD、告警0;下方可直接建立V1.0-14自主維運命令。
發表圖片提醒:ChatGPT工作區圖片不是公開網址。請在Blogger編輯器上傳本次儀表板截圖,取得Blogger圖片網址後,把本文唯一的 BLOGGER_IMAGE_URL 換掉,圖片就能穩定顯示。

三、第一階段:遠端完整診斷成功

我們先在Django後台建立 RUN_DIAGNOSTIC 命令。設備透過Heartbeat領取命令後,立即產生診斷快照,再把執行結果送回雲端。

[REMOTE] Heartbeat received RUN_DIAGNOSTIC
[DIAGNOSTIC] fw=V1.0-14,health=100/GOOD,state=IDLE,
track=0,vol=16,rssi=-27,queue=0,jq=0,busy=0,
btn=111111,alert=NONE
[REMOTE] Executed RUN_DIAGNOSTIC | result queued SUCCESS
[REMOTE] Result ACK SUCCESS

診斷資料如何解讀?

health=100/GOOD設備健康,沒有需要立即處理的異常。
state=IDLE故事機處於待機狀態。
vol=16JQ6500目前音量為16。
rssi=-27Wi-Fi訊號非常良好。
queue=0沒有尚未同步的離線事件。
jq=0本次運作尚未發生JQ6500復原。
busy=0播放器沒有正在播放。
btn=111111六個按鈕皆為釋放狀態,沒有腳位異常拉低。
alert=NONE目前沒有作用中的設備告警。

真正關鍵的不是看到 Executed,而是最後收到 Result ACK SUCCESS。這代表「雲端建立命令、設備領取、設備執行、結果回送、伺服器確認」五個環節全部完成。

四、第二階段:從雲端開啟維護模式

Django 建立命令
Heartbeat 領取命令
設備啟用鎖定
結果 ACK 確認
[REMOTE] Heartbeat received MAINTENANCE_ON
[MAINTENANCE] Enabled by cloud
[REMOTE] Executed MAINTENANCE_ON | result queued SUCCESS
[REMOTE] Result ACK SUCCESS

維護模式不是關閉整台設備,而是選擇性鎖定具有風險的操作。故事播放及音量調整會被拒絕,但停止功能仍保留,讓現場人員遇到播放器異常時仍能立即處理。

維護模式中的操作處理結果設計理由
故事按鈕拒絕防止維修時意外啟動音訊
Web故事播放拒絕避免遠端使用者干擾現場維護
音量+/-拒絕維持檢修時的固定條件
停止按鈕保留確保現場仍可立即停止播放器
遠端診斷保留維護期間仍需觀察設備狀態
解除維護保留讓雲端可以安全恢復服務

五、實體按鈕真的被鎖住了嗎?

進入維護模式後,我們分別按下GPIO11烏龜故事與GPIO10白馬故事,系統確實讀到按鍵,但沒有啟動JQ6500:

[BUTTON] Pressed GPIO11
[MAINTENANCE] Story command rejected
[BUTTON] Pressed GPIO10
[MAINTENANCE] Story command rejected

接著按下GPIO20停止鍵,系統仍然接受停止操作;因為當時播放器原本就在待機,所以正確回報:

[BUTTON] Pressed GPIO20
[STOP] Ignored: already idle

這個結果十分重要:維護模式沒有讓整個按鍵掃描停止,而是由命令層判斷哪些動作可以執行。設備仍持續掃描實體輸入,因此不會重演早期版本「網路工作時按鈕失去反應」的問題。

六、解除維護後立即恢復播放

[REMOTE] Heartbeat received MAINTENANCE_OFF
[MAINTENANCE] Disabled by cloud
[REMOTE] Executed MAINTENANCE_OFF | result queued SUCCESS
[REMOTE] Result ACK SUCCESS

解除維護後再次按下GPIO10,白馬故事成功播放,雲端事件也正常寫入本機佇列:

[BUTTON] Pressed GPIO10
[CLOUD QUEUE] + SHUIJING-002-G68F044D7-0000000033
| STORY_START | pending=1
[JQ] Play track 1
[STATE] STORY_PLAYING | 白馬故事
[BUSY] 0 -> 1 | idle=0

這表示解除維護不需要重新開機,也不需要重新燒錄程式;實體按鈕、JQ6500播放、BUSY狀態與雲端事件佇列一起恢復正常。

七、為什麼一直看到NUL filtered=1?

[HEARTBEAT HTTP] NUL filtered=1
[HEARTBEAT] Accepted HTTP 200

這不是錯誤。實測發現Ameba SSL接收的HTTP封包偶爾會夾帶一個 0x00 NUL位元組。早期版本會因此無法解析HTTP狀態列或JSON;現在程式會先過濾NUL,再解析完整回應。

判讀原則:只要NUL過濾後緊接著出現 Accepted HTTP 200,就代表封包已修正並成功處理。這一行是防護機制的工作紀錄,不是通訊失敗。

八、這次完整測試結果

  • STA網路連線與HTTPS Heartbeat正常。
  • Django成功送出RUN_DIAGNOSTIC命令。
  • 設備回傳完整健康診斷快照。
  • 命令執行結果收到雲端ACK。
  • MAINTENANCE_ON成功啟用。
  • 維護期間實體故事按鈕受到阻擋。
  • 維護期間停止按鈕仍可使用。
  • MAINTENANCE_OFF成功解除鎖定。
  • 解除維護後白馬故事成功播放。
  • STORY_START事件成功進入離線保護佇列。
  • JQ6500 BUSY腳位正確由0切換至1。
  • 設備健康分數100、等級GOOD、告警NONE。
實測結論:V1.0-14核心功能全部通過。

水井三寶故事機已從「可以被遠端操作的播放器」,進一步成為「可以自我回報、接受安全維護、保留現場控制並完成雲端稽核」的展場智慧設備。

九、這次最寶貴的工程經驗

展場設備的可靠性,不只取決於功能多不多,而是發生異常時能不能被看見、被限制、被診斷、被恢復。V1.0-14建立的健康分數、異常告警、維護鎖定、遠端診斷與結果ACK,形成一條完整的維運證據鏈。

感知設備健康
雲端派送命令
邊緣安全執行
回報並完成稽核

這也讓地方故事、樹藝作品與智慧展覽不再只是一次性的展示,而是可以長期運作、跨場域部署並由遠端團隊共同維護的數位文化服務。

測試平台:HUB 8735 Ultra、JQ6500、實體按鈕、LED、STA Wi-Fi、HTTPS、Django/PythonAnywhere。版本:V1.0-14「展場自主維運+異常告警+遠端診斷版」。