360 Secure SkillHub
360 安全技能中心

spreadsheet-ops

该 Skill 提供以编程方式处理电子表格的操作指南:按场景选择 openpyxl、xlsxwriter、pandas、gspread、Sheets API 或 Apps Script,覆盖公式修复、.xlsx 生成、云端读写、批处理管线与验证,并提示 openpyxl 不计算公式、服务账号需共享、API 配额和批量写入等陷阱。

ericrisco
热度 0.00
最近更新 2026-10-19 07:45:45
分类:数据依赖:代码执行 / 读写文件 / 第三方 API应用场景:数据处理 / 自动化脚本 / 报告撰写 / API 集成 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add ericrisco/rsc-harness/spreadsheet-ops
Metadata
name
spreadsheet-ops
description
Use when working a spreadsheet programmatically — writing or repairing a formula, generating an .xlsx with Python including charts and conditional formats, driving Google Sheets from a script, building a read-transform-write pipeline, or writing an in-sheet macro or trigger. Covers formula errors, Sheets API quota, service-account permissions, and formulas that read back blank. NOT pure dedupe or normalisation with no sheet output (that is `data-cleaning`), NOT scraping a web table into rows (that is `data-scraper`), NOT multi-app event wiring (that is `automation-flows`).
tags
["spreadsheet","excel","google-sheets","openpyxl","gspread","apps-script","formulas","automation"]
recommends
["data-cleaning","data-scraper","automation-flows","google-workspace","reporting","duckdb","document-processing"]
origin
risco

spreadsheet-ops — the spreadsheet as a programmable surface

A spreadsheet is a runtime, not a document. Your job is to emit a checkable artifact — a .xlsx file that opens, a script that compiles, a cell range you can re-read — never a hand-waved formula in prose. Pick the tool that matches the runtime (local file vs cloud sheet, one-shot vs event-driven), write the formula with comma arguments, and verify before you claim it works.

The trap that bites everyone first: openpyxl writes formulas but does not compute them. The string is stored; Excel evaluates it on open. If you read the cell back with Python it is None until a real Excel/LibreOffice session has saved it. Internalize that before you touch a workbook.

Pick the tool first (this branches, so decide before coding)

Your situation Reach for Why
Read and modify an existing local .xlsx openpyxl Only mainstream lib that round-trips an existing file.
Write a new .xlsx rich with charts/conditional formats, possibly large xlsxwriter (via pandas ExcelWriter) Write-only but the richest formatting + low-memory constant_memory mode.
Dump a DataFrame to a sheet, minimal fuss pandas df.to_excel(...) Wraps an engine; pass engine= explicitly so the result does not depend on what is installed.
Read/write a cloud Google Sheet from a script gspread (+ gspread-formatting for colors/rules) Friendly wrapper over the Sheets API for CRUD.
High-volume cloud writes, must beat quotas Sheets API v4 batchUpdate One batched call counts as one request — survives the per-minute limit.
In-sheet menus, time-driven triggers, custom functions Apps Script (V8) Runs inside the sheet; the only place for onEdit/menus.

Two you will confuse: xlsxwriter cannot open an existing file — if you need to edit one in place, that is openpyxl. And gspread vs the raw Sheets API is convenience vs throughput: reach for raw batchUpdate only when gspread's per-call writes would blow the quota.

Versions seen on PyPI when this was written (2026-06-02): openpyxl 3.1.5, gspread 6.2.1 (Python 3.8+), gspread-formatting 1.2.1, pandas on the 3.x line, Sheets API v4, Apps Script on the V8 runtime. Treat these as a floor, re-check PyPI before pinning, and pin whatever you actually resolve — do not copy a point release from a doc as gospel.

Formula rules (each rule, one reason)

  1. Arguments are comma-separated, always — never semicolons. openpyxl and the file format use commas regardless of your machine's locale; a semicolon is the single most common "my written formula is broken" cause. Bad: =SUM(A1;A2) → Good: =SUM(A1,A2).
  2. A formula string must start with =. ws["A1"] = "SUM(1,1)" stores literal text; ws["A1"] = "=SUM(1,1)" stores a formula. No leading = means no calculation, ever.
  3. Anchor references you intend to fill. A lookup table dragged down breaks unless absolute: Bad: =VLOOKUP(A2,Sheet2!A:B,2,0) → Good: =XLOOKUP(A2,Sheet2!$A:$A,Sheet2!$B:$B).
  4. Prefer XLOOKUP over VLOOKUP — exact match by default, no magic column index to miscount, works leftward. VLOOKUP's 4th arg silently defaulting to approximate match is a classic wrong-result bug.
  5. Prefer dynamic arrays over Ctrl+Shift+Enter array formulas — =FILTER(...), =SORT(...), =UNIQUE(...) spill automatically and are readable. A spill blocked by data below it raises #SPILL!; clear the range, do not array-enter.
  6. Never deep-nest IFs. Past two levels use LET (name sub-expressions once) or a helper column. Bad: =IF(A1>90,"A",IF(A1>80,"B",IF(A1>70,"C","D"))) → Good: a lookup table + XLOOKUP, or =LET(g,A1, IF(g>90,"A", IF(g>80,"B","C"))) kept shallow.

When generating cross-sheet formulas, SHEET() returns a sheet's index and SHEETS() returns the sheet count — both have shipped in Excel for years (SHEET/SHEETS since Excel 2013) and exist in Google Sheets, so they are safe to emit. Full XLOOKUP/SUMIFS/LET patterns and the #VALUE!/#REF!/#N/A/#NAME? error decoder live in references/formula-cookbook.md.

Python → Excel

The no-compute trap, stated as code:

