返回博客

如何在 Google Sheets 中获取实时汇率(GOOGLEFINANCE、Apps Script 与货币 API)

V
Vlado Grigirov
August 25, 2026
Currency API Exchange Rates Google Sheets Apps Script Tutorial Finexly

如何在 Google Sheets 中获取实时汇率(GOOGLEFINANCE、Apps Script 与货币 API)

Google Sheets 中获取实时汇率看起来是个早已解决的问题。你输入 =GOOGLEFINANCE("CURRENCY:USDEUR"),一个数字冒出来,然后你就继续做别的事了。用来算个人旅行预算,这完全够用。但只要这张表被用来生成发票、跑工资、出具收入报表,或者任何同事会据此做决策的场景,它就不灵了——原因是一条授权限制、一个有文档记载的日期怪癖,以及一个几乎没有教程提到的 Apps Script 限制。

本指南涵盖全部三种可行方案——内置的 GOOGLEFINANCE 函数、由货币 API 驱动的 Apps Script 自定义函数,以及由定时触发器刷新的计划汇率表——并附上真实的配额数字、Google 官方警示的原文措辞,以及可直接运行的代码。下文中每一条关于 Google 行为的说法都引自 Google 自己的文档,而每一条没有官方文档支撑的说法都会明确标注出来。


把汇率拉进 Google Sheets 的三种方式

GOOGLEFINANCEApps 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 说得很清楚:"若指定了任何日期参数,该请求即被视为历史数据请求,此时只允许使用历史属性。" 历史属性包括 openclosehighlowvolumeall——"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 分钟/次执行
每个用户每个脚本的触发器数2020
URL Fetch 的 URL 长度2 KB/次调用2 KB/次调用
那个 2 KB 的 URL 上限,实际决定了你能往单次 ?q= 批量请求里塞多少个货币对——轻松超过一百个,但并非无限。

在 API 这一侧,Finexly 的免费套餐允许每月 1,000 次请求,速率为每分钟 10 次。一个每小时执行的触发器每月大约消耗 730 次请求——在免费额度之内,而且还有富余。再加上少量临时的自定义函数调用,你可能就会想升级到 Starter 套餐;当前的各档位详见定价页面。请注意,历史汇率需要付费套餐,所以如果你既需要回溯日期的汇率没有预算,那么对于个人的、非专业的用途,GOOGLEFINANCE 仍然是诚实的答案。

每个响应都带有 X-RateLimit-LimitX-RateLimit-UsedX-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 tokenAPI 密钥缺失或错误确认脚本属性已设置,且请求头为 Bearer <key>
429 RATE_LIMIT_EXCEEDED触及每分钟或每月上限延长缓存 TTL、每次调用批量处理更多货币对,或升级套餐
You do not have permission to call X service.自定义函数调用了需要授权的服务把那部分逻辑挪到由触发器运行的函数里
历史公式溢出了多余的列属于官方记载的行为——历史结果以带表头的数组形式返回INDEX(..., 2, 2) 包一层
关于生产环境中的重试、退避和缓存失效等更深入的做法,请参阅我们的货币 API 缓存与错误处理指南


你该用哪种方法?

  • 个人表格、快速查询、不涉及钱: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 多种货币起步,等表格规模超出额度时再升级。

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 →