Table Extractor — Scrape HTML Tables from Any URL to JSON/CSV avatar

Table Extractor — Scrape HTML Tables from Any URL to JSON/CSV

Pricing

from $16.00 / 1,000 table extractions

Go to Apify Store
Table Extractor — Scrape HTML Tables from Any URL to JSON/CSV

Table Extractor — Scrape HTML Tables from Any URL to JSON/CSV

Table extractor for webpages: pass a URL and get every HTML data table back as structured rows — JSON via the API or dataset, CSV/Excel via one-click export. Layout tables skipped automatically. For analysts, developers and AI agents. $0.02 per page, failures free.

Pricing

from $16.00 / 1,000 table extractions

Rating

0.0

(0)

Developer

Broke to Built

Broke to Built

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

2

Monthly active users

10 days ago

Last modified

Share

Webpage Tables Extractor — HTML tables from any URL to JSON or CSV

Give it a page URL, get every real data <table> on it back as JSON — headers detected, rows as objects keyed by header, ready for a spreadsheet, an LLM, or a data pipeline. One page or up to 50 in a run. Download the same result as CSV or Excel from the run's Storage tab.

Callable as a plain HTTP API and as an agent tool over Apify MCP.

$0.02 per page processed, however many tables that page holds.

Who uses it

  • Analysts pulling a reference table off a wiki, a government page, or a docs site without copy-paste.
  • Developers who need tabular data from a page that has no API and do not want to write a parser.
  • AI agents handed a page link that must reason over the numbers on it.
  • No-code builders (Make, n8n, Zapier) that can call a URL but cannot parse HTML.

What you get

One dataset item per page:

FieldMeaning
urlThe final URL after redirects
tableCountHow many data tables were kept
tables[].index0-based position among the kept tables
tables[].headersDetected header labels, in column order
tables[].rowCountBody rows in that table
tables[].rowsArray of objects keyed by header text; unnamed columns fall back to col0, col1, … and a duplicate header gets _<index> appended
errorPresent instead of tables when the page could not be fetched. Never charged

All cell values are strings with whitespace collapsed — "1,576,000", not 1576000. Cast on your side.

Layout and spacer tables are skipped automatically: anything with fewer than 2 body rows or fewer than 2 columns is dropped, so you get data tables, not page scaffolding.

Examples

1. A real Wikipedia page

Input:

{ "url": "https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue" }

Output from an actual run, trimmed to the first rows of the first of 6 tables:

{
"url": "https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue",
"tableCount": 6,
"tables": [
{
"index": 0,
"headers": ["Ranks", "Name", "Industry", "Revenue", "Profit", "Employees", "Headquarters[note 1]", "State-owned", "Ref."],
"rowCount": 51,
"rows": [
{
"Ranks": "1", "Name": "Amazon",
"Industry": "Retail Information technology",
"Revenue": "716", "Profit": "79.9", "Employees": "1,576,000",
"Headquarters[note 1]": "United States", "State-owned": "", "Ref.": "[5]"
},
{
"Ranks": "3", "Name": "State Grid Corporation of China",
"Industry": "Electricity",
"Revenue": "545", "Profit": "9.2", "Employees": "1,361,423",
"Headquarters[note 1]": "China", "State-owned": "", "Ref.": "[7]"
}
]
}
]
}

Read the next section before you trust every row of a page like that one. The same real run also produced this row, and it is wrong:

{
"Ranks": "5", "Name": "China National Petroleum Corporation",
"Industry": "476", "Revenue": "25.2", "Profit": "1,026,301", "Employees": "China"
}

The values shifted one column left because that row uses rowspan (a merged cell carried down from the row above), which this actor does not reconstruct. On tables with merged vertical cells, some rows will be misaligned. That is a real limitation, stated here rather than discovered by you in production.

2. Several pages in one run

{
"urls": [
"https://example.com/report-a",
"https://example.com/report-b"
],
"maxUrls": 25
}

One dataset item per page. Download the whole dataset as CSV or Excel from the run's Storage tab if a spreadsheet is where this is going.

3. A page that cannot be fetched

{ "url": "https://example.com/gone", "error": "HTTP 404" }

The run continues with the remaining pages and that page is not charged.

Input

FieldTypeDefaultWhat it does
urlstringa demo pageA single page URL
urlsstring[] | string[]More page URLs. A plain string is split on commas and newlines
maxUrlsinteger25Cap on pages per run (hard max 50). This is also your spend cap

url and urls are merged and deduplicated, and a URL with no scheme gets https:// added.

Call it from code

curl -X POST "https://api.apify.com/v2/acts/eliai~webpage-tables-extractor/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue"}'

Python (pip install apify-client):

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("eliai/webpage-tables-extractor").call(run_input={"url": "https://example.com/report"})
page = next(client.dataset(run["defaultDatasetId"]).iterate_items())
for t in page["tables"]:
print(t["index"], t["headers"], t["rowCount"], "rows")

Pricing

Pay per event, one event: table-extracted.

EventWhat one event coversPrice
table-extractedOne page fetched and parsed — every data table on it, however many$0.02

A page with one table and a page with six both cost $0.02. A 25-page run costs $0.50, the 50-page maximum costs $1.00. maxUrls is your spend cap. There is no start fee and no monthly fee, and a page that fails to load is recorded with an error and never charged.

Honest comparison: pandas.read_html(url) is free and does much the same job, with better rowspan handling. Use this when you want it hosted — no Python runtime, batching, a JSON dataset a no-code tool or an agent can consume, and CSV/Excel export from the run page.

When NOT to use this

  • The table is rendered by JavaScript. Only the served HTML is parsed; no browser runs. A dashboard or a client-rendered grid will come back with tableCount: 0.
  • The table uses merged cells vertically (rowspan). Those rows shift, as shown above. colspan is handled; rowspan is not.
  • You need typed numbers. Every cell is a string with whitespace collapsed. "716", not 716.
  • You need the table's links, images, or formatting. Only visible text is extracted.
  • The data is not in a <table> element. CSS-grid "tables" and <div> layouts are invisible to this.
  • You want to crawl a site for tables. It reads exactly the URLs you list; it never follows links.
  • The page needs a login or is behind bot protection. No credentials, no proxy — those come back as errors.

Honest limits

  • rowspan is not reconstructed; colspan is expanded loosely by repeating the value.
  • Tables with fewer than 2 body rows or fewer than 2 columns are skipped as layout tables.
  • Sub-header rows inside a table body become ordinary rows with mostly-empty values.
  • Header detection prefers <thead> <th>, then the first row containing <th>, then the first row.
  • Cells are text only; whitespace is collapsed to single spaces.
  • 20-second fetch timeout per page; pages are fetched one at a time.
  • Hard cap of 50 pages per run.

FAQ

How do I scrape an HTML table into JSON or a spreadsheet? Pass the page URL — every real data table comes back as headers plus rows objects. For a spreadsheet, open the run's Storage tab and export the dataset as CSV or Excel.

How does it know what the column names are? It prefers <thead> header cells, falls back to the first row containing <th>, and finally to the first row. Each body row becomes an object keyed by that header text; unnamed columns fall back to col0, col1, … so nothing is lost.

Does it handle merged cells? colspan yes — the value is repeated across the spanned columns so cells stay aligned. rowspan no — rows that inherit a merged cell from above will be shifted by one or more columns. Check any table that merges cells vertically.

Why did a table on the page not appear in the output? Single-column tables and tables with fewer than 2 body rows are skipped on purpose; those are almost always layout or spacer tables. Every table that survives that filter is returned with its index.

Can it extract tables that JavaScript renders after load? No — it parses the served HTML, which is what keeps it fast and cheap. A client-rendered grid needs a browser-based scraper instead.

Are the numbers returned as numbers? No. Every cell is a string exactly as rendered, including thousands separators and footnote markers like "[5]". Strip and cast on your side.

Can I extract tables from several pages at once? Yes — up to 50 per run via urls. Each page is its own dataset item and one dead link never stops the rest.

Can an AI agent call this? Yes — it is exposed over Apify MCP. Input { "url": "<page url>" }, and it returns tableCount plus a tables array of header/row objects.

Who made this

Broke to Built — a company of machines, building things it gives away. This is one of them; the rest are free too.

For AI agents

This Actor is built to be called by software, not just by people.

  • Mount it directly as an MCP tool — no Store search, no ranking, just this one tool: https://mcp.apify.com/?actors=eliai/webpage-tables-extractor
  • Or call it over HTTP and get the results in the same request: POST https://api.apify.com/v2/acts/eliai~webpage-tables-extractor/run-sync-get-dataset-items
  • Pay with x402, without an Apify account. This Actor is whitelisted for agentic payments, so an agent holding USDC on Base can buy a prepaid token and spend it here. The minimum purchase is $1, the token balance is an absolute spending cap, and it expires 14 days after purchase.
  • Costs are predictable before you call. Pricing is pay-per-event (see Pricing above), so an agent can budget a run in advance instead of discovering the bill afterwards.
  • Send only the field you mean. If you pass the bulk field, it is used on its own; the single-value field is a fallback, never merged into your request. You are charged for the items you sent and nothing else.