Firexport指南

如何将 CSV 和 JSON 导入 Firestore

Firebase 控制台只能用添加文档(Add document)一次添加一个 Firestore 文档,没有加载文件的按钮。Google 自己的导入工具 gcloud firestore import 只读取 Firestore 自己的导出文件,不读取 CSV 或 JSON。要把文件导入集合,有三种方法。哪一种合适,取决于文件的大小和来源。

更新于 2026年10月21日。 方法 1 中的扩展程序 Firexport 是我们开发的。方法 2 和 3 不需要它。

简短回答

  • 现在就要把一个 CSV 或 JSON 文件导入一个集合: 用浏览器扩展程序从控制台的面板视图(Panel view)导入。 方法 1
  • 非常大的文件、按计划运行的任务,或需要按自己的方式转换: 一段简短的 Admin SDK 脚本。 方法 2
  • 来自另一个项目或备份的整个数据库: 用 gcloud 托管导出和导入(它不读取 CSV 或 JSON)。 方法 3
1. 控制台 + 扩展程序2. Admin SDK 脚本3. gcloud 托管导入
准备工作安装一个 Chrome 扩展程序,连接你的 Google 账号Node.js 和 Google Cloud 凭据Cloud Storage 存储桶和 gcloud
可读取CSV、JSON、JSON Lines 或粘贴的文本CSV、JSON、JSON Lines(取决于你的代码读取什么)只有 Firestore 自己的导出文件
字段类型按字段选择,附预览由你的代码转换沿用导出中的类型
已有文档保留、替换或合并创建、覆盖或合并被覆盖
需要启用结算不需要不需要需要(Blaze 方案)

1. 用 Firexport 从 Firebase 控制台导入

Firexport 是一款免费的 Chrome 扩展程序,会在 Firestore 的面板视图中每个集合的添加文档(Add document)行右端添加一个导入图标。它使用你连接的 Google 账号写入,因此无需设置服务账号密钥,也无需编写代码。

  1. 从 Chrome 应用商店安装 Firexport。
  2. 在 Firebase 控制台中,以面板视图(Panel view)打开 Firestore Database > 数据(Data),然后点击要导入的集合。
  3. 点击添加文档(Add document)行右端的导入图标。
  4. 选择或拖放一个 CSV、JSON 或 JSON Lines 文件,或者切换到粘贴文本并点击读取。
  5. 检查每个字段的类型以及文档 ID 的来源,并选择文档已存在时如何处理。
  6. 第一次使用时,窗口会要求你连接 Google 账号:选择能写入该项目的账号,并在 Google 的窗口中允许访问。然后点击导入。
Firebase 控制台中 Firestore 的面板视图,以及 Firexport 在“添加文档”行末端添加的导入图标
导入图标位于你要填充的集合中“添加文档”的右端。
读取 CSV 后的导入窗口:三个文档、八个字段,每个字段都选好了类型
每一列都会成为一个字段,并带有建议的类型。Document ID 列会成为文档的 ID。

字段类型和文档 ID

  • CSV 的单元格是文本,JSON 也没有时间戳或引用。所以 Firexport 会根据每个字段的值为它建议一个类型:字符串、整数、数字 (double)、布尔值、时间戳、引用、地理位置或字节 (base64),或者不导入。只有当所有值都符合某个类型时,它才会建议该类型,而且每一个你都可以更改。
  • 时间戳可以是 ISO 8601(2026-10-01T09:30:00Z)、按浏览器时区解读的 2026-10-01 09:30:00,或毫秒数。引用可以是 users/u1 这样的路径。映射和数组中的值保持各自的 JSON 类型,所以嵌套数据应放在 JSON 中:CSV 的一列始终是一个顶级字段。
  • 文档 ID 可以来自你选择的一列、JSON 对象的键,也可以是新的随机 ID。名为 Document ID 的列会被自动选中,所以 Firexport 导出的文件可以原样导入。以路径形式写的 ID(如 /users/u1)使用最后一段。
  • 预览会按写入时的样子显示第一个文档,以及每个值的类型。无法写入的行,例如值不符合其类型或 ID 重复,会在发送任何内容之前被列出并排除。

文档已存在时

保留原文档是默认选项,只创建新文档:ID 已存在的行会失败并被列出。替换让文档只包含文件中的字段。合并写入文件中的字段(包括映射内部),并保留文档的其他字段。新的随机 ID 永远不会与现有文档重复。

针对已有文档的三个选项、第一个文档及其类型的预览,以及“导入 3 个文档”按钮
开始之前,窗口会告诉你文档将写入哪里。

每个文档计为一次 Firestore 写入,费用计入该项目。Firexport 每秒最多发送 500 次写入,并会重试因临时原因失败的写入。所有行都写入后,窗口会关闭并显示一条简短的通知;如果有失败的行,它会列出这些行,提供 CSV 报告,并且可以继续导入。写入使用所连接账号的权限,因此不受安全规则约束。你的文件从浏览器直接发送到 Google,从不发送给我们。文件和粘贴的文本最大可达 100 MB。

不用脚本、不用服务账号密钥,直接从控制台把 CSV 或 JSON 文件导入 Firestore。

添加到 Chrome — 免费

2. 用 Admin SDK 脚本导入

如果文件超过 100 MB、导入需要按计划运行,或者需要按自己的方式转换,一段使用 Firebase Admin SDK 的 Node.js 脚本可以自行写入这些行。下面的脚本可读取带标题行的 CSV、JSON 数组、以文档 ID 为键的 JSON 对象或 JSON Lines,并为每一行写入一个文档。它需要一个能写入 Firestore 的账号:项目 Owner 或 Editor,或者 Cloud Datastore User 角色。

  1. 安装 SDK 和一个 CSV 解析器:npm install firebase-admin csv-parse
  2. 登录以获取本地凭据:gcloud auth application-default login。在服务器上,改为让 GOOGLE_APPLICATION_CREDENTIALS 指向服务账号密钥。
  3. 把下面的脚本保存为 import-to-firestore.mjs,替换 your-project-id,把 TYPES 设为你的字段,然后运行 node import-to-firestore.mjs orders.csv orders。
// Usage: node import-to-firestore.mjs orders.csv orders   → one document per row in orders
// Reads .csv (with a header row), .json (an array, or an object keyed by document ID) and .jsonl.
import { readFileSync } from 'node:fs';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
import { parse } from 'csv-parse/sync';

initializeApp({ projectId: 'your-project-id' });
const db = getFirestore();
const [file, collection] = process.argv.slice(2);
if (!file || !collection) throw new Error('Usage: node import-to-firestore.mjs <file> <collection>');

// The column or key that holds each document's ID. Rows without one get a random ID.
const ID_FIELD = 'Document ID';
// CSV cells are all text and JSON has no timestamps: list the fields to convert. The rest stay as they are.
const TYPES = { createdAt: 'timestamp', items: 'integer', total: 'number' };
// 'create' never touches an existing document (that row fails), 'set' replaces it, 'merge' adds to it.
const MODE = 'create';

const text = readFileSync(file, 'utf8');
const isCsv = file.endsWith('.csv');
let rows;
if (isCsv) {
  rows = parse(text, { columns: true, bom: true, skip_empty_lines: true });
} else if (file.endsWith('.jsonl')) {
  rows = text.split('\n').filter((line) => line.trim()).map((line) => JSON.parse(line));
} else {
  const json = JSON.parse(text);
  rows = Array.isArray(json) ? json : Object.entries(json).map(([id, data]) => ({ [ID_FIELD]: id, ...data }));
}

function convert(type, value) {
  if (type === 'timestamp') {
    const date = new Date(/^\d+$/.test(value) ? Number(value) : value); // ISO 8601 or milliseconds
    if (Number.isNaN(date.getTime())) throw new Error(`not a timestamp: ${value}`);
    return Timestamp.fromDate(date);
  }
  if (type === 'integer' || type === 'number') {
    const number = Number(value);
    if (value === '' || !Number.isFinite(number) || (type === 'integer' && !Number.isInteger(number))) {
      throw new Error(`not ${type === 'integer' ? 'an integer' : 'a number'}: ${value}`);
    }
    return number;
  }
  if (type === 'boolean') return value === true || value === 'true';
  return value;
}

// Convert every row before writing anything, so a bad value stops the import instead of half of it.
const docs = rows.map((row, index) => {
  const data = {};
  for (const [field, value] of Object.entries(row)) {
    if (field === ID_FIELD || (isCsv && value === '')) continue; // an empty CSV cell is a missing field
    try {
      data[field] = TYPES[field] ? convert(TYPES[field], value) : value;
    } catch (error) {
      throw new Error(`Row ${index + 1}, ${field}: ${error.message}`);
    }
  }
  const id = row[ID_FIELD] ? String(row[ID_FIELD]).split('/').pop() : undefined; // a path keeps its last part
  return { row: index + 1, ref: id ? db.collection(collection).doc(id) : db.collection(collection).doc(), data };
});

