Python JSON 转 CSV — pandas 指南

·Backend Developer·审阅者Priya Sharma·发布日期

直接在浏览器中使用免费的 JSON转CSV,无需安装。

在线试用 JSON转CSV →

几乎每个数据管道最终都会遇到同一个步骤:API 返回 JSON,但下游消费者——电子表格、导入脚本、Redshift COPY 命令——需要 CSV。将 JSON 转换为 CSV(Python 实现)听起来微不足道,直到你遇到嵌套对象、键不一致或需要特殊处理的日期时间值。Python 提供了两条可靠的路径:内置的 json + csv 模块用于零依赖脚本,以及 pandas 用于嵌套展平和较大数据集——或者使用 在线 JSON 转 CSV 工具 进行快速一次性转换,无需任何代码。本指南端到端涵盖两种方法,附带可运行的 Python 3.8+ 示例。

  • csv.DictWriter 以零依赖将字典列表转换为 CSV——使用 json.load() 解析,然后调用 writeheader() + writerows()。
  • 在 Windows 上始终以 newline="" 打开 CSV 文件,以防止数据行之间出现空行。
  • pd.json_normalize() 在调用 to_csv() 之前将嵌套 JSON 展平为平坦 DataFrame——自动处理多层嵌套。
  • 向 DataFrame.to_csv() 传入 index=False——否则 pandas 会写入一个不需要的行号列。
  • 对于超过 500 MB 的文件,使用 ijson 进行流式 JSON 解析,结合 csv.DictWriter 实现恒定内存使用。

什么是 JSON 到 CSV 的转换?

JSON 到 CSV 的转换将 JSON 对象数组转换为表格格式,其中每个对象变为一行,每个键变为列标题。JSON 是层级结构——对象可以任意深度嵌套。CSV 是平坦结构——每个值都位于行列网格中。当每个对象共享相同的顶级键集合时,转换可以顺畅进行。嵌套对象、数组和不一致的键才是有趣之处。原始数据保持不变,只有结构发生改变。

Before · json
After · json
[{"order_id":"ord_91a3","total":149.99,"status":"shipped"},
 {"order_id":"ord_b7f2","total":34.50,"status":"pending"}]
order_id,total,status
ord_91a3,149.99,shipped
ord_b7f2,34.50,pending

csv.DictWriter — 不使用 pandas 将 JSON 转换为 CSV

csv 模块随每个 Python 安装一起提供。无需 pip install, 无需虚拟环境折腾。 csv.DictWriter 接受字典列表,将每个字典写为 CSV 行,将字典键映射到列标题。 fieldnames 参数控制列顺序和包含哪些键。

Python 3.8+ — 最简 json 转 csv 示例
import json
import csv

# 示例 JSON 数据——订单对象数组
json_string = """
[
  {"order_id": "ord_91a3", "product": "Wireless Keyboard", "quantity": 2, "unit_price": 74.99},
  {"order_id": "ord_b7f2", "product": "USB-C Hub", "quantity": 1, "unit_price": 34.50},
  {"order_id": "ord_c4e8", "product": "Monitor Stand", "quantity": 3, "unit_price": 29.95}
]
"""

records = json.loads(json_string)

