2026年3月8日 星期日

Google Colab 掛載 Google Drive

Google Colab (Colaboratory) 是 Google 提供可以免費線上執行 Python 程式的服務,雲端的 Jupyter Notebook 執行環境,自己不用安裝,只要瀏覽器就可以使用、執行 Python 程式。
而且還免費提供 GPU、TUP 使用。

進到 Colab,開的檔案叫做筆記本(Notebook),會儲存在 Google 雲端硬碟(Google Drive)裡的 Colab Notebooks 資料夾底下,副檔名 .ipynb,就像 Google文件(Google Docs)、Google試算表(Google Sheets) 存放在雲端硬碟一樣。只不過 Colab 筆記本存的是程式執行相關的東西。

Google 提供的  Colab 免費執行環境,是虛擬機,有執行時間限制
免費版 Colab 的筆記本最多可運行 12 小時,具體時間取決於資源可用性和使用模式。
而且開著網頁太久沒操作,也會中斷執行階段(執行階段可想成目前的執行環境)。
如果原本執行階段刪除了,或玩壞不得不重啟,重開後,除了筆記本上的東西還在,其他虛擬機上的東西都會重置回到初始狀態,資料若存在虛擬機空間上,不小心就會消失。

所以當有需要保留的資料,可將 Google 雲端硬碟掛載進虛擬機,當作儲存空間。

掛載方式:

  1. 按「檔案」

  2. 按「掛接雲端硬碟」

  3. 出現是否允許筆記本存取雲端硬碟,按「連線至 Google 雲端硬碟」


    其他方式掛載:前面步驟用指令執行
    from google.colab import drive
    drive.mount('/content/drive')

  4. 繼續授權操作,完成後,就可看到掛接的雲端硬碟



  5. 雲端硬碟掛接在 drive 資料夾

  6. 檔案完整路徑,可按「右鍵」-->「複製路徑」得知


  7. 將複製的 /content/drive/MyDrive/helloworld.py 路徑執行看看,可正常執行

    我們目前的目錄往上還有目錄,往上瞧瞧

    是作業系統資料夾的結構


刪除雲端硬碟授權:

若之後用不到,刪除雲端硬碟授權方式如下
  1. 到「Google 帳戶」管理,點「第三方應用程式和服務」


  2. 點「Google Drive for desktop」


  3. 點「刪除您帳戶與 Google Drive for desktop 之間的所有連結」


  4. 按「確認」完成刪除授權




2026年3月2日 星期一

Google Sheets 抓取公開資訊觀測站股利公告

在 Google Sheets 使用 Apps Script 抓取公開資訊觀測站(公告快易查)股票股利公告。


補充說明:

  • 後來才發現在彙總報表->股利分派情形可以一次抓整年,這更方便抓取。
    我一開始誤以為這是彙整最終實際股利分派的結果(因為未發前有可能公告更改),
    也誤以為未實際發放前的公告統整在彙總報表->除權息公告,沒想到前者是含未確定發放日的公告統整資料,後者是只含已確定發放日的公告統整資料,當時還想說除權息公告彙整資料那麼少,想說可能不是即時彙整。
  • 至於臺灣證券交易所 OpenAPI證券櫃檯買賣中心 OpenAPI,很久以前看過,但特性讓我不自覺忘記它們的存在(不能設查詢條件、新資料不夠新、甚至只看到幾年前的資料...)。

    後圖是櫃買 API 抓的資料



