anydoc Node.js 绑定完整指南:用 @firecrawl/anydoc 将 Word、PPT、Excel、PDF 等文档转换为 GitHub-Flavored Markdown
【免费下载链接】anydocConvert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF to clean Markdown. Built in Rust, with Node.js and Python bindings.项目地址: https://gitcode.com/gh_mirrors/any/anydoc
本指南围绕node/README.md展开,系统讲解 anydoc 官方 Node.js 绑定@firecrawl/anydoc的安装、CLI 与编程接口。该包以 N-API 原生模块形式封装 anydoc 的 Rust 转换内核,能把 Word、PowerPoint、Excel、OpenDocument、RTF、EPUB、CSV 与 PDF 等八类格式统一转换为干净的 GitHub-Flavored Markdown(GFM),且转换在 libuv 线程池上执行、绝不阻塞事件循环。读完本文,你将掌握npx零安装转换、基于文件路径/字节/文档模型三种调用方式、错误码驱动的健壮批处理、基于内容的格式探测,以及文档模型(块、表格、列表、脚注、内嵌资源)的完整结构,可直接用于构建文档批处理管线或 Agent 工具。
设计理念:一种输出模型,统一所有格式
任何格式的文件都会先被各自的格式解析器解析进同一个共享文档模型,再经同一个 Markdown 序列化器渲染输出。因此,无论输入是 2003 年的.doc还是新生成的.pptx,标题、表格、列表、脚注、转义规则和标题锚点的表现都完全一致。正如根目录 README.md 中的架构示意所示:
document bytes │ ├─► format detection → content markers, not the extension │ ├─► format parser → one per format (doc, docx, ppt, pptx, xls, │ xlsx, odt/ods/odp, rtf, epub, csv) │ │ │ └─► Document → shared model: blocks, inlines, tables, │ footnotes, assets │ │ │ └─► GFM serializer → Markdown │ └─► PDF → pdf-inspector → Markdown directly这一设计的收益在于:因为所有格式都汇入同一套模型与序列化器,输出质量问题只需修复一次——例如 docx 的表格转义修复,会自动成为 rtf、odt 等其他格式的表格转义修复。
安装与环境要求
npm install @firecrawl/anydoc- Node 版本:
node/package.json中engines声明为node >= 20。 - 原生模块:包通过 NAPI-RS(
node/package.json中的@napi-rs/cli构建)生成平台专属二进制,index.js在运行时按process.platform与process.arch自动加载对应.node文件。官方构建目标覆盖 macOS(x64/arm64)、Linux(x64/arm64,含 musl 与 gnu 变体)、Windows(x64-msvc)等平台(见node/package.json的napi.targets)。 - TypeScript 类型:包随附
index.d.ts,所有接口与枚举均带完整 JSDoc 注释,ConvertErrorCode等联合类型开箱即用。 - 若原生绑定加载失败,
index.js会回退尝试 WASI 构建(anydoc.wasi.cjs),并给出明确的排查提示。
支持的格式与扩展名
| 格式 | 扩展名 |
|---|---|
| Word | .doc,.docx,.docm |
| PowerPoint | .ppt,.pps,.pot,.pptx,.pptm,.ppsx,.ppsm |
| Excel | .xls,.xlsx,.xlsm,.xlsb |
| OpenDocument | .odt,.ods,.odp |
| Rich Text Format | .rtf |
| EPUB | .epub |
| CSV | .csv |
.pdf |
从 node/src/lib.rs 可以看到,Node 绑定只暴露 12 个核心Format枚举值(doc、docx、odt、pdf、ppt、pptx、rtf、epub、xlsx、ods、odp、csv)。像.docm、.xlsm、.ppsx这类与某解析器共享实现的容器变体,在枚举层面会映射到对应的核心格式——例如formatFromExtension('.pptm')返回'pptx'、formatFromExtension('xls')返回'xlsx'。
CLI:无需安装即可转换
包在bin字段中注册了anydoc命令(见 node/package.json),因此可以直接用npx调用,无需先安装:
npx @firecrawl/anydoc report.docx # Markdown 输出到 stdout npx @firecrawl/anydoc slides.pptx -o slides.md # 或输出到文件 npx @firecrawl/anydoc - --format csv < data.csv # 从 stdin 读取Markdown 走 stdout,错误信息走 stderr。想常驻使用,可全局安装npm install -g @firecrawl/anydoc获得永久anydoc命令;anydoc --help可查看全部选项。
CLI 完整参数
根据 node/cli.js 中的帮助文本与参数解析实现,完整用法如下:
anydoc <file> [options] anydoc - [options] < file| 选项 | 说明 |
|---|---|
-o, --output <path> | 将 Markdown 写入<path>而不是 stdout |
-f, --format <format> | 显式指定输入格式而非自动探测。可选值:doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv(xls、docm、ppsx等扩展名别名也会解析到上述格式) |
-h, --help | 打印帮助并退出 |
-V, --version | 打印版本号并退出 |
CLI 的退出码约定(对脚本化调用至关重要):
| 退出码 | 含义 |
|---|---|
0 | 成功 |
1 | 文档无法读取或转换 |
2 | 用法错误:未知选项、缺少输入、--format非法 |
几个值得注意的 CLI 行为细节(均可在 node/cli.js 源码中验证):
- 一次调用只转换一个文档,传入第二个输入文件会以退出码 2 报错;
--之后的参数一律视为位置参数,用于处理以连字符开头的文件名;- 支持
--output=path、--format=csv这类=内联传值形式; - 从 stdin 读取时若检测到 stdin 是终端(TTY),会直接以用法错误退出,提示需要管道或重定向;
- stdin 没有扩展名,所以从 stdin 输入 CSV 必须显式加
--format csv; - 输出到管道被下游提前关闭(如
anydoc big.xlsx | head)时按成功处理(EPIPE不视为转换失败); --help与--version在加载原生绑定之前处理,因此即使本机缺少可用绑定,这两个命令也能正常工作;- 扫描版/纯图片 PDF 需要 OCR,而 anydoc 不做 OCR,此类输入会以
unsupported错误失败。
Node.js API:三种调用方式
import { toDocument, toMarkdown, toMarkdownBytes } from '@firecrawl/anydoc'; // 方式一:从文件路径转换(格式从文件内容探测) const markdown = await toMarkdown('report.docx'); // 方式二:从字节转换,格式由内容自动探测 const fromBytes = await toMarkdownBytes(bytes); // 方式三:显式指定格式(无签名格式如 CSV 必须如此) const fromCsv = await toMarkdownBytes(bytes, 'csv'); // 方式四:止步于文档模型,模型同时携带内嵌资源 const document = await toDocument(bytes);三个函数的语义差异
| 函数 | 输入 | 输出 | 适用场景 |
|---|---|---|---|
toMarkdown(path) | 文件路径 | Promise<string> | 直接处理磁盘文件;文件不可读时拒绝为io错误 |
toMarkdownBytes(bytes, format?) | Uint8Array+ 可选格式 | Promise<string> | 内存/网络流数据;CSV 必须显式传'csv' |
toDocument(bytes, format?) | Uint8Array+ 可选格式 | Promise<Document> | 需要文档模型(块结构、表格、脚注、内嵌资源)的二次加工场景 |
从 node/src/lib.rs 的实现可见,三个函数全部返回AsyncTask:Rust 侧的计算在 libuv 线程池上执行(compute),完成后再切回 JS 主线程resolve/reject,因此任何转换都不会阻塞事件循环,可在高并发 Web 服务中放心使用。
一个特殊限制:PDF 不支持 toDocument
根据 node/src/lib.rs 与index.d.ts中Format.pdf的注释:PDF 由 pdf-inspector 直接产出 Markdown,没有文档模型形式,因此toDocument对 PDF 输入会失败;如需 PDF 请走toMarkdownBytes/toMarkdown。
错误处理:以error.code驱动批处理
转换只在无法产出任何有意义的 Markdown 时才拒绝(Promise reject),文件里的小瑕疵会被尽量恢复或跳过,而不是报错。拒绝时抛出的Error带有code属性,指明失败类别。官方推荐的批处理模式:
try { return await toMarkdown(path); } catch (error) { // 这类文件永远转换不出来,记录下来继续处理下一个。 if (error.code === 'encrypted' || error.code === 'unsupported') { unconverted.push({ path, reason: error.code }); return null; } throw error; }错误码全表
code | 含义 |
|---|---|
unsupported | 未知格式,或无法转换的格式(如纯图片 PDF) |
malformed | 结构不可用:无法提取任何有意义的内容 |
encrypted | 加密或受密码保护 |
resourceLimit | 触发了固定安全上限(解压、嵌套深度、节点数量) |
missingPart | 缺少产出任何有意义输出所必需的部件(package part) |
io | 文件无法读取,仅toMarkdown会抛出 |
error.message携带详细说明;当格式能识别出具体部件时,会点名出错的包部件。在 src/error.rs 中可以看到这些code字符串与 Rust 侧ConvertError变体的一一对应:Unsupported→"unsupported"、Malformed→"malformed"、Encrypted→"encrypted"、ResourceLimit→"resourceLimit"、MissingPart→"missingPart"、Io→"io"。Node 侧通过 node/src/lib.rs 的Failure结构在跨线程传递时保留错误类别,最终以error.code形式呈现给 JS。
TypeScript 用户可直接使用随包导出的联合类型ConvertErrorCode进行类型安全的错误分支:
import type { ConvertErrorCode } from '@firecrawl/anydoc';该类型定义在 node/index.d.ts 中,六个取值与上表完全一致。
格式检测:看内容,不看扩展名
格式从文件内容本身读取,依据各格式规范指定的特征标记:
- PDF:文件头(
%PDF-等 header); - RTF:开头的
{\rtf开放组; - OLE 容器(
.doc/.ppt/.xls等老格式):OLE 流名称; - ZIP 容器(
.docx/.pptx/.odt等):包内的 mimetype 与 content types; - CSV:没有任何特征标记,因此探测返回
null,只能靠扩展名或显式指定格式来命名。
formatFromBytes(bytes); // 'docx',或什么都匹配不上时返回 null formatFromExtension('.pptm'); // 'pptx' formatFromPath('report.odt'); // 'odt'三个函数的行为可从 node/src/lib.rs 确认:
formatFromBytes(bytes):纯内容探测,返回Format | null;formatFromExtension(extension):接受带或不带前导点号的扩展名(trim_start_matches('.')),.pptm归一为pptx;formatFromPath(path):取路径的扩展名再探测。
由于检测基于内容而非文件名,即使文件被错误命名(如把.docx内容命名为.txt)仍能正确转换。需要说明的是:CSV 因无签名,formatFromBytes对它返回null,测试用例 node/test.mjs 中也专门断言了这一点(formatFromBytes对 CSV fixture 返回null,且不传格式直接转 CSV 会以unsupported拒绝,显式传'csv'才能成功)。
深入文档模型:Document、Block、Inline 与各类结构化对象
toDocument返回的Document对象由三部分组成(见 node/src/document.rs 与 node/index.d.ts):
interface Document { blocks: Array<Block>; // 顶层块(标题、段落、列表、表格、引用、代码块…) notes: Array<Note>; // 脚注/尾注正文,正文中的 noteRef 按 id 引用 assets: Array<Asset>; // 内嵌二进制资源(图片、对象载荷) }Block:七种块类型
type BlockKind = 'heading' | 'paragraph' | 'list' | 'table' | 'blockQuote' | 'codeBlock' | 'rule'; interface Block { kind: BlockKind; level?: number; // heading: 1-6 anchor?: string; // heading: 文档内部指向该标题时的稳定锚点 id content?: Array<Inline>; // heading、paragraph list?: List; // list table?: Table; // table blocks?: Array<Block>; // blockQuote(嵌套块) lang?: string; // codeBlock:代码语言 text?: string; // codeBlock:代码文本 }Inline:六种行内元素
type InlineKind = 'text' | 'link' | 'image' | 'anchor' | 'noteRef' | 'lineBreak'; interface Inline { kind: InlineKind; text?: string; // text style?: Style; // text:已完全解析的字符样式 content?: Array<Inline>; // link:链接内联内容 target?: LinkTarget; // link alt?: string; // image:替代文本 source?: ImageSource; // image anchor?: string; // anchor:锚点 id noteId?: string; // noteRef:Document.notes 中的 id }其中Style是完全解析后的字符样式(bold、italic、strike、code四个布尔值),意味着继承链上的样式已折叠成最终生效值,无需调用方自己回溯样式表。
LinkTarget区分三种目标(node/src/document.rs):
type LinkTargetKind = 'external' | 'relative' | 'anchor'; interface LinkTarget { kind: LinkTargetKind; value: string; }external:带 scheme 的绝对 URL;relative:无 scheme 的相对引用,按原样保留;anchor:内部目标,指向标题锚点或某个anchor内联元素。
List 与 ListItem:保留源文档编号
type MarkerKind = 'bullet' | 'decimal' | 'lowerAlpha' | 'upperAlpha' | 'lowerRoman' | 'upperRoman'; interface List { marker: MarkerKind; // 源文档使用的标记族 start: number; // 首个条目的起始序号 items: Array<ListItem>; } interface ListItem { blocks: Array<Block>; checked?: boolean; // 任务列表复选框状态(仅当条目带复选框时) markerLabel?: string; // 字面标记文本,当源编号无法由 marker+位置重现时使用 // (如 "1-a)" 这类复合编号文本) }Table:规范化网格 + 合并单元格
表格被表示为规范化网格:每个逻辑网格位置恰好出现一次。内容与跨行跨列信息放在origin槽位上,被覆盖的每个位置则是一个指向 origin 的covered槽位(node/src/document.rs):
type TableKind = 'data' | 'layout'; // layout 指文本框、定位表等布局脚手架 interface Table { grid: Array<Array<CellSlot>>; // 规范化网格 headerRows: number; // 前导表头行数(0 = 无表头) kind: TableKind; } interface CellSlot { kind: 'origin' | 'covered'; cell?: Cell; // origin:单元格内容与跨度 originRow?: number; // covered:所属 origin 的行 originCol?: number; // covered:所属 origin 的列 } interface Cell { blocks: Array<Block>; colSpan: number; rowSpan: number; }Note 与 Asset:脚注和二进制资源
type NoteKind = 'footnote' | 'endnote'; interface Note { id: string; kind: NoteKind; blocks: Array<Block>; } interface Asset { id: number; // 在 Document.assets 中的索引,供 image source 引用 mediaType: string; // MIME 类型,如 "image/png" originPart: string; // 来源的包部件或流,用于溯源 data: Buffer; // 原始字节 }Asset的字节始终完整保留,因此文档在模型层面自包含(node/src/document.rs),这对需要把图片落盘或转存的对象存储的调用方非常方便。
图片与嵌入对象:Markdown 里留 alt,字节留在 assets
Markdown 无法内嵌二进制字节,因此 anydoc 的处理策略是:
- 内嵌图片:在 Markdown 中渲染为其 alt 文本,原始字节保留在
document.assets中,并附带 MIME 类型与来源部件(originPart); - 带外部 URL 的图片:渲染为普通 Markdown 图片(
alt); - 来源不可用(图片部件缺失或不可读且无 URL):仅保留 alt 文本。
ImageSource用kind区分三种情况(node/src/document.rs):
type ImageSourceKind = 'external' | 'asset' | 'unavailable'; interface ImageSource { kind: ImageSourceKind; url?: string; // external assetId?: number; // asset:指向 Document.assets 的索引 }在 node/test.mjs 的测试中可以验证:toDocument后document.assets中能直接找到mediaType === 'image/png'的资源,data是合法的Buffer(Buffer.isBuffer(image.data)且长度大于 0),且asset.id等于其在数组中的索引。
事件循环友好:libuv 线程池上的异步转换
这是 Node 绑定区别于其他语言绑定的关键设计。从 node/src/lib.rs 的实现可以看到:MarkdownFileTask、MarkdownBytesTask、DocumentTask都是 napi 的AsyncTask,其compute方法在libuv 线程池上执行 Rust 转换,reject阶段再切回 JS 线程构造携带code的错误对象。这意味着:
- 大批量转换不会阻塞事件循环,其他请求/IO 照常处理;
- 多个转换可在线程池中并行执行;
- 错误对象跨线程传递时通过
Failure结构保留错误类别,避免在线程池上构造 JS 错误(那里没有Env)。
测试与质量保障(仓库内的验证手段)
node 绑定自带冒烟测试(node/test.mjs),覆盖了本指南涉及的全部行为,可作为理解 API 语义的最佳参考资料:
toMarkdown从内容探测格式并输出标题(/^# /m);toMarkdownBytes支持显式格式与自动探测两种路径,且断言 CSV 必须显式命名;toDocument暴露文档模型:标题level在 1–6 之间、内联text与style.bold为布尔值;toDocument携带内嵌资源为 Buffer;- 格式探测三函数(content/extension/path)的行为逐一断言;
- 错误分支:
malformed、unsupported、encrypted、io各类code均被固定下来; - CLI 的 stdout 输出、
-o写文件、stdin 显式格式、退出码 1(转换失败)与 2(用法错误)、--help/--version退出码 0 全部有测试用例。
仓库的根级tests/fixtures/下维护了一整套各格式 fixture 语料(docx、pptx、ods、rtf、epub、pdf 等),并配有快照测试(tests/snapshots/)与健壮性变异测试(tests/robustness.rs);fuzz/目录还提供了按格式划分的 cargo-fuzz 模糊测试目标。
开发与自构建(可选)
如果需要在本地从源码构建 node 绑定:
cd node && npm install && npm run build && npm testnode/package.json中的脚本说明:npm run build使用napi build --platform --release生成当前平台的 release 原生绑定,npm test运行node --test执行上述冒烟测试。
小结
@firecrawl/anydoc把 Rust 核心的高性能和文档还原能力带入了 Node.js 生态:toMarkdown面向磁盘文件、toMarkdownBytes面向内存字节、toDocument面向需要结构化访问文档模型的场景,三者均异步运行于 libuv 线程池;error.code错误码、基于内容的格式探测、以及统一文档模型共同构成了构建稳健文档处理管线的基础。更多关于整体架构、基准与各语言绑定(Python、WebAssembly、Rust)的说明见根目录 README.md。
License
本项目采用 MIT 开源协议。
【免费下载链接】anydocConvert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF to clean Markdown. Built in Rust, with Node.js and Python bindings.项目地址: https://gitcode.com/gh_mirrors/any/anydoc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考