如何在 Google Sheets 中获取实时汇率(GOOGLEFINANCE、Apps Script 与货币 API)
在 Google Sheets 中获取实时汇率看起来是个早已解决的问题。你输入 =GOOGLEFINANCE("CURRENCY:USDEUR"),一个数字冒出来,然后你就继续做别的事了。用来算个人旅行预算,这完全够用。但只要这张表被用来生成发票、跑工资、出具收入报表,或者任何同事会据此做决策的场景,它就不灵了——原因是一条授权限制、一个有文档记载的日期怪癖,以及一个几乎没有教程提到的 Apps Script 限制。
本指南涵盖全部三种可行方案——内置的 GOOGLEFINANCE 函数、由货币 API 驱动的 Apps Script 自定义函数,以及由定时触发器刷新的计划汇率表——并附上真实的配额数字、Google 官方警示的原文措辞,以及可直接运行的代码。下文中每一条关于 Google 行为的说法都引自 Google 自己的文档,而每一条没有官方文档支撑的说法都会明确标注出来。
把汇率拉进 Google Sheets 的三种方式
GOOGLEFINANCE | Apps Script 自定义函数 | 计划汇率表 | |
|---|---|---|---|
| 配置耗时 | 几秒钟 | 约 10 分钟 | 约 20 分钟 |
| 是否需要 API 密钥 | 否 | 是 | 是 |
| 刷新方式 | 自动,随重新计算 | 随重新计算 | 由你掌控的触发器 |
| 历史汇率 | 支持(有前提条件) | 取决于你的套餐 | 支持 |
| 可在 Apps Script / Sheets API 中使用 | 历史数据:否 | 是 | 是 |
| 商业 / 专业用途 | 受 Google 条款限制 | 受你的 API 服务商条款约束 | 受你的 API 服务商条款约束 |
| 最适合 | 快速查询、个人表格 | 临时性的换算列 | 任何别人要依赖的场景 |
方法一:GOOGLEFINANCE,以及 Google 对它的确切说法
基础汇率公式
所有教程都会教的写法,是把两个 ISO 4217 货币代码拼接成一个 CURRENCY: 代码:
=GOOGLEFINANCE("CURRENCY:USDEUR")换算单元格 A2 中的金额:
=A2 * GOOGLEFINANCE("CURRENCY:USDEUR")用两个存放货币代码的单元格拼出货币对:
=GOOGLEFINANCE("CURRENCY:" & B2 & C2)一条其他同类文章都不会说的实话: CURRENCY:XXXYYY 这种代码写法在 Google 官方的 GOOGLEFINANCE 文档里根本找不到。有文档记载的函数签名是 GOOGLEFINANCE(ticker, [attribute], [start_date], [end_date|num_days], [interval]),而帮助页面里唯一与货币相关的内容,是一个 "currency" 属性(某只证券的计价货币)和一个图表示例。CURRENCY: 货币对语法确实能用,而且用了很多年,但它属于社区约定俗成的行为,而非有文档背书的契约——这意味着 Google 完全可以不发弃用通知就直接改掉它。Google 同样没有公布任何受支持货币对的清单,所以任何告诉你它"支持大约 50 种货币"的文章都是在猜。
历史汇率:人人都搞错的属性规则
传入日期,你就能拿到历史数据:
=GOOGLEFINANCE("CURRENCY:USDEUR", "close", DATE(2026,1,15))=GOOGLEFINANCE("CURRENCY:USDEUR", "close", DATE(2026,1,1), DATE(2026,1,31), "DAILY")注意这里的属性。Google 说得很清楚:"若指定了任何日期参数,该请求即被视为历史数据请求,此时只允许使用历史属性。" 历史属性包括 open、close、high、low、volume 和 all——"price" 不在其中。"price" 在文档中仅被定义为实时属性("实时报价,延迟最多 20 分钟")。
现在就去翻翻排名靠前的 Google Sheets 汇率教程:其中大多数会直接给你 GOOGLEFINANCE("CURRENCY:USDEUR", "price", DATE(...))。这个组合与 Google 自己的文档相矛盾。请使用 "close"。
还有两条值得记牢的官方行为说明:
- "若指定了
start_date但未指定end_date|num_days,则只返回该单日的数据。" - "实时结果会作为单个单元格内的一个值返回。历史数据即便只有一天,也会以带列标题的展开数组形式返回。" 这就是为什么一条看起来没问题的历史公式,会把两列数据和一行表头溢出到你整洁的版面里,也是你通常需要在外面套一层
INDEX(...,2,2)的原因。
UTC 正午偏移
这一条会悄无声息地毁掉按日期匹配的报表。以下是 Google 的原话:
"Google 会将传入 GOOGLEFINANCE 的日期视为 UTC 时间的正午。在该时间之前收市的交易所,其数据可能会偏移一天。"
如果你在核对一本每行都必须使用该记账日汇率的账簿,一天的偏移就是一次真正的对账断裂,而不是四舍五入问题。如果这正是你表格的用途,请在把溢出的 GOOGLEFINANCE 数组当作审计记录之前,先读一读我们关于汇率与税务申报的指南。
延迟是 3 分钟,不是 20 分钟
几乎每一篇讲这个主题的文章都会告诉你,GOOGLEFINANCE 的汇率"延迟最多 20 分钟"。这个 20 分钟的数字来自 "price" 属性上那句通用的证券报价免责声明。Google 自己的 Google Finance 免责声明页面公布了一张按资产类别划分的延迟表,其中货币那一行——全球范围,由 Morningstar 提供——写的是 3 分钟。加密货币同样是 3 分钟。
所以数据比互联网上流传的要新鲜得多。Google 没有公布的,是 Sheets 多久重新计算一次公式,而这恰恰才是你真正在意的数字——一个只有 3 分钟延迟的汇率,如果待在一个自周二起就没重算过的单元格里,它依然是周二的汇率。任何人给出具体的重算间隔,都是在引用 Google 从未记录过的东西。
作为对比,Finexly 每分钟刷新一次汇率并按需返回,因此新鲜度取决于你何时调用,而不是取决于电子表格什么时候想重算。如果你想从整体上理解各家服务商所说的"实时"到底是什么意思,请参阅汇率 API 的数据从哪里来。
没人引用的使用限制
这是本文最重要的一段,而它没有出现在任何一篇排名靠前的教程里。以下逐字引自 Google 的 GOOGLEFINANCE 帮助页面:
"使用限制: 本数据不适用于金融行业专业人士使用,也不适用于非金融机构(包括政府机构)中其他专业人士的使用。专业用途可能需要向第三方数据提供方另行支付授权费用。"
以及 Google Finance 的免责声明:
"您同意,未事先取得书面同意,不得复制、修改、重新编排格式、下载、存储、再现、再加工、传输或再分发此处的任何数据或信息,也不得将任何此类数据或信息用于商业企业。"
"Google 无法保证所显示汇率的准确性。在进行任何可能受汇率变动影响的交易之前,您应先确认当前汇率。"
对照一下你的表格实际在做什么。给客户发票定价、为财务团队换算供应商账单,或是把换算后的收入数字导出到你要发给客户的报告里——这些显然算不上"仅供参考"。一个有授权的汇率 API 能彻底消除这个问题,而这才是财务团队弃用 GOOGLEFINANCE 的真正原因,而非单纯的精度问题。
还有一堵硬墙
"历史数据无法通过 Sheets API 或 Apps Script 下载或访问。若尝试这样做,你会在电子表格的相应单元格中看到 #N/A 错误,而不是数值。"
只要你想自动化任何东西——每晚导出、用 Sheets API 拉取数据、写脚本给汇率做快照——GOOGLEFINANCE 的历史数据在设计上就被排除在外。这堵墙正是把人们推向方法二的原因。
方法二:由货币 API 驱动的 Apps Script 自定义函数
自定义函数就是写在 Apps Script 里的一个 JavaScript 函数,你可以像调用任何内置函数一样在单元格中调用它。Google 明确确认它可以发起网络请求:自定义函数"只能调用无法访问个人数据的服务",而 URL Fetch 就在允许的名单上。
第 1 步:获取 API 密钥并妥善存放
在 Finexly 控制台领取一个免费密钥——每月 1,000 次请求,无需信用卡。然后,这一点很关键:不要把它硬编码进代码。两篇被引用最多的 Google Sheets 汇率教程,直接把 API 密钥粘贴到了脚本里——而这个文件会随电子表格一起,流向每一个你分享给的人。
在 Apps Script 编辑器中(扩展程序 → Apps Script),从编辑器里运行一次下面的代码,把密钥存进脚本属性:
function storeApiKey() {
PropertiesService.getScriptProperties()
.setProperty('FINEXLY_API_KEY', 'YOUR_API_KEY');
}之后请把文件中的字面量删掉。注意官方给出的限制:单个属性值上限为 9 KB,整个属性存储上限为 500 KB——存一个密钥绰绰有余,但它不是用来缓存数据的地方。
第 2 步:单货币对的自定义函数
/**
* Returns the live exchange rate for a currency pair.
*
* @param {string} from Base currency code, e.g. "USD".
* @param {string} to Quote currency code, e.g. "EUR".
* @return The exchange rate.
* @customfunction
*/
function FX_RATE(from, to) {
if (!from || !to) throw new Error('Both currency codes are required.');
var pair = String(from).toUpperCase() + '_' + String(to).toUpperCase();
var cache = CacheService.getScriptCache();
var hit = cache.get(pair);
if (hit !== null) return Number(hit);
var key = PropertiesService.getScriptProperties().getProperty('FINEXLY_API_KEY');
var res = UrlFetchApp.fetch(
'https://api.finexly.com/v1/rate?from=' + encodeURIComponent(from) +
'&to=' + encodeURIComponent(to),
{
headers: { Authorization: 'Bearer ' + key },
muteHttpExceptions: true
}
);
var code = res.getResponseCode();
var body = JSON.parse(res.getContentText());
if (code !== 200) {
throw new Error(body.error ? body.error.code + ': ' + body.error.message : 'HTTP ' + code);
}
cache.put(pair, String(body.rate), 300); // 5 minutes
return body.rate;
}在单元格中这样使用:
=FX_RATE("USD","EUR")
=A2 * FX_RATE($B$1, $C$1)/v1/rate 端点返回 {"pair": "USD_EUR", "rate": 0.9215}。完整的参数说明见 Finexly API 文档。
第 3 步:批量处理整个区域——所有竞品都漏掉的部分
问题出在这里。Google 有直接的文档说明:
"每当电子表格中使用一次自定义函数,Sheets 就会向 Apps Script 服务器发起一次单独的调用。如果你的电子表格包含数十次(甚至数百、数千次!)自定义函数调用,这个过程可能会很慢。"
把 FX_RATE 往下拖 400 行,你就发起了 400 次往返请求。自定义函数有每次执行 30 秒的上限,因此你会在耗尽 API 配额之前很久,就先撞上 #ERROR! 以及那句提示 Exceeded maximum execution time (line 0).。
Google 推荐的解决办法是数组批处理:接收一个区域,返回一个数组。Finexly 的 /v1/convert 端点支持以逗号分隔的货币对,于是整整一列只需一次 HTTP 请求:
/**
* Converts a column of amounts from one currency to another in a single API call.
*
* @param {A2:A400} amounts Range of amounts.
* @param {string} from Base currency code.
* @param {string} to Quote currency code.
* @return {Array} Converted amounts.
* @customfunction
*/
function FX_CONVERT_RANGE(amounts, from, to) {
var rate = FX_RATE(from, to); // one fetch, then cached
var rows = Array.isArray(amounts) ? amounts : [[amounts]];
return rows.map(function (row) {
return row.map(function (v) {
return (v === '' || v === null) ? '' : Number(v) * rate;
});
});
}=FX_CONVERT_RANGE(A2:A400, "USD", "EUR")一条公式、一次网络调用,少了 399 次往返。如果你一次需要多个货币对,请调用 /v1/convert?q=USD_EUR,USD_GBP,USD_JPY 并读取 body["USD_EUR"].rate。
Google 对自定义函数中的缓存究竟怎么说
上面用到了 CacheService,但要说清楚用它的理由。Google 的自定义函数指南把 Cache 服务评价为"可以用,但在自定义函数中并不特别有用"。缓存的优势只在不同的执行之间才真正成立——也就是对同一个货币对的反复重算——而不是在同一次溢出数组的内部。数组批处理才是有文档背书的优化手段;缓存只是有用的辅助。
官方给出的缓存限制:键最长 250 个字符,值最大 100 KB,条目上限 1,000 条,过期时间介于 1 秒到 21,600 秒(6 小时)之间,默认 600 秒。缓存调用没有公布每日配额。
还有一个有文档记载的陷阱,因为这个"偏方"流传甚广:把 NOW() 作为参数传进去以强制刷新,会让函数失效。Google 的说法是:"自定义函数的参数必须是确定性的……如果自定义函数试图返回基于这些易变内置函数的值,它会一直显示 Loading...。"
方法三:计划汇率表(生产环境的做法)
自定义函数什么时候重算全看 Sheets 的心情,而这恰恰是别人要读的表格最不该有的特性。稳健的做法是彻底不再从单元格发起请求:按计划把汇率写进一张表,然后用普通公式去查它。
刷新脚本
var PAIRS = ['USD_EUR', 'USD_GBP', 'USD_JPY', 'USD_CAD', 'USD_AUD', 'USD_CHF'];
function refreshRates() {
var key = PropertiesService.getScriptProperties().getProperty('FINEXLY_API_KEY');
var res = UrlFetchApp.fetch(
'https://api.finexly.com/v1/convert?q=' + PAIRS.join(','),
{ headers: { Authorization: 'Bearer ' + key }, muteHttpExceptions: true }
);
if (res.getResponseCode() !== 200) {
console.error('Finexly refresh failed: ' + res.getContentText());
return; // keep yesterday's rates rather than blanking the sheet
}
var data = JSON.parse(res.getContentText());
var stamp = new Date();
var rows = PAIRS.map(function (p) {
return [p, p.split('_')[0], p.split('_')[1], data[p].rate, stamp];
});
var sheet = SpreadsheetApp.getActive().getSheetByName('Rates') ||
SpreadsheetApp.getActive().insertSheet('Rates');
sheet.clear();
sheet.getRange(1, 1, 1, 5)
.setValues([['Pair', 'From', 'To', 'Rate', 'Updated (UTC)']])
.setFontWeight('bold');
sheet.getRange(2, 1, rows.length, 5).setValues(rows);
}有两个细节把它和你在别处能找到的脚本区分开来。第一,只调用一次 setValues(),而不是逐个单元格调用——批量区域写入要快得多。第二,请求失败时直接提前返回,而不是清空表格,这样服务商偶尔抖动一下,留下的是"过时但有时间戳标注"的汇率,而不是一整张空白表——后者会悄悄把下游所有合计变成零。
创建触发器
function installTrigger() {
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === 'refreshRates') ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger('refreshRates').timeBased().everyHours(1).create();
}Google 的文档指出,定时触发器的运行频率"最密可以每分钟一次,最疏可以每月一次",而且触发时刻是被刻意模糊化的:"如果你创建一个每天上午 9 点的重复触发器,Apps Script 会在 9 点到 10 点之间挑一个时间。" 如果使用 everyMinutes(n),n 必须是 1、5、10、15 或 30——其他值一律不接受。
读取汇率
=XLOOKUP("USD_EUR", Rates!A:A, Rates!D:D)
=A2 * XLOOKUP($B$1 & "_" & $C$1, Rates!A:A, Rates!D:D)现在整个工作簿里的每一次换算,都能从本地单元格瞬间取值、使用同一个一致的汇率,并且带有可见的时间戳。最后这一点,正是审计人员会问的。
你真的会撞上的配额与限制
以下是 Google 公布的 Apps Script 数字——在围绕它们做设计之前,值得先了解清楚:
| 限制项 | 个人账号(gmail.com) | Google Workspace 账号 |
|---|---|---|
| URL Fetch 调用次数 | 20,000 次/天 | 100,000 次/天 |
| 触发器总运行时长 | 90 分钟/天 | 6 小时/天 |
| 属性读写次数 | 50,000 次/天 | 500,000 次/天 |
| 自定义函数运行时长 | 30 秒/次执行 | 30 秒/次执行 |
| 脚本运行时长 | 6 分钟/次执行 | 6 分钟/次执行 |
| 每个用户每个脚本的触发器数 | 20 | 20 |
| URL Fetch 的 URL 长度 | 2 KB/次调用 | 2 KB/次调用 |
?q= 批量请求里塞多少个货币对——轻松超过一百个,但并非无限。在 API 这一侧,Finexly 的免费套餐允许每月 1,000 次请求,速率为每分钟 10 次。一个每小时执行的触发器每月大约消耗 730 次请求——在免费额度之内,而且还有富余。再加上少量临时的自定义函数调用,你可能就会想升级到 Starter 套餐;当前的各档位详见定价页面。请注意,历史汇率需要付费套餐,所以如果你既需要回溯日期的汇率又没有预算,那么对于个人的、非专业的用途,GOOGLEFINANCE 仍然是诚实的答案。
每个响应都带有 X-RateLimit-Limit、X-RateLimit-Used 和 X-RateLimit-Units 这几个头部,因此你可以从 res.getAllHeaders() 里记录用量,提前预见限流的到来。
常见错误及其解决办法
| 你看到的现象 | 原因 | 解决办法 |
|---|---|---|
GOOGLEFINANCE 返回 #N/A | 货币对不受支持或拼写有误,或是通过 Apps Script / Sheets API 请求了历史数据 | 检查两个 ISO 代码;若要自动化,请改用 API |
#ERROR! 并提示 "Exceeded maximum execution time (line 0)." | 单独的自定义函数调用次数太多 | 改用批处理的 FX_CONVERT_RANGE 写法 |
一直显示 Loading... | 把 NOW()、RAND() 或其他易变函数当作自定义函数的参数传入 | 删掉它——Google 明确记录了这是不支持的 |
401 UNAUTHORIZED / invalid token | API 密钥缺失或错误 | 确认脚本属性已设置,且请求头为 Bearer <key> |
429 RATE_LIMIT_EXCEEDED | 触及每分钟或每月上限 | 延长缓存 TTL、每次调用批量处理更多货币对,或升级套餐 |
You do not have permission to call X service. | 自定义函数调用了需要授权的服务 | 把那部分逻辑挪到由触发器运行的函数里 |
| 历史公式溢出了多余的列 | 属于官方记载的行为——历史结果以带表头的数组形式返回 | 用 INDEX(..., 2, 2) 包一层 |
你该用哪种方法?
- 个人表格、快速查询、不涉及钱: 用
GOOGLEFINANCE。免费、即时,而且授权限制影响不到你。 - 实际工作表中的一列换算: 用批处理版的 Apps Script 自定义函数。数据源可预期、授权上没有歧义、每个区域只需一次调用。
- 任何同事、客户或审计人员会读的东西: 用计划汇率表。每个刷新周期一个汇率、一个可见的时间戳,也不必依赖 Sheets 什么时候想重算。
改用 Excel 的话?对应的方法——Power Query、WEBSERVICE 以及"货币"数据类型——在如何在 Excel 中获取实时汇率一文中有介绍。如果你还在评估服务商,我们的货币 API 对比列出了各家差异,你也可以用 Finexly 货币换算器来核对任何汇率。
常见问题
如何在 Google Sheets 中免费获取实时汇率?
=GOOGLEFINANCE("CURRENCY:USDEUR") 不花一分钱,也不需要任何配置。但要注意 Google 声明的使用限制——该数据"不适用于金融行业专业人士使用,也不适用于非金融机构中其他专业人士的使用"——以及历史数值无法通过 Apps Script 或 Sheets API 读取。若想要零成本的正规授权替代方案,每月 1,000 次请求的 API 免费额度足以轻松覆盖每小时一次的刷新(约 730 次调用)。
GOOGLEFINANCE 多久更新一次汇率?
Google Finance 免责声明中的表格列出,货币数据的延迟为 3 分钟,数据源为 Morningstar——而不是网上普遍流传的 20 分钟,后者是挂在 "price" 属性上的通用证券报价数字。另外,Google 从未公布过 Sheets 多久重新计算一次公式,所以你单元格里那个数字有多"旧",是靠不住的。
我能在 Apps Script 里使用 GOOGLEFINANCE 吗?
历史数据不行。Google 明确表示:"历史数据无法通过 Sheets API 或 Apps Script 下载或访问。若尝试这样做,你会看到 #N/A 错误。" 此外,任何 Apps Script 服务中都不存在 GOOGLEFINANCE 方法——脚本只能读取某个单元格已经算好的值。如果你需要在代码里拿到汇率,请用 UrlFetchApp 调用货币 API。
为什么我的 Google Sheets 汇率公式很慢或显示 #ERROR!?
每一次自定义函数调用都是一次前往 Apps Script 服务器的独立往返,而每次执行的上限是 30 秒。数百次单独调用必然超时。请改成接收一个区域并返回一个数组,让一次调用覆盖整列;把汇率缓存起来;至于任何需要按计划执行的事情,把请求从单元格挪进定时触发器里。
我能在 Google Sheets 中获取历史汇率吗?
可以,有两种办法。GOOGLEFINANCE("CURRENCY:USDEUR", "close", DATE(2026,1,1), DATE(2026,1,31), "DAILY") 会返回一组日度序列——记得用 "close",因为 "price" 不是合法的历史属性;同时别忘了 Google 把日期当作 UTC 正午处理,这可能让某个值偏移一天。另一种办法是用付费套餐从 API 拉取历史汇率,再用触发器写入表格——这也是唯一能扛住自动化的路子。
准备好把有正规授权、每分钟刷新的汇率放进你的电子表格了吗?领取你的免费 Finexly API 密钥——无需信用卡。从每月 1,000 次请求、覆盖 170 多种货币起步,等表格规模超出额度时再升级。
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →