<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-4877487320781767952</id><updated>2026-05-05T00:03:13.242+08:00</updated><category term="生活"/><category term="Python"/><category term="網購"/><category term="好書"/><category term="物聯網"/><category term="好站"/><category term="網頁技術"/><category term="樹莓派"/><category term="電腦"/><category term="還書"/><category term="機器學習"/><category term="ESP8266"/><category term="Raspberry Pi"/><category term="Arduino"/><category term="評論"/><category term="MicroPython"/><category term="主機"/><category term="ESP32"/><category term="理財"/><category term="能源"/><category term="jQuery"/><category term="人工智慧"/><category term="PHP"/><category term="食譜"/><category term="Java"/><category term="Javascript"/><category term="網路爬蟲"/><category term="音樂"/><category term="量化投資"/><category term="旅行"/><category term="小狐狸"/><category term="Linux"/><category term="歷史"/><category term="網路"/><category term="NLP"/><category term="其他"/><category term="LLM"/><category term="EasyUI"/><category term="語言學"/><category term="醫藥"/><category term="Streamlit"/><category term="工作"/><category term="資料科學"/><category term="雲端"/><category term="手機"/><category term="索引"/><category term="HTML5"/><category term="Google"/><category term="R 語言"/><category term="GAE"/><category term="硬體"/><category term="Django"/><category term="自動化"/><category term="OpenAI"/><category term="jQuery UI"/><category term="電子零件"/><category term="App"/><category term="C 語言"/><category term="Mapleboard"/><category term="PyTorch"/><category term="Ubuntu"/><category term="資料視覺化"/><category term="Bootstrap"/><category term="創客"/><category term="飛控"/><category term="Windows"/><category term="jQuery Mobile"/><category term="單晶片"/><category term="tkinter"/><category term="戲劇"/><category term="AIGC"/><category term="Flask"/><category term="ChatGPT"/><category term="數學"/><category term="木工"/><category term="Praat"/><category term="ExtJS"/><category term="農業"/><category term="TensorFlow"/><category term="修行"/><category term="小狐狸生態"/><category term="科學"/><category term="養生"/><category term="Telegram"/><category term="WordPress"/><category term="無人機"/><category term="Line"/><category term="♥"/><category term="♥♥♥"/><category term="專案"/><category term="Excel"/><category term="電子學"/><category term="Fintech"/><category term="p5.js"/><category term="筆電"/><category term="AWS"/><category term="Stable Diffusion"/><category term="gradio"/><category term="教育"/><category term="邏輯設計"/><category term="Gemini"/><category term="SQL"/><category term="架站"/><category term="AutoIt"/><category term="Node.js"/><category term="線上工具"/><category term="電腦技術"/><category term="Android"/><category term="C++"/><category term="Julia"/><category term="Matplotlib"/><category term="vibe coding"/><category term="保險"/><category term="旅遊"/><category term="英文"/><category term="Azure"/><category term="Gemini CLI"/><category term="Jetson"/><category term="LangChain"/><category term="LoRa"/><category term="ASP"/><category term="Banana Pi"/><category term="CSS"/><category term="Git"/><category term="LINE Bot"/><category term="MacOS"/><category term="物理學"/><category term="Chatbot"/><category term="IC 設計"/><category term="♥♥"/><category term="多媒體"/><category term="新軟體"/><category term="統計學"/><category term="計算語言學"/><category term="語音辨識"/><category term="資安"/><category term="韓劇"/><category term="GAS"/><category term="Pandas"/><category term="日文"/><category term="深度學習"/><category term="演算法"/><category term="生活雜記"/><category term="讀書劄記"/><category term="遠端桌面"/><category term="Colab"/><category term="GitHub"/><category term="借書"/><category term="軟體開發"/><category term="Blynk"/><category term="Bokeh"/><category term="Micro:bit"/><category term="OpenCV"/><category term="Vue"/><category term="大數據"/><category term="好文"/><category term="影像處理"/><category term="機器人"/><category term="自作專案"/><category term="資料庫"/><category term="D3.js"/><category term="GCP"/><category term="Lua"/><category term="Nginx"/><category term="Plotly"/><category term="Swift"/><category term="TCP/IP"/><category term="占星術"/><category term="書籤"/><category term="量子計算"/><category term="Altair"/><category term="NLTK"/><category term="WSH"/><category term="信號處理"/><category term="語音技術"/><category term="Appfog"/><category term="Claude Code"/><category term="DSP"/><category term="GPU"/><category term="SDD"/><category term="SciPy"/><category term="Seaborn"/><category term="SpaCy"/><category term="scikit-learn"/><category term="區塊鏈"/><category term="相術"/><category term="系統"/><category term="紫微斗數"/><category term="統計"/><category term="AMB82"/><category term="Corona"/><category term="Firebase"/><category term="Hugging Face"/><category term="Joomla"/><category term="MQTT"/><category term="MongoDB"/><category term="Node-RED"/><category term="OpenSpec"/><category term="SQLite"/><category term="Transformer"/><category term="UAV"/><category term="WebGL"/><category term="tensorflow.js"/><category term="動畫"/><category term="排版"/><category term="繪圖"/><category term="虛擬幣"/><category term="通訊"/><category term="Chart.js"/><category term="Cheatsheet"/><category term="Highcharts"/><category term="JSP"/><category term="Keras"/><category term="Mathematics"/><category term="Midjourney"/><category term="Numpy"/><category term="RAG"/><category term="React"/><category term="STM32"/><category term="Verilog"/><category term="Wireshark"/><category term="ml5.js"/><category term="reflex"/><category term="備忘"/><category term="影像辨識"/><category term="比特幣"/><category term="股市"/><category term="藝術"/><category term="API"/><category term="AngularJS"/><category term="Anime"/><category term="Big Data"/><category term="CKeditor"/><category term="Canva"/><category term="DALL-E"/><category term="Data Mining"/><category term="FPGA"/><category term="Gimini"/><category term="Go"/><category term="Home Assistant"/><category term="JavaFX"/><category term="Kali"/><category term="Kotlin"/><category term="Make"/><category term="Masonite"/><category term="NoSQL"/><category term="NotebookLM"/><category term="Notion"/><category term="OrangePi"/><category term="Ruby"/><category term="Scrapy"/><category term="Three.js"/><category term="TinyMCE"/><category term="TinyML"/><category term="VHDL"/><category term="VoIP"/><category term="YOLO"/><category term="brython"/><category term="bubble.io"/><category term="jqPlot"/><category term="mermaid.js"/><category term="no-code"/><category term="中醫"/><category term="人工"/><category term="保養品"/><category term="儀錶板"/><category term="八字學"/><category term="出版"/><category term="園藝"/><category term="控制系統"/><category term="新產品"/><category term="新知識"/><category term="易經"/><category term="智慧音箱"/><category term="監控"/><category term="線上學習"/><category term="線性代數"/><category term="論文"/><category term="運動"/><category term="電路學"/><category term="韓文"/><title type='text'>小狐狸事務所</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default?start-index=26&amp;max-results=25'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>6299</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-819793978405298042</id><published>2026-05-04T21:42:00.002+08:00</published><updated>2026-05-04T21:42:12.342+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="保險"/><title type='text'>菁菁 MAZDA MX5 車險續約</title><content type='html'>&lt;div&gt;菁菁的小跑車 MX5 車險 5/8 日到期, 那天剛好從沖繩回來, 無法處理續約, 所以提前今天上網續保, 今年是第二年, 參考去年保險內容 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/05/mazda-mx-5.html&quot; target=&quot;_blank&quot;&gt;MAZDA MX-5 汽車險&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;今年強制+任意險比去年稍微便宜 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ5KUi7j-deFgCLr2y8nBaZhR3kcF6ijxvWeczvK3NdQbajqX2Cl6xFJutTqm0dYchH2WHOz4EZ7vhmxz4fAnsFE4n5FrpHHQWMPAuqpPbC3fS4D6fYHfSjxaF6bGsBVTvzpJn1q1hNdSqz6YP7VGpNMogzkIcfCjJY7I-ceSQYIjWdWJWo6qZE_aSggMq/s1816/%E8%8F%81%E8%8F%81-%E9%A6%AC%E8%87%AA%E9%81%94%E8%BB%8A%E9%9A%AA-20260508.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;667&quot; data-original-width=&quot;1816&quot; height=&quot;118&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ5KUi7j-deFgCLr2y8nBaZhR3kcF6ijxvWeczvK3NdQbajqX2Cl6xFJutTqm0dYchH2WHOz4EZ7vhmxz4fAnsFE4n5FrpHHQWMPAuqpPbC3fS4D6fYHfSjxaF6bGsBVTvzpJn1q1hNdSqz6YP7VGpNMogzkIcfCjJY7I-ceSQYIjWdWJWo6qZE_aSggMq/s320/%E8%8F%81%E8%8F%81-%E9%A6%AC%E8%87%AA%E9%81%94%E8%BB%8A%E9%9A%AA-20260508.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/819793978405298042/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/819793978405298042' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/819793978405298042'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/819793978405298042'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/mazda-mx5.html' title='菁菁 MAZDA MX5 車險續約'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ5KUi7j-deFgCLr2y8nBaZhR3kcF6ijxvWeczvK3NdQbajqX2Cl6xFJutTqm0dYchH2WHOz4EZ7vhmxz4fAnsFE4n5FrpHHQWMPAuqpPbC3fS4D6fYHfSjxaF6bGsBVTvzpJn1q1hNdSqz6YP7VGpNMogzkIcfCjJY7I-ceSQYIjWdWJWo6qZE_aSggMq/s72-c/%E8%8F%81%E8%8F%81-%E9%A6%AC%E8%87%AA%E9%81%94%E8%BB%8A%E9%9A%AA-20260508.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-7064142661948070937</id><published>2026-05-04T21:05:00.003+08:00</published><updated>2026-05-04T21:44:05.728+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="旅行"/><title type='text'>機票英文名字拼錯問題</title><content type='html'>今天做行前檢查, 發現早上水某寄的登機證中, 爸的英文名字拼音與護照有一個字母不正確, 原因是水某訂機票時我傳遞的資料打錯了, 趕緊撥打華航客服 02-4129000, 告知有此情況, 客服先叫我到華航網站先取消爸的報到單, 然後寄一封確認信給我, 要求填寫錯誤拚寫與正確拼寫, 並附上護照內頁照片佐證, 叮囑我若一小時後仍未收到已更正回函, 要再次撥打客服催促, 因客服員有限, 務必在線等候直到有人接聽.&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://www.china-airlines.com/tw/zh/prepare-for-the-fly/support/faq&quot; target=&quot;_blank&quot;&gt;華航客服 FAQ&lt;/a&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://www.china-airlines.com/tw/zh/prepare-for-the-fly/information/check-in/online-check-in&quot; target=&quot;_blank&quot;&gt;華航網路報到網頁&lt;/a&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;到中午還沒收到完成回覆, 我再次打電話去詢問處理進度, 這回真的等了十分鐘才輪到, 客服表示已要求加速進行, 終於在下午三點收到更正回函, 上官網輸入機票號碼與正確姓名拼字, 果然就能進入機票與登機資訊網頁, 但無法線上寄發新的登機證, 說 &quot;說請洽機場櫃檯領取登機證&quot;, 所以明天要早一點到櫃檯報到, 說明有更正姓名拼寫錯誤, 請其人工列印新登機證. 所以出國旅行前務必檢查旅行文件, 最好是列印出來做為備份, 也較能檢查出錯誤, 不要小看一個字母之差, 這可能會讓旅伴到機場才發現無法同行. 即使旅伴不是親人, 最好也能互相檢查一下較妥當.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/7064142661948070937/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/7064142661948070937' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7064142661948070937'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7064142661948070937'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/blog-post.html' title='機票英文名字拼錯問題'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1634020131572058683</id><published>2026-05-03T23:23:00.005+08:00</published><updated>2026-05-03T23:23:40.879+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="生活"/><title type='text'>2026 年第 17 周記事</title><content type='html'>&lt;div&gt;本周五勞動節連假只上四天班, 我週三沒有回鄉下, 因為周四下班就要回去了. 以往勞動節都去加班, 今年不加了, 即使可加也不想去了, 邁入耳順之年後更想要的是放假. 周六老同學大帥與仲仔, 風大師來訪, 因為之前我都下廚在加吃便飯, 坐下來聊天時間不甚多, 這回剛好峰大師四月底退休, 預定了菸樓坊一桌請大伙去吃飯, 但到了餐廳才發現遊覽車一車車進來, 不知要等多久才有空位, 大帥建議取消, 去人少的地方, 我想起鎮上的八分飽, 到餐廳時下車問還好有空桌, 於是便在此用餐. 我看以後還是自己下廚吧, 外面餐廳假日都是人.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;連假三天我都在家測試程式, 一口氣把 Plotly 與 Bokeh 畫 K 線圖的方法搞定. 雖然 AI 時代漸漸都不自己寫程式了, 小白或非科班就算呃, 但做為工程師還是得看得懂 AI 在寫甚麼吧?&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;週五舅媽叫小舅帶來涼麵與木瓜粄, 加上峰大師周六帶來一隻烤雞, 所以連假三天都在消耗這些糧食, 甚至今天周日我也沒上市場買菜, 因為下周要去沖繩, 食材放太久也不行.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下午要去菜園移植韭菜時, 經過車庫看見毛小妹與它的一隻小貓躺在一起睡覺, 看似小貓吃完奶就地睡著了, 那畫面看來好幸福, 趕緊進屋拿手機拍了下來 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-NTSuDvtrHTbpju0So5jHbZcwF6CuyguC2-p7Nq81ZcNZQTKQTq3yKuPdnnr3MpqA92RFQ3rxzx5TRzDhrylYcOnNYHPFOHowmipvz_hUcIM-mI-BS8nilhBdb9I5xghHwfTHBzJvjmGTSsbckt6SZnfNNBkFFCwDAa5QufNZngN1hhyphenhyphenaCTpJqqwJ_gw1/s1477/%E6%AF%9B%E5%B0%8F%E5%A6%B9%E8%88%87%E5%AE%83%E7%AC%AC%E4%BA%8C%E8%83%8E%E7%9A%84%E5%B0%8F%E8%B2%93.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1108&quot; data-original-width=&quot;1477&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-NTSuDvtrHTbpju0So5jHbZcwF6CuyguC2-p7Nq81ZcNZQTKQTq3yKuPdnnr3MpqA92RFQ3rxzx5TRzDhrylYcOnNYHPFOHowmipvz_hUcIM-mI-BS8nilhBdb9I5xghHwfTHBzJvjmGTSsbckt6SZnfNNBkFFCwDAa5QufNZngN1hhyphenhyphenaCTpJqqwJ_gw1/s320/%E6%AF%9B%E5%B0%8F%E5%A6%B9%E8%88%87%E5%AE%83%E7%AC%AC%E4%BA%8C%E8%83%8E%E7%9A%84%E5%B0%8F%E8%B2%93.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;但晚上爸在曬穀場騎腳踏車時, 一隻小貓跑到路對面去, 在橫越馬路跑回來時被一台機車撞到, 那人也沒停下來逕自騎走了, 爸把小貓報進來, 我看口鼻皆流血已無呼吸, 因它們四兄妹毛色很像, 不確定是否為下午那隻. 我家緊鄰馬路邊, 路雖不大但為交通要道, 來往車子都開太快, 打算買一個太陽能警示燈安裝在路旁電線桿, 希望提醒減速慢行.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1634020131572058683/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1634020131572058683' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1634020131572058683'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1634020131572058683'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/2026-17.html' title='2026 年第 17 周記事'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-NTSuDvtrHTbpju0So5jHbZcwF6CuyguC2-p7Nq81ZcNZQTKQTq3yKuPdnnr3MpqA92RFQ3rxzx5TRzDhrylYcOnNYHPFOHowmipvz_hUcIM-mI-BS8nilhBdb9I5xghHwfTHBzJvjmGTSsbckt6SZnfNNBkFFCwDAa5QufNZngN1hhyphenhyphenaCTpJqqwJ_gw1/s72-c/%E6%AF%9B%E5%B0%8F%E5%A6%B9%E8%88%87%E5%AE%83%E7%AC%AC%E4%BA%8C%E8%83%8E%E7%9A%84%E5%B0%8F%E8%B2%93.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1210608893710327059</id><published>2026-05-03T13:43:00.004+08:00</published><updated>2026-05-03T16:19:14.671+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Bokeh"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="資料視覺化"/><title type='text'>Python 學習筆記 : 用 bokeh 繪製 K 線圖 (二)</title><content type='html'>&lt;div&gt;Bokeh 的優點是高自由度與高效能, 但其 API 較低階, 故並未內建像 Plotly 的 CandleStick 那樣的類別, 本篇旨在為 Bokey 山寨一個 CandleStick 來簡化 K 線圖的繪製. 為了讓程式碼不至於過度複雜, 以下測試不處理非交易日 K 棒空缺問題.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;本系列全部測試文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python.html&quot; target=&quot;_blank&quot;&gt;Python 資料視覺化學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 用封裝的類別繪製 K 線圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下面範例改寫自前一篇測試的&amp;nbsp;bokeh_candlestick_3.py, 將繪製 K 線圖的邏輯封裝在自訂的 BokehChart 類別裡, 只要呼叫其建構式並傳入 title/width/height 參數建立物件, 然後在主程式中呼叫&amp;nbsp;add_candlestick(df) 即可繪製 K 線圖, 程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_class_1.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;class BokehChart:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;封裝 Bokeh K線圖與技術分析工具的繪圖類別&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def __init__(self, title=&quot;K線圖&quot;, width=800, height=400):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立並初始化畫布&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x_axis_type=&#39;datetime&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=title,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=width,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=height,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, box_zoom, reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 初始化虛線十字游標&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def _add_crosshair(self):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;內部方法：加入虛線十字游標&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; w_span=Span(dimension=&quot;width&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; h_span=Span(dimension=&quot;height&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_candlestick(self, df, date_col=&#39;Date&#39;, open_col=&#39;Open&#39;, high_col=&#39;High&#39;, low_col=&#39;Low&#39;, close_col=&#39;Close&#39;, vol_col=&#39;Volume&#39;):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;對外介面：加入 K 線圖與懸停工具&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 複製資料以避免修改到原始 DataFrame&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data=df.copy()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 定義漲跌顏色 (紅漲綠跌)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(data[open_col], data[close_col])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立資料來源&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; source=ColumnDataSource(data)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 繪製上下影線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig.segment(date_col, high_col, date_col, low_col, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 繪製 K 棒 (12 小時寬度)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width_ms=12 * 60 * 60 * 1000&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig.vbar(date_col, width_ms, open_col, close_col,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 加入互動式懸停工具 (HoverTool)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, f&quot;@{date_col}{{%F}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, f&quot;@{open_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, f&quot;@{close_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, f&quot;@{high_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, f&quot;@{low_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;成交量&quot;, f&quot;@{vol_col}{{0,0}}&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; formatters={f&#39;@{date_col}&#39;: &#39;datetime&#39;}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def show(self):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;顯示圖表&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; show(self.fig)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;# 主程式：使用類別繪圖 (如同 Plotly 般簡潔)&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 1. 下載真實 OHLCV 資料&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 2. 實例化畫布並繪圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart=BokehChart(title=&#39;0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)&#39;, width=800, height=400)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_candlestick(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 3. 顯示圖表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6DnN8Abo1SfBzzrmZC6TQDbkWMGOrh5Ai2t8U3AQIC1flSPNGkXUTUq1JTe8ljMZyVLJZ5NcIDfu4KoXc8vAUqYBjGwu25HgOKAiIBw-hJUjvpUA-p1i0SMD9lfONxMjAtMlfOszo_FAbuA2iJnJ5pNLdP7tDDVjW2v9RQYzPIAB7NUzW0Sy3ihI6lCV4/s1777/bokeh-candlestick-class-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;886&quot; data-original-width=&quot;1777&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6DnN8Abo1SfBzzrmZC6TQDbkWMGOrh5Ai2t8U3AQIC1flSPNGkXUTUq1JTe8ljMZyVLJZ5NcIDfu4KoXc8vAUqYBjGwu25HgOKAiIBw-hJUjvpUA-p1i0SMD9lfONxMjAtMlfOszo_FAbuA2iJnJ5pNLdP7tDDVjW2v9RQYzPIAB7NUzW0Sy3ihI6lCV4/s320/bokeh-candlestick-class-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;5. 用封裝的類別繪製 K 線圖 + 成交量副圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;如果要同時繪製 K 線圖與成交量副圖, 則須匯入 bokeh.layouts.column() 函式進行垂直布局, 讓主圖 (fig_k) 與副圖能共用 X 軸並連動. 程式碼改寫如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_class_2.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter&lt;/div&gt;&lt;div&gt;from bokeh.layouts import column&amp;nbsp; # 引入垂直佈局工具&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;class BokehChart:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;封裝 Bokeh K線圖與成交量子圖的繪圖類別&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 稍微調整初始化參數，允許分別設定主圖與副圖的高度&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def __init__(self, title=&quot;K線圖&quot;, width=800, height_k=400, height_v=150):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 1. 建立主圖 (K線圖)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=title,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=width, height=height_k,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, box_zoom, reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.xaxis.visible=False&amp;nbsp; # 隱藏主圖的 X 軸，讓畫面更緊湊&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 2. 建立副圖 (成交量圖)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=width, height=height_v,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x_range=self.fig_k.x_range,&amp;nbsp; # 【關鍵連動】將副圖的 X 軸範圍綁定為主圖的 X 軸&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, reset&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.y_range.start=0&amp;nbsp; &amp;nbsp; &amp;nbsp;# 確保成交量 Y 軸從 0 開始&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 3. 組合為垂直佈局&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.layout=column(self.fig_k, self.fig_v)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 4. 初始化虛線十字游標 (加在兩張圖上)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(self.fig_k)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(self.fig_v)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def _add_crosshair(self, fig):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;內部方法：為指定的畫布加入虛線十字游標&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; w_span=Span(dimension=&quot;width&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; h_span=Span(dimension=&quot;height&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_candlestick(self, df, date_col=&#39;Date&#39;, open_col=&#39;Open&#39;, high_col=&#39;High&#39;, low_col=&#39;Low&#39;, close_col=&#39;Close&#39;, vol_col=&#39;Volume&#39;):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;對外介面：處理資料並繪製 K 線與成交量&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data=df.copy()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立連續整數序列 (消除假日空缺)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;seq&#39;]=range(len(data))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 定義漲跌顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(data[open_col], data[close_col])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 用 Python 預先處理好 Bokeh 風格的日期字串&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; date_map={}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i, d in enumerate(data[date_col]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label=f&quot;{d.strftime(&#39;%b&#39;)} {d.day}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if i == 0 or d.year != prev_year:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label += f&quot;\n{d.year}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; date_map[str(i)]=label&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=d.year&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; source=ColumnDataSource(data)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width_val=0.8&amp;nbsp; # 因為 X 軸變成了連續整數，寬度直接用比例 0.8&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # --- 繪製主圖 (K線) ---&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.segment(&#39;seq&#39;, high_col, &#39;seq&#39;, low_col, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.vbar(&#39;seq&#39;, width_val, open_col, close_col, fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # --- 繪製副圖 (成交量) ---&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 使用 vbar，top 參數指定為成交量欄位&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.vbar(&#39;seq&#39;, width_val, top=vol_col, fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # --- 設定副圖的 X 軸日期標籤 ---&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; js_code=&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var idx=Math.round(tick).toString();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (date_map[idx] !== undefined) {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return date_map[idx];&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &quot;&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={&#39;date_map&#39;: date_map})&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.xaxis.major_label_orientation=0&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.xaxis.major_label_standoff=15&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # --- 加入互動式懸停工具 ---&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, f&quot;@{date_col}{{%F}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, f&quot;@{open_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, f&quot;@{close_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, f&quot;@{high_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, f&quot;@{low_col}{{0.00}}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;成交量&quot;, f&quot;@{vol_col}{{0,0}}&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; formatters={f&#39;@{date_col}&#39;: &#39;datetime&#39;},&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; mode=&#39;vline&#39;&amp;nbsp; # 【推薦】改為 vline 模式，滑鼠對齊同一個垂直線就會觸發&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_v.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def show(self):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;顯示圖表佈局&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; show(self.layout)&amp;nbsp; # 【修改】現在是顯示整個 layout 而不是單一 fig&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;# 主程式&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 1. 下載實盤 OHLCV 資料&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 2. 建立畫布並繪圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart=BokehChart(title=&#39;0050.TW 台灣50 ETF (含同步成交量)&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_candlestick(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 3. 顯示圖表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, 此例之 BokehChart 類別還封裝了處理非交易日空缺的邏輯, 這讓整個畫布上的 K 棒看起來是連續無空缺的, 結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicU7zTyd31x5m93jBa80Xhh2lSjoQfaW6YBxtyyIzecr2HA6QTEZx6MUiQyiSUVjvCXCudftnzma8KzLvtDYobudjcUHq3V4vXpeZE70yuQwcfk2w2ifxQTn7Tru8eZAPt2bRkLU7hAOmC7MRCd5CfWSp4GBObImBOsmPjdJe7Ojpk_tKX0Ge_F9QPm6RG/s1762/bokeh-candlestick-class-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1161&quot; data-original-width=&quot;1762&quot; height=&quot;211&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicU7zTyd31x5m93jBa80Xhh2lSjoQfaW6YBxtyyIzecr2HA6QTEZx6MUiQyiSUVjvCXCudftnzma8KzLvtDYobudjcUHq3V4vXpeZE70yuQwcfk2w2ifxQTn7Tru8eZAPt2bRkLU7hAOmC7MRCd5CfWSp4GBObImBOsmPjdJe7Ojpk_tKX0Ge_F9QPm6RG/s320/bokeh-candlestick-class-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;6. 用封裝的類別繪製 K 線圖 + 成交量 + 技術指標副圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;上面範例中的 BokehChart 類別把主圖與副圖邏輯都寫在類別裡, 每次要添加技術指標副圖都要去改類別並不符合軟工原則, 若要讓程式碼具有擴充性, 必須對架構做一次重構, 要建立一個 動態串列 self.figures = [] 的來儲存所有畫布, 並增加通用副圖方法, 例如 add_bar_subplot() (畫成交量, MACD柱狀圖) 與 add_line_subplot() (畫 RSI, 均線).&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_class_3.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&amp;nbsp; #&amp;nbsp;&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure&lt;/div&gt;&lt;div&gt;from bokeh.io import show as show_bokeh # 避免名稱衝突&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter&lt;/div&gt;&lt;div&gt;from bokeh.layouts import column&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;class BokehChart:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;高擴充性 Bokeh 繪圖類別，支援動態疊加無限多個副圖&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def __init__(self, title=&quot;K線圖&quot;, width=800, main_height=400):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.width=width&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.title=title&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.main_height=main_height&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 動態儲存所有畫布的清單 (主圖會在 index 0)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.source=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map={}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k=None # 參照主圖，用來同步 x_range&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def _add_crosshair(self, fig):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;內部方法：加入虛線十字游標&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; w_span=Span(dimension=&quot;width&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; h_span=Span(dimension=&quot;height&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_candlestick(self, df, date_col=&#39;Date&#39;, open_col=&#39;Open&#39;, high_col=&#39;High&#39;, low_col=&#39;Low&#39;, close_col=&#39;Close&#39;):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;【步驟 1】：初始化主圖與共用資料源&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data=df.copy()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立連續整數序列 (消除假日空缺)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;seq&#39;]=range(len(data))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(data[open_col], data[close_col])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 製作日期對照表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map={}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i, d in enumerate(data[date_col]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label=f&quot;{d.strftime(&#39;%b&#39;)} {d.day}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if i == 0 or d.year != prev_year:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label += f&quot;\n{d.year}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map[str(i)]=label&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=d.year&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立全局共用的資料源 (此時 df 裡面已經包含外部算好的 RSI 等指標)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.source=ColumnDataSource(data)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立主畫布&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools=&#39;xpan, xwheel_zoom, box_zoom, reset, save&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 繪製 K 線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.segment(&#39;seq&#39;, high_col, &#39;seq&#39;, low_col, color=&quot;black&quot;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.vbar(&#39;seq&#39;, 0.8, open_col, close_col, fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 設定主圖 Hover&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tooltips=[(&quot;日期&quot;, f&quot;@{date_col}{{%F}}&quot;), (&quot;開盤&quot;, f&quot;@{open_col}{{0.00}}&quot;), (&quot;收盤&quot;, f&quot;@{close_col}{{0.00}}&quot;)],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; formatters={f&#39;@{date_col}&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(self.fig_k)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 加入畫布清單&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(self.fig_k)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_bar_subplot(self, col_name, title=&quot;&quot;, color_col=None, color=&#39;blue&#39;, height=150):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;通用介面：新增柱狀圖副圖 (例如：成交量、MACD柱)&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools=&#39;xpan, xwheel_zoom, reset&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.y_range.start=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 若有指定顏色欄位 (如成交量的紅綠色)，則使用該欄位；否則使用單一顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fill_c=color_col if color_col else color&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.vbar(&#39;seq&#39;, 0.8, top=col_name, fill_color=fill_c, line_color=&#39;black&#39;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 專屬 Hover&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(tooltips=[(&quot;日期&quot;, &quot;@Date{%F}&quot;), (title or col_name, f&quot;@{col_name}{{0,0}}&quot;)], formatters={&#39;@Date&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(fig)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(fig)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_line_subplot(self, col_name, title=&quot;&quot;, color=&#39;blue&#39;, height=150, hlines=None):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;通用介面：新增折線圖副圖 (例如：RSI、均線、KD)&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools=&#39;xpan, xwheel_zoom, reset&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.line(&#39;seq&#39;, col_name, color=color, line_width=1.5, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 支援加入水平參考線 (例如 RSI 的 30, 70 超買超賣線)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if hlines:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for y_val in hlines:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; line=Span(location=y_val, dimension=&#39;width&#39;, line_color=&#39;gray&#39;, line_dash=&#39;dashed&#39;, line_alpha=0.5)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_layout(line)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 專屬 Hover&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(tooltips=[(&quot;日期&quot;, &quot;@Date{%F}&quot;), (title or col_name, f&quot;@{col_name}{{0.00}}&quot;)], formatters={&#39;@Date&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(fig)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(fig)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def show(self):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;渲染圖表：隱藏除最後一張圖外的 X 軸，並套用自訂日期標籤&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i, fig in enumerate(self.figures):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 不是最後一張圖，隱藏 X 軸&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if i &amp;lt; len(self.figures) - 1:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.visible=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 是最後一張圖，掛上 JavaScript 日期查表邏輯&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; js_code=&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var idx=Math.round(tick).toString();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (date_map[idx] !== undefined) {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return date_map[idx];&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &quot;&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={&#39;date_map&#39;: self.date_map})&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.major_label_orientation=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.major_label_standoff=15&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 垂直疊加所有被加入的圖表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; layout=column(*self.figures)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; show_bokeh(layout)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;# 主程式：展現架構擴充性的時刻&lt;/div&gt;&lt;div&gt;# ==========================================&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 1. 獲取資料&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2025-10-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 2. 在外部計算所有技術指標 (類別完全不需要知道 RSI 怎麼算的)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;RSI&#39;]=ta.rsi(df[&#39;Close&#39;], length=14)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 3. 開始繪圖 (如堆積木般直覺)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart=BokehChart(title=&#39;0050.TW 台灣50 ETF (整合 RSI 指標)&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 初始化主圖與資料源&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_candlestick(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 呼叫通用介面新增成交量 (使用 color 欄位決定紅綠)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_bar_subplot(&#39;Volume&#39;, title=&#39;成交量&#39;, color_col=&#39;color&#39;, height=100)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 呼叫通用介面新增 RSI (自訂顏色，並加上 30/70 水平參考線)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_line_subplot(&#39;RSI&#39;, title=&#39;RSI(14)&#39;, color=&#39;purple&#39;, height=150, hlines=[30, 70])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 顯示成果&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此例使用 pandas_ta 套件計算 RSI 指標值, 結果放進 df[&#39;RSI&#39;] 欄位, 結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1f5JP1G2v7sMj8w45gFSUNqmO0icYRSQGysziRNOfM1uzLctGK_l4DE5_3eMO9Y4y0za5v3r2bvpRZFWRljqpym8JpwoNDzDuzwZAHFQxufJu7FUgE5Vie3P-mDBAhsGo6s7Xyh9gnvmVQQ4576NhAwx3Zvdyip88NFj44Hd027cY4W7vFE10xZmf2oSC/s1594/bokeh-candlestick-class-3.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1258&quot; data-original-width=&quot;1594&quot; height=&quot;253&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1f5JP1G2v7sMj8w45gFSUNqmO0icYRSQGysziRNOfM1uzLctGK_l4DE5_3eMO9Y4y0za5v3r2bvpRZFWRljqpym8JpwoNDzDuzwZAHFQxufJu7FUgE5Vie3P-mDBAhsGo6s7Xyh9gnvmVQQ4576NhAwx3Zvdyip88NFj44Hd027cY4W7vFE10xZmf2oSC/s320/bokeh-candlestick-class-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;7. 獨立的 Bokeh K 線圖模組 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;我們可以將上面範例的 BokehChart 類別寫在一個獨立的模組檔案例如 bokeh_chart.py 中, 再用 import 方式匯入 BokehChart 類別來繪製 K 線圖, 這樣主程式檔就會較簡潔了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;模組檔案 bokeh_chart.py 內容如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;# bokeh_chart.py&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure&lt;/div&gt;&lt;div&gt;from bokeh.io import show as show_bokeh&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter&lt;/div&gt;&lt;div&gt;from bokeh.layouts import column&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;class BokehChart:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;高擴充性 Bokeh 繪圖類別，支援動態疊加無限多個副圖&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def __init__(self, title=&#39;K線圖&#39;, width=800, main_height=400):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.width=width&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.title=title&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.main_height=main_height&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.source=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map={}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k=None&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def _add_crosshair(self, fig):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;加入虛線十字游標&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; w_span=Span(dimension=&#39;width&#39;, line_dash=&#39;dashed&#39;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; h_span=Span(dimension=&#39;height&#39;, line_dash=&#39;dashed&#39;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_candlestick(self, df, date_col=&#39;Date&#39;, open_col=&#39;Open&#39;, high_col=&#39;High&#39;, low_col=&#39;Low&#39;, close_col=&#39;Close&#39;):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;初始化主圖與共用資料源&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data=df.copy()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 建立連續整數序列與漲跌顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;seq&#39;]=range(len(data))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; data[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(data[open_col], data[close_col])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 製作日期對照表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map={}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i, d in enumerate(data[date_col]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label=f&quot;{d.strftime(&#39;%b&#39;)} {d.day}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if i == 0 or d.year != prev_year:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label += f&quot;\n{d.year}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.date_map[str(i)]=label&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; prev_year=d.year&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.source=ColumnDataSource(data)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k=figure(title=self.title, width=self.width, height=self.main_height, tools=&#39;xpan, xwheel_zoom, box_zoom, reset, save&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.segment(&#39;seq&#39;, high_col, &#39;seq&#39;, low_col, color=&quot;black&quot;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.vbar(&#39;seq&#39;, 0.8, open_col, close_col, fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; tooltips=[(&quot;日期&quot;, f&quot;@{date_col}{{%F}}&quot;), (&quot;開盤&quot;, f&quot;@{open_col}{{0.00}}&quot;), (&quot;收盤&quot;, f&quot;@{close_col}{{0.00}}&quot;)],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; formatters={f&#39;@{date_col}&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.fig_k.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(self.fig_k)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(self.fig_k)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_bar_subplot(self, col_name, title=&quot;&quot;, color_col=None, color=&#39;blue&#39;, height=150, on_fig=None, y_min_zero=False):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;新增柱狀圖副圖 (支援傳入畫布疊加到現有畫布)&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 如果有指定 on_fig，就用現成的畫布；否則建立新畫布&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if on_fig:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig=on_fig&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig=figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools=&#39;xpan, xwheel_zoom, reset&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(fig) # 只有新畫布才需要加入清單&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(fig) # 只有新畫布才需要加十字線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 只有成交量這類數值絕對為正的圖表，才需要強制 Y 軸從 0 開始&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if y_min_zero:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.y_range.start=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fill_c=color_col if color_col else color&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.vbar(&#39;seq&#39;, 0.8, top=col_name, fill_color=fill_c, line_color=&#39;black&#39;, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 專屬 Hover&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover=HoverTool(tooltips=[(&quot;日期&quot;, &quot;@Date{%F}&quot;), (title or col_name, f&quot;@{col_name}{{0.00}}&quot;)], formatters={&#39;@Date&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return fig # 回傳畫布讓後續的線可以畫在它上面&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def add_line_subplot(self, col_name, title=&quot;&quot;, color=&#39;blue&#39;, height=150, hlines=None, on_fig=None):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;新增折線圖副圖 (支援疊加到現有畫布)&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 如果有指定 on_fig，就用現成的畫布；否則建立新畫布&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if on_fig:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig = on_fig&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig = figure(width=self.width, height=height, x_range=self.fig_k.x_range, tools=&#39;xpan, xwheel_zoom, reset&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.grid.grid_line_alpha = 0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self.figures.append(fig)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; self._add_crosshair(fig)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.line(&#39;seq&#39;, col_name, color=color, line_width=1.5, source=self.source)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if hlines:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for y_val in hlines:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; line = Span(location=y_val, dimension=&#39;width&#39;, line_color=&#39;gray&#39;, line_dash=&#39;dashed&#39;, line_alpha=0.5)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_layout(line)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hover = HoverTool(tooltips=[(&quot;日期&quot;, &quot;@Date{%F}&quot;), (title or col_name, f&quot;@{col_name}{{0.00}}&quot;)], formatters={&#39;@Date&#39;: &#39;datetime&#39;}, mode=&#39;vline&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return fig # 回傳畫布讓後續的線可以畫在它上面&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; def show(self):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;渲染圖表&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i, fig in enumerate(self.figures):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if i &amp;lt; len(self.figures) - 1:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.visible=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; js_code=&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var idx=Math.round(tick).toString();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (date_map[idx] !== undefined) {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return date_map[idx];&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; } else {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &quot;&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={&#39;date_map&#39;: self.date_map})&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.major_label_orientation=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fig.xaxis.major_label_standoff=15&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; layout=column(*self.figures)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; show_bokeh(layout)&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;欲繪製 K 線圖+成交量+技術指標時, 主程式檔只要先從 bokeh_chart.py 匯入 BokehChart 類別並建立 BokehChart 物件後, 呼叫 add_candlestick() 繪製主圖 (K 線圖), 呼叫 add_bar_subplot() 或 add_line_subplot() 添加副圖 (成交量 &amp;amp; 技術指標), 下面範例添加了 RSI 與 MACD 副圖 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_class_4.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&lt;/div&gt;&lt;div&gt;from bokeh_chart import BokehChart&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 1. 獲取資料&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2025-10-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 2. 計算技術指標&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # RSI&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;RSI&#39;]=ta.rsi(df[&#39;Close&#39;], length=14)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # MACD (將計算結果合併回原本的 df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_df=ta.macd(df[&#39;Close&#39;])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=pd.concat([df, macd_df], axis=1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 計算 MACD 柱狀圖的顏色 (大於等於 0 紅色，小於 0 綠色)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;macd_color&#39;]=[&#39;#ff0000&#39; if m &amp;gt;= 0 else &#39;#00aa00&#39; for m in df[&#39;MACDh_12_26_9&#39;]]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 3. 畫圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 畫主圖 : K 線圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart=BokehChart(title=&#39;0050.TW 台灣50 ETF (整合 MACD)&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_candlestick(df)&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 畫成交量副圖 (加上 y_min_zero=True，避免長條圖懸空)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_bar_subplot(&#39;Volume&#39;, title=&#39;成交量&#39;, color_col=&#39;color&#39;, height=100, y_min_zero=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # ==========================================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 繪製 MACD 複合副圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # ==========================================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 第一步：先畫 MACD 柱狀圖，並用變數 macd_fig 把畫布接住&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;macd_fig&lt;/span&gt;=chart.add_bar_subplot(&#39;MACDh_12_26_9&#39;, title=&#39;MACD柱&#39;, color_col=&#39;macd_color&#39;, height=150)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 第二步：把 MACD 線畫在 macd_fig 身上&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_line_subplot(&#39;MACD_12_26_9&#39;, title=&#39;MACD線&#39;, color=&#39;blue&#39;, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;on_fig=macd_fig&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 第三步：把 Signal 線也畫在 macd_fig 身上&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.add_line_subplot(&#39;MACDs_12_26_9&#39;, title=&#39;Signal線&#39;, color=&#39;orange&#39;, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;on_fig=macd_fig&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # ==========================================&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; chart.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, 此處 MACD 指標因為含有快線, 慢線, 與柱狀圖三個圖形, 通常都是疊在一個畫布上, 所以要分成三個步驟把三個圖層疊上去, 首先繪製柱狀圖, 然後將傳回的畫布物件 macd_fig 在後續繪製快線與慢線時傳給 on_fonfig 參數, 這樣三張圖就會疊在同一張畫布上了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGZqBIRd7JFcTU5d2LrdBR6rhilytxIVsNiDPqvcvufw4tu7JTY5qVAqoRBw0nCRKQuhE5AToIeA3Q-PvmFHgCEhoi2zLMwIcBQoIWOg8eTSGWcX6BzrB0RgMxP9V78bBwIOpjNTRqxLsYaVl4k6qkfgYN53agh7nkLR1vOBU418G7pG484jTQ-JlTs3xl/s1585/bokeh-candlestick-class-4.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1255&quot; data-original-width=&quot;1585&quot; height=&quot;253&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGZqBIRd7JFcTU5d2LrdBR6rhilytxIVsNiDPqvcvufw4tu7JTY5qVAqoRBw0nCRKQuhE5AToIeA3Q-PvmFHgCEhoi2zLMwIcBQoIWOg8eTSGWcX6BzrB0RgMxP9V78bBwIOpjNTRqxLsYaVl4k6qkfgYN53agh7nkLR1vOBU418G7pG484jTQ-JlTs3xl/s320/bokeh-candlestick-class-4.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見將繪圖類別獨立出去後, 主程式結構就清爽易讀多了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1210608893710327059/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1210608893710327059' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1210608893710327059'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1210608893710327059'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/python-bokeh-k_3.html' title='Python 學習筆記 : 用 bokeh 繪製 K 線圖 (二)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6DnN8Abo1SfBzzrmZC6TQDbkWMGOrh5Ai2t8U3AQIC1flSPNGkXUTUq1JTe8ljMZyVLJZ5NcIDfu4KoXc8vAUqYBjGwu25HgOKAiIBw-hJUjvpUA-p1i0SMD9lfONxMjAtMlfOszo_FAbuA2iJnJ5pNLdP7tDDVjW2v9RQYzPIAB7NUzW0Sy3ihI6lCV4/s72-c/bokeh-candlestick-class-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-9222008153954468030</id><published>2026-05-03T07:42:00.007+08:00</published><updated>2026-05-03T10:04:55.800+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Bokeh"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="資料視覺化"/><title type='text'>Python 學習筆記 : 用 bokeh 繪製 K 線圖 (一)</title><content type='html'>Bokeh 不像 Plotly 那樣有內建專門繪製 K 線圖的 CandleStick 類別, 它需透過 bokeh.models 模組中的 Segment 物件 (用來繪製上下影線) 與 Vbar 物件 (用來繪製實體紅黑棒) 來達成, 方法雖然較低階, 但卻比 Plotly 擁有更高的自由度與效能, 尤其是要在瀏覽器中渲染數萬根 K 線時, Bokeh 的 WebGL 渲染模式通常比 Plotly 更為輕快, 且保持流暢的縮放和平移效果.&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列全部測試文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python.html&quot; target=&quot;_blank&quot;&gt;Python 資料視覺化學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;首先匯入繪圖用的函式 figure() 與 show() :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;from bokeh.plotting import figure, show&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;然後從元件模組 bokeh.models 中匯入匯入用來繪製 K 棒的 ColumnDataSource 類別, 以及繪製互動效果的懸停工具類別 HoverTool :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;from bokeh.models import ColumnDataSource, HoverTool&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;接著匯入 Pandas 來處理 OHLC 資料 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;import pandas as pd&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;data={&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;date&#39;: pd.to_datetime([&#39;2026-05-01&#39;, &#39;2026-05-02&#39;, &#39;2026-05-03&#39;, &#39;2026-05-04&#39;]),&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;open&#39;:&amp;nbsp; [100, 110, 105, 120],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;high&#39;:&amp;nbsp; [115, 120, 110, 130],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;low&#39;:&amp;nbsp; &amp;nbsp;[95, 105, 100, 115],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &#39;close&#39;: [110, 105, 108, 125]&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;df=pd.DataFrame(data)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在 df 中添加一個 color 欄位來記錄漲跌顏色 (紅漲綠跌) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;df[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(df.open, df.close)]&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫&amp;nbsp;ColumnDataSource 類別的建構式 ColumnDataSource() 並傳入 df 建立 ColumnDataSource 物件 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;source=ColumnDataSource(df)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;type(source)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;lt;class &#39;bokeh.models.sources.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;ColumnDataSource&lt;/span&gt;&#39;&amp;gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 figure() 函式建立畫布物件 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig=figure(&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; x_axis_type=&#39;datetime&#39;,&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; title=&#39;Bokeh K線圖&#39;,&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; width=800,&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; height=400&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; )&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 segment() 函式繪製上下影線 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.segment(&#39;date&#39;, &#39;high&#39;, &#39;date&#39;, &#39;low&#39;, color=&#39;black&#39;, source=source)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;GlyphRenderer(id=&#39;p1138&#39;, ...)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 Figure 物件的 vbar() 方法繪製 K 棒 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.vbar(&#39;date&#39;, pd.Timedelta(hours=12), &#39;open&#39;, &#39;close&#39;,&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;GlyphRenderer(id=&#39;p1147&#39;, ...)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫&amp;nbsp;HoverTool 類別的建構式建立懸停工具物件 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;hover=HoverTool(&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; tooltips=[&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, &quot;@date{%F}&quot;),&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, &quot;@open&quot;),&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, &quot;@close&quot;),&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, &quot;@high&quot;),&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, &quot;@low&quot;)&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; formatters={&#39;@date&#39;: &#39;datetime&#39;}&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; )&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 Figure 物件的 add_tool() 方法將懸停工具物件加入畫布中 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.add_tools(hover)&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣便可呼叫 show() 函式來顯示畫布了 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;show(fig)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Bokeh 會自動開啟瀏覽器顯示網頁來呈現畫布內容 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihGhIgdtdFEqcSnAdn2JkmnC80-6zprE-inMu1GJEhHYmsyeBGfjB9yqGwTVUayddUN4o6JG3Qpk3desDNTam67tjDBjD6qpeDqBlbdjd0zmlaXgIxFukpF0f_h98qJDXsYDlGTlI3macMT9hnjQcU_T0uXHxH93_wu-prqE-tgB-kZjzNqfd3UfcPDg9c/s1773/bokeh-candlestick-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;882&quot; data-original-width=&quot;1773&quot; height=&quot;159&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihGhIgdtdFEqcSnAdn2JkmnC80-6zprE-inMu1GJEhHYmsyeBGfjB9yqGwTVUayddUN4o6JG3Qpk3desDNTam67tjDBjD6qpeDqBlbdjd0zmlaXgIxFukpF0f_h98qJDXsYDlGTlI3macMT9hnjQcU_T0uXHxH93_wu-prqE-tgB-kZjzNqfd3UfcPDg9c/s320/bokeh-candlestick-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;完整程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_1.py&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# OHLC 資料 :&lt;/div&gt;&lt;div&gt;data={&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;date&#39;: pd.to_datetime([&#39;2026-05-01&#39;, &#39;2026-05-02&#39;, &#39;2026-05-03&#39;, &#39;2026-05-04&#39;]),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;open&#39;:&amp;nbsp; [100, 110, 105, 120],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;high&#39;:&amp;nbsp; [115, 120, 110, 130],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;low&#39;:&amp;nbsp; &amp;nbsp;[95, 105, 100, 115],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;close&#39;: [110, 105, 108, 125]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;df=pd.DataFrame(data)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 定義漲跌顏色&lt;/div&gt;&lt;div&gt;df[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(df.open, df.close)]&lt;/div&gt;&lt;div&gt;source=ColumnDataSource(df)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 建立畫布&lt;/div&gt;&lt;div&gt;fig=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; x_axis_type=&#39;datetime&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; title=&#39;Bokeh K線圖&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; height=400,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #tools=&#39;pan, wheel_zoom, box_zoom, reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 繪製上下影線 (Segment 物件)&lt;/div&gt;&lt;div&gt;fig.segment(&#39;date&#39;, &#39;high&#39;, &#39;date&#39;, &#39;low&#39;, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 繪製 K 棒 (Vbar 物件)&lt;/div&gt;&lt;div&gt;# width 設定為 12 小時 (以毫秒計算) 以確保條形之間有間隔&lt;/div&gt;&lt;div&gt;fig.vbar(&#39;date&#39;, pd.Timedelta(hours=12), &#39;open&#39;, &#39;close&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 加入互動式懸停工具 (HoverTool)&lt;/div&gt;&lt;div&gt;hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, &quot;@date{%F}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, &quot;@open&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, &quot;@close&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, &quot;@high&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, &quot;@low&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; formatters={&#39;@date&#39;: &#39;datetime&#39;}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 輸出與顯示&lt;/div&gt;&lt;div&gt;#output_file(&quot;candlestick.html&quot;)&lt;/div&gt;&lt;div&gt;show(fig)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1. 繪製從 yfinance 下載的實盤 K 線圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下面範例改從 yfinance 抓取實盤 OHLC 資料來繪製 K 線圖, 程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_2.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 1. 下載真實 OHLCV 資料&lt;/div&gt;&lt;div&gt;df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位&lt;/div&gt;&lt;div&gt;df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;df=df.reset_index()&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 2. 定義漲跌顏色 (紅漲綠跌)&lt;/div&gt;&lt;div&gt;df[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(df[&#39;Open&#39;], df[&#39;Close&#39;])]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 3. 建立資料來源&lt;/div&gt;&lt;div&gt;source=ColumnDataSource(df)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 4. 建立畫布&lt;/div&gt;&lt;div&gt;fig=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; x_axis_type=&#39;datetime&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; title=&#39;0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; height=400,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 加入十字游標 (crosshair) 與鎖定 X 軸縮放(xwheel_zoom) 以利看盤&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, box_zoom, crosshair, reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;# 讓網格線淡一點，視覺更聚焦在 K 棒上&lt;/div&gt;&lt;div&gt;fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 5. 繪製上下影線&lt;/div&gt;&lt;div&gt;fig.segment(&#39;Date&#39;, &#39;High&#39;, &#39;Date&#39;, &#39;Low&#39;, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 6. 繪製 K 棒&lt;/div&gt;&lt;div&gt;# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒&lt;/div&gt;&lt;div&gt;width_ms=12 * 60 * 60 * 1000&lt;/div&gt;&lt;div&gt;fig.vbar(&#39;Date&#39;, width_ms, &#39;Open&#39;, &#39;Close&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 7. 加入互動式懸停工具 (HoverTool)&lt;/div&gt;&lt;div&gt;# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)&lt;/div&gt;&lt;div&gt;hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, &quot;@Date{%F}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, &quot;@Open{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, &quot;@Close{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, &quot;@High{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, &quot;@Low{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;(&quot;成交量&quot;, &quot;@Volume{0,0}&quot;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; formatters={&#39;@Date&#39;: &#39;datetime&#39;}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 8. 輸出與顯示&lt;/div&gt;&lt;div&gt;show(fig)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此例的關鍵之處是 yf 下載的 df 日期欄位會被設為索引, 所以必須呼叫 df.reset_index() 將日期欄位重設為一般欄位, 這樣 ColumnDataSource 物件才能讀得到, 另外, 懸停工具則添加了 yf 取得的成交量資訊, 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwarQ4SznHNY_oBjfZdG8j5aGDg4EWaeUuTk0k6_xT3AyI_o18iXx7NYkyEGWmeqq7LaSJuZUZdpGUDQSEwQvPHX8nOxMHj_Sc46jSRzAm6ek_hg4QvyTZnDkrM05t2Y4ZJgfBf4o8tXvHMo9bhlvgKXJTWgwr8eBZtV8YVV_1HPxLevS_K_yJGeL04C0l/s1748/bokeh-candlestick-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;880&quot; data-original-width=&quot;1748&quot; height=&quot;161&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwarQ4SznHNY_oBjfZdG8j5aGDg4EWaeUuTk0k6_xT3AyI_o18iXx7NYkyEGWmeqq7LaSJuZUZdpGUDQSEwQvPHX8nOxMHj_Sc46jSRzAm6ek_hg4QvyTZnDkrM05t2Y4ZJgfBf4o8tXvHMo9bhlvgKXJTWgwr8eBZtV8YVV_1HPxLevS_K_yJGeL04C0l/s320/bokeh-candlestick-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見十字線會隨著滑鼠在畫布中移動, 當指向 K 棒時會出現懸停提示, 顯示 OHLCV 數據.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 客製化的虛線十字輔助線 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;預設的十字線是實線, 如果要改為虛線須取消 tools 參數中的 crosshair, 然後匯入&amp;nbsp;CrosshairTool, Span 這兩個類別 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;其中 CrosshairTool 用來建立客製化十字輔助線, 而 Span 則用來建立具有 line_dash 屬性的 Span (跨距線) 物件, 然後把這些線覆蓋 (overlay) 到 CrosshairTool 物件上, 程式改寫如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_3.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;CrosshairTool, Span&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 1. 下載真實 OHLCV 資料&lt;/div&gt;&lt;div&gt;df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;# 攤平 yfinance 最新版本回傳的 MultiIndex 欄位&lt;/div&gt;&lt;div&gt;df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;# yfinance 的日期會被設為索引, 重設為一般欄位 以利 ColumnDataSource 讀取&lt;/div&gt;&lt;div&gt;df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 2. 定義漲跌顏色 (紅漲綠跌)&lt;/div&gt;&lt;div&gt;df[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(df[&#39;Open&#39;], df[&#39;Close&#39;])]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 3. 建立資料來源&lt;/div&gt;&lt;div&gt;source=ColumnDataSource(df)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 4. 建立畫布&lt;/div&gt;&lt;div&gt;fig=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; x_axis_type=&#39;datetime&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; title=&#39;0050.TW 台灣50 ETF K線圖 (2026-03 ~ 2026-04)&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; height=400,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 鎖定 X 軸縮放(xwheel_zoom) 以利看盤 (&lt;span style=&quot;background-color: #fcff01;&quot;&gt;取消 crosshair&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, box_zoom, , reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;# 讓網格線淡一點，視覺更聚焦在 K 棒上&lt;/div&gt;&lt;div&gt;fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 5. 繪製上下影線&lt;/div&gt;&lt;div&gt;fig.segment(&#39;Date&#39;, &#39;High&#39;, &#39;Date&#39;, &#39;Low&#39;, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 6. 繪製 K 棒&lt;/div&gt;&lt;div&gt;# Bokeh 處理時間軸時寬度用毫秒計算最為精準與穩定。12小時=12 * 60 * 60 * 1000 毫秒&lt;/div&gt;&lt;div&gt;width_ms=12 * 60 * 60 * 1000&lt;/div&gt;&lt;div&gt;fig.vbar(&#39;Date&#39;, width_ms, &#39;Open&#39;, &#39;Close&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 7. 加入互動式懸停工具 (HoverTool)&lt;/div&gt;&lt;div&gt;# 加上了數值格式化 {0.00} 讓小數點對齊並順手加入成交量 (Volume)&lt;/div&gt;&lt;div&gt;hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, &quot;@Date{%F}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, &quot;@Open{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, &quot;@Close{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, &quot;@High{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, &quot;@Low{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;成交量&quot;, &quot;@Volume{0,0}&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; formatters={&#39;@Date&#39;: &#39;datetime&#39;}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 8. 加入虛線十字游標 (建立水平與垂直方向的 Span 物件, 設定為 dashed 虛線)&lt;/div&gt;&lt;div&gt;w_span=Span(dimension=&quot;width&quot;, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;line_dash=&quot;dashed&quot;&lt;/span&gt;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;h_span=Span(dimension=&quot;height&quot;, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;line_dash=&quot;dashed&quot;&lt;/span&gt;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;fig.add_tools(&lt;span style=&quot;background-color: #fcff01;&quot;&gt;CrosshairTool(overlay=[w_span, h_span])&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 9. 輸出與顯示&lt;/div&gt;&lt;div&gt;show(fig)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-UekOj-5DMd8KNBn6blbM3AR2Eds3OUR35LaRVdfgv-eXKoHRQnp5mcqMZ_FGAV6NPkNbULqosoFiofal0uAijg7-LEDKJO-ZMjwwvkhu3XUPyjV3EgoQjKfA3JXMhBUzlP36Tgw2Lei90vri81QXAAV8F36pzVQRMtI_1fyXOa-1fYHOrdfJmz7ki5bo/s1763/bokeh-candlestick-3.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;880&quot; data-original-width=&quot;1763&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-UekOj-5DMd8KNBn6blbM3AR2Eds3OUR35LaRVdfgv-eXKoHRQnp5mcqMZ_FGAV6NPkNbULqosoFiofal0uAijg7-LEDKJO-ZMjwwvkhu3XUPyjV3EgoQjKfA3JXMhBUzlP36Tgw2Lei90vri81QXAAV8F36pzVQRMtI_1fyXOa-1fYHOrdfJmz7ki5bo/s320/bokeh-candlestick-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見十字輔助線已經變成虛線了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;3. 消除非交日空缺 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;與 Plotly 一樣, Bokeh&amp;nbsp;在繪製 K 線圖時會自動把 yfinance 下載的 OHLCV 資料 X 軸的日期序列補齊, 使得 K 棒在非交易日無 K 棒出現不連續的空缺.&amp;nbsp;Plotly 的 Figure 物件有內建的 rangebreaks 參數可用來隱藏這些空缺, 但 Bokeh 並沒有.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在 Bokeh 中可以用 Index Mapping 來解決此問題. 概念是不要把 X 軸當成時間, 而是把它當成 連續的整數數列 (0, 1, 2, 3...), 然後透過一個標籤轉換器騙過使用者的眼睛, 讓 X 軸的數字顯示成對應的日期. 具體而言是透過&amp;nbsp;CustomJSTickFormatter 來實現, 但如果要維持原本 Bokeh 的 X 軸標籤顯示風格, 必須修改 Javascript 程式碼, 例如 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;# bokeh_candlestick_4.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from bokeh.plotting import figure, show&lt;/div&gt;&lt;div&gt;from bokeh.models import ColumnDataSource, HoverTool, CrosshairTool, Span, CustomJSTickFormatter&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 1. 下載真實 OHLCV 資料&lt;/div&gt;&lt;div&gt;df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;df=df.reset_index()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 2. 建立連續整數序列&lt;/div&gt;&lt;div&gt;df[&#39;seq&#39;]=range(len(df))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 用 Python 預先將日期轉為 Bokeh 風格的 &quot;Apr 1&quot; 格式&lt;/div&gt;&lt;div&gt;date_map={}&lt;/div&gt;&lt;div&gt;prev_year=None&lt;/div&gt;&lt;div&gt;for i, d in enumerate(df[&#39;Date&#39;]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 取得月份縮寫與日期 (例如 &quot;Apr 1&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # strftime(&#39;%b&#39;) 會輸出英文月份縮寫，d.day 會輸出不補零的日期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; label=f&quot;{d.strftime(&#39;%b&#39;)} {d.day}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 如果是圖表的第一根 K 棒，或者是跨年的第一根 K 棒，在底下加上年份&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 使用 \n 來讓 Bokeh 產生換行效果&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if i == 0 or d.year != prev_year:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; label += f&quot;\n{d.year}&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_map[str(i)]=label&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; prev_year=d.year&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 3. 定義漲跌顏色&lt;/div&gt;&lt;div&gt;df[&#39;color&#39;]=[&#39;#ff0000&#39; if c &amp;gt;= o else &#39;#00aa00&#39; for o, c in zip(df[&#39;Open&#39;], df[&#39;Close&#39;])]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 4. 建立資料來源&lt;/div&gt;&lt;div&gt;source=ColumnDataSource(df)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 5. 建立畫布&lt;/div&gt;&lt;div&gt;fig=figure(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; title=&#39;0050.TW 台灣50 ETF K線圖 (完美日期標籤版)&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; height=400,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tools=&#39;xpan, xwheel_zoom, box_zoom, reset, save&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;fig.grid.grid_line_alpha=0.3&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 6. 使用 JavaScript 取出排版好的字串&lt;/div&gt;&lt;div&gt;js_code=&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; var idx=Math.round(tick).toString();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if (date_map[idx] !== undefined) {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return date_map[idx];&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; } else {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &quot;&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;fig.xaxis.formatter=CustomJSTickFormatter(code=js_code, args={&#39;date_map&#39;: date_map})&lt;/div&gt;&lt;div&gt;# 把傾斜拿掉 (設為 0) 讓標籤恢復水平，這樣帶有 \n 年份的文字才會對齊&lt;/div&gt;&lt;div&gt;fig.xaxis.major_label_orientation=0&lt;/div&gt;&lt;div&gt;# 拉開標籤與 X 軸的間距 (預設通常是 5)&lt;/div&gt;&lt;div&gt;fig.xaxis.major_label_standoff=15&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 7. 繪製上下影線&amp;nbsp;&lt;/div&gt;&lt;div&gt;fig.segment(&#39;seq&#39;, &#39;High&#39;, &#39;seq&#39;, &#39;Low&#39;, color=&quot;black&quot;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 8. 繪製 K 棒&lt;/div&gt;&lt;div&gt;width_val=0.8&lt;/div&gt;&lt;div&gt;fig.vbar(&#39;seq&#39;, width_val, &#39;Open&#39;, &#39;Close&#39;,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;fill_color=&#39;color&#39;, line_color=&#39;black&#39;, source=source)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 9. 加入互動式懸停工具&amp;nbsp;&lt;/div&gt;&lt;div&gt;hover=HoverTool(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; tooltips=[&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;日期&quot;, &quot;@Date{%F}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;開盤&quot;, &quot;@Open{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;收盤&quot;, &quot;@Close{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最高&quot;, &quot;@High{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;最低&quot;, &quot;@Low{0.00}&quot;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; (&quot;成交量&quot;, &quot;@Volume{0,0}&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; formatters={&#39;@Date&#39;: &#39;datetime&#39;}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;fig.add_tools(hover)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 10. 加入虛線十字游標&amp;nbsp;&lt;/div&gt;&lt;div&gt;w_span=Span(dimension=&quot;width&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;h_span=Span(dimension=&quot;height&quot;, line_dash=&quot;dashed&quot;, line_color=&quot;gray&quot;, line_alpha=0.6, line_width=1)&lt;/div&gt;&lt;div&gt;fig.add_tools(CrosshairTool(overlay=[w_span, h_span]))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 11. 輸出與顯示&lt;/div&gt;&lt;div&gt;show(fig)&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiA6ncD6qRUXy87Qt06uGmtdUQGdEeGtJJg01PB6pWF6rQ8YXZzbpxxmzHbgTedpUtad4V7BICPCRReSBzaxpWoAWjmZ6DBP6xl4kF7yhafA3B6-gUuIogX3jf2A-coyJGyf1-0FT8TAzeiKTcmvYRhFCoWiuMHkNGFGqMN4bzRoOO-f31Ni85nfONlZtAH/s1769/bokeh-candlestick-4.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;858&quot; data-original-width=&quot;1769&quot; height=&quot;155&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiA6ncD6qRUXy87Qt06uGmtdUQGdEeGtJJg01PB6pWF6rQ8YXZzbpxxmzHbgTedpUtad4V7BICPCRReSBzaxpWoAWjmZ6DBP6xl4kF7yhafA3B6-gUuIogX3jf2A-coyJGyf1-0FT8TAzeiKTcmvYRhFCoWiuMHkNGFGqMN4bzRoOO-f31Ni85nfONlZtAH/s320/bokeh-candlestick-4.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;不過這樣的程式碼看起來就有點複雜了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/9222008153954468030/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/9222008153954468030' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9222008153954468030'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9222008153954468030'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/python-bokeh-k.html' title='Python 學習筆記 : 用 bokeh 繪製 K 線圖 (一)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihGhIgdtdFEqcSnAdn2JkmnC80-6zprE-inMu1GJEhHYmsyeBGfjB9yqGwTVUayddUN4o6JG3Qpk3desDNTam67tjDBjD6qpeDqBlbdjd0zmlaXgIxFukpF0f_h98qJDXsYDlGTlI3macMT9hnjQcU_T0uXHxH93_wu-prqE-tgB-kZjzNqfd3UfcPDg9c/s72-c/bokeh-candlestick-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-666668762849965976</id><published>2026-05-02T22:04:00.003+08:00</published><updated>2026-05-02T22:58:48.761+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="量化投資"/><title type='text'>Python 學習筆記 : 利用語言模型計算技術指標 (二)</title><content type='html'>&lt;div&gt;在&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/04/python_29.html&quot; target=&quot;_blank&quot;&gt;前一篇測試&lt;/a&gt;中, 我們透過串接 LLM 模型 (OpenAI &amp;amp; Gemini) 要求 AI 生成計算技術指標的程式碼在本機執行運算, 毋須使用例如 Ta-Lib 或 ta, pandas-ta 等技術指標函式庫. 本篇旨在前一篇的基礎上取得 AI 生成的程式碼後算出技術指標 (MACD) 數據, 並繪製 K 線圖.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列全部測試文章索引參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2024/08/python.html&quot; target=&quot;_blank&quot;&gt;Python 量化投資筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;關於 MACD 技術指標計算, 參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/11/python-pandas-ta_92.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 技術分析套件 pandas-ta 用法 (四)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1. 用 pandas_ta 套件計算 MACD 指標 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;MACD 由快線 (DIF), 慢線 (DEA) 和 柱狀圖 (Histogram) 三部分組成, 當快速線向上突破訊號線時為買進訊號; 快速線向下跌破訊號線時為賣出訊號. 柱狀圖用來觀察市場動能強弱, 柱狀圖由負轉正表示多方動能變強; 柱狀圖由正轉負表示空方動能變強. 訊號線則是 MACD 快速線的平滑化版本, 用來確認趨勢是否開始反轉.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;MACD 指標預設參數通常是快線 12 日, 慢線 26 日, 信號線 9 日, 以 pandas_ta 套件來說,&amp;nbsp;呼叫 df.ta.macd() 或 ta.macd() 即可計算這三組 MACD 指標數值. 注意, 在之前測試中呼叫 df.ta.macd() 時傳入 append=True, 這樣 pandas_ta 會將計算出來的 MACD 指標值自動插入 df 的新增欄位中, 預設欄位名稱&lt;span style=&quot;background-color: #fcff01;&quot;&gt;依序是&lt;/span&gt;&amp;nbsp;MACD_12_26_9 (MACD 線), MACDh_12_26_9 (MACD 柱狀圖), 與 MACDs_12_26_9 (MACD 信號線), 為了後續繪製 K 線圖方便, 這次不使用 append=True 參數, 而是在 df 手動指定傳回的三個 Series 的欄位名稱 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;df[[&#39;MACD_line&#39;, &#39;MACD_histogram&#39;, &#39;MACD_signal&#39;]]=df.ta.macd(close=&#39;Close&#39;)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# ai_stock_test_5.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&amp;nbsp;&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&lt;/div&gt;&lt;div&gt;from kbar import KBar&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2024-11-06&#39;, end=&#39;2025-01-09&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;df[[&#39;MACD_line&#39;, &#39;MACD_histogram&#39;, &#39;MACD_signal&#39;]]=df.ta.macd(close=&#39;Close&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(df.tail())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_line_name=&#39;MACD_line&#39;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# MACD 線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_signal_name=&#39;MACD_signal&#39;&amp;nbsp; &amp;nbsp;# MACD 訊號線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_histogram_name=&#39;MACD_histogram&#39;&amp;nbsp; # MACD 柱狀圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb=KBar(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; histogram_colors=[&#39;g&#39; if v &amp;gt;= 0 else &#39;r&#39; for v in df[macd_histogram_name]]&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_panel=2&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_histogram_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type=&#39;bar&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=histogram_colors,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; alpha=0.4,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ylabel=&#39;MACD&#39; # 設定此副圖的 Y 軸標籤&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_line_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel, # 必須使用與柱狀圖相同的 Panel&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=&#39;fuchsia&#39;, # MACD 線顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=1.0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_signal_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel, # 必須使用與柱狀圖相同的 Panel&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=&#39;blue&#39;,&amp;nbsp; &amp;nbsp; # 信號線顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=1.0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.plot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; volume=True,&amp;nbsp; # 顯示成交量副圖 (Panel 1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; mav=(5, 10),&amp;nbsp; # K 線圖上疊加 5 日和 10 日均線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;K 線圖與 MACD 指標&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run ai_stock_test_5.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;[*********************100%***********************]&amp;nbsp; 1 of 1 completed&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Close&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;High&amp;nbsp; ...&amp;nbsp; MACD_histogram&amp;nbsp; MACD_signal&lt;/div&gt;&lt;div&gt;Date&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;2025-01-02&amp;nbsp; 46.854988&amp;nbsp; 47.241319&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-0.011482&amp;nbsp; &amp;nbsp; &amp;nbsp;0.202802&lt;/div&gt;&lt;div&gt;2025-01-03&amp;nbsp; 47.325832&amp;nbsp; 47.555217&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-0.023311&amp;nbsp; &amp;nbsp; &amp;nbsp;0.196974&lt;/div&gt;&lt;div&gt;2025-01-06&amp;nbsp; 48.810799&amp;nbsp; 48.847020&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.063469&amp;nbsp; &amp;nbsp; &amp;nbsp;0.212842&lt;/div&gt;&lt;div&gt;2025-01-07&amp;nbsp; 49.354084&amp;nbsp; 49.740419&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.147264&amp;nbsp; &amp;nbsp; &amp;nbsp;0.249658&lt;/div&gt;&lt;div&gt;2025-01-08&amp;nbsp; 48.617638&amp;nbsp; 49.100554&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.142791&amp;nbsp; &amp;nbsp; &amp;nbsp;0.285355&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;[5 rows x 8 columns]&lt;/div&gt;&lt;div&gt;設定字型為: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;使用指定字型: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;字型候選清單: [&#39;Microsoft JhengHei&#39;, &#39;DejaVu Sans&#39;, &#39;Arial&#39;]&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitXqpEQsEub764gGA-rLQaurfJ2aRvRBlXhjA39VcX1eZUeNdpB_eWTckaw4IiOEzFgpDx9pIcz-bu_qgd2Vmmr7cLZb5EskmQg2jwkpGEk0LoCDhlN2m_4EGyZt2TCEFiJqCxlqyVvmmhVQbg4htcdehduzaTtuMGKLgzHbtJVH-HM9Bl7kyEDNV8k4G5/s1201/ai-stock-test-macd-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1004&quot; data-original-width=&quot;1201&quot; height=&quot;268&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitXqpEQsEub764gGA-rLQaurfJ2aRvRBlXhjA39VcX1eZUeNdpB_eWTckaw4IiOEzFgpDx9pIcz-bu_qgd2Vmmr7cLZb5EskmQg2jwkpGEk0LoCDhlN2m_4EGyZt2TCEFiJqCxlqyVvmmhVQbg4htcdehduzaTtuMGKLgzHbtJVH-HM9Bl7kyEDNV8k4G5/s320/ai-stock-test-macd-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 串接 OpenAI API 計算 SMA 指標 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下面範例是用提示詞要求 LLM 提供 MACD 指標計算函式, 讓本地程式呼叫 exce() 來執行 MACD 計算函式, 最後用 kbar 套件繪製 K 線圖, 提示詞要明確給 LLM 計算 MACD 所需參數, 即快慢線與信號線週期, 另外, 為了後面用 kbar 繪製 K 線圖時取值與運算方便, 還需明確指定 MACD 傳回計算結果時的欄位名稱 :&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp; 計算 MACD 指標, 參數 : fast=12 日, slow=26 日, signal=9 日。&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp; 計算結果的欄位名稱 : MACD_line, MACD_signal, MACD_histogram。&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp; 請注意：務必在 ewm() 函式中設定 min_periods 參數（例如 slow 設為 min_periods=26），&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp; 以確保前期資料不足天數時，算出的結果必須是 NaN。&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;# ai_stock_test_6.py&lt;/div&gt;&lt;div&gt;from&amp;nbsp; openai import OpenAI, APIError&amp;nbsp;&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&lt;/div&gt;&lt;div&gt;from dotenv import dotenv_values&lt;/div&gt;&lt;div&gt;from kbar import KBar&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ask_gpt(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; messages: list[dict[str, str]],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; model: str=&#39;gpt-3.5-turbo&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ) -&amp;gt; str:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reply=client.chat.completions.create(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; model=model,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; messages=messages&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return reply.choices[0].message.content or &#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except APIError as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return e.message&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ai_helper(df, user_msg):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; role=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 作為一個專業的程式碼生成機器人，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 我需要您的協助來根據特定的用戶需求生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 為了進行下去，我將提供給您一個遵循格式 {list(df.columns)} 的 DataFrame（df）。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是仔細分析用戶的需求並相應地生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 請注意，您的回應須僅包含代碼本身，並且不應包含任何額外的資訊。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 把 user_msg 加入到 task 的敘述中，讓 AI 知道要算什麼&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; task=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是開發一個名為 &#39;calculate(df)&#39; 的 Python 函式。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 這個函式應接受一個 DataFrame 作為其參數。確保您僅使用資料集中存在的欄，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 特別是 {list(df.columns)}。&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 用戶的具體運算需求為：【 {user_msg} 】&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 處理後，該函式應返回處理過的 DataFrame。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的回應應嚴格包含 &#39;calculate(df)&#39; 函式的 Python 程式碼，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 並排除任何無關的內容。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=[{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: role},&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; reply_data=ask_gpt(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 清理 markdown 語法&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=reply_data.replace(&quot;```&quot;, &quot;&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.replace(&quot;python&quot;, &quot;&quot;)&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.strip() # 建議加上 strip() 去除頭尾多餘的空白或換行&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 傳回程式碼&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return cleaned_code&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=dotenv_values(&#39;.env&#39;)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; openai_api_key=config.get(&#39;OPENAI_API_KEY&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; client=OpenAI(api_key=openai_api_key)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2024-11-06&#39;, end=&#39;2025-01-09&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; prompt=&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 計算 MACD 指標, 參數 : fast=12 日, slow=26 日, signal=9 日。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 計算結果的欄位名稱 : MACD_line, MACD_signal, MACD_histogram。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 請注意：務必在 ewm() 函式中設定 min_periods 參數（例如 slow 設為 min_periods=26），&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 以確保前期資料不足天數時，算出的結果必須是 NaN。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; code_str=ai_helper(df, prompt)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; exec(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=calculate(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(df.tail())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_line_name=&#39;MACD_line&#39;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# 快速線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_signal_name=&#39;MACD_signal&#39;&amp;nbsp; &amp;nbsp;# 訊號線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_histogram_name=&#39;MACD_histogram&#39;&amp;nbsp; # 柱狀圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb=KBar(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; histogram_colors=[&#39;g&#39; if v &amp;gt;= 0 else &#39;r&#39; for v in df[macd_histogram_name]]&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_panel=2&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_histogram_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; type=&#39;bar&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=histogram_colors,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; alpha=0.4,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ylabel=&#39;MACD&#39; # 設定此副圖的 Y 軸標籤&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_line_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel, # 必須使用與柱狀圖相同的 Panel&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=&#39;fuchsia&#39;, # MACD 線顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=1.0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df[macd_signal_name],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; panel=macd_panel, # 必須使用與柱狀圖相同的 Panel&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; color=&#39;blue&#39;,&amp;nbsp; &amp;nbsp; # 信號線顏色&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=1.0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.plot(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; volume=True,&amp;nbsp; # 顯示成交量副圖 (Panel 1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; mav=(5, 10),&amp;nbsp; # K 線圖上疊加 5 日和 10 日均線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;K 線圖與 MACD 指標&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;執行結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run ai_stock_test_6.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;[*********************100%***********************]&amp;nbsp; 1 of 1 completed&lt;/div&gt;&lt;div&gt;def calculate(df):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fast = 12&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; slow = 26&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; signal = 9&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ema_fast = df[&#39;Close&#39;].ewm(span=fast, min_periods=fast).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ema_slow = df[&#39;Close&#39;].ewm(span=slow, min_periods=slow).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_line = ema_fast - ema_slow&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; signal_line = macd_line.ewm(span=signal, min_periods=signal).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_histogram = macd_line - signal_line&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACD_line&#39;] = macd_line&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACD_signal&#39;] = signal_line&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACD_histogram&#39;] = macd_histogram&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return df&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Close&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;High&amp;nbsp; ...&amp;nbsp; MACD_signal&amp;nbsp; MACD_histogram&lt;/div&gt;&lt;div&gt;Date&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;2025-01-02&amp;nbsp; 46.854992&amp;nbsp; 47.241323&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp;0.237614&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-0.050295&lt;/div&gt;&lt;div&gt;2025-01-03&amp;nbsp; 47.325832&amp;nbsp; 47.555217&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp;0.223323&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-0.055554&lt;/div&gt;&lt;div&gt;2025-01-06&amp;nbsp; 48.810799&amp;nbsp; 48.847020&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp;0.231812&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.033190&lt;/div&gt;&lt;div&gt;2025-01-07&amp;nbsp; 49.354080&amp;nbsp; 49.740415&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp;0.261899&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.118182&lt;/div&gt;&lt;div&gt;2025-01-08&amp;nbsp; 48.617638&amp;nbsp; 49.100554&amp;nbsp; ...&amp;nbsp; &amp;nbsp; &amp;nbsp;0.291621&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0.117175&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;[5 rows x 8 columns]&lt;/div&gt;&lt;div&gt;設定字型為: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;使用指定字型: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;字型候選清單: [&#39;Microsoft JhengHei&#39;, &#39;DejaVu Sans&#39;, &#39;Arial&#39;]&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgp3ID3j39LeoSYV0AvO6fGCRFyuwGeS9Z_bBT2W1-G8AuTyeLUyJLmlmFoQQfPdGsELUvF66efYXFRDa1tNyqouSM-sTYv9K0o2E11asGAgmaT1XyUElkhktimuraSch-ozNlrcanbMwk3pc3G_ZOrMBVElWRRF_cigEQoGN44ipX7tqBgD7eUl8p27IVo/s1200/ai-stock-test-macd-5.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1008&quot; data-original-width=&quot;1200&quot; height=&quot;269&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgp3ID3j39LeoSYV0AvO6fGCRFyuwGeS9Z_bBT2W1-G8AuTyeLUyJLmlmFoQQfPdGsELUvF66efYXFRDa1tNyqouSM-sTYv9K0o2E11asGAgmaT1XyUElkhktimuraSch-ozNlrcanbMwk3pc3G_ZOrMBVElWRRF_cigEQoGN44ipX7tqBgD7eUl8p27IVo/s320/ai-stock-test-macd-5.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;但是, 這結果與上面用 pandas_ta 計算出來的有些出入, 圖形大致是一致, 但仔細看 MACD 數據有些誤差, 我詢問 AI 這個 LLM 生成的 calculate() 算法是否正確, 結論是 : 讓 LLM 生成以 Pandas 為基礎的函式, 會與專業金融套件例如 pandas_ta 計算的結果有些出入, 因為它們在底層實作指數移動平均 (EMA) 遞迴公式時, 對第一天的 EMA 該怎麼決定有所分歧, Pandas 會直接拿資料陣列中的第一筆收盤價當作 EMA 的初始種子, 然後從第二天開始套用 EMA 公式; 而 pandas_ta 在計算 12 日 EMA 時會先精準計算前 12 天的簡單移動平均 (SMA)作為第 12 天的 EMA 初始種子, 然後從第 13 天開始才套用 EMA 公式.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在前一篇 SMA 測試不會出現因計算細節分岐產生的錯誤 (因為 SMA 沒有遞迴依賴, 也不需要初始種子),&amp;nbsp;只要指標名稱裡帶有 &quot;E&quot; (Exponential 指數型, 例如 EMA, MACD) 或者是 &quot;S&quot; / &quot;W&quot; (Smoothed / Weighted 平滑加權型, 例如 RSI, WMA), 在跨套件核對時往往就需要特別去確認底層對第一筆初始值的實作邏輯.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;從以上測試得到一個教訓 : 金融計算應該用專業套件自行編碼, 不要靠提示詞叫 AI 生成, 如果直接執行這些未經審查的程式碼, 那麼計算出來的金融資料可能是錯誤的. &lt;span style=&quot;background-color: #fcff01;&quot;&gt;如果用 pandas_ta 一行指令就能搞定的事, 為何要用附帶許多注意事項的提示詞來要求 AI 生成可能計算出錯誤結果的程式碼&lt;/span&gt;? 本系列測試將到此為止, 量化投資程式在計算金融資訊部分還是要用專業套件去計算, 不要透過 AI.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/666668762849965976/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/666668762849965976' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/666668762849965976'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/666668762849965976'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/python.html' title='Python 學習筆記 : 利用語言模型計算技術指標 (二)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitXqpEQsEub764gGA-rLQaurfJ2aRvRBlXhjA39VcX1eZUeNdpB_eWTckaw4IiOEzFgpDx9pIcz-bu_qgd2Vmmr7cLZb5EskmQg2jwkpGEk0LoCDhlN2m_4EGyZt2TCEFiJqCxlqyVvmmhVQbg4htcdehduzaTtuMGKLgzHbtJVH-HM9Bl7kyEDNV8k4G5/s72-c/ai-stock-test-macd-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-405458708133950358</id><published>2026-05-01T23:17:00.014+08:00</published><updated>2026-05-02T00:47:21.498+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Plotly"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="網頁技術"/><category scheme="http://www.blogger.com/atom/ns#" term="資料科學"/><category scheme="http://www.blogger.com/atom/ns#" term="資料視覺化"/><category scheme="http://www.blogger.com/atom/ns#" term="量化投資"/><title type='text'>Python 學習筆記 : 用 plotly 繪製 K 線圖 (二)</title><content type='html'>&lt;div&gt;在前一篇測試中已對 Plotly 的 K 線圖類別 plotly.graph_objects.CandleStick 的用法有了基本了解, 本篇旨在利用 Figure 物件的 add_trace() 方法在畫布上添加子圖 (例如成交量或 RSI 等技術指標), 同時透過 plotly.subplots.make_subplot() 函式來規劃畫布上的子圖佈局.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;本系列測試文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python.html&quot; target=&quot;_blank&quot;&gt;Python 資料視覺化學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 添加成交量子圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;要在畫布上繪製子圖有兩種方式, 第一種是使用較低階的絕對領域 (Domain) 劃分法, 此方法須手動切割 Y 軸空間自行布局, 好處是可以客製化布局, 缺點是維護難度較高, 調整彈性較差, 在 &quot;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2024/02/ai.html&quot; target=&quot;_blank&quot;&gt;最強 AI 投資分析&lt;/a&gt;&quot; 這本書的第四章就是採用此方法, 參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://www.flag.com.tw/bk/t/f3933&quot; target=&quot;_blank&quot;&gt;https://www.flag.com.tw/bk/t/f3933&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第二種方式是使用 plotly.subplots.make_subplot() 函式, 這是採用網格系統來進行排版的高階 API, 只要指定子圖的列 (row) 與欄 (col),&amp;nbsp;Plotly 會在底層自動計算所有圖表的高度, 對齊與間距, 是一種較簡易的低耦合設計.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;首先匯入子圖布局函式 make_subplots() :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;from plotly.subplots import make_subplots&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;make_subplots() 的參數說明如下表 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 159.011px;&quot;&gt;&amp;nbsp;參數名稱&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 432.011px;&quot;&gt;&amp;nbsp;說明&lt;/th&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;rows&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;子圖的列數（預設為 1）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;cols&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;子圖的欄數（預設為 1）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;shared_xaxes&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否共用 X 軸（True / False / &#39;all&#39; / &#39;rows&#39; / &#39;columns&#39;）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;shared_yaxes&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否共用 Y 軸（True / False / &#39;all&#39; / &#39;rows&#39; / &#39;columns&#39;）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;start_cell&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;子圖起始位置（&#39;top-left&#39; 或 &#39;bottom-left&#39;）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;subplot_titles&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;每個子圖的標題（list）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;specs&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;自訂每個子圖的型態與配置（例如 type=&#39;xy&#39;, &#39;domain&#39;, &#39;scene&#39; 等）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;row_heights&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;各列高度比例（list，例如 [0.7, 0.3]）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;column_widths&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;各欄寬度比例（list，例如 [0.6, 0.4]）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;horizontal_spacing&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;子圖之間的水平間距（0~1）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;vertical_spacing&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;子圖之間的垂直間距（0~1）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;insets&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定內嵌子圖（inset charts）的位置與大小&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;column_titles&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;每一欄的標題（list）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;row_titles&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;每一列的標題（list）&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;x_title&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;整體 X 軸標題&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;y_title&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;整體 Y 軸標題&lt;/td&gt;
    &lt;/tr&gt;

    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;figure&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;將子圖加入既有的 Figure 物件&lt;/td&gt;
    &lt;/tr&gt;

  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;其中最常用的參數是 rows, cols, shared_xaxes, vertical_spacing, 與 row_heights 等.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下列程式將畫布做 2 列 1 欄布局, 第一列高度占 70% 放 K 線圖 trace; 第二列高度占 30% 放成交量長條圖 (有做漲紅跌綠顏色設定) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_5.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;from plotly.subplots import make_subplots&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 建立子圖 (2 rows: 上列放 K 線圖，下列放成交量) ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=make_subplots(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;rows=2&lt;/span&gt;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;cols=1&lt;/span&gt;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; shared_xaxes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; vertical_spacing=0.03,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; row_heights=[&lt;span style=&quot;background-color: #fcff01;&quot;&gt;0.7, 0.3&lt;/span&gt;]&amp;nbsp; &amp;nbsp;# 列高度占比&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === K 線圖 ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; price=go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;Price&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;add_trace&lt;/span&gt;(price, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;row=1&lt;/span&gt;, col=1)&amp;nbsp; &amp;nbsp;# K 線圖放 row 1&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 成交量顏色（漲紅跌綠） ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; colors=[&#39;red&#39; if c &amp;gt;= o else &#39;green&#39; for c, o in zip(df[&#39;Close&#39;], df[&#39;Open&#39;])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; volume=go.Bar(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; y=df[&#39;Volume&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; marker_color=colors,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;Volume&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;add_trace&lt;/span&gt;(volume, &lt;span style=&quot;background-color: #fcff01;&quot;&gt;row=2&lt;/span&gt;, col=1)&amp;nbsp; # 成交量放 row 2&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 移除非交易日 ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_range=pd.date_range(start=df.index.min(), end=df.index.max())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks=date_range[~date_range.isin(df.index)]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks_list=breaks.tolist()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_xaxes(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rangebreaks=[{&#39;values&#39;: breaks_list}],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikemode=&#39;across&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === Y 軸 spike（兩個子圖都套用）===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_yaxes(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikemode=&#39;across&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === Layout ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;台灣五十 (0050) K線 + 成交量&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=700,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;hovermode=&#39;x unified&#39;&lt;/span&gt;&amp;nbsp; &amp;nbsp;# 很重要：跨子圖同步 hover&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, 之前沒有子圖時是呼叫 fig=go.Figure() 建立 Figure 物件, 但此處是用 make_subplots() 來建立. 其次, 有子圖的情況時, hovermode 參數務必要設為 &#39;x unified&#39;, 否則會有多個 tooltip 分散, 以及子圖之間不同步問題 (無子圖時因所有 trace 都在同一個座標系故不會有這些問題).&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzEesehnpAm6P3JXFVLs-_8nVd1DBn2TRYjvwlKgjegdKnxVqC97iXrvCJ_5ek-g2wjJ0tJiAnK4ClQjvNz2QEjqkPX0BNS7dZ_43WLCo6FAv8yBfazeIrfPxyF4uplcSt4tmOIANgAw5WPB-sJ8RhLL3jPNEo_VNdEtGFnolChs6xM-aNCFcfUsfa2U0D/s1345/plotly-candlestick-9.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1128&quot; data-original-width=&quot;1345&quot; height=&quot;268&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzEesehnpAm6P3JXFVLs-_8nVd1DBn2TRYjvwlKgjegdKnxVqC97iXrvCJ_5ek-g2wjJ0tJiAnK4ClQjvNz2QEjqkPX0BNS7dZ_43WLCo6FAv8yBfazeIrfPxyF4uplcSt4tmOIANgAw5WPB-sJ8RhLL3jPNEo_VNdEtGFnolChs6xM-aNCFcfUsfa2U0D/s320/plotly-candlestick-9.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQWYll5vdVB6kUWRQ8feGVGBclG7JZFoFfbYbRHT8FCzU7MAh3HHmEa8yslNJkPWST6dBhfcP1sKAiJY7LLn2NiXx9y-FGQPdbJ-thclyDjxOBuzSA2t7E3ovTBRC0vloa0Kai87OTlyW8Y31Lmf56h42AkRermC1xdYb-F6-cXPySJN4r68eWkyeA4Zot/s1326/plotly-candlestick-10.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1105&quot; data-original-width=&quot;1326&quot; height=&quot;267&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQWYll5vdVB6kUWRQ8feGVGBclG7JZFoFfbYbRHT8FCzU7MAh3HHmEa8yslNJkPWST6dBhfcP1sKAiJY7LLn2NiXx9y-FGQPdbJ-thclyDjxOBuzSA2t7E3ovTBRC0vloa0Kai87OTlyW8Y31Lmf56h42AkRermC1xdYb-F6-cXPySJN4r68eWkyeA4Zot/s320/plotly-candlestick-10.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;5. 添加技術指標子圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;接下來要在成交量下面添加 MACD 指標, 使用 pandas-ta 套件計算, 參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/11/python-pandas-ta_92.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 技術分析套件 pandas-ta 用法 (四)&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣畫布上總共有三個子圖 : K 線圖, 成交量, 與 MACD.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_6.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&lt;/div&gt;&lt;div&gt;from plotly.subplots import make_subplots&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 下載資料 ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 計算 MACD（pandas-ta）===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd=ta.macd(df[&#39;Close&#39;], fast=12, slow=26, signal=9)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # pandas-ta 會回傳三欄 :contentReference[oaicite:0]{index=0}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACD&#39;]=macd[&#39;MACD_12_26_9&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACDs&#39;]=macd[&#39;MACDs_12_26_9&#39;]&amp;nbsp; &amp;nbsp;# signal&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;MACDh&#39;]=macd[&#39;MACDh_12_26_9&#39;]&amp;nbsp; &amp;nbsp;# histogram&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # === 建立子圖 ===&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=make_subplots(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;rows=3&lt;/span&gt;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cols=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; shared_xaxes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; vertical_spacing=0.02,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; row_heights=[0.5, 0.2, 0.3]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 1. K線圖&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.add_trace(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;Price&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; row=1, col=1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 2. 成交量&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; vol_colors=[&#39;red&#39; if c &amp;gt;= o else &#39;green&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for c, o in zip(df[&#39;Close&#39;], df[&#39;Open&#39;])]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.add_trace(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; go.Bar(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; y=df[&#39;Volume&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; marker_color=vol_colors,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;Volume&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; row=2, col=1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 3. MACD&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # Histogram（紅綠柱）&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; macd_colors=[&#39;red&#39; if v &amp;gt;= 0 else &#39;green&#39; for v in df[&#39;MACDh&#39;]]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.add_trace(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; go.Bar(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; y=df[&#39;MACDh&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; marker_color=macd_colors,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;MACD Hist&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;row=3&lt;/span&gt;, col=1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # MACD 線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.add_trace(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; go.Scatter(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; y=df[&#39;MACD&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; line=dict(color=&#39;blue&#39;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;MACD&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;row=3&lt;/span&gt;, col=1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # Signal 線&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.add_trace(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; go.Scatter(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; y=df[&#39;MACDs&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; line=dict(color=&#39;orange&#39;),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; name=&#39;Signal&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ),&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;row=3&lt;/span&gt;, col=1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 移除非交易日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_range=pd.date_range(start=df.index.min(), end=df.index.max())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks=date_range[~date_range.isin(df.index)]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_xaxes(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rangebreaks=[{&#39;values&#39;: breaks.tolist()}],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikemode=&#39;across&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_yaxes(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; spikemode=&#39;across&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # Layout&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # =====================&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;0050 台灣五十 - K線 + 成交量 + MACD&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=900,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; hovermode=&#39;x unified&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, MACD 的紅綠柱, MACD 線與信號線都是要畫在 row=3 子圖疊在一起, 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmwhL6_qm-3LP_GL3W2LB_-cwr7Ck9jBg470GKWhq32q2VA-rIa5Sp5MTjkJMfK1YJ-dJepwlWrJxE9uYkCWS2nTsxOLvJWlO_UydP8qOmL25J6bV9NLee3OpS28SsiWEfprv1f1fJRmGSn1oyPHwufUoXMbCpQ6jlLfmS19Yth8HGtyrc2ExPdXMzeEwd/s1509/plotly-candlestick-11.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1234&quot; data-original-width=&quot;1509&quot; height=&quot;262&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmwhL6_qm-3LP_GL3W2LB_-cwr7Ck9jBg470GKWhq32q2VA-rIa5Sp5MTjkJMfK1YJ-dJepwlWrJxE9uYkCWS2nTsxOLvJWlO_UydP8qOmL25J6bV9NLee3OpS28SsiWEfprv1f1fJRmGSn1oyPHwufUoXMbCpQ6jlLfmS19Yth8HGtyrc2ExPdXMzeEwd/s320/plotly-candlestick-11.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/405458708133950358/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/405458708133950358' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/405458708133950358'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/405458708133950358'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/python-plotly-k_1.html' title='Python 學習筆記 : 用 plotly 繪製 K 線圖 (二)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzEesehnpAm6P3JXFVLs-_8nVd1DBn2TRYjvwlKgjegdKnxVqC97iXrvCJ_5ek-g2wjJ0tJiAnK4ClQjvNz2QEjqkPX0BNS7dZ_43WLCo6FAv8yBfazeIrfPxyF4uplcSt4tmOIANgAw5WPB-sJ8RhLL3jPNEo_VNdEtGFnolChs6xM-aNCFcfUsfa2U0D/s72-c/plotly-candlestick-9.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-190572974110668065</id><published>2026-05-01T14:18:00.011+08:00</published><updated>2026-05-01T19:56:38.447+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Plotly"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="網頁技術"/><category scheme="http://www.blogger.com/atom/ns#" term="資料科學"/><category scheme="http://www.blogger.com/atom/ns#" term="資料視覺化"/><category scheme="http://www.blogger.com/atom/ns#" term="量化投資"/><title type='text'>Python 學習筆記 : 用 plotly 繪製 K 線圖 (一)</title><content type='html'>&lt;div&gt;雖然 mplfinance 套件可以繪製精美的 K 線圖, 但那是沒有互動效果的靜態圖表, 也無法直接佈署在網頁應用中. 繪製互動式 K 線圖可以使用 plotly 套件的低階繪圖模組 graph_objects&amp;nbsp; (注意, plotly.express 模組沒有繪製 K 線圖功能, 它主要用來快速繪製常見統計圖表).&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列測試文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python.html&quot; target=&quot;_blank&quot;&gt;Python 資料視覺化學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;關於 plotly.graph_objects 用法參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2022/09/python-plotly.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 用 plotly 繪製互動式圖表 (一)&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1. 新版 (v4.0+) Plotly 的輸出方式 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Plotly 的繪圖輸出方式已與以往不同, v4.0 之前的 Plotly 主要商業模式是線上繪圖, 預設會將圖表上傳到他們的雲端伺服器 (Chart Studio), 如果要離線繪圖必須明確呼叫 plotly.offline 模組; 然而 Plotly v4 對原本分散的繪圖方式 (離線 vs 雲端) 進行重整, 採取了離線優先原則, 繪製圖表時不需要再區分線上或離線 (原本的雲端功能被拆分出去變成一個獨立的套件 chart-studio), 改為直接呼叫 Figure 物件的方法來處理 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;fig.show() : 用於開發過程中的快速預覽 (依據環境自動選擇方式)&lt;/li&gt;&lt;li&gt;fig.write_html() : 用於產生最終的報告檔案 (於網頁中繪製圖表)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;當呼叫 fig.show() 時, Plotly 會自動偵測程式是在 VS Code, Jupyter Notebook, Google Colab 還是純 Python 腳本中執行, 並選擇最適合的方式來顯示圖表. plotly.offline 的功能已經被 fig.write_html() 取代了, 但為了向下相容,&amp;nbsp;Plotly 官方目前仍保留對 plotly.offline.plot() 的支援.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意,&amp;nbsp;fig.write_html() 只是單純輸出 HTML 檔, 不會自動開啟瀏覽器顯示繪圖結果, 如果希望在存檔後順便開啟瀏覽器展示繪製結果, 可以傳入&amp;nbsp;auto_open=True 參數 (預設 False), 例如 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;fig.write_html(&quot;plot.html&quot;, auto_open=True)&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;或者先呼叫 write_html() 再呼叫 show() :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;fig.write_html(&quot;plot.html&quot;)&lt;/div&gt;&lt;div&gt;fig.show()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;除此之外, Plotly v4.0 也把以前的獨立套件 plotly_express&amp;nbsp;納入主套件中成為&amp;nbsp;plotly.express 模組,&amp;nbsp;確立了 Plotly 未來高階 API 使用 plotly.express;&amp;nbsp;低階 API 使用 plotly. graph_objects 的雙軌應用模式.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 建立 CandleStick 物件繪製 K 線圖 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;首先匯入 plotly.graph_objects 模組, 通常取簡名為 go :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;import plotly.graph_objects as go&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;plotly.graph_objects 模組中有一個 Candlestick 用來繪製 K 線圖 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;import plotly.graph_objects as go&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;dir(go)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;[&#39;AngularAxis&#39;, &#39;Annotation&#39;, &#39;Annotations&#39;, &#39;Bar&#39;, &#39;Barpolar&#39;, &#39;Box&#39;, &#39;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;Candlestick&lt;/span&gt;&#39;, &#39;Carpet&#39;, &#39;Choropleth&#39;, &#39;Choroplethmap&#39;, &#39;Choroplethmapbox&#39;, &#39;ColorBar&#39;, &#39;Cone&#39;, &#39;Contour&#39;, &#39;Contourcarpet&#39;, &#39;Contours&#39;, &#39;Data&#39;, &#39;Densitymap&#39;, &#39;Densitymapbox&#39;, &#39;ErrorX&#39;, &#39;ErrorY&#39;, &#39;ErrorZ&#39;, &#39;Figure&#39;, &#39;FigureWidget&#39;, &#39;Font&#39;, &#39;Frame&#39;, &#39;Frames&#39;, &#39;Funnel&#39;, &#39;Funnelarea&#39;, &#39;Heatmap&#39;, &#39;Histogram&#39;, &#39;Histogram2d&#39;, &#39;Histogram2dContour&#39;, &#39;Histogram2dcontour&#39;, &#39;Icicle&#39;, &#39;Image&#39;, &#39;Indicator&#39;, &#39;Isosurface&#39;, &#39;Layout&#39;, &#39;Legend&#39;, &#39;Line&#39;, &#39;Margin&#39;, &#39;Marker&#39;, &#39;Mesh3d&#39;, &#39;Ohlc&#39;, &#39;Parcats&#39;, &#39;Parcoords&#39;, &#39;Pie&#39;, &#39;RadialAxis&#39;, &#39;Sankey&#39;, &#39;Scatter&#39;, &#39;Scatter3d&#39;, &#39;Scattercarpet&#39;, &#39;Scattergeo&#39;, &#39;Scattergl&#39;, &#39;Scattermap&#39;, &#39;Scattermapbox&#39;, &#39;Scatterpolar&#39;, &#39;Scatterpolargl&#39;, &#39;Scattersmith&#39;, &#39;Scatterternary&#39;, &#39;Scene&#39;, &#39;Splom&#39;, &#39;Stream&#39;, &#39;Streamtube&#39;, &#39;Sunburst&#39;, &#39;Surface&#39;, &#39;Table&#39;, &#39;Trace&#39;, &#39;Treemap&#39;, &#39;Violin&#39;, &#39;Volume&#39;, &#39;Waterfall&#39;, &#39;XAxis&#39;, &#39;XBins&#39;, &#39;YAxis&#39;, &#39;YBins&#39;, &#39;ZAxis&#39;, &#39;bar&#39;, &#39;barpolar&#39;, &#39;box&#39;, &#39;candlestick&#39;, &#39;carpet&#39;, &#39;choropleth&#39;, &#39;choroplethmap&#39;, &#39;choroplethmapbox&#39;, &#39;cone&#39;, &#39;contour&#39;, &#39;contourcarpet&#39;, &#39;densitymap&#39;, &#39;densitymapbox&#39;, &#39;funnel&#39;, &#39;funnelarea&#39;, &#39;heatmap&#39;, &#39;histogram&#39;, &#39;histogram2d&#39;, &#39;histogram2dcontour&#39;, &#39;icicle&#39;, &#39;image&#39;, &#39;indicator&#39;, &#39;isosurface&#39;, &#39;layout&#39;, &#39;mesh3d&#39;, &#39;ohlc&#39;, &#39;parcats&#39;, &#39;parcoords&#39;, &#39;pie&#39;, &#39;sankey&#39;, &#39;scatter&#39;, &#39;scatter3d&#39;, &#39;scattercarpet&#39;, &#39;scattergeo&#39;, &#39;scattergl&#39;, &#39;scattermap&#39;, &#39;scattermapbox&#39;, &#39;scatterpolar&#39;, &#39;scatterpolargl&#39;, &#39;scattersmith&#39;, &#39;scatterternary&#39;, &#39;splom&#39;, &#39;streamtube&#39;, &#39;sunburst&#39;, &#39;surface&#39;, &#39;table&#39;, &#39;treemap&#39;, &#39;violin&#39;, &#39;volume&#39;, &#39;waterfall&#39;]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 Candlestick 類別的建構式&amp;nbsp;Candlestick() 會建立一個&amp;nbsp;Candlestick 物件, 這在 Plotly 文件中被稱為&amp;nbsp;Trace 物件 (畫布上的數據內容, 例如一組 K 線).&amp;nbsp;一個完整的 Plotly 圖表 (Figure) 是由 data 和 layout 兩大部分組成的, 而 Candlestick 物件就是放在 data 清單中的一個成員.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Candlestick() 建構式的參數如下表 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 189.011px;&quot;&gt;&amp;nbsp;參數名稱&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 402.011px;&quot;&gt;&amp;nbsp;說明&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;x&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;時間序列數據（通常為日期字串或 datetime 物件）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;open&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;開盤價序列 (Open prices)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;high&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;最高價序列 (High prices)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;low&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;最低價序列 (Low prices)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;close&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;收盤價序列 (Close prices)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;increasing_line_color&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;上漲時 K 線外框與影線的顏色（例：&#39;red&#39;）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;decreasing_line_color&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;下跌時 K 線外框與影線的顏色（例：&#39;green&#39;）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;name&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;此數據序列在圖例中顯示的名稱。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;text&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;滑鼠懸停時顯示的額外文字內容。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;hoverinfo&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定懸停顯示的資訊內容（如 &#39;all&#39;, &#39;x+y&#39; 等）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;opacity&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定 K 線圖層的透明度（0.0 到 1.0）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;whiskerwidth&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定上下影線相對於 K 線實體寬度的比例。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;其中前 7 個參數 x, open, high, low, close, 以及 increasing_line_color 與 decreasing_line_color (漲跌顏色, 漲紅跌綠)&amp;nbsp;最常用, 例如 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;price=go.Candlestick(&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; x=[&#39;2026-05-01&#39;, &#39;2026-05-02&#39;],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; open=[100, 110],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; high=[115, 120],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; low=[95, 105],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; close=[110, 105],&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&amp;nbsp; &amp;nbsp; )&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;type(price)&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;lt;class &#39;plotly.graph_objs._candlestick.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;Candlestick&lt;/span&gt;&#39;&amp;gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣就建立了一個 CandleStick 物件了. 接下來只要將此 trace 放入串列, 傳給 go.Figure() 的 data 參數即可建立 Figure 畫布物件 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig=go.Figure(data=[price])&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;type(fig)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;lt;class &#39;plotly.graph_objs._figure.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;Figure&lt;/span&gt;&#39;&amp;gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;最後呼叫 fig.show() 即可展示所繪製之圖表 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.show()&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;fig.show() 會開啟一個 127.0.0.1:port 的網頁來繪製圖表 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihy0qfM8zX4ZJXTOu-1EH8WQlhHMoVGKEGQYwLWeNcvjSxAQx9BDcbl7g8Wbp0rQ50q2HU4wiGt8W3NWIYafLZUT0DLRqJJVPA6se_cSw9BcodEo8im6xDopd6R6EFcoiQM5b8N5lRMn_twSKT2eQnFA_rFdfihN84xIRXt5bEDi65tNHgfX1KPxhfSPbv/s2330/plotly-candlestick-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;967&quot; data-original-width=&quot;2330&quot; height=&quot;133&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihy0qfM8zX4ZJXTOu-1EH8WQlhHMoVGKEGQYwLWeNcvjSxAQx9BDcbl7g8Wbp0rQ50q2HU4wiGt8W3NWIYafLZUT0DLRqJJVPA6se_cSw9BcodEo8im6xDopd6R6EFcoiQM5b8N5lRMn_twSKT2eQnFA_rFdfihN84xIRXt5bEDi65tNHgfX1KPxhfSPbv/s320/plotly-candlestick-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見當滑鼠移到 K 棒上時就會動態地彈出其 OHLC 價格資訊. 注意, Plotly 預設會在 K 線圖底下開啟一個範圍滑桿 (RangeSlider 物件), 可透過滑動或縮放來觀察不同時段的價格細節. 若想取消此預設之範圍滑桿, 可呼叫 fig.update_layout() 並傳入&amp;nbsp;xaxis_rangeslider_visible=False 參數 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.update_layout(xaxis_rangeslider_visible=False)&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;Figure({&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;data&#39;: [{&#39;close&#39;: [110, 105],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;decreasing&#39;: {&#39;line&#39;: {&#39;color&#39;: &#39;green&#39;}},&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;high&#39;: [115, 120],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;increasing&#39;: {&#39;line&#39;: {&#39;color&#39;: &#39;red&#39;}},&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;low&#39;: [95, 105],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;open&#39;: [100, 110],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;type&#39;: &#39;candlestick&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;x&#39;: [2026-05-01, 2026-05-02]}],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;layout&#39;: {&#39;template&#39;: &#39;...&#39;, &#39;xaxis&#39;: {&#39;rangeslider&#39;: {&#39;visible&#39;: False}}}&lt;/div&gt;&lt;div&gt;})&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;fig.show()&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQ2NA_LqjAls5oDUESTYFOF-M6jEbKZlhj4xbh0fXj9rkDVbcPD0ED2HCeS6sPanc2gXtp7yKZ-vsgIhFVW7Jp0FzWz-b_EdF519DdssTdPDBwhB4qNVHGjjwzUlgrRkW2-ybiIng-4L_opsQj8nA7L43nmjOoc_pMIRqtpFX7UOBOs7kndFHlgysEY-mi/s2334/plotly-candlestick-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1023&quot; data-original-width=&quot;2334&quot; height=&quot;140&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQ2NA_LqjAls5oDUESTYFOF-M6jEbKZlhj4xbh0fXj9rkDVbcPD0ED2HCeS6sPanc2gXtp7yKZ-vsgIhFVW7Jp0FzWz-b_EdF519DdssTdPDBwhB4qNVHGjjwzUlgrRkW2-ybiIng-4L_opsQj8nA7L43nmjOoc_pMIRqtpFX7UOBOs7kndFHlgysEY-mi/s320/plotly-candlestick-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;fig.update_layout() 是 Plotly 中最核心的佈局控制方法, 它負責處理圖表內除了數據本身以外的所有視覺元素, 例如標題, 座標軸, 圖例及畫布大小等, 常用參數如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 159.011px;&quot;&gt;&amp;nbsp;參數名稱&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 502.011px;&quot;&gt;&amp;nbsp;說明&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;title&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;圖表標題。可傳入字串或 dict（包含 text, font, x, y 等屬性）。預設為 None。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;width / height&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定畫布的寬度與高度（像素）。預設通常隨容器自動縮放。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;xaxis / yaxis&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定座標軸屬性（如標題、範圍、網格）。例如 xaxis_title=&#39;時間&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;xaxis_rangeslider_visible&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定 X 軸下方的範圍滑桿是否顯示。預設值 True。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;showlegend&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示圖例。預設值：當有多組數據序列時為 True。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;legend&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定圖例的位置與外觀（如 x, y 位置、orientation &#39;h&#39; 或 &#39;v&#39;）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;margin&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定圖表與邊界的距離。傳入 dict(l, r, t, b) 分別代表左、右、上、下邊距。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;template&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定圖表主題（如 &#39;plotly_dark&#39;, &#39;ggplot2&#39;, &#39;seaborn&#39; 等）。預設為 &#39;plotly&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;hovermode&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定懸停資訊的觸發模式（&#39;x&#39;, &#39;y&#39;, &#39;closest&#39;, False）。預設為 &#39;closest&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;font&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定圖表全域字體（family, size, color）。預設字體依環境而定。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;paper_bgcolor&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定整張畫布（紙張）的背景顏色。預設為 &#39;#fff&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;plot_bgcolor&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定繪圖區域（座標軸內）的背景顏色。預設為 &#39;#E5ECF6&#39;。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這些參數中以 title, width, heigh, 與 showlegend 等最常用.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下面範例從 yfinance 取得股價資料後用 plotly 繪製 K 線圖 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_1.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; price=go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=go.Figure(data=[price])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;台灣五十股價&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=600,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此處以 width 與 height 參數設定畫布尺寸, 以 title 設定圖表標題, 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgU4H86fRxYSAk0GF8Z2ISdZHRG9G25N7VzRQf6ePsX7DQ08jYjnh333iStqcITy2XVXKE2GDG7AH6mt7Jd1HuCBS6eDDOgOWhy5FbzSK1emM1km0ehyphenhyphenw737SS0XbIwritMECIH8MgHfOCnlmE5aI2TbcKbx2l5fgVBKv0AAXT9Wm44P4HL-12U00QkQ88c/s1238/plotly-candlestick-3.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;942&quot; data-original-width=&quot;1238&quot; height=&quot;243&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgU4H86fRxYSAk0GF8Z2ISdZHRG9G25N7VzRQf6ePsX7DQ08jYjnh333iStqcITy2XVXKE2GDG7AH6mt7Jd1HuCBS6eDDOgOWhy5FbzSK1emM1km0ehyphenhyphenw737SS0XbIwritMECIH8MgHfOCnlmE5aI2TbcKbx2l5fgVBKv0AAXT9Wm44P4HL-12U00QkQ88c/s320/plotly-candlestick-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;3. 移除 K 線圖中的非交易日空格 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;仔細看上面範例中的 K 線圖, 會發現例假日股市休息, 從 yfinance 取得的收盤資料都會跳過這些日期, 只傳回交易日之資料, 但 Plotly 在繪製 K 線圖時會自動把 X 軸的日期序列補齊, 但因為無 OHLC 欄位值, 所以 K 棒為空格, 看起來不連續, 這可以透過呼叫 fig.update_xaxes() 方法, 並傳入rangebreaks 參數來移除, 其值為一個非交易日的日期串列的字典 (鍵為 values), 例如 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_2.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;import pandas as pd&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; price=go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=go.Figure(data=[price])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 移除 X 軸的非交易日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_range=pd.date_range(start=df.index.min(), end=df.index.max())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks=date_range[~date_range.isin(df.index)]&amp;nbsp; # 取出 df 空缺之日期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks_list=breaks.tolist()&amp;nbsp; # 轉成串列&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.&lt;span style=&quot;background-color: #fcff01;&quot;&gt;update_xaxes&lt;/span&gt;(rangebreaks=[{&#39;values&#39;: breaks_list}])&amp;nbsp; # 移除非交易日空缺&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;台灣五十股價&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=600,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此處使用 Pandas 的 date_range() 函式取得 df 起迄日期間的所有連續日期序列, 然後用串列運算與 df 比對抓出 df 中空缺之日期即為非交易日, 轉成串列後做為字典 values 鍵之值. 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTfUq3-Pe7nn6jAksYpGisls7PJjVbaPKnRByoEoEaz3OHVgY0IZ_7GqMliyFINZhPrctsz1k7ZW7x3o3U8nANWAPAbHxRV9RhkOTzhyptbEjbjQpJNv8kF6m9w0PzI7XZbR7YZTfjuhJrv4Co6iQyWsq4dW9NQf18GgTO73SxooD5DT1P_-w_9NwtJLan/s1245/plotly-candlestick-4.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;948&quot; data-original-width=&quot;1245&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTfUq3-Pe7nn6jAksYpGisls7PJjVbaPKnRByoEoEaz3OHVgY0IZ_7GqMliyFINZhPrctsz1k7ZW7x3o3U8nANWAPAbHxRV9RhkOTzhyptbEjbjQpJNv8kF6m9w0PzI7XZbR7YZTfjuhJrv4Co6iQyWsq4dW9NQf18GgTO73SxooD5DT1P_-w_9NwtJLan/s320/plotly-candlestick-4.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見非交易日的空 K 棒已消失, 整個 K 線圖 X 軸看起來是連續無空缺了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;fig.update_xaxes() 常用參數如下表 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 139.011px;&quot;&gt;&amp;nbsp;參數名稱&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 482.011px;&quot;&gt;&amp;nbsp;說明&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;title&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸標題。可傳入字串或 dict(text=&#39;...&#39;, font=...)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;type&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;座標軸類型：&#39;linear&#39; (數值), &#39;date&#39; (時間), &#39;category&#39; (類別), &#39;log&#39; (對數)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;range&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定顯示範圍 [min, max]，對於時間軸 [&#39;2026-01-01&#39;, &#39;2026-05-01&#39;]。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;rangebreaks&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;隱藏特定時間區段（如週末或收盤時段）。常用於金融圖表。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;rangeslider_visible&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示底部的時間範圍滑桿（True/False）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;showgrid&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示垂直網格線（True/False）。預設為 True。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;gridcolor / gridwidth&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定網格線的顏色與寬度。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;tickformat&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定刻度文字格式（如 &#39;%Y-%m-%d&#39; 顯示日期，&#39;.2f&#39; 顯示兩位小數）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;tickangle&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;刻度文字的旋轉角度（例如 -45 代表順時針旋轉 45 度）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;side&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸的位置。可設定為 &#39;bottom&#39; (預設) 或 &#39;top&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;nticks&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定刻度的最大數量（Plotly 會嘗試在此範圍內自動分配）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;fixedrange&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否鎖定縮放（True 代表禁止使用者縮放該軸）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;showspikes&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示 Y 軸輔助線。預設 False (不顯示)。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikemode&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸輔助線模式 : &#39;toaxis&#39; (預設指向坐標軸)、&#39;across&#39; (橫跨整個繪圖區)。&lt;/td&gt;
    &lt;/tr&gt;  
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikecolor&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸輔助線顏色 : 顏色名稱或色碼 (預設 None 視資料點自動指配)。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikethickness&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸輔助線厚度 : 預設 3px。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikedash&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;X 軸輔助線樣式 : &#39;dash&#39; (預設), &#39;dot&#39;, &#39;solid&#39;。&lt;/td&gt;
    &lt;/tr&gt;           
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;除了有 X 軸設定方法 fig.update_xaxes() 外, 還有 Y 軸設定方法 fig.update_yaxes(), 參數結構類似 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 139.011px;&quot;&gt;&amp;nbsp;參數名稱&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 482.011px;&quot;&gt;&amp;nbsp;說明&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;title&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸標題。可設定為字串或 dict(text=&#39;...&#39;, font=...)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;type&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;座標軸類型：&#39;linear&#39; (預設)、&#39;log&#39; (對數軸)、&#39;date&#39;。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;range&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;設定 Y 軸顯示數值範圍。例如 [0, 1000]。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;autorange&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否自動縮放範圍：True, False 或 &#39;reversed&#39;（數值由大到小反轉）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;showgrid&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示水平網格線（True/False）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;zeroline&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否加粗顯示數值為 0 的那條基準線。預設為 True。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;tickformat&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;刻度數值格式。例如 &#39;,d&#39; (千分位整數)、&#39;.2f&#39; (兩位小數)、&#39;%&#39; (百分比)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;tickprefix / ticksuffix&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;在刻度數值前後加上符號（如 tickprefix=&#39;$&#39; 或 ticksuffix=&#39;元&#39;）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;side&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸的位置。可設定為 &#39;left&#39; (預設) 或 &#39;right&#39; (常見於雙 Y 軸設計)。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;secondary_y&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;多軸圖表中指定此資料是否為第二個 Y 軸（配合 make_subplots 使用）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;fixedrange&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否固定 Y 軸。設為 True 時，使用者無法透過滑鼠縮放 Y 軸方向。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;showspikes&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;是否顯示 Y 軸輔助線。預設 False (不顯示)。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikemode&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸輔助線模式 : &#39;toaxis&#39; (預設指向坐標軸)、&#39;across&#39; (橫跨整個繪圖區)。&lt;/td&gt;
    &lt;/tr&gt;  
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikecolor&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸輔助線顏色 : 顏色名稱或色碼 (預設 None 視資料點自動指配)。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikethickness&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸輔助線厚度 : 預設 3px。&lt;/td&gt;
    &lt;/tr&gt;   
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;spikedash&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;&amp;nbsp;Y 軸輔助線樣式 : &#39;dash&#39; (預設), &#39;dot&#39;, &#39;solid&#39;。&lt;/td&gt;
    &lt;/tr&gt;       
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 添加輔助十字線 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;輔助十字線是當滑鼠在畫布上移動時會出現一個平行於 X 與 Y 軸十字線的互動效果, 開啟輔助十字線顯示需要將 fig.update_xaxes() 與 fig.update_yaxes() 的 showspike 參數設為 True. 例如 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_3.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; price=go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=go.Figure(data=[price])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 移除 X 軸的非交易日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_range=pd.date_range(start=df.index.min(), end=df.index.max())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks=date_range[~date_range.isin(df.index)]&amp;nbsp; # 取出 df 空缺之日期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks_list=breaks.tolist()&amp;nbsp; # 轉成串列&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_xaxes(rangebreaks=[{&#39;values&#39;: breaks_list}],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;showspikes=True&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_yaxes(&lt;span style=&quot;background-color: #fcff01;&quot;&gt;showspikes=True&lt;/span&gt;)&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;台灣五十股價&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=600,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此處將 X/Y 軸設定參數 showspikes 設為 True 後, 當滑鼠移到 K 棒上面時就會顯示指向座標軸的 輔助線, 由於 spikemode 預設 &#39;toaxis&#39;, 故輔助線是指向坐標軸, 而非貫穿繪圖區的十字線; 由於 spikecolor 預設為 None, 所以滑鼠移到紅 K 棒時輔助線為紅色, 移到綠 K 棒時輔助線為綠色 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5vSVzgj994Nf0RKgE_blCdZUzFipIO0jdf4ubXjyBPc47zthgPPy41CrODy8MbemOVYe3lZoSD_ZINyN-UCIbNZLj_LCUHvwpMlOEuT_xxlXRT0dgH0xuRM6IVbj9V6AnG5UzoVtheaKsjzyDwTQpj0XnT9Nwbe8DfzZMvi_fLccCVpf033hfmmUXNjF7/s1243/plotly-candlestick-5.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;935&quot; data-original-width=&quot;1243&quot; height=&quot;241&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5vSVzgj994Nf0RKgE_blCdZUzFipIO0jdf4ubXjyBPc47zthgPPy41CrODy8MbemOVYe3lZoSD_ZINyN-UCIbNZLj_LCUHvwpMlOEuT_xxlXRT0dgH0xuRM6IVbj9V6AnG5UzoVtheaKsjzyDwTQpj0XnT9Nwbe8DfzZMvi_fLccCVpf033hfmmUXNjF7/s320/plotly-candlestick-5.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyWTYN5aeMYWkz4dFFrP_i83vLWsSQ-WSfRMWTOrO-wL7phFMwuRtDKUFvRvWjRIL2V1a_kDbODBSYtYUF8uvmoJKzNxxaug8h-8UXBp8qIqhaiKQE4oHQ0iQITpOn0fiBBfrx1XQuDDtuIwB48UoJv0Nxr-WNgg6dYUgexJze16hXAEzfsCLCFrabGaoH/s1246/plotly-candlestick-6.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;934&quot; data-original-width=&quot;1246&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyWTYN5aeMYWkz4dFFrP_i83vLWsSQ-WSfRMWTOrO-wL7phFMwuRtDKUFvRvWjRIL2V1a_kDbODBSYtYUF8uvmoJKzNxxaug8h-8UXBp8qIqhaiKQE4oHQ0iQITpOn0fiBBfrx1XQuDDtuIwB48UoJv0Nxr-WNgg6dYUgexJze16hXAEzfsCLCFrabGaoH/s320/plotly-candlestick-6.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;下面是傳入全部輔助線參數的範例 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_candlestick_4.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import plotly.graph_objects as go&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2026-03-01&#39;, end=&#39;2026-04-30&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; price=go.Candlestick(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; x=df.index,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; open=df[&#39;Open&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; high=df[&#39;High&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; low=df[&#39;Low&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; close=df[&#39;Close&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; increasing_line_color=&#39;red&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; decreasing_line_color=&#39;green&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig=go.Figure(data=[price])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 移除 X 軸的非交易日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; date_range=pd.date_range(start=df.index.min(), end=df.index.max())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks=date_range[~date_range.isin(df.index)]&amp;nbsp; # 取出 df 空缺之日期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; breaks_list=breaks.tolist()&amp;nbsp; # 轉成串列&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_xaxes(rangebreaks=[{&#39;values&#39;: breaks_list}],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikemode=&#39;across&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_yaxes(showspikes=True,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikethickness=1,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikecolor=&#39;blue&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikedash=&#39;dot&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;spikemode=&#39;across&#39;)&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.update_layout(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=&#39;台灣五十股價&#39;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; width=800,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; height=600,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xaxis_rangeslider_visible=False,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; #&lt;span style=&quot;background-color: #fcff01;&quot;&gt;hovermode=&#39;x&#39;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fig.show()&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此例傳入輔助線參數更改預設值, 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6oKbySrRGSbvDEBwj4FGMSRM1hHHbQqJXTRBBnFar0g-JYTRkcKoVMLDqL1HnQTR-VkHWe-6nX_pobDf-w-Ti91q0hiFKAlo_Ju4HO5RvvAccx9XFYWxMJ_lh0HG9UxEwBdhY9J-BeAd9oOGatGDRSjN_G6ez4mzlSdTtQCTvJx9Er7Y8N4LEP79NzfqH/s1236/plotly-candlestick-7.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;954&quot; data-original-width=&quot;1236&quot; height=&quot;247&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh6oKbySrRGSbvDEBwj4FGMSRM1hHHbQqJXTRBBnFar0g-JYTRkcKoVMLDqL1HnQTR-VkHWe-6nX_pobDf-w-Ti91q0hiFKAlo_Ju4HO5RvvAccx9XFYWxMJ_lh0HG9UxEwBdhY9J-BeAd9oOGatGDRSjN_G6ez4mzlSdTtQCTvJx9Er7Y8N4LEP79NzfqH/s320/plotly-candlestick-7.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLNSrt8QxWj_gOlHBuvgthnxt12UuM6Ar952o7o1l2I3jZkuQqW2HpgnXgVAbTB778_62il3XpnddW8TIXXTLZABqc00eJicBhj9xCXLgdJnNA0dg6rIfhbbNxmJBoJzvWLJtQ7eEcie87TfXtipwJL4jcd2EVXKIUhNFUUtpxkKcXQLXbWzlC38t-ALrB/s1265/plotly-candlestick-8.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;954&quot; data-original-width=&quot;1265&quot; height=&quot;241&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjLNSrt8QxWj_gOlHBuvgthnxt12UuM6Ar952o7o1l2I3jZkuQqW2HpgnXgVAbTB778_62il3XpnddW8TIXXTLZABqc00eJicBhj9xCXLgdJnNA0dg6rIfhbbNxmJBoJzvWLJtQ7eEcie87TfXtipwJL4jcd2EVXKIUhNFUUtpxkKcXQLXbWzlC38t-ALrB/s320/plotly-candlestick-8.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;可見 spikethickness=1 讓輔助線變細了, spikecolor=&#39;blue&#39; 則使其顏色不論紅 K 或綠 K 都顯示藍色; 而 spikemode=&#39;across&#39; 則使輔助線貫穿整個畫布成為名符其實的十字線. 注意, 預設須當滑鼠移到 K 棒上時才會顯示輔助線, 如果要讓滑鼠在畫布任何位置都顯示輔助線, 則在呼叫 fig.update_layout() 要傳入&amp;nbsp;hovermode=&#39;x&#39; 參數, 因為 K 線圖是按時間 (X 軸) 排列的, 每一格時間通常都有資料, 所以滑鼠左右移動時, 輔助線會非常流暢地跟隨.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/190572974110668065/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/190572974110668065' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/190572974110668065'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/190572974110668065'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/python-plotly-k.html' title='Python 學習筆記 : 用 plotly 繪製 K 線圖 (一)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihy0qfM8zX4ZJXTOu-1EH8WQlhHMoVGKEGQYwLWeNcvjSxAQx9BDcbl7g8Wbp0rQ50q2HU4wiGt8W3NWIYafLZUT0DLRqJJVPA6se_cSw9BcodEo8im6xDopd6R6EFcoiQM5b8N5lRMn_twSKT2eQnFA_rFdfihN84xIRXt5bEDi65tNHgfX1KPxhfSPbv/s72-c/plotly-candlestick-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1009135875055078322</id><published>2026-05-01T00:38:00.001+08:00</published><updated>2026-05-01T00:38:28.547+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="網購"/><title type='text'>momo 購買泰達椅 Pro</title><content type='html'>&lt;div&gt;因下周要帶爸與岳父母去沖繩旅遊, 為了在排隊時沒椅子坐, 上 momo 買了僅 1 kg 的泰達椅 Pro :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=11843761&quot; target=&quot;_blank&quot;&gt;Ta-Da 泰達 最新升級版 泰達隨身椅PRO 原廠一年保固(健走杖釣魚椅 戶外拐杖椅 可折疊座杖 健行助力杖)&lt;/a&gt; $2980&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9gWpDhzUSeyVIcx2Ob8xM81y4dVamw_vwB0YT5aAVlgyAQG6X8zx1wf16adVpbFlzZ-mdoSc6XHXlU34cuDtY3L4m3nQXl61gJdMVqhej1g448EiOyTg9uCbU5TX_m5PsW3qs1QnuEm8sjYTZIHoQ8AHPSDPCQutX_nbxqQoudQTqMiwyg83N-cB1YCN2/s1759/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-1.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;935&quot; data-original-width=&quot;1759&quot; height=&quot;170&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9gWpDhzUSeyVIcx2Ob8xM81y4dVamw_vwB0YT5aAVlgyAQG6X8zx1wf16adVpbFlzZ-mdoSc6XHXlU34cuDtY3L4m3nQXl61gJdMVqhej1g448EiOyTg9uCbU5TX_m5PsW3qs1QnuEm8sjYTZIHoQ8AHPSDPCQutX_nbxqQoudQTqMiwyg83N-cB1YCN2/s320/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;滿一件折 350, 還另送 350 元 momo 幣 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS9YA-LacNetWGQhiMp8DQQol5sK95Aso45YR_zEusMCEjXhZeuBvjveVRvN5van1Hc4IRYcyhyphenhyphenyyUzPP8F73tyZKD9DEzkSZ4358B4o_YO5c6z9zAAOeBAbpocp1UtQtsrirNmzi4TYv2rxzGY9VXRnqHzqe0WxpAekrzrQxjSoNce-akewiQiDJvNaBQ/s1796/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-3.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;530&quot; data-original-width=&quot;1796&quot; height=&quot;94&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS9YA-LacNetWGQhiMp8DQQol5sK95Aso45YR_zEusMCEjXhZeuBvjveVRvN5van1Hc4IRYcyhyphenhyphenyyUzPP8F73tyZKD9DEzkSZ4358B4o_YO5c6z9zAAOeBAbpocp1UtQtsrirNmzi4TYv2rxzGY9VXRnqHzqe0WxpAekrzrQxjSoNce-akewiQiDJvNaBQ/s320/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicnR-LPrGZPkIcF5nrSDV7RF45vIW6Mn8xew7HxhqzuhhHVKsME9q181HOK4OsHTlYX-_3iuhDiIm_wAetw0_myTQSls1vAKn8qlbWZImtUf1nVjnssoIrFzQqVkk6IF8BIjYiyrrg2RKtwu9C3TKx8dKlGfH2ZpFWkT86cm68HD21xvOZbo8C8BCvKvNA/s1705/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-2.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;742&quot; data-original-width=&quot;1705&quot; height=&quot;139&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicnR-LPrGZPkIcF5nrSDV7RF45vIW6Mn8xew7HxhqzuhhHVKsME9q181HOK4OsHTlYX-_3iuhDiIm_wAetw0_myTQSls1vAKn8qlbWZImtUf1nVjnssoIrFzQqVkk6IF8BIjYiyrrg2RKtwu9C3TKx8dKlGfH2ZpFWkT86cm68HD21xvOZbo8C8BCvKvNA/s320/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;實付 2630 元, 送 momo 幣 350 元, 實際上只花 2280 元.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1009135875055078322/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1009135875055078322' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1009135875055078322'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1009135875055078322'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/05/momo-pro.html' title='momo 購買泰達椅 Pro'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9gWpDhzUSeyVIcx2Ob8xM81y4dVamw_vwB0YT5aAVlgyAQG6X8zx1wf16adVpbFlzZ-mdoSc6XHXlU34cuDtY3L4m3nQXl61gJdMVqhej1g448EiOyTg9uCbU5TX_m5PsW3qs1QnuEm8sjYTZIHoQ8AHPSDPCQutX_nbxqQoudQTqMiwyg83N-cB1YCN2/s72-c/momo%20%E8%B3%BC%E8%B2%B7%E6%B3%B0%E9%81%94%E6%A4%85%20Pro-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-4431168071299862016</id><published>2026-04-30T11:53:00.005+08:00</published><updated>2026-04-30T11:53:35.897+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="網購"/><category scheme="http://www.blogger.com/atom/ns#" term="能源"/><title type='text'>momo 購買小米 20000 mAh 行動電源</title><content type='html'>因下周要帶爸與岳父母去沖繩玩, 今天上網買了一顆小米行動電源 :&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=13951759&quot; target=&quot;_blank&quot;&gt;小米 官方旗艦館 PB2020MI 自帶線行動電源 20000 22.5W 2孔輸出(USB-A/USB-C 可充/自帶線/74Wh)&lt;/a&gt;&amp;nbsp;$599&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;100Wh 以下可帶上飛機&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW2uQGYod6SIkzO11U2_eshILwgHMknSJBxfuRvhC8rvo-CHzO8AYwQghMEud1EKJLmXdhDbUaybxpyPCOvUIGijlGSwZv6jlDcqMLTI_PeMV3rUl21hLN23hedFzgXoIQxZg4GnMauesveeZHJTA6d6qNPFWmTke1spXPLKHKy-sddQavv8G8s0opyfwh/s1671/momo%20%E8%B3%BC%E8%B2%B7%E5%B0%8F%E7%B1%B3%E8%A1%8C%E5%8B%95%E9%9B%BB%E6%BA%90.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;627&quot; data-original-width=&quot;1671&quot; height=&quot;120&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW2uQGYod6SIkzO11U2_eshILwgHMknSJBxfuRvhC8rvo-CHzO8AYwQghMEud1EKJLmXdhDbUaybxpyPCOvUIGijlGSwZv6jlDcqMLTI_PeMV3rUl21hLN23hedFzgXoIQxZg4GnMauesveeZHJTA6d6qNPFWmTke1spXPLKHKy-sddQavv8G8s0opyfwh/s320/momo%20%E8%B3%BC%E8%B2%B7%E5%B0%8F%E7%B1%B3%E8%A1%8C%E5%8B%95%E9%9B%BB%E6%BA%90.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;用掉 74 元 momo 幣實付 525 元.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/4431168071299862016/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/4431168071299862016' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/4431168071299862016'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/4431168071299862016'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/momo-20000-mah.html' title='momo 購買小米 20000 mAh 行動電源'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW2uQGYod6SIkzO11U2_eshILwgHMknSJBxfuRvhC8rvo-CHzO8AYwQghMEud1EKJLmXdhDbUaybxpyPCOvUIGijlGSwZv6jlDcqMLTI_PeMV3rUl21hLN23hedFzgXoIQxZg4GnMauesveeZHJTA6d6qNPFWmTke1spXPLKHKy-sddQavv8G8s0opyfwh/s72-c/momo%20%E8%B3%BC%E8%B2%B7%E5%B0%8F%E7%B1%B3%E8%A1%8C%E5%8B%95%E9%9B%BB%E6%BA%90.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-7247231832036012440</id><published>2026-04-30T11:42:00.001+08:00</published><updated>2026-04-30T11:42:07.430+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Claude Code"/><category scheme="http://www.blogger.com/atom/ns#" term="網購"/><title type='text'>(補記) 購買 HiSKIO 課程 : Claude Code 深度應用</title><content type='html'>&lt;div&gt;我可能線上課程買太多買到頭昏了, 最近在臉書看到一門 Claude Code 課程 56 折即將結束, 趁中午截止前上網想先買下 (雖然可能要半年後才會開啟 Claude Code 付費) :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://hiskio.com/courses/2475&quot; target=&quot;_blank&quot;&gt;https://hiskio.com/courses/2475&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;沒想到登入 HiSKIO 赫然看到網站提醒 : 你已購買本課程! 蝦米? 查了訂購記錄, 這門課我今年 2/6 就買了, 當時可能一忙忘了記下來. 這就像我存摺有很多本 (但也不至於到 100 本啦) 藏錢都藏到忘記自己很有錢了. 補記一下免得又忘記了 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmdhaBHYJSIOZxvrpD1ftOKmRClAR6y5vpUPS-oBQECn9dqWH5htVUe5l13E6hpoMtvJm_ngRypMD9O_G6F9lVA0RNdjnS_adg0TyGjhNrQkL44Aqw9wt_Q0RxczOrcBMZvaG8ur8o52kQUIMYBPoV3VUxXEReJ7PyF-IAOFvXRHJxJuHJkN_YQYDMILLH/s1745/HiSKIO%20%E8%AA%B2%E7%A8%8B-Claude%20Code%20%E6%B7%B1%E5%BA%A6%E6%87%89%E7%94%A8.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;770&quot; data-original-width=&quot;1745&quot; height=&quot;141&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmdhaBHYJSIOZxvrpD1ftOKmRClAR6y5vpUPS-oBQECn9dqWH5htVUe5l13E6hpoMtvJm_ngRypMD9O_G6F9lVA0RNdjnS_adg0TyGjhNrQkL44Aqw9wt_Q0RxczOrcBMZvaG8ur8o52kQUIMYBPoV3VUxXEReJ7PyF-IAOFvXRHJxJuHJkN_YQYDMILLH/s320/HiSKIO%20%E8%AA%B2%E7%A8%8B-Claude%20Code%20%E6%B7%B1%E5%BA%A6%E6%87%89%E7%94%A8.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;我在 HiSKIO 目前就只有林彥文老師的 &quot;Vibe Coding 全能實戰課&quot; (已上完, 要複習) 與這堂 Claude Code 課, 等沖繩回來要開始上課了 (還有 TibaMe 與 Hahow 的呵呵).&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/7247231832036012440/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/7247231832036012440' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7247231832036012440'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7247231832036012440'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/hiskio-claude-code.html' title='(補記) 購買 HiSKIO 課程 : Claude Code 深度應用'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmdhaBHYJSIOZxvrpD1ftOKmRClAR6y5vpUPS-oBQECn9dqWH5htVUe5l13E6hpoMtvJm_ngRypMD9O_G6F9lVA0RNdjnS_adg0TyGjhNrQkL44Aqw9wt_Q0RxczOrcBMZvaG8ur8o52kQUIMYBPoV3VUxXEReJ7PyF-IAOFvXRHJxJuHJkN_YQYDMILLH/s72-c/HiSKIO%20%E8%AA%B2%E7%A8%8B-Claude%20Code%20%E6%B7%B1%E5%BA%A6%E6%87%89%E7%94%A8.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-9134200790282890354</id><published>2026-04-30T10:07:00.005+08:00</published><updated>2026-04-30T10:12:08.841+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Plotly"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="網頁技術"/><category scheme="http://www.blogger.com/atom/ns#" term="資料科學"/><category scheme="http://www.blogger.com/atom/ns#" term="資料視覺化"/><title type='text'>Python 學習筆記 : 用 plotly 繪製互動式圖表 (三)</title><content type='html'>&lt;div&gt;本篇旨在測試 Plotly 的圖表匯出功能.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;本系列之前的測試文章參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2022/09/python-plotly.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 用 plotly 繪製互動式圖表 (一)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python-plotly.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 用 plotly 繪製互動式圖表 (二)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;更多 Python 資料視覺化學習筆記參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/06/python.html&quot; target=&quot;_blank&quot;&gt;Python 資料視覺化學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Plotly 支援多種檔案類型匯出, 可呼叫下表中 Figure 物件的方法匯出 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;padding: 3px; width: 190px;&quot;&gt;Figure 物件的檔案匯出方法&lt;/th&gt;
      &lt;th style=&quot;padding: 3px; width: 430px;&quot;&gt;說明&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.write_image(file, **kwargs)&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;匯出為靜態圖片（PNG、JPEG、SVG、PDF 等），需安裝 &lt;code&gt;kaleido&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.write_html(file, **kwargs)&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;匯出為互動式 HTML 檔，可直接用瀏覽器開啟&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.to_html(full_html=True, include_plotlyjs=&#39;cdn&#39;)&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;將圖表轉為 HTML 字串（用於網頁內嵌或 API 回傳）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.to_json()&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;將圖表轉為 JSON 格式（適合儲存、API 傳遞、版本控管）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.write_json(file)&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;將圖表 JSON 結構直接寫入檔案&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;fig.to_dict()&lt;/td&gt;
      &lt;td style=&quot;padding: 3px;&quot;&gt;將圖表轉為 Python 字典格式，可進一步程式操作&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;


&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, fig.write_image() 方法須依賴&amp;nbsp;kaleido 模組, 這是 Plotly 官方推出的匯圖引擎模組, 用來將 Plotly 圖表儲存為靜態圖片.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1. 匯出圖檔 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;呼叫 Figure 物件的 write_image() 方法可將繪製的圖表匯出成圖片檔 (支援 PNG, JPG, SVG, PDF 等檔案類型), 其參數結構如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;fig.write_image(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; file,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# 必填，檔案路徑字串或類似檔案的物件&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; format=None,&amp;nbsp; &amp;nbsp; &amp;nbsp; # 圖片格式，如 &#39;png&#39;、&#39;jpeg&#39;、&#39;svg&#39;、&#39;pdf&#39;，若省略會自動從副檔名判斷&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; width=None,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;# 圖片寬度（像素），預設為圖表原始寬度&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; height=None,&amp;nbsp; &amp;nbsp; &amp;nbsp; # 圖片高度（像素），預設為圖表原始高度&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; scale=1,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 圖片縮放倍數（例如 2 表示解析度加倍）&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; validate=True,&amp;nbsp; &amp;nbsp; # 是否檢查圖表是否有效（預設 True）&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; engine=&#39;kaleido&#39;&amp;nbsp; # 使用的圖像引擎，預設為 &#39;kaleido&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;不過使用此方法之前須先安裝 Plotly 的&amp;nbsp;kaleido 模組, 而且 plotly 也要提升至最新版 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;pip install kaleido&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;pip install plotly -U&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在前一篇測試中使用了 plotly.express 來繪製長條圖, 下列沿用此範例來匯出所繪製的圖檔 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_chart_export_1.py&lt;/div&gt;&lt;div&gt;import plotly.express as px&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import os&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 1. 資料來源&lt;/div&gt;&lt;div&gt;data={&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;月份&#39;: [&#39;一月&#39;, &#39;二月&#39;, &#39;三月&#39;, &#39;四月&#39;, &#39;五月&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;營收&#39;: [120000, 135000, 99000, 150000, 170000]&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 2. 建立 Figure 圖表物件&lt;/div&gt;&lt;div&gt;# 注意：這裡設定了 width 和 height，匯出圖片時會以此為基準&lt;/div&gt;&lt;div&gt;fig=px.bar(data, x=&#39;月份&#39;, y=&#39;營收&#39;, width=800, height=600, title=&quot;月營收統計圖&quot;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 3. 顯示圖表 (選用)&lt;/div&gt;&lt;div&gt;fig.show()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 4. 匯出圖檔&amp;nbsp;&lt;/div&gt;&lt;div&gt;# 建立儲存目錄（選用，避免檔案雜亂）&lt;/div&gt;&lt;div&gt;if not os.path.exists(&quot;output&quot;):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; os.mkdir(&quot;output&quot;)&lt;/div&gt;&lt;div&gt;# 匯出為 PNG&lt;/div&gt;&lt;div&gt;fig.write_image(&quot;output/revenue_report.png&quot;, scale=2)&lt;/div&gt;&lt;div&gt;# 匯出為 JPG&lt;/div&gt;&lt;div&gt;fig.write_image(&quot;output/revenue_report.jpg&quot;, scale=2)&lt;/div&gt;&lt;div&gt;print(&quot;圖檔已匯出至 output 資料夾中。&quot;)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;執行結果除了 fig.show() 會開啟瀏覽器顯示長條圖外, 也會在目前工作目錄下建立 output 子目錄存放匯出的兩個圖檔 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run plotly_chart_export_1.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;圖檔已匯出至 output 資料夾中。&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWhuaXD5-Qkr9bQZFY5l6UIVh2JQOrFVhcFDw_GyXcQDH6IEqLXDHUJq6goPPq5PK4zczAD9DnMi1itazjdsP3SIHQJh7oD8-fg8cNmOGJXKanN9U_rrc1Br6aPLsSnr6E3q3a0fcNkyt8-Y2u1uQgYD8neUVvPd59jDq1fxFVT-Hwy-lin-YYgzvDJd7p/s1131/plotly-export-picture.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;861&quot; data-original-width=&quot;1131&quot; height=&quot;244&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWhuaXD5-Qkr9bQZFY5l6UIVh2JQOrFVhcFDw_GyXcQDH6IEqLXDHUJq6goPPq5PK4zczAD9DnMi1itazjdsP3SIHQJh7oD8-fg8cNmOGJXKanN9U_rrc1Br6aPLsSnr6E3q3a0fcNkyt8-Y2u1uQgYD8neUVvPd59jDq1fxFVT-Hwy-lin-YYgzvDJd7p/s320/plotly-export-picture.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 匯出網頁 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;呼叫 fig.write_html() 可將繪製之圖表匯出為 HTML 檔, 若傳入 include_plotlyjs=&#39;cdn&#39; 參數會使用 CDN 的 plotly 函式庫, 這樣匯出的 HTML 檔較小但須連網才能看到互動圖表; 否則會將 plotly 函式庫一同匯出, 檔案較大些 (約 4MB) 但不須連網, 離線開啟網頁即可看到互動圖表.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;程式碼如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# plotly_chart_export_2.py&lt;/div&gt;&lt;div&gt;import plotly.express as px&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;import os&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 1. 資料來源&lt;/div&gt;&lt;div&gt;data={&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;月份&#39;: [&#39;一月&#39;, &#39;二月&#39;, &#39;三月&#39;, &#39;四月&#39;, &#39;五月&#39;],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &#39;營收&#39;: [120000, 135000, 99000, 150000, 170000]&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 2. 建立 Figure 圖表物件&lt;/div&gt;&lt;div&gt;fig=px.bar(data, x=&#39;月份&#39;, y=&#39;營收&#39;, width=800, height=600, title=&quot;月營收統計圖 (互動式 HTML)&quot;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 3. 建立儲存目錄&lt;/div&gt;&lt;div&gt;output_dir=&quot;output&quot;&lt;/div&gt;&lt;div&gt;if not os.path.exists(output_dir):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; os.makedirs(output_dir)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# --- 匯出 HTML&amp;nbsp; ---&lt;/div&gt;&lt;div&gt;# 方式 A：標準匯出 (將 Plotly.js 核心程式碼打包進去，檔案約 4 MB，可離線開啟)&lt;/div&gt;&lt;div&gt;fig.write_html(os.path.join(output_dir, &quot;report_full.html&quot;))&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 方式 B：輕量化匯出 (使用 CDN 連結，檔案僅約 50KB，開啟時需連網載入 JS)&lt;/div&gt;&lt;div&gt;fig.write_html(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; os.path.join(output_dir, &quot;report_cdn.html&quot;),&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; include_plotlyjs=&#39;cdn&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;print(f&quot;✅ HTML 檔案已匯出至 {output_dir} 資料夾。&quot;)&lt;/div&gt;&lt;div&gt;print(&quot;- report_full.html (可離線檢視)&quot;)&lt;/div&gt;&lt;div&gt;print(&quot;- report_cdn.html (體積小，需連網)&quot;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# 顯示圖表&lt;/div&gt;&lt;div&gt;fig.show()&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;執行結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run plotly_chart_export_2.py&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;✅ HTML 檔案已匯出至 output 資料夾。&lt;/div&gt;&lt;div&gt;- report_full.html (可離線檢視)&lt;/div&gt;&lt;div&gt;- report_cdn.html (體積小，需連網)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;開啟 output 資料夾下的網頁檔即可看到長條圖 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiefIMDZ8k9UT2zMlN3nwP9RVhR6frN-W_-PGQkFyvhDruRPAZhpiBOuHlh1Q1Vkn0qb2-D91_L0VYC5QNaxNWLcyQosiV3rQTz8poeq4GZmXb3J6qMblwHvpmBQwcnCTMmxx5uMzFvhgbOs9H8HPfWs9w3vhehZsjh8EZ5Ie8lB2HhcRf1cn3imyaI-H9P/s1635/plotly-export-html-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1417&quot; data-original-width=&quot;1635&quot; height=&quot;277&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiefIMDZ8k9UT2zMlN3nwP9RVhR6frN-W_-PGQkFyvhDruRPAZhpiBOuHlh1Q1Vkn0qb2-D91_L0VYC5QNaxNWLcyQosiV3rQTz8poeq4GZmXb3J6qMblwHvpmBQwcnCTMmxx5uMzFvhgbOs9H8HPfWs9w3vhehZsjh8EZ5Ie8lB2HhcRf1cn3imyaI-H9P/s320/plotly-export-html-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYF7giOBTHEke9Vi0r3che7LdYLUngE4l63_ANse5HSXiNjLtQXvawHYxv9NnJga2rxx4J1SFZVvVDY__pozO9zhKk9pMymKhsA6lSCf-np0_8lbjxyY1i-DFbLHknM1IEM3p2jJath-F817lc6SNoILKSh9lGuViVaJIn46LTEQVlcog89qe1eamFlOAq/s1638/plotly-export-html-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1330&quot; data-original-width=&quot;1638&quot; height=&quot;260&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYF7giOBTHEke9Vi0r3che7LdYLUngE4l63_ANse5HSXiNjLtQXvawHYxv9NnJga2rxx4J1SFZVvVDY__pozO9zhKk9pMymKhsA6lSCf-np0_8lbjxyY1i-DFbLHknM1IEM3p2jJath-F817lc6SNoILKSh9lGuViVaJIn46LTEQVlcog89qe1eamFlOAq/s320/plotly-export-html-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/9134200790282890354/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/9134200790282890354' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9134200790282890354'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9134200790282890354'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/python-plotly.html' title='Python 學習筆記 : 用 plotly 繪製互動式圖表 (三)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWhuaXD5-Qkr9bQZFY5l6UIVh2JQOrFVhcFDw_GyXcQDH6IEqLXDHUJq6goPPq5PK4zczAD9DnMi1itazjdsP3SIHQJh7oD8-fg8cNmOGJXKanN9U_rrc1Br6aPLsSnr6E3q3a0fcNkyt8-Y2u1uQgYD8neUVvPd59jDq1fxFVT-Hwy-lin-YYgzvDJd7p/s72-c/plotly-export-picture.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-2060285184414244330</id><published>2026-04-29T23:44:00.012+08:00</published><updated>2026-04-30T00:40:48.478+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="量化投資"/><title type='text'>Python 學習筆記 : 利用語言模型計算技術指標 (一)</title><content type='html'>&lt;div&gt;最近重讀旗標出版的 &quot;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2024/02/ai.html&quot; target=&quot;_blank&quot;&gt;最強 AI 投資分析&lt;/a&gt;&quot; 這本書, 此書於 2023 年底買來看了前幾章便擱下, 也沒時間做測試, 今天重讀第四章後, 決定動手來測試看看, 因為去年 10/7 儲值 5 美元的 OpenAI API Key 目前只用了 0.01 美元, 只剩半年就要被清零了, 得在這之前趕快用掉 (在 Vibe coding 時代親自寫程式已淪落為純興趣了).&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://platform.openai.com/settings/organization/billing/overview&quot; target=&quot;_blank&quot;&gt;https://platform.openai.com/settings/organization/billing/overview&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzlauJzas3jWj_jiy7u4uazjM_nP9EBusQCOxyxVT7XSOR8XBGQC5S9j4b69njka5XNQZnPQyHds6eWDAbzAuqzsV5sOgO7zbjAijM8uxuUqe6EvVaJuEmciz9FNQUL4JmT5hO1lAyYXaArnaa1_2wuNNsvPWd3nKPymVcKbfq1AVnvt4mtG38TpkJUEK/s1335/openai-api-key-balance.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;617&quot; data-original-width=&quot;1335&quot; height=&quot;148&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzlauJzas3jWj_jiy7u4uazjM_nP9EBusQCOxyxVT7XSOR8XBGQC5S9j4b69njka5XNQZnPQyHds6eWDAbzAuqzsV5sOgO7zbjAijM8uxuUqe6EvVaJuEmciz9FNQUL4JmT5hO1lAyYXaArnaa1_2wuNNsvPWd3nKPymVcKbfq1AVnvt4mtG38TpkJUEK/s320/openai-api-key-balance.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;書中範例程式碼下載網址 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://www.flag.com.tw/bk/t/f3933&quot; target=&quot;_blank&quot;&gt;https://www.flag.com.tw/bk/t/f3933&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b style=&quot;background-color: #fce5cd;&quot;&gt;&lt;span style=&quot;color: #990000;&quot;&gt;1. 利用 pandas_ta 計算 SMA 指標 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;首先用 pandas_ta 來計算移動平均指標 SMA8 與 SMA13 暖暖身, 畢竟已有近半年沒接觸了, 關於&amp;nbsp; pandas_ta 套件用法參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2024/08/python.html&quot; target=&quot;_blank&quot;&gt;Python 量化投資筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下列程式使用 yfinance 取得收盤資料, 然後用 pandas_ta 套件的擴展屬性用法呼叫 df.ta.ma() 計算 SMA 指標, 結果會自動放入 df 的指定欄位, 最後用 kbar 套件繪製 K 線圖, 關於 kbar 套件用法參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://github.com/tony1966/kbar&quot; target=&quot;_blank&quot;&gt;https://github.com/tony1966/kbar&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# ai_stock_test_1.py&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&amp;nbsp;&lt;/div&gt;&lt;div&gt;import pandas_ta as ta&lt;/div&gt;&lt;div&gt;from kbar import KBar&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2024-07-01&#39;, end=&#39;2024-08-21&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_8&#39;]=df.ta.sma(length=8)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_13&#39;]=df.ta.sma(length=13)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(df.tail())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb=KBar(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(df[&#39;SMA_8&#39;], panel=2, ylabel=&#39;SMA_8&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(df[&#39;SMA_13&#39;], panel=2, ylabel=&#39;SMA_13&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.plot(volume=True, mav=[8, 13])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;此處除了在 panel 2 上繪製 SMA8 與 SMA13 指標外, 同時也在 plot() 方法中指定 mav=[8, 13] 繪製 K 線圖之疊圖 (預設 panel=0), 結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run ai_stock_test_1.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;[*********************100%***********************]&amp;nbsp; 1 of 1 completed&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Close&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;High&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Low&amp;nbsp; ...&amp;nbsp; &amp;nbsp; Volume&amp;nbsp; &amp;nbsp; &amp;nbsp; SMA_8&amp;nbsp; &amp;nbsp; &amp;nbsp;SMA_13&lt;/div&gt;&lt;div&gt;Date&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;2024-08-14&amp;nbsp; 43.643597&amp;nbsp; 43.909202&amp;nbsp; 43.450429&amp;nbsp; ...&amp;nbsp; 74857276&amp;nbsp; 41.775311&amp;nbsp; 42.438161&lt;/div&gt;&lt;div&gt;2024-08-15&amp;nbsp; 43.305553&amp;nbsp; 43.703958&amp;nbsp; 43.233115&amp;nbsp; ...&amp;nbsp; 45926588&amp;nbsp; 42.397066&amp;nbsp; 42.414943&lt;/div&gt;&lt;div&gt;2024-08-16&amp;nbsp; 44.283455&amp;nbsp; 44.343819&amp;nbsp; 44.029927&amp;nbsp; ...&amp;nbsp; 52823660&amp;nbsp; 42.876964&amp;nbsp; 42.466949&lt;/div&gt;&lt;div&gt;2024-08-19&amp;nbsp; 44.343822&amp;nbsp; 44.597354&amp;nbsp; 44.223093&amp;nbsp; ...&amp;nbsp; 37122372&amp;nbsp; 43.163695&amp;nbsp; 42.518955&lt;/div&gt;&lt;div&gt;2024-08-20&amp;nbsp; 44.367966&amp;nbsp; 44.718080&amp;nbsp; 44.355892&amp;nbsp; ...&amp;nbsp; 43139504&amp;nbsp; 43.562101&amp;nbsp; 42.514312&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;[5 rows x 7 columns]&lt;/div&gt;&lt;div&gt;設定字型為: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;使用指定字型: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;字型候選清單: [&#39;Microsoft JhengHei&#39;, &#39;DejaVu Sans&#39;, &#39;Arial&#39;]&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOW5ON25mXgBGgFfbiyqEfBs3ErQHiGgDfeslN8FMr7uzOmI-A5VJ_2R_UguSwTpQAGSRsoTiEZC4f5-y19SOX-LhsJ8gt8EsUbWTQlEtV-tpTNu9NnDBP0OfZZqr8QtnyHtBQ_7CqJuAz-VRT_c7Ucsg_m2B_mcyQXtfvRhPeZNe9DGhKlbhQhJTF7SD0/s1160/ai-stock-test-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;847&quot; data-original-width=&quot;1160&quot; height=&quot;234&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOW5ON25mXgBGgFfbiyqEfBs3ErQHiGgDfeslN8FMr7uzOmI-A5VJ_2R_UguSwTpQAGSRsoTiEZC4f5-y19SOX-LhsJ8gt8EsUbWTQlEtV-tpTNu9NnDBP0OfZZqr8QtnyHtBQ_7CqJuAz-VRT_c7Ucsg_m2B_mcyQXtfvRhPeZNe9DGhKlbhQhJTF7SD0/s320/ai-stock-test-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fce5cd;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #990000;&quot;&gt;2. 串接 OpenAI API 計算 SMA 指標 :&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;接下來要串接 OpenAI API, 讓 LLM 模型來生成計算技術指標的程式碼後, 用 exec() 執行該程式碼計算技術指標, 好處是毋須去熟悉例如 pandas_ta, ta, 或 Ta-Lib 套件之函式呼叫介面, 直接用自然語言來指揮 LLM 傳回技術指標計算式, 做法參考書中 ˋ4-1 的範例 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://colab.research.google.com/drive/19Wg-NToZJ_Io_K6ESOJJFqEYHCR8lQvJ#scrollTo=jkH64Sng6jZh&quot; target=&quot;_blank&quot;&gt;讓 AI 自動生成技術指標的程式碼&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;原程式碼的提示詞使用英文, 作者說經測試使用英文較能得到穩定之回應, 但現在 LLM 日新月異, 對中文的理解能力已非常精準, 因此我將其改寫為中文提示詞, 程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# ai_stock_test_2.py&lt;/div&gt;&lt;div&gt;from&amp;nbsp; openai import OpenAI, APIError&amp;nbsp;&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&amp;nbsp;&lt;/div&gt;&lt;div&gt;from dotenv import dotenv_values&lt;/div&gt;&lt;div&gt;from kbar import KBar&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ask_gpt(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; messages: list[dict[str, str]],&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; model: str=&#39;gpt-3.5-turbo&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ) -&amp;gt; str:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reply=client.chat.completions.create(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; model=model,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; messages=messages&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return reply.choices[0].message.content or &#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except APIError as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return e.message&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ai_helper(df, user_msg):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; role=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 作為一個專業的程式碼生成機器人，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 我需要您的協助來根據特定的用戶需求生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 為了進行下去，我將提供給您一個遵循格式 {list(df.columns)} 的 DataFrame（df）。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是仔細分析用戶的需求並相應地生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 請注意，您的回應須僅包含代碼本身，並且不應包含任何額外的資訊。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 把 user_msg 加入到 task 的敘述中，讓 AI 知道要算什麼&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; task=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是開發一個名為 &#39;calculate(df)&#39; 的 Python 函式。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 這個函式應接受一個 DataFrame 作為其參數。確保您僅使用資料集中存在的欄，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 特別是 {list(df.columns)}。&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 用戶的具體運算需求為：【 {user_msg} 】&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 處理後，該函式應返回處理過的 DataFrame。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的回應應嚴格包含 &#39;calculate(df)&#39; 函式的 Python 程式碼，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 並排除任何無關的內容。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=[{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: role},&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: task}]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; reply_data=ask_gpt(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 清理 markdown 語法&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=reply_data.replace(&quot;```&quot;, &quot;&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.replace(&quot;python&quot;, &quot;&quot;)&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.strip() # 建議加上 strip() 去除頭尾多餘的空白或換行&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 傳回程式碼&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return cleaned_code&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=dotenv_values(&#39;.env&#39;)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; openai_api_key=config.get(&#39;OPENAI_API_KEY&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; client=OpenAI(api_key=openai_api_key)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2024-07-01&#39;, end=&#39;2024-08-21&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; code_str=ai_helper(df, &quot;計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; exec(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; new_df=calculate(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(new_df.tail())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb=KBar(new_df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(new_df[&#39;SMA_8&#39;], panel=2, ylabel=&#39;SMA_8&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(new_df[&#39;SMA_13&#39;], panel=2, ylabel=&#39;SMA_13&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.plot(volume=True, mav=[8, 13])&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此程式的 ask_gpt() 函式負責向 GPT 提問並取得回應, 注意, ask_gpt() 的傳入參數都使用了類型提示語法以增加程式碼可讀性. 例如 ask_gpt() 中的 messages: list[dict[str, str]] 意思是 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;messages 是一個串列, 裡面的每個元素都是字典.&lt;/li&gt;&lt;li&gt;字典的鍵與值都是字串, 例如 {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;hello&quot;}&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/04/openai-api.html&quot; target=&quot;_blank&quot;&gt;AI 應用程式專案 (一) : 新聞稿生成器&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;而 ai_helper() 函式則負責組裝提示詞 (字典串列) 並呼叫 ask_gpt(), 取得回應的指標計算程式碼後進行清理, 傳回純淨之 Python 程式碼給主函式以 exec() 執行, 結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run ai_stock_test_2.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;[*********************100%***********************]&amp;nbsp; 1 of 1 completed&lt;/div&gt;&lt;div&gt;def calculate(df):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_8&#39;] = df[&#39;Close&#39;].rolling(window=8).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_13&#39;] = df[&#39;Close&#39;].rolling(window=13).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return df&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Close&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;High&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Low&amp;nbsp; ...&amp;nbsp; &amp;nbsp; Volume&amp;nbsp; &amp;nbsp; &amp;nbsp; SMA_8&amp;nbsp; &amp;nbsp; &amp;nbsp;SMA_13&lt;/div&gt;&lt;div&gt;Date&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;2024-08-14&amp;nbsp; 43.643597&amp;nbsp; 43.909202&amp;nbsp; 43.450429&amp;nbsp; ...&amp;nbsp; 74857276&amp;nbsp; 41.775311&amp;nbsp; 42.438161&lt;/div&gt;&lt;div&gt;2024-08-15&amp;nbsp; 43.305553&amp;nbsp; 43.703958&amp;nbsp; 43.233115&amp;nbsp; ...&amp;nbsp; 45926588&amp;nbsp; 42.397066&amp;nbsp; 42.414943&lt;/div&gt;&lt;div&gt;2024-08-16&amp;nbsp; 44.283455&amp;nbsp; 44.343819&amp;nbsp; 44.029927&amp;nbsp; ...&amp;nbsp; 52823660&amp;nbsp; 42.876964&amp;nbsp; 42.466949&lt;/div&gt;&lt;div&gt;2024-08-19&amp;nbsp; 44.343822&amp;nbsp; 44.597354&amp;nbsp; 44.223093&amp;nbsp; ...&amp;nbsp; 37122372&amp;nbsp; 43.163695&amp;nbsp; 42.518955&lt;/div&gt;&lt;div&gt;2024-08-20&amp;nbsp; 44.367966&amp;nbsp; 44.718080&amp;nbsp; 44.355892&amp;nbsp; ...&amp;nbsp; 43139504&amp;nbsp; 43.562101&amp;nbsp; 42.514312&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;[5 rows x 7 columns]&lt;/div&gt;&lt;div&gt;設定字型為: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;使用指定字型: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;字型候選清單: [&#39;Microsoft JhengHei&#39;, &#39;DejaVu Sans&#39;, &#39;Arial&#39;]&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5A_4sOgHQUnJsPWPIvP-wAkRTzRgg4Sqfv76vJWEHprrDTBmXMEL-pVkJ3olAUsvmad0T1-wLpHQKCoDyscwCTKmJR116WNSI3v2QfSEG-9ep70dH8uBycWiFAhC_YBSjE5uNrC83j9sKuItyHN7gG6BGl5EZ6OWaUXEaN8_93_2RpZk7QGpWkKpLPBBW/s1169/ai-stock-test-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;843&quot; data-original-width=&quot;1169&quot; height=&quot;231&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5A_4sOgHQUnJsPWPIvP-wAkRTzRgg4Sqfv76vJWEHprrDTBmXMEL-pVkJ3olAUsvmad0T1-wLpHQKCoDyscwCTKmJR116WNSI3v2QfSEG-9ep70dH8uBycWiFAhC_YBSjE5uNrC83j9sKuItyHN7gG6BGl5EZ6OWaUXEaN8_93_2RpZk7QGpWkKpLPBBW/s320/ai-stock-test-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;計算出來的 SMA 數值與用 pandas_ta 計算的結果相同, 可見即使沒學過技術指標套件, 也可以利用 LLM 來進行技術指標的量化分析.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fce5cd;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #990000;&quot;&gt;3. 串接 Gemini API 計算 SMA 指標 :&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Gemini 版本的函式要改成 ask_gemini(), 而 ai_helper() 函式基本不變, 只有提示詞類型不同, OpenAI 的提示詞為字典字串, 而 Gemini 則是純字串. 程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# ai_stock_test_3.py&lt;/div&gt;&lt;div&gt;from google import genai&lt;/div&gt;&lt;div&gt;from google.genai.errors import APIError&lt;/div&gt;&lt;div&gt;import yfinance as yf&lt;/div&gt;&lt;div&gt;import pandas as pd&amp;nbsp;&lt;/div&gt;&lt;div&gt;from dotenv import dotenv_values&lt;/div&gt;&lt;div&gt;from kbar import KBar&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ask_gemini(messages: str, model: str=&#39;gemini-2.5-flash&#39;) -&amp;gt; str:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reply=client.models.generate_content(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; model=model,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; contents=messages&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return reply.text or &#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except APIError as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return e.message&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def ai_helper(df, user_msg):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; role=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 作為一個專業的程式碼生成機器人，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 我需要您的協助來根據特定的用戶需求生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 為了進行下去，我將提供給您一個遵循格式 {list(df.columns)} 的 DataFrame（df）。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是仔細分析用戶的需求並相應地生成 Python 程式碼。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 請注意，您的回應須僅包含代碼本身，並且不應包含任何額外的資訊。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; task=f&#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的任務是開發一個名為 &#39;calculate(df)&#39; 的 Python 函式。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 這個函式應接受一個 DataFrame 作為其參數。確保您僅使用資料集中存在的欄，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 特別是 {list(df.columns)}。&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 用戶的具體運算需求為：【 {user_msg} 】&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 處理後，該函式應返回處理過的 DataFrame。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 您的回應應嚴格包含 &#39;calculate(df)&#39; 函式的 Python 程式碼，&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 並排除任何無關的內容。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # Gemini 的提示詞為字串型態 : 將系統設定與任務直接合併成一段完整的字串&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;msg=f&quot;{role}\n\n{task}&quot;&lt;/span&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 呼叫 ask_gemini&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; reply_data=&lt;span style=&quot;background-color: #fcff01;&quot;&gt;ask_gemini&lt;/span&gt;(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 清理傳回 markdown 語法&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=reply_data.replace(&quot;```&quot;, &quot;&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.replace(&quot;python&quot;, &quot;&quot;)&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; cleaned_code=cleaned_code.strip() # 去除頭尾多餘的空白或換行&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 傳回程式碼&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return cleaned_code&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &quot;__main__&quot;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=dotenv_values(&#39;.env&#39;)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; gemini_api_key=config.get(&#39;GEMINI_API_KEY&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; client=genai.Client(api_key=gemini_api_key)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df=yf.download(&#39;0050.tw&#39;, start=&#39;2024-07-01&#39;, end=&#39;2024-08-21&#39;, auto_adjust=True)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df.columns=df.columns.map(lambda x: x[0])&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; code_str=ai_helper(df, &quot;計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; exec(code_str)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; new_df=calculate(df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(new_df.tail())&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb=KBar(new_df)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(new_df[&#39;SMA_8&#39;], panel=2, ylabel=&#39;SMA_8&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.addplot(new_df[&#39;SMA_13&#39;], panel=2, ylabel=&#39;SMA_13&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; kb.plot(volume=True, mav=[8, 13])&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;結果與上面是一樣的 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;gt;&amp;gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;%Run ai_stock_test_3.py&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;[*********************100%***********************]&amp;nbsp; 1 of 1 completed&lt;/div&gt;&lt;div&gt;import pandas as pd&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def calculate(df):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 計算 8 日 MA (欄名 SMA_8) 與 13 日 MA (欄名 SMA_13)。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; Args:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; df (pd.DataFrame): 包含 &#39;Close&#39;, &#39;High&#39;, &#39;Low&#39;, &#39;Open&#39;, &#39;Volume&#39; 欄位的 DataFrame。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; Returns:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; pd.DataFrame: 處理後包含 &#39;SMA_8&#39; 和 &#39;SMA_13&#39; 欄位的 DataFrame。&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_8&#39;] = df[&#39;Close&#39;].rolling(window=8).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; df[&#39;SMA_13&#39;] = df[&#39;Close&#39;].rolling(window=13).mean()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return df&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Close&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;High&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Low&amp;nbsp; ...&amp;nbsp; &amp;nbsp; Volume&amp;nbsp; &amp;nbsp; &amp;nbsp; SMA_8&amp;nbsp; &amp;nbsp; &amp;nbsp;SMA_13&lt;/div&gt;&lt;div&gt;Date&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;2024-08-14&amp;nbsp; 43.643597&amp;nbsp; 43.909202&amp;nbsp; 43.450429&amp;nbsp; ...&amp;nbsp; 74857276&amp;nbsp; 41.775310&amp;nbsp; 42.438160&lt;/div&gt;&lt;div&gt;2024-08-15&amp;nbsp; 43.305553&amp;nbsp; 43.703958&amp;nbsp; 43.233115&amp;nbsp; ...&amp;nbsp; 45926588&amp;nbsp; 42.397066&amp;nbsp; 42.414943&lt;/div&gt;&lt;div&gt;2024-08-16&amp;nbsp; 44.283459&amp;nbsp; 44.343823&amp;nbsp; 44.029930&amp;nbsp; ...&amp;nbsp; 52823660&amp;nbsp; 42.876964&amp;nbsp; 42.466949&lt;/div&gt;&lt;div&gt;2024-08-19&amp;nbsp; 44.343822&amp;nbsp; 44.597354&amp;nbsp; 44.223093&amp;nbsp; ...&amp;nbsp; 37122372&amp;nbsp; 43.163696&amp;nbsp; 42.518955&lt;/div&gt;&lt;div&gt;2024-08-20&amp;nbsp; 44.367966&amp;nbsp; 44.718080&amp;nbsp; 44.355892&amp;nbsp; ...&amp;nbsp; 43139504&amp;nbsp; 43.562102&amp;nbsp; 42.514312&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;[5 rows x 7 columns]&lt;/div&gt;&lt;div&gt;設定字型為: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;使用指定字型: Microsoft JhengHei&lt;/div&gt;&lt;div&gt;字型候選清單: [&#39;Microsoft JhengHei&#39;, &#39;DejaVu Sans&#39;, &#39;Arial&#39;]&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3ERzV5xHDPEnu2eR7_VpwDVa8LTQV2d4rVAvEVqsTSDXFQI-J7vllyMIU4yA0cYCa81BJAG6HRqW0Fpt2gEl0S6KUyDXzYmqxY4SOlAZaA_xnmfjslFnBzisxTF_lAjnFxnQyIIKmax7xrLeZ1znlyilKmeaJW-VdWGK4qo640v3a0duo-tOKKxgJd-u_/s1139/ai-stock-test-3.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;847&quot; data-original-width=&quot;1139&quot; height=&quot;238&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3ERzV5xHDPEnu2eR7_VpwDVa8LTQV2d4rVAvEVqsTSDXFQI-J7vllyMIU4yA0cYCa81BJAG6HRqW0Fpt2gEl0S6KUyDXzYmqxY4SOlAZaA_xnmfjslFnBzisxTF_lAjnFxnQyIIKmax7xrLeZ1znlyilKmeaJW-VdWGK4qo640v3a0duo-tOKKxgJd-u_/s320/ai-stock-test-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/2060285184414244330/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/2060285184414244330' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2060285184414244330'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2060285184414244330'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/python_29.html' title='Python 學習筆記 : 利用語言模型計算技術指標 (一)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzlauJzas3jWj_jiy7u4uazjM_nP9EBusQCOxyxVT7XSOR8XBGQC5S9j4b69njka5XNQZnPQyHds6eWDAbzAuqzsV5sOgO7zbjAijM8uxuUqe6EvVaJuEmciz9FNQUL4JmT5hO1lAyYXaArnaa1_2wuNNsvPWd3nKPymVcKbfq1AVnvt4mtG38TpkJUEK/s72-c/openai-api-key-balance.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1190065142980599060</id><published>2026-04-28T10:30:00.007+08:00</published><updated>2026-04-28T10:30:53.032+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="React"/><category scheme="http://www.blogger.com/atom/ns#" term="好書"/><title type='text'>市圖還書兩本 (React)</title><content type='html'>&lt;div&gt;前陣子因為在 Vibe Coding 開發中, 發現 AI 經常使用 React 作為前端框架, 於是興起一股學學看的念頭, 從市圖借來幾本 React 的書, 沒有打算深入研究, 只想對核心運作有個基本認識. 不過還沒開卷呢, 下面兩本已被預約須還 :&lt;/div&gt;&lt;div&gt;&lt;ol style=&quot;text-align: left;&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://webpacx.ksml.edu.tw/bookDetail/1644531&quot; target=&quot;_blank&quot;&gt;React全方位基礎入門實戰 : 初心者開始的第一堂必修課&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://webpacx.ksml.edu.tw/bookDetail/1772675&quot; target=&quot;_blank&quot;&gt;流暢的React : 建構快速、高效、直覺的Web應用程式&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;No.1 作者是 ReacJS 新聞站長, 此書雖較舊了, 但前半部有豐富的 ES6 語法介紹; No.2 書況極新 (2024 年出版), 內容也更豐富, 包含伺服端 React, 與 Next.js 框架等, 下次再回借.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1190065142980599060/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1190065142980599060' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1190065142980599060'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1190065142980599060'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/react.html' title='市圖還書兩本 (React)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-9131446641142612145</id><published>2026-04-26T22:51:00.009+08:00</published><updated>2026-04-26T22:51:55.398+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="生活"/><title type='text'>2026 年第 16 周記事</title><content type='html'>&lt;div&gt;週五天氣轉陰, 下班時差點淋到雨, 傍晚回鄉下時開始下起小雨, 整個晚上都在下, 甚至連周六也是下整天, 雨天無訪客剛好在家趕 SDD 線上課程的作業, 花了整整一天終於在周六晚上午夜關檔前完成作業上傳, 好險! 45 個學員也只有 9 個趕上截止期限 (我是最後一個哈哈).&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;幸好今天出太陽, 趕緊將沙發罩洗好拿去曬, 因下周大帥與仲仔要造訪鄉下家, 得事先整理一下客廳. 下午把馬路邊的三棵芒果樹都套袋完畢, 約莫 60 顆左右, 樹梢還有很多太高無法套袋, 下周要去小漢買網子攤開綁在樹下, 這樣等自然熟掉下來時才不會摔壞.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmrEFu_AVYbmnmj4Iffy4Kjs8F5QsCDGBj6PpGXo83YdrNhFELMWNDplsRf9zpCk77UtX3Adgm93gdj_WMJqz53_EN0LIfQIsLeg_QV0kv6ln-YsgReYvi-X-fYHz09-DSbo8w-uWHTtSTgjLJoy3wwh-18V7ua9-idVlGrkRyFhPafaKIGxl79uhKx-sR/s1477/%E8%B7%AF%E6%97%81%E8%8A%92%E6%9E%9C%E5%A5%97%E8%A2%8B.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1477&quot; data-original-width=&quot;1108&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmrEFu_AVYbmnmj4Iffy4Kjs8F5QsCDGBj6PpGXo83YdrNhFELMWNDplsRf9zpCk77UtX3Adgm93gdj_WMJqz53_EN0LIfQIsLeg_QV0kv6ln-YsgReYvi-X-fYHz09-DSbo8w-uWHTtSTgjLJoy3wwh-18V7ua9-idVlGrkRyFhPafaKIGxl79uhKx-sR/s320/%E8%B7%AF%E6%97%81%E8%8A%92%E6%9E%9C%E5%A5%97%E8%A2%8B.jpg&quot; width=&quot;240&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;由於雨季即將來臨, 蔥價會攀高, 早上跑了一趟種子行買了 20 株青蔥苗+一株九層塔+六株皇宮菜, 年初種了一盆香菜長不好, 傍晚全部拔掉改種青蔥, 九層塔也是盆栽, 皇宮菜則暫時放著澆水, 小舅五月初要叫小耕耘機把菜園的土翻一遍, 說之後保留兩畦給他秋天時種小番茄, 其餘我要種菜或種果樹均好, 因為他家那邊也有一個菜園要顧 (去年他朋友借他使用). 菜園南側因為較遠, 種菜澆水較不便, 打算再種兩棵芭樂樹與木瓜樹.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;毛小妹第二胎 (也是四隻) 小貓現在都在室外了, 這梯都很怕人不親, 我一靠近就跑掉. 上一梯的四隻目前只剩小黑與吉哇哇在家, 小乖與哇哇吉都離家超過一個月不回來了. 毛小妹的妹妹捲尾阿姨我也一周未見她出現, 恐怕也不回來了. 雖說不回來, 其實我認為它們應該都是遇險 (中毒/車禍) 回不來.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQdQcodvrnIlP402ZvBtAewKn5k1boyfA5igMfRgTT8Iw2R0lvpaYpGVaIKtEHZD24rrdtVc0pokfqAF2wQRK4mmjaPiZ_z54L7UwdVtqBhE_xPORcxOzUIAyTezprYW7VzkAlmotrE0IEYUzitS7MrpjVm5YCh8fQGPf_oVJf2Gzf4ipZXS8wI5SpD0ed/s1477/%E6%AF%9B%E5%B0%8F%E5%A6%B9%E7%9A%84%E7%AC%AC%E4%BA%8C%E8%83%8E.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1108&quot; data-original-width=&quot;1477&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQdQcodvrnIlP402ZvBtAewKn5k1boyfA5igMfRgTT8Iw2R0lvpaYpGVaIKtEHZD24rrdtVc0pokfqAF2wQRK4mmjaPiZ_z54L7UwdVtqBhE_xPORcxOzUIAyTezprYW7VzkAlmotrE0IEYUzitS7MrpjVm5YCh8fQGPf_oVJf2Gzf4ipZXS8wI5SpD0ed/s320/%E6%AF%9B%E5%B0%8F%E5%A6%B9%E7%9A%84%E7%AC%AC%E4%BA%8C%E8%83%8E.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;車庫雜物實在太多了, 等沖繩回來得來個斷捨離大清理了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/9131446641142612145/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/9131446641142612145' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9131446641142612145'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/9131446641142612145'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/2026-16.html' title='2026 年第 16 周記事'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmrEFu_AVYbmnmj4Iffy4Kjs8F5QsCDGBj6PpGXo83YdrNhFELMWNDplsRf9zpCk77UtX3Adgm93gdj_WMJqz53_EN0LIfQIsLeg_QV0kv6ln-YsgReYvi-X-fYHz09-DSbo8w-uWHTtSTgjLJoy3wwh-18V7ua9-idVlGrkRyFhPafaKIGxl79uhKx-sR/s72-c/%E8%B7%AF%E6%97%81%E8%8A%92%E6%9E%9C%E5%A5%97%E8%A2%8B.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-5784999000111854772</id><published>2026-04-25T22:17:00.007+08:00</published><updated>2026-04-25T23:32:05.307+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Gemini CLI"/><category scheme="http://www.blogger.com/atom/ns#" term="OpenSpec"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="SDD"/><title type='text'>Gemini CLI 學習筆記 : OpenSpec 初體驗 (三)</title><content type='html'>&lt;div&gt;離作業交卷只剩 2.5 小時, 刻不容緩繼續進行第三次迭代.&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;本系列全部測試文章參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/01/gemini-cli_30.html&quot; target=&quot;_blank&quot;&gt;Gemini CLI 學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第三次迭代要在前次基礎上添加對數與三角函數科學計算功能, 同樣使用逐步推進模式, 工作流所需指令如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;/opsx:new &amp;lt;iteration_name&amp;gt; (建立迭代之專屬的工作區)&lt;/li&gt;&lt;li&gt;/opsx:propose &amp;lt;requirements&amp;gt; (依需求起草提案書, 規格定義書, 架構設計書, 與任務清單)&lt;/li&gt;&lt;li&gt;/opsx:apply &amp;lt;iteration_name&amp;gt; (依照 tasks.md 中的任務清單逐一實作此迭代功能)&lt;/li&gt;&lt;li&gt;/opsx:archive (迭代完成歸檔)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;廢話不多說以免誤了軍期, 馬上開工.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1.&amp;nbsp;建立迭代之專屬工作區 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;第三次迭代工作區取名為 calc-scientific :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:new calc-scientific&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCc-m8PVOwRclhl-fPSAYxP5hUkMBhG4QUUEOcoeca7A1CJehptHd7akmMmHBLR5Irlf74cgPTrvo4g32WY5aH_RIGN-BXp3W-JsC_ULCKlT7kz7kXa3q_5Srq31vxhUeo9RF6hk5uZpqPAxwzmcMyGjl9qXzev4yLypmYh5wRPIZkI0orOZH-9bpgvhRa/s2220/gemin-cli-openspec-25.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1073&quot; data-original-width=&quot;2220&quot; height=&quot;155&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCc-m8PVOwRclhl-fPSAYxP5hUkMBhG4QUUEOcoeca7A1CJehptHd7akmMmHBLR5Irlf74cgPTrvo4g32WY5aH_RIGN-BXp3W-JsC_ULCKlT7kz7kXa3q_5Srq31vxhUeo9RF6hk5uZpqPAxwzmcMyGjl9qXzev4yLypmYh5wRPIZkI0orOZH-9bpgvhRa/s320/gemin-cli-openspec-25.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd98hC3mZJYFrlJOVjdqZ2-UaAJojz82eECaEZvwVT2UaNc3E756ddvMsxRf4JFh3C8GADGAL0E9TzTxcEJ_iwRpuK9aBKhiByZE1eFZ_7dKqGr5_8r06PG5zlj5cfRvI2gO6qAfSXa7AwUnjrRcq7eq9VRkvTVYbxA_qudkj1APE2zAUiE-L-JA2mJk18/s2292/gemin-cli-openspec-26.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1139&quot; data-original-width=&quot;2292&quot; height=&quot;159&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgd98hC3mZJYFrlJOVjdqZ2-UaAJojz82eECaEZvwVT2UaNc3E756ddvMsxRf4JFh3C8GADGAL0E9TzTxcEJ_iwRpuK9aBKhiByZE1eFZ_7dKqGr5_8r06PG5zlj5cfRvI2gO6qAfSXa7AwUnjrRcq7eq9VRkvTVYbxA_qudkj1APE2zAUiE-L-JA2mJk18/s320/gemin-cli-openspec-26.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 根據需求填寫提案書 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:propose &quot;在現有計算器專案上擴充科學計算功能, 包含三角函數 (sin, cos, tan) 與對數 (以 10 為底的 log, 以及自然對數 ln), 請務必處理以下邊界與轉換邏輯：1. 三角函數的輸入值預設為「角度 (Degree)」, 後端需自行轉換為弧度進行計算, 2. 處理 tan(90) 等無效角度的防呆機制, 3. 對數運算需阻擋小於或等於 0 的無效輸入, 並回傳明確的 HTTP 錯誤, 4. 前端介面需優雅地加入這些新按鈕. &quot;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjL23uZfPjSHYTzVRNtaHyzVQCXEDZWR0rtVPSi9Rs8UQYcGF7PyPQul0g7FnH275_dbNDCopqqbdmzQXiMacjLnKeAp8d5p6SxQTNZ76In9gMnRT3u2N_0dwIrsf8q2xDw1r-eVSsxZ5eLkDDouXq7RkjutjtuO7P72HFjgxXU_m0duJks2QayiiBKNrg4/s2282/gemin-cli-openspec-27.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1118&quot; data-original-width=&quot;2282&quot; height=&quot;157&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjL23uZfPjSHYTzVRNtaHyzVQCXEDZWR0rtVPSi9Rs8UQYcGF7PyPQul0g7FnH275_dbNDCopqqbdmzQXiMacjLnKeAp8d5p6SxQTNZ76In9gMnRT3u2N_0dwIrsf8q2xDw1r-eVSsxZ5eLkDDouXq7RkjutjtuO7P72HFjgxXU_m0duJks2QayiiBKNrg4/s320/gemin-cli-openspec-27.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjJsONsUegsn0fDtg_Md7CHUpKwTHd63t4I3lZQ79a8eVlpcp7TeirLrnoMWNgk9T1i72qhFOk_Hf9sBhoY_k7urRqCV-Fok29WEinfCdduSowrjulTVaKH1RPWPcusDzAxiuPM4nIdj_M-pCtAsnu3IVThkKQPkN5apDO1Bq18XRlf-fcq1XiKWvYnQvD/s2301/gemin-cli-openspec-28.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1213&quot; data-original-width=&quot;2301&quot; height=&quot;169&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjJsONsUegsn0fDtg_Md7CHUpKwTHd63t4I3lZQ79a8eVlpcp7TeirLrnoMWNgk9T1i72qhFOk_Hf9sBhoY_k7urRqCV-Fok29WEinfCdduSowrjulTVaKH1RPWPcusDzAxiuPM4nIdj_M-pCtAsnu3IVThkKQPkN5apDO1Bq18XRlf-fcq1XiKWvYnQvD/s320/gemin-cli-openspec-28.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;3. 依據任務清單實作程式碼 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:apply calc-scientific&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6VpQ1E0RYu-xu5ZOtDjFhV2MSJyEP6AwVbmK1mutHZ7SHeXWpqTmU8Lxx2PpbY9sVqAZPS5bboQBgadXUHMRYbtL_BkMQi9wKNQUHDh4O3fj7Jx1x5vtCiMI846M8LQ0YyMkzLaRP5aheasR86QU0W1FfplT8vPSo91RRgI57kH5H_X3uZNxjSbh8_lvr/s2226/gemin-cli-openspec-29.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1157&quot; data-original-width=&quot;2226&quot; height=&quot;166&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6VpQ1E0RYu-xu5ZOtDjFhV2MSJyEP6AwVbmK1mutHZ7SHeXWpqTmU8Lxx2PpbY9sVqAZPS5bboQBgadXUHMRYbtL_BkMQi9wKNQUHDh4O3fj7Jx1x5vtCiMI846M8LQ0YyMkzLaRP5aheasR86QU0W1FfplT8vPSo91RRgI57kH5H_X3uZNxjSbh8_lvr/s320/gemin-cli-openspec-29.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP7lSQOQrg3OeKpJoP6_1crq87rvs51_6mVhPRVAl80aesF0GFCmEIkytygt3r4f2wFyopaj6HFVa2yyCsiR8qo6TumbbKyPr7g2riPSW5IJAIxNjmyKc473I3x93_d2Whu8f9yuU9MYnt22LMJBE9HqrvqkqDVOR300wqPg7aCS99osJuou4Ky5agtoNv/s2294/gemin-cli-openspec-30.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1147&quot; data-original-width=&quot;2294&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP7lSQOQrg3OeKpJoP6_1crq87rvs51_6mVhPRVAl80aesF0GFCmEIkytygt3r4f2wFyopaj6HFVa2yyCsiR8qo6TumbbKyPr7g2riPSW5IJAIxNjmyKc473I3x93_d2Whu8f9yuU9MYnt22LMJBE9HqrvqkqDVOR300wqPg7aCS99osJuou4Ky5agtoNv/s320/gemin-cli-openspec-30.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;完成專案實作馬上作人工測試, 開啟 127.0.0.1:5000 網頁果然多了很多科學計算按鈕 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixtiuBvi0MvidYmauXvb74sFDZjMYQRvbH8tGqnXTeYzJqp3M-BBV5CDKPEb0TkHTK3kZoGNoKskxL3KkC0RnFLLqgERah82iFde7Qvur7BMJdhIL5KpgkWB3tVX0sXqcelB447E1kg371iDj8s3fnmCu6ArhL8awynPmTr7AVU0ouzXzIWl1BUkhqEGvV/s1076/gemin-cli-openspec-31.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1076&quot; data-original-width=&quot;634&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixtiuBvi0MvidYmauXvb74sFDZjMYQRvbH8tGqnXTeYzJqp3M-BBV5CDKPEb0TkHTK3kZoGNoKskxL3KkC0RnFLLqgERah82iFde7Qvur7BMJdhIL5KpgkWB3tVX0sXqcelB447E1kg371iDj8s3fnmCu6ArhL8awynPmTr7AVU0ouzXzIWl1BUkhqEGvV/s320/gemin-cli-openspec-31.jpg&quot; width=&quot;189&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;輸入 30 或 390 按 sin 都會得到正確結果 0.5 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuHEd-Xm0jM2yS4nid0b9P6vIi36qYZZss2p-XI3cPOsJrCm1ONqCUAaX2AIEbmMmqlZPKK5Bl-wHG7E3RKjZRLlwJV_vBUdddAVTUB5p9M6DnQ4VkShqgWvcIZycKeRNvK5-NucX3X33hmrH1Gi0at-GcNZ7xjkcHCG2xNgDkFHvavsp68_lpeT0xWfHW/s1079/gemin-cli-openspec-32.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1079&quot; data-original-width=&quot;626&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuHEd-Xm0jM2yS4nid0b9P6vIi36qYZZss2p-XI3cPOsJrCm1ONqCUAaX2AIEbmMmqlZPKK5Bl-wHG7E3RKjZRLlwJV_vBUdddAVTUB5p9M6DnQ4VkShqgWvcIZycKeRNvK5-NucX3X33hmrH1Gi0at-GcNZ7xjkcHCG2xNgDkFHvavsp68_lpeT0xWfHW/s320/gemin-cli-openspec-32.jpg&quot; width=&quot;186&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;輸入 -30 按 sin 也得到正確結果 -0.5 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdjHrQTDccp8haOZHaYNhAnkVxtrPdzpxBmYDmNpn9M_gx6Cz9lgX9_Qim9pHuqGUca2uQqn2-lub361SGfyh6uEhplHO73AcgY74GdBoGiqUutwnYE3itWHWlurdgYe4fwEANUGmhJbQ5hhEaishKBVnvQEV5rqQDNmsSqzSMdWk_8VCr97e2TrZR-0RR/s1079/gemin-cli-openspec-33.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1079&quot; data-original-width=&quot;635&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdjHrQTDccp8haOZHaYNhAnkVxtrPdzpxBmYDmNpn9M_gx6Cz9lgX9_Qim9pHuqGUca2uQqn2-lub361SGfyh6uEhplHO73AcgY74GdBoGiqUutwnYE3itWHWlurdgYe4fwEANUGmhJbQ5hhEaishKBVnvQEV5rqQDNmsSqzSMdWk_8VCr97e2TrZR-0RR/s320/gemin-cli-openspec-33.jpg&quot; width=&quot;188&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;接下來做 cos 的精度測試, 輸入 90 按 cos 得到一個接近 0 的極小值而非 0, 這是因為我們忘了要 AI 做微小誤差抹零處理之故, 可以在後續迭代中處理掉. 做對數測試, 輸入 1 按 log 得到正確 0, 輸入 0 按 log 則得到 Error (無限大) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXZqvffXyDoWQXSAR2SW3T0zQlSGgS2lg4FE3n0KWfjRPf4SFxzoYGnZRyU-YyA5d1azGirIij_ScHMNzWdU1PGPiL9_uXop3VWQH6gd12SQqUU2Jfl1FpstwQdMVpBrbRVF8JWkaTJdlX6FdAtCcolBOEwgvMBkrjgG5RqBTCmSmRrXRIkkqtATHUd0Zj/s1066/gemin-cli-openspec-35.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1066&quot; data-original-width=&quot;628&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXZqvffXyDoWQXSAR2SW3T0zQlSGgS2lg4FE3n0KWfjRPf4SFxzoYGnZRyU-YyA5d1azGirIij_ScHMNzWdU1PGPiL9_uXop3VWQH6gd12SQqUU2Jfl1FpstwQdMVpBrbRVF8JWkaTJdlX6FdAtCcolBOEwgvMBkrjgG5RqBTCmSmRrXRIkkqtATHUd0Zj/s320/gemin-cli-openspec-35.jpg&quot; width=&quot;189&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 歸檔結案 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx: archive&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfiyOtHhn0Am9X6IiM5f1Q87XWMcln5JcVwHHs6n417izXzbX7BBluMCU3LAfRZBY914NdvbZY4B5GJXRAs6lrJP0-13K7xwZ5VcwKgZGBbVeZfEnHtp1r7yNvz-XlGJeheK5Epd-ojELHlVixjPC-OSlcVFO7YM0bjYzp0Pm7QwRwHDJBKuJpOZ9JUcYr/s2213/gemin-cli-openspec-36.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1168&quot; data-original-width=&quot;2213&quot; height=&quot;169&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfiyOtHhn0Am9X6IiM5f1Q87XWMcln5JcVwHHs6n417izXzbX7BBluMCU3LAfRZBY914NdvbZY4B5GJXRAs6lrJP0-13K7xwZ5VcwKgZGBbVeZfEnHtp1r7yNvz-XlGJeheK5Epd-ojELHlVixjPC-OSlcVFO7YM0bjYzp0Pm7QwRwHDJBKuJpOZ9JUcYr/s320/gemin-cli-openspec-36.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkLiqR_dnG0E8-GPrheKHWdviyuFeDfTmpkchdHOMOFR3mG_PR6P-LsOF9JfIN5UIOKZ8Uj-IcIi5TgfyWAndjkRq6YEYOdEC2c-ieYH2CcVE5O_17wrA1lw6gfVOryAlbnf6EmmPXZ7cx1_ielR-aFaU4MshfTuR4-IzPk3edSYiK9UQxBU1YnC_qDRXU/s2281/gemin-cli-openspec-37.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1134&quot; data-original-width=&quot;2281&quot; height=&quot;159&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkLiqR_dnG0E8-GPrheKHWdviyuFeDfTmpkchdHOMOFR3mG_PR6P-LsOF9JfIN5UIOKZ8Uj-IcIi5TgfyWAndjkRq6YEYOdEC2c-ieYH2CcVE5O_17wrA1lw6gfVOryAlbnf6EmmPXZ7cx1_ielR-aFaU4MshfTuR4-IzPk3edSYiK9UQxBU1YnC_qDRXU/s320/gemin-cli-openspec-37.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;第三次迭代歸檔完畢, 終於搞定了, 趕緊來去交作業啦!&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;5. 打包專案上傳 GitHub :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;作業繳交要求將專案上傳 GitHub, 然後將 repo 網址填入 Google 試算表內.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(1). 將所有變更加入暫存區 (打包) :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git add .&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/commands/opsx/apply.toml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/commands/opsx/archive.toml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/commands/opsx/explore.toml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/commands/opsx/propose.toml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/skills/openspec-apply-change/SKILL.md&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/skills/openspec-archive-change/SKILL.md&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/skills/openspec-explore/SKILL.md&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gemini/skills/openspec-propose/SKILL.md&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.gitignore&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;.python-version&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;main.py&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;openspec/changes/archive/2026-04-25-calc-basic/.openspec.yaml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;openspec/changes/archive/2026-04-25-calc-power-root/.openspec.yaml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;openspec/changes/archive/2026-04-25-calc-scientific/.openspec.yaml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;openspec/config.yaml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;pyproject.toml&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;div&gt;warning: in the working copy of &#39;uv.lock&#39;, LF will be replaced by CRLF the next time Git touches it&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;出現的這些 warning 是在 Windows 環境下執行 Git 時常見的警告, 它完全不影響程式碼功能或 GitHub 的上傳結果, 這只是 Git 在提醒換行符號的格式要統一, 因為 OpenSpec 工具或 AI 產生的檔案可能預設使用了 Unix 格式的換行字符 LF, 在 Windows 的命令提示字元下操作時 Git 偵測到這種不一致, 所以主動告知它會自動把這些檔案轉換成 Windows 標準的 CRLF, 因此毋須理會.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(2). 設定使用者名稱與 Email :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git config --global user.name &quot;Tony&quot;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git config --global user.email &quot;blablabla@ms5.hinet.net&quot;&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(3). 提交變更 (貼標籤/存檔) :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git commit -m &quot;feat: 完成計算機專案 (基礎運算、次方根號、科學計算)&quot;&amp;nbsp;&lt;/span&gt;&lt;/b&gt; &amp;nbsp;&lt;/div&gt;&lt;div&gt;[master (root-commit) 17a5d99] feat: 完成計算機專案 (基礎運算、次方根號、科學計算)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp;44 files changed, 2523 insertions(+)&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/commands/opsx/apply.toml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/commands/opsx/archive.toml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/commands/opsx/explore.toml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/commands/opsx/propose.toml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/skills/openspec-apply-change/SKILL.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/skills/openspec-archive-change/SKILL.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/skills/openspec-explore/SKILL.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gemini/skills/openspec-propose/SKILL.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .gitignore&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 .python-version&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 GEMINI.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 README.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 calculator/__init__.py&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 calculator/logic.py&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 main.py&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/.openspec.yaml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/design.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/proposal.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/specs/arithmetic-api/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/specs/calculator-ui/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-basic/tasks.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/.openspec.yaml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/design.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/proposal.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/specs/advanced-arithmetic/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/specs/calculator-ui/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-power-root/tasks.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/.openspec.yaml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/design.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/proposal.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/specs/calculator-ui/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/specs/scientific-functions/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/changes/archive/2026-04-25-calc-scientific/tasks.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/config.yaml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/specs/advanced-arithmetic/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/specs/arithmetic-api/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/specs/calculator-ui/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 openspec/specs/scientific-functions/spec.md&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 pyproject.toml&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 static/index.html&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 static/script.js&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 static/style.css&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 test_api.py&lt;/div&gt;&lt;div&gt;&amp;nbsp;create mode 100644 uv.lock&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(4). 在 GitHub 建立空的儲存庫 (Repository) :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;建立一個空專案 (剛好 calculator-project 可用), 注意,&amp;nbsp;因為在本機已經有 README.md 與 .gitignore 檔案了, 不要勾選 &quot;Add a README&quot; 或 &quot;Add .gitignore&quot; 這兩項, 保持預設的空專案即可, 點擊 Create repository 新增 repo.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://github.com/tony1966/calculator-project&quot; target=&quot;_blank&quot;&gt;https://github.com/tony1966/calculator-project&lt;/a&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;&lt;b&gt;(5). 綁定並推上雲端&amp;nbsp;:&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;告訴本機 Git 這個專案要連線到哪個 GitHub 網址 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git remote add origin https://github.com/tony1966/calculator-project.git&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;把專案推上雲端&amp;nbsp; :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;D:\gemini\calculator-project&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git push -u origin main&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;Enumerating objects: 67, done.&lt;/div&gt;&lt;div&gt;Counting objects: 100% (67/67), done.&lt;/div&gt;&lt;div&gt;Delta compression using up to 16 threads&lt;/div&gt;&lt;div&gt;Compressing objects: 100% (51/51), done.&lt;/div&gt;&lt;div&gt;Writing objects: 100% (67/67), 39.46 KiB | 3.04 MiB/s, done.&lt;/div&gt;&lt;div&gt;Total 67 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)&lt;/div&gt;&lt;div&gt;remote: Resolving deltas: 100% (3/3), done.&lt;/div&gt;&lt;div&gt;To https://github.com/tony1966/calculator-project.git&lt;/div&gt;&lt;div&gt;&amp;nbsp;* [new branch]&amp;nbsp; &amp;nbsp; &amp;nbsp; main -&amp;gt; main&lt;/div&gt;&lt;div&gt;branch &#39;main&#39; set up to track &#39;origin/main&#39;.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此指令會出現詢問視窗, 用預設 manager 按 Select 鈕即可, 然後登入 GitHub 帳號即可上傳. 成功後到 Google sheet 登錄專案 repo 的 GitHub 網址即完成作業繳交啦! 我原以為時間太趕只能聽完課程, 沒想到忙了一整天居然搞定作業了, 哈哈.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;心得 : 親自動手做一遍才能真正學會.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000; font-weight: 700;&quot;&gt;6. 跳出 Gemini CLI :&amp;nbsp;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;專案結束, 輸入 exit 離開專案 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpdkpI0Ul5Sczvn7HsckORNT7ogJE-IvDdGDg8yx8cqUc-U6yKnidoXeek4fyECbGuSFL5W4EaJj0fU6c82wcXKsMUODyuokG2TpGskIDFsNp4Fw0wW1nn9y3cy3aaIbg_uwyZ0D0vfMN1iXvFXGQqKGYdmoPYkORfRBQHmQwD527MItjKaQZFGyl7Ia2T/s2277/gemin-cli-openspec-39.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;887&quot; data-original-width=&quot;2277&quot; height=&quot;125&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpdkpI0Ul5Sczvn7HsckORNT7ogJE-IvDdGDg8yx8cqUc-U6yKnidoXeek4fyECbGuSFL5W4EaJj0fU6c82wcXKsMUODyuokG2TpGskIDFsNp4Fw0wW1nn9y3cy3aaIbg_uwyZ0D0vfMN1iXvFXGQqKGYdmoPYkORfRBQHmQwD527MItjKaQZFGyl7Ia2T/s320/gemin-cli-openspec-39.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;used 從 2% 到 10%, 用掉了 8% 資源. 連續按兩次 Ctrl+C 即可跳出 Gemini CLI 回到 PS 視窗.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/5784999000111854772/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/5784999000111854772' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/5784999000111854772'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/5784999000111854772'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/gemini-cli-openspec_46.html' title='Gemini CLI 學習筆記 : OpenSpec 初體驗 (三)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhCc-m8PVOwRclhl-fPSAYxP5hUkMBhG4QUUEOcoeca7A1CJehptHd7akmMmHBLR5Irlf74cgPTrvo4g32WY5aH_RIGN-BXp3W-JsC_ULCKlT7kz7kXa3q_5Srq31vxhUeo9RF6hk5uZpqPAxwzmcMyGjl9qXzev4yLypmYh5wRPIZkI0orOZH-9bpgvhRa/s72-c/gemin-cli-openspec-25.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-7024209365445197089</id><published>2026-04-25T21:24:00.001+08:00</published><updated>2026-04-25T22:18:18.721+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Gemini CLI"/><category scheme="http://www.blogger.com/atom/ns#" term="OpenSpec"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="SDD"/><title type='text'>Gemini CLI 學習筆記 : OpenSpec 初體驗 (二)</title><content type='html'>&lt;div&gt;白天完成初次迭代後, 傍晚快馬加鞭進行第二次迭代.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;本系列全部測試文章參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/01/gemini-cli_30.html&quot; target=&quot;_blank&quot;&gt;Gemini CLI 學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;前一篇的初次迭代由於功能較簡單, 我們使用了 /opsx:ff 快轉指令一鍵生成模式, 快速地完成了從規劃到程式碼生成與測試驗證的工作流. 本篇將在初次碟待的基礎上為基本的四則運算計算器添加次方與根號功能.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此二次迭代將改用逐步推進模式, 舊版 OpenSec 原本的的工作流需要依序執行下列指令 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;/opsx:new &amp;lt;iteration_name&amp;gt; (建立迭代之專屬的工作區)&lt;/li&gt;&lt;li&gt;/opsx:propose &amp;lt;requirements&amp;gt; (依需求起草提案書)&lt;/li&gt;&lt;li&gt;/opsx:continue (完成規格定義書 spec.md)&lt;/li&gt;&lt;li&gt;/opsx:continue (完成架構設計書 design.md)&lt;/li&gt;&lt;li&gt;/opsx:continue (完成任務清單 tasks.md)&lt;/li&gt;&lt;li&gt;/opsx:apply (依照 tasks.md 中的任務清單逐一實作功能)&lt;/li&gt;&lt;li&gt;/opsx:verify (驗證程式碼與規格完整性, 正確性, 一致性)&lt;/li&gt;&lt;li&gt;/opsx:archive (迭代完成歸檔)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;執行 /opsx:propose 後, AI 只會生成第一份文件 proposal.md (提案書)&amp;nbsp;並把控制權交還給我們來審查功能描述是否與需求符合.&amp;nbsp;審查通過後就可執行三個連續的 /opsx:continue 指令, 一次只產出一份文件, 審查通過後再執行下一個 continue 指令.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第一個 /opsx:continue 指令, 它會生成規格定義書 spec.md, 注意, 這個檔案可能不只一個, 因為在模組化的軟體架構中, 功能會被解耦分拆在不同資料夾, 例如 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;openspec/changes/calc-power-root/specs/arithmetic-api/spec.md&lt;/div&gt;&lt;div&gt;openspec/changes/calc-power-root/specs/calculator-ui/spec.md&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;每一個功能都會有一個 spec.md 檔, 這些分散的規格定義書共同構成了這次迭代的完整規格.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第二個 /opsx:continue 指令會產生架構設計書 design.md, 主要是描述內部程式碼要怎麼實作,&amp;nbsp;審查重點在於有沒有過度設計 (Over-engineering), 例如明明 math 套件能做到的功能卻使用 numpy, 這實要將其改為 &quot;使用 Python 內建 math 即可&quot;.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第三個 (也是最後一個) /opsx:continue 指令會生成任務清單&amp;nbsp;(施工單)&amp;nbsp;tasks.md 檔,&amp;nbsp;這份文件是設計階段與實作階段之間的最後一座橋樑, 也是 AI 執行 /opsx:apply 時唯一的行動指南, 它只關注具體要做哪些動作. 審查時要檢查施工順序是否合理, 若無問題就可以下達 /opsx:apply 指令叫 AI 依照清單逐一實作程式碼, 完成驗證後即可歸檔.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;但新版的 OpenSec 已經把三次 /opsx: continue 指令整合進 /opsx:propose 裡面了, 所以新版的工作流指令序列改為 :&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;/opsx:new &amp;lt;iteration_name&amp;gt; (建立迭代之專屬的工作區)&lt;/li&gt;&lt;li&gt;/opsx:propose &amp;lt;requirements&amp;gt; (依需求起草提案書, 規格定義書, 架構設計書, 與任務清單)&lt;/li&gt;&lt;li&gt;/opsx:apply &amp;lt;iteration_name&amp;gt; (依照 tasks.md 中的任務清單逐一實作此迭代功能)&lt;/li&gt;&lt;li&gt;/opsx:archive (迭代完成歸檔)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;填寫四大核心工件的步驟其實都整合到 /opsx:propose 指令裡了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1.&amp;nbsp;建立迭代之專屬工作區 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;第二次迭代的目標是要為四則運算計算器添加次方與開根號功能, 因此迭代名稱可取為&amp;nbsp;calc-power-root :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:new calc-power-root&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;同樣地會有一連串的授權詢問, 一律選擇預設的 Allow once 觀察每一步在做甚麼 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2UTWVG2KhUh4BD1pDMtspFOcT5zfySpW7lrUa10pp2CpcAT3BEEOSPEgXkVVj1k7TbkcD7JzBHtGJRYTB_V5zc_y2VXeEz66jrJ7Vb6wEezI2VIKz6ueOR5VDvyyxYTuu8kfeNUDLOxUTir-Pzv4sVIdMLkRgUts7dMC3C2zZbOg9XFvVlfRoT9ii8jOA/s2242/gemin-cli-openspec-15.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;994&quot; data-original-width=&quot;2242&quot; height=&quot;142&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2UTWVG2KhUh4BD1pDMtspFOcT5zfySpW7lrUa10pp2CpcAT3BEEOSPEgXkVVj1k7TbkcD7JzBHtGJRYTB_V5zc_y2VXeEz66jrJ7Vb6wEezI2VIKz6ueOR5VDvyyxYTuu8kfeNUDLOxUTir-Pzv4sVIdMLkRgUts7dMC3C2zZbOg9XFvVlfRoT9ii8jOA/s320/gemin-cli-openspec-15.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHKnSjJZVpd6yl4ejk_Yk7EPt6rqHCB0CU57G11gpZ4pfojQ6q3Bdy12rqC1fwc2z_QX7bN3MOzEEk6weDPsguS_Z6mR8fanzXAUQOPZzqDDE0xG0XLMbKkQ56NZEmsSksCaGjCoMcx1l6RLFPKLsh8abJR9hoi0CBTf0FJNEXkaONKcQVpCL0tAG2KZUe/s2292/gemin-cli-openspec-16.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1199&quot; data-original-width=&quot;2292&quot; height=&quot;167&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHKnSjJZVpd6yl4ejk_Yk7EPt6rqHCB0CU57G11gpZ4pfojQ6q3Bdy12rqC1fwc2z_QX7bN3MOzEEk6weDPsguS_Z6mR8fanzXAUQOPZzqDDE0xG0XLMbKkQ56NZEmsSksCaGjCoMcx1l6RLFPKLsh8abJR9hoi0CBTf0FJNEXkaONKcQVpCL0tAG2KZUe/s320/gemin-cli-openspec-16.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見 /opsx: new 指令主要是生成四個核心工件 (artifacts) 的待填空模板, 其中的 spec.md 是每個功能會有一個, 放在各自功能的資料夾下面.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 根據需求填寫提案書 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;有了核心工件的空模板後, 接下來要用 /opsx: propose 指令注入需求, 讓 AI 協助填寫提案書 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:propose &quot;在現有的基礎四則運算計算器專案上, 加上次方與開根號功能. 請確保處理好基礎的邊界條件與防呆機制 (例如對負數開偶數根的錯誤處理) 等. &quot;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這時 OpenSpec 偵測到這個需求的主題跟上面剛剛用 /opsx:new calc-power-root 建好的 calc-power-root 工作區完全契合, 詢問是否要把這份提案放進剛才那個工作區? 那是當然的, 選 &amp;nbsp;1. 繼續使用 calc-power-root :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA0Sk9XBtgSCwKcaHJ8_H853dAn1xIr5545ZDhdrvZ6C8S3aer-04dHXV1jYz0drm36iN_-_T9GLithcnqvpsrAyzBmzl12jA5DyAJ-mHwtBCzVCPayKrBWliv9FYyvEwLZWcNRiBZEEokpXoedu0gk1K730_owuSFdbeyxWRPL858cUoHUnHPwp8DNPRT/s2220/gemin-cli-openspec-17.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1021&quot; data-original-width=&quot;2220&quot; height=&quot;147&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA0Sk9XBtgSCwKcaHJ8_H853dAn1xIr5545ZDhdrvZ6C8S3aer-04dHXV1jYz0drm36iN_-_T9GLithcnqvpsrAyzBmzl12jA5DyAJ-mHwtBCzVCPayKrBWliv9FYyvEwLZWcNRiBZEEokpXoedu0gk1K730_owuSFdbeyxWRPL858cUoHUnHPwp8DNPRT/s320/gemin-cli-openspec-17.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLLKno3eK_Yog1NQDReq06JSKsz8vCVbs5dlVZ5EuTorQ9m93kJcg3r8S2zC_-sRvLMgeH_LkljkT6sCFDGTj7_fR_0YfyOmKV4QV643nwKvI1lhmsY4w0btVeijSe2myz7PL7VHB7epjhHO_nEeeTRP-FCReaUlhwod14WqmQiP4p2aTJqwi0TZ7iCMvn/s2312/gemin-cli-openspec-18.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1127&quot; data-original-width=&quot;2312&quot; height=&quot;156&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLLKno3eK_Yog1NQDReq06JSKsz8vCVbs5dlVZ5EuTorQ9m93kJcg3r8S2zC_-sRvLMgeH_LkljkT6sCFDGTj7_fR_0YfyOmKV4QV643nwKvI1lhmsY4w0btVeijSe2myz7PL7VHB7epjhHO_nEeeTRP-FCReaUlhwod14WqmQiP4p2aTJqwi0TZ7iCMvn/s320/gemin-cli-openspec-18.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見四大核心工件檔案的填寫都已完成, 開啟這些檔案審查若不需要修改就可以下 /opsx:apply 指令來實作程式碼了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;3. 依據任務清單實作程式碼 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;用 /opsx: apply 指令並指定迭代工作區名稱 (防呆防猜測) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:apply calc-power-root&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此指令將依照任務清單 tasks.md 內容依序實作程式碼,&amp;nbsp;同樣地會有一連串的授權詢問, 一律選擇預設的 Allow once 觀察每一步在做甚麼 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkjcz5FmKSsiXRsTRispX3qKB-s-hxVDA2W7pWvNraS4Ey7dLzgDwNeRtvMV8lo-To3WYc8-zLjkGtvQJSTopFbSxtzxrEGRJj9G254O-DxoD0Z1BaGXR6LkkxmEHaX3FktX_cCsNxd7qtqjXZzkwz9LVud3eLQ_S4iK8sp0SpJHSgzTSTDf5h5VuLDKEw/s2219/gemin-cli-openspec-19.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1158&quot; data-original-width=&quot;2219&quot; height=&quot;167&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkjcz5FmKSsiXRsTRispX3qKB-s-hxVDA2W7pWvNraS4Ey7dLzgDwNeRtvMV8lo-To3WYc8-zLjkGtvQJSTopFbSxtzxrEGRJj9G254O-DxoD0Z1BaGXR6LkkxmEHaX3FktX_cCsNxd7qtqjXZzkwz9LVud3eLQ_S4iK8sp0SpJHSgzTSTDf5h5VuLDKEw/s320/gemin-cli-openspec-19.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj18j-bA1FxGLANzBaZQa8Ia-GSVD9ubWYVnSqhmX7rREhm0OQtGdzjcpYnft_ttKcZyXxYsvY-wkTiVqDJaSmS8drbFtrTzFofcCs5xqb2kzZlDppl_JSQATR3dYHzhwGcg7pqNZNJHb7Gb1NrziUi8uEQfy1xzP6OQeKf7BwxIY7qZYF0nyj5NSsezlUF/s2291/gemin-cli-openspec-20.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1083&quot; data-original-width=&quot;2291&quot; height=&quot;151&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj18j-bA1FxGLANzBaZQa8Ia-GSVD9ubWYVnSqhmX7rREhm0OQtGdzjcpYnft_ttKcZyXxYsvY-wkTiVqDJaSmS8drbFtrTzFofcCs5xqb2kzZlDppl_JSQATR3dYHzhwGcg7pqNZNJHb7Gb1NrziUi8uEQfy1xzP6OQeKf7BwxIY7qZYF0nyj5NSsezlUF/s320/gemin-cli-openspec-20.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣就完成專案實作了 (咦, 這次沒跟我說要開啟 127.0.0.1:5000 來測試?), 我前次迭代執行的 uv rum main.py 沒關掉, 馬上測試 2 的 3 次方得到正確結果 8 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_KmJjW8OhBK5YvtB7wtQTywYN6RzBYyBN7eK9SX0nTqC9lBHTwiSDi0Zf6KLMLgnEdb-v3x7vk_KVtSgdugi84MQjwm_8CUMVH3SQxYwZCq4ImKWQviLAF0DI2tBHJJItkSrzuNwRQgtZrvj7XZRkhOehtI9icY-huvTt3w7AtvSUgS7oXTqZojqRcaVD/s835/gemin-cli-openspec-21.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;835&quot; data-original-width=&quot;635&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg_KmJjW8OhBK5YvtB7wtQTywYN6RzBYyBN7eK9SX0nTqC9lBHTwiSDi0Zf6KLMLgnEdb-v3x7vk_KVtSgdugi84MQjwm_8CUMVH3SQxYwZCq4ImKWQviLAF0DI2tBHJJItkSrzuNwRQgtZrvj7XZRkhOehtI9icY-huvTt3w7AtvSUgS7oXTqZojqRcaVD/s320/gemin-cli-openspec-21.jpg&quot; width=&quot;243&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;按 C 鍵清除輸入 49 按開根號也正確得到 7 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5hqZTyr_QhhGn9Ybuz8nuoS1YiTKY5JALbtSvd8vmARkOc7FPDuTWswpyUaSnQCedNMBM5wOccaWlRGGVrvv8jm4qK0q_gua-WfsgnAg9Rh4c6dOIiO9QZyy47VTqeTdtxhyZV2nCYN25QMPmuhXL7dXWIK1cNJnfx0AXFrJGKa9J1ssvhCYSArbgPibz/s830/gemin-cli-openspec-22.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;830&quot; data-original-width=&quot;627&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5hqZTyr_QhhGn9Ybuz8nuoS1YiTKY5JALbtSvd8vmARkOc7FPDuTWswpyUaSnQCedNMBM5wOccaWlRGGVrvv8jm4qK0q_gua-WfsgnAg9Rh4c6dOIiO9QZyy47VTqeTdtxhyZV2nCYN25QMPmuhXL7dXWIK1cNJnfx0AXFrJGKa9J1ssvhCYSArbgPibz/s320/gemin-cli-openspec-22.jpg&quot; width=&quot;242&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;原先的基本四則運算功能維持一樣沒被改壞, 二次迭代終於完成了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 歸檔結案 :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下 /opsx: archive 指令歸檔&amp;nbsp; :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx: archive&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWnT5YQwoGtaHPcVk6GR2cxnqtInyo2mu-IzxAjs8-H2Uchf5kTpb9oZ2U4SneDZSF0Mit0WOEMQCeOvjDEXqSAxAsGWFDRsycfUTSVNDSJyJhosUyP2sExpjw8b0c1UHHb-LvmBEeTKBbV1Wbbg9u9mmcTk4BCsu044yz03ByIBMnFFI7nGokY9oaggLb/s2066/gemin-cli-openspec-23.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1147&quot; data-original-width=&quot;2066&quot; height=&quot;178&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWnT5YQwoGtaHPcVk6GR2cxnqtInyo2mu-IzxAjs8-H2Uchf5kTpb9oZ2U4SneDZSF0Mit0WOEMQCeOvjDEXqSAxAsGWFDRsycfUTSVNDSJyJhosUyP2sExpjw8b0c1UHHb-LvmBEeTKBbV1Wbbg9u9mmcTk4BCsu044yz03ByIBMnFFI7nGokY9oaggLb/s320/gemin-cli-openspec-23.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBemin_ycglNOR0a1TdqMa2DKQEdPAdxNyaqlPXOMhiz2CtLF2H7YCeMzd4EcqO5gO7eMrYy6X0bY0o3lh50Q2ktfV1wxeVxOcrbYW4CGFVOY9nquPXYgFILaqcQFKIK2hLPvBgMuUNGYc0XhvUpkLST3_qlxo_TZETsHGaJfxdf_Lorh-Ghrk4neOYG54/s2285/gemin-cli-openspec-24.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1146&quot; data-original-width=&quot;2285&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBemin_ycglNOR0a1TdqMa2DKQEdPAdxNyaqlPXOMhiz2CtLF2H7YCeMzd4EcqO5gO7eMrYy6X0bY0o3lh50Q2ktfV1wxeVxOcrbYW4CGFVOY9nquPXYgFILaqcQFKIK2hLPvBgMuUNGYc0XhvUpkLST3_qlxo_TZETsHGaJfxdf_Lorh-Ghrk4neOYG54/s320/gemin-cli-openspec-24.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;終於完成二次迭代了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/7024209365445197089/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/7024209365445197089' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7024209365445197089'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7024209365445197089'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/gemini-cli-openspec_25.html' title='Gemini CLI 學習筆記 : OpenSpec 初體驗 (二)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2UTWVG2KhUh4BD1pDMtspFOcT5zfySpW7lrUa10pp2CpcAT3BEEOSPEgXkVVj1k7TbkcD7JzBHtGJRYTB_V5zc_y2VXeEz66jrJ7Vb6wEezI2VIKz6ueOR5VDvyyxYTuu8kfeNUDLOxUTir-Pzv4sVIdMLkRgUts7dMC3C2zZbOg9XFvVlfRoT9ii8jOA/s72-c/gemin-cli-openspec-15.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-2574066280757431013</id><published>2026-04-25T17:37:00.011+08:00</published><updated>2026-04-25T19:15:59.895+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Gemini CLI"/><category scheme="http://www.blogger.com/atom/ns#" term="OpenSpec"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="SDD"/><title type='text'>Gemini CLI 學習筆記 : OpenSpec 初體驗 (一)</title><content type='html'>&lt;div&gt;最近幾天上完 TibaMe 的規格驅動開發 (SDD) 課程, 準備找一個小專案 (計算器) 用 Gemini CLI 來跑一下 OpenSpec 交作業.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列全部測試文章參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/01/gemini-cli_30.html&quot; target=&quot;_blank&quot;&gt;Gemini CLI 學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;關於 SDD 我已看完高見龍老師的部落格文章, 摘要整理在這篇 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/04/blog-post_16.html&quot; target=&quot;_blank&quot;&gt;好站 : 高見龍老師的部落格 (SDD 規格驅動開發)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;1. Gemini CLI 升版 :&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;距離上次測試已經過了快三個月了, Gemini CLI 版本應該也演進了不少, 所以先來升版. 開啟 PS 視窗, 輸入下列指令查詢目前本機版本 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;gemini --version&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;0.26.0&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;用下列指令升到最新版 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;npm install -g @google/gemini-cli@latest&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;added 1 package, removed 609 packages, and changed 6 packages in 3m&lt;/div&gt;&lt;div&gt;npm notice&lt;/div&gt;&lt;div&gt;npm notice New minor version of npm available! 11.6.2 -&amp;gt; 11.13.0&lt;/div&gt;&lt;div&gt;npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.13.0&lt;/div&gt;&lt;div&gt;npm notice To update run: npm install -g npm@11.13.0&lt;/div&gt;&lt;div&gt;npm notice&lt;/div&gt;&lt;div&gt;PS D:\gemini&amp;gt; gemini --version&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;0.39.1&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;哇, 一季不見已從 0.26.0 跳到 0.39.1 版. npm 也有新版, 順便也升版 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;npm install -g npm@11.13.0&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;removed 60 packages, and changed 90 packages in 9s&lt;/div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;npm --version&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;11.13.0&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;2. 安裝 OpenSpec :&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;OpenSpec 是一款由 Fission AI 研發的開源輕量級規格驅動開發 (SDD) 工具, 是一套讓 AI 程式助理 (coding agents) 遵循規格的工作流程與 CLI 工具, 目前支援 20 種以上 AI 工具, 它的目標是讓工程師與 AI 在開發軟體之前先對規格達成共識, 減少 vibe coding 常見的偏題與反覆修改弊病, 並且能留下規格紀錄與決策脈絡備查, 提高軟體可維護性.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;安裝 OpenSpec 須有 Node v20.19.0 以上執行環境, 先檢視 Node 版本 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;node --version&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;v25.2.1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;我的 Node 是利用 Scoop 安裝 Node 的, 可管理多版本的 Node, 參考 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/12/windows-claude-code.html&quot; target=&quot;_blank&quot;&gt;在 Windows 上安裝原生版 Claude Code&lt;/a&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/12/windows-gemini-cli.html&quot; target=&quot;_blank&quot;&gt;在 Windows 上安裝 Gemini CLI&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣就可以在 PS 視窗安裝 OpenSpec 了 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;npm -g install @fission-ai/openspec@latest&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;added 74 packages in 34s&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這樣就完成 OpenSpec 安裝了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;3. 用 uv 建立專案目錄 &amp;amp; 加入版控 :&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;由於作業要求用 OpenSpe 做三次迭代, 我打算用 OpenSpec 來實作一個網頁計算器專案, 第一次迭代要實作基本的四則運算計算機, 第二次迭代添加次方與開根號功能; 第三次迭代則添加對數, 指數, 與三角函數.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;首先用 uv init 指令建立一個專案目錄 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;uv init calculator-project&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;Initialized project `calculator-project` at `D:\gemini\calculator-project`&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;然後切換到專案目錄下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;cd calculator-project&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;對此專案進行版本控制, 先檢視 Git 是否已安裝 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git --version&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;git version 2.52.0.windows.1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;關於 Git 安裝與用法參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2020/01/git.html&quot; target=&quot;_blank&quot;&gt;Git 學習筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在專案目錄下用 git init 初始化版控 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;git init&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;Reinitialized existing Git repository in D:/gemini/calculator-project/.git/&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;版控資訊會儲存在隱藏目錄 .git 底下.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;4. 初始化 OpenSpec :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;接著用 openspec init 指令為此專案做 OpenSpec 的初始化 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;openspec init&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這時會出現 OpenSpec 歡迎畫面 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFnvV5j2FfTfH_CB6FiUtgwbMs9GM2nrQCaQHKdoZDRXCmW7-pRv9IpHsqpv84DssS99ohEwADWhFQceAwFkltTdULEf0FJ_c0DQbCvzhN-BjYmAe-KoLBmXHvAwd-ns7vuuzMzbE_szUwYvvbftTPdjCSek4TDTPxOY-5o5y2DcLP0sQsKoyfp2Icn2QQ/s1467/openspec-init-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;746&quot; data-original-width=&quot;1467&quot; height=&quot;163&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFnvV5j2FfTfH_CB6FiUtgwbMs9GM2nrQCaQHKdoZDRXCmW7-pRv9IpHsqpv84DssS99ohEwADWhFQceAwFkltTdULEf0FJ_c0DQbCvzhN-BjYmAe-KoLBmXHvAwd-ns7vuuzMzbE_szUwYvvbftTPdjCSek4TDTPxOY-5o5y2DcLP0sQsKoyfp2Icn2QQ/s320/openspec-init-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;按 Enter 會出現 AI 程式代理工具選單, 可以按上下鍵移動指標來選擇要用的 AI 工具, 按 Space 鍵選擇要使用的代理工具 (可複選, 目前已支援 28 種 AI 程式代理工具), 此處我只選 Gemini CLI :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcTSSHBhCf0Tv9AhE0_SjBoNGL1qjfXulQfy3WD71XOHdPMNQNxTdvgaMk2STmsiYtb1mL5Tk0IeayrF_SBqNWO7NLstXhNgjKXRklJc0JoqfliFFznflM4ibO3FND3uqNpW8lQFOkITDoNISIBfEGM7FMeBnPtCU-8UxcoRmQwu4aywLonGBtp7MU4zui/s1487/openspec-init-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;844&quot; data-original-width=&quot;1487&quot; height=&quot;182&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcTSSHBhCf0Tv9AhE0_SjBoNGL1qjfXulQfy3WD71XOHdPMNQNxTdvgaMk2STmsiYtb1mL5Tk0IeayrF_SBqNWO7NLstXhNgjKXRklJc0JoqfliFFznflM4ibO3FND3uqNpW8lQFOkITDoNISIBfEGM7FMeBnPtCU-8UxcoRmQwu4aywLonGBtp7MU4zui/s320/openspec-init-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;選定的工具名稱會被列在上方的 Selected : 後面&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIPTd-qHrMJ46zlzwMP7zqHV3OvvL_2k2osFEs3i2KAG3yWN8P8nAMxI8jZdLrXLvpA5T1RCjijCdhsbUJpiHRkGMO4dJWnM6B3S4kj6QONCqPUKBVME0yBj6Ipb1qStlx2O1x85TNegAdSMA026CiVHjfDPhh-BllFdF3SKavLoOQHeB15YxbI5xfiOTF/s1472/openspec-init-3.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;878&quot; data-original-width=&quot;1472&quot; height=&quot;191&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIPTd-qHrMJ46zlzwMP7zqHV3OvvL_2k2osFEs3i2KAG3yWN8P8nAMxI8jZdLrXLvpA5T1RCjijCdhsbUJpiHRkGMO4dJWnM6B3S4kj6QONCqPUKBVME0yBj6Ipb1qStlx2O1x85TNegAdSMA026CiVHjfDPhh-BllFdF3SKavLoOQHeB15YxbI5xfiOTF/s320/openspec-init-3.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;選完後按 Enter 退出 OpenSpec 初始化畫面回到 PS 終端機, 可見已為 Gemini CLI 程式代理建立了 4 個技能 (skills) 與 OpenSpeck 相關之命令 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0&lt;/div&gt;&lt;div&gt;√ Select tools to set up (28 available) &lt;span style=&quot;background-color: #fcff01;&quot;&gt;Gemini CLI&lt;/span&gt;&lt;/div&gt;&lt;div&gt;▌ OpenSpec structure created&lt;/div&gt;&lt;div&gt;√ Setup complete for &lt;span style=&quot;background-color: #fcff01;&quot;&gt;Gemini CLI&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;OpenSpec Setup Complete&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Created: &lt;span style=&quot;background-color: #fcff01;&quot;&gt;Gemini CLI&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;4 skills and 4 commands&lt;/span&gt; in .gemini/&lt;/div&gt;&lt;div&gt;Config: openspec/config.yaml (schema: spec-driven)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Getting started:&lt;/div&gt;&lt;div&gt;&amp;nbsp; Start your first change: /opsx:propose &quot;your idea&quot;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Learn more: https://github.com/Fission-AI/OpenSpec&lt;/div&gt;&lt;div&gt;Feedback:&amp;nbsp; &amp;nbsp;https://github.com/Fission-AI/OpenSpec/issues&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Restart your IDE for slash commands to take effect.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;做完上面初始化後, 用 tree /f 指令檢視專案目錄 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;tree /f&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;列出磁碟區 新增磁碟區 的資料夾 PATH&lt;/div&gt;&lt;div&gt;磁碟區序號為 1258-16B8&lt;/div&gt;&lt;div&gt;D:.&lt;/div&gt;&lt;div&gt;│&amp;nbsp; .gitignore&lt;/div&gt;&lt;div&gt;│&amp;nbsp; .python-version&lt;/div&gt;&lt;div&gt;│&amp;nbsp; main.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; pyproject.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; README.md&lt;/div&gt;&lt;div&gt;│&lt;/div&gt;&lt;div&gt;├─.gemini&lt;/div&gt;&lt;div&gt;│&amp;nbsp; ├─commands&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; └─opsx&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; apply.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; archive.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; explore.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; propose.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; └─skills&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; ├─openspec-apply-change&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; SKILL.md&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; ├─openspec-archive-change&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; SKILL.md&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; ├─openspec-explore&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; SKILL.md&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; └─openspec-propose&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; SKILL.md&lt;/div&gt;&lt;div&gt;│&lt;/div&gt;&lt;div&gt;└─openspec&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; │&amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;config.yaml&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; │&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ├─changes&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; │&amp;nbsp; └─archive&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; └─&lt;span style=&quot;background-color: #fcff01;&quot;&gt;specs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見 OpenSec 初始化時已在&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;5. 安裝專案所需套件 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;雖然對一個計算器專案來說, 只需要純前端 (HTML/CSS/JavaScript) 技術就能完成所有功能, 但為了模擬真實軟體架構常見的前後端搭配組態, 我打算將計算功能邏輯交給後端 Flask 框架來完成, 所以必須先用 uv 工具安裝 Flask, 這樣當 AI 生成程式碼後就可以叫 Gemini CLI&amp;nbsp; 用 uv run 執行專案與驗證結果, 不需要再跳出來處理環境問題. 其次,&amp;nbsp;在使用 /opsx:verify 進行自動化檢查或測試時, 系統會依賴現有的虛擬環境, 如果環境未就緒 (例如缺少 Flask 套件), 驗證步驟可能會出錯.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;uv add flask&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;Using CPython 3.12.1 interpreter at: C:\Users\tony1\AppData\Local\Programs\Python\Python312\python.exe&lt;/div&gt;&lt;div&gt;Creating virtual environment at: .venv&lt;/div&gt;&lt;div&gt;Resolved 9 packages in 758ms&lt;/div&gt;&lt;div&gt;Prepared 7 packages in 466ms&lt;/div&gt;&lt;div&gt;░░░░░░░░░░░░░░░░░░░░ [0/8] Installing wheels...&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;If the cache and target directories are on different filesystems, hardlinking may not be supported.&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.&lt;/div&gt;&lt;div&gt;Installed 8 packages in 98ms&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ blinker==1.9.0&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ click==8.3.3&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ colorama==0.4.6&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ flask==3.1.3&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ itsdangerous==2.2.0&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ jinja2==3.1.6&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ markupsafe==3.0.3&lt;/div&gt;&lt;div&gt;&amp;nbsp;+ werkzeug==3.1.8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;安裝完後用 tree /f 檢視專案目錄, 會多出一個 .venv 隱藏子目錄, 裡面有包含 Flask 與所依賴的 Jinjia 等套件 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;├─&lt;span style=&quot;background-color: #fcff01;&quot;&gt;.venv&lt;/span&gt;&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; .gitignore&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; .lock&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; CACHEDIR.TAG&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; pyvenv.cfg&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; ├─Lib&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; └─site-packages&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; _virtualenv.pth&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; _virtualenv.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; ├─blinker&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; base.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; py.typed&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; _utilities.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; __init__.py&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; ├─flask&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; app.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; blueprints.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; cli.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; config.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; ctx.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; debughelpers.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; globals.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; helpers.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; logging.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; py.typed&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; sessions.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; signals.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; templating.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; testing.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; typing.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; views.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; wrappers.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; __init__.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; │&amp;nbsp; │&amp;nbsp; __main__.py&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;6. 編輯專案語境檔 GEMINI.md :&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在之前使用 Vibe coding 的進階方式開發時, 我們透過事先編輯好的專案語境檔 GEMINI.md 一次將專案需求與結構, 程式風格, 任務模板, 限制和偏好等資訊一口氣描述好, 當啟動 Gemini CLI 時它便能了解專案內容, 從而減少來回問答的次數. 這種語境檔因為要交待較多訊息, 所以內容比較冗長, 參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/01/gemini-cli-geminimd.html&quot; target=&quot;_blank&quot;&gt;Gemini CLI 學習筆記 : 專案語境檔 GEMINI.md&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;如果使用 OpenSpec 做 SDD 開發,&amp;nbsp;GEMINI.md 就會比較簡短, 例如下面的通用模板 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;# Project Global Guidelines (GEMINI.md)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;## 1. AI 角色設定 (通用)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;你是一位資深的 [Python 後端與全端] 開發專家，精通 [Flask 框架與現代前端技術]，並具備極高的軟體工程素養。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;## 2. 技術棧與環境配置 (專案特製)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 核心語言：Python 3.12+ (嚴格使用 `uv` 進行依賴與環境管理)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 後端框架：[Flask]&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 前端技術：[Vanilla JS, HTML5, CSS3]&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 其他工具：[若無則免，例如 SQLite, 特定硬體 SDK 等]&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;## 3. 開發流程與規格遵循 (通用，針對 OpenSpec 用戶)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 本專案嚴格遵循 Fission AI 的 OpenSpec 工作流 (`/opsx` 指令集)。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 你的所有實作必須以 `specs/` 目錄下的文件與 `tasks.md` 為「唯一真相來源 (Single Source of Truth)」。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 絕對禁止在未經使用者同意且未更新 Spec 的情況下，自行發明、擴充或竄改 API 規格與業務邏輯。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;## 4. 程式碼風格守則 (通用，Python 標準)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 必須包含 Type Hints (型別提示) 與清楚的 Docstrings。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 遵守 PEP 8 命名規範 (變數與函式使用 `snake_case`，類別使用 `PascalCase`)。&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe; font-size: x-small;&quot;&gt;* 保持模組化，避免單一檔案過於龐大。&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;其中專案特製部分視專案而異, 括號 [] 內容需要手動修改, 其他通用部分則適用於任何用 OpenSpec 開發的專案. GEMINI.md 的角色是專案的憲法, 負責告訴 AI 我們這個專案要用甚麼 approach 來做 (How), 而專案的需求 (what) 會放在 /opsx: propose 指令來交待.&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;在 PS 視窗的專案目錄下輸入 notepad GEMINI.md :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;notepad GEMINI.md&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這時 PS 發現專案目錄下並無 GEMINI.md 檔, 就彈出詢問是否新建此檔, 按是就會開啟記事本 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyUkXZMzRGvhYf2JdGJEXg7Z74I39nYaJuB8LWUJDG-j4wYQgkqkUijIYF6h5Ap2LZ8Uk55kobzvlR3BjVM1KB-2iryOoS5yb__TaR5q8YtYYMiDNK07NvBm-bH2Jw_5hnegjMy30a8Hd5Y6L8FhoCXVgK96XiQ_gZCsSa5o7cxTCnQNBZTQazXtem0xuW/s785/notepad-GEMINI.md-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;411&quot; data-original-width=&quot;785&quot; height=&quot;168&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyUkXZMzRGvhYf2JdGJEXg7Z74I39nYaJuB8LWUJDG-j4wYQgkqkUijIYF6h5Ap2LZ8Uk55kobzvlR3BjVM1KB-2iryOoS5yb__TaR5q8YtYYMiDNK07NvBm-bH2Jw_5hnegjMy30a8Hd5Y6L8FhoCXVgK96XiQ_gZCsSa5o7cxTCnQNBZTQazXtem0xuW/s320/notepad-GEMINI.md-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;複製上面的語境檔通用範本貼到 GEMINI.md 後存檔 (此處我將 ## 2 的其他工具內容改為 [無]) :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjc_e2SpxsyLpw7f1AoBbrw4fcaUoS6uklK2YSsT1n3YINgw6iYj2h23q0Wuq8namrHZaoCGrIAr3MBZoZ4kunRzgX9p4EpFwPmLS2bAmCQ3QmXhFhdDNNlrSPckX_YrmilyTvTbNjvKgy4T6INJOzVhRb85OWpxH1k6AGP0_s5FmY-flO-0kLtB28KQNQ/s1984/notepad-GEMINI.md-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1056&quot; data-original-width=&quot;1984&quot; height=&quot;170&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhjc_e2SpxsyLpw7f1AoBbrw4fcaUoS6uklK2YSsT1n3YINgw6iYj2h23q0Wuq8namrHZaoCGrIAr3MBZoZ4kunRzgX9p4EpFwPmLS2bAmCQ3QmXhFhdDNNlrSPckX_YrmilyTvTbNjvKgy4T6INJOzVhRb85OWpxH1k6AGP0_s5FmY-flO-0kLtB28KQNQ/s320/notepad-GEMINI.md-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;再次用 tree /s 檢視專案目錄下已有此 GEMINI.md 檔了 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;tree /f&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;列出磁碟區 新增磁碟區 的資料夾 PATH&lt;/div&gt;&lt;div&gt;磁碟區序號為 1258-16B8&lt;/div&gt;&lt;div&gt;D:.&lt;/div&gt;&lt;div&gt;│&amp;nbsp; .gitignore&lt;/div&gt;&lt;div&gt;│&amp;nbsp; .python-version&lt;/div&gt;&lt;div&gt;│&amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;GEMINI.md&lt;/span&gt;&lt;/div&gt;&lt;div&gt;│&amp;nbsp; main.py&lt;/div&gt;&lt;div&gt;│&amp;nbsp; pyproject.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; README.md&lt;/div&gt;&lt;div&gt;│&amp;nbsp; uv.lock&lt;/div&gt;&lt;div&gt;│&lt;/div&gt;&lt;div&gt;├─.gemini&lt;/div&gt;&lt;div&gt;│&amp;nbsp; ├─commands&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; └─opsx&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; apply.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; archive.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; explore.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; propose.toml&lt;/div&gt;&lt;div&gt;│&amp;nbsp; │&lt;/div&gt;&lt;div&gt;│&amp;nbsp; └─skills&lt;/div&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;7. 啟動 Gemini CLI 進行初次迭代&amp;nbsp; :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;完成上面準備工作後, 終於要開啟 Gemini CLI 開始用 OpenSpec 幹活了. 在專案目錄下輸入 gemini 指令 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;gemini&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;詢問是否信任此目錄, 當然要選 1. Trust folder (calculator-project) 才會進入 Gemini CLI 介面 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhakJCQEpxa5sDDy1xKhWjOhQKh7XbBHtf8lWpt1TjT0Jh5XU6Ml16VWuiVc-6Jv0AxDV4fZdhPu4WKEjrkJWzHjN1SaUYnl3c7xP9hQQZ3eRqoZHqYO9aw0hYmVfMvQkYyQUCRYg1ioHoRM7YlUdGukcVkxQvU15AahT4WN4rVy8YYBeiJ4MvVYkoFDSBa/s2279/gemin-cli-openspec-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1085&quot; data-original-width=&quot;2279&quot; height=&quot;152&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhakJCQEpxa5sDDy1xKhWjOhQKh7XbBHtf8lWpt1TjT0Jh5XU6Ml16VWuiVc-6Jv0AxDV4fZdhPu4WKEjrkJWzHjN1SaUYnl3c7xP9hQQZ3eRqoZHqYO9aw0hYmVfMvQkYyQUCRYg1ioHoRM7YlUdGukcVkxQvU15AahT4WN4rVy8YYBeiJ4MvVYkoFDSBa/s320/gemin-cli-openspec-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIdkDF-siZX8TgaNP-ifJ_voheuDefFvMDLJT41R8tgqi7pr7xAFb9Fupjujy2pStj1YKv7tqWs4MfAZ-kTRErxaDUhEIcIkVvpZFjIm5nq08NFxmJ4lMb83pNYlgVrKUplkKFVjYdz2YU_AoLSkpVAToTX08UE5yj3r79tPAHnqfH831_qz2OggAPjXlF/s2300/gemin-cli-openspec-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1070&quot; data-original-width=&quot;2300&quot; height=&quot;149&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIdkDF-siZX8TgaNP-ifJ_voheuDefFvMDLJT41R8tgqi7pr7xAFb9Fupjujy2pStj1YKv7tqWs4MfAZ-kTRErxaDUhEIcIkVvpZFjIm5nq08NFxmJ4lMb83pNYlgVrKUplkKFVjYdz2YU_AoLSkpVAToTX08UE5yj3r79tPAHnqfH831_qz2OggAPjXlF/s320/gemin-cli-openspec-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;出現 &amp;gt; 提示號表示 AI 已經讀取了 GEMINI.md 內容知道自己是誰, 也知道這個資料夾裡有 OpenSpec 環境, 這樣就可以開始用 OpenSpec 的 斜線指令集 /opsx 進行 SDD 開發了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;如上所述, 這個專案作業要求至少進行三次開發迭代, 第一次迭代是要做出一個基本的四則運算計算器, 由於較簡單, 此處會使用快轉模式, 直接用 /opsx: ff 指令一鍵生成完成專案規劃與程式碼生成與測試, 整個工作流只需要的指令如下 (依序) :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;/opsx:new &amp;lt;iteration_name&amp;gt; (建立迭代之專屬的工作區)&lt;/li&gt;&lt;li&gt;/opsx:ff &amp;lt;requirements&amp;gt; (依需求快轉生成設計文件與程式碼並完成測試)&lt;/li&gt;&lt;li&gt;/opsx:archive (迭代完成歸檔)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;其中 iteration_name 是自訂的, 為了專案的易讀性以及讓 AI 能隱約猜到任務方向, 通常會遵循以下命名最佳實踐來取名 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;使用 Kebab-case (連字號命名法) : &lt;br /&gt;全小寫英文, 單字之間用連字號 - 隔開, 避免使用空格, 大寫或特殊符號.&amp;nbsp;&lt;/li&gt;&lt;li&gt;具備語意 (Semantic) : 名稱要能直接反映這次迭代的核心目的.&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;初次迭代我選用 cals-basic 作為工作區名稱.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(1).&amp;nbsp;建立迭代之專屬工作區 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這會在專案目錄下建立此次迭代之專屬工作區, 例如迭代名稱是 calc-basic 的話就建立對應的資料夾 specs/changes/calc-basic/&amp;nbsp;來收納這次迭代的所有相關討論與設計文件.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;系統會自動生成標準 SDD 流程所需的空文件或帶有基礎標題的模板, 例如空的 proposal.md, spec.md, design.md 以及 tasks.md, 此階段相當於是買了一本有分類索引標籤 (提案, 規格, 設計, 任務) 的空白筆記本, 以便在後續工作流中依需求來填空或修改.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;輸入下列指令並於一連串授權詢問時一律選擇預設的 Allow once :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:new calc-basic&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoRR189R-dfKeHdue_YNNraR2PxAUOZS9XrghUfdCML6ECaAzSon_z0w8I8jJtp4K1sdLRaRHiPQXTADJRsoOsSrfzLIu8OclNqkXmUS6KNRvY5OuF1Xx3ZrBWpYobSlxRu7VXsK2Vjc_Y9YQinnBFEIFZEWEa9iITIgYRGc05jHE0X6dhHkPZMPf0rIp9/s2301/gemin-cli-openspec-4.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1062&quot; data-original-width=&quot;2301&quot; height=&quot;148&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoRR189R-dfKeHdue_YNNraR2PxAUOZS9XrghUfdCML6ECaAzSon_z0w8I8jJtp4K1sdLRaRHiPQXTADJRsoOsSrfzLIu8OclNqkXmUS6KNRvY5OuF1Xx3ZrBWpYobSlxRu7VXsK2Vjc_Y9YQinnBFEIFZEWEa9iITIgYRGc05jHE0X6dhHkPZMPf0rIp9/s320/gemin-cli-openspec-4.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhK9iwjS6IP6uQYATBVMvT_v9ZSCFx_dC_QPqtGaH5FCxTX2YVtHeyFyCZVKp-VeoTYi6r2upngo3y7fRv6FZhyWWW_3ErncR72bpriAWXjqD71DZqInLIR-GqcCm86iLqCRV5h9RUfCETlys4zFk18hZPRg38zkgEsjUMl1kSlj5M-iDgdIfdVtKmzuNhC/s2294/gemin-cli-openspec-6.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1130&quot; data-original-width=&quot;2294&quot; height=&quot;158&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhK9iwjS6IP6uQYATBVMvT_v9ZSCFx_dC_QPqtGaH5FCxTX2YVtHeyFyCZVKp-VeoTYi6r2upngo3y7fRv6FZhyWWW_3ErncR72bpriAWXjqD71DZqInLIR-GqcCm86iLqCRV5h9RUfCETlys4zFk18hZPRg38zkgEsjUMl1kSlj5M-iDgdIfdVtKmzuNhC/s320/gemin-cli-openspec-6.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;工作區建完後, 專案所需的 OpenSpec 模板文件也都已建好, 但它們目前只是空洞的骨架而已, 接下來的工作流 (/opsx:ff 或 /opsx:continue) 就會根據需求來填寫這些標準 SSD 流程的四份核心文件模板 (proposal.md, spec.md, design.md 以及 tasks.md), 如果需求較複雜, 可能也會生成額外的 spec-xxx.md 文件, 相當於是在骨架中進行靈魂注入.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(2). 依需求快轉 (ff) 生成設計文件與程式碼 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;此階段 OpenSpec 會根據我們提供的專案需求, 透過 AI 去修改或填空上一個指令 (/opsx: new) 生出的四份核心文件 proposal.md (提案), spec.md (介面規格), design.md (架構設計) 與 tasks.md (任務清單), 並且據此生成程式碼同時完成測試, 可說是集成了 /opsx:propose, /opsx:continue, /opsx: apply, 以及 /opsx: verify 這四個指令的功能於一身, 一氣呵成完成專案實作.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;輸入如下指令來進行快轉 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:ff &quot;請幫我規劃一個基礎四則運算計算機軟體, 後端使用 Flask 提供 POST API, 前端使用純 HTML/JS, 並提供基本的使用者介面.&quot;&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjml_ZP61fHhTCDUogtqwg3nh_JKiuJJQxqDJNjDejHmtNZOH9S2hXeVzLaI__fpwaWO9i7t6ypkxGKr0K19PPaVzzUa__yjHZKwVPgokto7TAEEmG-TZtITmz-Yz4AJNlJajTkjz0LgTK6KaZ5oijJA0omSmg9tLF2qDW1DwALtSzdzwswWWtFiP3A007Y/s2295/gemin-cli-openspec-8.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1016&quot; data-original-width=&quot;2295&quot; height=&quot;142&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjml_ZP61fHhTCDUogtqwg3nh_JKiuJJQxqDJNjDejHmtNZOH9S2hXeVzLaI__fpwaWO9i7t6ypkxGKr0K19PPaVzzUa__yjHZKwVPgokto7TAEEmG-TZtITmz-Yz4AJNlJajTkjz0LgTK6KaZ5oijJA0omSmg9tLF2qDW1DwALtSzdzwswWWtFiP3A007Y/s320/gemin-cli-openspec-8.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNcrd87nu-zjjcxFrT__r6PEF1Hm8kBG55GdyRwf18TMck_uGKIQ8yfJwX4Hi22NH54X9q_TcheEpB-A2vOBRBeOCP335SstRvwxchmxQRVLBEK6ADdfGv9ilcO1pPcgGNz7nwuc8ylV9ugl2BldHuSVRXuibsQoCQETsntG6fimcEdAFGp6aTsBYYVw2I/s1922/gemin-cli-openspec-9.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1120&quot; data-original-width=&quot;1922&quot; height=&quot;186&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNcrd87nu-zjjcxFrT__r6PEF1Hm8kBG55GdyRwf18TMck_uGKIQ8yfJwX4Hi22NH54X9q_TcheEpB-A2vOBRBeOCP335SstRvwxchmxQRVLBEK6ADdfGv9ilcO1pPcgGNz7nwuc8ylV9ugl2BldHuSVRXuibsQoCQETsntG6fimcEdAFGp6aTsBYYVw2I/s320/gemin-cli-openspec-9.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;同樣地, 在它修改生成檔案與依此生成整個專案程式碼過程中會不斷要求授權, 我都選預設的 Allow once, 這樣可以一步步觀察 OpenSpec 做了哪些事, 過程中會顯示生成的程式碼與單元測試檔, 完成後結果如下 :&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqLa16H54dlHjO9ldHm4US-LDSXhoOiQX3v0G1hW5pxPGQE5NjLTSMcDK4nI49WDXHueRy4qEwpb8JgtcZW5qthencT6fBUMYKR3a-dENIgDap8-nhyphenhyphenRpYIZ2ybW7GtyJIn-aikOHpZSgsoNjXd0y91YBfQPRtHIDOqwrgZpgNSrHkVgukboal8Kw1ORfw/s2316/gemin-cli-openspec-10.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1043&quot; data-original-width=&quot;2316&quot; height=&quot;144&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqLa16H54dlHjO9ldHm4US-LDSXhoOiQX3v0G1hW5pxPGQE5NjLTSMcDK4nI49WDXHueRy4qEwpb8JgtcZW5qthencT6fBUMYKR3a-dENIgDap8-nhyphenhyphenRpYIZ2ybW7GtyJIn-aikOHpZSgsoNjXd0y91YBfQPRtHIDOqwrgZpgNSrHkVgukboal8Kw1ORfw/s320/gemin-cli-openspec-10.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這時可以另開一個命令提示字元或 PS 視窗, 切到專案目錄下, 用 python main.py 或 uv run main.py 執行此專案, 然後開啟瀏覽器訪問&amp;nbsp;http://127.0.0.1:5000 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;PS D:\gemini&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;cd calculator-project&amp;nbsp;&lt;/span&gt;&lt;/b&gt; &amp;nbsp;&lt;/div&gt;&lt;div&gt;PS D:\gemini\calculator-project&amp;gt; uv run main.py&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Serving Flask app &#39;main&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Debug mode: on&lt;/div&gt;&lt;div&gt;WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Running on &lt;span style=&quot;background-color: #fcff01;&quot;&gt;http://127.0.0.1:5000&lt;/span&gt;&lt;/div&gt;&lt;div&gt;Press CTRL+C to quit&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Restarting with stat&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Debugger is active!&lt;/div&gt;&lt;div&gt;&amp;nbsp;* Debugger PIN: 125-928-819&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:44:54] &quot;GET / HTTP/1.1&quot; 200 -&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:44:54] &quot;GET /static/style.css HTTP/1.1&quot; 200 -&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:44:54] &quot;GET /static/script.js HTTP/1.1&quot; 200 -&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:44:54] &quot;GET /favicon.ico HTTP/1.1&quot; 404 -&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:45:11] &quot;POST /api/multiply HTTP/1.1&quot; 200 -&lt;/div&gt;&lt;div&gt;127.0.0.1 - - [25/Apr/2026 16:45:23] &quot;POST /api/divide HTTP/1.1&quot; 400 -&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;果然顯示了四則運算計算器頁面, 測試計算功能正常 (7 * 8=56, 1 除以 0 顯示 Error ) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpOosZvw7gFFXkx3HzGu8JB1XL3tM-7AbegU4IbGsOIet0XrcUhyO8zkr5RrxJ66bwWc1YDJnZ4oNYZYiDNQxOtFsloVJaKLVxfyuLRKVTuM-j-6Zqn1WZCi388QjSo85FdgkHfu3gnDvuuHRDvsrIUOvp9oF0lZuVku_D0sNvOL9W4R3cCZF6kshfSQgE/s841/gemin-cli-openspec-11.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;841&quot; data-original-width=&quot;638&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpOosZvw7gFFXkx3HzGu8JB1XL3tM-7AbegU4IbGsOIet0XrcUhyO8zkr5RrxJ66bwWc1YDJnZ4oNYZYiDNQxOtFsloVJaKLVxfyuLRKVTuM-j-6Zqn1WZCi388QjSo85FdgkHfu3gnDvuuHRDvsrIUOvp9oF0lZuVku_D0sNvOL9W4R3cCZF6kshfSQgE/s320/gemin-cli-openspec-11.jpg&quot; width=&quot;243&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlRTP-OXGPlsR3RA9zVRPz1-R5iW8gHC3dB3GK4DTyntrg90pke3TBPZesGnKIwybCQSVtbcEoALPRtHcBkDRdgteuYEyfxHd8aVq6_KlA-RbYwHGX77hVVxSXR2mS1xnZAG7-CxCVjuP_cnyT-QVkuRosj0jVVX-luE9Hh5PTemijeAoiDH-8Fd_0t-jT/s838/gemin-cli-openspec-12.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;838&quot; data-original-width=&quot;638&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjlRTP-OXGPlsR3RA9zVRPz1-R5iW8gHC3dB3GK4DTyntrg90pke3TBPZesGnKIwybCQSVtbcEoALPRtHcBkDRdgteuYEyfxHd8aVq6_KlA-RbYwHGX77hVVxSXR2mS1xnZAG7-CxCVjuP_cnyT-QVkuRosj0jVVX-luE9Hh5PTemijeAoiDH-8Fd_0t-jT/s320/gemin-cli-openspec-12.jpg&quot; width=&quot;244&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;(3). 歸檔結案 :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;完成以上功能驗證後, 就可以用 /opsx: archive 指令檢查所有的 Artifacts 是否齊全, 是的話將此次迭代的所有紀錄, 文件, 與過程收整, 並移動到 archive 資料夾收存 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;/opsx:archive&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;同樣地會有一連串的授權請求, 一律選擇預設的 Allow once :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgY-Cw_27A3cZKcPy1c0RFZ_I45cvezbcejMNZ-UkS8JVftlZEPWx-6BmZfeJC_U29Ysx8S_T2AygHJFYzb6LDohjCwVp1K8wMMZhWfoS88Moq41bkiFyT_HKmzrnTITSgPkzRXQBKL0EY4yeHJhonmD08-VNMcZTqx5XF9ToCw0Fv7RXbHZFp8XOnHcoa0/s2240/gemin-cli-openspec-13.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1090&quot; data-original-width=&quot;2240&quot; height=&quot;156&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgY-Cw_27A3cZKcPy1c0RFZ_I45cvezbcejMNZ-UkS8JVftlZEPWx-6BmZfeJC_U29Ysx8S_T2AygHJFYzb6LDohjCwVp1K8wMMZhWfoS88Moq41bkiFyT_HKmzrnTITSgPkzRXQBKL0EY4yeHJhonmD08-VNMcZTqx5XF9ToCw0Fv7RXbHZFp8XOnHcoa0/s320/gemin-cli-openspec-13.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;... (略) ...&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgViAl7Ou6zEWtcBlMoepdFsYftigUppEOX6o_MARMsXTtcxKLA8Xg9M05oD2qi5K711BTaT5W-PWXxwlS18uOyCyMVpBU6hJE1x_MXEzbrdApUIvktKrcaVjh_mLkOUILuZa-fCkY1X6kCDRXsxFa_wY7hr9hDUQ3xgnNcFXwDfXL_emaaOUBc17SubR7w/s2288/gemin-cli-openspec-14.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1135&quot; data-original-width=&quot;2288&quot; height=&quot;159&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgViAl7Ou6zEWtcBlMoepdFsYftigUppEOX6o_MARMsXTtcxKLA8Xg9M05oD2qi5K711BTaT5W-PWXxwlS18uOyCyMVpBU6hJE1x_MXEzbrdApUIvktKrcaVjh_mLkOUILuZa-fCkY1X6kCDRXsxFa_wY7hr9hDUQ3xgnNcFXwDfXL_emaaOUBc17SubR7w/s320/gemin-cli-openspec-14.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見所有此次迭代的紀錄文件都已歸檔於&amp;nbsp;openspec/changes/archive/2026-04-25-calc-basic/ 資料夾底下, 這些都是未來維護軟體的重要資料. 好啦! 終於完成初次迭代.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/2574066280757431013/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/2574066280757431013' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2574066280757431013'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2574066280757431013'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/gemini-cli-openspec.html' title='Gemini CLI 學習筆記 : OpenSpec 初體驗 (一)'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFnvV5j2FfTfH_CB6FiUtgwbMs9GM2nrQCaQHKdoZDRXCmW7-pRv9IpHsqpv84DssS99ohEwADWhFQceAwFkltTdULEf0FJ_c0DQbCvzhN-BjYmAe-KoLBmXHvAwd-ns7vuuzMzbE_szUwYvvbftTPdjCSek4TDTPxOY-5o5y2DcLP0sQsKoyfp2Icn2QQ/s72-c/openspec-init-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-505762303419409700</id><published>2026-04-23T09:55:00.010+08:00</published><updated>2026-04-23T13:57:34.874+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="網路爬蟲"/><title type='text'>Python 學習筆記 : 市圖借書與預約爬蟲程式架構</title><content type='html'>&lt;div&gt;這兩天完成市圖爬蟲改版 (v13), 由於此爬蟲機制較複雜, 由地端與雲端程式共同運作來維護佈署於 Render 的 Serverless 平台上的資料庫 (kmsl_books 資料表), 這與 v10 版之前的系統完全由地端爬蟲程式一手包辦爬取與傳訊完全不同. 爬蟲任務主要由放在高雄家的樹莓派主機負責, 目前主要是市圖借書與預約狀況與母校圖書館自動續借兩個任務而已, 今天抽空畫了一張架構圖以資備忘.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Pi 3B 對接 render.com 上的 5e6i 這個端點 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiErtb4Mj_Q_at4ite3rqt2_gdsD4griLbxmfAYizjpopUbOs5m3YxCQ1FZtVK4So95QhmhQIIBITNxavK5GpETGsknHJiG00SLNq9Rj-J-ChijmI5x3Y_MhhBj7Ae4z5-0HGbLDeKcQ_jVBW78_cTxlqn2nN6MiGSqkZdzPbKUWAayv44xbCsYCzH7wf5W/s1221/kaopi3-ksml-lib.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1154&quot; data-original-width=&quot;1221&quot; height=&quot;302&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiErtb4Mj_Q_at4ite3rqt2_gdsD4griLbxmfAYizjpopUbOs5m3YxCQ1FZtVK4So95QhmhQIIBITNxavK5GpETGsknHJiG00SLNq9Rj-J-ChijmI5x3Y_MhhBj7Ae4z5-0HGbLDeKcQ_jVBW78_cTxlqn2nN6MiGSqkZdzPbKUWAayv44xbCsYCzH7wf5W/s320/kaopi3-ksml-lib.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;Pi 3A+&amp;nbsp; 則對接 render.com 上的 fdof 這個端點 :&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi599cT41wH65obfHF9pQvlbOyMS6u2hU6BaJdLCVJIAQ1qJXxBQyy4i1UR0sRT7uVyrbVy6BIek4a-sRHY1CRvrmz0Ckl65h5HSDuLlpaKRFU76cr-XbUNXuPWOFVWivUH341Vvbr2FZTNzUZZQvjk6d4cSdCQqTG_Ym8Yem4Vgau0aDGCjHVm8hs-X_AV/s1240/pi3aplus-ksml-lib.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1168&quot; data-original-width=&quot;1240&quot; height=&quot;301&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi599cT41wH65obfHF9pQvlbOyMS6u2hU6BaJdLCVJIAQ1qJXxBQyy4i1UR0sRT7uVyrbVy6BIek4a-sRHY1CRvrmz0Ckl65h5HSDuLlpaKRFU76cr-XbUNXuPWOFVWivUH341Vvbr2FZTNzUZZQvjk6d4cSdCQqTG_Ym8Yem4Vgau0aDGCjHVm8hs-X_AV/s320/pi3aplus-ksml-lib.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;佈署於樹莓派上的市圖爬蟲程式 ksml_lib_13.py 負責每小時爬取網頁擷取借書與預約狀態後, 呼叫佈署於 render.com 的 serverless 平台函式 update_ksml_books.py, 將資訊儲存在 kmsl_books 資料表裡, 結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTcyxwQOwlySsSity5g2IpZN05vGeBqPVuLOTSwXY3nTDsow1hvW28iJLH29hZVHGnc724RRy5TrytWz52uBVK7HWNQ9cN0I1Usm14WAm4WLdqlFKm8gZxYkm14YWLGLKYgPbPj1ElGYXRUok0shUu7yFx63URmXrXkfwvLk1-h37ZrHBJ6-THeWE76xJu/s2537/render.com-ksml-books-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1222&quot; data-original-width=&quot;2537&quot; height=&quot;154&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTcyxwQOwlySsSity5g2IpZN05vGeBqPVuLOTSwXY3nTDsow1hvW28iJLH29hZVHGnc724RRy5TrytWz52uBVK7HWNQ9cN0I1Usm14WAm4WLdqlFKm8gZxYkm14YWLGLKYgPbPj1ElGYXRUok0shUu7yFx63URmXrXkfwvLk1-h37ZrHBJ6-THeWE76xJu/s320/render.com-ksml-books-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;另外一支本地程式 get_books_messages.py 透過 crontab 驅動定時呼叫 serverless 平台函式 send_books_messages.py 讀取 kmsl_books 資料表, 然後由發送到 Telegram, 參考最近三版異動 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/10/python-v10_20.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v11&lt;/a&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/12/python-v12.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v12 (於 Pi 400)&lt;/a&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/04/python-v13.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v13&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;母校圖書館爬蟲 nkust_lib_11.py 的任務相對簡單, 只是登入網站後按全部續借鈕, 然後瀏覽借書頁面, 擷取已被預約的書目後用 Telegram 傳送通知訊息而已, 參考 :&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2026/04/python-v11.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 母校圖書館借書與預約爬蟲程式改版 v11&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;至於 Pi 400 上佈署的爬蟲與 Pi 3B 的相同 (對接 5e6i), 但正常情況會關掉 crontab 內的 ksml_lib_13.py (不執行), 僅執行 get_books_messages.py 與 nkust_lib_11.py, 若 Pi 3 主機出問題再開啟 Pi 400 的市圖爬蟲.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/505762303419409700/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/505762303419409700' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/505762303419409700'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/505762303419409700'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/python.html' title='Python 學習筆記 : 市圖借書與預約爬蟲程式架構'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiErtb4Mj_Q_at4ite3rqt2_gdsD4griLbxmfAYizjpopUbOs5m3YxCQ1FZtVK4So95QhmhQIIBITNxavK5GpETGsknHJiG00SLNq9Rj-J-ChijmI5x3Y_MhhBj7Ae4z5-0HGbLDeKcQ_jVBW78_cTxlqn2nN6MiGSqkZdzPbKUWAayv44xbCsYCzH7wf5W/s72-c/kaopi3-ksml-lib.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-505050466771746572</id><published>2026-04-21T11:06:00.015+08:00</published><updated>2026-05-04T16:33:35.945+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="旅行"/><title type='text'>沖繩之旅行程規劃 : 5/5~5/8</title><content type='html'>&lt;div&gt;年初時原本打算入春後帶爸坐郵輪遊沖繩, 但想到上回搭麗星郵輪去香港 (原本是石垣島+沖繩, 因颱風改走香港) 的經驗, 在海上時無法上網形同斷訊, 在船上日吃五餐會變胖, 於是打消了遊輪計畫, 改為搭飛機自助遊, 原本想說嘗試在沖繩自駕, 但又對右駕系統沒自信能駕馭, 那就包車吧! 我跟水某與菁菁加上爸四個人包一台車即可, 不過後來加入岳父母, 小姨子, 與剛高中畢業的外甥, 團員來到 8 人, 所以必須包 10 人座的小巴啦 (Toyota Hiace)!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;今天用 Gemini 規劃這四天三夜親子沖繩旅遊行程如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Day 1 :&amp;nbsp;5/5 (週一) 抵達那霸入住國際通嘉新酒店&lt;/div&gt;&lt;div&gt;&lt;br /&gt;


&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 99.011px;&quot;&gt;&amp;nbsp;時間軸&lt;/th&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 502.011px;&quot;&gt;&amp;nbsp;行程說明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;15:55 - 18:45&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;搭乘華航 CI132 抵達那霸機場，可預約 10人座單程接機直達飯店。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;20:30 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;Check-in國際通嘉新酒店。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;21:00 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;晚餐 ： 國際通島唄與地料理 Tubaraama 餐廳。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;


&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;PS :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;晚餐請包車業者沖繩行腳代訂島唄與地料理 Tubaraama 餐廳 21:00 用餐.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;Day 2 :&amp;nbsp;5/6 (週二) 北部遠征（水族館＋古宇利島）&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 99.011px;&quot;&gt;&amp;nbsp;時間軸&lt;/th&gt;&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 502.011px;&quot;&gt;&amp;nbsp;行程說明&lt;/th&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;09:00 -&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;出發 (建議包車 10 小時)： 往返北部車程長，包車可在車上補眠。&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;10:40 -&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;古宇利島： 欣賞跨海大橋美景，推薦搭乘古宇利海洋塔的電動導覽車。&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;12:00 -&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;午餐： 北部景觀餐廳（如：百年古家大家）。&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;14:00 -&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;沖繩美麗海水族館： 觀看黑潮之海大水槽與鯨鯊。&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;17:00 -&amp;nbsp;&lt;/td&gt;&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;啟程返回那霸返回那霸晚餐 (那霸的許田交流道常塞車須提早返航)。&lt;/td&gt;&lt;/tr&gt;    
  &lt;/tbody&gt;
&lt;/table&gt;
  &lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;Day 3 :&amp;nbsp;5/7 (週三)&amp;nbsp;景觀與購物（瀨長島＋永旺來客夢）&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 99.011px;&quot;&gt;&amp;nbsp;時間軸&lt;/th&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 502.011px;&quot;&gt;&amp;nbsp;行程說明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;09:00 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;出發 (包車 10 小時，今天景點較多且分散，包車最省體力。)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;09:40 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;知念岬公園： 欣賞 270 度無敵海景，這裡步道平緩，停留約 40 分鐘。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;11:00 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;瀨長島 Umikaji Terrace：&amp;nbsp;午餐推薦幸福鬆餅/天使鬆餅 Happy Pancake。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;13:30 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;美國村： 異國風情街區散步，拍拍彩色房子。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;15:30 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;永旺來客夢 (AEON Mall Rycom)：直衝寶可夢中心，晚餐： 永旺美食街&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;18:15 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;回國際通： 回飯店後可進行採買 (傍晚從中部回國際通可能有車潮)。&lt;/td&gt;
&lt;/tr&gt;  
&lt;/tbody&gt;
&lt;/table&gt;



&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;註 : 瀨長島午餐人潮多，長輩在白色階梯區步行較費力，建議司機停在最上層或最下層。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Day 4 :&amp;nbsp;5/8 (週四)&amp;nbsp;神社祈福・悠閒歸途&lt;/div&gt;&lt;div&gt;&lt;br /&gt;

&lt;table border=&quot;1&quot; bordercolor=&quot;#888&quot; cellspacing=&quot;0&quot; style=&quot;background-color: white; border-collapse: collapse; border-color: rgb(136, 136, 136); border-width: 1px; color: #534741; font-family: Arial, Verdana, sans-serif; font-size: 13.3333px; margin: 0px;&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 99.011px;&quot;&gt;&amp;nbsp;時間軸&lt;/th&gt;
&lt;th style=&quot;background-color: #f2f2f2; padding: 10px; text-align: left; width: 502.011px;&quot;&gt;&amp;nbsp;行程說明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;09:00 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;波上宮： 到琉球第一神社祈福。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;12:00 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;午餐： 國際通周邊（如：第一牧志公設市場吃海鮮或道地阿古豬）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;15:30 -&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;啟程前往機場： 長輩搭計程車，其他往縣廳站坐單軌 (13-16 分鐘)。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;18:30 - 19:15&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&amp;nbsp;搭乘華航 CI2133 返回高雄小港。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;


&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;💡 行程建議：&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;知念岬 vs 美國村： &lt;br /&gt;知念岬在東南角，美國村與永旺在中部。這天行程景點多，需要 10 人座包車接送。&lt;/li&gt;&lt;li&gt;關於「天使鬆餅」： &lt;br /&gt;瀨長島的幸福鬆餅（Happy Pancake）極熱門，建議事先網路預約，否則現場排隊會很久。&lt;/li&gt;&lt;li&gt;寶可夢中心： &lt;br /&gt;永旺來客夢的寶可夢中心很大可以慢慢逛，長輩可以帶到商場中間的大型水族箱區坐著休息。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;💡 關於美麗海水族館 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;黑潮之海 (The Kuroshio Sea)：&lt;br /&gt;這是水族館的靈魂。巨大的壓克力水槽中，數隻巨大的鯨鯊與鬼蝠魟在藍色海水中緩緩游動。&lt;/li&gt;&lt;li&gt;海豚秀 (Okichan Theater)：&lt;br /&gt;位於水族館主體建築外的海邊。看表演時可以吹海風、看海景。提醒：海豚表演場地離水族館出口有一小段路，建議推輪椅前往較輕鬆。&lt;/li&gt;&lt;li&gt;觸摸池：&lt;br /&gt;可以嘗試觸摸海星和海參，是很有趣的互動。&lt;/li&gt;&lt;li&gt;水族館的「鯨鯊餵食秀」：&lt;br /&gt;通常在下午 15:00 和 17:00 各有一場。鯨鯊會直立起來進食，非常震撼，建議將行程調整在下午抵達，看完餵食秀再離開。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;輪椅借用：&lt;br /&gt;美麗海水族館的入口處提供免費輪椅借用。即便長輩平時行走自如，但水族館園區很大，強烈建議借一台預備，當長輩逛到一半疲累時可以推著他們走，確保行程愉快不掃興。&lt;/li&gt;&lt;li&gt;避開正午陽光：&lt;br /&gt;5 月的沖繩陽光已很強。建議中午在古宇利島附近的景觀餐廳用餐（吹冷氣看海），下午最熱的時候留在水族館室內，這對長輩的體力保持最有幫助。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirklcvH_qcii8vDhRMAabCqq_cPc3rVZOsF7xNEh1p1Sfm7b_8xAjwqoUSBU-7Z9LNBl2xuL8HbSCH2AJOdKYmp-uN5Qy-eMchForOYs9vEi1o8R-LnzqPAWHz_MdcOrlZt37Zb60At-MlL3qNStYw5GkBOTzN_EngDiqpnPyod6P4l8SOZL3f-e7bLY46/s1035/%E6%B2%96%E7%B9%A9%E7%BE%8E%E9%BA%97%E6%B5%B7%E6%B0%B4%E6%97%8F%E9%A4%A8-1jpg.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1035&quot; data-original-width=&quot;838&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirklcvH_qcii8vDhRMAabCqq_cPc3rVZOsF7xNEh1p1Sfm7b_8xAjwqoUSBU-7Z9LNBl2xuL8HbSCH2AJOdKYmp-uN5Qy-eMchForOYs9vEi1o8R-LnzqPAWHz_MdcOrlZt37Zb60At-MlL3qNStYw5GkBOTzN_EngDiqpnPyod6P4l8SOZL3f-e7bLY46/s320/%E6%B2%96%E7%B9%A9%E7%BE%8E%E9%BA%97%E6%B5%B7%E6%B0%B4%E6%97%8F%E9%A4%A8-1jpg.jpg&quot; width=&quot;259&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;💡 古宇利島 (Kouri Island) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;古宇利島被稱為「神之島」或「戀之島」，從本島開車經過古宇利大橋 時，兩側的翡翠綠海色被譽為「古宇利藍」，是沖繩最美的公路景觀之一。&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;古宇利大橋：&lt;br /&gt;長約 2 公里。建議請包車司機在過橋時放慢車速，這段路不用走路，坐在車內就能欣賞絕佳海景。&lt;/li&gt;&lt;li&gt;古宇利海洋塔 (Kouri Ocean Tower)：&lt;br /&gt;這裡有自動駕駛的無人導覽小車，會載著全家人緩緩繞行山坡抵達塔頂。沿途會播放語音介紹，並能從高處俯瞰整座大橋，風景極其壯觀。&lt;/li&gt;&lt;li&gt;心形岩 (Heart Rock)：&lt;br /&gt;這位於島的另一端海灘。注意：前往沙灘的路徑較不平坦且多沙，長輩可在車上休息或在岸邊咖啡廳喝茶看海。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik9CajEscV1dbLKWCtbkmrruGekEMt3DGnQHiF1khHnvUjX3_thI1zqbltRrzqyrXA9nQB5N_jX2bLLrkhZ42yUhDacMq7alsvBuaiDSBu2L1Akb4xY5WjR6g1o4iPX_a-y80uea97Q9HFuj8JkOhrCy3H5C-gZiIJ0souztoDVPpz6r5_3wbUyDDN9Mqz/s1329/%E6%B2%96%E7%B9%A9%E5%8F%A4%E5%AE%87%E5%88%A9%E5%B3%B6-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;794&quot; data-original-width=&quot;1329&quot; height=&quot;191&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik9CajEscV1dbLKWCtbkmrruGekEMt3DGnQHiF1khHnvUjX3_thI1zqbltRrzqyrXA9nQB5N_jX2bLLrkhZ42yUhDacMq7alsvBuaiDSBu2L1Akb4xY5WjR6g1o4iPX_a-y80uea97Q9HFuj8JkOhrCy3H5C-gZiIJ0souztoDVPpz6r5_3wbUyDDN9Mqz/s320/%E6%B2%96%E7%B9%A9%E5%8F%A4%E5%AE%87%E5%88%A9%E5%B3%B6-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;💡知念岬公園 (Cape Chinen Park) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;位於沖繩本島東南部的南城市，是一個非常純粹、能讓人感受到大自然震撼的美麗景點。沿著公園的紅色扶手步道往下走一點點，可以拍到步道延伸進大海的感覺。建議停留 30 到 45 分鐘。這裡適合安靜地看海，當作早上的第一個景點，呼吸一下新鮮空氣。知念岬的風有時會比較大，即便 5 月天氣轉熱，建議帶件輕薄的防風外套。&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;270 度的無敵海景 :&lt;br /&gt;知念岬三面環海，視野極其開闊。從展望台望出去，可以看到湛藍的太平洋（天氣好時呈現漸層的藍色），還能遠眺被琉球人視為聖島的「久高島」。這裡與其他懸崖景點不同，從停車場到主要的景觀台和草地步道都非常平緩，推輪椅或讓長輩慢慢散步都非常輕鬆。即使長輩不想走太遠，只要下車站在入口附近的涼亭，就能看到極佳的海景，不需要長途跋涉。&lt;/li&gt;&lt;li&gt;滑翔傘愛好者的天堂 :&lt;br /&gt;這裡因為地形關係，常能看到彩色滑翔傘在空中飛翔，看著滑翔傘配上背景的海景，拍照也非常漂亮。&lt;/li&gt;&lt;li&gt;緊鄰「齋場御嶽」:&lt;br /&gt;知念岬公園就在琉球王國第一聖地「齋場御嶽」附近（走路約 5-10 分鐘）。對文化感興趣，可以順便去走走；但要注意「齋場御嶽」內有很多崎嶇的石頭路，對長輩來說比較吃力，建議留在知念岬公園看海、吹風即可。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiywbmJkfq2t_7EjuRW-841no4VemvNlUsj9KCwOs_uxwHvyVrtNQgMeRh1gFrJi5b-mbME6kYSb-gRzOGGAhar02FYl9sRnwRhOgExNgCgvlpwNJrVfPSFjhcS17wPn7CurrUhTunpF7Dn2bghFJY1ZtDsnp0tlFqqpTS89JQ9sln7Csa-PRHDbkrRde2a/s2033/%E7%9F%A5%E5%BF%B5%E5%B2%AC%E5%85%AC%E5%9C%92-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1003&quot; data-original-width=&quot;2033&quot; height=&quot;158&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiywbmJkfq2t_7EjuRW-841no4VemvNlUsj9KCwOs_uxwHvyVrtNQgMeRh1gFrJi5b-mbME6kYSb-gRzOGGAhar02FYl9sRnwRhOgExNgCgvlpwNJrVfPSFjhcS17wPn7CurrUhTunpF7Dn2bghFJY1ZtDsnp0tlFqqpTS89JQ9sln7Csa-PRHDbkrRde2a/s320/%E7%9F%A5%E5%BF%B5%E5%B2%AC%E5%85%AC%E5%9C%92-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;💡瀨長島 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;瀨長島位於那霸機場南端，從那霸市區（嘉新酒店）往南開到瀨長島只要約 20 分鐘，接著再往北去美國村和永旺來客夢百貨，動線非常順。雖然它是一座島，但開車過橋只需要 1 分鐘，完全沒有「搭船」的負擔。因為離機場極近，這裡是看飛機起降的絕佳地點。瀨長島的白色建築是依山坡而建，樓梯較多。建議包車司機直接載到最上層放人，由上往下逛會輕鬆很多。&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Umikaji Terrace (瀨長島陽台) :&lt;br /&gt;這是像希臘聖托里尼一樣的純白色建築群，有非常多靠海的露天咖啡廳，可以坐在遮陽傘下吹著海風、看著藍色海洋和飛機。不需要爬高爬低，非常放鬆。也有很多文創小店和網美下午茶店（例如幸福鬆餅），非常適合拍照打卡。&lt;/li&gt;&lt;li&gt;足湯體驗： &lt;br /&gt;島上有免費的足湯（靠近琉球溫泉瀨長島飯店），如果走累了，在那裡泡腳看飛機也是一種享受。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;💡美國村（American Village）:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;位於沖繩本島中部的北谷町，是一個將美式西海岸風情與沖繩海島氛圍完美融合的綜合型景區。這裡曾經是美軍基地，後來改建為商圈，是沖繩最具異國風情的地方，非常適合下午短暫停留。美國村範圍很大，建議請包車司機載到最靠近海邊的 「Depot Island Seaside」 區下車。那裡是最精華、色彩最鮮艷的區域 (寶可夢迷會發現建築的牆面上隱藏了很多「皮卡丘」彩繪)，也是步道起點，從那裡沿著海岸步道往南逛，走到落日海灘 (Sunset Beach) 大約只需 10-15 分鐘，那裡有一間 Aeon 北谷店（超市），可以買點當地零食或飲料，司機可以約在 Aeon 門口接。美國村離永旺來客夢百貨（AEON Mall Rycom）車程只需約 15-20 分鐘，建議下午在美國村拍照散步，傍晚再前往永旺吃晚餐與購物。&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;獨特的建築與視覺（拍照聖地）:&lt;br /&gt;這裡的建築色彩鮮艷，牆面充滿了各種美式風格的塗鴉與裝飾，隨處都能拍出極具風格的照片。&lt;/li&gt;&lt;li&gt;海濱步道： &lt;br /&gt;美國村緊鄰著北谷公園的落日海灘。這裡有一條非常平緩的海濱步道，可以一邊吹著海風，一邊散步看海，完全沒有階梯負擔。&lt;/li&gt;&lt;li&gt;美式潮流文創 :&lt;br /&gt;這裡有很多美式古著店、潮流服飾以及好拍的文創商店。此外，這裡也常能看到大型的模型或獨特的車輛裝飾。也有非常多舒適的戶外咖啡廳（如著名的 Pokébowls 或 ZHYVAGO COFFEE），走累了可以坐在靠海的位子看著來往的各國遊客與美軍家庭，也是一種很不一樣的觀察體驗。&lt;/li&gt;&lt;li&gt;落日夕陽： &lt;br /&gt;美國村的夕陽是全沖繩前三名的美景。在傍晚時分坐在岸邊看夕陽是非常浪漫的。傍晚在 Depot Island 的紅磚區有時會有街頭魔術表演。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqs7X9fuqjj_xAUxsIbWAc3FjDmW_mv176rL8Jxb_nzB822vrNLZbiKaO9WyE0wF8gl4-XxkLCPb74v9jqh_2MxHHQnCQf_uX9ESCY7G-WJwaFsL4kjZrVCI-siLdphRPseXCKQ2pC_JkcFa5_mXWiXcW5Hi6rBm_V-JvFgIYaXF0s2HUZVnSKdPw6j4UA/s1827/%E6%B2%96%E7%B9%A9%E7%BE%8E%E5%9C%8B%E6%9D%91-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1131&quot; data-original-width=&quot;1827&quot; height=&quot;198&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqs7X9fuqjj_xAUxsIbWAc3FjDmW_mv176rL8Jxb_nzB822vrNLZbiKaO9WyE0wF8gl4-XxkLCPb74v9jqh_2MxHHQnCQf_uX9ESCY7G-WJwaFsL4kjZrVCI-siLdphRPseXCKQ2pC_JkcFa5_mXWiXcW5Hi6rBm_V-JvFgIYaXF0s2HUZVnSKdPw6j4UA/s320/%E6%B2%96%E7%B9%A9%E7%BE%8E%E5%9C%8B%E6%9D%91-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA30zNEGnXmRhRIBj4E3aIUrGcamR_f6inxItGjNzw18fRUDLuXy2oweGI9Knevp-ooOSwXTp68V2Q-_ggQkLRanT2hPwTmGOSVC2yqS39_VDL5ccgtNcV3VcY14KiFniHjDlqJecVE3XHcSfdnnZSh17SHs4iC7hGutzgMtXH-txQScEPa97_TY1npvY4/s1927/%E6%B2%96%E7%B9%A9%E7%BE%8E%E5%9C%8B%E6%9D%91-2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1157&quot; data-original-width=&quot;1927&quot; height=&quot;192&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjA30zNEGnXmRhRIBj4E3aIUrGcamR_f6inxItGjNzw18fRUDLuXy2oweGI9Knevp-ooOSwXTp68V2Q-_ggQkLRanT2hPwTmGOSVC2yqS39_VDL5ccgtNcV3VcY14KiFniHjDlqJecVE3XHcSfdnnZSh17SHs4iC7hGutzgMtXH-txQScEPa97_TY1npvY4/s320/%E6%B2%96%E7%B9%A9%E7%BE%8E%E5%9C%8B%E6%9D%91-2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;💡永旺來客夢 (AEON Mall Rycom) :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;永旺夢樂城來客夢（AEON Mall Okinawa Rycom）是沖繩規模最大、設施最完善的購物中心，對您的家庭成員來說，這裡不僅是購物點，更是一個全齡化的休閒空間。從美國村到永旺來客夢約 20 分鐘車程.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;震撼的入口大水族箱（Rycom Aquarium）:&lt;br /&gt;一進大門，一樓大廳就有一個巨大的水族箱。雖然比不上美麗海水族館，但裡面有近千條色彩繽紛的熱帶魚和小型鯊魚。&lt;/li&gt;&lt;li&gt;寶可夢中心 (Pokémon Center)： &lt;br /&gt;位於四樓，是沖繩唯一的官方專賣店，有許多沖繩限定（如穿著花襯衫的皮卡丘）的商品。&lt;/li&gt;&lt;li&gt;潮流品牌： &lt;br /&gt;這裡聚集了 Uniqlo, GU, H&amp;amp;M, Old Navy 等各大品牌，還有日本當地的文創小物，非常適合年輕人。&lt;/li&gt;&lt;li&gt;AEON Style： &lt;br /&gt;二樓有大型超市和藥妝區，可以在這裡一次買齊要帶回台灣的沖繩零食、泡麵或是藥妝，不用在國際通辛苦提貨。&lt;/li&gt;&lt;li&gt;Rycom Sky Diner :&lt;br /&gt;五樓有非常多美食餐廳，從傳統琉球料理、日式豬排、壽司到美式牛排都有。五樓的戶外露台可以俯瞰中部的海景。如果天氣好，在這裡吃午餐或晚餐視野極佳。&lt;/li&gt;&lt;li&gt;美食街： &lt;br /&gt;三樓、四樓的美食廣場設有寬敞的無障礙座位區。&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgey9np5mPZobzRUaSZrsrxVvnGPZtD-mzGs9doVLKPtkO8WmPn5qQ40HvJ4z8DYgaq0zdutudTJBoisL8f296-qh5JpjRI67E-HBKQP21pTaueQXnpXzcN6luiq3lQyjItRvrJuOLJdVrnxw7hsHm4cZwh_lE5R66BgX2WvHZOPG10vnsl211L4KFCsYyq/s2182/%E6%B2%96%E7%B9%A9%E6%B0%B8%E6%97%BA%E4%BE%86%E5%AE%A2%E5%A4%A2-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;981&quot; data-original-width=&quot;2182&quot; height=&quot;144&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgey9np5mPZobzRUaSZrsrxVvnGPZtD-mzGs9doVLKPtkO8WmPn5qQ40HvJ4z8DYgaq0zdutudTJBoisL8f296-qh5JpjRI67E-HBKQP21pTaueQXnpXzcN6luiq3lQyjItRvrJuOLJdVrnxw7hsHm4cZwh_lE5R66BgX2WvHZOPG10vnsl211L4KFCsYyq/s320/%E6%B2%96%E7%B9%A9%E6%B0%B8%E6%97%BA%E4%BE%86%E5%AE%A2%E5%A4%A2-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;💡包車資訊 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;5/6 與 5/7 日兩天需包車, 8 個人需 10 人座的車.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;# &lt;a href=&quot;https://footprint-rentacar.com/&quot; target=&quot;_blank&quot;&gt;沖繩行腳&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;先掃 LINE 官方帳號 QR 碼 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdGtBdWY6oNhRpUF8WMuPRrXTF4wKOPN39WaDg50akxMVb26tESMUvH2G3RWfYIg8iDSQzHoq-yc6_Uk_kG6gNPSgGZHh_Ib-aEQtXmayFeS6Zel-bad4IKVO4AqMzlBDB63A0H5OtX0cst21w7cuj-O2nvhVE6b_kQNs-BjnIou4VwNqWNQJdMQaGaUxz/s497/%E6%B2%96%E7%B9%A9%E8%A1%8C%E8%85%B3-1.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;482&quot; data-original-width=&quot;497&quot; height=&quot;310&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdGtBdWY6oNhRpUF8WMuPRrXTF4wKOPN39WaDg50akxMVb26tESMUvH2G3RWfYIg8iDSQzHoq-yc6_Uk_kG6gNPSgGZHh_Ib-aEQtXmayFeS6Zel-bad4IKVO4AqMzlBDB63A0H5OtX0cst21w7cuj-O2nvhVE6b_kQNs-BjnIou4VwNqWNQJdMQaGaUxz/s320/%E6%B2%96%E7%B9%A9%E8%A1%8C%E8%85%B3-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;然後簡述包車日期與行程概要, 請其報價 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;您好，我打算與親友去沖繩旅行，需要中文司機包車兩天，每天10小時，麻煩您報價，謝謝您。行程大致如下：&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;沖繩&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;1.乘客人數：8&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;2.行李件數：4&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;3.包車日期：5/6~5/7&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;4.行程規劃：5/6 古宇利島，水族館；5/7 知念岬公園，瀬長島，美國村，永旺來客夢&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;5.出發地點：國際通嘉新酒店&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;&lt;i&gt;6.結束地點：國際通嘉新酒店&lt;/i&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/505050466771746572/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/505050466771746572' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/505050466771746572'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/505050466771746572'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/5558.html' title='沖繩之旅行程規劃 : 5/5~5/8'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirklcvH_qcii8vDhRMAabCqq_cPc3rVZOsF7xNEh1p1Sfm7b_8xAjwqoUSBU-7Z9LNBl2xuL8HbSCH2AJOdKYmp-uN5Qy-eMchForOYs9vEi1o8R-LnzqPAWHz_MdcOrlZt37Zb60At-MlL3qNStYw5GkBOTzN_EngDiqpnPyod6P4l8SOZL3f-e7bLY46/s72-c/%E6%B2%96%E7%B9%A9%E7%BE%8E%E9%BA%97%E6%B5%B7%E6%B0%B4%E6%97%8F%E9%A4%A8-1jpg.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-7859785069258808687</id><published>2026-04-20T16:26:00.003+08:00</published><updated>2026-04-20T19:18:21.398+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="電腦"/><title type='text'>HDD/SSD 健康狀態檢測軟體 CrystalDiskInfo</title><content type='html'>&lt;div&gt;昨天成功地用 chdisk 救回 WD 5TB 硬碟後, 用&amp;nbsp;CrystalDiskInfo (CDI) 檢查了硬碟健康狀況為優良, 詢問 Gemini 得知它不僅能檢測硬碟, 還能檢測 SDD, 目前幾乎所有電腦都改用 SDD, 這種儲存跟汽車電瓶一樣, 接近壽命終點時說掛就掛, 而且內容就不回來, 所以要經常查看 SDD 健康狀態, 低於 50% 壽命時就要考慮更換或提高備份頻率, 以免後悔不及.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;以下是 Gemini 對 CDI 的說明與建議 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;===========================================================&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;p data-path-to-node=&quot;1&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;對現代電腦來說，&lt;span style=&quot;background-color: #fcff01;&quot;&gt;用 CDI 監測 SSD 的健康狀況甚至比 HDD 更重要&lt;/span&gt;，因為 SSD 有明確的**「寫入壽命限制」**。&lt;/p&gt;&lt;p data-path-to-node=&quot;2&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;不過，看 SSD 的報告時，重點指標與你剛才看那顆 5TB HDD 的邏輯不太一樣：&lt;/p&gt;&lt;h3 data-path-to-node=&quot;3&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;1. SSD 檢測的黃金指標：剩餘壽命&lt;/h3&gt;&lt;p data-path-to-node=&quot;4&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;在 SSD 的報告中，最上方大大的「健康狀態」通常會直接顯示一個百分比，例如 &lt;b data-index-in-node=&quot;39&quot; data-path-to-node=&quot;4&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;「良好 98%」&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;5,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;這是什麼：&lt;/b&gt; 這是根據 SSD 廠商定義的 &lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;5,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;TBW (Total Bytes Written，總寫入量)&lt;/b&gt; 估算出來的。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;5,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;怎麼看：&lt;/b&gt; * &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;5,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;100% - 90%：&lt;/b&gt; 像新的一樣。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,1,1&quot; style=&quot;line-height: 1.15; margin-top: 0px; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;5,1,1,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,1,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;低於 50%：&lt;/b&gt; 雖然還能用，但建議開始尋找下一顆硬碟。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;5,1,1,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,1,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;低於 10% 或 0%：&lt;/b&gt; 隨時可能進入「唯讀模式」或直接掛掉，必須立刻更換。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;6&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;2. 重點關注項目（對比 HDD）&lt;/h3&gt;&lt;p data-path-to-node=&quot;7&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;打開 CDI 看你的 SSD 時，請盯住這兩項：&lt;/p&gt;&lt;ul data-path-to-node=&quot;8&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;8,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;03 剩餘壽命 (Percentage Used)：&lt;/b&gt; 顯示已消耗的設計壽命。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;8,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;F1 總計主機寫入量 (Total Host Writes)：&lt;/b&gt; 這告訴你這顆 SSD 自出廠以來到底寫入了多少資料（通常以 GB 或 TB 為單位）。對於開發者來說，如果你頻繁編譯大型專案或跑大量 Log，這個數字會跳很快。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;8,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;01 介面 CRC 錯誤計數：&lt;/b&gt; 如果這個數值不是 0，通常代表你的 &lt;b data-index-in-node=&quot;34&quot; data-path-to-node=&quot;8,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;SATA 線&lt;/b&gt;或是 &lt;b data-index-in-node=&quot;43&quot; data-path-to-node=&quot;8,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;M.2 插槽&lt;/b&gt;接觸不良，而不是 SSD 本身壞了。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;9&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot; /&gt;&lt;h3 data-path-to-node=&quot;10&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;3. SSD 與 HDD 在 CDI 中的差異&lt;/h3&gt;&lt;table data-path-to-node=&quot;11&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-bottom: 32px; margin-top: 0px;&quot;&gt;&lt;thead style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;tr style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;strong style=&quot;line-height: 1.15; margin-bottom: 0px; margin-top: 0px;&quot;&gt;項目&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;strong style=&quot;line-height: 1.15; margin-bottom: 0px; margin-top: 0px;&quot;&gt;HDD (傳統硬碟)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;strong style=&quot;line-height: 1.15; margin-bottom: 0px; margin-top: 0px;&quot;&gt;SSD (固態硬碟)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;tr style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,1,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;主要風險&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,1,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;物理壞軌、機械磨損&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,1,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;閃存顆粒寫入次數耗盡&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,2,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;關鍵指標&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,2,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;05 (重定位磁區)、C5 (等候重定)&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,2,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;03 (剩餘壽命百分比)&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,3,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,3,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;溫度&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,3,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;建議 50°C 以下&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,3,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;較高，NVMe 飆到 60-70°C 算常見&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,4,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,4,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;異常徵兆&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,4,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;異音、讀取極慢、chkdsk 噴錯誤&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span data-path-to-node=&quot;11,4,2,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;突然偵測不到、檔案消失、藍屏&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr data-path-to-node=&quot;12&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot; /&gt;&lt;h3 data-path-to-node=&quot;13&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;4. 檢查建議&lt;/h3&gt;&lt;p data-path-to-node=&quot;14&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;切換到你的 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;14&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;C 槽（通常是 SSD）&lt;/b&gt; 看一下：&lt;/p&gt;&lt;ol data-path-to-node=&quot;15&quot; start=&quot;1&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;15,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;檢查百分比：&lt;/b&gt; 確認你的系統碟還有多少壽命。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;p data-path-to-node=&quot;15,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;檢查傳輸模式：&lt;/b&gt; 確認它是否跑在 &lt;code data-index-in-node=&quot;16&quot; data-path-to-node=&quot;15,1,0&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;PCIe 4.0 x4&lt;/code&gt; 或是你主機板支援的最高速度（如果你是用高效能 NVMe SSD）。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-path-to-node=&quot;16&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16&quot; style=&quot;line-height: 1.15; margin-top: 0px;&quot;&gt;特別提醒：&lt;/b&gt;&lt;/p&gt;&lt;p data-path-to-node=&quot;16&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;SSD 如果顯示「黃燈（警告）」，通常不像 HDD 那樣還能透過 &lt;code data-index-in-node=&quot;39&quot; data-path-to-node=&quot;16&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif; line-height: 1.15; margin-top: 0px;&quot;&gt;chkdsk&lt;/code&gt; 救回「邏輯損壞」。SSD 一旦顆粒老化出現錯誤，通常是不可逆的，**「備份」**是唯一的救贖&lt;/span&gt;。&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;下面是我的 LG Gram 筆電兩顆 SSD 檢測結果 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;一. C 碟 : 三星 1TB Samsung PM9A1 (MZVL21T0HCLR)&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiU6uYl5lm_UP5oir8rvvFBgmQv41XJFpMYd8RQ-5keh0TGu65GPmrafJSDMoIc7-mY3jLoOQy6x7-rmss8xbAQmO7cCeGebaXv5hMSiQjQTw4CqPKGaLz9LNQrswuuLTWvdNd7DbxXPZQKHKlRN_0swibeXbtFHCf1WxNrHVfgzEXT3wrDkAfjDGqpTRV9/s1207/LG_GRAM_SSD_C%E7%A2%9F%E6%AA%A2%E6%B8%AC%E7%B5%90%E6%9E%9C.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1207&quot; data-original-width=&quot;1002&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiU6uYl5lm_UP5oir8rvvFBgmQv41XJFpMYd8RQ-5keh0TGu65GPmrafJSDMoIc7-mY3jLoOQy6x7-rmss8xbAQmO7cCeGebaXv5hMSiQjQTw4CqPKGaLz9LNQrswuuLTWvdNd7DbxXPZQKHKlRN_0swibeXbtFHCf1WxNrHVfgzEXT3wrDkAfjDGqpTRV9/s320/LG_GRAM_SSD_C%E7%A2%9F%E6%AA%A2%E6%B8%AC%E7%B5%90%E6%9E%9C.jpg&quot; width=&quot;266&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Gemini 的 SSD 健康檢查報告分析：&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;h3 data-path-to-node=&quot;2&quot;&gt;1. 核心健康指標&lt;/h3&gt;&lt;ul data-path-to-node=&quot;3&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot;&gt;健康狀態：良好 98%&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;3,0,1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,0,1,0,0&quot;&gt;對於已經使用 &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;3,0,1,0,0&quot;&gt;5323 小時&lt;/b&gt;（約 7 個多月不間斷運作）的系統碟來說，只消耗了 2% 的壽命，這代表它的耐用度非常高。依照這個速度，這顆 SSD 要達到壽命臨界值可能還需要十年以上。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;SSD 總寫入量：29,768 GB (約 29 TB)&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;3,1,1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,1,1,0,0&quot;&gt;這反映了你作為軟體工程師的工作量。頻繁的編譯、下載與檔案操作都會累積寫入量。29 TB 雖然聽起來很多，但這顆 SSD 的設計寫入壽命通常在 600 TBW 以上，目前僅用了不到 5%。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;4&quot;&gt;2. 值得注意的細節&lt;/h3&gt;&lt;ul data-path-to-node=&quot;5&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;傳輸模式：PCIe 4.0 x4 | PCIe 4.0 x4&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,0,1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,0,1,0,0&quot;&gt;這代表你的硬碟正確地運行在最高頻寬下，沒有因為插槽或設定問題被降速。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;溫度：39°C&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,1,1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,1,1,0,0&quot;&gt;對於 NVMe SSD 來說，這個溫度非常低且理想。這說明 LG Gram 的內部散熱設計（或這顆 Samsung SSD 的發熱控制）做得很好，在高負載運作時也不容易發生因過熱而降速（Thermal Throttling）的情況。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;不安全關機次數：33 次 (0x21)&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,2,1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,2,1,0,0&quot;&gt;這通常發生在電腦當機、強制長按電源鍵關機，或是電池完全沒電自動關機時。雖然次數不多，但 SSD 對斷電比 HDD 敏感，建議盡量避免在檔案寫入時強制關機。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;6&quot; /&gt;&lt;h3 data-path-to-node=&quot;7&quot;&gt;3. 對於這顆 SSD 的維護建議&lt;/h3&gt;&lt;p data-path-to-node=&quot;8&quot;&gt;注意, &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;8&quot;&gt;SSD 維護上&lt;/b&gt;做法與 HDD 完全相反：&lt;/p&gt;&lt;ol data-path-to-node=&quot;9&quot; start=&quot;1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;絕對不要執行「磁碟重組」：&lt;/b&gt;
SSD 內部有自己的「磨損均衡 (Wear Leveling)」機制，會自動安排資料存放位置以延長壽命。手動重組只會白白浪費寫入次數，對速度毫無幫助。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;確保開啟 TRIM 指令：&lt;/b&gt;
Windows 10/11 預設會開啟。它會告訴 SSD 哪些區塊的資料已刪除可以回收。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;保留 15% 以上空間：&lt;/b&gt;
雖然 1TB 很大，但 SSD 如果塞得太滿（超過 90%），寫入放大效應會增加，導致壽命消耗變快且速度變慢。建議 C 碟隨時保留約 150GB 左右的空閒空間。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,3,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,3,0&quot;&gt;定期用 CDI 檢查「03 可用備用 (Available Spare)」：&lt;/b&gt;
如果這個數值（目前是 100%）開始下降，才代表 SSD 真的開始出現損壞的記憶體顆粒。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-path-to-node=&quot;10&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10&quot;&gt;總結：&lt;/b&gt;
這是一顆頂級的系統碟，目前正處於巔峰狀態，無需任何特殊整理。你那顆 WD 5TB HDD 比較需要關注「整理」，而這顆 Samsung SSD 只要維持正常使用、不要塞太滿即可。&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce5cd; color: #990000;&quot;&gt;二. D 碟 WD Blue SN570 (1TB) :&lt;/span&gt;&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ8c57F05dZHeFEqSzLFGt6ICFRYN1yvAaHBiL_iSZM4_oYHg3fMbJU1yPKbEZqSQduuvKTX9Uj6eocQw-PuLSgCliVOvPoMwz8RiCoZp5IpY9sq7RfJBXTWLL_7evwpNlnT-sBGAWrimGXwqFT7VS3suKNc1UM07H4Q9wkcJeLGlpqEt6mvcCmXum9yEF/s1206/LG_GRAM_SSD_D%E7%A2%9F%E6%AA%A2%E6%B8%AC%E7%B5%90%E6%9E%9C.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1206&quot; data-original-width=&quot;1012&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ8c57F05dZHeFEqSzLFGt6ICFRYN1yvAaHBiL_iSZM4_oYHg3fMbJU1yPKbEZqSQduuvKTX9Uj6eocQw-PuLSgCliVOvPoMwz8RiCoZp5IpY9sq7RfJBXTWLL_7evwpNlnT-sBGAWrimGXwqFT7VS3suKNc1UM07H4Q9wkcJeLGlpqEt6mvcCmXum9yEF/s320/LG_GRAM_SSD_D%E7%A2%9F%E6%AA%A2%E6%B8%AC%E7%B5%90%E6%9E%9C.jpg&quot; width=&quot;269&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Gemini 的 SSD 健康檢查報告分析：&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;p data-path-to-node=&quot;0&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這顆 &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;WD Blue SN570&lt;/b&gt; 簡直就是「新車剛過磨合期」的狀態！雖然你是後來擴充的，但從數據上看，這顆硬碟幾乎沒怎麼被「操」到。&lt;/p&gt;&lt;p data-path-to-node=&quot;1&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;以下是針對這顆擴充碟的分析：&lt;/p&gt;&lt;h3 data-path-to-node=&quot;2&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;1. 核心健康指標&lt;/h3&gt;&lt;ul data-path-to-node=&quot;3&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;健康狀態：良好 100%&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;3,0,1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,0,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這非常罕見，代表這顆 SSD 的設計壽命完全還沒被消耗。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;已用時間總計：82 小時&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;3,1,1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,1,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這真的非常新！對比你 C 碟的 5323 小時，這顆 D 碟基本上才剛開始運作不久。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;SSD 總寫入量：137 GB&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;3,2,1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;3,2,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這個寫入量極低。以 SN570 1TB 版本高達 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;3,2,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;600 TBW&lt;/b&gt; 的耐用度來看，你目前只用了 &lt;span class=&quot;math-inline&quot; data-index-in-node=&quot;47&quot; data-math=&quot;0.02\%&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;$0.02\%$&lt;/span&gt; 的壽命。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;4&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;2. 性能與規格分析&lt;/h3&gt;&lt;ul data-path-to-node=&quot;5&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;5,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;傳輸模式：PCIe 3.0 x4&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,0,1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;5,0,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這是正確的。SN570 本身就是 PCIe 3.0 的規格，所以它跑在 3.0 x4 是發揮全速。雖然你的筆電插槽可能支援到 4.0（如 C 碟那顆），但這顆 D 碟會穩定運行在 3.0 的最高速。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;5,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;溫度：33°C&lt;/b&gt;。&lt;/p&gt;&lt;ul data-path-to-node=&quot;5,1,1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;5,1,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;比 C 碟還要低，這是一個非常涼爽的操作溫度，對電子零件的長壽非常有幫助。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;6&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot; /&gt;&lt;h3 data-path-to-node=&quot;7&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;3. 為什麼這顆硬碟特別適合當「資料存放區」？&lt;/h3&gt;&lt;p data-path-to-node=&quot;8&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;SN570 是屬於「無快取 (DRAM-less)」但性能平衡的 SSD。這類硬碟的特性是：&lt;/p&gt;&lt;ul data-path-to-node=&quot;9&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;9,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;省電：&lt;/b&gt; 對於 LG Gram 這種強調續航的筆電非常友善。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;9,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;發熱低：&lt;/b&gt; 很適合放在筆電的第二個擴充槽。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;9,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;穩定：&lt;/b&gt; 非常適合作為存放專案代碼、文件或照片的副碟。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;10&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;4. 給 Tony 的維護建議&lt;/h3&gt;&lt;p data-path-to-node=&quot;11&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;這顆硬碟目前完全不需要任何維護。但既然它是 D 碟，我建議你可以利用它來&lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;11&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;分擔 C 碟的壓力&lt;/b&gt;：&lt;/p&gt;&lt;ul data-path-to-node=&quot;12&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;12,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;存放開發專案：&lt;/b&gt; 既然你的工作涉及 Python 和金融資料分析（kbar 庫等），可以把這些專案放在 D 碟。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;12,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;虛擬機或 Docker：&lt;/b&gt; 如果有使用這些工具，將它們的映像檔放在 D 碟，可以避免 C 碟過快消耗壽命，且這顆 SN570 的速度對於這類應用綽綽有餘。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;13&quot; style=&quot;font-family: &amp;quot;Google Sans&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;總結&lt;/h3&gt;&lt;p data-path-to-node=&quot;14&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;你這台 LG Gram 的硬碟配置非常健康：&lt;/p&gt;&lt;ol data-path-to-node=&quot;15&quot; start=&quot;1&quot; style=&quot;font-family: &amp;quot;Google Sans Text&amp;quot;, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important; padding-inline-start: 32px;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;15,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;C 碟 (Samsung)：&lt;/b&gt; 強大的系統碟，負責作業系統與軟體運行。&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;15,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;D 碟 (WD Blue)：&lt;/b&gt; 極新的擴充碟，適合存放長期資料與專案。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/7859785069258808687/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/7859785069258808687' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7859785069258808687'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/7859785069258808687'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/hddssd-crystaldiskinfo.html' title='HDD/SSD 健康狀態檢測軟體 CrystalDiskInfo'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiU6uYl5lm_UP5oir8rvvFBgmQv41XJFpMYd8RQ-5keh0TGu65GPmrafJSDMoIc7-mY3jLoOQy6x7-rmss8xbAQmO7cCeGebaXv5hMSiQjQTw4CqPKGaLz9LNQrswuuLTWvdNd7DbxXPZQKHKlRN_0swibeXbtFHCf1WxNrHVfgzEXT3wrDkAfjDGqpTRV9/s72-c/LG_GRAM_SSD_C%E7%A2%9F%E6%AA%A2%E6%B8%AC%E7%B5%90%E6%9E%9C.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-2958842096396053002</id><published>2026-04-20T10:57:00.000+08:00</published><updated>2026-04-20T10:57:05.075+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="電腦"/><title type='text'>WD 5TB 隨身硬碟無法讀取問題</title><content type='html'>&lt;div&gt;週六下午在上緯育的 SSD (OpenSec) 線上課程時, 插在 USB 的 WD 5TB 隨身硬碟突然擱登兩聲離線, 我以為是碰到插頭接觸不良而已. 週日晚上要複製檔案時接上 USB, 有讀到驅動 G 碟, 但無法讀取檔案目錄結構 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3uC5yfCLyoR8aPBSyKZX8wnk7ifN6_aMk2YD0iFesLCtRdH49nk9fRqxuzRtBhcafPl7ajWQ_lMkacA2eqjjoSv-KW_8DX2pD3Ebqf_qaraYjQZqC-125ZmNd2T9LU7ixxf4aNfzH5TgwewrbyGfv1PuIJXIe_fzrQalHYnGxFCw9sfF2eCzzjgYsSW-k/s612/WD-5TB-%E7%84%A1%E6%B3%95%E8%AE%80%E5%8F%96-1.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;313&quot; data-original-width=&quot;612&quot; height=&quot;164&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3uC5yfCLyoR8aPBSyKZX8wnk7ifN6_aMk2YD0iFesLCtRdH49nk9fRqxuzRtBhcafPl7ajWQ_lMkacA2eqjjoSv-KW_8DX2pD3Ebqf_qaraYjQZqC-125ZmNd2T9LU7ixxf4aNfzH5TgwewrbyGfv1PuIJXIe_fzrQalHYnGxFCw9sfF2eCzzjgYsSW-k/s320/WD-5TB-%E7%84%A1%E6%B3%95%E8%AE%80%E5%8F%96-1.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;週日晚上回到高雄後, 拿另外一條 USB 線連接筆電還是一樣, 於是問 Gemini 該怎麼辦, 它建議我先以管理員身分開啟命令提示字元視窗, 然後用 chdisk 檢查修復檔案目錄連結 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;C:\Users\tony1&amp;gt;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;chkdsk G: /f&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;檔案系統的類型是 NTFS。&lt;/div&gt;&lt;div&gt;磁碟區標籤為 Elements。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;階段 1: 正在檢查基本檔案系統結構...&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 249088 個檔案記錄。&lt;/div&gt;&lt;div&gt;檔案驗證完成。&lt;/div&gt;&lt;div&gt;階段期間 (檔案記錄驗證): 22.14 秒。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 47 個大型檔案記錄。&lt;/div&gt;&lt;div&gt;階段期間 (孤立檔案記錄修復): 0.52 毫秒。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 0 個損毀的檔案記錄。&lt;/div&gt;&lt;div&gt;階段期間 (不正確檔案記錄檢查): 0.98 毫秒。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;階段 2: 正在檢查檔案名稱連結...&lt;/div&gt;&lt;div&gt;正在修正檔案 5 中索引 $I30 的錯誤。&lt;/div&gt;&lt;div&gt;CHKDSK 在檔案 5 之索引 $I30 的點陣圖中發現可用空間被標示為已配置。&lt;/div&gt;&lt;div&gt;正在為檔案 5 中的索引 $I30 進行排序。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 2386 個重新分析記錄。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 276974 個索引項目。&lt;/div&gt;&lt;div&gt;索引驗證完成。&lt;/div&gt;&lt;div&gt;階段期間 (索引驗證): 3.86 分鐘。&lt;/div&gt;&lt;div&gt;CHKDSK 正在掃描未編製索引的檔案以重新連線到其原始目錄。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $MFT (0) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $MFTMirr (1) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $LogFile (2) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $Volume (3) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $AttrDef (4) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 . (5) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $Bitmap (6) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $Boot (7) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $BadClus (8) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在將孤立的檔案 $Secure (9) 復原成目錄檔案 5。&lt;/div&gt;&lt;div&gt;正在略過關於復原孤立項的後續訊息。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經掃描 51 個未編製索引的檔案。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 51 個未編製索引的檔案已復原到原始目錄。&lt;/div&gt;&lt;div&gt;階段期間 (孤立重新連接): 2.44 秒。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 0 個未編製索引的檔案已復原到 [Lost and Found]。&lt;/div&gt;&lt;div&gt;階段期間 (孤兒復原丟失與發現): 36.93 毫秒。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 2386 個重新分析記錄。&lt;/div&gt;&lt;div&gt;階段期間 (重新剖析點與物件識別碼驗證): 14.32 毫秒。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;階段 3: 正在檢查安全性描述元...&lt;/div&gt;&lt;div&gt;安全性描述元驗證完成。&lt;/div&gt;&lt;div&gt;階段期間 (安全性描述項驗證): 50.83 毫秒。&lt;/div&gt;&lt;div&gt;&amp;nbsp; 已經處理 13943 個資料檔案。&lt;/div&gt;&lt;div&gt;階段期間 (資料屬性驗證): 1.80 毫秒。&lt;/div&gt;&lt;div&gt;正在修正主檔案表格 (MFT) 鏡像的錯誤。&lt;/div&gt;&lt;div&gt;正在修正磁碟區點陣圖的錯誤。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Windows 已修正檔案系統。&lt;/div&gt;&lt;div&gt;不需要進一步的動作。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;磁碟空間總計&amp;nbsp; &amp;nbsp; 4769272 MB。&lt;/div&gt;&lt;div&gt;234806 個檔案共&amp;nbsp; &amp;nbsp; 4371444 MB。&lt;/div&gt;&lt;div&gt;13945 個索引共&amp;nbsp; &amp;nbsp; &amp;nbsp; 71928 KB。&lt;/div&gt;&lt;div&gt;損壞的磁區共&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 0 KB。&lt;/div&gt;&lt;div&gt;系統使用空間&amp;nbsp; &amp;nbsp; &amp;nbsp;464123 KB。&lt;/div&gt;&lt;div&gt;記錄檔共佔用&amp;nbsp; &amp;nbsp; &amp;nbsp; 65536 KB。&lt;/div&gt;&lt;div&gt;磁碟可用空間&amp;nbsp; 406840116 KB。&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;每個配置單位有&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;4096 個位元組。&lt;/div&gt;&lt;div&gt;磁碟上共有 1220933887 個配置單位。&lt;/div&gt;&lt;div&gt;磁碟上有&amp;nbsp; 101710029 個可用的配置單位。&lt;/div&gt;&lt;div&gt;總持續時間: 4.28 分鐘 (256882 毫秒)。&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;----------------------------------------&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;我把以上結果貼給 Gemini 評估, 結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;-----------------------------------------------------------&lt;/div&gt;&lt;div&gt;這代表你的 5TB 硬碟硬體暫時看起來是健康的，這次純粹是軟體層面的「索引混亂」。&lt;/div&gt;&lt;div&gt;&lt;p data-path-to-node=&quot;1&quot;&gt;從你的 Log 來看，這次修復了幾個關鍵問題：&lt;/p&gt;&lt;h3 data-path-to-node=&quot;2&quot;&gt;1. 修復了什麼？&lt;/h3&gt;&lt;ul data-path-to-node=&quot;3&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot;&gt;索引點陣圖 (Bitmap) 錯誤&lt;/b&gt;：這就像是圖書館的目錄說某個位置是空的，但實際上那裡放了書。&lt;code data-index-in-node=&quot;47&quot; data-path-to-node=&quot;3,0,0&quot;&gt;chkdsk&lt;/code&gt; 重新校正了這些空間分配。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;孤立檔案 (Orphaned Files) 復原&lt;/b&gt;：這部分最關鍵。你的 &lt;code data-index-in-node=&quot;35&quot; data-path-to-node=&quot;3,1,0&quot;&gt;$MFT&lt;/code&gt;（主檔案表格）等系統檔案一度失去了連結，導致 Windows 看不到目錄。&lt;code data-index-in-node=&quot;76&quot; data-path-to-node=&quot;3,1,0&quot;&gt;chkdsk&lt;/code&gt; 把這些「走丟」的系統檔案重新接回了目錄樹（檔案 5）。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,2,0&quot;&gt;MFT 鏡像修正&lt;/b&gt;：主檔案表格的備份也被同步修正了，確保下次讀取穩定。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;4&quot; /&gt;&lt;h3 data-path-to-node=&quot;5&quot;&gt;2. 後續建議 (SOP)&lt;/h3&gt;&lt;p data-path-to-node=&quot;6&quot;&gt;雖然現在讀得到了，但身為開發者，我們都知道「事出必有因」。為了確保這顆 5TB 隨身硬碟之後不再搞事，建議你做以下幾件事：&lt;/p&gt;&lt;ul data-path-to-node=&quot;7&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;7,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;立刻檢查 S.M.A.R.T. 資訊：&lt;/b&gt;
雖然 &lt;code data-index-in-node=&quot;23&quot; data-path-to-node=&quot;7,0,0&quot;&gt;chkdsk&lt;/code&gt; 顯示 0 壞軌，但那是「邏輯層面」。建議用 &lt;b data-index-in-node=&quot;52&quot; data-path-to-node=&quot;7,0,0&quot;&gt;CrystalDiskInfo&lt;/b&gt; 看一下 &lt;code data-index-in-node=&quot;72&quot; data-path-to-node=&quot;7,0,0&quot;&gt;05 (重新分配磁區數)&lt;/code&gt; 和 &lt;code data-index-in-node=&quot;87&quot; data-path-to-node=&quot;7,0,0&quot;&gt;C5 (等候重映射的磁區數)&lt;/code&gt; 是否為 0。如果這兩個數值不是 0，代表硬碟物理壽命快到頭了，趕快趁現在備份。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;7,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;養成「安全移除」的習慣：&lt;/b&gt;
這次的錯誤（索引與點陣圖不符）最常發生在&lt;b data-index-in-node=&quot;33&quot; data-path-to-node=&quot;7,1,0&quot;&gt;資料寫入中途拔掉 USB&lt;/b&gt;，或是 &lt;b data-index-in-node=&quot;49&quot; data-path-to-node=&quot;7,1,0&quot;&gt;USB 供電不穩&lt;/b&gt;（例如插在前端 USB 孔或 Hub 上）。下次拔除前，務必點選右下角的「安全移除硬體」。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;7,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;停用「快速啟動」或檢查寫入快取：&lt;/b&gt;
如果你常用這顆硬碟，可以在「裝置管理員」&amp;gt;「磁碟機」&amp;gt;「WD Elements」內容中，確認「原則」設定。如果追求安全，可以選擇「快速移除」（停用寫入快取）。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;8&quot; /&gt;&lt;h3 data-path-to-node=&quot;9&quot;&gt;3. 關於資料安全&lt;/h3&gt;&lt;p data-path-to-node=&quot;10&quot;&gt;這顆硬碟已經 4.3TB 滿載（剩下約 400GB），這對 5TB 硬碟來說負擔不小，且檔案系統毀損過一次後，穩定性會略微下降。&lt;/p&gt;&lt;p data-path-to-node=&quot;11&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11&quot;&gt;建議下一步：&lt;/b&gt;
既然現在恢復了，趁這幾天趕快把裡面&lt;b data-index-in-node=&quot;24&quot; data-path-to-node=&quot;11&quot;&gt;最不可取代&lt;/b&gt;的資料（例如你的 e-book 草稿、kbar 原始碼等）同步到雲端或另一顆實體硬碟上。&lt;/p&gt;&lt;/div&gt;&lt;div&gt;-----------------------------------------------------&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;做完 chdisk 果然就恢復正常了, 還好能修復連結, 否則裡面去年以來的上課資料毀壞那就虧大了. 接下來我按照其建議去下載安裝&amp;nbsp;CrystalDiskInfo 來檢測硬碟狀況, 此軟體是日本人開發的, 體積很小直接到微軟 Store 下載安裝 (網路上的免安裝版我用 VirusTotal 掃描幾乎都有夾帶木馬) :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://apps.microsoft.com/detail/xp8k4rgx25g3gm?hl=zh-TW&amp;amp;gl=TW&quot; target=&quot;_blank&quot;&gt;https://apps.microsoft.com/detail/xp8k4rgx25g3gm?hl=zh-TW&amp;amp;gl=TW&lt;/a&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;檢測結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgry1FAnVrkrYwnpbo4FOot8B6A2NzhGszF6JcF_KflJiQ7T7xo_78QBJvX8v-a1JHEbez4NBiI-Vv1DYthIz2CY27NtwH984aWRUdnD2Xs_P1w3qqVCbB30P3DMqc4WF4Zj9Azyu24NWuDpymfOzHp8xrdyl-HTG1kbCQf3iiIZ1i7TJLYEEzbOjxdoEhs/s1050/WD-5TB-%E7%84%A1%E6%B3%95%E8%AE%80%E5%8F%96-2.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1050&quot; data-original-width=&quot;1012&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgry1FAnVrkrYwnpbo4FOot8B6A2NzhGszF6JcF_KflJiQ7T7xo_78QBJvX8v-a1JHEbez4NBiI-Vv1DYthIz2CY27NtwH984aWRUdnD2Xs_P1w3qqVCbB30P3DMqc4WF4Zj9Azyu24NWuDpymfOzHp8xrdyl-HTG1kbCQf3iiIZ1i7TJLYEEzbOjxdoEhs/s320/WD-5TB-%E7%84%A1%E6%B3%95%E8%AE%80%E5%8F%96-2.jpg&quot; width=&quot;308&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;貼給 Gemini 分析結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;p data-path-to-node=&quot;0&quot;&gt;這份健康報告非常漂亮，這顆 WD 5TB 隨身硬碟&lt;b data-index-in-node=&quot;46&quot; data-path-to-node=&quot;0&quot;&gt;目前的硬體狀態非常健康&lt;/b&gt;。&lt;/p&gt;&lt;p data-path-to-node=&quot;1&quot;&gt;以下是針對這張截圖的幾個核心分析：&lt;/p&gt;&lt;h3 data-path-to-node=&quot;2&quot;&gt;1. 關鍵健康指標 (核心數據)&lt;/h3&gt;&lt;ul data-path-to-node=&quot;3&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,0,0&quot;&gt;健康狀態：良好&lt;/b&gt;。這是最直觀的結果。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,1,0&quot;&gt;05 (重定位磁區計數)：&lt;/b&gt; 原始值為 &lt;code data-index-in-node=&quot;19&quot; data-path-to-node=&quot;3,1,0&quot;&gt;0&lt;/code&gt;。這代表硬碟沒有任何物理壞軌被替換掉，碟盤表面狀況良好。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,2,0&quot;&gt;C5 (等候重定的磁區計數)：&lt;/b&gt; 原始值為 &lt;code data-index-in-node=&quot;21&quot; data-path-to-node=&quot;3,2,0&quot;&gt;0&lt;/code&gt;。這代表沒有任何疑似壞軌在排隊。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;3,3,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;3,3,0&quot;&gt;C6 (無法校正的磁區計數)：&lt;/b&gt; 原始值為 &lt;code data-index-in-node=&quot;21&quot; data-path-to-node=&quot;3,3,0&quot;&gt;0&lt;/code&gt;。再次確認磁區資料讀寫穩定。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-path-to-node=&quot;4&quot;&gt;2. 使用記錄&lt;/h3&gt;&lt;ul data-path-to-node=&quot;5&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;已用時間總計：3931 小時&lt;/b&gt;。這顆硬碟大約已經服役了 5 個多月（如果是 24 小時不關機計算），對於一顆隨身硬碟來說，正處於「壯年期」。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;已用次數總計：578 次&lt;/b&gt;。平均每次開機使用約 6.8 小時。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;5,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;轉速：4800 RPM&lt;/b&gt;。這是典型的 2.5 吋節能/高容量硬碟規格，發熱量會比較低，目前的 &lt;b data-index-in-node=&quot;46&quot; data-path-to-node=&quot;5,2,0&quot;&gt;37°C&lt;/b&gt; 非常理想。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;hr data-path-to-node=&quot;6&quot; /&gt;&lt;h3 data-path-to-node=&quot;7&quot;&gt;3. 為什麼會發生「檔案目錄毀損」？&lt;/h3&gt;&lt;p data-path-to-node=&quot;8&quot;&gt;既然硬體沒問題，那為什麼會噴錯誤？最可能的兇手有三個：&lt;/p&gt;&lt;ol data-path-to-node=&quot;9&quot; start=&quot;1&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;異常拔除：&lt;/b&gt; 在 Windows 還在寫入快取資料時，就直接拔掉 USB 線，導致 NTFS 的 &lt;b data-index-in-node=&quot;48&quot; data-path-to-node=&quot;9,0,0&quot;&gt;MFT (Master File Table)&lt;/b&gt; 沒寫完。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;供電瞬間不穩：&lt;/b&gt; 5TB 的碟盤密度很高，對電流波動敏感。如果插在不穩的 USB 插座，讀寫中途掉電也會導致邏輯錯誤。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;9,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,2,0&quot;&gt;磁碟已滿的副作用：&lt;/b&gt; 你的硬碟已經用了 4.3TB 以上，當空間極度剩餘不足時，NTFS 的碎片整理或元數據更新會變得很頻繁，這時候若發生中斷，毀損機率會變高。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;h3 data-path-to-node=&quot;10&quot;&gt;4. 維護建議&lt;/h3&gt;&lt;ul data-path-to-node=&quot;11&quot;&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;11,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;繼續使用：&lt;/b&gt; 沒問題，它不是快掛了，只是「軟體感冒」剛修好。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;11,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;安全退出：&lt;/b&gt; 之後請務必點選 Windows 右下角的「安全移除硬體」。&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p data-path-to-node=&quot;11,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;避免前端插槽：&lt;/b&gt; 建議優先插在電腦機殼後方的 USB 孔（或供電足夠的電競筆電插孔），穩定電壓是 5TB 硬碟的長壽關鍵。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;------------------------------------------------&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這顆硬碟就回來了真是好佳在啊! 趕緊開啟 MIS 桌機, 把內容複製到 18TB 大硬碟裡備份. 這顆也快裝滿了, 也準備要換新的一顆來用了.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/2958842096396053002/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/2958842096396053002' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2958842096396053002'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2958842096396053002'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/wd-5tb.html' title='WD 5TB 隨身硬碟無法讀取問題'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3uC5yfCLyoR8aPBSyKZX8wnk7ifN6_aMk2YD0iFesLCtRdH49nk9fRqxuzRtBhcafPl7ajWQ_lMkacA2eqjjoSv-KW_8DX2pD3Ebqf_qaraYjQZqC-125ZmNd2T9LU7ixxf4aNfzH5TgwewrbyGfv1PuIJXIe_fzrQalHYnGxFCw9sfF2eCzzjgYsSW-k/s72-c/WD-5TB-%E7%84%A1%E6%B3%95%E8%AE%80%E5%8F%96-1.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1874631718687413114</id><published>2026-04-19T17:44:00.001+08:00</published><updated>2026-04-19T17:50:22.101+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="生活"/><title type='text'>2026 年第 15 周記事</title><content type='html'>&lt;div&gt;又過了一周了, 四月下旬天氣變得熱起來, 今年的芒果樹結果不多, 菜園的只有兩棵樹有結果, 另外路邊的只有零星結一些, 我今天下午拿去年剩餘的袋子去套袋, 只完成菜園的那兩棵樹, 路邊的等下周再套. 套袋時在儲水池旁的芒果樹上發現一個鳥巢, 裡面有孵化的雛鳥兩隻 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJtZiwoU24VZ2emCMvplF2W0FzxdMlgq_DKE-eEfYPpNLDSURSpt3l7DLmrRpJ9D_jsQiv0bf5gA-PBAnSMnH_rNapc_S_OE9vAtbBkreE2ZfKm5C-xi_IEKhtKXMajV8SQXsEUi4GO_GmRstoxqqItp-w_PbObm_LfYXUMZFn73RNn8d8vYCeVBBgh-E/s1477/%E8%8A%92%E6%9E%9C%E6%A8%B9%E4%B8%8A%E7%9A%84%E9%B3%A5%E5%B7%A2.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1108&quot; data-original-width=&quot;1477&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJtZiwoU24VZ2emCMvplF2W0FzxdMlgq_DKE-eEfYPpNLDSURSpt3l7DLmrRpJ9D_jsQiv0bf5gA-PBAnSMnH_rNapc_S_OE9vAtbBkreE2ZfKm5C-xi_IEKhtKXMajV8SQXsEUi4GO_GmRstoxqqItp-w_PbObm_LfYXUMZFn73RNn8d8vYCeVBBgh-E/s320/%E8%8A%92%E6%9E%9C%E6%A8%B9%E4%B8%8A%E7%9A%84%E9%B3%A5%E5%B7%A2.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;我傳給 Gemini 辨認, 說是白頭翁的雛鳥 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6XzKWU7N1iacz2KdhzNsixVB5OO4HIqQHAVoXv_UHF4NLn9kPEK1O3en2Oc6hHhyMNKuZNemr4soY-Z3zsqSquNKkJgddqw0pivlp9WHzXe6QL2lHi-vTfEQXStQuyQOOUUaAL-ugB_Ex7rkzEToKlTZCYi7Q9Os9CiL65o6qoZ1nJ-8yLHKx91ixWcWH/s912/%E7%99%BD%E9%A0%AD%E7%BF%81.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;683&quot; data-original-width=&quot;912&quot; height=&quot;240&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6XzKWU7N1iacz2KdhzNsixVB5OO4HIqQHAVoXv_UHF4NLn9kPEK1O3en2Oc6hHhyMNKuZNemr4soY-Z3zsqSquNKkJgddqw0pivlp9WHzXe6QL2lHi-vTfEQXStQuyQOOUUaAL-ugB_Ex7rkzEToKlTZCYi7Q9Os9CiL65o6qoZ1nJ-8yLHKx91ixWcWH/s320/%E7%99%BD%E9%A0%AD%E7%BF%81.jpg&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;這幾天回頭整理我的 5 台 Linux 主機, 其中三台樹莓派是我的主要爬蟲主機, 前陣子不明原因當機, 直到前幾天才有時間重灌, 順便把爬蟲程式升版. 接下來要做的有下列三件 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Serverless 平台加入 API Key&amp;nbsp;&lt;/li&gt;&lt;li&gt;高雄光世代固定 IP &amp;amp; 全屋通路由器設定&lt;/li&gt;&lt;li&gt;Mapleboard 外接 USB 儲存設定&lt;/li&gt;&lt;li&gt;鄉下的 Pi 3 主機升版為 Bulleye&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;鄉下的 Pi 3 主機目前跑的是 Stretch, 這根本是老古董了, 今天已備份 SD 卡內資料, 準備買一張 32 GB 卡來升版為 Bulleye.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1874631718687413114/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1874631718687413114' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1874631718687413114'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1874631718687413114'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/2026-15.html' title='2026 年第 15 周記事'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjcJtZiwoU24VZ2emCMvplF2W0FzxdMlgq_DKE-eEfYPpNLDSURSpt3l7DLmrRpJ9D_jsQiv0bf5gA-PBAnSMnH_rNapc_S_OE9vAtbBkreE2ZfKm5C-xi_IEKhtKXMajV8SQXsEUi4GO_GmRstoxqqItp-w_PbObm_LfYXUMZFn73RNn8d8vYCeVBBgh-E/s72-c/%E8%8A%92%E6%9E%9C%E6%A8%B9%E4%B8%8A%E7%9A%84%E9%B3%A5%E5%B7%A2.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-1290185826844348559</id><published>2026-04-18T23:32:00.007+08:00</published><updated>2026-04-19T07:53:57.995+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="網路爬蟲"/><title type='text'>Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v13</title><content type='html'>&lt;div&gt;將母校圖書館爬蟲升版後打鐵趁熱, 今天順便將此次優化 Selenium 爬蟲的技巧也套用在市圖爬蟲程式上, 可同時於 Pi 400, Pi 3B, 與 Pi 3A+ 上執行.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列全部測試文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/03/blog-post_10.html&quot; target=&quot;_blank&quot;&gt;Python 網頁爬蟲筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;新版 v13 程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# ksml_lib_13.py&lt;/div&gt;&lt;div&gt;from selenium import webdriver&lt;/div&gt;&lt;div&gt;from selenium.webdriver.common.by import By&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.options import Options&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.service import Service&lt;/div&gt;&lt;div&gt;import re&lt;/div&gt;&lt;div&gt;from datetime import datetime&lt;/div&gt;&lt;div&gt;import time&lt;/div&gt;&lt;div&gt;import requests&lt;/div&gt;&lt;div&gt;import sys&lt;/div&gt;&lt;div&gt;from dotenv import dotenv_values&lt;/div&gt;&lt;div&gt;import os&lt;/div&gt;&lt;div&gt;import socket&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;async def telegram_send_text(text):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; bot=Bot(token=TELEGRAM_TOKEN)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await bot.send_message(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chat_id=TELEGRAM_ID,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text=text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return True&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&#39;Error sending text: {e}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return False&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def get_books(account, password):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; browser=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;result=(None, None)&lt;/span&gt;&amp;nbsp; # 預設回傳值&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 登入我的書房&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 設定一個在 SD 卡上的暫存目錄 (for Trixie)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chrome_tmp_path=os.path.expanduser(&#39;~/chrome_tmp&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not os.path.exists(chrome_tmp_path):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; os.makedirs(chrome_tmp_path)&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options=Options()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--headless=new&quot;) # 新版無頭擬真瀏覽器&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--no-sandbox&quot;) # Trixie 必加&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--disable-dev-shm-usage&quot;) # 避免擠爆 /dev/shm&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&#39;--disable-gpu&#39;) # 避免 GPU 驅動崩潰&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 強迫使用 SD 卡空間 (特別是 Trixie 必須)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(f&#39;--user-data-dir={chrome_tmp_path}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&#39;--disk-cache-size=104857600&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.binary_location=&#39;/usr/bin/chromium&#39;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; service=Service(&#39;/usr/bin/chromedriver&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser=webdriver.Chrome(service=service, options=options)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.implicitly_wait(60)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.set_window_size(1920, 1080)&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 載入網頁&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.get(&#39;https://webpacx.ksml.edu.tw/personal/&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; loginid=browser.find_element(By.ID, &#39;logxinid&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; loginid.send_keys(account)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; pincode=browser.find_element(By.ID, &#39;pincode&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; pincode.send_keys(password)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; div_btn_grp=browser.find_element(By.CLASS_NAME, &#39;btn_grp&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn=div_btn_grp.find_element(By.TAG_NAME, &#39;input&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn.click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 擷取借閱紀錄&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; div_redblock=browser.find_element(By.CLASS_NAME, &#39;redblock&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; div_redblock.click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; books=browser.find_elements(By.CLASS_NAME, &#39;bookdata&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow_books=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for book in books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item=dict()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_name=book.find_element(By.XPATH, &#39;./h2/a&#39;).text&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;book_name&#39;]=book_name.replace(&#39;/&#39;, &#39;&#39;).strip()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_site=book.find_element(By.XPATH, &#39;./ul[3]/li[1]&#39;).text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reg=r&#39;典藏地：(\S+)&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;book_site&#39;]=re.findall(reg, book_site)[0]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reg=r&#39;\d{4}-\d{2}-\d{2}&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_date=book.find_element(By.XPATH, &#39;./ul[4]/li[2]&#39;).text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;due_date&#39;]=re.findall(reg, due_date)[0]&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_times=book.find_element(By.XPATH, &#39;./ul[5]/li[1]&#39;).text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;due_times&#39;]=re.findall(r&#39;\d{1}&#39;, due_times)[0]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try:&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; state=book.find_element(By.XPATH, &#39;./ul[6]/li[1]&#39;).text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; except:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; state=&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; finally:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if &#39;有人預約&#39; in state:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;state&#39;]=&#39;, 有人預約&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;state&#39;]=&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow_books.append(item)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;擷取借閱紀錄 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.back() # 回上一頁&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 擷取預約紀錄&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; div_blueblock=browser.find_element(By.CLASS_NAME, &#39;blueblock&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; div_blueblock.click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; books=browser.find_elements(By.CLASS_NAME, &#39;bookdata&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reserve_books=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for book in books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item=dict()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_name=book.find_element(By.XPATH, &#39;./h2/a&#39;).text&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;book_name&#39;]=book_name.replace(&#39;/&#39;, &#39;&#39;).strip()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sequence=book.find_element(By.XPATH, &#39;./ul[7]/li[1]&#39;).text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if &#39;預約待取&#39; in sequence:&amp;nbsp; # 已到館&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;ready_for_pickup&#39;]=True&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reg=r&#39;\d{4}-\d{2}-\d{2}&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;expiration&#39;]=re.findall(reg, sequence)[0]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;sequence&#39;]=&#39;0&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else: # 預約中&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;ready_for_pickup&#39;]=False&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;expiration&#39;]=&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item[&#39;sequence&#39;]=re.findall(r&#39;\d+&#39;, sequence)[0]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reserve_books.append(item)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;擷取預約紀錄 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;result=(borrow_books, reserve_books)&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&#39;發生錯誤 : {e}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; finally:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if browser:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;browser.quit()&lt;/span&gt;&amp;nbsp; # 釋放記憶體&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;資源已釋放&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; except:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; pass&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;return result&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;if __name__ == &#39;__main__&#39;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; start=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=dotenv_values(&#39;.env&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; TELEGRAM_TOKEN=config.get(&#39;TELEGRAM_TOKEN&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; TELEGRAM_ID=config.get(&#39;TELEGRAM_ID&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_TOKEN)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_ID)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; host_name=socket.gethostname()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(f&#39;主機 : {host_name}&#39;)&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if len(sys.argv) != 3:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&#39;用法: {sys.argv[0]} 帳號 密碼&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sys.exit(1)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 取得傳入的帳密參數&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; account=sys.argv[1]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; password=sys.argv[2]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 呼叫 get_books() 取得借書與預約書&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; borrow_books, reserve_books=get_books(account, password)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; b_msg=&#39;&#39;&amp;nbsp; # 借書資訊字串初始值&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; r_msg=&#39;&#39;&amp;nbsp; # 預約資訊字串初始值&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 處理借書&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if borrow_books:&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for book in borrow_books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_name=book[&#39;book_name&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_site=book[&#39;book_site&#39;]&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_times=book[&#39;due_times&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_date=book[&#39;due_date&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; state=book[&#39;state&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_date=datetime.strptime(due_date, &#39;%Y-%m-%d&#39;) # 到期日&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; today_str=datetime.today().strftime(&#39;%Y-%m-%d&#39;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; today=datetime.strptime(today_str, &quot;%Y-%m-%d&quot;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; delta=(due_date-today).days&amp;nbsp; # 計算離到期日還有幾天&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if delta &amp;lt; 0:&amp;nbsp; # 負數=已逾期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;🅧 {book_name} (逾期 {abs(delta)} 天{state}, {book_site})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; elif delta == 0:&amp;nbsp; # 0=今天到期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;⓿ {book_name} (今日到期, 續借次數 {due_times}{state}, {book_site})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; elif delta == 1:&amp;nbsp; # 1=明天到期&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;❶ {book_name} (明日到期, 續借次數 {due_times}{state}, {book_site})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; elif delta == 2:&amp;nbsp; # 2=後天到期&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;❷ {book_name} (後天到期, 續借次數 {due_times}{state}, {book_site})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; elif 2 &amp;lt; delta &amp;lt; 8:&amp;nbsp; # 3 天以上一周內到期&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;✦ {book_name} ({book[&quot;due_date&quot;]} 到期, &#39;\&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; f&#39;續借次數 {due_times}{state}, {book_site})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 製作借書到期摘要字串&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if len(borrow) != 0:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; borrow.insert(0, f&#39;\n❖ {account} 的借閱 :&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; b_msg=&#39;\n&#39;.join(borrow)&amp;nbsp; # 更新借書資訊字串&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;產生借書到期摘要 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 處理預約書&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if reserve_books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reserve=[]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; i=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; j=[&#39;①&#39;, &#39;②&#39;, &#39;③&#39;, &#39;④&#39;, &#39;⑤&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; k=[&#39;❶&#39;, &#39;❷&#39;, &#39;❸&#39;, &#39;❹&#39;, &#39;❺&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 預約狀態&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for book in reserve_books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; book_name=book[&#39;book_name&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; sequence=book[&#39;sequence&#39;]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ready_for_pickup=book[&#39;ready_for_pickup&#39;] # 已到館&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; expiration=book[&#39;expiration&#39;]&amp;nbsp; # 取書截止日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if ready_for_pickup:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;{k[i]} {book_name} (已到館, 保留期限 {expiration})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=f&#39;{j[i]} {book_name} (順位 {sequence})&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reserve.append(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; i += 1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 製作預約書摘要字串&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if len(reserve) != 0:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; reserve.insert(0, f&#39;\n❖ {account} 的預約 :&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; r_msg=&#39;\n&#39;.join(reserve)&amp;nbsp; # 更新資訊字串&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(&#39;產生預約書摘要 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if b_msg or r_msg:&amp;nbsp; # 任一不為空字串就更新資料表&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; url=&quot;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;https://serverless-5e6i.onrender.com/function/update_ksml_books&lt;/span&gt;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; payload={&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;account&quot;: account,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;borrow_books&quot;: b_msg,&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;reserve_books&quot;: r_msg&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; res=requests.post(url, json=payload)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(res.json())&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; end=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(f&#39;執行時間:{end-start}&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;此次改版也修正了 try-except-finally 結構, 把 browser.close() 改成 browser.quit(), 前者雖然會把視窗關掉, 但背景的 chromedriver 可能還在跑繼續吃 RAM,&amp;nbsp;導致背景殘留了一堆 chromedriver 的殭屍進程. 此優化版程式在 Pi 3B, 3A+ 與 Pi 400 均可順利執行.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;執行結果如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;pi@kaopi3:~ $ &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;python ksml_lib_12.py faxxxxxx 123456&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;擷取借閱紀錄 ... OK&lt;/div&gt;&lt;div&gt;擷取預約紀錄 ... OK&lt;/div&gt;&lt;div&gt;產生借書到期摘要 ... OK&lt;/div&gt;&lt;div&gt;產生預約書摘要 ... OK&lt;/div&gt;&lt;div&gt;{&#39;message&#39;: &#39;faxxxxxx 的資料已更新&#39;, &#39;status&#39;: &#39;success&#39;}&lt;/div&gt;&lt;div&gt;執行時間:667.9564106464386&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;pi@pi3aplus:~ $&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;python ksml_lib_12.py faxxxxxx 123456&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;主機 : pi3aplus&lt;/div&gt;&lt;div&gt;擷取借閱紀錄 ... OK&lt;/div&gt;&lt;div&gt;擷取預約紀錄 ... OK&lt;/div&gt;&lt;div&gt;資源已釋放&lt;/div&gt;&lt;div&gt;產生借書到期摘要 ... OK&lt;/div&gt;&lt;div&gt;產生預約書摘要 ... OK&lt;/div&gt;&lt;div&gt;{&#39;message&#39;: &#39;faxxxxxx 的資料已更新&#39;, &#39;status&#39;: &#39;success&#39;}&lt;/div&gt;&lt;div&gt;執行時間:675.6979095935822&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;(myenv313) pi@pi400:~ $&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;python ksml_lib_12.py faxxxxxx 123456&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;主機 : pi400&lt;/div&gt;&lt;div&gt;擷取借閱紀錄 ... OK&lt;/div&gt;&lt;div&gt;擷取預約紀錄 ... OK&lt;/div&gt;&lt;div&gt;資源已釋放&lt;/div&gt;&lt;div&gt;產生借書到期摘要 ... OK&lt;/div&gt;&lt;div&gt;產生預約書摘要 ... OK&lt;/div&gt;&lt;div&gt;{&#39;message&#39;: &#39;faxxxxxx 的資料已更新&#39;, &#39;status&#39;: &#39;success&#39;}&lt;/div&gt;&lt;div&gt;執行時間:573.2406423091888&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;可見 Pi 3B 與 3A+ 速度差不多, 但 Pi 400 就快了 100 秒.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;注意, 此處呼叫的後端端點有兩個, 都是建置在 Render 平台上的 serverless 服務, 目前我有兩個端點, 分配如下 :&lt;/div&gt;&lt;div&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;kaopi3 : https://serverless-5e6i.onrender.com/function/send_books_messages&lt;/li&gt;&lt;li&gt;pi3aplus :&amp;nbsp;https://serverless-fdof.onrender.com/function/send_books_messages&lt;/li&gt;&lt;li&gt;pi400 :&amp;nbsp;https://serverless-fdof.onrender.com/function/send_books_messages (備用)&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;擷取並傳送借書資訊的程式 get_ksml_books_messages.py 也是要設定對應端點, 例如 kaopi3 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;# get_ksml_books_messages.py&lt;/div&gt;&lt;div&gt;import requests&lt;/div&gt;&lt;div&gt;import socket&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;host_name=socket.gethostname()&lt;/div&gt;&lt;div&gt;print(f&#39;主機 : {host_name}&#39;)&lt;/div&gt;&lt;div&gt;params={&#39;crawler&#39;: f&#39;{host_name}&#39;}&lt;/div&gt;&lt;div&gt;url=&#39;https://serverless-5e6i.onrender.com/function/send_books_messages&#39;&lt;/div&gt;&lt;div&gt;res=requests.get(url, params=params)&lt;/div&gt;&lt;div&gt;print(res)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;此爬蟲程式會呼叫 serverless 平台上的 send_books_messages.py 函式讀取 serverless.db 上記錄的借書與預約資訊, 並送出 Telegram 訊息. send_books_messages.py 函式內容如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# send_ksml_books_messages.py&lt;/div&gt;&lt;div&gt;import asyncio&lt;/div&gt;&lt;div&gt;import sqlite3&lt;/div&gt;&lt;div&gt;from telegram import Bot&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;async def telegram_send_text(token, chat_id, text):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &quot;&quot;&quot;非同步傳送 Telegram 訊息&quot;&quot;&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; bot=Bot(token=token)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await bot.send_message(chat_id=chat_id, text=text)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return True&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&quot;傳送失敗: {e}&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return False&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def main(request, **kwargs):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; DB_PATH=&#39;./serverless.db&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=kwargs.get(&#39;config&#39;, {})&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; telegram_token=config.get(&#39;TELEGRAM_TOKEN&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; telegram_id=config.get(&#39;TELEGRAM_ID&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if not telegram_token or not telegram_id:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &#39;未設定 TELEGRAM_TOKEN 或 TELEGRAM_ID&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&amp;nbsp; # 連線資料庫&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conn=sqlite3.connect(DB_PATH)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cur=conn.cursor()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cur.execute(&quot;SELECT borrow_books, reserve_books FROM ksml_books;&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rows=cur.fetchall()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conn.close()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return f&#39;資料庫讀取失敗: {e}&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if not rows:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &#39;沒有任何資料可傳送&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; # 傳送訊息&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; success_count=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; fail_count=0&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; for borrow_books, reserve_books in rows:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for msg in [borrow_books, reserve_books]:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if msg and msg.strip():&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ok=asyncio.run(telegram_send_text(telegram_token, telegram_id, msg))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if ok:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; success_count += 1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fail_count += 1&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; return f&#39;傳送完成：成功 {success_count} 筆，失敗 {fail_count} 筆&#39;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;市圖爬蟲架構較複雜, 得畫一張圖來備忘才行.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/1290185826844348559/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/1290185826844348559' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1290185826844348559'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/1290185826844348559'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/python-v13.html' title='Python 學習筆記 : 市圖借書與預約爬蟲程式改版 v13'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4877487320781767952.post-2141063547542946216</id><published>2026-04-18T00:51:00.006+08:00</published><updated>2026-04-18T11:32:20.106+08:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="樹莓派"/><category scheme="http://www.blogger.com/atom/ns#" term="網路爬蟲"/><title type='text'>Python 學習筆記 : 母校圖書館借書與預約爬蟲程式改版 v11</title><content type='html'>&lt;div&gt;之前版本的爬蟲我都把帳號密碼金鑰等等都寫在程式碼裡, 這實在不是好的做法, 今天將爬蟲進行改版, 將帳密金鑰都存放在隱藏檔 .env 中, 於程式裡利用 dotenv 從 .env 檔讀取出來. 關於 dotenv 用法參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2024/01/api.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : 如何隱藏 API 金鑰或權杖&lt;/a&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/05/python-dotenv.html&quot; target=&quot;_blank&quot;&gt;Python 學習筆記 : dotenv 取值的另類用法&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;本系列全部文章索引參考 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;#&amp;nbsp;&lt;a href=&quot;https://yhhuang1966.blogspot.com/2025/03/blog-post_10.html&quot; target=&quot;_blank&quot;&gt;Python 網頁爬蟲筆記索引&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;先將目前的第 10 版爬蟲複製一份到第 11 版 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;pi@kaopi3:~ $ &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;cp nkust_lib_10.py nkust_lib_11.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;用 nano 編輯程式碼 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;pi@kaopi3:~ $ &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;nano nkust_lib_11.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;修改為如下 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# nkust_lib_11.py&lt;/div&gt;&lt;div&gt;from selenium import webdriver&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;from selenium.webdriver.common.by import By&lt;/div&gt;&lt;div&gt;from selenium.webdriver.common.action_chains import ActionChains&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.options import Options&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.service import Service&lt;/div&gt;&lt;div&gt;import time&lt;/div&gt;&lt;div&gt;import requests&lt;/div&gt;&lt;div&gt;from datetime import datetime&lt;/div&gt;&lt;div&gt;import asyncio&lt;/div&gt;&lt;div&gt;from telegram import Bot&lt;/div&gt;&lt;div&gt;import re&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;from dotenv import dotenv_values&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;async def telegram_send_text(text):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; bot=Bot(token=&lt;span style=&quot;background-color: #fcff01;&quot;&gt;TELEGRAM_TOKEN&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await bot.send_message(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chat_id=&lt;span style=&quot;background-color: #fcff01;&quot;&gt;TELEGRAM_ID&lt;/span&gt;,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text=text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return True&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&#39;Error sending text: {e}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return False&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def get_nkust_lib():&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; browser=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=&quot;無法取得資料&quot;&amp;nbsp; # ✅ 防止未賦值&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options=Options()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--headless=new&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--no-sandbox&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--disable-dev-shm-usage&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.binary_location=&quot;/usr/bin/chromium&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; service=Service(&quot;/usr/bin/chromedriver&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser=webdriver.Chrome(service=service, options=options)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.implicitly_wait(60)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.set_window_size(1920, 1080)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; url=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;https://nkust.primo.exlibrisgroup.com/discovery/login?&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;vid=886NKUST_INST:86NKUST&amp;amp;lang=zh-tw&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.get(url)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按其他讀者&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; md_list=browser.find_elements(By.TAG_NAME, &#39;md-list-item&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if len(md_list) &amp;gt; 1:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; md_list[1].click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按其他讀者 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 登入系統&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_user_name=browser.find_element(By.ID, &#39;LoginUserName&#39;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_user_name.send_keys(&lt;span style=&quot;background-color: #fcff01;&quot;&gt;NKUST_LIB_ID&lt;/span&gt;)&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_password=browser.find_element(By.ID, &#39;LoginPassword&#39;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_password.send_keys(&lt;span style=&quot;background-color: #fcff01;&quot;&gt;NKUST_LIB_PWD&lt;/span&gt;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn=browser.find_element(By.CLASS_NAME, &#39;button-large&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn.click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;登入系統 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按名字顯現選單&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; user_btn=browser.find_element(By.CLASS_NAME, &#39;user-button&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions=ActionChains(browser)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(user_btn)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(user_btn)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按名字顯現選單 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按我的借閱鈕&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=&#39;/html/body/div[3]/md-menu-content/md-menu-item[3]/button&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; my_borrow=browser.find_element(By.XPATH, xpath)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(my_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(my_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按我的借閱 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按全部續借&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/html/body/primo-explore/div/prm-account/md-content&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/div[2]/prm-account-overview/md-content/md-tabs/&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;md-tabs-content-wrapper/md-tab-content[2]/div/&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;div/prm-loans/div[1]/div[2]/div[2]/button&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; all_borrow=browser.find_element(By.XPATH, xpath)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(all_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(all_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按全部續借 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 檢查續借結果&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/html/body/primo-explore/div/prm-account/md-content&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/div[2]/prm-account-overview/md-content/md-tabs&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/md-tabs-content-wrapper/md-tab-content[2]/div/div&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/prm-loans/div[2]/prm-alert-bar/div/div/span&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; alert_span=browser.find_element(By.XPATH, xpath)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if &#39;所有借閱資料已成功續借&#39; in alert_span.text:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;❖ 所有借閱資料已成功續借&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;❖ 只有部分借閱資料已成功續借&#39;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 檢查是否有 &quot;載入更多結果&quot; 按鈕&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i in range(3):&amp;nbsp; # 最多 3 頁&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; load_more=browser.find_elements(By.CLASS_NAME, &#39;button-confirm&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not load_more:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; break&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; load_more[0].click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; time.sleep(2)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 抓取所有借閱書目&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; loan_items=browser.find_elements(By.TAG_NAME, &#39;md-list-item&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; unrenew_books=[]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for item in loan_items:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 1. 必須有書名才是書&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title_elem=item.find_elements(By.CSS_SELECTOR, &quot;h3 a&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not title_elem:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; continue&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=title_elem[0].text.strip()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item_text=item.text.strip()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 2. 取得到期日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_text=&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_elem=item.find_elements(By.XPATH, &#39;.//p[@data-qa=&quot;automation_mlc_record_date&quot;]&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if due_elem:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; m=re.search(r&#39;\d{2}/\d{2}/\d{4}&#39;, due_elem[0].text)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if m:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_text=m.group(0)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 3. 只收真正「被預約 / 無法續借」&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if any(k in item_text for k in [&quot;被預約&quot;, &quot;無法續借&quot;, &quot;recall&quot;]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; unrenew_books.append((title, due_text))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; except Exception:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; continue&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if unrenew_books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&quot;被預約的書：\n&quot; + &quot;\n&quot;.join(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [f&quot;{i+1:&amp;gt;2}. {t[0]}　到期日: {t[1]}&quot; for i, t in enumerate(unrenew_books)]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&quot;全部書籍皆已續借&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;搜尋被預約書籍 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(e)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; finally:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if browser:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.quit()&amp;nbsp; # ✅ 防呆&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return msg&amp;nbsp; # ✅ msg 一定有值&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &#39;__main__&#39;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; start=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;config=dotenv_values(&#39;.env&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;NKUST_LIB_ID=config.get(&#39;NKUST_LIB_ID&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;NKUST_LIB_PWD=config.get(&#39;NKUST_LIB_PWD&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;TELEGRAM_TOKEN=config.get(&#39;TELEGRAM_TOKEN&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;TELEGRAM_ID=config.get(&#39;TELEGRAM_ID&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(NKUST_LIB_ID)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(NKUST_LIB_PWD)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_TOKEN)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_ID)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=get_nkust_lib()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if msg:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; now=datetime.now().strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;\n&#39; + now + &#39;\n&#39; + msg&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if asyncio.run(telegram_send_text(msg)):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;訊息傳送成功!&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;訊息傳送失敗!&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; end=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(f&#39;執行時間:{end-start}&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;黃底高亮者為修改或添加的部分, 執行結果如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;pi@kaopi3:~ $ &lt;b&gt;&lt;span style=&quot;color: #2b00fe;&quot;&gt;python nkust_lib_11.py&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;按其他讀者 ... OK&lt;/div&gt;&lt;div&gt;登入系統 ... OK&lt;/div&gt;&lt;div&gt;按名字顯現選單 ... OK&lt;/div&gt;&lt;div&gt;按我的借閱 ... OK&lt;/div&gt;&lt;div&gt;按全部續借 ... OK&lt;/div&gt;&lt;div&gt;訊息傳送成功!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2026-04-18 00:37:52&lt;/div&gt;&lt;div&gt;❖ 所有借閱資料已成功續借&lt;/div&gt;&lt;div&gt;執行時間:81.74577593803406&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2026-04-18 補充 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;早上又對程式碼進行局部微調, 版本號仍維持 v11, 主要修改如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;1. 添加主機名稱訊息 :&amp;nbsp;&lt;br /&gt;利用&amp;nbsp;socket.gethostname()&amp;nbsp;即可得到主機名稱. 這樣收到 Telegram 才知道是哪台主機的爬蟲完成的.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2. 使用 SD 卡儲存 Chrome/Chromium 暫存資料 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;由於 Pi 400 的 Trixie 仍然開發演進中, 最新版本改用 Chrome 而非 Chromium, 跑 Selenium 爬蟲時使用 DRAM 來暫存資料, 可能導致 DRAM 塞滿而使瀏覽器閃退, 於是改為強制將暫存資料放在 chrome_tmp 資料夾下, 瀏覽器設定修改為 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 設定一個在 SD 卡上的暫存目錄 (for Trixie)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;chrome_tmp_path=os.path.expanduser(&#39;~/chrome_tmp&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;if not os.path.exists(chrome_tmp_path):&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;os.makedirs(chrome_tmp_path)&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options=Options()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--headless=new&quot;) # 新版無頭擬真瀏覽器&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--no-sandbox&quot;) # Trixie 必加&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--disable-dev-shm-usage&quot;) # 避免擠爆 /dev/shm&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;options.add_argument(&#39;--disable-gpu&#39;)&lt;/span&gt; # 避免 GPU 驅動崩潰&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 強迫使用 SD 卡空間 (特別是 Trixie 必須)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;options.add_argument(f&#39;--user-data-dir={chrome_tmp_path}&#39;)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #fcff01;&quot;&gt;options.add_argument(&#39;--disk-cache-size=104857600&#39;)&lt;/span&gt;&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;以上設定雖然是針對 Trixie, 但在 Bulleye (Pi 3 &amp;amp; Pi 3A+) 上跑也是可以的. 完整程式碼如下 :&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;# nkust_lib_11.py&lt;/div&gt;&lt;div&gt;from selenium import webdriver&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;from selenium.webdriver.common.by import By&lt;/div&gt;&lt;div&gt;from selenium.webdriver.common.action_chains import ActionChains&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.options import Options&lt;/div&gt;&lt;div&gt;from selenium.webdriver.chrome.service import Service&lt;/div&gt;&lt;div&gt;import time&lt;/div&gt;&lt;div&gt;import requests&lt;/div&gt;&lt;div&gt;from datetime import datetime&lt;/div&gt;&lt;div&gt;import asyncio&lt;/div&gt;&lt;div&gt;from telegram import Bot&lt;/div&gt;&lt;div&gt;import re&lt;/div&gt;&lt;div&gt;from dotenv import dotenv_values&lt;/div&gt;&lt;div&gt;import os&lt;/div&gt;&lt;div&gt;import socket&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;async def telegram_send_text(text):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; bot=Bot(token=TELEGRAM_TOKEN)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; await bot.send_message(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chat_id=TELEGRAM_ID,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; text=text&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return True&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(f&#39;Error sending text: {e}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return False&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;def get_nkust_lib():&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; browser=None&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=&quot;無法取得資料&quot;&amp;nbsp; # ✅ 防止未賦值&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 設定一個在 SD 卡上的暫存目錄 (for Trixie)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; chrome_tmp_path=os.path.expanduser(&#39;~/chrome_tmp&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not os.path.exists(chrome_tmp_path):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; os.makedirs(chrome_tmp_path)&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options=Options()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--headless=new&quot;) # 新版無頭擬真瀏覽器&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--no-sandbox&quot;) # Trixie 必加&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&quot;--disable-dev-shm-usage&quot;) # 避免擠爆 /dev/shm&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&#39;--disable-gpu&#39;) # 避免 GPU 驅動崩潰&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 強迫使用 SD 卡空間 (特別是 Trixie 必須)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(f&#39;--user-data-dir={chrome_tmp_path}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 限制快取大小為 100MB (防止 chrome_tmp 資料夾隨著時間變得巨大)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.add_argument(&#39;--disk-cache-size=104857600&#39;)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; options.binary_location=&quot;/usr/bin/chromium&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; service=Service(&quot;/usr/bin/chromedriver&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser=webdriver.Chrome(service=service, options=options)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.implicitly_wait(60)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.set_window_size(1920, 1080)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; url=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;https://nkust.primo.exlibrisgroup.com/discovery/login?&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;vid=886NKUST_INST:86NKUST&amp;amp;lang=zh-tw&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.get(url)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按其他讀者&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; md_list=browser.find_elements(By.TAG_NAME, &#39;md-list-item&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if len(md_list) &amp;gt; 1:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; md_list[1].click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按其他讀者 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 登入系統&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_user_name=browser.find_element(By.ID, &#39;LoginUserName&#39;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_user_name.send_keys(NKUST_LIB_ID)&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_password=browser.find_element(By.ID, &#39;LoginPassword&#39;)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_password.send_keys(NKUST_LIB_PWD)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn=browser.find_element(By.CLASS_NAME, &#39;button-large&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; login_btn.click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;登入系統 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按名字顯現選單&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; user_btn=browser.find_element(By.CLASS_NAME, &#39;user-button&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions=ActionChains(browser)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(user_btn)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(user_btn)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按名字顯現選單 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按我的借閱鈕&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=&#39;/html/body/div[3]/md-menu-content/md-menu-item[3]/button&#39;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; my_borrow=browser.find_element(By.XPATH, xpath)&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(my_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(my_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按我的借閱 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 按全部續借&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/html/body/primo-explore/div/prm-account/md-content&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/div[2]/prm-account-overview/md-content/md-tabs/&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;md-tabs-content-wrapper/md-tab-content[2]/div/&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;div/prm-loans/div[1]/div[2]/div[2]/button&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; all_borrow=browser.find_element(By.XPATH, xpath)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.move_to_element(all_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.click(all_borrow)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; actions.perform()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;按全部續借 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 檢查續借結果&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xpath=(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/html/body/primo-explore/div/prm-account/md-content&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/div[2]/prm-account-overview/md-content/md-tabs&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/md-tabs-content-wrapper/md-tab-content[2]/div/div&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &#39;/prm-loans/div[2]/prm-alert-bar/div/div/span&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; alert_span=browser.find_element(By.XPATH, xpath)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if &#39;所有借閱資料已成功續借&#39; in alert_span.text:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;❖ 所有借閱資料已成功續借&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;❖ 只有部分借閱資料已成功續借&#39;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 檢查是否有 &quot;載入更多結果&quot; 按鈕&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for i in range(3):&amp;nbsp; # 最多 3 頁&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; load_more=browser.find_elements(By.CLASS_NAME, &#39;button-confirm&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not load_more:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; break&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; load_more[0].click()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; time.sleep(2)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # 抓取所有借閱書目&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; loan_items=browser.find_elements(By.TAG_NAME, &#39;md-list-item&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; unrenew_books=[]&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; for item in loan_items:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 1. 必須有書名才是書&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title_elem=item.find_elements(By.CSS_SELECTOR, &quot;h3 a&quot;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if not title_elem:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; continue&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; title=title_elem[0].text.strip()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; item_text=item.text.strip()&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 2. 取得到期日&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_text=&#39;&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_elem=item.find_elements(By.XPATH, &#39;.//p[@data-qa=&quot;automation_mlc_record_date&quot;]&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if due_elem:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; m=re.search(r&#39;\d{2}/\d{2}/\d{4}&#39;, due_elem[0].text)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if m:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; due_text=m.group(0)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # ✅ 3. 只收真正「被預約 / 無法續借」&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if any(k in item_text for k in [&quot;被預約&quot;, &quot;無法續借&quot;, &quot;recall&quot;]):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; unrenew_books.append((title, due_text))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; except Exception:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; continue&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if unrenew_books:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&quot;被預約的書：\n&quot; + &quot;\n&quot;.join(&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [f&quot;{i+1:&amp;gt;2}. {t[0]}　到期日: {t[1]}&quot; for i, t in enumerate(unrenew_books)]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&quot;全部書籍皆已續借&quot;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;搜尋被預約書籍 ... OK&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; except Exception as e:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(e)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; finally:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if browser:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; browser.quit()&amp;nbsp; # ✅ 防呆&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return msg&amp;nbsp; # ✅ msg 一定有值&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if __name__ == &#39;__main__&#39;:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; start=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; config=dotenv_values(&#39;.env&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; NKUST_LIB_ID=config.get(&#39;NKUST_LIB_ID&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; NKUST_LIB_PWD=config.get(&#39;NKUST_LIB_PWD&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; TELEGRAM_TOKEN=config.get(&#39;TELEGRAM_TOKEN&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; TELEGRAM_ID=config.get(&#39;TELEGRAM_ID&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(NKUST_LIB_ID)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(NKUST_LIB_PWD)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_TOKEN)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; #print(TELEGRAM_ID)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; host_name=socket.gethostname()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(f&#39;主機 : {host_name}&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; msg=get_nkust_lib()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; if msg:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; now=datetime.now().strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; msg=&#39;\n&#39; + now + &#39;\n&#39; + msg + &#39;\n&#39; + f&#39;(host_name)&#39;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if asyncio.run(telegram_send_text(msg)):&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;訊息傳送成功!&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; else:&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; print(&#39;訊息傳送失敗!&#39;)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(msg)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; end=time.time()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; print(f&#39;執行時間:{end-start}&#39;)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;執行結果 :&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;pi@kaopi3:~ $ python nkust_lib_11.py&lt;/div&gt;&lt;div&gt;主機 : kaopi3&lt;/div&gt;&lt;div&gt;按其他讀者 ... OK&lt;/div&gt;&lt;div&gt;登入系統 ... OK&lt;/div&gt;&lt;div&gt;按名字顯現選單 ... OK&lt;/div&gt;&lt;div&gt;按我的借閱 ... OK&lt;/div&gt;&lt;div&gt;按全部續借 ... OK&lt;/div&gt;&lt;div&gt;搜尋被預約書籍 ... OK&lt;/div&gt;&lt;div&gt;訊息傳送成功!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;2026-04-18 11:14:19&lt;/div&gt;&lt;div&gt;全部書籍皆已續借&lt;/div&gt;&lt;div&gt;(kaopi3)&lt;/div&gt;&lt;div&gt;執行時間:358.6611738204956&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='https://yhhuang1966.blogspot.com/feeds/2141063547542946216/comments/default' title='張貼留言'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment/fullpage/post/4877487320781767952/2141063547542946216' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2141063547542946216'/><link rel='self' type='application/atom+xml' href='https://www.blogger.com/feeds/4877487320781767952/posts/default/2141063547542946216'/><link rel='alternate' type='text/html' href='https://yhhuang1966.blogspot.com/2026/04/python-v11.html' title='Python 學習筆記 : 母校圖書館借書與預約爬蟲程式改版 v11'/><author><name>小狐狸事務所</name><uri>http://www.blogger.com/profile/09435160519044041137</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>