// BulkWriter sends the writes in batches, keeps under Firestore's rate limits and retries what can be retried.
const writer = db.bulkWriter();
const failures = [];
for (const { row, ref, data } of docs) {
  const write = MODE === 'create' ? writer.create(ref, data) : writer.set(ref, data, { merge: MODE === 'merge' });
  write.catch((error) => failures.push(`Row ${row} (${ref.id}): ${error.message}`));
}
await writer.close();
console.log(`Wrote ${docs.length - failures.length} of ${docs.length} documents to ${collection}`);
failures.forEach((failure) => console.log(failure));
  • CSV 的单元格是文本:脚本只转换 TYPES 中列出的字段,其余的保留为字符串。空单元格会让文档中缺少该字段。JSON 的值保持各自的类型,映射和数组按原样写入,但直接位于另一个数组中的数组除外,因为 Firestore 不接受这种数组。
  • 写入任何内容之前,每一行都会先完成转换,所以遇到错误的值时,脚本会停下并指出它所在的行和字段,而不是留下只导入了一半的数据。
  • 当 MODE 设为 'create' 时,已有文档保持不变,它们所在的行会被列为失败。'set' 会替换这些文档,'merge' 会添加文件中的字段。没有 ID 的行每次运行都会得到新的随机 ID,所以把脚本运行两次会让这些行重复。
  • BulkWriter 会批量写入,从每秒 500 次开始,按 Firestore 的 500/50/5 规则允许的速度逐步提高,并重试因临时原因失败的写入。集合可以是子集合路径,例如 users/u1/orders。与方法 1 一样,它会绕过安全规则,每个文档计为一次写入。

3. 用 gcloud 托管导入复制整个数据库

gcloud firestore import 是 Firestore 的托管导入,在 Google Cloud 控制台中 Firestore 的导入/导出页面上也能使用。它只读取 gcloud firestore export 以 Firestore 自己的格式写入 Cloud Storage 存储桶的内容,不读取 CSV 或 JSON。用它来恢复备份,或者把集合连同其类型一起从一个项目或数据库复制到另一个。

# 1. Export the collection from the source project.
gcloud firestore export gs://YOUR_BUCKET/orders-export --collection-ids=orders --project=source-project-id

# 2. Import that export into the destination project.
gcloud firestore import gs://YOUR_BUCKET/orders-export --project=destination-project-id
  • 必须启用结算(Blaze 方案)。导入按每个文档一次写入计费。
  • 导入会覆盖 ID 相同的文档,其他文档保持不变。它不会触发 Cloud Functions。
  • 你的账号需要 Owner、Cloud Datastore Owner 或 Cloud Datastore Import Export Admin 角色。要从另一个项目的存储桶导入,目标项目的 Firestore 服务代理 service-PROJECT_NUMBER@gcp-sa-firestore.iam.gserviceaccount.com 需要拥有该存储桶的访问权限。

常见问题

Firebase 控制台能把 CSV 或 JSON 文件导入 Firestore 吗?

不能。Firestore 的“数据”标签页只能用添加文档(Add document)一次添加一个文档,而 gcloud firestore import 只读取 Firestore 自己的导出文件。要加载 CSV 或 JSON 文件,需要使用 Admin SDK 脚本,或者像 Firexport 这样在控制台中添加导入功能的浏览器扩展程序。

如何把 JSON 文件导入 Firestore?

使用由对象组成的 JSON 数组(每个对象一个文档),或以文档 ID 为键的对象。在 Firexport 中,点击集合的添加文档(Add document)行上的导入图标并选择文件;映射和数组保持各自的 JSON 类型。使用 Admin SDK 时,读取文件并用 BulkWriter 写入每个对象,如上面的脚本所示。

时间戳和引用如何导入?

CSV 和 JSON 无法携带 Firestore 类型,所以文件中的时间戳在被转换之前只是文本。在 Firexport 中,把字段的类型设为时间戳、引用或地理位置时就会转换。在脚本中,请自行转换,例如使用 Timestamp.fromDate(new Date(value)) 或 db.doc(path)。

导入时安全规则会生效吗?

不会。Firexport 的导入、Admin SDK 和 gcloud 都使用某个账号的 IAM 权限写入,而安全规则只对客户端 SDK 生效。导入之前请检查数据和账号。

导入 Firestore 要花多少钱?

无论用哪种方法,每个导入的文档都计为一次写入。在免费的 Spark 方案中,这会占用每天 20,000 次写入的配额,超出后写入会失败,直到配额重置。在 Blaze 方案中,超出免费额度的写入按数据库所在位置的费率计费。