Table Extractor — Scrape HTML Tables from Any URL to JSON/CSV
Pricing
from $16.00 / 1,000 table extractions
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
2
Monthly active users
10 days ago
Last modified
Categories
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:
| Field | Meaning |
|---|---|
url | The final URL after redirects |
tableCount | How many data tables were kept |
tables[].index | 0-based position among the kept tables |
tables[].headers | Detected header labels, in column order |
tables[].rowCount | Body rows in that table |
tables[].rows | Array of objects keyed by header text; unnamed columns fall back to col0, col1, … and a duplicate header gets _<index> appended |
error | Present 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
| Field | Type | Default | What it does |
|---|---|---|---|
url | string | a demo page | A single page URL |
urls | string[] | string | [] | More page URLs. A plain string is split on commas and newlines |
maxUrls | integer | 25 | Cap 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 ApifyClientclient = 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.
| Event | What one event covers | Price |
|---|---|---|
table-extracted | One 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.colspanis handled;rowspanis not. - You need typed numbers. Every cell is a string with whitespace collapsed.
"716", not716. - 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
rowspanis not reconstructed;colspanis 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.


