我有一张金山文档表格,用来跟踪 7 只票的挂单价位:招金黄金、东百集团、博纳影业、天顺风能、西部黄金、顺灏股份、钧达股份。每只票一个区块,结构是「基准价 → 上涨触发价 / 下跌触发价 → 现价」。
盯盘软件当然能看价格,但我要的不是价格,是我自己设的那几个价位离现价还有多远。这个判断只有把行情填进我这张表里才成立。于是每天早上开盘,我就开始重复一个动作:切到行情软件,看一个价格,切回表格,手打进去,再切走……七只票,一天几十次。
我决定把它自动化。但我不会写前端,也不想搭服务器、买域名,更没有精力维护一套定时任务。我的想法很简单:能不能让 AI 把这件事从头到尾做完,最后我只负责打开文档看一眼?
下面是我和 AI 一起把这件事做成的全过程,包括中间踩的四个坑——我认为这几个坑比最后的成品更有价值。
金山文档自带 AirScript(基于 JavaScript 的表格自动化脚本),理论上最优雅的方案是:脚本定时执行 → 请求行情接口 → 写入单元格。
我一开始就是这么干的,核心代码大概长这样:
function fetchQuotes() {
var url = "https://qt.gtimg.cn/q=sh601808,sz000506";
var resp = HTTP.get(url); // 期望拿到行情字符串
var text = resp.text;
// 解析 "v_sh601808=~中海油服~601808~12.75~..." 这种格式
return parse(text);
}跑起来直接报错:
hostName not in whitelists这是第一个认知纠正:AirScript 的 HTTP 请求走的是平台侧代理,只允许访问白名单内的域名。所有行情站点(腾讯、新浪、东方财富)都不在名单里,而且这个名单用户改不了。
也就是说,"文档内脚本直接抓行情"这条路在架构上就是死的,不是我代码写错了。这时候正确的选择是换架构,而不是继续调代码。
既然文档脚本出不去,那就让外面的人进来。金山文档给每个脚本提供一个 Script Token Webhook:外部程序 POST 一个 JSON 过去,就能触发文档里的脚本函数,并且能把参数传进来。
于是架构变成两条腿:
[本机 Python 推送器] --每30秒--> 腾讯行情接口(qt.gtimg.cn,GBK 编码)
|
| POST + AirScript-Token
v
[金山文档 AirScript] --> 解析参数 --> 写入对应单元格推送器核心就二十行:
CODES = ["sz000506", "sh600693", "sz001330", "sz002531",
"sh601069", "sz002565", "sz002865"]
raw = requests.get("https://qt.gtimg.cn/q=" + ",".join(CODES),
timeout=15).content.decode("gbk", "ignore")
quotes = {}
for seg in raw.split(";"):
m = re.search(r'v_(s[hz]\d+)="([^"]*)"', seg)
if not m:
continue
f = m.group(2).split("~")
quotes[m.group(1)] = {
"price": float(f[3]), # 现价
"pct": float(f[32]), # 涨跌幅
"high": float(f[33]), # 当日最高
"low": float(f[34]), # 当日最低
}
requests.post(WEBHOOK, headers={"AirScript-Token": TOKEN},
json={"Context": {"argv": {"quotes": quotes}}})这里有个值得记的细节:腾讯这个接口返回的是 GBK 编码,直接 resp.text 会得到一串乱码股票名。必须 content.decode("gbk")。我第一次就是因为这个,明明请求成功却解析出 0 条数据,排查了半小时。
另外,f[33] / f[34] 这两个字段直接给出当日最高 / 最低——这意味着"记录当天最高价"这件事根本不需要我自己维护状态、不需要存历史,接口白送。省掉一整个模块。
AirScript 2.0 环境和浏览器里的 JS 不完全一样,我踩了三条:
1)写值必须用 Value2,不能用 Value
// 静默失败,单元格毫无变化,也不报错
sheet.Range("B6").Value = 13.97;
// 正确
sheet.Range("B6").Value2 = 13.97;2)读文本用 Text,不要用 Value2
判断某个格子是不是"现价"这个标签时,Value2 有时拿到的是数字类型,字符串比较会失败。Range("B6").Text 才是稳定拿到显示文本的方式。
3)不要写死行号
我最初把 7 只票的位置写死成 B6 / B12 / B18……结果用户在表格里插了两行,全部错位。后来改成按股票名称做锚点,再向下找标签:
function findLabelRow(sheet, label, anchor) {
// 只在锚点下方找,不能向上搜
for (var r = anchor + 1; r <= anchor + 12; r++) {
if (sheet.Range("B" + r).Text === label) return r;
}
return null;
}这段代码里"只在下方找"是关键——我第一版写成上下各搜 12 行,结果每只票都找到了上一只票的"现价"标签,7 个价格整体往上串了一行。表面上"有数字",实际上全错位,这种 bug 最阴险。
ClearContents() 静默失效我要清掉表格底部一堆残留的旧公式(全是 #DIV/0!)。写的是:
sheet.Range("A45:J60").ClearContents(); // 没有任何效果,也不报错在 AirScript 2.0 里这个方法对合并单元格区域是静默失败的。最后改成逐格写空字符串:
function clearCells(sheet, r1, r2, c1, c2) {
var n = 0;
for (var r = r1; r <= r2; r++) {
for (var c = c1; c <= c2; c++) {
var addr = String.fromCharCode(64 + c) + r;
try { sheet.Range(addr).Value2 = ""; n++; } catch (e) {}
}
}
return n;
}教训:在一个沙箱化的脚本环境里,"没报错"不等于"成功了"。凡是写操作,做完必须读回来验证。
用户手动调格式时把 B8:B10 合并了。之后往 B9 写值,值确实写进去了,但显示在合并区域的左上角 B8——肉眼看就是"没写进去"。更麻烦的是,同一行其他列(C、D)没有被合并,于是整行看起来错位。
检测方法:
var mc = sheet.Range("B9").MergeCells; // 判断是否处于合并区域结论:任何"按坐标写入"的自动化,遇到用户手改过格式的表,都要先做一次结构体检。
我做"每日收盘存档"功能时,脚本自动 Sheets.Add 建了一张「历史记录」表。建完之后,文档当前显示的工作表变成了这张空白新表。
用户打开文档,看到一片空白,第一反应是"我的数据被你搞没了"。
实际上数据都在「工作表1」里,只是视图被切走了。修复只需要一行:
Application.Sheets.Item("工作表1").Activate();这是纯粹的体验教训:任何会自动改动文档结构的功能,都必须自己负责把用户送回原来的位置,并在日志里明确说明"新建了什么、现在在哪张表"。技术上 100% 正确,体验上依然是事故。
我在文档里批量改了配色和结构,用户看到某处不对,习惯性按 Ctrl+Z——把我刚修好的东西一起撤回了,于是同样的乱象反复出现两次。
这事没法用代码解决,只能靠沟通:自动化接管了表格的"样式层"之后,人工撤销会破坏一致性。要么接受由脚本统一管理样式,要么明确哪些区域脚本不碰。
功能跑通那天,7 只票的价格每 30 秒精准落位。但我打开文档看了三秒就关了——太丑,不想看。
于是又花了一轮时间重做视觉,规则很简单:
项目 | 做法 | 原因 |
|---|---|---|
区块底色 | 7 只票统一淡蓝灰 | 交替底色会让人以为是两张表拼的 |
区块分隔 | 块之间加粗分隔线 | 密集数字里靠颜色分块不够,要有物理边界 |
现价区 | 米黄色底 + 加大加粗 | 一眼区分"我设的价位"和"市场当前价" |
涨 / 跌 | 鲜红 / 青绿(同花顺配色) | 用户长期看同花顺,颜色语义必须一致 |
当日最高 / 最低 | 红 / 绿加粗,紧跟现价 | 高低价差就是当天的波动幅度,放一起才有意义 |
这里有个通用原则:表格类工具的可用性,很大程度取决于「扫一眼能不能抓到关键信息」。如果用户需要逐行读,那自动化就只完成了一半——把判断成本从"抄写"降到了"阅读",但没降到"扫视"。
图 1:7 只票统一区块结构,现价 / 当日最高 / 当日最低实时刷新
图 2:统一为同花顺配色语义,涨红跌绿
现在的状态:
用户后来还问:"我把这个脚本拷走,是不是就不依赖你了?"——是的。pusher.py 加一个 启动.bat,双击就跑;离线包里还带了 一键开机自启.bat,用 schtasks 注册登录时自动启动:
schtasks /create /tn "股价自动推送" /tr "%~dp0启动推送.bat" /sc onlogon /f1. 平台限制要先摸清,再动手写代码。
"hostName not in whitelists" 这个错误,本质是产品设计层面的决策(安全隔离),不是 bug。在这种约束下继续优化原来的代码路径,纯属浪费时间。识别"这是路封死了"比"把路走通"更重要。
2. 自动化最脆弱的地方不是算法,是人的手工操作。
四个坑里有三个(合并单元格、插行、撤销)都是"用户手动改过表格"引起的。任何面向真实用户的表格自动化,都必须假设结构随时会被人改,所以定位要用语义锚点(按名称找),不要用坐标。
3. 写操作必须回读验证。
ClearContents() 不报错但没生效这件事,如果不是我顺手读回来检查,会一直以为清干净了。沙箱环境的 API 完整度参差不齐,"无异常"是最不可靠的成功信号。
4. AI 的价值在于扛住"脏活",但判断权得留给人。
这个项目里 AI 干了什么:查接口字段、调编码、写脚本、部署到文档编辑器、反复跑验证、改配色。我干了什么:说清我要什么、在表格里指出"这块乱了"、决定价位。分工其实很清晰——AI 处理确定性劳动,人处理意图和验收。
同构的需求可以直接套:任何"把外部实时数据填进一张自己设计的在线表格",比如库存监控、竞品价格跟踪、服务器指标看板、项目进度日报。核心就三步:
不适合的场景也说清楚:需要毫秒级响应的(推送有延迟)、需要多人协同实时编辑同一区域的(会互相覆盖)、数据源需要付费授权的。
本文提到的接口字段与平台行为基于 2026 年 11 月的实测,平台迭代后可能变化;涉及股票数据仅为技术演示,不构成任何投资建议。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。