HTML Input

CSV Output

Success
Warning

Why copying a table out of a page is harder than it looks

You have a table on a web page and you want the data. Copy-pasting into a spreadsheet loses the column boundaries about half the time, and writing a scraper for a one-off job is an hour you will not get back. View source, copy the <table>, paste it here.

The reason a five-line parser is not enough is colspan and rowspan. Read the cells in the order they appear in the markup and every row after a merged cell lands one column to the left — the values are all present, all in the wrong place, and the output looks entirely reasonable. That is worse than an error, because nothing tells you to check. Cells here are placed into a reserved grid, the same way a browser lays a table out, so a merged cell occupies the slots it actually covers.

Nested tables are the second trap. Asking a table for all its <tr> elements also returns the rows of any table inside one of its cells, which used to be how half the web did layout. The rows are collected by walking direct children instead, so an inner table stays inside its cell rather than merging into the outer one.

And the values themselves stay text on the way in. A table cell has no type — the HTML specification has nothing to say about whether 007 is a number — so the typing decision is yours, made on the way out, and made conservatively.

Getting the data out

  1. Paste the markupThe whole page works, or just the <table> element. In most browsers, right-click the table, Inspect, then right-click the <table> node and copy its outer HTML.
  2. Pick the tablePages usually have more than one. Each is named from its caption or id where it has one, so a page full of tables is still navigable.
  3. Check the header guessA <thead>, or a first row made entirely of <th>, is treated as column names. Some tables use neither — turn the toggle off and you get arrays instead.
  4. Decide about typed valuesOn, and 42 becomes a number. Off, and everything is a string. Either way nothing that would change under conversion gets converted.
  5. Copy or downloadNothing is uploaded. The markup is parsed inertly in this tab, so no script in it runs and no image or tracker in it loads.

If the columns look shifted, check for a merged cell above the point where things go wrong — and look at the merged-cells badge. That number is the fastest way to tell a genuinely ragged table from one this page has laid out correctly.

A table with a merged cell

The second row has one cell spanning two columns. A parser that reads cells in order puts −92 under imsi and loses a column; this one keeps every value under the right name.

table markup → JSON Spans laid out, not flattened
page.htmlOne colspan="2"
<tr><th>msisdn</th><th>iccid</th><th>rsrp</th></tr>
<tr><td>447700900142</td><td>8901240544102066246</td><td>-92</td></tr>
<tr><td>447700900377</td><td colspan="2">Suspended</td></tr>
table.jsonColumns still aligned
msisdn,iccid,rsrp
447700900142,8901240544102066246,-92
447700900377,Suspended,Suspended
# the merged cell fills both columns it covers

When this is what you want

A one-off extraction

A reference table, a price list, a fixture list — something you need once and will never need again. Writing and debugging a scraper for that is disproportionate.

Checking what a scraper should produce

Before you write the real thing, paste the markup here and look at the shape. Merged cells and header rows are exactly the details that make a scraper wrong on page two.

Rescuing data from a report

Exported reports are frequently HTML pretending to be a document. The table inside is real data, and this gets it out without opening anything that would reformat it on the way.

Feeding the rest of the site

Once it is JSON you can read it as a table, export it as CSV, or convert it to YAML. Or go straight to HTML Table to CSV if a spreadsheet is where it is heading.

What it handles

  • colspan and rowspan are laid out, not ignored. A merged cell fills every position it covers, so the columns after it stay aligned — verified against twenty cases including a rowspan in a middle column, which is the one that usually breaks.
  • Nested tables stay nested. An inner table does not leak its rows into the outer one, and is not offered twice.
  • Header detection follows the markup. <thead> first, then an all-<th> first row, then nothing.
  • Typing only when it is reversible. 8901240544102066246, 007 and 1.50 stay strings, because converting any of them changes what they say. The rule is the same one CSV to JSON applies.
  • Ragged rows are padded rather than dropped, so the row count out equals the row count in.
  • Nothing is executed and nothing is fetched. The markup is parsed into an inert document — no script runs, no tracking pixel loads — and nothing is uploaded.

Questions people actually ask

Is it safe to paste a page from a site I do not trust?

Yes. DOMParser builds a document that is inert by specification: scripts in it never execute and its images and iframes are never fetched. Only the text of cells is read, and none of the markup is ever inserted back into this page.

Why is the same value repeated across several columns?

Because a cell spanning two columns says that value applies to both. Leaving the extra columns blank would be the alternative, and it produces unnamed columns you cannot filter or key on. The badge tells you how many cells were merged so the repetition is never a surprise.

My table came out with no column names.

It has no <thead> and its first row is not made of <th> cells, so there was nothing to read them from — plenty of tables are styled to look like they have a header without marking one up. Leave the header toggle on and the first row is used anyway, or turn it off for plain arrays.

Can I paste a whole page rather than just the table?

Yes, and it is usually easier. Every table in the document is found and listed, so you can page through them and take the one you want.

What happens to links and images inside a cell?

You get the text. A cell containing a link becomes its label, and an image with no text alongside becomes an empty cell. If you need the URLs, this is the point where a real scraper starts to earn its keep.

Why is my long ID a string?

Because it cannot be a number without changing. JavaScript stops holding integers exactly past 253, so an account number would come back rounded, and two adjacent IDs can collapse onto the same value.

Related tools

Worth reading