Learn how to use Python web scraping to collect data from any website using simple tools like Beautiful Soup and Requests. This tutorial shows you how to build a working web scraper, handle dynamic content, and export your data—all with clean, straightforward code that actually works.
Python web scraping has become the go-to method for collecting data at scale, mostly because the language reads like English and comes with powerful libraries that do the heavy lifting. You don't need to be a coding wizard—if you can read a recipe, you can write a scraper.
The real beauty is in the ecosystem. Python has libraries for every part of the scraping process: downloading pages, parsing HTML, handling JavaScript, storing data. And when you inevitably run into problems (because you will), there's a massive community and documentation ready to help.
If you're trying to gather data for analysis, track prices, or just see what's out there on the web, Python gives you the tools without making you work harder than necessary.
The syntax is simple. Libraries like Scrapy and Beautiful Soup are specifically built for scraping. There are tutorials everywhere. The community is active and helpful.
But here's the thing that makes Python especially useful: it's also the main language for data analysis. So after you scrape your data, you already have Pandas, NumPy, and Matplotlib sitting there ready to process it. No switching languages, no converting formats—everything stays in one place.
This guide walks through building an actual scraper, not just theory. We're going to pull job listings from Indeed, extract the details we want, and save everything to a CSV file you can actually use.
We're scraping an Indeed job search page to grab job titles, company names, locations, and URLs. Then we'll format it all into a CSV file.
Web scraping breaks down into three basic steps: request the page, download the HTML response, and parse it to extract what you need. While we're using Indeed as an example, these steps work for almost any site—just remember that every page structures its HTML differently.
Before writing any code, you need to understand what you're working with. Modern web pages are essentially two things: HTML and CSS.
HTML for Web Scraping
HTML is the skeleton of every webpage. It uses tags to tell your browser what to display. Press ctrl/command + shift + c on any page to see the HTML underneath.
The structure is consistent across sites. Everything sits between <html></html> tags, with <head></head> containing metadata and <body></body> holding the actual content—that's where we'll be digging.
Tags nest inside other tags, creating a hierarchy. When scraping, you use these HTML tags to locate the specific information you want.
Common tags you'll see:
div — organizes sections of a page
h1 to h6 — headings
p — paragraphs
a — links (with an href property containing the URL)
CSS for Web Scraping
CSS styles the HTML elements, telling the browser how everything should look. We care about CSS because we can use CSS selectors to identify and extract specific elements.
When writing CSS, developers add classes and IDs to HTML elements. We can use those same classes and IDs to pinpoint exactly what we want to scrape. The dot (.) represents a class in CSS—so .how-it-section-heading selects all elements with that class.
First, our scraper needs to download the page. We'll use the Requests library to send a GET request to the server.
Install Requests: pip3 install requests
Create a new Python file (soup_scraper.py) and import the library:
python
import requests
url = 'https://www.indeed.com/jobs?q=web+developer&l=New+York'
page = requests.get(url)
print(page.content)
The print(page.content) logs the response—a giant string of HTML code. If you see HTML, the request worked. You can also use print(page.status_code) to verify—a 200 status means success.
Now comes the detective work. Before parsing the HTML, we need to know how to identify each element.
Go to the Indeed URL and right-click anywhere on the page, then select "inspect." The dev tools open, showing you the page's HTML structure.
After some digging (and this page is messy, so don't worry if it takes a minute), you'll find that each job listing sits inside a div with the class jobsearch-SerpJobCard unifiedRow row result. Inside that div, you'll find the job title in an <a> tag with class="jobtitle turnstileLink", the company name with class="company", and the location with class="location accessible-contrast-color-location".
Install Beautiful Soup: pip3 install beautifulsoup4
Import it and create a Beautiful Soup object:
python
import requests
from bs4 import BeautifulSoup
url = 'https://www.indeed.com/jobs?q=web+developer&l=New+York'
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')
results = soup.find(id='resultsCol')
print(results.prettify())
The prettify() method formats the output so it's easier to read. Your scraper is now parsing HTML.
We've created a results object that shows all the information inside our main element. Now we need to dig deeper and find just the elements we want.
python
indeed_jobs = results.select('div.jobsearch-SerpJobCard.unifiedRow.row.result')
When using select() with Beautiful Soup, each dot (.) represents a class. This is CSS selector syntax. We also had to remove the last class (clickcard) to get it working—this kind of experimentation is normal when building scrapers.
If you prefer using find_all(), another solution is:
python
indeed_jobs = results.find_all(class_='jobsearch-SerpJobCard unifiedRow row result')
Now we're close. This last step uses everything we've learned to grab just the information we care about.
python
for indeed_job in indeed_jobs:
job_title = indeed_job.find('h2', class_='title')
job_company = indeed_job.find('span', class_='company')
job_location = indeed_job.find('span', class_='location accessible-contrast-color-location')
Add .text to extract only the text content—without it, you get the entire HTML element:
python
print(job_title.text)
print(job_company.text)
print(job_location.text)
The output has extra whitespace. Fix it by adding .strip():
python
print(job_title.text.strip())
print(job_company.text.strip())
print(job_location.text.strip())
Now the data looks clean.
To grab the URL from the href attribute:
python
job_url = indeed_job.find('a')['href']
print(job_url)
You can use this same syntax to extract any attribute from an element.
Note: Indeed only includes the URL extension in their href attribute, so you'll need to add https://www.indeed.com/ at the beginning to make the link work.
Logging data to your terminal isn't particularly useful. Let's export it to a CSV file for actual analysis.
Add import csv at the top of your file. After finding the divs, open a new file and create a writer:
python
file = open('indeed-jobs.csv', 'w')
writer = csv.writer(file)
Add a header row:
python
writer.writerow(['Title', 'Company', 'Location', 'Apply'])
Now update your variables to extract content immediately and pass it to the writer:
python
import csv
import requests
from bs4 import BeautifulSoup
url = "https://www.indeed.com/jobs?q=web+developer&l=New+York"
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')
results = soup.find(id='resultsCol')
indeed_jobs = results.select('div.jobsearch-SerpJobCard.unifiedRow.row.result')
file = open('indeed-jobs.csv', 'w')
writer = csv.writer(file)
writer.writerow(['Title', 'Company', 'Location', 'Apply'])
for indeed_job in indeed_jobs:
job_title = indeed_job.find('h2', class_='title').text.strip()
job_company = indeed_job.find('span', class_='company').text.strip()
job_location = indeed_job.find('span', class_='location accessible-contrast-color-location').text.strip()
job_url = indeed_job.find('a')['href']
writer.writerow([job_title.encode('utf-8'), job_company.encode('utf-8'), job_location.encode('utf-8'), job_url.encode('utf-8')])
file.close()
Run the code and you'll see a new CSV file in your project folder with all the job listings.
You just built your first Python web scraper using Requests and Beautiful Soup.
Building a scraper is one thing. Running it at scale without getting blocked is another. Websites track IP addresses and will shut you down after too many requests. When you're ready to scrape hundreds or thousands of pages, you need to manage IP rotation, headers, and retries—the kind of infrastructure work that takes time away from actually collecting data.
That's where professional scraping tools become worth it. Instead of managing proxy pools and building retry logic yourself, you can route your requests through a service that handles all the anti-bot measures automatically. 👉 Let ScraperAPI handle proxy rotation, headers, and retries so you can focus on extracting data
To use it, just construct your URL to route through ScraperAPI:
python
url = "http://api.scraperapi.com?api_key={YOUR_API_KEY}&url=https://www.indeed.com/jobs?q=web+developer&l=New+York"
ScraperAPI selects the best proxy and headers for each request. If a request fails, it automatically retries with a different proxy for up to 60 seconds. If it still can't get a 200 response after that, it returns a 500 status code so you know something's wrong.
Sometimes you need to do more than just download HTML—you need to fill out forms, click buttons, or navigate through multiple pages. While Selenium offers full browser automation, MechanicalSoup provides a lighter option for simpler tasks. It's built on top of Requests and Beautiful Soup, making it efficient for interacting with HTML elements.
When to Use MechanicalSoup:
Websites without APIs
Testing your own websites
Simple automation tasks
When Not to Use MechanicalSoup:
Sites with APIs (just use the API)
Non-HTML content
JavaScript-heavy websites (use Selenium or ScraperAPI instead)
Install MechanicalSoup: pip install mechanicalsoup
Example of navigating links:
python
import mechanicalsoup
browser = mechanicalsoup.StatefulBrowser()
browser.open("https://httpbin.org/")
browser.follow_link("forms")
print(browser.url)
Dynamic websites load content with JavaScript, which traditional scraping tools can't handle. You need something that actually renders the JavaScript.
Using ScraperAPI for Dynamic Content
ScraperAPI's render=true parameter tells the service to fully render the page before returning the HTML:
python
import requests
url = 'http://api.scraperapi.com?api_key=YOUR_API_KEY&url=https://example.com&render=true'
response = requests.get(url)
if response.status_code == 200:
print(response.text)
For more complex interactions, ScraperAPI offers a Render Instruction Set that lets you type into search bars, click buttons, and wait for elements to load.
Using Selenium for Dynamic Content
Selenium gives you complete control over a browser session:
python
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
options = webdriver.ChromeOptions()
driver = webdriver.Chrome(options=options)
try:
driver.get('https://www.imdb.com')
search_bar = driver.find_element(By.ID, 'suggestion-search')
search_bar.send_keys('Inception')
time.sleep(10)
suggestions = driver.find_elements(By.CSS_SELECTOR, '.searchResult')
for suggestion in suggestions:
print(suggestion.text)
finally:
driver.quit()
Choose ScraperAPI for efficiency and scale, or Selenium when you need precise control over browser interactions.
Raw scraped data is messy. You'll have missing values, inconsistent formats, duplicates, and irrelevant information. Before you can analyze anything, you need to clean it up.
Essential cleaning steps:
Handle missing data (fill gaps or remove incomplete rows)
Standardize formats (dates, units, text case)
Remove duplicates
Filter out irrelevant columns
Validate data for logical consistency
Good data cleaning turns a pile of HTML text into something you can actually work with.
403 Forbidden: The site blocked your request. Rotate user-agents, use proxies, add delays between requests.
404 Not Found: The page doesn't exist. Check your URLs and handle redirects.
429 Too Many Requests: You hit the rate limit. Slow down or use proxy rotation.
500 Internal Server Error: The server's having problems. Retry after a delay.
301/302 Redirect: The page moved. Make sure your scraper follows redirects automatically.
Understanding these errors helps you build scrapers that don't fall apart the first time something goes wrong.
Python web scraping gives you access to data that's locked behind HTML. With a few libraries and some detective work, you can automate what would take hours of manual copying and pasting. Start small, understand the basics, and gradually tackle more complex projects. The data's out there—now you know how to get it. For web scraping projects at scale, 👉 use ScraperAPI to handle the infrastructure while you focus on the data