资讯动态

Notion表格导出CSV全攻略:从复制粘贴到API自动化

发布时间:2026/10/14 14:52:31 来源:尧图企业网站定制
把 Notion 表格导出成 CSV这句话看起来只是个格式转换的小需求但真正动手的时候你会发现 Notion 的导出逻辑和普通表格软件完全不在一个频道上。我之前帮某业务系统做历史数据迁移对方只要一张 CSV正好我手上是在 Notion 里维护了一年的项目台账结果找导出按钮就找了十几分钟最后发现官方根本没有“某个数据库 → 一键导出 CSV”这种菜单。折腾了一下午才把数据完整搬出来中间踩了日期时区、中文乱码、字段丢失一堆坑。这篇文章就把 Notion 表格到 CSV 的所有可行路径拆开讲清楚包括官方导出、复制粘贴、第三方网页工具、API 脚本以及我最后推荐的自动化方案。适合想把 Notion 数据导到 Excel、处理数据、做分析的运营、数据分析师和开发同学也适合只是偶尔需要导出一张表给同事的普通用户。1. 先说结论官方导出并不是“一个表格一个按钮”1.1 当你按下 Export 时你拿到的是什么Notion 的导出入口在左侧 Settings → Export格式选项是 HTML、Markdown CSV 和 PDF。很多人一看有 CSV 三个字母就选了 Markdown CSV结果下载回来一个压缩包里面是整个工作区的全部内容根本不是你想要的那一张表。我实测拿到的压缩包里大致是这个结构export.zip ├── assets/ # 图片、附件等资源文件 ├── pages/ # 所有普通页面的 Markdown 或 HTML └── databases/ # 数据库导出的 CSV 文件也就是说你得先下载整个工作区再从里面翻出对应数据库的 CSV 文件。如果工作区里数据库多还要逐个确认文件名对得上。更麻烦的是这个 CSV 的质量并不干净relation 关系字段导出来是一串内部 IDURL 字段可能被处理成短链接形式数据库行的正文内容不会合并进 CSV而是单独拆成一个个 Markdown 文件放在 pages 目录下和 CSV 行很难一一对上。所以官方导出更适合做“整个工作区的备份归档”不适合“我要一张能直接拿去做分析的干净表格”。1.2 Notion 的数据模型天生和 CSV 不对付为什么 Notion 不做“单个数据库导出 CSV”这种常规功能因为它内部根本不是二维表。Notion 数据库里的每一行本质是一个 Page页面每一列是一个 Property属性。属性只是挂在这个页面上的一组元数据页面本身还可以包含正文、子页面、附件、评论这些内容在数据库视图里默认是不显示的但它们又是页面数据的一部分。用生活化的类比来理解Excel 是一张照片所有内容平铺在一个格子里Notion 数据库更像是一个文件柜每个文件夹外侧贴着几张标签属性文件夹里面还有一堆文件正文、子页面。导出成 CSV相当于只把外侧标签抄成一张表里面文件的内容怎么处理官方选择是拆开另存成独立的 Markdown 文件。正是这种结构差异导致官方导出的 CSV 里经常出现“看起来缺了关键内容”的情况。比如你在 Notion 视图里能看到每个任务有“负责人”“截止日期”“关联客户”但导出后才发现负责人变成了内部用户 ID关联客户的 relation 字段是一串 UUID根本没法直接给别人用。2. 四条可行路线横评从复制粘贴到 API 脚本不同场景下需要的导出方案完全不一样。我按自己实际用过的顺序整理了一张对比表。方案原理优点明显缺点适合场景官方全量导出下载整个工作区 ZIP数据完整适合备份多级目录CSV 质量差关系字段丢失工作区整体备份归档复制粘贴浏览器选中表格区域粘贴到 Excel十秒搞定无需任何配置只有当前视图隐藏列和折叠分组会错乱几百行以内的小表网页导出工具第三方站点授权后输出 CSV速度快保留部分视图逻辑数据要过第三方服务器有隐私风险一次性使用、非敏感数据Notion API 脚本直接走官方接口拉数据可控、可自动化、字段类型清晰需要一点代码能力需配置权限定期同步、数据量大、生产环境我自己试过所有方案之后长期留下来的其实是复制粘贴和 API 脚本两种。复制粘贴处理小表格是真快API 脚本负责一切需要自动化、可复现、不能丢数据的场景。下面分别说清楚。2.1 复制粘贴最省事但别在大表上冒险操作上很简单在 Notion 表格视图里把鼠标放到表格左上角按住往下拖选中所有行然后 CtrlC再到 Excel 里 CtrlV最后另存为 CSV。这个方案有几个限制要先讲明白。第一复制的数据完全取决于当前屏幕里的视图状态。如果当前视图做了筛选那你复制出来的就是筛选后的结果如果有些列被手工隐藏了复制出来的表格里那些列会直接消失如果按某个字段做了分组折叠粘贴到 Excel 后会多出一堆分组占位行整个表格错乱到没法看。第二几百行数据问题不大几千行以上浏览器会很卡复制的时候可能会出现“页面无响应”。我试过一次性复制一个 5000 行的表格Notion 直接卡了快半分钟才弹出复制成功提示粘贴后还发现有几行被截断了。那次之后就再也没用复制粘贴处理大表。第三Notion 里多行文本、换行符会保留在单元格里粘贴到 Excel 后虽然显示正常但你要是直接“另存为 CSV”Excel 可能会把单元格里的换行处理成 CSV 换行导致行数变多。这个坑后面单独展开。2.2 网页导出工具快是真的快但要谨慎授权社区里有不少网页版的 Notion 导出器原理基本是在浏览器里拿到 Notion 的授权 token然后模拟前端操作把数据库数据拉下来输出 CSV。用过一次体验确实流畅视图里的排序和筛选也能保留大部分特别适合不会写代码的人。我之所以没有把它作为长期方案主要是因为授权之后你的数据会经过第三方的服务器之后理论上对方可以用你的 token 继续读取数据。如果只是导出一张公开活动报名表倒无所谓但涉及客户信息、财务数据、内部项目协作内容我不会冒这个险。官方 API 方案虽然要写代码但数据是直接从 Notion 服务器到你自己电脑中间不经过任何人的服务器。2.3 为什么最后选了 API 脚本API 方案真正打动我的点有三个。一是字段类型不会丢。select、multi_select、date、formula、relation 这些在官方导出或复制粘贴里都会被打平、转字符串或直接丢失但 API 返回的是结构化 JSON每个属性的类型清清楚楚可以针对每种类型做精准的转换规则。二是可以自动化。脚本写好之后以后每个月把数据库同步成 CSV一条命令就完成不需要打开浏览器也不用担心复制粘贴时手抖。三是数据安全可控。整个流程只和 Notion 官方 API 通信不经过任何第三方token 只保存在本地环境变量里。后面我给的脚本可以直接拿去复用。3. 实战用 Notion API 把数据库拉成本地 CSV3.1 准备工作创建集成、授权页面、拿 ID第一步是去 Notion 的集成管理页面创建一个 Internal Integration。创建时只需要填一个名字选关联的 workspace然后创建完会得到一个 Secret Token格式是secret_xxxxxx这个 token 相当于这个集成的钥匙只用一个数据库的话一把钥匙够了。然后要授权页面。打开你想导出的那个数据库页面点击右上角的...菜单找到Connections在弹出的面板里搜索你刚才创建的集成名称点确认。这一步不做的话后面调 API 必然返回 403很多人第一次都栽在这。最后拿 database_id。打开数据库页面看浏览器地址栏URL 结构一般是这样的https://www.notion.so/workspace_name/0123456789abcdef0123456789abcdef?vabcdef...中间那串 32 位的十六进制字符串才是 database_id?v后面那一串是视图 ID别拿错了。如果你打开的是数据库页面但 URL 里没有明显 ID可以用CtrlL复制完整链接再解析。配置好之后先用一条curl验证一下权限和 ID 是否正确curl -X POST https://api.notion.com/v1/databases/YOUR_DATABASE_ID/query \ -H Authorization: Bearer YOUR_SECRET_TOKEN \ -H Notion-Version: 2022-06-28 \ -H Content-Type: application/json如果返回 JSON 里包含results数组说明权限和 ID 都没问题。注意Notion-Version这个 Header 是必填的不同版本返回的数据结构会有差异我这套代码基于2022-06-28验证过建议就用这个版本。3.2 读取数据库所有行分页循环是核心Notion API 查询数据库的接口是POST /v1/databases/{id}/query一次最多返回 100 行。如果你的数据超过 100 行必须依靠返回体里的has_more和next_cursor做循环翻页。翻页逻辑不复杂但很关键第一次请求不带start_cursor拿到响应后如果has_more为 True就把next_cursor塞进下一次请求的 body直到has_more为 False。漏掉这个步骤导出的 CSV 会只有前 100 行而且不容易被发现。def fetch_all_rows(database_id, token): url fhttps://api.notion.com/v1/databases/{database_id}/query headers { Authorization: fBearer {token}, Notion-Version: 2022-06-28, Content-Type: application/json, } all_rows [] start_cursor None while True: payload {page_size: 100} if start_cursor: payload[start_cursor] start_cursor resp requests.post(url, headersheaders, jsonpayload) resp.raise_for_status() data resp.json() all_rows.extend(data.get(results, [])) if not data.get(has_more): break start_cursor data.get(next_cursor) return all_rows注意start_cursor更新要放在has_more判断之后否则最后一次循环会把 None 塞进去导致多余请求。这个案例我写成代码时是清醒的但实际调试时还真因为这个多打了一次无意义请求。3.3 属性值提取每种字段类型都有坑这是整套脚本里最容易出错的部分。Notion API 返回的properties是一个 dict其中每个属性的 value 结构完全取决于这个属性的类型。拿到的属性可能是这个样子{ 姓名: { id: title, type: title, title: [ { plain_text: 张三 } ] }, 状态: { id: select, type: select, select: { name: 已完成 } }, 标签: { id: multi_select, type: multi_select, multi_select: [ { name: 紧急 } ] }, 截止日期: { id: date, type: date, date: { start: 2025-01-10 } } }如果直接粗暴地把这个 JSON 塞进 CSV那导出来的文件没办法看。需要针对每种类型单独写提取逻辑。我这里直接给一个可直接运行的提取函数覆盖了主要类型def convert_rich_text(items): if not items: return return .join(item.get(plain_text, ) for item in items) def extract_property_value(prop): prop_type prop.get(type) if prop_type in (title, rich_text): return convert_rich_text(prop.get(prop_type, [])) if prop_type number: value prop.get(number) return if value is None else value if prop_type in (select, status): value prop.get(prop_type) return value.get(name, ) if value else if prop_type multi_select: values prop.get(multi_select, []) return .join(item.get(name, ) for item in values) if prop_type date: date_obj prop.get(date) if not date_obj: return start date_obj.get(start) or end date_obj.get(end) or return start ( - end if end else ) if prop_type checkbox: return prop.get(checkbox, False) if prop_type url: return prop.get(url) or if prop_type in (email, phone_number): return prop.get(prop_type) or if prop_type formula: formula prop.get(formula, {}) f_type formula.get(type) if f_type string: return formula.get(string) or if f_type number: return formula.get(number) or if f_type boolean: return formula.get(boolean) or if f_type date: return formula.get(date) or return if prop_type relation: related prop.get(relation, []) return ,.join(item[id] for item in related) if prop_type people: people prop.get(people, []) return .join(p.get(name, ) for p in people) if prop_type created_by: return prop[created_by].get(name, ) if prop_type last_edited_by: return prop[last_edited_by].get(name, ) if prop_type created_time: return prop.get(created_time, ) if prop_type last_edited_time: return prop.get(last_edited_time, ) if prop_type rollup: rollup prop.get(rollup, {}) r_type rollup.get(type) if r_type number: return rollup.get(number, ) if r_type string: return rollup.get(string, ) if r_type date: return rollup.get(date, ) if r_type array: arr_items rollup.get(array, []) # 数组里每个元素仍然是一个属性值对象可以递归处理 return .join(str(extract_property_value(item)) for item in arr_items) return return 关于rollup要单独说两句rollup 是对关联数据库跨表聚合后的结果API 返回结构非常不稳尤其当聚合类型是数组时里面每个元素的类型取决于源属性类型。上面的递归调用能处理大多数情况但如果你实际跑脚本时发现 rollup 字段导出为空大概率是遇到了 API 文档中未覆盖的聚合类型。我的处理方法是不纠结安全兜底返回空字符串同时把原始 JSON 打一条日志后续可以针对特殊类型再补分支。relation字段我默认导出的是关联页面的 UUID这是最保险的做法。如果你想导出成“关联页面的标题”需要额外调用查询接口去拿目标页面的 title会显著增加请求次数。我的建议是第一版先导成 UUID如果下游系统真的需要可读文本再加一个映射表去翻译。3.4 写出 CSV列顺序固定编码用 utf-8-sig所有行数据提取完成后要写 CSV 文件。这里有两件事必须处理干净否则后面一定有同学踩坑。第一CSV 的列顺序。Notion API 返回的 properties 字典并不保证列顺序稳定所以不要用 Python 默认字典序去生成列头。我的做法是手工在脚本里维护一个FIELD_ORDER列表按数据库视图的顺序写清楚这样导出的 CSV 列顺序稳定可控下游加工时也方便。第二编码。直接用encodingutf-8写 CSV 没问题但 Excel 打开时大概率乱码。Windows 上的 Excel 默认用 GBK 读取无 BOM 的 UTF-8 文件所以导出时最好用utf-8-sig它会自动在文件开头加 BOMExcel 就能正确识别。import csv FIELD_ORDER [姓名, 状态, 标签, 截止日期, 负责人, 备注] def write_csv(rows, output_path): with open(output_path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesFIELD_ORDER) writer.writeheader() for row in rows: writer.writerow(row)加上前面读取数据的逻辑一个最小可用的完整脚本大概长这样import os import requests import csv NOTION_TOKEN os.environ.get(NOTION_TOKEN) DATABASE_ID os.environ.get(DATABASE_ID) FIELD_ORDER [姓名, 状态, 标签, 截止日期, 负责人, 备注] # 这里把 3.2 和 3.3 的函数直接搬过来 # fetch_all_rows(database_id, token) # extract_property_value(prop) def export_database_to_csv(output_path): pages fetch_all_rows(DATABASE_ID, NOTION_TOKEN) rows [] for page in pages: props page.get(properties, {}) row {} for field in FIELD_ORDER: prop props.get(field) if prop is None: row[field] else: row[field] extract_property_value(prop) rows.append(row) write_csv(rows, output_path)注意上面的代码里FIELD_ORDER中的名称必须和你 Notion 数据库中的列名完全一致包含空格和符号。如果不一致导出来会出现整列为空或字段丢失。为了便于调试建议把环境变量NOTION_TOKEN和DATABASE_ID放到.env文件或 shell 里不要硬编码进脚本。这样后续给人演示或者放到 CI 环境跑也不需要改代码。4. 实操中那些反复出现的坑问题排查实录这套脚本从跑通到稳定中间我踩了不少坑而且每一个都很有代表性逐个记录一下。4.1 403、404、401 状态码的确认顺序我在跑脚本时遇到过三个状态码排查顺序很重要。403 是最常见的数据库页面没有共享给集成。去页面右上角...→Connections里把集成加上基本能解决。注意集成必须在每个数据库页面单独添加一次即使同属一个 workspace也不会自动生效。404 通常是 database_id 错误。最常见的情况是把 URL 里的?v后面的 view_id 当成了 database_id。database_id 是 URL 中页面标题和 view_id 之间的那 32 位 hex 字符串用正则提取最稳。401 表示 token 无效。检查环境变量是否设置正确或者创建集成后是否重新生成了 secret。如果你把旧的 token 写死在脚本里重新生成密钥后脚本就会一直 401。4.2 Excel 打开 CSV 中文乱码这个坑出现概率极高但解决起来最简单。在 Notion 侧属性里的中文导出后是正常的 UTF-8直接写到文件没问题但用 Excel 打开 CSV 时Excel 会按照系统默认编码去猜Windows 中文系统默认 GBK于是 UTF-8 的文件会被看成乱码。处理方式是文件开头加 UTF-8 BOMPython 里写encodingutf-8-sig即可。如果你用其他语言写脚本记得确认对应的编码参数。另外一个冷知识macOS 上 Numbers 对无 BOM 的 UTF-8 识别没问题但 Windows Excel 一定用 BOM 方案处理通吃平台。4.3 日期字段时区偏移和格式问题官方导出 CSV 和 API 导出的日期都可能出现“和 Notion 页面里显示不一致”的情况。因为 Notion API 返回的date是 ISO 8601 UTC 格式。如果你的 Notion 数据库里设置的是东八区时间API 返回的日期可能比页面显示晚 8 小时。简单处理方式是把日期字段当成字符串保存不要让它转成 Excel 原生日期类型。我上面代码里直接返回start - end的字符串文本就是为了避免 Excel 自动推断类型。如果确实需要让 Excel 识别为日期建议显式转换成YYYY-MM-DD HH:mm格式并统一在脚本里加时区偏移。4.4 分页循环写错导致数据丢失分页循环里最容易犯的错是只取第一页然后看到has_more为 True 但没有继续循环。另一个错是把next_cursor更新写进if not has_more分支外层导致最后一次多请求一次。我之前调试一个 2000 行的库导出后只核对第一页以为没问题后来放到下游统计才发现总数差了几百行。从那以后我的脚本每次都返回总行数写完 CSV 后打印一行exported: X rows用来和 Notion 数据库右下角的总数核对。这一步操作虽然简单但能省下后面排查数据丢失的大量时间。4.5 视图里的排序筛选API 一概不认API 查询返回的是数据库的全部数据和当前视图是否做了筛选、排序、分组没有任何关系。即使你在 Notion 里把视图筛选成“仅本周任务”API 导出后你拿到的仍是所有历史任务。这一点非常重要。如果需求是“导出当前视图筛选后的结果”那 API 方案并不直接支持。我的变通做法有两种一是在 Notion 页面里先调整筛选条件然后复制粘贴二是把筛选条件翻译成 API 请求里的筛选参数写在脚本里。第二种做法适合筛选条件固定、要定期执行的任务而且能自动化长期看比复制粘贴靠谱。Notion 查询接口支持filter参数这部分逻辑可以单独封装本文先不展开。4.6 大量数据的请求频率限制Notion API 对每个集成有速率限制官方的阈值是每 5 秒最多 3 个请求超过会返回 429。如果数据库有几万行按页面 100 行算大约需要几百次请求跑下来大概十分钟。我在一个 3 万行的数据库上实测过不加任何限速时中间会触发一次 429加一个简单的重试逻辑就很稳定。重试逻辑不复杂遇到 429 时 sleep 两秒然后重试同一请求最多重试三次。这里不贪多保证过程不被打断就行。下面的片段可以直接嵌进fetch_all_rowsfor attempt in range(3): resp requests.post(url, headersheaders, jsonpayload) if resp.status_code 429: time.sleep(2) continue resp.raise_for_status() break5. 我的最终选型建议如果你只是想偶尔取一次数据复制粘贴最快但记得先检查当前视图的筛选分组避免拿到一份“以为全量其实只是视图内”的 CSV。如果你要定期把 Notion 数据同步到其他系统我强烈建议花半小时把 API 脚本搭起来做成一个小工具每次手动跑或者挂到定时任务里。这套脚本最大的价值不是省掉一次鼠标操作而是让导出结果可重复、可预期不会因为同事调整了视图就导出错数据。从我自己的实践来看有两点经验是最值钱的第一导出文件名务必带上日期比如tasks_20250108.csv避免把旧文件覆盖后找不到历史版本第二列顺序一定手工固定不要依赖 API 返回的字典顺序否则同一个脚本今天导出和明天导出的列顺序可能不同。把这些细节处理好Notion 导出 CSV 这个场景才能真正做到“一次配置长期可用”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