Knowledge Base

How to Scrape a Table Using Beautifulsoup

A focused Python tutorial on parsing HTML tables with BeautifulSoup, pulling headers and rows into structured data ready for CSV or a pandas DataFrame.

Tables are everywhere on the web: pricing grids, statistics, schedules and comparison charts. Turning that <table> markup into clean rows and columns is a classic BeautifulSoup task, and once you understand the structure it is very repeatable.

This guide shows how to locate a table, read its header cells, iterate over its rows, and export the result to CSV or a pandas DataFrame for analysis.

Quick answer

Extracting table rows is step one; usable data needs cleaning. Strip currency symbols and thousands separators before converting to numbers, expand rowspan and colspan cells algorithmically so every row aligns, handle multi-row headers, and validate that each row has the column count you expect. A table that parses without errors can still be silently misaligned, so verification matters as much as extraction.

Key takeaways

  • Raw cell text is strings; numeric analysis needs deliberate cleaning of symbols, separators and units.
  • A <code>rowspan</code> cell belongs to several rows, so you must carry its value downward to keep columns aligned.
  • Some tables stack two header rows, which a single-row header reader will mangle.
  • Asserting a consistent column count per row catches misalignment that would otherwise corrupt your dataset.
  • Cells often hold nested links or spans, so decide whether you want text, an attribute, or both.
  • A budget rotating proxy such as Cheapest Proxies suits repetitive table fetches across many similar URLs.

How an HTML table is structured

A standard table nests several tags. The <table> wraps everything; <tr> defines each row; <th> holds header cells and <td> holds data cells. Many tables also separate <thead> and <tbody>. Knowing this hierarchy is the key to a reliable scraper, because you walk the same nested shape every time.

Locating the right table

Pages often contain several tables, so target the one you want by id, class, or position. Inspect the page in your browser to find a stable selector.

import requests
from bs4 import BeautifulSoup

resp = requests.get("https://example.com/stats", timeout=10)
soup = BeautifulSoup(resp.text, "html.parser")

# By id or class is most reliable:
table = soup.find("table", {"id": "results"})
# Or grab the first table on the page:
# table = soup.find("table")

Extracting the header row

Read the header cells first so you know your column names. Most tables put them in <th> elements inside the first row or the <thead>.

headers = [th.get_text(strip=True) for th in table.select("thead th")]
# Fallback if there is no thead:
if not headers:
    headers = [th.get_text(strip=True) for th in table.find("tr").find_all("th")]
print(headers)

Iterating over the data rows

Now loop through each row and pull the text from its cells. Using get_text(strip=True) trims whitespace and gives you clean values.

rows = []
for tr in table.select("tbody tr"):
    cells = [td.get_text(strip=True) for td in tr.find_all("td")]
    if cells:  # skip empty or separator rows
        rows.append(cells)

for r in rows[:5]:
    print(r)

Pairing headers with values

To build dictionaries keyed by column name, zip the headers against each row. This makes downstream code much more readable.

records = [dict(zip(headers, row)) for row in rows]

Exporting the table

Once you have rows, saving them is straightforward. The standard csv module writes a flat file, while pandas gives you a DataFrame for filtering and analysis.

import csv

with open("table.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(headers)
    writer.writerows(rows)
import pandas as pd

df = pd.DataFrame(rows, columns=headers)
print(df.head())

The pandas shortcut

For simple, well-formed tables, pandas.read_html can parse every table on a page in one call. It is fast, but BeautifulSoup gives you finer control when tables have merged cells, nested markup, or messy structure that pandas misreads.

tables = pd.read_html(resp.text)
df = tables[0]  # first table on the page

Handling tricky tables

  • Merged cells using colspan or rowspan break simple zipping; you may need to pad rows manually.
  • Nested tables can cause your selector to grab cells from the wrong table, so scope selectors tightly.
  • JavaScript-rendered tables will not appear in the raw HTML and require a browser automation tool first.

Scraping tables at scale

If you are collecting tables from many URLs, sending all requests from one IP often triggers blocks. Rotating proxies keep a large table-scraping job moving, and Cheapest Proxies is a strong value-focused option worth considering. Compare providers on rotation, reliability and price so you pay for what your job actually needs.

Comparison snapshot

A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.

ProviderBest forProfileValue
Bright DataEnterprises needing huge pools and compliance controlsEnterprise FocusedPremium
OxylabsLarge-scale scraping and data APIsEnterprise FocusedPremium
Smartproxy (Decodo)Newcomers who want an easy dashboardBeginner FriendlyGood
SOAXPrecise city and carrier targetingAutomation FriendlyGood

Cleaning cell values into real data types

BeautifulSoup hands you strings, and a column that looks numeric on screen is often "$1,299.00" or "12.4%" in the markup. Before you can sum, sort or chart it, you have to strip the currency symbol, remove thousands separators, drop the percent sign and cast to a number. Doing this in a small per-column cleaning function keeps your extraction loop readable and makes the cleaning rules easy to audit later.

def to_number(text):
    cleaned = text.replace(",", "").replace("$", "").replace("%", "").strip()
    try:
        return float(cleaned)
    except ValueError:
        return text  # leave genuinely non-numeric values alone

Expanding rowspan and colspan properly

The base guide notes that merged cells break simple zipping; the fix is to honour the span attributes as you build the grid. A cell with colspan="2" should occupy two columns, and a cell with rowspan="3" should reappear in the same column on the next two rows. Maintaining a small carry-over map of pending rowspans as you iterate lets you reconstruct a clean rectangular grid even from heavily merged tables.

The mental model

  • Track each unfinished rowspan as a column index with a remaining count and value.
  • At the start of every row, fill those columns first before placing new cells.
  • Repeat a colspan cell's value across the number of columns it spans.
  • Decrement rowspan counters until they reach zero and drop out.

Multi-row and grouped headers

Statistical and financial tables frequently use two header rows, where a top row groups several sub-columns beneath it. Reading only the first <tr> of the <thead> gives you half the labels. Either flatten the two rows into combined names like "2024 Revenue" by pairing the group label with each sub-label, or keep them as a hierarchical column index if you are loading into pandas for analysis.

Validating the grid before you trust it

A table can parse cleanly yet be quietly wrong: a stray separator row, a footer totals row, or one misread span can shift every value one column left. Cheap assertions catch this. Check that each data row has the same length as your header, watch for rows that are entirely empty or hold a single spanning cell, and spot-check a few known values against the live page. This verification step is what separates a reliable extraction from a dataset that looks fine until someone relies on it.

Pros and cons to weigh

Strengths

  • BeautifulSoup gives cell-level control that pandas read_html cannot match on messy markup.
  • A dedicated cleaning function makes type conversion explicit and easy to audit.
  • Span-aware grid building handles real-world merged tables that naive loops corrupt.
  • Pairing extraction with row-length validation catches silent misalignment early.

Trade-offs

  • Clean extraction still leaves all the data-cleaning work to you.
  • Rowspan and colspan handling adds real complexity beyond a simple zip.
  • Multi-row headers require extra logic that single-header readers skip.
  • JavaScript-rendered tables remain invisible to the parser entirely.

Common mistakes to avoid

  • Saving raw strings and assuming numbers are numbers, then failing on the first sum or sort.
  • Ignoring span attributes so every row after a merged cell shifts out of alignment.
  • Reading only the first header row and losing the grouping labels above it.
  • Including footer or separator rows as if they were real data records.

Before-you-buy checklist

  • Confirm whether the table uses thead, tbody, or neither and adjust your selectors.
  • Write a per-column cleaning function for currency, separators, units and percentages.
  • Handle rowspan and colspan so the reconstructed grid stays rectangular.
  • Decide how to flatten or preserve multi-row headers before extraction.
  • Assert each row matches the expected column count and drop separator rows.
  • Match your proxy plan to how many table URLs you will fetch, not just one sample page.
$

How to get the best value

Right-size the plan

Start on the smallest sensible tier and scale only what proves itself on your real targets.

Type before brand

Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.

Read the fine print

Check traffic limits, rotation rules and what happens on overage before you commit.

Lead with value

Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.

📖

Key terms explained

Rowspan
a cell attribute that makes one cell occupy several stacked rows in the same column.
Colspan
a cell attribute that makes one cell stretch across several adjacent columns in a row.
thead and tbody
optional sections that separate a table's header rows from its data rows.
Type coercion
converting extracted text into a proper data type such as a number or date.
Grid validation
checking that every parsed row has the expected number of aligned columns.

Why compare before buying?

Table scraping jobs vary wildly in scale, from one page to thousands, so matching proxy spend to the workload matters. Comparing providers on value keeps a high-volume extraction affordable, and a budget-friendly rotating pool frequently handles repetitive table fetches just as well as a costly premium plan.

How we compare

Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.

?

Frequently asked questions

How do I scrape a specific table when a page has several?

Target the table by its id or class with soup.find("table", {"id": "results"}), or select by position if it has no stable attributes.

What is the difference between th and td when scraping?

<th> cells hold column or row headers and <td> cells hold the actual data, so read headers from th and values from td.

Should I use pandas read_html or BeautifulSoup?

Use read_html for clean, well-formed tables because it is fast, but switch to BeautifulSoup when tables have merged cells or messy markup needing manual handling.

How do I deal with merged cells using colspan?

Detect the colspan or rowspan attribute on a cell and repeat or pad its value so each row keeps the correct number of columns.

Why is my table empty after scraping?

The table is probably rendered by JavaScript, so it is not in the raw HTML; capture it with a browser automation tool or find the underlying data API.

Do I need proxies to scrape tables?

Not for one page, but scraping tables from many URLs quickly can get you blocked, so rotating proxies help larger jobs run without interruption.

Compare on value, then decide

For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.