ASL Tool 3 Development Plan: - Architecture blueprint v1.5 (6 rounds of architecture review, 13 red lines) - M1/M2/M3 sprint checklists (Skeleton Pipeline / HITL Workbench / Dynamic Template Engine) - Code patterns cookbook (9 chapters: Fan-out, Prompt engineering, ACL, SSE dual-track, etc.) - Key patterns: Fan-out with Last Child Wins, Optimistic Locking, teamConcurrency throttling - PKB ACL integration (anti-corruption layer), MinerU Cache-Aside, NOTIFY/LISTEN cross-pod SSE - Data consistency snapshot for long-running extraction tasks Platform capability: - Add distributed Fan-out task pattern development guide (7 patterns + 10 anti-patterns) - Add system-level async architecture risk analysis blueprint - Add PDF table extraction engine design and usage guide (MinerU integration) - Add table extraction source code (TableExtractionManager + MinerU engine) Documentation updates: - Update ASL module status with Tool 3 V2.0 plan readiness - Update system status document (v6.2) with latest milestones - Add V2.0 product requirements, prototypes, and data dictionary specs - Add architecture review documents (4 rounds of review feedback) - Add test PDF files for extraction validation Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
13 KiB
PDF 表格提取引擎使用指南
文档版本: v1.0
最后更新: 2026-02-23
状态: ✅ 已测试通过(MinerU 引擎)
目标读者: 业务模块开发者(ASL 全文复筛、系统综述数据提取等)
前置条件:backend/.env中已配置MINERU_API_TOKEN
快速开始
5 秒上手
import { getTableExtractionManager } from '../common/document/tableExtraction/index.js';
const manager = getTableExtractionManager();
const result = await manager.extractTables(pdfBuffer, 'paper.pdf');
for (const table of result.tables) {
console.log(`${table.title}: ${table.rows.length} 行 × ${table.headers.length} 列`);
}
完整调用示例
import fs from 'fs';
import { getTableExtractionManager } from '../common/document/tableExtraction/index.js';
const manager = getTableExtractionManager();
// 读取 PDF 文件
const pdf = fs.readFileSync('/path/to/medical-paper.pdf');
// 提取表格(自动使用默认引擎 MinerU)
const result = await manager.extractTables(pdf, 'medical-paper.pdf', {
keepRaw: true, // 保留原始 Markdown
});
console.log(`引擎: ${result.engine}`); // "mineru"
console.log(`耗时: ${result.duration}ms`); // ~6000-20000ms
console.log(`表格数: ${result.tables.length}`);
// 遍历每个表格
for (const table of result.tables) {
console.log(`\n[${table.title}]`);
console.log(` 列: ${table.headers.join(' | ')}`);
console.log(` 行数: ${table.rows.length}`);
console.log(` 合并单元格: ${table.mergedCells.length}`);
// 访问具体数据
for (const row of table.rows) {
// row 是 string[],与 headers 一一对应
console.log(` ${row.join(' | ')}`);
}
// 原始 HTML(可直接渲染到前端)
if (table.rawHtml) {
console.log(` [HTML] ${table.rawHtml.substring(0, 100)}...`);
}
}
核心概念
架构设计
┌────────────────────────────────────────────────────┐
│ 业务代码(ASL / 系统综述 / Meta 分析) │
│ │
│ manager.extractTables(pdf, filename) │
│ → 返回 ExtractedTable[] │
└──────────────────────┬─────────────────────────────┘
│
┌──────────────────────▼─────────────────────────────┐
│ TableExtractionManager (统一入口) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ MinerU (VLM) │ │ Qwen-VL │ │ Paddle │ │
│ │ ✅ 已接入 │ │ 📋 待接入 │ │ 📋 待接入 │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
└────────────────────────────────────────────────────┘
核心原则:使用者不需要关心底层引擎。 提交 PDF → 获取结构化表格。
数据结构
// 提取结果
interface ExtractionResult {
tables: ExtractedTable[]; // 表格列表
engine: string; // 使用的引擎名
duration: number; // 耗时 (ms)
pageCount?: number; // PDF 页数
fullMarkdown?: string; // 完整 Markdown (需 keepRaw: true)
}
// 单个表格
interface ExtractedTable {
title: string; // "Table 1 Baseline characteristics"
headers: string[]; // 表头列名
rows: string[][]; // 数据行(二维数组)
mergedCells: MergedCell[]; // 合并单元格
footnotes: string[]; // 脚注
pageNumber?: number; // 页码
rawHtml?: string; // 原始 HTML
rawMarkdown?: string; // 原始 Markdown
}
// 合并单元格
interface MergedCell {
row: number; // 起始行 (0-based)
col: number; // 起始列 (0-based)
rowSpan: number;
colSpan: number;
}
API 参考
getTableExtractionManager()
获取全局管理器单例。首次调用时自动注册 MinerU 引擎。
import { getTableExtractionManager } from '../common/document/tableExtraction/index.js';
const manager = getTableExtractionManager();
manager.extractTables(pdf, filename, options?)
提取 PDF 中的表格。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
pdf |
Buffer |
✅ | PDF 文件内容 |
filename |
string |
✅ | 文件名(含 .pdf 后缀) |
options.language |
'zh' | 'en' | 'auto' |
❌ | 语言提示 |
options.pages |
number[] |
❌ | 指定页码 |
options.keepRaw |
boolean |
❌ | 保留原始 Markdown |
options.engine |
EngineType |
❌ | 覆盖默认引擎 |
返回:Promise<ExtractionResult>
manager.availableEngines()
返回已注册的引擎名称列表。
console.log(manager.availableEngines()); // ['mineru']
manager.getEngine(name?)
获取指定引擎实例。
manager.setDefault(name)
切换默认引擎。
实战场景
场景 1:ASL 全文复筛 — 提取基线特征表
import { getTableExtractionManager } from '../common/document/tableExtraction/index.js';
async function extractBaselineTable(pdfBuffer: Buffer, filename: string) {
const manager = getTableExtractionManager();
const result = await manager.extractTables(pdfBuffer, filename);
// 找到 "Table 1" 或包含 "Baseline" 的表格
const baseline = result.tables.find(
(t) =>
/table\s*1\b/i.test(t.title) ||
/baseline/i.test(t.title),
);
if (baseline) {
return {
title: baseline.title,
columns: baseline.headers,
data: baseline.rows,
hasMergedCells: baseline.mergedCells.length > 0,
};
}
return null;
}
场景 2:系统综述 — 提取所有表格为 JSON
async function extractAllTablesAsJson(pdfBuffer: Buffer, filename: string) {
const manager = getTableExtractionManager();
const result = await manager.extractTables(pdfBuffer, filename);
return result.tables.map((table) => ({
title: table.title,
headers: table.headers,
rows: table.rows.map((row) => {
const obj: Record<string, string> = {};
table.headers.forEach((h, i) => {
obj[h] = row[i] || '';
});
return obj;
}),
}));
}
// 输出示例:
// [
// {
// title: "Table 1 Baseline characteristics",
// headers: ["", "", "EGb 761®(N=200)", "Placebo(N=202)", "p-value"],
// rows: [
// { "": "Sex female", "": "", "EGb 761®(N=200)": "139 (69.5)", ... },
// ...
// ]
// }
// ]
场景 3:Meta 分析 — 提取效应值
async function extractEffectSizes(pdfBuffer: Buffer, filename: string) {
const manager = getTableExtractionManager();
const result = await manager.extractTables(pdfBuffer, filename);
// 找结局指标表
const outcomeTable = result.tables.find(
(t) => /outcome|result|efficacy|effect/i.test(t.title),
);
if (!outcomeTable) return [];
return outcomeTable.rows.map((row) => ({
measure: row[0],
treatment: row[1],
control: row[2],
pValue: row[3],
}));
}
场景 4:在 API 路由中使用
import { getTableExtractionManager } from '../../../common/document/tableExtraction/index.js';
async function handleTableExtraction(request: FastifyRequest, reply: FastifyReply) {
const data = await request.file();
if (!data) return reply.status(400).send({ error: 'No file uploaded' });
const buffer = await data.toBuffer();
const manager = getTableExtractionManager();
const result = await manager.extractTables(buffer, data.filename);
return reply.send({
success: true,
engine: result.engine,
duration: result.duration,
tables: result.tables.map((t) => ({
title: t.title,
headers: t.headers,
rowCount: t.rows.length,
rows: t.rows,
mergedCells: t.mergedCells,
})),
});
}
环境配置
必需环境变量
# backend/.env
# MinerU Cloud API(必需)
MINERU_API_TOKEN=your_mineru_api_token
MINERU_API_BASE=https://mineru.net/api/v4
MINERU_MODEL_VERSION=vlm
获取 MinerU Token
- 注册 OpenDataLab
- 登录 MinerU 控制台
- 个人中心 → API Token → 复制
- 写入
backend/.env的MINERU_API_TOKEN
免费额度
| 项目 | 限制 |
|---|---|
| 日解析页数 | 2000 页 |
| 单文件大小 | ≤ 200 MB |
| 单文件页数 | ≤ 600 页 |
小型综述 20 篇 (200 页) → 1 天免费完成。大型综述 500 篇 (5000 页) → 分 3 天免费完成。
运行测试
cd backend
# 测试指定 PDF(推荐)
npx tsx src/tests/test-table-extraction.ts "../docs/03-业务模块/ASL-AI智能文献/05-测试文档/PDF/Herrschaft 2012.pdf"
# 自动选取测试目录中的第一个 PDF
npx tsx src/tests/test-table-extraction.ts
期望输出
========================================
PDF 表格提取引擎 — 集成测试
========================================
文件: Herrschaft 2012.pdf
引擎: mineru
耗时: 6.5s
检出表格: 3 个
────────────────────────────────────────
表格 1: Table 1 Baseline characteristics...
列数: 5
行数: 18
合并单元格: 2
表头: ... | EGb 761®(N = 200) | Placebo(N = 202) | p-value
表格 2: Table 2
列数: 4
行数: 10
表格 3: Table 3 Adverse events...
列数: 6
行数: 7
合并单元格: 4
测试通过
文件清单
backend/src/common/document/tableExtraction/
├── types.ts # 统一接口 + 类型定义
├── htmlTableParser.ts # HTML <table> → ExtractedTable 解析器
├── TableExtractionManager.ts # 引擎管理器(使用者入口)
├── engines/
│ └── MinerUEngine.ts # MinerU Cloud API 适配器
└── index.ts # 统一导出 + 全局单例
backend/src/tests/
└── test-table-extraction.ts # 集成测试脚本
扩展新引擎
添加新引擎只需 3 步:
Step 1: 实现接口
// engines/Qwen3VLEngine.ts
import type { ITableExtractionEngine, ExtractionOptions, ExtractionResult } from '../types.js';
export class Qwen3VLEngine implements ITableExtractionEngine {
readonly name = 'qwen3vl';
readonly displayName = 'Qwen3-VL 多模态';
async extractTables(
pdf: Buffer,
filename: string,
options?: ExtractionOptions,
): Promise<ExtractionResult> {
// 实现提取逻辑 ...
}
}
Step 2: 注册引擎
// index.ts 中添加
import { Qwen3VLEngine } from './engines/Qwen3VLEngine.js';
// 在 getTableExtractionManager() 中
if (process.env.QWEN3VL_API_KEY) {
_instance.register(new Qwen3VLEngine());
}
Step 3: 使用
const manager = getTableExtractionManager();
// 显式指定引擎
const result = await manager.extractTables(pdf, 'paper.pdf', {
engine: 'qwen3vl',
});
// 或切换默认引擎
manager.setDefault('qwen3vl');
常见问题
Q: 提取耗时多久?
MinerU Cloud API 通常 5-20 秒(取决于 PDF 页数和云端负载)。首次请求可能较慢(云端冷启动),后续请求更快。
Q: 没有检出表格?
- 确认 PDF 中确实包含表格(扫描件图片中的表格也能识别)
- 检查
fullMarkdown输出中是否有<table>标签 - MinerU 对极端复杂的嵌套表格可能识别不完整
Q: 合并单元格数据如何处理?
ExtractedTable.mergedCells 记录了所有合并单元格的位置和跨度。在 rows 中,被合并的单元格只在起始位置有值,其余位置为空字符串。
Q: 和文档处理引擎 (pymupdf4llm) 的关系?
两者分别负责不同场景:
| 引擎 | 路径 | 场景 |
|---|---|---|
| 文档处理引擎 | ExtractionClient.ts |
全文文本提取(标题摘要初筛、PKB 入库) |
| PDF 表格提取引擎 | tableExtraction/ |
结构化表格提取(全文复筛、Meta 分析) |
相关文档
- PDF 表格提取引擎设计方案 — 架构设计 + 候选引擎 + 对比测试
- 文档处理引擎使用指南 — 全文文本提取 (pymupdf4llm)
- 文档处理引擎 README — 引擎总览
维护人: 技术架构师
核心依赖: adm-zip (ZIP 解析), axios (HTTP 请求)