Web scraping in Python: from a Wikipedia table to a CSV file
A written lesson made by Unwatch from a free YouTube video, with the video's code, timestamps, a quiz and flashcards.
This lesson follows one complete scraping job: take the Wikipedia table of the largest US companies by revenue, pull it into a pandas DataFrame with requests and BeautifulSoup, and save it as a CSV file.
It suits you if you know basic Python and a little pandas and want to see the whole job done once: choose the right table, clean the headers, loop over the rows, then export.
When to watch instead: Watch the video if you want to see the browser's Inspect tool used live to find the right table; where to click is easier to follow in motion. Also, Wikipedia pages change, so the table position and class names may not match today's page. Treat the code as a pattern and adapt the selectors.
Overview
In this lesson, you will learn how to scrape data from a real website—specifically, a Wikipedia page listing the largest companies in the United States by revenue—using Python, BeautifulSoup, and Requests.
0:00 Introduction to Web Scraping from a Real Website
- Goal: Scrape tabular data from a real website, put it into a Pandas DataFrame, and optionally export it to a CSV file.
- Target Page: Wikipedia page titled "List of the largest companies in the United States by revenue" (rather than a simplified practice page).
- Complexity: Working with real Wikipedia tables introduces extra formatting steps and requires selecting the correct table when multiple tables share similar classes.
1:09 Importing Libraries and Requesting the Web Page
- Start by importing
BeautifulSoupfrombs4and importing therequestslibrary. - Set the target Wikipedia URL in a variable and use
requests.get()to fetch the page. - Create a BeautifulSoup object by passing
page.textand the parserhtml.
Run the following code in your Jupyter notebook:
from bs4 import BeautifulSoup
import requests
url = 'https://en.wikipedia.org/wiki/List_of_largest_companies_in_the_United_States_by_revenue'
page = requests.get(url)
soup = BeautifulSoup(page.text, 'html')
print(soup)
2:33 Inspecting the Web Page Structure for Data Extraction
- Inspect the Wikipedia page in the browser to identify the data to extract (rank, name, industry, revenue, etc.).
- Notice there are multiple tables on the page (e.g., list of companies by revenue, and further down, a list of companies by profit).
- Use the browser's developer tools inspector to examine the table classes. The primary table uses classes like
wikitable,sortable, andjquery-tablesorter. - Copy outer HTML snippets if needed to verify classes and structure, noting that multiple tables on the page might share the exact same class attributes (
wikitable sortable jquery-tablesorter).
5:00 Finding All Tables on the Page
We need to target the tables on the Wikipedia page. Looking at the browser's Elements panel, we see a box with more citations, and below that, the table elements.
Let's use soup.find_all('table') to extract all tables from the parsed HTML.
soup.find_all('table')
Running this command returns a list of multiple tables. Inspecting the output:
- The first table (index
0) contains something else (like the references/edit links box). - Scrolling further down, we see tables with classes like
wikitable sortable. - Index
1contains the table for "List of the largest companies in the United States by revenue". - Index
3(or further down) contains tables like "List of the largest companies by profit" (e.g., Ford Motor, Wells Fargo, Goldman Sachs).
To target a specific table using indexing:
soup.find_all('table')[1]
This returns the first company table (ranked by revenue), containing rows (<tr>), table headers (<th>), and table data cells (<td>).
6:54 Refining Table Selection with Attributes
We can make our selection more specific by filtering on attributes like class instead of relying solely on list indices.
Initially, we try filtering by class_:
soup.find('table', class_ = 'wikitable sortable')
However, this may return a TypeError: 'NoneType' object is not subscriptable if the class string doesn't match precisely. Inspecting the element in the browser reveals the full class string:
<table class="wikitable sortable jquery-tablesorter">
If we want to pull specifically by table class, let's assign the result of soup.find_all('table') to a variable:
table = soup.find_all('table')
And if we print table[1], we get the exact table data we want:
print(table)
8:59 Extracting Table Headers (<th>)
Now that we have isolated our target table, the first objective is to extract the table titles (headers).
Inspecting the table structure, the column headers are wrapped inside <th> tags:
RankNameIndustryRevenue (USD millions)Revenue growthEmployeesHeadquarters
We can extract all <th> tags from our table using .find_all('th'):
soup.find_all('th')
Because <th> tags are unique to the headers (whereas row data uses <td> tags), this makes pulling the headers very straightforward.
Let's store the extracted titles into a variable called world_titles:
world_titles = soup.find_all('th')
Printing world_titles shows a list of all <th> elements containing the header strings. In the next steps, we will loop through this list to clean and format the column titles.
10:00 Extracting and Cleaning Table Titles
Use list comprehension to extract the text from each table header element:
world_table_titles = [title.text for title in world_titles]
Print the titles to check the output:
print(world_table_titles)
Output:
['Rank\n', 'Name\n', 'Industry\n', 'Revenue (USD millions)\n', 'Revenue growth\n', 'Employees\n', 'Headquarters\n', 'Rank\n', 'Name\n', 'Industry\n', 'Profits(USD millions)\n']
The output contains newline characters (\n) and duplicate titles from multiple tables on the page. To remove the newline characters, apply .strip() to the text inside the list comprehension:
world_table_titles = [title.text.strip() for title in world_titles]
Print the cleaned titles:
print(world_table_titles)
Output:
['Rank', 'Name', 'Industry', 'Revenue (USD millions)', 'Revenue growth', 'Employees', 'Headquarters', 'Rank', 'Name', 'Industry', 'Profits(USD millions)']
11:19 Fixing Table Selection and DataFrame Creation
The list of titles contains headers from two different tables on the Wikipedia page (one for revenue and one for profits). Examining the table finding step, soup.find_all('th') pulled <th> elements from all tables on the page instead of just the first target table.
To target only the first table, find all tables first:
table = soup.find_all('table')
Then search within the correct table (index 0) for the header elements:
table = soup.find_all('table')[0]
world_titles = table.find_all('th')
world_table_titles = [title.text.strip() for title in world_titles]
print(world_table_titles)
Output:
['Rank', 'Name', 'Industry', 'Revenue (USD millions)', 'Revenue growth', 'Employees', 'Headquarters']
Now, import Pandas and create the initial DataFrame using these column titles:
import pandas as pd
df = pd.DataFrame(columns = world_table_titles)
df
Running this outputs an empty DataFrame with the correct column headers:
RankNameIndustryRevenue (USD millions)Revenue growthEmployeesHeadquarters
13:38 Inspecting Table Rows (<tr>) and Data (<td>)
To populate the DataFrame, extract the row data.
<th>tags represent table headers (Rfor rows, headers/titles).<td>tags represent table data inside the rows (Rfor rows,Dfor data).- These are all encapsulated within
<tr>tags (whereTRrepresents rows).
Test finding rows within the table:
table.find_all('tr')
When inspecting the output of table.find_all('tr'), rows start with the headers (<th>), followed by rows of data (<td>) separated by commas, with subsequent rows numbered sequentially (td1, td2, td3, etc.).
15:00 Extracting Table Rows (<tr>) and Cells (<td>)
To extract the column data from the table rows, loop through column_data (which contains all <tr> tags):
column_data = table.find_all('tr')
for row in column_data:
row.find_all('td')
If we want to inspect the cell data inside each row, we print out the find results:
for row in column_data:
print(row.find_all('td'))
Running this block outputs a list of all <td> HTML elements for every row in the table.
16:03 Extracting and Cleaning Cell Text
To extract the text from the HTML tags and clean up whitespace using .text.strip(), we loop through each row's data:
for row in column_data:
individual_row_data = [data.text.strip() for data in row.find_all('td')]
print(individual_row_data)
- Tip: Using
.text.strip()removes unwanted whitespace, newlines, and padding around the text inside the<td>tags.
17:32 Appending Individual Rows to a DataFrame
Currently, the loop prints individual lists of row data one by one as it iterates. To store this data inside our DataFrame (df), we need to append each row dynamically as the loop runs.
To insert rows one at a time at the correct position, we use .loc combined with len(df) to find the current length of the DataFrame:
df = pd.DataFrame(columns = world_table_titles)
for row in column_data:
individual_row_data = [data.text.strip() for data in row.find_all('td')]
length = len(df)
df.loc[length] = individual_row_data
How it works:
len(df): Checks the current number of rows in the DataFrame each time the loop runs.df.loc[length]: Places theindividual_row_datainto the next available row index in the DataFrame, ensuring data is added sequentially.
20:00 Appending Row Data to the DataFrame and Fixing Mismatched Columns
- Loop through
column_datato find all row tags (tr), then find all data tags (td) within each row:for row in column_data: row_data = row.find_all('tr') for row in row_data: row_data = row.find_all('td') individual_row_data = [data.text.strip() for data in row_data] length = len(individual_row_data) df.loc[length] = individual_row_data - Running this block causes a
ValueError: Cannot set a row with mismatched columns. - Inspecting
individual_row_datareveals that the first element in the list is empty ([]). This happens because the first<tr>insidecolumn_datacontains header/title data rather than standard table data (td). - To fix the mismatched columns error, change the index from
lengthto1(or use a list[1]) to target the correct table row list.
Updated cell (In [42]):
for row in column_data:
row_data = row.find_all('tr')
for row in row_data:
row_data = row.find_all('td')
individual_row_data = [data.text.strip() for data in row_data]
length = len(individual_row_data)
df.loc[len(df)] = individual_row_data
21:34 Viewing the Populated DataFrame
- Pulling up the DataFrame (
df):df - Output displays a 100 rows × 7 columns table:
- Rank
- Name
- Industry
- Revenue (USD millions)
- Revenue growth
- Employees
- Headquarters
22:10 Exporting the DataFrame to a CSV File
- To export the DataFrame into a CSV file, use the
.to_csv()method with the file path and name:df.to_csv(r'C:\Users\alex\OneDrive\Documents\Python Web Scraping\Folder for Output\Companies.csv') - Checking the output folder shows
Companies.csv. Opening it in Excel reveals an unwanted extra index column at the front of the data.
23:10 Removing the Index Column on Export
- To prevent the index from being exported into the CSV file, set
index=False:df.to_csv(r'C:\Users\alex\OneDrive\Documents\Python Web Scraping\Folder for Output\Companies.csv', index = False) - Refreshing the folder and reopening
Companies.csvin Excel shows a clean table without the unwanted index column.
23:46 Review of the Complete Web Scraping Project
Reviewing the full code built across the project steps:
-
Importing Libraries and Fetching the URL:
from bs4 import BeautifulSoup import requests import pandas as pd url = 'https://en.wikipedia.org/wiki/List_of_largest_companies_in_the_United_States_by_revenue' page = requests.get(url) soup = BeautifulSoup(page.text, 'html') -
Finding the Table:
world_table = soup.find('table', class_ = 'wikitable sortable') -
Extracting and Cleaning Table Titles:
world_table_titles = world_table.find_all('th') world_table_titles = [title.text.strip() for title in world_table_titles] -
Creating the DataFrame with Column Headers:
df = pd.DataFrame(columns = world_table_titles) -
Extracting Row Data, Cleaning Cells, and Appending to DataFrame:
column_data = world_table.find_all('tr') for row in column_data: row_data = row.find_all('td') individual_row_data = [data.text.strip() for data in row_data] length = len(individual_row_data) df.loc[len(df)] = individual_row_data -
Exporting to CSV:
df.to_csv(r'C:\Users\alex\OneDrive\Documents\Python Web Scraping\Folder for Output\Companies.csv', index = False)
25:00 Exporting to CSV and Conclusion
The final line of code exports the pandas DataFrame to a CSV file:
df.to_csv(r'C:\Users\alexf\OneDriveDocuments\Python Web Scraping\Folder for Output\Companies.csv', index = False)
By setting index = False, the row numbers (0, 1, 2...) are excluded from the output file so they aren't written as an extra column.
Key Takeaways
- Fetch Web Pages: Use the
requestslibrary to fetch HTML content andBeautifulSoupfrombs4to parse it for data extraction. - Inspect Page Structure: Use browser developer tools to inspect elements, identify specific table classes (like
wikitable sortable), and distinguish between multiple tables on a page. - Target Specific Elements: Isolate target tables using list indexing or class attributes to avoid pulling unwanted data from other sections.
- Extract Table Headers: Use
.find_all('th')to locate table column headers and apply.text.strip()via list comprehension to remove unwanted newline characters and whitespace. - Initialize Pandas DataFrames: Create an empty Pandas DataFrame using the cleaned header titles as column names.
- Extract Row Data: Loop through table rows (
<tr>) and extract cell data (<td>), applying.text.strip()to clean text values. - Dynamically Populate DataFrames: Use
df.loc[len(df)]within a loop to sequentially append extracted row lists into the DataFrame. - Handle Mismatched Rows: Filter out empty lists or header rows to prevent shape errors when appending data to a DataFrame.
- Export to CSV: Use the
.to_csv()method to save the final DataFrame to a file, settingindex=Falseto prevent exporting unwanted row index numbers.
Check yourself
Quiz
1. Why did running `soup.find_all('th')` initially pull header titles from multiple different tables on the page?
- Because Wikipedia restricts BeautifulSoup from searching inside individual tables.
- Because `th` tags globally span all tables on the entire parsed HTML page unless restricted to a specific table element.
- Because `th` tags are automatically converted to `td` tags by requests.
- Because the target URL redirects to a multi-page document.
Answer: Because `th` tags globally span all tables on the entire parsed HTML page unless restricted to a specific table element.
Searching directly on the `soup` object looks through the entire parsed document, capturing header elements from every table present on the page.
2. What is the primary purpose of applying `.strip()` during the extraction of table headers and cell text?
- To convert all characters to uppercase
- To filter out numeric characters from the strings
- To remove unwanted newline characters and surrounding whitespace
- To replace missing values with NaN
Answer: To remove unwanted newline characters and surrounding whitespace
The `.text` method often brings along hidden padding and newline characters (`\n`), which are cleaned up using `.strip()`.
3. When looping through row data to populate a Pandas DataFrame, what does `len(df)` achieve when used inside `df.loc[len(df)]`?
- It finds the current number of rows so the next row can be appended sequentially at the end.
- It sets a fixed size limit of 100 rows for the DataFrame.
- It checks how many columns are currently in the DataFrame.
- It deletes the last row of the DataFrame to save memory.
Answer: It finds the current number of rows so the next row can be appended sequentially at the end.
Using `len(df)` dynamically returns the current row count, allowing `df.loc` to place each new list of row data into the next available index position.
4. What error occurs if you try to append `individual_row_data` containing an empty list (from the header row `<tr>`) directly into a DataFrame built with 7 columns?
- TypeError: 'NoneType' object is not callable
- IndexError: list index out of range
- ValueError: Cannot set a row with mismatched columns
- KeyError: Column not found
Answer: ValueError: Cannot set a row with mismatched columns
Attempting to assign a row whose length does not match the DataFrame's column count results in a mismatched columns ValueError.
5. What is the difference between `<th>` and `<td>` tags in an HTML table structure?
- `<th>` is used only for the first row of a website, while `<td>` is used for footers.
- `<th>` represents hyperlinks, while `<td>` represents plain text fields.
- `<th>` stands for table height, while `<td>` stands for table depth.
- `<th>` represents table headers, while `<td>` represents standard table data cells inside rows.
Answer: `<th>` represents table headers, while `<td>` represents standard table data cells inside rows.
HTML tables use `<th>` for header cells (titles) and `<td>` for data cells containing the actual records.
6. Why is `index = False` important when exporting a Pandas DataFrame to a CSV file using `df.to_csv()`?
- It prevents the file from saving in read-only mode.
- It automatically reindexes the rows alphabetically before saving.
- It stops Pandas from exporting numerical values.
- It prevents an unwanted extra index column (0, 1, 2...) from being written into the CSV file.
Answer: It prevents an unwanted extra index column (0, 1, 2...) from being written into the CSV file.
By default, `.to_csv()` writes the DataFrame's row indices into the file as a separate column, which can be avoided by setting `index=False`.
7. Which library and function are used to fetch the raw HTML content of the target Wikipedia page?
- `requests.get()`
- `pandas.read_html()`
- `urllib.urlopen()`
- `bs4.fetch()`
Answer: `requests.get()`
The lesson uses the `requests` library with `requests.get(url)` to retrieve the webpage's content.
8. If inspecting a table in browser developer tools reveals multiple tables sharing the class string `wikitable sortable jquery-tablesorter`, how can you reliably isolate just the first company revenue table?
- By deleting all other tables from the browser DOM before scraping
- By using list indexing like `soup.find_all('table')[1]` or filtering specifically
- By setting `requests.get(url, table=1)`
- By converting the soup object into a CSV string first
Answer: By using list indexing like `soup.find_all('table')[1]` or filtering specifically
When multiple tables share the exact same class attributes, you can target the correct table using list indexing (e.g., index `1`) on the results of `find_all('table')`.
Flashcards
What three main libraries are imported for this web scraping project?
BeautifulSoup (from bs4), requests, and pandas.
How do you fetch a web page and parse it using BeautifulSoup?
page = requests.get(url) soup = BeautifulSoup(page.text, 'html')
Why might `soup.find_all('table')` return multiple tables on a Wikipedia page?
Wikipedia pages often contain multiple tables (such as a table for revenue and a separate table for profits) that share the same classes.
Which HTML tag is used for table headers vs. table data cells?
`<th>` is used for table headers, and `<td>` is used for table data cells, all wrapped inside `<tr>` (table row) tags.
How do you clean whitespace and newline characters (`\n`) from extracted header or cell text?
Use `.text.strip()` inside a list comprehension, e.g., `[title.text.strip() for title in world_titles]`.
How do you initialize an empty Pandas DataFrame using extracted column titles?
df = pd.DataFrame(columns=world_table_titles)
What tag do you loop through to extract individual rows from a table?
Loop through `<tr>` tags (`table.find_all('tr')`), then find all `<td>` tags within each row.
How do you dynamically append a new row of data to the end of a Pandas DataFrame inside a loop?
df.loc[len(df)] = individual_row_data
What causes a `ValueError: Cannot set a row with mismatched columns` during row appending?
Typically happens when the first row processed contains empty values or header tags (`<th>`) instead of standard data cells (`<td>`).
How do you export a Pandas DataFrame to a CSV file without including the row index column?
df.to_csv('path/to/file.csv', index=False)
Want a lesson like this from your video?
Paste any public YouTube video or playlist. Your lesson is ready in a few minutes.