步驟:

  1. 新增一個空白工作表
    到「擴充功能」 -> 「Apps Script」

  2. 填入以下程式碼
    function fetchDividendToSheet() {
      const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
      const lastRowS = sheet.getLastRow();
      Logger.log(`lastRowS:${lastRowS} `);
      // ══════════════════════════════════════════════
      // 設定區
      // ══════════════════════════════════════════════
      let SDATE = "115/02/10";//開始日期
      let EDATE = "115/02/10";//結束日期
      const BATCH_SIZE = 100;
    
      // ── 日期工具函式 ─────────────────────────────
      function rocToDate(rocStr) {             // "115/02/26" → Date
        const [y, m, d] = rocStr.split("/");
        return new Date(+y + 1911, +m - 1, +d);
      }
      function toRocDate(dt) {                 // Date → "115/02/26"
        return `${dt.getFullYear() - 1911}/${String(dt.getMonth() + 1).padStart(2, '0')}/${String(dt.getDate()).padStart(2, '0')}`;
      }
      function rocToAd(rocStr) {              // "115/02/26" → "2026/02/26"
        if (!rocStr) return "";
        const [y, m, d] = rocStr.split("/");
        return `${+y + 1911}/${m}/${d}`;
      }
    
      Logger.log(`本次查詢:${SDATE} ~ ${EDATE}`);
    
      // ── 呼叫 ezsearch_query ───────────────────────
      const searchResp = UrlFetchApp.fetch(
        "https://mopsov.twse.com.tw/mops/web/ezsearch_query",
        {
          method: "post",
          payload: `step=00&RADIO_CM=1&TYPEK=sii&CO_MARKET=&CO_ID=&PRO_ITEM=D02&SUBJECT=&SDATE=${SDATE}&EDATE=${EDATE}&lang=TW&AN=`,
          headers: {
            "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
            "Referer": "https://mopsov.twse.com.tw/mops/web/ezsearch",
            "X-Requested-With": "XMLHttpRequest",
            "User-Agent": "Mozilla/5.0"
          },
          muteHttpExceptions: true
        }
      );
      const records = JSON.parse(searchResp.getContentText("UTF-8")).data || [];
      Logger.log(`API 回傳共 ${records.length} 筆`);
    
      // ── F36:align='right' TD ────────────────────
      function extractF36(html) {
        const noDataMatch = html.match(/查無資料/i);
        if (noDataMatch) {
          return ["查無資料", "查無資料", "查無資料", "查無資料", "查無資料", "查無資料"];
        }
        const strip = s => s.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, '').trim();
        const rightTds = [...html.matchAll(/<TD[^>]*align=['"]right['"][^>]*>([\s\S]*?)<\/TD>/gi)]
          .map(m => strip(m[1]));
        Logger.log("F36 right TDs: " + rightTds.join(" | "));
        if (rightTds.length < 7) throw new Error(html);
        //if (rightTds.length < 7) return ["-", "-", "-", "-", "-", "-"];
        return [rightTds[0] || "--", rightTds[1] || "--", rightTds[2] || "--",
        rightTds[4] || "--", rightTds[5] || "--", rightTds[6] || "--"];
      }
    
      // ── M14:<pre> 全形冒號 ───────────────────────
      function extractM14(html) {
        const preMatch = html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i);
        if (!preMatch) throw new Error(html);
        //if (!preMatch) return ["-", "-", "-", "-", "-", "-"];
        const lines = preMatch[1].split('\n').map(l => l.trim()).filter(l => l);
        function findVal(kw) {
          const line = lines.find(l => l.includes(kw));
          if (!line) return "--";
          const parts = line.split(':');
          return parts.length > 1 ? parts[parts.length - 1].trim() : "---";
        }
        return [
          findVal("盈餘分配之現金股利"), findVal("法定盈餘公積發放之現金"),
          findVal("資本公積發放之現金"), findVal("盈餘轉增資配股"),
          findVal("法定盈餘公積轉增資配股"), findVal("資本公積轉增資配股"),
        ];
      }
    
      // ── 初次執行時寫入表頭(工作表為空才加)────────
      if (sheet.getLastRow() === 0) {
        sheet.appendRow([
          "發言日期", "發言時間", "公司代號", "公司簡稱", "AN_CODE", "主旨",  // A~F
          "盈餘分配之現金股利(元/股)",      // G
          "法定盈餘公積發放之現金(元/股)",  // H
          "資本公積發放之現金(元/股)",      // I
          "盈餘轉增資配股(元/股)",          // J
          "法定盈餘公積轉增資配股(元/股)",  // K
          "資本公積轉增資配股(元/股)"       // L
        ]);
      }
    
      // ── 逐筆處理 ─────────────────────────────────
      const ZERO = ["0", "0", "0", "0", "0", "0"];
      //let batch = [], totalWritten = 0;
      let batch = [], batchLinks = [], totalWritten = 0;
      let skipCount = 0, zeroCount = 0, f36Count = 0, m14Count = 0;
    
      function flushBatch() {
        if (!batch.length) return;
        const startRow = sheet.getLastRow() + 1;
        const numRows = batch.length;
        // 先寫所有純文字值
        sheet.getRange(startRow, 1, numRows, batch[0].length).setValues(batch);
        // 再對 F 欄(主旨,第6欄)補 HYPERLINK 公式
        const formulas = batchLinks.map(([url, text]) =>
          [`=HYPERLINK("${url}","${text.replace(/"/g, '""')}")`]
        );
        sheet.getRange(startRow, 6, numRows, 1).setFormulas(formulas);
        totalWritten += numRows;
        Logger.log(`📥 已寫入 ${totalWritten} 筆`);
        batch = [];
        batchLinks = [];   // ← 同步清空
      }
    
      records.forEach(rec => {
        const subject = (rec.SUBJECT || "").replace(/[\r\n]/g, " ");
        const coId = rec.COMPANY_ID || "";
        const coName = rec.COMPANY_NAME || "";
        const anCode = rec.AN_CODE || "";
        const hyperlink = rec.HYPERLINK || "";
        const cdateAd = rocToAd(rec.CDATE || "");
        const ctime = rec.CTIME || "";
    
        // 篩選
        if (subject.includes("子公司")) { skipCount++; return; }
        if (subject.includes("基準日")) { skipCount++; return; }
        if (subject.includes("交易日")) { skipCount++; return; }
        if (subject.includes("發放日")) { skipCount++; return; }
        if (!subject.includes("股利")) { skipCount++; return; }
    
        if (subject.includes("不分派股利")) {
          batch.push([cdateAd, ctime, coId, coName, anCode, subject, ...ZERO]);
          batchLinks.push([hyperlink, subject]);  // ← 新增
          zeroCount++;
    
        } else if (anCode === "F36") {
          try {
            Utilities.sleep(600);
            const html = UrlFetchApp.fetch(hyperlink, {
              headers: { "Referer": "https://mopsov.twse.com.tw", "User-Agent": "Mozilla/5.0" },
              muteHttpExceptions: true
            }).getContentText("UTF-8");
            const vals = extractF36(html);
            batch.push([cdateAd, ctime, coId, coName, anCode, subject, ...vals]);
            batchLinks.push([hyperlink, subject]);  // ← 新增
            Logger.log(`[F36] ${coId} ${coName} | ${vals[0]}`);
            f36Count++;
          } catch (e) {
            Logger.log(`[F36 錯誤] ${coId}:${e}`);
            throw e;
            //batch.push([cdateAd, ctime, coId, coName, anCode, subject, ...ZERO]);
          }
    
        } else if (anCode === "M14") {
          try {
            Utilities.sleep(600);
            const html = UrlFetchApp.fetch(hyperlink, {
              headers: { "Referer": "https://mopsov.twse.com.tw", "User-Agent": "Mozilla/5.0" },
              muteHttpExceptions: true
            }).getContentText("UTF-8");
            const vals = extractM14(html);
            batch.push([cdateAd, ctime, coId, coName, anCode, subject, ...vals]);
            batchLinks.push([hyperlink, subject]);  // ← 新增
            Logger.log(`[M14] ${coId} ${coName} | ${vals[0]}`);
            m14Count++;
          } catch (e) {
            Logger.log(`[M14 錯誤] ${coId}:${e}`);
            throw e;
            //batch.push([cdateAd, ctime, coId, coName, anCode, subject, ...ZERO]);
          }
    
        } else {
          skipCount++;
        }
    
        if (batch.length >= BATCH_SIZE) flushBatch();
      });
      flushBatch();
    
      // ── 內建 removeDuplicates(A,C,D,G~L 欄)────────
      // 保留舊資料一起判斷,不清空,只刪重複列
      // 欄位 1-based:A=1, C=3, D=4, G=7, H=8, I=9, J=10, K=11, L=12
      sheet.getDataRange().removeDuplicates([1, 3, 4, 7, 8, 9, 10, 11, 12]);
      Logger.log("去重完成");
    
      let totalChange = sheet.getLastRow() - lastRowS;
      if (lastRowS == 0) {
        totalChange--;
      }
      const msg = `✅ 完成!抓取 ${totalWritten} 筆\n去除重複資料後異動 ${totalChange} 筆\n` +
        `F36:${f36Count} M14:${m14Count} 不分派:${zeroCount} 略過:${skipCount}\n` +
        `查詢區間:${SDATE} ~ ${EDATE}`;
      Logger.log(msg);
      SpreadsheetApp.getUi().alert(msg);
    }
    
    function onOpen() {
      SpreadsheetApp.getUi()
        .createMenu("📊 股利爬蟲")
        .addItem("執行爬蟲", "fetchDividendToSheet")
        .addToUi();
    }
    
  3. 按「儲存」

  4. 選要執行的函示「fetchDividendToSheet」,按「執行」

  5. 初次執行,需要授權,按「審核權限」

  6. 按「進階」

  7. 繼續前往專案

  8. 抓取資料須「連線至外部服務」權限,
    寫入試算表須「查看、編輯、建立及刪除您的所有 Google 試算表檔案」權限。
    兩者勾選後,按「繼續」便會開始執行。

  9. 執行結果





2026年2月21日 星期六

Git 2.53.0 安裝

Git 安裝過程跟以前安裝時有些微不同,紀錄一下安裝過程。

環境:
Windows 11、Git 2.53.0


安裝步驟:

  1. 到官網 http://git-scm.com 下載最新版 Git,我下載的是 Git-2.53.0-64-bit.exe
  2. 執行安裝檔,安裝畫面按「Next」到下一步驟

  3. 選擇安裝到那個資料夾,按「Next」。

  4. 早期安裝的時候沒有 Git LFS 和 Scalar,這兩個我都照預設勾選,進行下一步。
    Git LFShttps://git-lfs.com/
    Git Large File Storage,一個對大型檔案進行版本控制的開源 Git 擴展。將大檔案(圖片、影片...)上傳至大檔案儲存服務,而與大檔案對應的指標檔案與其他程式碼檔案推送到 Git 倉庫中,以讓 Git 倉庫空間佔用減小。
    Scalarhttps://git-scm.tw/docs/scalar
    管理大型 Git 倉庫的工具。


  5. 是否在開始選單建立資料夾,按「Next」。

  6. 預設使用什麼文字編輯器,依自己喜好選擇,這邊我用預設的 Vim


  7. 設定執行 git init 時,建立的儲存庫分支名稱。早期安裝時沒這設定,名稱是 master,不解為什麼這要設定,查了一下,原來是跟種族歧視有關(master/slave),以前還真沒想到這些用語背後隱含的文化意義。。
    避免無心造成的紛爭,建議設定個中性的名稱。
    目前 GitHub 是使用 main,所以我也設定為 main。


  8. 設定執行 Git 指令的方式,以前我習慣不動到原本系統環境變數 PATH 的設定(Git Bash only)。
    但這次看到安裝畫面建議選第二項,優點是可讓其他要使用 Git 的軟體,在環境變數 PATH 裡找到 Git 使用。所以我選預設的第二項。


  9. 使用 SSH 時,ssh.exe 指令使用 Git 自帶捆綁的 OpenSSH ,還是外部的 OpenSSH。
    我安裝過 Windows 選用功能裡的 OpenSSH (設定 --> 系統 --> 選用功能),路徑 C:\Windows\System32\OpenSSH,所以我選擇使用外部的 OpenSSH。

    相關:開始使用 Windows 版 OpenSSH 伺服器 | Microsoft Learn


  10. 使用 HTTPS 時,使用 OpenSSL library 還是 Windows 原生的安全通道 library。
    Windows 原生的安全通道 library,優點是可使用公司內部發的 CA 證書。
    我選擇 Windows 原生的安全通道 library。
    相關:在高度網路管制的企業內部如何設定 Git 連接 Azure Repos 上的儲存庫 | The Will Will Web

  11. 換行轉換處理,在 Windows 安裝選第一項,checkout LF 轉成 CRLF,commit CRLF 轉成 LF。
    這務必大家處理原則都一致,避免發生因換行轉換原則不同,誤判每行都有修改。

  12. Git Bash 使用哪種終端機操作,MinTTY 或 Windows內建終端機。
    這裡選 MinTTY,比Windows內建終端機好用。

  13. 選擇 git pull 指令的行為,早期的安裝過程也無此選項。
    我通常都是先 git fetch,再看怎麼做,所以直接保留安裝程式預設的第一項。
    第一種:git pull = git pull --no-rebase = git fetch + git merge
    第二種:git pull = git pull --rebase = git fetch + git rebase
    第三種:git pull = git pull --ff-only ,如果本機分支與遠端分支出現分歧,則會失敗。

  14. 選擇是否使用 Git Credential Manager(GCM、Git 認證管理員) 。
    一個為 Git 提供無縫的多因素 HTTP(S) 身份驗證工具,優點不用每次連到 HTTP(S) 遠端儲存庫都要輸入密碼。
    當 git push 到遠端儲存庫時,系統會自動開啟一個 GCM 窗口,引導完成登入。然後 GCM  儲存認證(儲存在 Windows  認證管理員、控制台\使用者帳戶\認證管理員),之後在同一儲存庫中的後續 Git 命令將重複使用現有的認證(credentials or tokens、憑證或令牌),直到認證失效。

    安裝畫面上的兩個連結:Cross-platform Git Credential Managerhere

  15. 勾選 Enable file system caching:將資料快取到記憶體,供特定操作使用,此設定可顯著改善效能。

  16. 安裝完成



  17. 以上選擇安裝完後,Git 系統的設定值如下
    $ git config --list --system
    diff.astextplain.textconv=astextplain
    filter.lfs.clean=git-lfs clean -- %f
    filter.lfs.smudge=git-lfs smudge -- %f
    filter.lfs.process=git-lfs filter-process
    filter.lfs.required=true
    http.sslbackend=schannel
    core.autocrlf=true
    core.fscache=true
    core.symlinks=false
    pull.rebase=false
    credential.helper=manager
    credential.https://dev.azure.com.usehttppath=true
    init.defaultbranch=main



參考:


2026年2月15日 星期日

解決電腦設備符合 Win11 要求,卻仍顯示不符合執行 Win11 最低系統需求

問題:

主機板 BIOS 已開啟 TPM2.0、UEFI、安全開機。

執行 「tpm.msc」 ,查看目前的 TPM 資訊:


執行「msinfo32」  查看目前的 UEFI、安全開機 資訊:



以上皆已符合 Windows 11 需求,但在 Win10 的 Windows Update 裡,仍然顯示「您的電腦目前不符合執行Windows 11 的最低系統需求」,按檢查更新無法升級到 Windows 11:


當下我懷疑 Win10 在 Windows Update 裡顯示的訊息可能是快取,而不是最新的狀態。
所以去下載安裝了「電腦健康情況檢查(PC Health Check)」,
但執行後出乎意料,居然顯示「若要查看此電腦是否可以執行Windows 11,請檢查系統需求,或詢問製造您電腦的公司。」,甚至「查看結果」的按鈕都不能按。
加上「電腦健康情況檢查(PC Health Check)」執行的飛快,完全沒有檢查的感覺,讓我更覺得不符合 Win11 需求的判斷是快取。
這個畫面當時我沒有截圖,後面兩個圖是事後從網路找的。
後圖來源:https://learn.microsoft.com/zh-tw/answers/questions/4293493/question-4293493


後圖來源:https://learn.microsoft.com/zh-tw/answers/questions/4253480/windows-11?forum=windows-all&referrer=answers





解決方式:

手動讓系統立即評估檢查
後來在 reddit 看到有網友提供讓系統立即檢查的方法,手動執行工作排程器裡的 Microsoft Compatibility Appraiser。

Task Sceduler (工作排程器)-> Microsoft -> Windows -> Application Experience -> Microsoft Compatibility Appraiser

(Win10 和 Win11 有些微差異)
Win10:Microsoft Compatibility Appraiser
Win11:Microsoft Compatibility Appraiser Exp

我操作時間點,大約在傍晚,後面的圖則是在下午7點多擷取的,可發現上次執行是早上4點多,下次執行要等到隔天早上。
(註:事後我在微軟官網有找到系統大約24小時評估檢查一次。)



在 Microsoft Compatibility Appraiser 排程上,按「右鍵」-->「執行」


之後可發現 Microsoft Compatibility Appraiser 排程,處於執行中的狀態。
微軟官方是說等排程跑完,狀態變回「就緒」。
reddit 的網友是說等 10~15 分。

我是打開工作管理員,看到「Microsoft Compatibility Telemetry」占用很多 CPU,等「Microsoft Compatibility Telemetry」降下來後,就去Windows Update 看到變成符合升級 Win 11,按了檢查更新,就出現可下載 Win11 升級,
不用等 Microsoft Compatibility Appraiser 排程跑完,狀態變回「就緒」,因為不知道要等多久,也不知道是不是實際已跑完,只是狀態沒變回「就緒」。










參考: