如何将 CSV 和 JSON 导入 Firestore
Firebase 控制台只能用添加文档(Add document)一次添加一个 Firestore 文档,没有加载文件的按钮。Google 自己的导入工具 gcloud firestore import 只读取 Firestore 自己的导出文件,不读取 CSV 或 JSON。要把文件导入集合,有三种方法。哪一种合适,取决于文件的大小和来源。
更新于 2026年10月21日。 方法 1 中的扩展程序 Firexport 是我们开发的。方法 2 和 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 账号写入,因此无需设置服务账号密钥,也无需编写代码。
- 从 Chrome 应用商店安装 Firexport。
- 在 Firebase 控制台中,以面板视图(Panel view)打开 Firestore Database > 数据(Data),然后点击要导入的集合。
- 点击添加文档(Add document)行右端的导入图标。
- 选择或拖放一个 CSV、JSON 或 JSON Lines 文件,或者切换到粘贴文本并点击读取。
- 检查每个字段的类型以及文档 ID 的来源,并选择文档已存在时如何处理。
- 第一次使用时,窗口会要求你连接 Google 账号:选择能写入该项目的账号,并在 Google 的窗口中允许访问。然后点击导入。


字段类型和文档 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 永远不会与现有文档重复。

每个文档计为一次 Firestore 写入,费用计入该项目。Firexport 每秒最多发送 500 次写入,并会重试因临时原因失败的写入。所有行都写入后,窗口会关闭并显示一条简短的通知;如果有失败的行,它会列出这些行,提供 CSV 报告,并且可以继续导入。写入使用所连接账号的权限,因此不受安全规则约束。你的文件从浏览器直接发送到 Google,从不发送给我们。文件和粘贴的文本最大可达 100 MB。
不用脚本、不用服务账号密钥,直接从控制台把 CSV 或 JSON 文件导入 Firestore。
2. 用 Admin SDK 脚本导入
如果文件超过 100 MB、导入需要按计划运行,或者需要按自己的方式转换,一段使用 Firebase Admin SDK 的 Node.js 脚本可以自行写入这些行。下面的脚本可读取带标题行的 CSV、JSON 数组、以文档 ID 为键的 JSON 对象或 JSON Lines,并为每一行写入一个文档。它需要一个能写入 Firestore 的账号:项目 Owner 或 Editor,或者 Cloud Datastore User 角色。
- 安装 SDK 和一个 CSV 解析器:
npm install firebase-admin csv-parse - 登录以获取本地凭据:
gcloud auth application-default login。在服务器上,改为让GOOGLE_APPLICATION_CREDENTIALS指向服务账号密钥。 - 把下面的脚本保存为
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 方案中,超出免费额度的写入按数据库所在位置的费率计费。