with open("orders.csv", "w", newline="", encoding="utf-8") as csvfile:
    writer = csv.DictWriter(csvfile, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

# orders.csv:
# order_id,product,quantity,unit_price
# ord_91a3,Wireless Keyboard,2,74.99
# ord_b7f2,USB-C Hub,1,34.50
# ord_c4e8,Monitor Stand,3,29.95

open() 中的 newline="" 参数在 Windows 上不是可选项。没有它,你会得到双回车——在 Excel 中每行数据之间显示为空行。在 macOS 和 Linux 上无害,所以始终加上它。

上面的代码对字符串使用 json.loads()。 从文件句柄读取时使用 json.load() (无尾部 s)。 这一点经常让人踩坑——一个读字符串,另一个读文件对象。

Python 3.8+ — 读取 JSON 文件,写入 CSV 文件
import json
import csv

with open("server_metrics.json", encoding="utf-8") as jf:
    metrics = json.load(jf)  # json.load() 用于文件对象

# 显式指定 fieldnames 控制列顺序
columns = ["timestamp", "hostname", "cpu_percent", "memory_mb", "disk_io_ops"]

with open("server_metrics.csv", "w", newline="", encoding="utf-8") as cf:
    writer = csv.DictWriter(cf, fieldnames=columns, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(metrics)

# 只有指定的五列出现,且顺序完全一致

设置 extrasaction="ignore" 会静默丢弃字典中不在 fieldnames 列表中的键。默认值是 "raise", 如果任何字典有意外的键会抛出 ValueError。 根据你对意外情况的容忍度选择合适的值。

注意:csv.DictWritercsv.writer:DictWriter 自动将字典键映射到列位置。csv.writer 将原始列表写为行——由你自己处理列排序。对于 JSON 转 CSV,DictWriter 几乎总是正确的选择,因为 JSON 记录本身就是字典。

Python 的 csv 模块内置了三种命名方言: excel (逗号分隔,CRLF 行尾——默认), excel-tab (制表符分隔,CRLF 行尾),以及 unix (LF 行尾,引用所有非数字字段)。将方言名称作为 dialect 参数传入 csv.DictWriter。 当目标系统有特殊引用或分隔符规则时,也可以用 csv.register_dialect() 定义自定义方言。对大多数 JSON 转 CSV 工作流而言, excel 方言是正确的,但当文件将被 awk sort 等 POSIX 工具处理时,切换到 unix

处理非标准类型:datetime、UUID 和 Decimal

API 返回的 JSON 通常包含 ISO 字符串形式的日期、连字符字符串形式的 UUID 以及浮点数形式的货币值。当你在写入 CSV 之前将这些值解析为 Python 对象进行处理时,需要将它们转回字符串。 csv 模块会对每个值调用 str(), 所以大多数类型可以直接使用。但 datetime 对象会产生杂乱的默认字符串表示,而 Decimal 值需要显式格式化以避免科学计数法。

Python 3.8+ — 写入 CSV 前预处理 datetime 和 Decimal
import json
import csv
from datetime import datetime, timezone
from decimal import Decimal
from uuid import UUID

# 模拟包含 Python 类型的已解析 API 响应
transactions = [
    {
        "txn_id": UUID("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
        "created_at": datetime(2026, 3, 15, 9, 30, 0, tzinfo=timezone.utc),
        "amount": Decimal("1249.99"),
        "currency": "USD",
        "merchant": "CloudHost Inc.",
    },
    {
        "txn_id": UUID("b2c3d4e5-f6a7-8901-bcde-f12345678901"),
        "created_at": datetime(2026, 3, 15, 14, 12, 0, tzinfo=timezone.utc),
        "amount": Decimal("87.50"),
        "currency": "EUR",
        "merchant": "DataSync GmbH",
    },
]

def prepare_row(record: dict) -> dict:
    """将非字符串类型转换为适合 CSV 的字符串。"""
    return {
        "txn_id": str(record["txn_id"]),
        "created_at": record["created_at"].isoformat(),
        "amount": f"{record['amount']:.2f}",
        "currency": record["currency"],
        "merchant": record["merchant"],
    }

with open("transactions.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["txn_id", "created_at", "amount", "currency", "merchant"])
    writer.writeheader()
    for txn in transactions:
        writer.writerow(prepare_row(txn))

# transactions.csv:
# txn_id,created_at,amount,currency,merchant
# a1b2c3d4-e5f6-7890-abcd-ef1234567890,2026-03-15T09:30:00+00:00,1249.99,USD,CloudHost Inc.
# b2c3d4e5-f6a7-8901-bcde-f12345678901,2026-03-15T14:12:00+00:00,87.50,EUR,DataSync GmbH

这里 prepare_row() 函数是正确的方法。与其试图让 csv.DictWriter 了解自定义类型,不如在写入之前将每条记录规范化为字符串。建议对 datetime 对象显式调用 .isoformat(), 而不是依赖 str()——输出格式更可预测,下游解析器也能可靠地处理 ISO 8601。

警告:如果让 Decimal 值未经格式化直接传递,极小或极大的数字可能会以科学计数法渲染(例如 1.5E+7)。 向 CSV 写入金融数据时,始终用显式 f-string(如 f"{value:.2f}")格式化 Decimal。

对于包含多种自定义类型的管道,另一种替代模式是扩展 json.JSONEncoder。 对其进行子类化,覆盖 default() 方法,为每种自定义类型返回 JSON 可序列化的值,然后将该子类作为 cls 参数传入 json.dumps()。 在写入 CSV 之前通过自定义编码器重新编码,可以一步规范化所有类型,无需逐行调用 prepare_row()。 上面展示的 prepare_row() 模式对于一次性脚本更简单;当同一个包含自定义类型的领域模型在多个管道阶段或微服务间共享时,JSONEncoder 子类方法扩展性更好。

csv.DictWriter 参数参考

完整的构造函数签名为 csv.DictWriter(f, fieldnames, restval="", extrasaction="raise", dialect="excel", **fmtparams)。 大多数参数都有合理的默认值。你实际会更改的是 fieldnames delimiter extrasaction

参数
类型
默认值
说明
f
file object
(必填)
任何具有 write() 方法的对象——通常来自 open()
fieldnames
sequence
(必填)
定义 CSV 输出中列顺序的键列表
restval
str
""
当字典缺少 fieldnames 中某个键时写入的值
extrasaction
str
"raise"
"raise" 对额外的键抛出 ValueError;"ignore" 静默丢弃
dialect
str / Dialect
"excel"
预定义格式规则——"excel"、"excel-tab" 或 "unix"
delimiter
str
","
分隔字段的单个字符——使用 "\t" 输出 TSV
quotechar
str
"
用于引用包含分隔符的字段的字符
quoting
int
csv.QUOTE_MINIMAL
控制何时应用引用——MINIMAL、ALL、NONNUMERIC、NONE
lineterminator
str
"\r\n"
每行后追加的字符串——可覆盖为 "\n" 以输出 Unix 风格

pandas — 使用 DataFrame 将 JSON 转换为 CSV

如果你已经在大量使用 pandas 的代码库中工作,或者你的 JSON 包含需要展平的嵌套对象,pandas 方案的代码量明显少于标准库版本。代价是:pandas 是约 30 MB 的依赖。对于临时脚本,这没问题。对于发布到生产的 Docker 镜像,标准库方案更轻量。

Python 3.8+ — pandas read_json 后接 to_csv
import pandas as pd

# 直接将 JSON 数组读入 DataFrame
df = pd.read_json("warehouse_inventory.json")

# 写入 CSV——index=False 防止自动生成的行号
df.to_csv("warehouse_inventory.csv", index=False)

# 就这样。两行代码。pandas 自动推断列类型。

index=False 标志是那种每次都需要查阅的东西。没有它,pandas 会将 0, 1, 2, ... 作为 CSV 的第一列写入。没有人希望这样。

使用 json_normalize 展平嵌套 JSON

真实的 API 响应很少是平坦的。订单包含收货地址,用户包含嵌套的偏好设置,遥测事件包含嵌套的元数据。 pd.json_normalize() 遍历嵌套字典,将它们展平为以点分隔名称的列。

Python 3.8+ — 使用 json_normalize 展平嵌套 JSON
import json
import pandas as pd

api_response = """
[
  {
    "order_id": "ord_91a3",
    "placed_at": "2026-03-15T09:30:00Z",
    "customer": {
      "name": "Sarah Chen",
      "email": "s.chen@example.com",
      "tier": "premium"
    },
    "shipping": {
      "method": "express",
      "address": {
        "city": "Portland",
        "state": "OR",
        "zip": "97201"
      }
    },
    "total": 299.95
  },
  {
    "order_id": "ord_b7f2",
    "placed_at": "2026-03-15T14:12:00Z",
    "customer": {
      "name": "James Park",
      "email": "j.park@example.com",
      "tier": "standard"
    },
    "shipping": {
      "method": "standard",
      "address": {
        "city": "Austin",
        "state": "TX",
        "zip": "73301"
      }
    },
    "total": 87.50
  }
]
"""

orders = json.loads(api_response)

# json_normalize 展平嵌套字典——sep 控制分隔符
df = pd.json_normalize(orders, sep="_")
df.to_csv("flat_orders.csv", index=False)

# 生成的列:
# order_id, placed_at, customer_name, customer_email, customer_tier,
# shipping_method, shipping_address_city, shipping_address_state,
# shipping_address_zip, total

sep="_" 参数控制嵌套键名的连接方式。默认值是 ".", 会产生如 customer.name 这样的列名。建议使用下划线,因为列名中的点会在 SQL 导入和某些电子表格公式中造成问题。

对于将记录数组包装在嵌套键下的 API 响应,使用 record_path 参数。如果响应类似 {"data": {"orders": [...]}}, 传入 record_path=["data", "orders"] 导航到正确的列表。可选的 meta 参数可以将父级字段与嵌套记录一起提取——当响应包含需要作为每行列的顶级分页信息(页码、总数)时很有用。结合使用 record_path meta, 无需自定义预处理即可处理大多数真实世界的嵌套 API 响应结构。

DataFrame.to_csv() 参数参考

DataFrame.to_csv() 有超过 20 个参数。以下是 JSON 转 CSV 工作流中最重要的几个。

参数
类型
默认值
说明
path_or_buf
str / Path / None
None
文件路径或缓冲区——None 将 CSV 作为字符串返回
sep
str
","
字段分隔符——使用 "\t" 输出 TSV
index
bool
True
将行索引写为第一列——几乎总是设为 False
columns
list
None
在输出中筛选并重排列
header
bool / list
True
写入列名——追加到现有文件时设为 False
encoding
str
"utf-8"
输出编码——在 Windows 上使用 "utf-8-sig" 以兼容 Excel
na_rep
str
""
缺失值(NaN、None)的字符串表示
quoting
int
csv.QUOTE_MINIMAL
控制字段何时被引用
Python 3.8+ — to_csv 常用参数覆盖示例
import pandas as pd

df = pd.read_json("telemetry_events.json")

# TSV 输出,指定编码和缺失值处理
df.to_csv(
    "telemetry_events.tsv",
    sep="\t",
    index=False,
    encoding="utf-8",
    na_rep="NULL",
    columns=["event_id", "timestamp", "source", "severity", "message"],
)

# 写入 stdout 以便在 shell 脚本中管道传输
print(df.to_csv(index=False))

# 作为字符串返回(不写入文件)
csv_string = df.to_csv(index=False)
print(len(csv_string), "characters")

从文件和 API 响应转换 JSON 到 CSV

两种最常见的真实场景:从磁盘上的文件读取 JSON 并转换,或从 HTTP API 获取 JSON 并将结果保存为 CSV。开发阶段可以不做错误处理,但在生产环境中这种选择会变成凌晨两点的告警。文件可能不存在,API 可能返回 4xx 或 5xx 状态码而非 JSON,响应体可能是错误对象而非数组,JSON 也可能因网络超时而被截断。下面的模式明确处理了所有这些情况,将错误记录到 stderr,并返回行数,以便调用方检测零行输出并及时告警。

磁盘文件——读取、转换、保存

Python 3.8+ — 带错误处理的 JSON 文件转 CSV
import json
import csv
import sys

def json_file_to_csv(input_path: str, output_path: str) -> int:
    """将包含对象数组的 JSON 文件转换为 CSV。
    返回写入的行数。
    """
    try:
        with open(input_path, encoding="utf-8") as jf:
            data = json.load(jf)
    except FileNotFoundError:
        print(f"Error: {input_path} not found", file=sys.stderr)
        return 0
    except json.JSONDecodeError as exc:
        print(f"Error: invalid JSON in {input_path}: {exc.msg} at line {exc.lineno}", file=sys.stderr)
        return 0

    if not isinstance(data, list) or not data:
        print(f"Error: expected a non-empty JSON array in {input_path}", file=sys.stderr)
        return 0

    # 收集所有记录中的唯一键——处理不一致的 schema
    all_keys: list[str] = []
    seen: set[str] = set()
    for record in data:
        for key in record:
            if key not in seen:
                all_keys.append(key)
                seen.add(key)

    with open(output_path, "w", newline="", encoding="utf-8") as cf:
        writer = csv.DictWriter(cf, fieldnames=all_keys, restval="", extrasaction="ignore")
        writer.writeheader()
        writer.writerows(data)

    return len(data)

rows = json_file_to_csv("deploy_logs.json", "deploy_logs.csv")
print(f"Wrote {rows} rows to deploy_logs.csv")

HTTP API 响应——获取并转换

Python 3.8+ — 从 API 获取 JSON 并保存为 CSV
import json
import csv
import urllib.request
import urllib.error

def api_response_to_csv(url: str, output_path: str) -> int:
    """从 REST API 端点获取 JSON 并写入 CSV。"""
    try:
        req = urllib.request.Request(url, headers={"Accept": "application/json"})
        with urllib.request.urlopen(req, timeout=30) as resp:
            if resp.status != 200:
                print(f"Error: API returned status {resp.status}")
                return 0
            body = resp.read().decode("utf-8")
    except urllib.error.URLError as exc:
        print(f"Error: could not reach {url}: {exc.reason}")
        return 0

    try:
        records = json.loads(body)
    except json.JSONDecodeError as exc:
        print(f"Error: API returned invalid JSON: {exc.msg}")
        return 0

    if not isinstance(records, list) or not records:
        print("Error: expected a non-empty JSON array from the API")
        return 0

    with open(output_path, "w", newline="", encoding="utf-8") as cf:
        writer = csv.DictWriter(cf, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)

    return len(records)

rows = api_response_to_csv(
    "https://api.internal.example.com/v2/deployments?status=completed",
    "completed_deployments.csv",
)
print(f"Exported {rows} deployments to CSV")
注意:上面的示例使用标准库中的 urllib 以保持脚本无外部依赖。如果你已安装 requests, 将 urllib 部分替换为 resp = requests.get(url, timeout=30); records = resp.json()—— 其余的 CSV 写入代码保持不变。

命令行 JSON 转 CSV

有时你只需要在终端执行一个单行命令。Python 的 -c 标志让你无需创建脚本文件即可快速转换。对于更复杂的转换,先通过 jq 重塑数据,再进行转换。

bash — JSON 转 CSV 单行命令
# Python 单行命令:从 stdin 读取 JSON,向 stdout 写入 CSV
cat orders.json | python3 -c "
import json, csv, sys
data = json.load(sys.stdin)
w = csv.DictWriter(sys.stdout, fieldnames=data[0].keys())
w.writeheader()
w.writerows(data)
"

# 将输出保存到文件
cat orders.json | python3 -c "
import json, csv, sys
data = json.load(sys.stdin)
w = csv.DictWriter(sys.stdout, fieldnames=data[0].keys())
w.writeheader()
w.writerows(data)
" > orders.csv
bash — 带 argparse 的独立 CLI 脚本
# 保存为 json2csv.py 并运行:python3 json2csv.py input.json -o output.csv
python3 -c "
import json, csv, argparse, sys

parser = argparse.ArgumentParser(description='Convert JSON array to CSV')
parser.add_argument('input', help='Path to JSON file')
parser.add_argument('-o', '--output', default=None, help='Output CSV path (default: stdout)')
parser.add_argument('-d', '--delimiter', default=',', help='CSV delimiter')
args = parser.parse_args()

with open(args.input) as f:
    data = json.load(f)

out = open(args.output, 'w', newline='') if args.output else sys.stdout
writer = csv.DictWriter(out, fieldnames=data[0].keys(), delimiter=args.delimiter)
writer.writeheader()
writer.writerows(data)
if args.output:
    out.close()
    print(f'Wrote {len(data)} rows to {args.output}', file=sys.stderr)
" "$@"
bash — 使用 jq + csvkit 进行复杂转换
# 安装 csvkit:pip install csvkit

# jq 展平并筛选字段,in2csv 处理 CSV 格式化
cat api_response.json | jq '[.[] | {id: .order_id, customer: .customer.name, total}]' | in2csv -f json > orders.csv

# Miller (mlr) 是另一个 JSON 转 CSV 的选择
mlr --json2csv cat orders.json > orders.csv

Miller(mlr) 是一个独立二进制工具,将 JSON、CSV 和 TSV 视为一等格式,无需 Python 运行时。 --json2csv 标志在单次扫描中将 JSON 输入转换为 CSV,你可以在写入输出之前将 Miller 动词链接起来对列进行过滤、排序或重命名。在 macOS 上通过 Homebrew 安装(brew install miller)或使用 Linux 包管理器。它在 CI 管道中特别有用,无需启动 Python 环境即可快速完成 JSON 转 CSV。

高性能替代方案——pandas 结合 pyarrow

对于数千万行规模的数据集,使用 pyarrow 后端的 pandas 读写速度明显快于默认方式。C 语言支持的 Arrow 引擎比 Python 的逐行 csv 模块更高效地处理列式数据。API 保持不变——你只需设置 engine 参数。

bash — 安装 pyarrow
pip install pyarrow
Python 3.8+ — 使用 pyarrow 的 pandas 进行更快的 CSV 写入
import pandas as pd

# 使用 pyarrow 引擎读取 JSON(大文件解析更快)
df = pd.read_json("sensor_readings.json", engine="pyarrow")

# to_csv 没有 engine 参数,但读写之间的 DataFrame 操作
# 得益于 pyarrow 的列式布局
df.to_csv("sensor_readings.csv", index=False)

# 对于真正大型的导出,考虑写入 Parquet 而非 CSV
# ——二进制格式,小 5-10 倍,保留类型
df.to_parquet("sensor_readings.parquet", engine="pyarrow")

如果你处理的 JSON 超过几百 MB,且最终消费者接受 Parquet,可以完全跳过 CSV。Parquet 更小,保留列类型,Redshift 和 BigQuery 都能原生加载。CSV 是有损格式——每个值都变为字符串。

终端输出与语法高亮

rich 库在终端中渲染带边框、对齐和颜色的表格——在开发过程中无需打开输出文件即可预览转换结果,非常实用。

bash — 安装 rich
pip install rich
Python 3.8+ — 使用 rich 在终端预览 CSV 输出
import json
from rich.console import Console
from rich.table import Table

json_string = """
[
  {"hostname": "web-prod-1", "cpu_percent": 72.3, "memory_mb": 3840, "uptime_hours": 720},
  {"hostname": "web-prod-2", "cpu_percent": 45.1, "memory_mb": 2560, "uptime_hours": 168},
  {"hostname": "db-replica-1", "cpu_percent": 91.7, "memory_mb": 7680, "uptime_hours": 2160}
]
"""

records = json.loads(json_string)
console = Console()

table = Table(title="Server Metrics Preview", show_lines=True)
for key in records[0]:
    table.add_column(key, style="cyan" if key == "hostname" else "white")

for row in records:
    table.add_row(*[str(v) for v in row.values()])

console.print(table)
# 在终端渲染带边框的彩色高亮表格
警告:Rich 仅用于终端显示。不要用它生成 CSV 文件——它会添加 ANSI 转义码,从而损坏输出。用 csv.DictWriter DataFrame.to_csv() 写入文件,仅将 rich 用于预览。

处理大型 JSON 文件

json.load() 将整个文件读入内存。对于 200 MB 的 JSON 文件,这意味着约 200 MB 的原始文本加上 Python 对象开销——堆内存使用轻松超过 500 MB。对于超过 100 MB 的文件,使用 ijson 流式读取输入,并边读边写入 CSV 行。

bash — 安装 ijson
pip install ijson

使用 ijson 流式将 JSON 数组转为 CSV

Python 3.8+ — 以恒定内存流式将大型 JSON 数组转为 CSV
import ijson
import csv

def stream_json_to_csv(json_path: str, csv_path: str) -> int:
    """不将全部内容加载到内存,直接将大型 JSON 数组转换为 CSV。"""
    with open(json_path, "rb") as jf, open(csv_path, "w", newline="", encoding="utf-8") as cf:
        # ijson.items 一次产出顶层数组的每个元素
        records = ijson.items(jf, "item")

        first_record = next(records)
        fieldnames = list(first_record.keys())

        writer = csv.DictWriter(cf, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerow(first_record)

        count = 1
        for record in records:
            writer.writerow(record)
            count += 1

    return count

rows = stream_json_to_csv("clickstream_2026_03.json", "clickstream_2026_03.csv")
print(f"Streamed {rows} records to CSV")

NDJSON / JSON Lines——每行一个对象

NDJSON(换行分隔 JSON),也称为 JSON Lines 或 .jsonl, 每行存储一个有效的 JSON 对象,没有包裹数组。这种格式在日志管道、事件流(Kafka、Kinesis)以及 Elasticsearch、BigQuery 等服务的批量导出中很常见。因为每行都是独立的 JSON 对象,你可以用普通的 Python for 循环遍历文件句柄处理 NDJSON 文件——无需 ijson 库。无论文件大小,内存保持恒定,使其成为源数据已是 JSON Lines 格式时最简单的流式方案。

Python 3.8+ — 逐行将 NDJSON 转换为 CSV
import json
import csv

def ndjson_to_csv(ndjson_path: str, csv_path: str) -> int:
    """逐行将换行分隔的 JSON 文件转换为 CSV。"""
    with open(ndjson_path, encoding="utf-8") as nf:
        first_line = nf.readline()
        first_record = json.loads(first_line)
        fieldnames = list(first_record.keys())

        with open(csv_path, "w", newline="", encoding="utf-8") as cf:
            writer = csv.DictWriter(cf, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerow(first_record)

            count = 1
            for line in nf:
                line = line.strip()
                if not line:
                    continue
                try:
                    record = json.loads(line)
                    writer.writerow(record)
                    count += 1
                except json.JSONDecodeError:
                    continue  # 跳过格式错误的行

    return count

rows = ndjson_to_csv("access_log.ndjson", "access_log.csv")
print(f"Converted {rows} log entries to CSV")
注意:当 JSON 文件超过 100 MB 时切换到流式处理。用 json.load() 加载 1 GB 的 JSON 数组可能消耗 3–5 GB RAM,原因是 Python 对象开销。使用 ijson 时,无论文件大小内存保持不变。 如果只需要快速转换小文件,直接粘贴到 JSON 转 CSV 工具 即可。

常见错误

open() 缺少 newline=''——Windows 上出现空行

问题: csv 模块写入 行尾。没有 newline='',Python 的文本模式在 Windows 上会再添加一个 ,产生双倍行距输出。

解决方案: 打开文件进行 CSV 写入时始终传入 newline=''。在 macOS/Linux 上无害。

Before · Python
After · Python
with open("output.csv", "w") as f:
    writer = csv.DictWriter(f, fieldnames=columns)
    writer.writeheader()
    writer.writerows(data)
# Windows 上每行数据之间出现空行
with open("output.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=columns)
    writer.writeheader()
    writer.writerows(data)
# 在所有平台上输出干净
pandas to_csv() 忘记 index=False

问题: 没有 index=False,pandas 会在前面添加一个自动递增的行号列(0, 1, 2, ...),用从未在原始 JSON 中出现过的数据污染 CSV。

解决方案: 向 to_csv() 传入 index=False。如果确实需要索引列,用 df.index.name = 'row_num' 显式命名。

Before · Python
After · Python
df = pd.read_json("events.json")
df.to_csv("events.csv")
# CSV 多了一列未命名列:,event_id,timestamp,...
# 开头的逗号会导致许多 CSV 解析器出错
df = pd.read_json("events.json")
df.to_csv("events.csv", index=False)
# 干净的 CSV:event_id,timestamp,...
records 键不一致时使用 records[0].keys()

问题: 如果 JSON 对象的键不同(某些记录有可选字段),使用第一条记录的键作为 fieldnames 会静默丢弃只在后续记录中出现的列。

解决方案: 在创建 DictWriter 之前收集所有记录中的唯一键。

Before · Python
After · Python
records = json.load(f)
writer = csv.DictWriter(out, fieldnames=records[0].keys())
# 遗漏了只在 records[2] 中出现的 "discount" 字段
records = json.load(f)
all_keys = list(dict.fromkeys(k for r in records for k in r))
writer = csv.DictWriter(out, fieldnames=all_keys, restval="")
# 每条记录中的每个键都作为列包含在内
未展平嵌套字典直接写入 CSV

问题: csv.DictWriter 对嵌套字典调用 str(),产生类似 "{'city': 'Portland'}"的列值——原始 Python repr,而非实际数据。

解决方案: 先用 pd.json_normalize() 或自定义展平函数展平嵌套对象。

Before · Python
After · Python
records = [{"id": "evt_1", "meta": {"source": "web", "region": "us-west"}}]
writer = csv.DictWriter(f, fieldnames=["id", "meta"])
writer.writerows(records)
# meta 列包含:{'source': 'web', 'region': 'us-west'}
import pandas as pd
records = [{"id": "evt_1", "meta": {"source": "web", "region": "us-west"}}]
df = pd.json_normalize(records, sep="_")
df.to_csv("events.csv", index=False)
# 列:id, meta_source, meta_region

csv.DictWriter 与 pandas——快速对比

方法
嵌套 JSON
自定义类型
流式处理
依赖项
需要安装
csv.DictWriter
✗(手动展平)
✓(逐行)
否(标准库)
csv.writer
✓(逐行)
否(标准库)
pd.DataFrame.to_csv()
✗(仅平坦)
✓(通过 dtypes)
pandas + numpy
pip install
pd.json_normalize() + to_csv()
✓(通过 dtypes)
pandas + numpy
pip install
csv.writer + json_flatten
flatten_json
pip install
jq + csvkit(CLI)
✓(通过 jq)
N/A
jq, csvkit
系统安装

在以下情况使用 csv.DictWriter: 零依赖、JSON 是平坦结构、脚本在受限环境中运行(CI 容器、Lambda 函数、嵌入式 Python)。 在以下情况使用 pd.json_normalize() + to_csv(): JSON 是嵌套结构、导出前需要转换或过滤数据,或已在 pandas 工作流中。 对于不能放入内存的文件,将 ijson csv.DictWriter 结合使用,实现恒定内存流式处理。

如需快速的无代码转换,ToolDeck 上的 JSON 转 CSV 工具 无需任何 Python 环境即可处理。

常见问题

如何在不使用 pandas 的情况下用 Python 将 JSON 转换为 CSV?

使用内置的 json 和 csv 模块。调用 json.load() 将 JSON 文件解析为字典列表,从第一个字典的键中提取 fieldnames,创建 csv.DictWriter,调用 writeheader(),再调用 writerows()。这种方式没有外部依赖,在任何 Python 3.x 环境中均可运行。对于小文件,它也比 pandas 更快,因为没有 DataFrame 分配的开销。如果 JSON 对象在各条记录中键不一致,在将 fieldnames 传入之前,先用 dict.fromkeys(k for r in records for k in r) 收集所有唯一键,以避免列缺失。

Python
import json
import csv

with open("orders.json") as f:
    records = json.load(f)

with open("orders.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

转换为 CSV 时如何处理嵌套 JSON?

平坦的 JSON 数组可以直接映射到 CSV 行,但嵌套对象需要先展平。使用 pandas 时,pd.json_normalize() 会自动处理——它用点分隔符连接嵌套键(例如 "address.city")。不使用 pandas 时,编写一个递归函数遍历字典并用分隔符拼接键名。对于多层深度嵌套结构,json_normalize 一次即可完成处理。sep 参数控制键段之间的连接字符——下划线通常比默认的点更安全,适用于 SQL 导入和电子表格公式兼容性。

Python
import pandas as pd

nested_data = [
    {"id": "ord_91a3", "customer": {"name": "陈晓明", "email": "chen.xiaoming@example.com"}},
]
df = pd.json_normalize(nested_data, sep="_")
# 列:id, customer_name, customer_email
df.to_csv("flat_orders.csv", index=False)

为什么我的 CSV 在 Windows 上数据行之间有空行?

csv 模块默认写入 \r\n 行尾。在 Windows 上,以文本模式打开文件会再添加一个 \r,产生 \r\r\n——在 Excel 中显示为空行。解决方法是始终向 open() 传入 newline=""。这告诉 Python 不要转换行尾,让 csv 模块自己处理。无论操作系统如何,这种模式都是必要的——在 macOS 和 Linux 上无害,在 Windows 上则至关重要。Python 文档在 csv 模块章节中明确指出这是正确的 CSV 写入文件打开方式。

Python
# 错误——在 Windows 上会出现空行
with open("output.csv", "w") as f:
    writer = csv.writer(f)

# 正确——newline="" 防止双 \r
with open("output.csv", "w", newline="") as f:
    writer = csv.writer(f)

如何将 JSON 记录追加到现有 CSV 文件?

以追加模式("a")打开文件,并使用相同的 fieldnames 创建 DictWriter。跳过 writeheader(),因为标题行已经存在。使用 pandas 时,用 to_csv(mode="a", header=False)。确保列顺序与现有文件一致,否则数据会落到错误的列中。如果不确定现有文件的列顺序,先用 csv.DictReader 打开,并从其 fieldnames 属性读取字段名,再创建用于追加的写入器。

Python
import csv

new_records = [
    {"order_id": "ord_f4c1", "total": 89.50, "status": "shipped"},
]

with open("orders.csv", "a", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["order_id", "total", "status"])
    writer.writerows(new_records)

用 Python 将大型 JSON 文件转换为 CSV 的最快方法是什么?

对于 500 MB 以下的文件,pd.read_json() 后接 to_csv() 是最快的单次调用方式——pandas 内部使用优化的 C 代码。对于 500 MB 以上的文件,使用 ijson 流式解析 JSON 记录,并用 csv.DictWriter 逐行写入 CSV。无论文件大小,内存使用量保持不变。对于 NDJSON 文件(每行一个 JSON 对象),完全不需要 ijson——对文件句柄的普通 Python for 循环可独立处理每一行,无需任何第三方库即可实现恒定内存。

Python
# 适合能放入内存的文件
import pandas as pd
df = pd.read_json("large_dataset.json")
df.to_csv("large_dataset.csv", index=False)

# 适合不能放入内存的文件的流式处理
import ijson, csv
with open("huge.json", "rb") as jf, open("huge.csv", "w", newline="") as cf:
    records = ijson.items(jf, "item")
    first = next(records)
    writer = csv.DictWriter(cf, fieldnames=first.keys())
    writer.writeheader()
    writer.writerow(first)
    for record in records:
        writer.writerow(record)

在 Python 中能将 CSV 输出写入 stdout 而非文件吗?

可以。将 sys.stdout 作为文件对象传入 csv.writer() 或 csv.DictWriter()。这对于在 shell 脚本中管道传输输出或快速调试很有用。使用 pandas 时,调用 to_csv(sys.stdout, index=False) 或 to_csv(None) 获取可打印的字符串,无需临时文件。在 Windows 上向 stdout 写入时,先调用 sys.stdout.reconfigure(newline="") 以避免双回车问题,因为 stdout 默认以文本模式打开。

Python
import csv
import sys
import json

data = json.loads('[{"host":"web-1","cpu":72.3},{"host":"web-2","cpu":45.1}]')
writer = csv.DictWriter(sys.stdout, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
# host,cpu
# web-1,72.3
# web-2,45.1

相关工具

MS
Maria SantosBackend Developer

Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.

PS
Priya Sharma技术审阅者

Priya is a data scientist and machine learning engineer who has worked across the full Python data stack — from raw data ingestion and cleaning to model deployment and monitoring. She is passionate about reproducible research, Jupyter-based workflows, and the practical engineering side of ML. She writes about NumPy, Pandas, data serialisation, and the Python patterns that make data pipelines reliable at scale.