from openpyxl import load_workbook
wb = load_workbook("report.xlsx")
ws = wb.active
ws["C2"] = "=A2*B2"          # stores the FORMULA string, not a number
wb.save("report.xlsx")
# Re-reading C2 now returns "=A2*B2" (the formula), or with data_only=True
# returns None — because no Excel has opened+saved the file to cache a value.
back = load_workbook("report.xlsx", data_only=True)
assert back.active["C2"].value is None   # last cached value: none yet

So: if a downstream step needs the computed number, either compute it in Python and write the value, or open the file once in Excel/LibreOffice headless to populate the cache. Do not assume openpyxl evaluated it.

Append a sheet to an existing file (the only mode that does not silently truncate):

import pandas as pd
with pd.ExcelWriter("report.xlsx", engine="openpyxl", mode="a",
                    if_sheet_exists="replace") as xl:
    df.to_excel(xl, sheet_name="2026-Q2", index=False)

Charts and conditional formatting on a new file go through xlsxwriter (richer) — it is write-only, so never point it at a file you need to keep. Recipes (chart, conditional format, write_only/ constant_memory for big files) are in references/python-excel.md.

Google Sheets from a script

import gspread
gc = gspread.service_account(filename="bot-key.json")  # service account
sh = gc.open_by_key("SPREADSHEET_ID")
ws = sh.worksheet("Orders")
ws.append_rows([["2026-06-02", "INV-1001", 240.00]],
               value_input_option="USER_ENTERED")      # parses formulas/dates

The failure that wastes the most time: a service account has zero access until the sheet is shared with its email. Open the key JSON, copy client_email, and share the sheet with it (Editor) — a PermissionError/SpreadsheetNotFound here is almost always an unshared sheet, not bad code.

Quotas (as published 2026-06-02 — re-check Google's "Usage limits" page, these shift): 300 read req/min/project, 60 read req/min/user (writes mirror this), refilled each minute, no daily cap under the per-minute ceiling. A batchUpdate — with all its sub-requests inside — counts as one request. So batch: build one batchUpdate body instead of N update calls, and wrap calls in exponential backoff on HTTP 429. Batching is the difference between a nightly job that survives and one that 429s.

Creating the OAuth client or service account itself is google-workspace's job; this skill assumes the credential exists and owns the Sheets surface. batchUpdate request shapes and backoff code are in references/sheets-api-appsscript.md.

In-sheet automation (Apps Script V8)

Write for V8 — Google deprecated the legacy Rhino runtime (Feb 2025) and set its retirement for on or after 2026-01-31, so new code should never target it. V8 has no fetch, no timers, no streams — use UrlFetchApp.fetch() (or fetchAll() for parallel) for HTTP. No ES6 import/export, no private #fields.

Read and write the sheet in one range operation, never a per-cell loop — getValue()/setValue() inside a loop makes one API round-trip per cell and times out on real data:

function colorOverdue() {
  const sh = SpreadsheetApp.getActiveSheet();
  const data = sh.getDataRange().getValues();   // ONE read, all rows
  const out = data.map(r => r[3] === "OVERDUE" ? ["#fde0e0"] : ["#ffffff"]);
  sh.getRange(1, 1, out.length, 1).setBackgrounds(out.map(c => c)); // ONE write
}

Time-driven triggers (run nightly), onEdit simple triggers, and custom menus are covered in references/sheets-api-appsscript.md.

Pipeline pattern: read → transform → write

A pipeline that reruns must not duplicate rows. Decide the write mode first:

  1. Define a header contract — the exact columns and order. Assert the live header matches before writing; a shifted column silently corrupts everything downstream.
  2. Choose the write mode:
    • Full overwrite — clear the data range, write all rows. Simple, safe when the sheet is yours alone.
    • Keyed upsert — read existing keys (e.g. order_id) into a map, update matched rows, append the rest. Idempotent: rerunning yields the same sheet, no duplicates.
  3. Dry-run first. Compute the diff (rows to add/update) and log it; only write when a --apply flag is set. This catches a wrong key or a header drift before it touches the live sheet.
  4. Batch the write (batchUpdate / setValues) and back off on 429.

Anti-patterns

Anti-pattern Why it bites Do instead
Semicolon arguments =SUM(A1;A2) File format is comma-only; locale display fools you Always commas =SUM(A1,A2)
Formula string without leading = Stored as literal text, never calculates Start the string with =
Assuming openpyxl computed the result It only stores the formula; value is None until Excel opens it Write the value, or open once to cache
Per-cell getValue()/setValue() loop (Apps Script) One round-trip per cell → timeout getValues()/setValues() once per range
N single update calls to the Sheets API Blows 60/user-min, 300/project-min quota One batchUpdate; backoff on 429
Service account not shared with the sheet PermissionError, looks like a code bug Share the sheet with client_email from the key
Deep-nested IF chains Unreadable, miscounted parens, hard to repair LET, a lookup table, or a helper column
xlsxwriter to modify an existing file It is write-only; you lose the original openpyxl for read+modify
df.to_excel("f.xlsx") over an existing file Default mode truncates the whole workbook ExcelWriter(..., mode="a") to append
VLOOKUP with a hardcoded column index Index miscounts when columns move; approximate-match default XLOOKUP with explicit ranges

Verify

Run scripts/verify.sh (read-only by default; pass a path to a generated .py/.xlsx). It py_compiles generated scripts, opens produced workbooks with openpyxl to confirm a sheet + header row, and lints formula strings for the semicolon-argument bug. Cloud paths are validated by structure, not live Google calls — no network or credentials required. On an empty/clean target it exits 0.

References: python-excel.md · sheets-api-appsscript.md · formula-cookbook.md.