2026年3月10日 星期二

解決 Antigravity Failed to send 問題

問題:

使用 Antigravity 和 AI 對話時出現「Failed to send」


解決方式:

  1. 在程式碼編輯介面,按「Ctrl + Shift + P」,執行「Antigravity: Reset Onboarding」,重新跑一次當初安裝完軟體的初始設定。
    我執行之前,看了一下我目前的設定(rules、Browser URL Allowlist),想知道 Reset Onboarding 後,設定是否會不見。
    先講結論:最後我的設定還在,所以猜測除了過程中那些詢問的設定,對其他設定應該不會有影響。

  2. 之後就會進入初始設定,不贅述,只截圖。








  3. 前面導向瀏覽器,登入帳號,出現這個畫面後,就可以切換回 Antigravity,此時Antigravity 應該已切換到下一步的畫面。

  4. Antigravity 下一步的畫面,是關於隱私、安全,提醒避免處理高度敏感數據。
    讓我想起最初安裝 Antigravity 時,特意選了個無關緊要的舊帳號使用,用了幾天,才發現帳號裡還是有隱私資料,就另外註冊個新帳號使用,這樣最保險。建議大家目前使用這類產品,用平常沒在用的帳號,比較不用怕資料外洩。
    (前面的操作,會授權Antigravity 存取 Google 帳戶中的部分資訊,包括可能具機密性的資訊)

  5. 設定完,進到對話,已可正常使用。




其他補充:

如果有人跟我一樣換過帳號,記得解除舊帳號對Antigravity 的授權。
  1. 到「Google 帳戶」管理,點「第三方應用程式和服務」


  2. 點「Google Antigravity」


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






參考:


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



參考: