5.3 Web Scraping Pipelines
5.3 Web Scraping Pipelines
Web scraping is the process of programmatically retrieving information from web pages and extracting useful data from their HTML structure.
In Python, a common static-HTML scraping workflow uses:
requeststo send HTTP requests and retrieve web pages.BeautifulSoupto parse HTML and locate elements.- Python data structures to clean and organize extracted data.
pandaswhen the final result needs tabular analysis.
URL → HTTP Request → HTML Response → Parse HTML → Extract Data → Clean Data → Store Data
1. What Is Static HTML?
A static page contains the required information in the HTML response received from the server.
For such pages, requests can retrieve the HTML and
BeautifulSoup can parse it without opening a browser.
| Type | Typical Approach | Example Situation |
|---|---|---|
| Static HTML | requests + BeautifulSoup |
Data already exists in the downloaded HTML. |
| JavaScript-rendered | Browser automation or an underlying API | Content appears only after JavaScript executes. |
| API-based | requests + JSON processing |
Website provides data through a web API. |
Do not automatically use Selenium when the required data is
already present in the server response. For static pages,
requests + BeautifulSoup is generally
simpler and lighter.
2. Installing the Required Libraries
pip install requests beautifulsoup4
If you also want to work with extracted tabular data:
pip install pandas
In a Conda environment:
conda install requests beautifulsoup4 pandas
3. Importing the Libraries
import requests
from bs4 import BeautifulSoup
The package is installed as beautifulsoup4, while
the Python class is imported from the bs4 module.
4. Sending Your First HTTP Request
import requests
url = "https://example.com"
response = requests.get(url)
print(response.status_code)
The response object contains the information returned by the server.
5. Understanding HTTP Status Codes
| Status Code | Meaning |
|---|---|
200 |
Request succeeded. |
301 |
Permanent redirect. |
302 |
Temporary redirect. |
403 |
Forbidden. |
404 |
Resource not found. |
429 |
Too many requests. |
500 |
Server-side error. |
Always check whether the HTTP request succeeded before trying to parse the response.
6. Using raise_for_status()
import requests
url = "https://example.com"
response = requests.get(url)
response.raise_for_status()
print(response.text)
raise_for_status() raises an exception for unsuccessful
HTTP status codes.
7. Reading the HTML Response
response = requests.get(
"https://example.com"
)
html = response.text
print(html)
response.text provides the response body decoded as
text.
For raw bytes, use:
content = response.content
8. Parsing HTML with BeautifulSoup
from bs4 import BeautifulSoup
soup = BeautifulSoup(
response.text,
"html.parser"
)
The parser converts the HTML text into a structure that can be searched and navigated using Python.
9. Finding the First Matching Element
Consider this HTML:
<h1>Python Web Scraping</h1>
Extract the first <h1> element:
heading = soup.find("h1")
print(heading)
To obtain only its text:
print(
heading.get_text()
)
10. Finding Multiple Elements with find_all()
Suppose a page contains:
<p>First paragraph</p>
<p>Second paragraph</p>
<p>Third paragraph</p>
paragraphs = soup.find_all("p")
for paragraph in paragraphs:
print(
paragraph.get_text(strip=True)
)
11. Searching by HTML Tag
links = soup.find_all("a")
images = soup.find_all("img")
headings = soup.find_all("h2")
tables = soup.find_all("table")
HTML tag names are one of the simplest ways to begin extracting content.
12. Searching by CSS Class
Suppose the HTML contains:
<div class="product">
<h2>Laptop</h2>
</div>
Find the element using its class:
product = soup.find(
"div",
class_="product"
)
class_?
class is a Python keyword, so BeautifulSoup uses
class_ as the keyword argument.
13. Searching by HTML ID
<div id="main-content">
...
</div>
content = soup.find(
id="main-content"
)
14. Extracting Clean Text
HTML elements contain tags as well as text. Use
get_text() to extract the human-readable content.
element = soup.find("h1")
text = element.get_text(
strip=True
)
print(text)
strip=True removes unnecessary whitespace around the
extracted text.
15. Extracting HTML Attributes
HTML elements often contain useful attributes.
<a
href="https://example.com"
class="learn-link"
>
Learn
</a>
Access an attribute using dictionary-style syntax:
link = soup.find("a")
print(
link["href"]
)
print(
link["class"]
)
16. Safely Reading an Attribute with get()
href = link.get("href")
print(href)
get() returns None when the attribute is
absent instead of immediately raising a KeyError.
17. Extracting Links from a Page
links = soup.find_all("a")
for link in links:
text = link.get_text(
strip=True
)
href = link.get("href")
print(text, href)
This is one of the most common web scraping tasks.
18. Extracting Image URLs
images = soup.find_all("img")
for image in images:
src = image.get("src")
print(src)
Other useful attributes may include alt,
width, and height.
19. Using CSS Selectors
BeautifulSoup supports CSS-style selectors through
select() and select_one().
heading = soup.select_one(
"h1"
)
Select all matching elements:
items = soup.select(
".product"
)
20. Common CSS Selectors
| Selector | Meaning |
|---|---|
p |
All paragraph elements. |
.product |
Elements with class product. |
#main |
Element with ID main. |
div.product |
div elements with class product. |
article h2 |
h2 elements inside an article. |
ul li |
li elements inside a ul. |
a[href] |
Links containing an href attribute. |
21. Extracting Nested Content
Consider:
<article class="course">
<h2>Python</h2>
<p>Learn Python programming.</p>
</article>
course = soup.select_one(
"article.course"
)
title = course.select_one(
"h2"
).get_text(strip=True)
description = course.select_one(
"p"
).get_text(strip=True)
print(title)
print(description)
22. Scraping HTML Tables
Example HTML:
<table>
<tr>
<th>Name</th>
<th>Score</th>
</tr>
<tr>
<td>Alex</td>
<td>95</td>
</tr>
</table>
Extract rows:
table = soup.find("table")
rows = table.find_all("tr")
for row in rows:
cells = row.find_all(
["th", "td"]
)
values = [
cell.get_text(strip=True)
for cell in cells
]
print(values)
23. Extracting Tables with Pandas
When the page contains standard HTML tables, pandas can sometimes simplify extraction.
import pandas as pd
tables = pd.read_html(
"https://example.com"
)
print(
tables[0]
)
Web scraping often feeds directly into a pandas cleaning and analysis pipeline.
24. Sending Request Headers
HTTP requests can include headers such as User-Agent.
import requests
headers = {
"User-Agent":
"Mozilla/5.0"
}
response = requests.get(
"https://example.com",
headers=headers
)
response.raise_for_status()
Headers are part of the HTTP protocol and may affect how a server responds.
A User-Agent does not give permission to scrape a website. Follow the site's rules and applicable policies.
25. Always Use a Timeout
response = requests.get(
"https://example.com",
timeout=10
)
A timeout prevents the program from waiting indefinitely for a server response.
26. Handling Request Errors
import requests
url = "https://example.com"
try:
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
print("Request successful.")
except requests.Timeout:
print(
"The request timed out."
)
except requests.HTTPError as error:
print(
"HTTP error:",
error
)
except requests.RequestException as error:
print(
"Request failed:",
error
)
27. Using a requests.Session
A session can persist certain settings, such as headers and cookies, across multiple requests.
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0"
})
response = session.get(
"https://example.com",
timeout=10
)
response.raise_for_status()
Sessions can also reuse connections, which can be useful when making multiple requests to the same host.
28. Handling Relative URLs
Scraped links are often relative:
/courses/python.html
Convert them into absolute URLs with
urllib.parse.urljoin().
from urllib.parse import urljoin
base_url = (
"https://example.com"
)
relative_url = (
"/courses/python.html"
)
absolute_url = urljoin(
base_url,
relative_url
)
print(absolute_url)
29. Building a Link Extraction Pipeline
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
url = "https://example.com"
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(
response.text,
"html.parser"
)
for link in soup.select("a[href]"):
text = link.get_text(
" ",
strip=True
)
href = link.get("href")
absolute_url = urljoin(
url,
href
)
print(
text,
absolute_url
)
30. Cleaning Scraped Text
Web pages frequently contain extra whitespace and formatting.
text = element.get_text(
" ",
strip=True
)
Further cleaning can be performed using standard Python string methods.
clean_text = (
text
.replace("\n", " ")
.strip()
)
31. Normalizing Whitespace
import re
clean_text = re.sub(
r"\s+",
" ",
text
).strip()
This converts repeated whitespace characters into a single space.
32. Extracting Structured Records
Suppose a page contains:
<div class="course">
<h2>Python for Data Science</h2>
<p class="duration">
20 hours
</p>
</div>
courses = []
for item in soup.select(
".course"
):
title = item.select_one(
"h2"
).get_text(strip=True)
duration = item.select_one(
".duration"
).get_text(strip=True)
courses.append({
"title": title,
"duration": duration
})
print(courses)
This converts unstructured HTML into Python dictionaries.
33. Converting Scraped Data into a DataFrame
import pandas as pd
df = pd.DataFrame(
courses
)
print(df)
Once the data is in a DataFrame, the cleaning and transformation techniques from Module 3 can be applied.
34. Saving Scraped Data
df.to_csv(
"courses.csv",
index=False
)
Other useful formats include:
df.to_excel(
"courses.xlsx",
index=False
)
df.to_json(
"courses.json",
orient="records",
indent=2
)
35. Scraping Multiple Pages
Many websites divide results into multiple pages.
A simple pattern is:
import requests
from bs4 import BeautifulSoup
for page_number in range(1, 6):
url = (
f"https://example.com/"
f"products?page={page_number}"
)
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(
response.text,
"html.parser"
)
products = soup.select(
".product"
)
for product in products:
print(
product.get_text(
" ",
strip=True
)
)
Not every website uses ?page=2. Inspect the
site's actual pagination links and URL structure.
36. Following Pagination Links
Instead of constructing URLs manually, you can extract the next-page link.
next_link = soup.select_one(
"a.next[href]"
)
if next_link:
next_url = urljoin(
url,
next_link["href"]
)
print(next_url)
37. Multi-Page Scraping Workflow
current_url = (
"https://example.com/products"
)
while current_url:
response = requests.get(
current_url,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(
response.text,
"html.parser"
)
# Extract records here
next_link = soup.select_one(
"a.next[href]"
)
if next_link:
current_url = urljoin(
current_url,
next_link["href"]
)
else:
current_url = None
38. Rate Limiting and Request Delays
Sending requests too quickly can overload a server and may violate site rules.
import time
time.sleep(2)
In a multi-page scraper, a delay can be placed between requests when appropriate.
Scrape slowly, minimize unnecessary requests, and respect the website's published rules and applicable policies.
39. Understanding robots.txt
Many websites publish a robots.txt file describing
crawling preferences for automated agents.
It can often be found at:
https://example.com/robots.txt
Scrapers should review the site's published crawling rules and terms before collecting data.
robots.txt is not a universal legal authorization
or prohibition by itself. Treat it as one part of responsible
web-data collection, alongside terms, access controls,
applicable law, and the nature of the data.
40. Responsible Web Scraping
Technical capability does not automatically mean that data should be collected.
- Respect website terms and published policies.
- Check applicable crawling guidance.
- Avoid excessive request rates.
- Collect only the information required for the legitimate task.
- Avoid collecting sensitive personal information unnecessarily.
- Do not attempt to bypass authentication, access controls, or technical restrictions.
- Prefer official APIs when they are available and appropriate.
- Identify and handle data provenance appropriately.
41. API vs Web Scraping
If a website provides an official API for the information you need, the API is often the better engineering choice.
| Feature | Web Scraping | API |
|---|---|---|
| Data source | HTML page | Structured API response |
| Parsing | HTML parsing required | Usually JSON/XML parsing |
| Stability | Can break when page structure changes | Usually more structured |
| Authentication | Varies | Often API keys/tokens or other authentication |
| Preferred when official | Usually secondary option | Often preferred |
42. When requests + BeautifulSoup Is Not Enough
A page may display information in the browser that is not present in the initial HTML response.
This can happen when JavaScript obtains the data after page load.
Before using browser automation, inspect whether the site exposes the required data through an accessible API or embedded data source.
Is the data in HTML?
→ BeautifulSoup
Is there an official API?
→ API
Does the page require browser execution?
→ Consider browser automation
43. Complete Basic Web Scraper
import requests
from bs4 import BeautifulSoup
url = "https://example.com"
try:
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(
response.text,
"html.parser"
)
heading = soup.find("h1")
if heading:
print(
heading.get_text(
" ",
strip=True
)
)
except requests.RequestException as error:
print(
"Request failed:",
error
)
44. Mini Project — Product Data Scraper
Assume the page contains product cards structured like:
<div class="product-card">
<h2 class="product-name">
Laptop Pro
</h2>
<span class="price">
$999
</span>
</div>
Extract the product names and prices.
import requests
from bs4 import BeautifulSoup
import pandas as pd
url = "https://example.com/products"
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
soup = BeautifulSoup(
response.text,
"html.parser"
)
products = []
for card in soup.select(
".product-card"
):
name_element = card.select_one(
".product-name"
)
price_element = card.select_one(
".price"
)
name = (
name_element.get_text(
" ",
strip=True
)
if name_element
else None
)
price = (
price_element.get_text(
" ",
strip=True
)
if price_element
else None
)
products.append({
"name": name,
"price": price
})
df = pd.DataFrame(
products
)
df.to_csv(
"products.csv",
index=False
)
print(df)
Web Page → HTTP → HTML → CSS Selectors → Python Records → DataFrame → CSV
45. Validating Scraped Data
Scraping is not complete simply because HTML was downloaded. Extracted values should be validated.
if not name:
print(
"Missing product name."
)
if not price:
print(
"Missing product price."
)
Check for:
- Missing fields.
- Unexpected formats.
- Duplicate records.
- Invalid URLs.
- Changed HTML structures.
- Unexpected error pages.
46. Removing Duplicate Scraped Records
df = df.drop_duplicates()
When a particular field identifies a record:
df = df.drop_duplicates(
subset=["name"]
)
47. Cleaning Scraped Prices
Web pages often represent prices as strings.
df["price"] = (
df["price"]
.str.replace("$", "", regex=False)
.str.replace(",", "", regex=False)
)
df["price"] = pd.to_numeric(
df["price"],
errors="coerce"
)
This converts values such as $1,299 into numeric data
suitable for analysis.
48. Logging a Scraping Pipeline
import logging
logging.basicConfig(
level=logging.INFO,
format=(
"%(asctime)s - "
"%(levelname)s - "
"%(message)s"
)
)
logging.info(
"Scraping started."
)
logging.info(
"Page downloaded."
)
logging.info(
"Records extracted."
)
Logging becomes particularly useful when a scraper processes many pages.
49. Handling Temporary Failures
Real-world network requests can fail temporarily. A production pipeline may use controlled retries with backoff.
import time
import requests
for attempt in range(3):
try:
response = requests.get(
url,
timeout=10
)
response.raise_for_status()
break
except requests.RequestException:
if attempt == 2:
raise
time.sleep(
2 ** attempt
)
Waiting progressively longer between retries reduces repeated pressure on a server and gives transient network problems time to recover.
50. Designing a Maintainable Scraper
Avoid putting the entire scraper into one large block of code. Separate responsibilities into functions.
def fetch_page(url):
...
def parse_products(html):
...
def clean_data(records):
...
def save_data(records):
...
def main():
...
if __name__ == "__main__":
main()
This structure makes the scraper easier to test, debug, and maintain when the website changes.
51. A Reusable Scraping Architecture
URL
↓
fetch_page()
↓
parse_html()
↓
extract_records()
↓
clean_data()
↓
validate_data()
↓
save_data()
↓
log_result()
This separation is an important step from writing a simple scraper toward building a reliable data pipeline.
52. Important Concepts to Remember
| Concept | Remember |
|---|---|
requests.get() |
Sends an HTTP GET request. |
response.text |
Returns response content as decoded text. |
response.status_code |
Provides the HTTP status code. |
raise_for_status() |
Raises an HTTP-related exception for unsuccessful responses. |
BeautifulSoup() |
Parses HTML/XML content. |
find() |
Finds the first matching element. |
find_all() |
Finds multiple matching elements. |
select_one() |
Returns the first element matching a CSS selector. |
select() |
Returns elements matching a CSS selector. |
get_text() |
Extracts text from an element. |
urljoin() |
Combines base and relative URLs. |
53. Web Scraping Interview Questions
Q1. What is web scraping?
View Answer
Web scraping is the automated process of retrieving information from web pages and extracting useful data from their content.
Q2. What is the role of requests?
View Answer
requests is an HTTP client library used to send
requests to web servers and receive responses.
Q3. What is BeautifulSoup?
View Answer
BeautifulSoup is a Python library used to parse HTML and XML documents and navigate their element structure.
Q4. What is the difference between
find() and find_all()?
View Answer
find() returns the first matching element,
whereas find_all() returns all matching
elements.
Q5. What is the purpose of get_text()?
View Answer
It extracts the human-readable text contained within a BeautifulSoup element.
Q6. Why is timeout important in
requests?
View Answer
It prevents a request from waiting indefinitely for a response.
Q7. What is a CSS selector?
View Answer
A CSS selector is a pattern used to identify HTML elements based on tags, classes, IDs, attributes, relationships, and other selectors.
Q8. What is the difference between static and dynamic web pages?
View Answer
With a static page, the required information is present in the retrieved HTML. With a dynamic page, some content may be generated or fetched by JavaScript after the initial response.
Q9. Why might an API be preferable to scraping HTML?
View Answer
An API generally provides structured data and a defined interface, making extraction more predictable and less dependent on the website's visual HTML structure.
Q10. What should a responsible scraper consider?
View Answer
It should consider website terms, crawling guidance, request rates, data sensitivity, applicable law, access controls, and whether an official API is available.
54. Examination Questions — MCQs
Q1. Which library is commonly used to send HTTP requests in Python?
requestsopenpyxlmatplotlibtkinter
Answer: A
Q2. Which library is used to parse HTML in this lesson?
BeautifulSoupNumPyopenpyxlpypdf
Answer: A
Q3. Which property contains the HTTP status code?
response.coderesponse.status_coderesponse.httpresponse.status
Answer: B
Q4. Which method finds all matching HTML elements?
find()find_all()find_many()search_all()
Answer: B
Q5. Which method supports CSS selectors?
select()css()style()query_css()
Answer: A
Q6. Which selector targets an element with ID
main?
.main#mainmain()*main
Answer: B
Q7. What does strip=True commonly do in
get_text()?
- Removes HTML tags from the website
- Removes unnecessary surrounding whitespace
- Deletes the web page
- Downloads an image
Answer: B
Q8. Which function can convert a relative URL into an absolute URL?
urljoin()urlmake()urlabsolute()urlconvert()
Answer: A
55. Practical Examination Questions
Question 1 — Extract Headings
Retrieve a web page and print all h2 headings
using requests and BeautifulSoup.
Question 2 — Extract Links
Extract all links and their corresponding href
attributes from a web page.
Question 3 — Extract Images
Find all images and display their src and
alt attributes.
Question 4 — Extract Products
Extract product names and prices from HTML elements identified by CSS classes.
Question 5 — Multi-Page Scraping
Follow pagination links and collect records from multiple pages.
Question 6 — DataFrame Pipeline
Scrape structured records, convert them into a pandas DataFrame, clean the data, and save it as CSV.
56. Debugging a Scraper
When extraction fails, inspect the actual HTML before changing your Python code.
print(
response.status_code
)
print(
response.url
)
print(
response.text[:1000]
)
Also inspect:
- Whether the URL is correct.
- Whether the request succeeded.
- Whether the desired element exists in the response HTML.
- Whether the selector matches the current structure.
- Whether the content is generated dynamically.
- Whether the server returned an error or alternate page.
57. Common Web Scraping Errors
| Problem | Possible Cause | Solution |
|---|---|---|
404 |
Incorrect or unavailable URL. | Verify the URL. |
403 |
Access denied. | Respect access controls and site policies; do not attempt to bypass them. |
| Empty extraction | Selector does not match. | Inspect the current HTML structure. |
| Missing content | Content may be dynamically loaded. | Check the network/API architecture before choosing another tool. |
| Timeout | Slow or unavailable server. | Use a sensible timeout and controlled retry strategy. |
| Duplicate records | Repeated pages or elements. | Validate and deduplicate the extracted data. |
| Broken links | Relative or malformed URLs. | Use urljoin() and validate URLs. |
58. Web Scraping Best Practices
- Use timeouts for network requests.
- Check HTTP status codes.
-
Use
raise_for_status()where appropriate. - Use CSS selectors that accurately describe the required data.
- Keep fetching, parsing, cleaning, and saving as separate stages.
- Validate extracted data.
- Handle missing HTML elements gracefully.
- Avoid unnecessary requests.
- Use controlled request delays where appropriate.
- Prefer official APIs when available.
- Do not bypass authentication or access controls.
- Protect sensitive collected information.
- Log important processing events.
- Expect website HTML structures to change.
59. Expert Tips
First understand the HTML structure. Then write selectors.
Smaller extraction targets make pipelines faster and easier to maintain.
Prefer meaningful IDs, semantic classes, and stable structural relationships over brittle selectors based on changing presentation details.
A successful HTTP response does not guarantee that the desired data was extracted correctly.
Preserving raw extracted values can make debugging and reproducibility easier.
60. Capstone Project — Web Data Collection Pipeline
Build a reusable Python application that collects publicly accessible data from a static HTML website and prepares it for analysis.
Required Features
- Accept a starting URL.
-
Download the page with
requests. - Use a timeout and error handling.
- Parse HTML using BeautifulSoup.
- Extract structured records using CSS selectors.
- Convert relative URLs to absolute URLs.
- Follow permitted pagination links.
- Clean whitespace and malformed values.
- Remove duplicate records.
- Validate required fields.
- Store results in a pandas DataFrame.
- Export the final dataset to CSV and Excel.
- Log the number of pages processed and records collected.
61. Self-Assessment Checklist
Before moving to the next topic, make sure you can:
- ☐ Explain what web scraping is.
- ☐ Explain the difference between static and dynamic content.
- ☐ Install and import
requests. - ☐ Send an HTTP GET request.
- ☐ Read the HTTP status code.
- ☐ Use
raise_for_status(). - ☐ Set a request timeout.
- ☐ Parse HTML using BeautifulSoup.
- ☐ Use
find()andfind_all(). - ☐ Use
select()andselect_one(). - ☐ Extract text from elements.
- ☐ Extract HTML attributes.
- ☐ Extract links and images.
- ☐ Handle relative URLs.
- ☐ Scrape HTML tables.
- ☐ Extract structured records.
- ☐ Convert scraped records into a DataFrame.
- ☐ Save scraped data to CSV and Excel.
- ☐ Handle pagination.
- ☐ Add delays and controlled retries.
- ☐ Handle request and parsing errors.
- ☐ Validate scraped data.
- ☐ Explain responsible web scraping.
- ☐ Decide when an API is preferable to HTML scraping.
5.4 Browser Automation (Selenium) — Automating Browser Interactions, Form Filling, Navigation, Cookies and Dynamic Web Applications