HTML Input

CSV Output

Success
Warning

Why copying a table into a spreadsheet is harder than it looks

You have a table on a web page and you want it in a spreadsheet. Copy-pasting straight into Excel loses the column boundaries about half the time — merged cells especially — 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, and what comes out the other side pastes cleanly into a sheet.

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 CSV column, and the file looks entirely reasonable until someone sorts by the wrong header. 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, and its value is repeated into every one of them.

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 and does not add rows the outer table never had.

CSV itself adds a wrinkle the JSON version of this page does not have. A table cell has no type — the HTML specification has nothing to say about whether 007 is a number — but CSV cannot record a type either way. There is no toggle here for "make this a number," because a CSV field is just characters between commas; quoting it only means the value contains a comma, a quote or a line break, not that it is text. Every cell is written out exactly as its text, which happens to be the honest answer for a format with no type system of its own.

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>, becomes the first line of the CSV. Without either, every row — including whatever looks like a header — is written out as an ordinary line, which is what CSV expects since it has no separate concept of column names.
  4. Choose how much gets quotedOnly where needed follows RFC 4180 and wraps a field only when it holds a comma, a quote or a line break. Every field quotes everything, for loaders that expect it regardless.
  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 once the CSV is open in a spreadsheet, 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 third row has one cell spanning the last two columns. A parser that reads cells in order puts Suspended under iccid and drops the rsrp column entirely for that row; this one repeats the merged value into both columns it actually covers, so the CSV stays the right width.

table markup → CSV 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.csvColumns 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, and a CSV drops straight into a spreadsheet or a quick script.

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 as a flat file without opening anything that would reformat it on the way.

Feeding a bulk import or a script

A CSV pastes into a spreadsheet, loads with COPY or LOAD DATA INFILE, or reads with any language's standard library. If the destination speaks JSON instead, HTML Table to JSON runs the same parser and keeps values that would round as strings.

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 — and whichever it finds becomes the first CSV line if anything.
  • Quoting follows RFC 4180, and only wraps a field when it actually needs it. Long IDs and zero-padded codes are written as plain digits, unquoted, exactly as the cell held them.
  • Ragged rows are padded rather than dropped, so the field count out equals the widest row 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 a CSV with fewer fields on that line than every other row — which breaks the first tool that expects a rectangular file. The badge tells you how many cells were merged so the repetition is never a surprise.

My CSV came out with no header line.

The table has no <thead> and its first row is not made entirely of <th> cells, so there was nothing to treat as column names — plenty of tables are styled to look like they have a header without marking one up in the markup. Every row, including that one, is written out as data instead.

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 field. If you need the URLs, this is the point where a real scraper starts to earn its keep.

Why does a long ID stay exact instead of turning into a number?

Because CSV has nowhere to record that it is a number in the first place — a field is just text, and this page never runs it through Number() or anything like it. That happens to be the safe outcome: JavaScript stops holding integers exactly past 253, so a spreadsheet program that "helpfully" retypes the column on import is the thing to watch for, not this conversion.

Related tools

Worth reading