Skip to content
LlamaParse 文档版本 跟随上游 源快照 9ad27faf 更新于 2026-10-16

本教程走一遍最常见的 Parse 场景之一:把一份财务报告里的所有表格都抽出来、载入 pandas。我们用的是财政服务局 2024 年执行摘要,与快速上手教程 用的是同一份文档,因此你可以把两个例子端到端对照着看。

本教程里的模式可以推广到任何散文与表格混杂的财务文档 —— 10-K、业绩报告、投资人演示、审计报告。

什么时候用这套模式

  • 你的文档在多页上有多张表格,你要的是全部而非某一张
  • 你需要表格数据作为结构化的行,而不只是 markdown 文本
  • 你想把表格直接载入 pandas 做下游分析或 RAG 索引

如果你只需要某个已知页上的一张表,见解析 PDF 中的图表并用 pandas 分析 —— 那里演示的是更简单的"某一张指定表"版本。若你只需要文本或 markdown,快速上手:解析 PDF 并理解输出 就够了。

1. 准备

把 API key 设成环境变量,SDK 会自动读取:

终端窗口

export LLAMA_CLOUD_API_KEY="llx-..."

安装 SDK 并构造一个 client:

  • Python
  • TypeScript
  • Go
  • Java
  • CLI

终端窗口

pip install "llama-cloud>=2.8"
from llama_cloud import LlamaCloud

client = LlamaCloud()  # reads LLAMA_CLOUD_API_KEY from the environment

终端窗口

npm install @llamaindex/llama-cloud
import LlamaCloud from '@llamaindex/llama-cloud';
import fs from 'fs';

const client = new LlamaCloud(); // reads LLAMA_CLOUD_API_KEY from the environment

终端窗口

go get github.com/run-llama/llama-parse-go
import (
  "context"

  llamacloud "github.com/run-llama/llama-parse-go"
)

ctx := context.Background()
client := llamacloud.NewClient() // reads LLAMA_CLOUD_API_KEY from the environment
implementation("ai.llamaindex:llama-cloud:1.3.0")
import ai.llamaindex.llamacloud.client.LlamaCloudClient;
import ai.llamaindex.llamacloud.client.okhttp.LlamaCloudOkHttpClient;

// reads LLAMA_CLOUD_API_KEY from the environment
LlamaCloudClient client = LlamaCloudOkHttpClient.fromEnv();

终端窗口

go install github.com/run-llama/llama-parse-cli/cmd/llp@latest

llp 会从环境里读取 LLAMA_CLOUD_API_KEY(也可用 --api-key 传入)。

2. 解析文档

上传 PDF 并跑一个解析任务。我们选 agentic 以获得扎实的表格准确度,并请求 items 视图,这样就能拿到一棵可以遍历找表的结构化树。

对于篇幅长、复杂度混合的报告,请开启成本优化器。这份财政部文档有很多以文字为主的叙述页,只有少数几页是表格密集的汇总页 —— 成本优化器会自动把简单页路由到 cost_effective,只在表格页上花 premium credit。我们同时请求 metadata,以便看到哪些页被成本优化了。

下载这份报告:

终端窗口

curl -fL -o executive-summary-2024.pdf https://fiscal.treasury.gov/system/files/files/reports-statements/financial-report/2024/executive-summary-2024.pdf
  • Python
  • TypeScript
  • Go
  • Java
  • CLI
file = client.files.create(
    file="executive-summary-2024.pdf",  # use /content/executive-summary-2024.pdf in Colab
    purpose="parse",
)

result = client.parsing.parse(
    file_id=file.id,
    tier="agentic",
    version="latest",
    processing_options={
        "cost_optimizer": {"enable": True},
    },
    expand=["markdown", "items", "metadata"],
)

print(f"Job status: {result.job.status}")
print(f"Total pages: {len(result.items.pages)}")
const file = await client.files.create({
  file: fs.createReadStream('executive-summary-2024.pdf'),
  purpose: 'parse',
});

const result = await client.parsing.parse({
  file_id: file.id,
  tier: 'agentic',
  version: 'latest',
  processing_options: {
    cost_optimizer: { enable: true },
  },
  expand: ['markdown', 'items', 'metadata'],
});

console.log(`Job status: ${result.job.status}`);
console.log(`Total pages: ${result.items.pages.length}`);

在 Go 里 expand 是 GET 参数,所以要先创建任务、轮询到终态,再带着你要的字段取结果:

f, err := os.Open("executive-summary-2024.pdf")
if err != nil {
  log.Fatal(err)
}
defer f.Close()

file, err := client.Files.New(ctx, llamacloud.FileNewParams{
  File:    f,
  Purpose: "parse",
})
if err != nil {
  log.Fatal(err)
}

// Enable Cost Optimizer on the create call
job, err := client.Parsing.New(ctx, llamacloud.ParsingNewParams{
  FileID:  llamacloud.String(file.ID),
  Tier:    llamacloud.ParsingNewParamsTierAgentic,
  Version: llamacloud.ParsingNewParamsVersionLatest,
  ProcessingOptions: llamacloud.ParsingNewParamsProcessingOptions{
    CostOptimizer: llamacloud.ParsingNewParamsProcessingOptionsCostOptimizer{
      Enable: llamacloud.Bool(true),
    },
  },
})
if err != nil {
  log.Fatal(err)
}

// Request markdown, items, and metadata when you fetch the result
getParams := llamacloud.ParsingGetParams{Expand: []string{"markdown", "items", "metadata"}}
result, err := client.Parsing.Get(ctx, job.ID, getParams)
if err != nil {
  log.Fatal(err)
}
for result.Job.Status != "COMPLETED" && result.Job.Status != "FAILED" && result.Job.Status != "CANCELLED" {
  time.Sleep(2 * time.Second)
  result, err = client.Parsing.Get(ctx, job.ID, getParams)
  if err != nil {
    log.Fatal(err)
  }
}
if result.Job.Status != "COMPLETED" {
  log.Fatalf("parse ended as %s", result.Job.Status)
}

fmt.Printf("Job status: %s\n", result.Job.Status)
fmt.Printf("Total pages: %d\n", len(result.Items.Pages))

在 Java 里 expand 是查询参数,所以要先创建任务、轮询到终态,再带着你要的字段取结果:

FileCreateResponse file = client.files().create(FileCreateParams.builder()
        .file(Paths.get("executive-summary-2024.pdf"))
        .purpose("parse")
        .build());

// Enable Cost Optimizer on the create call
ParsingCreateResponse job = client.parsing().create(ParsingCreateParams.builder()
        .fileId(file.id())
        .tier(ParsingCreateParams.Tier.AGENTIC)
        .version(ParsingCreateParams.Version.LATEST)
        .processingOptions(ParsingCreateParams.ProcessingOptions.builder()
                .costOptimizer(ParsingCreateParams.ProcessingOptions.CostOptimizer.builder()
                        .enable(true)
                        .build())
                .build())
        .build());

// Request markdown, items, and metadata when you fetch the result
ParsingGetParams getParams = ParsingGetParams.builder()
        .jobId(job.id())
        .addExpand("markdown")
        .addExpand("items")
        .addExpand("metadata")
        .build();

ParsingGetResponse result = client.parsing().get(getParams);
while (!result.job().status().equals(ParsingGetResponse.Job.Status.COMPLETED)
        && !result.job().status().equals(ParsingGetResponse.Job.Status.FAILED)
        && !result.job().status().equals(ParsingGetResponse.Job.Status.CANCELLED)) {
    Thread.sleep(2000);
    result = client.parsing().get(getParams);
}
if (!result.job().status().equals(ParsingGetResponse.Job.Status.COMPLETED)) {
    throw new RuntimeException("parse ended as " + result.job().status());
}

System.out.println("Job status: " + result.job().status());
System.out.println("Total pages: " + result.items().get().pages().size());

在 CLI 里 expand 也是查询参数,所以要先上传、创建任务、轮询到终态,再带着你要的字段取结果:

终端窗口

# Upload the report
FILE_ID=$(llp files create \
  --file executive-summary-2024.pdf \
  --purpose parse | jq -r '.id')

# Start a parse job with Cost Optimizer enabled
JOB_ID=$(llp parsing create \
  --file-id "$FILE_ID" \
  --tier agentic \
  --version latest \
  --processing-options.cost-optimizer '{enable: true}' | jq -r '.id')

# Poll until the job reaches a terminal status
while true; do
  STATUS=$(llp parsing get --job-id "$JOB_ID" | jq -er '.job.status') || break
  case "$STATUS" in COMPLETED|FAILED|CANCELLED) break ;; esac
  sleep 2
done
echo "Job status: $STATUS"

# Total pages
llp parsing get --job-id "$JOB_ID" --expand items | jq -r 'if .job.status == "COMPLETED" then .items.pages | length
  else "Parse job failed: \(.job.error_message // .)" end'

各选项在请求结构里的位置:tier 与 version 是顶层必填字段,processing_options.cost_optimizer.enable 开启逐页档位路由,而 expand 是一个顶层数组,控制返回什么。完整请求形状见配置模型。

3. 看哪些页被成本优化了

成本优化器会在逐页元数据里加上 cost_optimized 标记。检查它来确认路由是否落在你预期的位置 —— 以文字为主的叙述页应当是 True(按 cost_effective 处理),表格密集的页应当是 False(按 agentic 处理)。

  • Python
  • TypeScript
  • Go
  • Java
  • CLI
for page in result.metadata.pages:
    flag = "cost-optimized" if page.cost_optimized else "premium tier"
    print(f"page {page.page_number}: {flag}")
for (const page of result.metadata.pages) {
  const flag = page.cost_optimized ? "cost-optimized" : "premium tier";
  console.log(`page ${page.page_number}: ${flag}`);
}
for _, page := range result.Metadata.Pages {
  flag := "premium tier"
  if page.CostOptimized {
    flag = "cost-optimized"
  }
  fmt.Printf("page %d: %s\n", page.PageNumber, flag)
}
for (ParsingGetResponse.Metadata.Page page : result.metadata().get().pages()) {
    String flag = page.costOptimized().orElse(false) ? "cost-optimized" : "premium tier";
    System.out.printf("page %d: %s%n", page.pageNumber(), flag);
}

CLI 用 --expand metadata 重新请求同一个任务,并用 jq 读出逐页标记:

终端窗口

llp parsing get --job-id "$JOB_ID" --expand metadata | jq -r '
  .metadata.pages[]
  | "page \(.page_number): \(if .cost_optimized then "cost-optimized" else "premium tier" end)"'

如果你看到一页明明有清晰表格却是 cost_optimized: True,那就不对了 —— 分类器本该把表格页路由到 premium 档。请带上这份文档提一个支持工单,好让团队调整这个启发式。关于路由如何工作的更多说明,见成本优化器。

4. 在 items 树里找出所有表格

items 视图返回逐页的结构化树,元素都带类型:标题、段落、表格、图形。表格是 item.type == "table",带一个 rows 字段(列表的列表),以及同一份数据的 csv、html 与 md 三种表示。

遍历每一页、收集每一张表,并给每张表标上它的来源页,方便日后回溯:

  • Python
  • TypeScript
  • Go
  • Java
  • CLI
all_tables = []

for page in result.items.pages:
    for item in page.items:
        if getattr(item, "type", None) == "table":
            all_tables.append({
                "page_number": page.page_number,
                "rows": item.rows,
                "csv": item.csv,
            })

print(f"Found {len(all_tables)} tables across {len(result.items.pages)} pages.")
for t in all_tables:
    n_rows = len(t["rows"])
    n_cols = len(t["rows"][0]) if t["rows"] else 0
    print(f"  page {t['page_number']}: {n_rows} rows × {n_cols} cols")
const allTables = [];

for (const page of result.items.pages) {
  for (const item of page.items) {
    if ("type" in item && item.type === "table" && "rows" in item) {
      allTables.push({
        page_number: page.page_number,
        rows: item.rows,
        csv: item.csv,
      });
    }
  }
}

console.log(`Found ${allTables.length} tables across ${result.items.pages.length} pages.`);
for (const t of allTables) {
  const nRows = t.rows.length;
  const nCols = t.rows[0]?.length ?? 0;
  console.log(`  page ${t.page_number}: ${nRows} rows × ${nCols} cols`);
}
type tableRecord struct {
  PageNumber int64
  Rows       [][]*llamacloud.TableItemRowUnion
  CSV        string
}

var allTables []tableRecord
for _, page := range result.Items.Pages {
  for _, item := range page.Items {
    if item.Type == "table" {
      allTables = append(allTables, tableRecord{
        PageNumber: page.PageNumber,
        Rows:       item.Rows,
        CSV:        item.Csv,
      })
    }
  }
}

fmt.Printf("Found %d tables across %d pages.\n", len(allTables), len(result.Items.Pages))
for _, t := range allTables {
  nRows := len(t.Rows)
  nCols := 0
  if nRows > 0 {
    nCols = len(t.Rows[0])
  }
  fmt.Printf("  page %d: %d rows x %d cols\n", t.PageNumber, nRows, nCols)
}
int tableCount = 0;
java.util.List<ParsingGetResponse.Items.Page> pages = result.items().get().pages();
for (ParsingGetResponse.Items.Page page : pages) {
    if (!page.isStructuredResult()) {
        continue; // skip failed pages
    }
    ParsingGetResponse.Items.Page.StructuredResultPage structured = page.asStructuredResult();
    for (ParsingGetResponse.Items.Page.StructuredResultPage.Item item : structured.items()) {
        if (item.isTable()) {
            tableCount++;
            int nRows = item.asTable().rows().size();
            int nCols = nRows > 0 ? item.asTable().rows().get(0).size() : 0;
            System.out.printf("  page %d: %d rows x %d cols%n",
                    structured.pageNumber(), nRows, nCols);
        }
    }
}
System.out.printf("Found %d tables across %d pages.%n", tableCount, pages.size());

终端窗口

llp parsing get --job-id "$JOB_ID" --expand items | jq -r '
  .items.pages[] as $page
  | $page.items[]
  | select(.type == "table")
  | "page \($page.page_number): \(.rows | length) rows x \(.rows[0] | length) cols"'

就这份财政部 PDF 而言,第一页含那张核心的 "Financial Position & Condition" 汇总表 —— 也就是快速上手教程 里详细展示的那一张。其后的页面则是叙述文字与更小的汇总表混排。

5. 把每张表载入 pandas

到此为止的部分在任何 SDK 上都能用。从这里开始的分析是 Python + pandas,作用在第 4 步里攒出的 all_tables 列表上。还没装 pandas 的话先装上:

终端窗口

pip install pandas

item.csv 是进入 pandas 最省事的通路。Parse 会为每张表生成一份干净的 CSV 字符串,pandas.read_csv 配合一个 StringIO 包装就能直接吃进去:

import io
import pandas as pd

dataframes = []
for t in all_tables:
    df = pd.read_csv(io.StringIO(t["csv"]))
    df["_source_page"] = t["page_number"]  # keep the source page for traceability
    dataframes.append(df)

# Quick look at the first table
if dataframes:
    print(dataframes[0].head())
    print(f"\nColumns: {list(dataframes[0].columns)}")

每个 DataFrame 都保留一个 _source_page 列,于是你可以按来源分组、带着出处写回数据仓库,或者搭一个能引用出处的 RAG 索引 —— 每一行都知道自己来自哪一页。

6. 筛出你真正关心的表

多数财务报告里只有那么几张你真正在意的“汇总”表,后面跟着一长串可以略过的附表。按内容或形状把这个列表筛一遍:

# Tables with at least 5 rows (skip tiny one-line summaries)
substantial_tables = [df for df in dataframes if len(df) >= 5]

# Or filter by column header contents
financial_tables = [
    df for df in dataframes
    if any("$" in str(c) or "Dollar" in str(c) for c in df.columns)
]

print(f"Substantial tables: {len(substantial_tables)}")
print(f"Financial tables (with $ in column headers): {len(financial_tables)}")

如果你已经知道报告里哪几节重要,也可以按来源页筛选:

# Only tables from the executive summary section (e.g. pages 1-3)
exec_summary_tables = [
    pd.read_csv(io.StringIO(t["csv"]))
    for t in all_tables
    if t["page_number"] in (1, 2, 3)
]

7. 把表格存下来供下游使用

有了 DataFrame,剩下的就是常规 pandas 操作。几种常见写法:

# Save each table as a separate CSV with a deterministic filename
for i, t in enumerate(all_tables):
    filename = f"table_p{t['page_number']:03d}_{i}.csv"
    pd.read_csv(io.StringIO(t["csv"])).to_csv(filename, index=False)

# Or write them all into a single Excel workbook, one sheet per table
with pd.ExcelWriter("financial_tables.xlsx") as writer:
    for i, t in enumerate(all_tables):
        df = pd.read_csv(io.StringIO(t["csv"]))
        sheet_name = f"page{t['page_number']}_{i}"[:31]  # Excel limit
        df.to_excel(writer, sheet_name=sheet_name, index=False)

如果你更想让 Parse 一次性把表格导出成真正的 .xlsx 文件(而不是在 Python 里循环),就开启表格转电子表格输出,并用 expand=["xlsx_content_metadata"] 取回 —— 它会给出那个 Excel 文件的预签名地址。

拿到表格之后做什么

几个建立在你现在成果之上、性价比很高的模式:

  • 能引用出处的 RAG。 靠 _source_page 列,每一行都知道自己的来源页。把这些行嵌入并建索引时,把页号存进元数据,检索器就能在回答里引用具体的页。
  • 模式校验。 如果你需要形状有保证的 JSON 而不是表格,就用 LlamaExtract 在同一份文档上配 Pydantic schema —— Extract 正是为此而生的。
  • 跨文档比较。 把同一套模式跑在多个年度的 10-K 上,再把这些 DataFrame 拼起来、加一个 _year 列,就能跟踪各项指标随时间的变化。

另请参阅

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。