# PDF Table and Text Extractor: Tables to JSON, CSV, Markdown (`nouten-nouen/pdf-table-extractor`) Actor

Extract tables and text from PDF files. Tables keep their rows and columns, as JSON, CSV and Markdown. Handles ruled and borderless tables, tables across pages, and CJK text. Pay per page read. Scanned pages and failed files are free.

- **URL**: https://apify.com/nouten-nouen/pdf-table-extractor.md
- **Developed by:** [のうてん 農園](https://apify.com/nouten-nouen) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 page reads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PDF Table and Text Extractor

Get the tables and the text out of a PDF, such as financial statements, statistical reports, price lists and government data. Give it a PDF link, and get each table back as JSON, CSV (which opens in Excel) and Markdown, plus the text of each page.

General PDF text extractors tend to flatten a table into lines of text, which loses track of which value belongs to which column. This Actor keeps that structure, and gives you the plain text as well.

### Quick start

1. Paste one or more PDF links into **PDF URLs**. The prefilled example works as it is.
2. Click **Start**.
3. Open the **Results** view, or export the dataset.

Choose **Text only** if you don't need tables. It is faster, and costs the same per page.

Here is one table from the output:

```json
{
  "recordType": "table",
  "page": 1,
  "rowCount": 3,
  "columnCount": 4,
  "headers": ["Region", "Q1", "Q2", "Total"],
  "rows": [{ "Region": "North", "Q1": "1,200", "Q2": "1,350", "Total": "2,550" }],
  "csv": "Region,Q1,Q2,Total\nNorth,\"1,200\",\"1,350\",\"2,550\"\n"
}
```

### What it does

- Finds tables drawn with ruling lines, and tables that only line up by their text
- Joins a table that runs over several pages into one table, and removes repeated header rows
- Separates rows that the PDF stacks inside one cell, which is common in statistical reports
- Reads tables on sideways pages, and text the PDF draws twice to fake bold
- Sets aside title rows and footnotes in `caption` and `notes`, so they don't end up in your data
- Keeps blank cells in place, so values stay in the right column
- Joins text that wraps inside a cell, with correct handling of Japanese and Chinese
- Skips ordinary paragraphs, page frames and charts instead of reporting them as tables
- Reads text in order on two-column pages, so the left and right columns don't get mixed together

### How accurate is it?

We measured table extraction on the ICDAR 2013 table competition set (67 real PDFs with 156 human-checked tables), scoring which cell sits next to which.

| Tool | Finds (recall) | Correct (precision) | F1 |
|---|---|---|---|
| This Actor | 0.908 | 0.914 | 0.911 |
| Camelot 2.0 `hybrid`, default settings | 0.861 | 0.747 | 0.800 |
| Camelot 2.0 `network`, default settings | 0.785 | 0.802 | 0.794 |
| Camelot 2.0 `stream`, default settings | 0.860 | 0.685 | 0.762 |
| Camelot 2.0 `lattice`, default settings | 0.227 | 0.941 | 0.366 |

This Actor was improved using the same set, so expect lower scores on your own files. About 6 in 10 of the documents scored 90% or better on both measures. The rest had some cells misplaced or missed, so check any figures that matter.

### What it does not do

- **Scanned PDFs:** files with no text layer need OCR first. They return `no_text_layer`, and you are not charged.
- **Password-protected PDFs** are not supported.
- **Multi-row and merged headers:** these are not rebuilt. The item includes a warning when many header cells are blank.
- **Columns set very close together:** these may be merged into one column.
- **Tables next to charts:** on a busy page, a table beside a chart may be missed.
- **Pages with three or more columns:** the text is read straight across, so the columns get mixed. The text of a table also reads straight across, row by row.

### Input

```json
{
  "pdfUrls": ["https://example.com/report.pdf"],
  "content": "both",
  "pages": "1-3, 5",
  "strategy": "auto",
  "firstRowIsHeader": true
}
```

### Output

`content` can be `both`, `tables` or `text`. Each table is one dataset item:

```json
{
  "recordType": "table",
  "sourceUrl": "https://example.com/report.pdf",
  "tableIndex": 1,
  "page": 1,
  "pages": [1, 2],
  "tableIndexOnPage": 1,
  "strategy": "lines",
  "rowCount": 3,
  "columnCount": 4,
  "caption": "Monthly report",
  "notes": null,
  "headers": ["Region", "Q1", "Q2", "Total"],
  "rows": [{ "Region": "North", "Q1": "1,200", "Q2": "1,350", "Total": "2,550" }],
  "cells": [["Region", "Q1", "Q2", "Total"], ["North", "1,200", "1,350", "2,550"]],
  "markdown": "| Region | Q1 | Q2 | Total |\n| --- | --- | --- | --- |\n| North | 1,200 | 1,350 | 2,550 |",
  "csv": "Region,Q1,Q2,Total\nNorth,\"1,200\",\"1,350\",\"2,550\"\n",
  "boundingBox": [72.0, 72.0, 523.3, 150.0],
  "warnings": []
}
```

A very large table includes only the `csv` field, with a warning, because dataset items have a size limit.

Each page with text is one item, with `recordType`, `sourceUrl`, `page` and `text`.

Each PDF also gets one item with `"recordType": "status"`. Its `status` is one of:

| Status | Meaning | Charged |
|---|---|---|
| `ok` | Tables or text were found | Per page read |
| `no_tables` | With `tables` only: the file was read, and no tables were found | No |
| `no_text_layer` | The file looks scanned | No |
| `failed` | The file could not be downloaded or read. See `message` | No |
| `spending_limit` | Your spending limit doesn't cover this file's pages, so its tables are not included and the run stops | No |

### Pricing

You pay per page that has text, whether you ask for tables, text or both. Use the `pages` input to read only the pages you need. Scanned pages and files that fail are free. With `tables` only, a file with no tables is free too. The `pagesCharged` field in each file's status shows how many pages you paid for.

### Your responsibility

Only process documents you have the right to use. Do not send confidential documents through public URLs.

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct links to PDF files. A viewer page will not work. The file must be readable without logging in.

## `content` (type: `string`):

Tables come back as rows and columns. Text comes back page by page, in reading order.

## `pages` (type: `string`):

Pages to read, such as 1-3, 5, 8-. Leave empty to read every page.

## `strategy` (type: `string`):

Auto uses ruling lines first, and looks at text alignment on pages where it finds none.

## `firstRowIsHeader` (type: `boolean`):

Use the first row as column names. Turn this off for tables with no header row.

## `minRows` (type: `integer`):

Ignore tables with fewer rows than this, counting the header.

## `minColumns` (type: `integer`):

Ignore tables with fewer columns than this.

## `maxFileSizeMb` (type: `integer`):

Skip files larger than this.

## `downloadTimeoutSecs` (type: `integer`):

Give up on a file if the server stays silent for this long.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/v0.11.7/examples/pdfs/ca-warn-report.pdf"
  ],
  "content": "both",
  "strategy": "auto",
  "firstRowIsHeader": true,
  "minRows": 2,
  "minColumns": 2,
  "maxFileSizeMb": 100,
  "downloadTimeoutSecs": 120
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "pdfUrls": [
        "https://raw.githubusercontent.com/jsvine/pdfplumber/v0.11.7/examples/pdfs/ca-warn-report.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nouten-nouen/pdf-table-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "pdfUrls": ["https://raw.githubusercontent.com/jsvine/pdfplumber/v0.11.7/examples/pdfs/ca-warn-report.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("nouten-nouen/pdf-table-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "pdfUrls": [
    "https://raw.githubusercontent.com/jsvine/pdfplumber/v0.11.7/examples/pdfs/ca-warn-report.pdf"
  ]
}' |
apify call nouten-nouen/pdf-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nouten-nouen/pdf-table-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ccoJalLPwLUiymCMh/builds/TaKBMctTgqXTkcenj/openapi.json
