# HTML Table Extractor: Web Tables to JSON and CSV (`itayozer9/html-table-extractor`) Actor

Extract every HTML table from any list of web pages into clean JSON rows and CSV: headers detected, merged cells expanded, numbers parsed, with caption and nearest heading for context. Plain HTTP, obeys robots.txt. Built for AI agents.

- **URL**: https://apify.com/itayozer9/html-table-extractor.md
- **Developed by:** [Itay Ozer](https://apify.com/itayozer9) (community)
- **Categories:** AI, Agents, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 table extracteds

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

## HTML Table Extractor: Web Tables to JSON and CSV

Give it web page URLs, get back every data table on those pages as clean JSON rows and CSV. Headers are detected, merged cells are expanded, numbers come out as numbers, and each table carries its caption and the nearest heading above it, so an AI agent knows what the table is about without reading the page.

Built for AI agents, RAG pipelines and anyone who needs a Wikipedia list, a stats page or a price list as data. It reads plain HTML over HTTP (no browser), obeys each site's robots.txt and Crawl-delay, and asks one host for one page at a time.

### What you get

One dataset item per table:

- `headers`: column names in page order. Two-row headers are joined (`City proper / Population`), repeated names get `(2)`, missing ones become `column1`, `column2`
- `rows`: one object per data row, keyed by header. `1,234`, `$99.90`, `12%` and `-3.5` become numbers; ids like `007` stay text; empty cells are `null`
- `csv`: the same table as CSV text with a header line, original cell text
- `caption` (the table's caption or its title row) and `heading` (the nearest h1 to h6 above it)
- `tableIndex`: which `<table>` on the page it is, counted from 1
- `rowCount`, `columnCount`, `hasHeader`, `truncated`
- `url`, `finalUrl` (after redirects), `pageTitle`, `statusCode`, `fetchedAt`

How tricky tables are handled:

- **Merged cells**: `rowspan` and `colspan` are expanded, so every row has every column (a merged value is copied into each cell it covers)
- **Group rows**: a row that is one cell across the whole table (Paris, Rome; Asia, Europe) labels the rows under it and becomes a `section` column
- **Title rows**: a single cell spanning the top of the table becomes the caption, not a header (Wikipedia infoboxes)
- **Noise removed**: footnote markers like `[1]`, hidden sort keys and edit links never reach your data
- **Layout tables skipped**: navigation boxes, `role="presentation"` tables, wrapper tables and tables of one row or one column are left out and never billed. Tables nested inside a wrapper are still read

Pages that give no table still get one free item with `status` and a plain `error`, so you always know what happened to every URL:

| `status` | Meaning |
|---|---|
| `ok` | A table (billed) |
| `no-tables` | The page has no data tables, or builds them with JavaScript |
| `blocked-by-robots` | The site's robots.txt does not allow it |
| `http-error` | The site answered 4xx or 5xx |
| `fetch-failed` | Timeout, DNS or connection error |
| `invalid-url`, `unsupported-content-type`, `too-many-redirects` | Bad address, not an HTML page, redirect loop |

### Input

| Field | Required | Example | Notes |
|---|---|---|---|
| `urls` | yes | `["https://en.wikipedia.org/wiki/List_of_largest_cities"]` | Up to 10,000 per run. Duplicates are read once |
| `maxTablesPerPage` | no | `50` | 1 to 1000, default 50, in page order |
| `parseNumbers` | no | `true` | Default true. Off: every value stays text |

```json
{
    "urls": [
        "https://en.wikipedia.org/wiki/List_of_largest_cities",
        "https://www.w3.org/WAI/tutorials/tables/irregular/"
    ],
    "maxTablesPerPage": 50,
    "parseNumbers": true
}
```

### Output example

```json
{
    "url": "https://www.w3.org/WAI/tutorials/tables/irregular/",
    "finalUrl": "https://www.w3.org/WAI/tutorials/tables/irregular/",
    "status": "ok",
    "statusCode": 200,
    "error": null,
    "pageTitle": "Tables with Irregular Headers | Web Accessibility Initiative (WAI) | W3C",
    "tableIndex": 1,
    "caption": null,
    "heading": "Table with two tier headers",
    "headers": ["column1", "Mars / Produced", "Mars / Sold", "Venus / Produced", "Venus / Sold"],
    "rows": [
        { "column1": "Teddy Bears", "Mars / Produced": 50000, "Mars / Sold": 30000, "Venus / Produced": 100000, "Venus / Sold": 80000 },
        { "column1": "Board Games", "Mars / Produced": 10000, "Mars / Sold": 5000, "Venus / Produced": 12000, "Venus / Sold": 9000 }
    ],
    "csv": "column1,Mars / Produced,Mars / Sold,Venus / Produced,Venus / Sold\nTeddy Bears,\"50,000\",\"30,000\",\"100,000\",\"80,000\"\nBoard Games,\"10,000\",\"5,000\",\"12,000\",\"9,000\"",
    "rowCount": 2,
    "columnCount": 5,
    "hasHeader": true,
    "truncated": false,
    "fetchedAt": "2026-10-31T19:00:00.000Z"
}
```

Column order is the order of `headers`. Some JSON tools sort number-like keys (such as `2024`) to the front of an object, so use `headers` (or `csv`) when column order matters.

### Pricing

You pay per table returned, nothing per page or per run beyond Apify's standard start fee:

| Event | Price | When |
|---|---|---|
| Table extracted | $0.0005 | Every table in your dataset |

Example: 1,000 tables cost $0.50. A Wikipedia page with 4 data tables costs $0.002.

Free: pages that fail, pages blocked by robots.txt, pages with no tables, and layout or navigation tables. The actor checks your run's maximum charge before each page and stops cleanly when it is reached, so a run never goes over the limit you set.

### Limits

- Plain HTTP only: tables that a page draws with JavaScript after loading are not seen (you get `no-tables`). Most Wikipedia, government, sports-reference, documentation and price-list pages serve their tables in the HTML
- Pages are read up to 4 MB and 100,000 cells; a table cut at that point has `truncated: true`. A single table too large for one dataset item (about 9 MB) keeps its rows, drops the `csv` copy and, if still too large, is cut
- One host is read one request at a time, at least 0.25 s apart (longer when its robots.txt sets a Crawl-delay, up to 10 s). Many URLs on one site therefore take a while; different sites run in parallel
- Public http and https addresses only. No logins, no cookies, no proxies
- Numbers: European formats like `1.234,5` and ranges like `10-20` stay text on purpose, so nothing is guessed

### FAQ

**Which pages work best?**
Any page whose tables are in the HTML: Wikipedia lists and infoboxes, government statistics, sports stats, exchange or rate tables, documentation reference tables, price lists.

**How do I use it from an AI agent?**
Call it through the Apify API or the Apify MCP server with a list of URLs. Each table comes back with its `heading` and `caption`, so the agent can pick the right table without the page.

**Why did a page give `no-tables` when I can see a table in my browser?**
The site builds that table with JavaScript, or the "table" is made of styled boxes rather than a real `<table>`. Use a browser-based scraper for those pages.

**Do you respect robots.txt?**
Yes. Every site's robots.txt is read before its pages (the bot name is `TableExtractorBot`). Disallowed pages are skipped, returned with `blocked-by-robots` and not charged.

**Is there personal data in the results?**
The actor returns only what the tables on the pages you choose contain. You are responsible for the URLs you send and for how you use the data.

**Something broke or a table looks wrong. What do I do?**
Open an issue on the Issues tab with your run link and the page URL. Issues are answered within a few days.

# Actor input Schema

## `urls` (type: `array`):

Web pages to read, one full address per line (https://...). Up to 10,000 per run. Every data table on each page becomes one result. You pay only per table returned: pages that fail, are blocked by robots.txt or have no tables are free.

## `maxTablesPerPage` (type: `integer`):

Most tables to return from one page (1 to 1000), in page order. Layout and navigation tables are skipped and never count.

## `parseNumbers` (type: `boolean`):

Turn cells such as 1,234 or $99.90 or 12% or -3.5 into JSON numbers in rows. Ids with leading zeros and very long digit strings stay text. The csv field always keeps the original text.

## Actor input object example

```json
{
  "urls": [
    "https://en.wikipedia.org/wiki/List_of_largest_cities",
    "https://www.w3.org/WAI/tutorials/tables/irregular/"
  ],
  "maxTablesPerPage": 50,
  "parseNumbers": true
}
```

# Actor output Schema

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

One item per table found (headers, rows, CSV, caption, heading), plus one free item per page that failed or had no tables.

# 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 = {
    "urls": [
        "https://en.wikipedia.org/wiki/List_of_largest_cities",
        "https://www.w3.org/WAI/tutorials/tables/irregular/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("itayozer9/html-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 = { "urls": [
        "https://en.wikipedia.org/wiki/List_of_largest_cities",
        "https://www.w3.org/WAI/tutorials/tables/irregular/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("itayozer9/html-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 '{
  "urls": [
    "https://en.wikipedia.org/wiki/List_of_largest_cities",
    "https://www.w3.org/WAI/tutorials/tables/irregular/"
  ]
}' |
apify call itayozer9/html-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,itayozer9/html-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/fYEIh5WaxAe561grm/builds/f3zyw1NiSB1fBFK0I/openapi.json
