2024年3月29日 星期五

好書 : 動手學深度學習 (PyTorch 版)

今天在找資料時發現了左岸的一個網站 : 


仔細看原來是 Cambridge 出版的 "Dive into Deep Learning (2nd ed.)" 這本書的簡體版, 作者群為 Amazon 人工智慧專家, 第一版是用 Amazon 的 MXNet 框架實做的, 第二版改用 PyTorch (網站電子版還提供 JAX/TensorFlow 程式碼), 英文版紙本書天瓏書店有賣 : 



Source : 天瓏

簡體中文版博客來有, 但似乎已賣完 : 



Source : 博客來


此書看起來是電子版 Open Source, 因為全書內容都公布在網站上了, 英文版網址如下 :


書中範例程式碼也都可以在 GitHub 找到 :


我看了其中第 11 章的 Transformer 說明, 唉呦, 夭壽, 居然用 PyTorch 實作把 Transformer 架構講解得如此詳細 (例如 Q, K, V 的原理, 計算, 與圖解), 這就是我一直在尋找的 NLP 聖經啊! 我看完這一段的感想是 : 

在注意力經濟時代, 注意力可不是免費的, 當你集中注意力在某件事物 (例如學習 NLP 或打遊戲) 上時, 你支付的是機會成本 (追劇或交女友); 在使用免費資源 (例如 YT 影片) 上, 如果你不想被廣告分散注意力, 那就得掏錢. 對於大腦的算力而言, 注意力是稀缺的資源, 必須將有限的資源放在重要的資訊上, 視覺與語音的理解特別需要注意力, 否則大腦根本無法負荷龐大的影音資訊流, 所以 Attention is all you need 所言不虛啊! 

2024年3月28日 星期四

momo 購買微星電競桌機

上週看到旗標 "AI 繪圖夢工廠" 這本書裡面提到在 Colab 安裝 Stable Diffusion 用 TPU/GPU 生圖大概只能畫 1 張就到達門檻, 加上最近上了許多 RAG 課程很想來試試, 就去建國路電腦街看有顯卡的桌機, 茂訊店長推薦了 ASUS RTX3050 8GVRAM 的電競桌機, 價格 28900, 加裝 32GB DRAM 合計有 48GB DRAM 總價 30900, 現金價可退 2%. 

我回家爬文研究了一下, 發現跑 AI 顯卡 VRAM 要大, 8GB 似乎少了點, 最好是 12GB 以上. 於是上 momo 搜尋, 找到下面這款微星的電競機 : 





這款滿足我的要求 : RTX3060 12GB , 已安裝 Win11 (隨機版), 1TB SSD, 只是 DRAM 只有 16GB, 所以我加購 2089 元的 32 GB 一排, 合起來是 48GB :




另外加購 199 元的耳麥組, 總價 23828 + 2089 + 199 = 26116, 扣掉 momo 幣 549 元實付 25567 元 :



 
上周日桃金日優惠價格 24436 元還好沒倉促買, 本周直降 24436-23828=608. 這台規格如果去原價屋估價, 雖然也是 26000 左右, 但差在 momo 多買了 32GB DRAM 與耳麥 :




雖然這只是 AI 入門款規格的桌機, 但還是要多比較. 參考 :


花旗信用卡到期換發星展 eco 卡

前幾天收到星展暨來信用卡, 以為是 Travel 卡到期換新, 今日得空拆開才發現是綠色的 eco 卡, 奇怪. 我沒申請啊? 打客服才知是原花旗信用卡快到期, 因花期消金併入星展, 所以改發星展 eco 卡, 卡號不變, 一樣年刷 12 次免年費, 國內外 2% 現金紅利, 開卡刷一筆送星巴克 110 元星享飲料券. 此卡附加悠遊卡功能, 所以可以留著. 

阿蘭對參年

今天早上請假回鄉下的觀音廟為吾妹阿蘭做對參年, 昨晚到高鐵接二哥回一起回鄉下, 因菁菁班已排定, 姐姐去韓國, 水某早上有病人回診, 所以今天只有我跟二哥去拜拜 (還好二哥有南下, 不然只有我一個人也太寒酸了). 

對參年祭品與作百日一樣 :

"祭品兩份 (葷牲禮或素水果), 紅粄與發粄 (各兩包), 花束一對, 蠟燭一對, 九金銀紙 (各一條), 壽金 (一仟), 香 (適量)"

我今早搜尋到去年做百日的記事才發現祭品要兩份, 但仔細看是葷素皆可, 所以早上去退伍軍人拿粄時買了素三牲, 外加全聯買的祭拜用水果籃, 素的供王官 (做百日時也是這樣, 否則兩份牲禮冰箱肯定要塞爆)

三牲 (魚罐頭), 酒, 茶, 泡麵, 餅乾, 水果, 紅粄與發粄各兩包, 王官的素三牲, 對參年金包, 壽金, 九金銀紙, 防風蠟燭, 紙杯.

但到鎮上時才發現三家花店都沒開, 原來他們只在周末掃墓的人多才營業. 燒完紙錢回頭看到路邊有三台機車從上面觀音廟騎下來, 接著師姐也下來了, 我問她廟裡有工程進行嗎? 我剛剛看到三個人從廟裡下來, 師姐說不是啦, 那是要偷香油錢的小偷, 難怪剛才看到她突然騎機車回廟裡, 原來是認出慣竊光臨. 她說這是鎮上一個宮廟的人, 一家子都是賊, 專騙信徒奉獻土地, 兼職偷遍鎮內其他宮廟. 哇! 還有這種奇葩家庭, 聞所未聞. 

2024年3月27日 星期三

小米 Poco M3 一直顯示 Fastboot 無法正常開機問題

上周末爸的小米 Poco M3 手機一直顯示 Fastboot 字樣無法開機, 今晚回到鄉下本想上 momo 幫爸買支新手機, 但下單之前想說爬一下網路, 看看有無解決辦法, 還真給我找到了 :


在 "soulman" 的回覆中, 提供了三種讓手機重開機的辦法 :

方法一
1. 按住電源鍵 10 秒或更長
2.當標誌出現時,鬆開電源鍵

方法二
1. 按住音量上按鈕和電源鍵 10 秒鐘。
2.如果出現 Poco 標誌,鬆開兩個鍵並等待進入恢復模式。

方法三
1.使用交流電充電,將手機充電於延長線或是牆壁插座。
2.無論您的 Poco M3 是否顯示充電標誌,請充電至少 5 分鐘。
3.在手機連接充電器狀態下,按住電源鍵 10 秒鐘或更長時間。
4.如果出現畫面,鬆開電源鍵,它應該會成功啟動。

第一個方法不行, 第二個我一試即正常重開機, 耶! 

小米這種 3000~4000 元價位的便宜手機品質不佳, 這支 Poco M3 買來第一年也是出現無法開機現象, 送小米售後維修, 說是機板故障換新. 

2024年3月26日 星期二

Python 學習筆記 : 在樹莓派上安裝 Selenium 與 Web Driver

由於在 Win11 上安裝 Selenium + Web Driver 時遇到版本匹配問題, 所以我轉而到要佈署爬蟲的樹莓派 Pi 3 上直接開發. 本系列之前的文章參考 :


由於樹莓派內建的是 Chromium 瀏覽器, Chrome 的 Web Driver 應該是沒辦法直接用在樹莓派, 所幸找到下面這篇好文章 :


那就來依樣畫葫蘆實作看看唄. 


一. 下載安裝 chromium-chromedriver 驅動程式 : 

Chromium 的驅動程式可從下列網址下載 :


可用 wget 下載驅動程式的 deb 檔 : 

pi@raspberrypi:~ $ wget http://launchpadlibrarian.net/361669488/chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb   
--2024-03-26 14:26:31--  http://launchpadlibrarian.net/361669488/chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb
正在查找主機 launchpadlibrarian.net (launchpadlibrarian.net)... 185.125.189.229, 185.125.189.228, 2620:2d:4000:1009::13e, ...
正在連接 launchpadlibrarian.net (launchpadlibrarian.net)|185.125.189.229|:80... 連上了。
已送出 HTTP 要求,正在等候回應... 200 OK
長度: 2787410 (2.7M) [application/x-debian-package]
Saving to: ‘chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb’

chromium-chromedriver_65. 100%[====================================>]   2.66M   643KB/s    in 4.2s    

