Parse a Financial Report and Extract Every Table
Extract every table from a multi-page financial report with LlamaParse, walking the items tree and loading each table into pandas via its CSV.
This tutorial walks through one of the most common Parse use cases: pulling every table out of a financial report and loading it into pandas. We use the same 2024 Executive Summary by the Bureau of the Fiscal Service as the Quick Start tutorial, so you can compare the two end-to-end.
The pattern in this tutorial generalizes to any financial document with mixed prose and tables — 10-Ks, earnings reports, investor decks, audit reports.
When to use this pattern
Section titled “When to use this pattern”- Your document has multiple tables across multiple pages and you want all of them, not just one
- You need the table data as structured rows, not just markdown text
- You want to load tables directly into pandas for downstream analysis or RAG indexing
If you only need a single table from a known page, see Parse Charts in PDFs and Analyze with Pandas — it shows the simpler “one specific table” version. If you only need text or markdown, Quick Start: Parse a PDF & Interpret Outputs is enough.
1. Setup
Section titled “1. Setup”Set your API key as an environment variable so the SDKs pick it up automatically:
export LLAMA_CLOUD_API_KEY="llx-..."Install the SDK and construct a client:
pip install "llama-cloud>=2.8"from llama_cloud import LlamaCloud
client = LlamaCloud() # reads LLAMA_CLOUD_API_KEY from the environmentnpm install @llamaindex/llama-cloudimport LlamaCloud from '@llamaindex/llama-cloud';import fs from 'fs';
const client = new LlamaCloud(); // reads LLAMA_CLOUD_API_KEY from the environmentgo get github.com/run-llama/llama-parse-goimport ( "context"
llamacloud "github.com/run-llama/llama-parse-go")
ctx := context.Background()client := llamacloud.NewClient() // reads LLAMA_CLOUD_API_KEY from the environmentimplementation("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 environmentLlamaCloudClient client = LlamaCloudOkHttpClient.fromEnv();go install github.com/run-llama/llama-parse-cli/cmd/llp@latestllp reads LLAMA_CLOUD_API_KEY from the environment (or pass --api-key).
2. Parse the document
Section titled “2. Parse the document”Upload the PDF and run a parse job. We pick agentic for solid table accuracy and request the items view so we get a structured tree we can walk for tables.
For a long mixed-complexity report, enable Cost Optimizer. The Treasury document has many text-heavy narrative pages and a handful of table-heavy summary pages — Cost Optimizer will route the simple pages to cost_effective automatically and only spend premium credits on the table pages. We also request metadata so we can see which pages got cost-optimized.
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}`);expand is a GET parameter in Go, so create the job, poll until it reaches a terminal status, then fetch the result with the fields you want:
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 calljob, 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 resultgetParams := 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))expand is a query parameter in Java, so create the job, poll until it reaches a terminal status, then fetch the result with the fields you want:
FileCreateResponse file = client.files().create(FileCreateParams.builder() .file(Paths.get("executive-summary-2024.pdf")) .purpose("parse") .build());
// Enable Cost Optimizer on the create callParsingCreateResponse 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 resultParsingGetParams 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());expand is a query parameter for the CLI, so upload, create the job, poll until it reaches a terminal status, then fetch the result with the fields you want:
# Upload the reportFILE_ID=$(llp files create \ --file executive-summary-2024.pdf \ --purpose parse | jq -r '.id')
# Start a parse job with Cost Optimizer enabledJOB_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 statuswhile true; do STATUS=$(llp parsing get --job-id "$JOB_ID" | jq -r '.job.status') case "$STATUS" in COMPLETED|FAILED|CANCELLED) break ;; esac sleep 2doneecho "Job status: $STATUS"
# Total pagesllp parsing get --job-id "$JOB_ID" --expand items | jq '.items.pages | length'Where each option lives in the request shape: tier and version are top-level required fields, processing_options.cost_optimizer.enable enables per-page tier routing, and expand is a top-level array that controls what comes back. See Configuration Model for the full picture.
3. See which pages were cost-optimized
Section titled “3. See which pages were cost-optimized”Cost Optimizer adds a cost_optimized flag to the per-page metadata. Inspect it to confirm the routing landed where you’d expect — text-heavy narrative pages should be True (processed on cost_effective) and table-heavy pages should be False (processed on agentic).
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);}The CLI re-requests the same job with --expand metadata and reads the per-page flag with 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)"'If you see a page with a clear table but cost_optimized: True, something’s off — the classifier is supposed to route table pages to the premium tier. File a support ticket with the document so the team can tune the heuristic. For more on how routing works, see Cost Optimizer.
4. Find every table in the items tree
Section titled “4. Find every table in the items tree”The items view returns a structured tree of typed elements per page: headings, paragraphs, tables, figures. Tables are item.type == "table" and carry a rows field (a list of lists), plus csv, html, and md representations of the same data.
Walk every page, collect every table, and tag each with its source page so you can trace back later:
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 []tableRecordfor _, 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"'For our Treasury PDF, the first page contains the marquee “Financial Position & Condition” summary table — the same one shown in detail in the Quick Start tutorial. Subsequent pages contain narrative prose mixed with smaller summary tables.
5. Load each table into pandas
Section titled “5. Load each table into pandas”Everything up to this point works from any SDK. The analysis from here is Python + pandas, operating on the all_tables list built in step 4. Install pandas if you don’t have it already:
pip install pandasitem.csv is the easiest path into pandas. Parse generates a clean CSV string for every table, which pandas.read_csv can ingest directly via a StringIO wrapper:
import ioimport 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 tableif dataframes: print(dataframes[0].head()) print(f"\nColumns: {list(dataframes[0].columns)}")Each DataFrame keeps a _source_page column so you can group by source, write back to your data warehouse with provenance, or build a citation-aware RAG index where every row knows which page it came from.
6. Filter to specific tables you care about
Section titled “6. Filter to specific tables you care about”Most financial reports have a few “summary” tables you actually care about and a long tail of supplementary tables you’d skip. Filter the list by content or shape:
# 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 contentsfinancial_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)}")You can also filter by source page if you already know which sections of the report matter:
# 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. Save the tables for downstream use
Section titled “7. Save the tables for downstream use”Once you have the DataFrames, the rest is standard pandas. A few common patterns:
# Save each table as a separate CSV with a deterministic filenamefor 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 tablewith 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)If you’d rather have Parse export the tables as a real .xlsx file in one shot (instead of looping in Python), enable tables-as-spreadsheet output and retrieve via expand=["xlsx_content_metadata"] — it returns a presigned URL for the Excel file.
What to do with the tables next
Section titled “What to do with the tables next”A few high-leverage patterns that build on what you have now:
- Citation-aware RAG. Each row knows its source page from the
_source_pagecolumn. When you embed and index the rows, store the page number in metadata. Your retriever can cite specific pages back in answers. - Schema validation. Use LlamaExtract on the same document with a Pydantic schema if you need guaranteed-shape JSON instead of tables — Extract is purpose-built for that.
- Cross-document comparison. Run this same pattern across multiple years’ worth of 10-Ks, then concatenate the DataFrames with a
_yearcolumn to track metric changes over time.
See also
Section titled “See also”- Parse Charts in PDFs and Analyze with Pandas — the chart-focused version of this tutorial
- Quick Start: Parse a PDF & Interpret Outputs — what each
expandview returns, in detail - Cost Optimizer — per-page tier routing
- Tiers — pick the right tier for your document
- Retrieving Results — every legal
expandvalue - LlamaExtract — schema-driven extraction for when you need guaranteed JSON shape