2024-03-26 14:26:36 (643 KB/s) - ‘chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb’ saved [2787410/2787410]

再用 sudo dpkg 安裝驅動程式 : 

pi@raspberrypi:~ $ sudo dpkg -i chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb 
選取了原先未選的套件 chromium-chromedriver。
(讀取資料庫 ... 目前共安裝了 142836 個檔案和目錄。)
Preparing to unpack chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb ...
Unpacking chromium-chromedriver (65.0.3325.181-0ubuntu0.14.04.1) ...
設定 chromium-chromedriver (65.0.3325.181-0ubuntu0.14.04.1) ...


二. 建立 WebDriver 物件 : 

先進入 Python 執行環境 : 

pi@raspberrypi:~ $ python3 
Python 3.5.3 (default, Sep 27 2018, 17:25:39) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.

從 selenium 套件匯入 webdriver 模組 : 

>>> from selenium import webdriver    
>>> webdriver.__version__    
'3.14.1'   

呼叫 webdriver 的 Chrome 類別建構式 Chrome() 以建立 WebDriver 物件 : 

>>> browser=webdriver.Chrome('/usr/lib/chromium-browser/chromedriver')   
>>> type(browser)     
<class 'selenium.webdriver.chrome.webdriver.WebDriver'>

用 dir() 檢視 WebDriver 物件之內容 : 

>>> dir(browser)   
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_file_detector', '_is_remote', '_mobile', '_switch_to', '_unwrap_value', '_web_element_cls', '_wrap_value', 'add_cookie', 'application_cache', 'back', 'capabilities', 'close', 'command_executor', 'create_options', 'create_web_element', 'current_url', 'current_window_handle', 'delete_all_cookies', 'delete_cookie', 'desired_capabilities', 'error_handler', 'execute', 'execute_async_script', 'execute_cdp_cmd', 'execute_script', 'file_detector', 'file_detector_context', 'find_element', 'find_element_by_class_name', 'find_element_by_css_selector', 'find_element_by_id', 'find_element_by_link_text', 'find_element_by_name', 'find_element_by_partial_link_text', 'find_element_by_tag_name', 'find_element_by_xpath', 'find_elements', 'find_elements_by_class_name', 'find_elements_by_css_selector', 'find_elements_by_id', 'find_elements_by_link_text', 'find_elements_by_name', 'find_elements_by_partial_link_text', 'find_elements_by_tag_name', 'find_elements_by_xpath', 'forward', 'fullscreen_window', 'get', 'get_cookie', 'get_cookies', 'get_log', 'get_network_conditions', 'get_screenshot_as_base64', 'get_screenshot_as_file', 'get_screenshot_as_png', 'get_window_position', 'get_window_rect', 'get_window_size', 'implicitly_wait', 'launch_app', 'log_types', 'maximize_window', 'minimize_window', 'mobile', 'name', 'orientation', 'page_source', 'quit', 'refresh', 'save_screenshot', 'service', 'session_id', 'set_network_conditions', 'set_page_load_timeout', 'set_script_timeout', 'set_window_position', 'set_window_rect', 'set_window_size', 'start_client', 'start_session', 'stop_client', 'switch_to', 'switch_to_active_element', 'switch_to_alert', 'switch_to_default_content', 'switch_to_frame', 'switch_to_window', 'title', 'w3c', 'window_handles']

其中的 get() 方法用來開啟網頁, 例如 : 

>>> browser.get('https://tw.yahoo.com')   



Python 學習筆記 : Thonny 安裝 Selenium 與 Web Driver 的版本匹配問題

最近想要利用 Selenium 來寫一個爬蟲來擷取市圖網站的借還書與預約狀態, 自動以 Line Notify 通知我最近一周須要跑圖書館的日期. 我在 2018 年時曾測試過 Slenium, 參考 : 



一. 安裝 Selenium 與 Web Driver : 

之前都是直接在 Windows 的 Python 執行環境下安裝 Selenium, 這幾年都改用 Thonny IDE, 所以今天是改在 Thonny 上安裝. 

首先點選 "工具" 選單的 "管理套件" : 




在上方搜尋框輸入 "selenium" 按右邊的搜尋鈕, 找到後按 "安裝" 鈕, 此處因我之前已安裝過, 所以是按 "更新" 鈕升版為 3.18.1 :





二. 安裝 Chrome Web Driver : 

連線 Chrome Web Driver 網站 : 





點選上面最新版的 Web Driver 連結 : 




解壓縮後會得到一個執行檔 chromedriver.exe (約 12MB), 必須將此驅動程式放在 Python 0 安裝位置的 Scripts 子目錄下才能被 Selenium 調用到. 點選 "工具" 選單的 "開啟 Thonny 程式資料夾" :




這會開啟一個檔案總管視窗, 顯示 Thonny 的 Python 執行環境是在 C:\使用者\AppData\Local\Program\Thonny 下 : 




點選 Tonny 底下的 Scripts 子目錄 :




然後將上面的 chromedriver.exe 複製到此資料夾下即可 : 




然後在 Thonny 互動環境下輸入下列程式碼 : 

>>> from selenium import webdriver   
>>> webdriver.__version__   
'4.18.1'

可見 Selenium 似乎已可正常使用. 但真正要開啟 Chrome 時卻出現錯誤 : 

>>> browser=webdriver.Chrome()   

"raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 114
Current browser version is 123.0.6312.59 with binary path C:\Program Files\Google\Chrome\Application\chrome.exe
Stacktrace:"

原來 Selenium 的 Web Driver 版本必須搭配 Chrome 瀏覽器版本, 在下載 Chrome 的 Web Driver 時, 下載連結底下有標明此最新版驅動程式只支援 Chrome 114 版 : 




我查詢自己 Chrome 版本為 123 版, 太新了難怪沒辦法用 : 




但難道 Chrome 要為此降版嗎? 真是傷腦筋. 

但其實我只是想先用 Windows 開發測試爬蟲程式, 最後是要佈署到樹莓派上, 既然 Windows 開發環境卡在版本匹配 (或許用 FireFox 可以), 那就不浪費時間先擱著, 直接到樹莓派上開發吧! 


2024-03-27 補充 :

晚上想說既然 Chrome 要降版很麻煩, 乾脆試試看 FireFox.


三. 安裝 FireFox Web Driver : 

FireFox 的 WebDriver 下載網址如下 :





點選 win32 版的 geckodriver-v0.34.0-win32.zip 下載 (雖然尾的 OS 是 Win 11 64 位元), 解壓後得到驅動程式 geckodriver.exe, 如上面 Chrome 的 Web Driver 一樣, 複製到 Thonny 的 Scripts 目錄下, 測試 OK :

>>> from selenium import webdriver   
>>> browser=webdriver.Firefox()   
>>> browser.get("http://tw.yahoo.com")   



2024年3月25日 星期一

Python 學習筆記 : 用 technews-tw 爬台灣科技新聞網站 (二)

由於前一篇內容太長了, 所以用 Line Notify 推播所爬取的科技新聞改記在此篇, 本系列前一篇文章參考 :  


Line Notify 的用法參考 :  



一. Line Notify 函式 : 

Line Notify 的語法很制式, 可以寫成下列函式來方便呼叫 : 

import requests

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

此函式使用第三方模組 requests 傳送 HTTP POST 方法給 Line Notify 伺服器.


二. 科技新聞網站爬蟲函式 :   

參考前一篇測試記錄, 將爬蟲程式寫成函式 : 


注意, 只要爬今日新聞即可, 因為經過測試, 發現爬近一周新聞訊息會過長, 會超過 Line Notify 的限制 (每則訊息不可以超過 1000 個字元) 而導致訊息傳送失敗.

今日新聞的爬蟲函式如下 : 

def get_today_news(tn):   # tn=科技新聞網站名稱, 例如 'ithome'
    msg=[]    # 儲存新聞訊息
    news=TechNews(tn)   # 建立物件
    try:   # 捕捉爬蟲套件無法載入頁面例外
        today_news=news.get_today_news()   # 取得今日新聞
        time.sleep(5)   # 等候爬蟲取得新聞
        if today_news['news_counts'] > 0:   # 有新聞才製作回傳訊息
            msg.append(f'@ {tn} ({technews[tn]})')   # 訊息標題
            news_contents=today_news['news_contents']   # 取得全部新聞內容
            the_date=''   # 記錄新聞日期
            for key in news_contents:     # 拜訪各新聞條目
                contents=news_contents[key]    # 取得新聞內容
                title=f'❖ {contents["title"]}'   # 新聞標題
                msg.append(title)    # 加入訊息串 list
                msg.append(f'▶ {contents["link"]}')    # 將連結加入訊息串 list
                the_date=contents["date"]   # 更新新聞日期
            msg.insert(0, f'\n今日科技新聞 ({the_date})')    # 標題含日期 (開頭須跳 1 行)    
    except Exception as e:
        print(e)
    return '\n'.join(msg)   # 以跳行串接訊息串 list 為字串後傳回

此函式針對傳入的新聞網站名稱取得其新聞內容, 然後用迴圈將新聞逐條放入串列中, 最後用跳行串接成一個字串傳回.


三. 發行 Line Notify 存取權杖 :   

要推送 Line Notify 訊息必須先取得其伺服器之存取權杖, 作法參考 : 


先用手機 Line 帳號登入 Line Notify 網站 : 


登入成功後按按右上角帳號名稱, 點選 "個人頁面" :




拉到頁面底部按 "發行權杖" 鈕 :



在彈出視窗中填寫訊息標題, 並點選要推送的群組 (也可以選一對一) 後按發行 : 




這時會顯示發行之權杖, 請先按左邊的 "複製" 將權杖貼到文字檔中保存 : 




四. 佈署爬蟲程式 :   

取得權杖後就可以將上面函式整合成完整的爬蟲程式如下 : 

# technews_1.py
import time
import requests
from technews import TechNews

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

def get_today_news(tn):
    msg=[]
    news=TechNews(tn)
    try:
        today_news=news.get_today_news()
        time.sleep(5)
        if today_news['news_counts'] > 0:
            msg.append(f'@ {tn} ({technews[tn]})')
            news_contents=today_news['news_contents']
            the_date=''
            for key in news_contents:    
                contents=news_contents[key]
                title=f'❖ {contents["title"]}'
                msg.append(title)
                msg.append(f'▶ {contents["link"]}')
                the_date=contents["date"]
            msg.insert(0, f'\n今日科技新聞 ({the_date})')        
    except Exception as e:
        print(e)
    return '\n'.join(msg)

if __name__ == '__main__':
    technews={'ithome': '電腦報',
              'business': '數位時代',
              'inside': '硬塞的'}
    msg=[]
    for tn in technews:    
        msg=get_today_news(tn) 
        print(msg)
        print(f'訊息長度={len(msg)} 字元')
        if len(msg):
            token='ud7PaDL45fz849A0e1f5oaMCbRIkxMXapQCt7PfNkzz'  # 此為範例權杖
            code=line_notify(msg, token)
            if code==200:
                print('Line 訊息發送成功!')
            else:
                print(f'Line 訊息發送失敗! (code={code})')
        else:
            print(f'{technews[tn]} ({tn}) 無資料')

由於將三個網站新聞一次送給 Line Notify 伺服器訊息長度會超過限制, 導致訊息傳送失敗 (code=400), 因此最好是每個網站傳送一則訊息, 不要全部擠在一起送, 關於 Line Notify 的回傳碼說明參考 : 


此程式將三個科技新聞網站名稱做成字典, 然後用迴圈一一去呼叫 get_today_news() 爬新聞, 將取得之各條新聞內容串成字串後傳回. 因為每則新聞日期都是同一天, 因此在迭代每則新聞時會持續記錄日期 (其實都是同一日期), 迭代完再將日期與推播標題插入 msg 串列的最前面. 

執行結果如下 : 




測試 OK 後把此 technews_1.py 程式上傳到高雄家裡的那台樹莓派 Pi 3, 把其中 print(msg) 註解掉後存檔, 但此 Python 程式檔預設並無可執行權限 : 

pi@raspberrypi:~ $ ls -ls technews_1.py   
4 -rw-r--r-- 1 pi pi 1786  3月 25 23:56 technews_1.py       

可用 chmod 指令更改權限為可執行 : 

pi@raspberrypi:~ $ sudo chmod +x /home/pi/technews_1.py   
pi@raspberrypi:~ $ ls -ls technews_1.py   
4 -rwxr-xr-x 1 pi pi 1786  3月 25 23:56 technews_1.py   

接著編輯 Crontab 添加自動執行此程式的時間 :

pi@raspberrypi:~ $ crontab -e    

加入一筆定時執行設定 :

0 9,17 * * * /usr/bin/python3 /home/pi/technews_1.py

這是設定每天 09:00 與 17:00 執行此程式一次 : 




編輯完按 Ctrl+O 存檔, 再按 Ctrl+X 關閉檔案即可. 可用 crontab -l 顯示 crontab 目前設定 :

pi@raspberrypi:~ $ crontab -l   
0 16 * * 1-5 /usr/bin/python3 /home/pi/twstock_dashboard_update.py
*/31 9-13 * * 1-5 /usr/bin/python3 /home/pi/yahoo_twstock_monitor_table.py
0 8,18 * * * /usr/bin/python3 /home/pi/btc_eth_prices_line_notify.py
0 9,17 * * * /usr/bin/python3 /home/pi/technews_1.py

參考 : 


2024年3月24日 星期日

2024 年第 12 周記事

本周學習仍聚焦在 Google Apps Script, 這個我在 3~4 年前起了頭卻一直塵封的學習領域前陣子因為謝老師的 NGO 社群發起共讀活動而重拾, 趁這機會一舉拿下 GAS 也算是了了一樁心事. 其實谷歌在免費資源上面算是很佛心的了. 接下兩周想處理掉爬蟲筆記, 雖然常常用Python 爬蟲蒐集資料, 但是沒寫完筆記總是不放心. 

週六峰大師嫁女兒, 我原本想開車去, 但因水某本周沒有回鄉下, 所以吃完喜酒還得回高雄, 所以決定坐捷運到橋頭搭主人家安排的接駁車. 到橋頭時還有 20 分鐘車才要開, 就跟水某逛了一下糖廠園區, 剛好遇到有人舉行露天婚禮, 非常熱鬧. 逛到冰品區買了兩碗冰來解熱, 吃完剛好車就要開了. 看地圖以為到燕巢很近, 沒想到也花了 20 分鐘. 同學來了一桌, 婚禮請來鋼管秀女郎, 哇, 很多年沒見到這陣仗了, 說是要讓澳門來的親家們體會台灣庶民文化. 我當然記得重要的橋段是, 女郎會下台來找賓客共同演出, 我早就伺機離位去拍照, 但水某則與兩位女郎拍了一張合照. 哈哈, 真是一個好玩又有趣的下午時光 

今天周日小舅家掃外公外婆的墓, 我備辦果品過去, 今年兩位阿姨都沒來, 左營的阿姨去德國看孫女, 外家舅只有我跟爸參加. 掃完我走到家祠那邊巡一下, 順便把花瓶收起來. 下周四要幫吾妹阿蘭做對三年, 所以下午把冰箱裡上周掃墓的雞豬肉都拿出來料理, 我下周午餐就要努力消耗掉麻油雞了 (已經吃了兩週了 ~~~),  

Google Apps Script (GAS) 學習筆記 (四) : 試算表操作 (上)

本篇記錄用 GAS 操作 Google 試算表的測試結果.

試算表 (SpreadSheetApp) 是 Google 雲端硬碟最常用的 App, 除了可以取代 Excel 外, 它還可以做為一個小型資料庫. GAS 提供豐富的 API 來存取與操控試算表, 搭配觸發器可以打造免費的雲端自動化應用, 教學文件參考 :


GAS 程式可以是獨立放在 Google 雲端硬碟下的 .gs 檔; 也可以是寄生在各種 Google Apps (文件, 試算表, 或表單等), 它們的差別在於 GAS API 的存取範圍, 以存取試算表為例, SpreadSheetApp 物件提供了四個方法 (API) 來取得試算表 (SpreadSheet) 或工作表 (Sheet) 物件 : 


 SpreadSheetApp 物件方法 說明
 openById(id) 根據 ID 開啟試算表, 傳回 SpreadSheet 物件
 openByUrl(url) 根據 URL 開啟試算表, 傳回 SpreadSheet 物件
 getActiveSpreadsheet() 傳回使用中的試算表物件 (SpreadSheet), 僅寄生的 GAS 可用.
 getActiveSheet() 傳回使用中的工作表 (Sheet) 物件, 僅寄生的 GAS 可用.


注意, getActiveSpreadsheet() 與 getActiveSheet() 這兩個方法只能在寄生的 GAS 程式中可以呼叫, 而 openById() 與 openByUrl() 則不論是在獨立的 GAS 檔或寄生的 GAS 程式碼中均可呼叫. 


一. 新增試算表 : 

為了以下測試, 我們先在雲端硬碟建立一個 GAS 資料夾, 並在其下建立一個試算表, 並將其更名為 myproject : 






將底下預設的工作表更名為 scores, 然後在儲存格中輸入成績資料 :




點選上方 "擴充功能" 選單, 再點選 "Apps Script" 開啟一個 GAS 程式編輯視窗 :




把上方預設的 GAS 專案名稱改為 scores : 




二. 用 GAS 操作試算表 : 

操作試算表之前須先開啟試算表取得 Spreadsheet 物件, 然後利用其方法來存取儲存格內容. 對於一個已建立之試算表, 我們可以透過下列 SpreadSheetApp 物件的四個方法開啟它 : 
  • openById(id)  
  • openByUrl(url)
  • getActiveSpreadsheet() 
  • getActiveSheet()
如前所述, 前兩者不管是獨立的 GAS 程式或寄生於試算表內的 GAS 程式都可呼叫, 但後兩者只有寄生的 GAS 程式可呼叫. 它們都會傳回一個 Spreadsheet 物件, 它有許多方法, 其中最常用的是 getSheetByName(), 只要傳入工作表名稱就會傳回一個 Sheet 物件, 呼叫其 getRange() 方法就能定位出儲存格取得 Range 物件, 最後呼叫 Range 物件的 setValue() 與 getValue() 方法就可以存取儲存格內容了. 




注意, 若 getRange() 選取的是單一儲存格, 存取時是呼叫 getValue() 與 setValue(); 若選取一個區域, 則存取時是呼叫 getValues() 與 setValues(), 傳回值或傳入值均為陣列. 

首先來釐清試算表的 ID 與 URL, 在試算表視窗上方的網址列就是此試算表的 URL, 例如 : 

https://docs.google.com/spreadsheets/d/1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4/edit#gid=0

其中 https://docs.google.com/spreadsheets/d/ 後面一直到 /edit... 之間的字串就是其 id :




以下分別測試這三種開啟試算表的方式 : 


1. 呼叫 openByUrl() 開啟試算表並存取工作表 :   

只要將試算表的 URL 傳入 openByUrl() 方法即可開啟試算表, 它會傳回一個 SpreadSheet 物件. 在 GAS 程式編輯視窗輸入下列程式碼 : 

function myFunction() {
  var url="https://docs.google.com/spreadsheets/d/1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4/edit#gid=0";
  var ss=SpreadsheetApp.openByUrl(url); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1); //取得第 2 列第 1 欄儲存格 Range 物件
  Logger.log(range.getValue()); //取得第 2 列第 1 欄儲存格內容 ('金智媛')
}

此程式用 openByUrl() 取得試算表物件 ss, 然後呼叫 ss 的 getSheetByName() 取得名為 scores 的工作表物件 sheet, 接著呼叫 sheet 的 getRange() 方法取得第 2 列第 1 欄的儲存格物件 range, 最後呼叫 range 物件的 getValue() 取得儲存格內容. 

注意 getRange() 的參數結構有兩種, 第一種是使用索引來選取 :

getRange(row, column [, numRows, numColumns]) 

前兩個參數分別是列索引與欄索引 (都是 1 起始, A 欄為 1, B 欄為 2, ...); 後兩個為可有可無參數, 分別為列數與欄數 (用來選取一個矩形區域之儲存格). 

第二種是使用試算表的欄列名稱標示法 (傳入值為字串), 例如 : 

getRange('A2:C6')   //選取 A2~C6 範圍內之儲存格

也可以跳過取得工作表物件步驟, 直接呼叫 Spreadsheet 物件的 getRange() 方法, 但在傳入參數前面添加工作表名稱 (用 ! 隔開欄列名稱), 例如 :

ss.getRange('scores!A2:C6')  //選取 scores 工作表的 A2~C6 範圍內之儲存格

執行結果如下 : 




上面程式碼是取得單一儲存格, 如果 getRange() 時是選取一個區域, 則要呼叫 getValues(), 它會傳回一個陣列, 例如將上面的程式碼改成如下 :

function myFunction() {
  var url="https://docs.google.com/spreadsheets/d/1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4/edit#gid=0";
  var ss=SpreadsheetApp.openByUrl(url); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)
}

注意, 此處是選取一個區域的儲存格, 所以應該呼叫 getValues() 而非 getValue(), 結果會傳回三個學生的全部成績, 結果如下 :




2. 呼叫 openById() 開啟試算表並存取工作表 :   

將上面程式碼改為呼叫 openById() 來開啟試算表, 同樣會傳回一個 Spreadsheet 物件 : 

function myFunction() {
  var id="1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4";
  var ss=SpreadsheetApp.openById(id); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)
}

執行結果與上面相同 : 




3. 呼叫 getActiveSpreadsheet() 開啟試算表並存取工作表 :  

將上面程式碼改為呼叫 getActiveSpreadsheet() 來開啟試算表, 同樣會傳回一個 Spreadsheet 物件 : 

function myFunction() {
  var ss=SpreadsheetApp.getActiveSpreadsheet(); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)
}

結果如下 : 




也可以呼叫 getActiveSheet() 方法直接取得使用中的工作表, 例如 :

function myFunction() {
  var sheet=SpreadsheetApp.getActiveSheet(); //傳回使用中的 Sheet 物件 (工作表)
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)
}

執行結果與上面相同 : 




注意, getActiveSpreadsheet() 與 getActiveSheet() 方法只能在寄生於試算表中的 GAS 程式碼可呼叫, 無法於獨立的 GAS 程式中使用. 


4. 於獨立的 GAS 程式中開啟試算表 :  

上面的測試中所使用的 GAS 是寄生於試算表的程式碼, 可以使用 SpreadsheetApp 物件的openByUrl(), openById(), 與 getActiveSpreadsheet() 方法開啟試算表, 但在獨立於 App 外的 .gs 檔案中, 只能呼叫 openByUrl() 與 openById() 這兩個方法來開啟試算表. 

首先按雲端硬碟底下的 "+ 新增" 鈕, 點選 "更多..." 後再點選 "Google Apps Script" : 



 
這時會彈出一個視窗, 提醒此資料夾的所有協作者皆能存取此 GAS 檔, 按 "建立指令碼" 鈕會開啟一個 "未命名的專案" 的 Google Apps 程式編輯視窗 : 






這時切回雲端硬碟視窗會發現底下出現一個 "未命名的專案.gs" 程式檔 :




點選 Google Apps 程式編輯視窗左上角的 "未命名的專案" 將其重新命名為 "myproject" : 





這時切回雲端硬碟視窗, 原本的 "未命名的專案.gs" 也會更名為 "myproject.gs" :



 
然後編輯 GAS 程式專案 myproject 裡面的預設函式 myFunction(), 輸入如下程式碼 : 

function myFunction() {
  var url="https://docs.google.com/spreadsheets/d/1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4/edit#gid=0"; 
  var ss=SpreadsheetApp.openByUrl(url); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)  
}




按 "存檔" 鈕後按 "執行" 會出現授權頁面, 按 "審查權限" 鈕 :




選擇自己的 Google 帳戶 : 



在警示視窗中按 "進階" : 




按右下角的 "允許" 即完成授權 :




完成授權即馬上會執行 GAS 程式檔, 結果與上面寄生於試算表的 GAS 程式相同 : 




也可以更改 myproject.gs 檔改用 openById() 開啟試算表 :

function myFunction() {
  var id="1ZEGvDt7JtglqVS3IuAPyelSOOjWe_BHN0jT4R7rHNk4";
  var ss=SpreadsheetApp.openById(id); //傳回 Spreadsheet 物件
  var sheet=ss.getSheetByName('scores'); //傳回 Sheet 物件
  var range=sheet.getRange(2, 1, 3, 4); //取得第 2 列第 1 欄起 3 列 4 欄的區域儲存格 Range 物件
  Logger.log(range.getValues()); //取得第 2 列第 1 欄起 3 列 4 欄的區域內容 (陣列)
}

結果與上面相同.