Comprehensive Web Scraping Guide
Web scraping from basics to advanced techniques, covering Requests, Beautiful Soup, Selenium, and Scrapy.
Table of Contents
- Introduction to Web Scraping
- Setting Up Your Environment
- Primer on Web Technologies
- Mastering Requests Library
- Beautiful Soup for HTML Parsing
- Selenium for Dynamic Content
- Scrapy for Large-Scale Scraping
- Handling Challenges
- Best Practices
- Real-World Projects
Introduction to Web Scraping
What is Web Scraping?
Web scraping is the process of extracting data from websites automatically. Instead of manually copying information, you write programs to fetch and parse web pages.
Common Use Cases:
- Price monitoring (e-commerce)
- News aggregation
- Research data collection
- Social media analysis
- Job listings aggregation
- Real estate listings
- Stock market data
- Product reviews
Types of Web Scraping
1. Static Web Scraping
- What: Scraping HTML content that's already loaded
- Tools: Requests + Beautiful Soup
- Use When: Content is in the initial HTML response
- Example: Blog posts, product descriptions
2. Dynamic Web Scraping
- What: Scraping content loaded by JavaScript
- Tools: Selenium, Playwright
- Use When: Content loads after page load via JavaScript
- Example: Social media feeds, single-page applications
3. API-Based Scraping
- What: Using official APIs to get data
- Tools: Requests library
- Use When: Website provides API access
- Example: Twitter API, GitHub API
Ethical Considerations
Always:
- Check robots.txt:
https://website.com/robots.txt - Read Terms of Service: Ensure scraping is allowed
- Respect rate limits: Don't overload servers
- Use delays: Add time.sleep() between requests
- Identify yourself: Use proper User-Agent headers
- Respect copyright: Don't republish copyrighted content
Legal Considerations:
- Some websites explicitly prohibit scraping
- Check website's Terms of Service
- Consider reaching out for permission
- Be aware of data protection laws (GDPR, etc.)
Advantages of Web Scraping
- Automation: Collect data automatically
- Efficiency: Faster than manual collection
- Scalability: Handle large amounts of data
- Real-time: Get up-to-date information
- Cost-effective: No need for paid APIs
Disadvantages of Web Scraping
- Fragile: Breaks when website structure changes
- Legal risks: May violate Terms of Service
- Technical complexity: Requires programming skills
- Maintenance: Needs ongoing updates
- Rate limiting: Websites may block excessive requests
Alternatives to Web Scraping
- Official APIs: Best option if available
- RSS Feeds: For news and blog content
- Data Exports: Some sites offer CSV/JSON exports
- Third-party Services: Paid data providers
- Partnerships: Direct data sharing agreements
Setting Up Your Environment
Required Libraries
# Core libraries
pip install requests beautifulsoup4 lxml
# Selenium for dynamic content
pip install selenium
# Scrapy for large-scale scraping
pip install scrapy
# Additional utilities
pip install pandas openpyxl # For data handling
pip install fake-useragent # For user agent rotation
Installing Selenium WebDriver
Chrome WebDriver:
# Install ChromeDriver
# macOS
brew install chromedriver
# Or download from: https://chromedriver.chromium.org/
# Add to PATH
Using Selenium Manager (Automatic):
# Selenium 4.6+ automatically manages drivers
from selenium import webdriver
driver = webdriver.Chrome() # No need to specify driver path
Primer on Web Technologies
Client-Server Architecture
Client (Browser/Scraper) ←→ Server (Website)
Request Response
How it works:
- Client sends HTTP request
- Server processes request
- Server sends HTTP response
- Client receives and displays/processes data
HTTP Request & Response
HTTP Request Structure:
GET /page.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0...
Accept: text/html
HTTP Response Structure:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234
<html>...</html>
HTTP Methods
| Method | Purpose | Use Case |
|---|---|---|
| GET | Retrieve data | Fetching web pages, API data |
| POST | Send data | Form submissions, API calls |
| PUT | Update resource | API updates |
| DELETE | Remove resource | API deletions |
HTTP Status Codes
| Code | Meaning | Scraping Implication |
|---|---|---|
| 200 | OK | Success, proceed |
| 301/302 | Redirect | Follow redirect |
| 404 | Not Found | Page doesn't exist |
| 403 | Forbidden | Access denied, may need authentication |
| 429 | Too Many Requests | Rate limited, slow down |
| 500 | Server Error | Server issue, retry later |
Web Technologies Overview
HTML: Structure of web pages
<div class="product">
<h2>Product Name</h2>
<p class="price">$99.99</p>
</div>
CSS: Styling (selectors used in scraping)
.product .price { color: green; }
JavaScript: Dynamic content loading
fetch('/api/data').then(response => response.json())
Understanding these helps you:
- Find the right HTML elements to scrape
- Understand why some content needs Selenium
- Debug scraping issues
Mastering Requests Library
About Requests
The requests library is the standard Python library for making HTTP requests. It's simple, intuitive, and powerful.
Basic GET Request
import requests
# Simple GET request
resp>'https://example.com')
print(response.status_code) # 200
print(response.text) # HTML content
print(response.headers) # Response headers
Working with GET Method
import requests
# GET with parameters
params = {
'q': 'python',
'page': 1
}
resp>'https://example.com/search', params=params)
print(response.url) # https://example.com/search?q=python&page=1
# GET with custom headers
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml'
}
resp>'https://example.com', headers=headers)
# GET with timeout
resp>'https://example.com', timeout=10)
Working with POST Method
import requests
# POST with form data
data = {
'username': 'user',
'password': 'pass'
}
resp>'https://example.com/login', data=data)
# POST with JSON
js>
'name': 'Product',
'price': 99.99
}
resp>'https://api.example.com/products', json=json_data)
# POST with files
files = {'file': open('data.csv', 'rb')}
resp>'https://example.com/upload', files=files)
Working with PUT and DELETE Methods
import requests
# PUT request (update)
data = {'name': 'Updated Product'}
resp>'https://api.example.com/products/1', json=data)
# DELETE request
resp>'https://api.example.com/products/1')
Working with HTTP Headers
import requests
from fake_useragent import UserAgent
# Custom headers
headers = {
'User-Agent': UserAgent().random, # Random user agent
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': 'https://google.com'
}
resp>'https://example.com', headers=headers)
# View request headers sent
print(response.request.headers)
Working with Response Object
import requests
resp>'https://example.com')
# Status code
print(response.status_code)
# Headers
print(response.headers)
print(response.headers['Content-Type'])
# Content
print(response.text) # String (for text)
print(response.content) # Bytes (for binary)
print(response.json()) # JSON (if applicable)
# URL
print(response.url) # Final URL (after redirects)
print(response.history) # Redirect history
# Cookies
print(response.cookies)
Working with Public APIs
import requests
import json
# Example: GitHub API
url = 'https://api.github.com/users/octocat'
resp>
if response.status_code == 200:
data = response.json()
print(f"Username: {data['login']}")
print(f"Followers: {data['followers']}")
print(f"Repositories: {data['public_repos']}")
else:
print(f"Error: {response.status_code}")
# Example: REST API with authentication
headers = {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
}
resp>'https://api.example.com/data', headers=headers)
Error Handling
import requests
from requests.exceptions import RequestException, Timeout, ConnectionError
try:
resp>'https://example.com', timeout=5)
response.raise_for_status() # Raises exception for bad status codes
print("Success!")
except Timeout:
print("Request timed out")
except ConnectionError:
print("Connection error")
except RequestException as e:
print(f"Request failed: {e}")
Beautiful Soup for HTML Parsing
About Beautiful Soup
Beautiful Soup is a Python library for parsing HTML and XML documents. It creates a parse tree that makes it easy to extract data.
Why Beautiful Soup?
- Easy to use
- Handles malformed HTML
- Powerful search capabilities
- Works well with Requests
Creating a Soup Object
from bs4 import BeautifulSoup
import requests
# From string
html = '<html><body><p>Hello World</p></body></html>'
soup = BeautifulSoup(html, 'html.parser')
# From file
with open('page.html', 'r') as f:
soup = BeautifulSoup(f, 'html.parser')
# From URL (using Requests)
resp>'https://example.com')
soup = BeautifulSoup(response.text, 'html.parser')
# Using different parsers
soup = BeautifulSoup(html, 'html.parser') # Built-in (slower)
soup = BeautifulSoup(html, 'lxml') # Fast, requires lxml
soup = BeautifulSoup(html, 'html5lib') # Most lenient, slowest
Exploring the Soup Object
from bs4 import BeautifulSoup
import requests
resp>'https://example.com')
soup = BeautifulSoup(response.text, 'html.parser')
# Access tags
print(soup.title) # <title>Example</title>
print(soup.title.string) # Example
print(soup.h1) # First <h1> tag
# Find single element
first_p = soup.find('p')
print(first_p.text)
# Find all elements
all_links = soup.find_all('a')
for link in all_links:
print(link.get('href'))
# Find by class
products = soup.find_all('div', class_='product')
# Find by ID
header = soup.find(id='header')
# Find by attributes
images = soup.find_all('img', src=True)
# CSS selectors (powerful!)
prices = soup.select('.price')
titles = soup.select('h2.product-title')
Common Beautiful Soup Operations
from bs4 import BeautifulSoup
html = '''
<div class="product">
<h2>Laptop</h2>
<p class="price">$999.99</p>
<a href="/product/1">View Details</a>
</div>
'''
soup = BeautifulSoup(html, 'html.parser')
# Get text content
product = soup.find('div', class_='product')
print(product.get_text()) # All text
print(product.get_text(strip=True)) # Text without extra whitespace
# Get attribute values
link = soup.find('a')
print(link.get('href')) # /product/1
print(link['href']) # Alternative syntax
# Navigate the tree
product = soup.find('div', class_='product')
title = product.find('h2')
price = product.find('p', class_='price')
# Find parent, siblings
price = soup.find('p', class_='price')
parent = price.parent
siblings = price.next_siblings
# Extract data
products = []
for div in soup.find_all('div', class_='product'):
products.append({
'title': div.find('h2').text,
'price': div.find('p', class_='price').text,
'link': div.find('a')['href']
})
Mini Project: Scraping Product Listings
import requests
from bs4 import BeautifulSoup
import pandas as pd
def scrape_products(url):
"""Scrape product information from a webpage"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
resp headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
products = []
# Find all product containers (adjust selector based on website)
product_divs = soup.find_all('div', class_='product-item')
for product in product_divs:
try:
name = product.find('h3', class_='product-name').text.strip()
price = product.find('span', class_='price').text.strip()
rating = product.find('div', class_='rating').get('data-rating', 'N/A')
link = product.find('a')['href']
products.append({
'name': name,
'price': price,
'rating': rating,
'link': link
})
except AttributeError:
continue # Skip if element not found
return products
# Example usage
url = 'https://example-store.com/products'
products = scrape_products(url)
# Convert to DataFrame
df = pd.DataFrame(products)
print(df)
# Save to CSV
df.to_csv('products.csv', index=False)
Selenium for Dynamic Content
About Selenium
Selenium is a tool for automating web browsers. It's essential for scraping JavaScript-rendered content that doesn't appear in the initial HTML.
When to Use Selenium:
- Content loaded by JavaScript
- Need to interact with page (clicks, forms)
- Single-page applications (SPAs)
- Infinite scroll pages
Getting Started with Selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Initialize driver
driver = webdriver.Chrome() # Or Firefox(), Edge(), etc.
# Navigate to page
driver.get('https://example.com')
# Get page source
html = driver.page_source
# Get current URL
print(driver.current_url)
# Get page title
print(driver.title)
# Close browser
driver.quit()
Strategies for Locating Web Elements
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://example.com')
# By ID
element = driver.find_element(By.ID, 'username')
# By Class Name
elements = driver.find_elements(By.CLASS_NAME, 'product')
# By Tag Name
links = driver.find_elements(By.TAG_NAME, 'a')
# By CSS Selector
price = driver.find_element(By.CSS_SELECTOR, '.price')
products = driver.find_elements(By.CSS_SELECTOR, 'div.product-item')
# By XPath
title = driver.find_element(By.XPATH, '//h1[@class="title"]')
# By Link Text
link = driver.find_element(By.LINK_TEXT, 'Click Here')
# By Partial Link Text
link = driver.find_element(By.PARTIAL_LINK_TEXT, 'Click')
Understanding XPath
XPath is a language for finding elements in XML/HTML documents.
# Absolute XPath (fragile)
element = driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/p')
# Relative XPath (better)
element = driver.find_element(By.XPATH, '//p[@class="description"]')
# XPath examples
# Find by text
element = driver.find_element(By.XPATH, '//button[text()="Submit"]')
# Find by attribute
element = driver.find_element(By.XPATH, '//input[@type="text"]')
# Find by contains
element = driver.find_element(By.XPATH, '//div[contains(@class, "product")]')
# Find parent
parent = driver.find_element(By.XPATH, '//span[@class="price"]/parent::div')
# Find following sibling
next = driver.find_element(By.XPATH, '//h2/following-sibling::p')
Basic Interaction with Web Elements
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
driver = webdriver.Chrome()
driver.get('https://example.com')
# Click element
button = driver.find_element(By.ID, 'submit-btn')
button.click()
# Type text
search_box = driver.find_element(By.ID, 'search')
search_box.send_keys('python')
search_box.send_keys(Keys.RETURN) # Press Enter
# Clear text
search_box.clear()
# Get text
title = driver.find_element(By.TAG_NAME, 'h1').text
# Get attribute
link = driver.find_element(By.TAG_NAME, 'a')
href = link.get_attribute('href')
# Check if element is displayed
if button.is_displayed():
button.click()
Working with Dropdowns
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
driver = webdriver.Chrome()
driver.get('https://example.com')
# Select by visible text
dropdown = Select(driver.find_element(By.ID, 'country'))
dropdown.select_by_visible_text('United States')
# Select by value
dropdown.select_by_value('us')
# Select by index
dropdown.select_by_index(0)
# Get all options
opti>
for option in options:
print(option.text)
# Get selected option
selected = dropdown.first_selected_option.text
Working with Multiselect
from selenium.webdriver.support.ui import Select
multiselect = Select(driver.find_element(By.ID, 'languages'))
multiselect.select_by_visible_text('Python')
multiselect.select_by_visible_text('JavaScript')
# Deselect
multiselect.deselect_by_visible_text('Python')
multiselect.deselect_all()
Basic Scrolling
from selenium.webdriver.common.keys import Keys
# Scroll to element
element = driver.find_element(By.ID, 'footer')
driver.execute_script("arguments[0].scrollIntoView();", element)
# Scroll by pixels
driver.execute_script("window.scrollBy(0, 1000);")
# Scroll to bottom
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Scroll to top
driver.execute_script("window.scrollTo(0, 0);")
# Using keyboard
driver.find_element(By.TAG_NAME, 'body').send_keys(Keys.PAGE_DOWN)
Infinite Scrolling
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get('https://example.com/infinite-scroll')
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
# Scroll to bottom
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Wait for new content
time.sleep(2)
# Calculate new height
new_height = driver.execute_script("return document.body.scrollHeight")
if new_height == last_height:
break # No more content
last_height = new_height
# Now scrape all loaded content
products = driver.find_elements(By.CLASS_NAME, 'product')
print(f"Found {len(products)} products")
Explicit Waits
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://example.com')
# Wait for element to be present
wait = WebDriverWait(driver, 10)
element = wait.until(EC.presence_of_element_located((By.ID, 'content')))
# Wait for element to be clickable
button = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))
# Wait for element to be visible
title = wait.until(EC.visibility_of_element_located((By.TAG_NAME, 'h1')))
# Wait for text to be present
wait.until(EC.text_to_be_present_in_element((By.ID, 'status'), 'Loaded'))
Implicit Waits
from selenium import webdriver
driver = webdriver.Chrome()
driver.implicitly_wait(10) # Wait up to 10 seconds for elements
driver.get('https://example.com')
# All find_element calls will wait up to 10 seconds
element = driver.find_element(By.ID, 'content')
Working with IFrames
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://example.com')
# Switch to iframe by index
driver.switch_to.frame(0)
# Switch to iframe by name or ID
driver.switch_to.frame('iframe-name')
# Switch to iframe by element
iframe = driver.find_element(By.TAG_NAME, 'iframe')
driver.switch_to.frame(iframe)
# Do something in iframe
element = driver.find_element(By.ID, 'content')
# Switch back to main content
driver.switch_to.default_content()
Working with Alerts
from selenium.webdriver.common.alert import Alert
driver = webdriver.Chrome()
driver.get('https://example.com')
# Wait for alert
alert = driver.switch_to.alert
# Get alert text
print(alert.text)
# Accept alert
alert.accept()
# Dismiss alert
alert.dismiss()
# Send text to prompt
alert.send_keys('input text')
alert.accept()
Best Practices & Optimization
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# Configure Chrome options
chrome_opti>
chrome_options.add_argument('--headless') # Run without GUI
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
# Set user agent
chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
driver = webdriver.Chrome(options=chrome_options)
# Execute script to hide webdriver property
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': 'Object.defineProperty(navigator, "webdriver", {get: () => undefined})'
})
Scrapy for Large-Scale Scraping
Introduction to Scrapy
Scrapy is a powerful framework for large-scale web scraping. It's faster and more efficient than Requests + Beautiful Soup for scraping multiple pages.
When to Use Scrapy:
- Scraping multiple pages
- Large-scale data extraction
- Need for speed and efficiency
- Complex crawling requirements
Installation
pip install scrapy
Basic Setup & Project Structure
# Create new Scrapy project
scrapy startproject myproject
# Project structure:
# myproject/
# scrapy.cfg
# myproject/
# __init__.py
# items.py # Define data structure
# middlewares.py # Custom middleware
# pipelines.py # Data processing pipelines
# settings.py # Project settings
# spiders/ # Spiders directory
# __init__.py
Running a Spider
# Create spider
cd myproject
scrapy genspider example example.com
# Run spider
scrapy crawl example
# Save output
scrapy crawl example -o items.json
scrapy crawl example -o items.csv
Spiders
Definition: A spider is a class that defines how to scrape a website.
Anatomy of a Spider:
import scrapy
class ExampleSpider(scrapy.Spider):
name = 'example' # Unique identifier
allowed_domains = ['example.com']
start_urls = ['https://example.com']
def parse(self, response):
# Extract data
title = response.css('h1::text').get()
# Follow links
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
# Yield items
yield {
'title': title,
'url': response.url
}
Types of Spiders
1. Spider (Basic)
import scrapy
class BasicSpider(scrapy.Spider):
name = 'basic'
start_urls = ['https://example.com']
def parse(self, response):
yield {'data': response.css('p::text').getall()}
2. CrawlSpider (Rule-based)
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
class CrawlSpiderExample(CrawlSpider):
name = 'crawl'
start_urls = ['https://example.com']
rules = (
Rule(LinkExtractor(allow=r'/page/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
yield {'title': response.css('h1::text').get()}
3. XMLFeedSpider
from scrapy.spiders import XMLFeedSpider
class XMLSpider(XMLFeedSpider):
name = 'xml'
start_urls = ['https://example.com/feed.xml']
iterator = 'iternodes'
def parse_node(self, response, node):
yield {'title': node.xpath('title/text()').get()}
Scrapy Shell
# Interactive shell for testing selectors
scrapy shell 'https://example.com'
# In shell:
>>> response.css('h1::text').get()
>>> response.xpath('//h1/text()').get()
>>> response.css('a::attr(href)').getall()
Scrapy Spider with Python
import scrapy
class ProductSpider(scrapy.Spider):
name = 'products'
start_urls = ['https://example-store.com/products']
def parse(self, response):
# Extract product links
product_links = response.css('a.product-link::attr(href)').getall()
for link in product_links:
yield response.follow(link, self.parse_product)
# Follow pagination
next_page = response.css('a.next::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
def parse_product(self, response):
yield {
'name': response.css('h1.product-title::text').get(),
'price': response.css('.price::text').get(),
'description': response.css('.description::text').getall(),
'url': response.url
}
Advanced Features
Custom Spider Settings
# In spider
class MySpider(scrapy.Spider):
custom_settings = {
'DOWNLOAD_DELAY': 2, # 2 second delay
'CONCURRENT_REQUESTS': 16,
'USER_AGENT': 'MyBot 1.0'
}
Data & Item Pipelines
# items.py
import scrapy
class ProductItem(scrapy.Item):
name = scrapy.Field()
price = scrapy.Field()
url = scrapy.Field()
# pipelines.py
class PricePipeline:
def process_item(self, item, spider):
# Clean price
price = item['price']
item['price'] = float(price.replace('$', '').replace(',', ''))
return item
class DuplicatesPipeline:
def __init__(self):
self.ids_seen = set()
def process_item(self, item, spider):
if item['url'] in self.ids_seen:
raise DropItem(f"Duplicate item: {item['url']}")
else:
self.ids_seen.add(item['url'])
return item
User-Agent Rotation & Proxy Usage
# settings.py
ROTATING_PROXY_LIST = [
'proxy1:port',
'proxy2:port',
]
DOWNLOADER_MIDDLEWARES = {
'rotating_proxies.middlewares.RotatingProxyMiddleware': 610,
'rotating_proxies.middlewares.BanDetectionMiddleware': 620,
}
# Or use fake-useragent
from fake_useragent import UserAgent
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
ua = UserAgent()
request.headers['User-Agent'] = ua.random
return None
Login Pages
import scrapy
class LoginSpider(scrapy.Spider):
name = 'login'
start_urls = ['https://example.com/login']
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={'username': 'user', 'password': 'pass'},
callback=self.after_login
)
def after_login(self, response):
if 'Welcome' in response.text:
# Login successful, start scraping
yield scrapy.Request('https://example.com/dashboard', self.parse_dashboard)
def parse_dashboard(self, response):
# Scrape protected content
pass
Handling APIs
import scrapy
import json
class APISpider(scrapy.Spider):
name = 'api'
def start_requests(self):
# Make API request
yield scrapy.Request(
'https://api.example.com/data',
headers={'Authorization': 'Bearer TOKEN'},
callback=self.parse_api
)
def parse_api(self, response):
data = json.loads(response.text)
for item in data['results']:
yield item
Mini Project: Scrapy E-commerce Scraper
import scrapy
from scrapy.crawler import CrawlerProcess
class EcommerceSpider(scrapy.Spider):
name = 'ecommerce'
start_urls = ['https://example-store.com']
custom_settings = {
'DOWNLOAD_DELAY': 1,
'RANDOMIZE_DOWNLOAD_DELAY': 0.5,
'USER_AGENT': 'Mozilla/5.0...'
}
def parse(self, response):
# Extract product links
products = response.css('div.product-item')
for product in products:
yield {
'name': product.css('h3::text').get(),
'price': product.css('.price::text').get(),
'rating': product.css('.rating::text').get(),
'link': product.css('a::attr(href)').get()
}
# Pagination
next_page = response.css('a.next-page::attr(href)').get()
if next_page:
yield response.follow(next_page, self.parse)
# Run spider
if __name__ == '__main__':
process = CrawlerProcess()
process.crawl(EcommerceSpider)
process.start()
Handling Challenges
Understanding CAPTCHAs
CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is used to prevent automated access.
Types:
- Text-based CAPTCHA
- Image selection
- reCAPTCHA v2/v3
- hCaptcha
Preventing CAPTCHAs
# Best practices to avoid CAPTCHAs:
# 1. Use delays
import time
time.sleep(2) # Between requests
# 2. Rotate user agents
from fake_useragent import UserAgent
ua = UserAgent()
headers = {'User-Agent': ua.random}
# 3. Use proxies
proxies = {
'http': 'http://proxy:port',
'https': 'https://proxy:port'
}
# 4. Respect robots.txt
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url('https://example.com/robots.txt')
rp.read()
if rp.can_fetch('*', 'https://example.com/page'):
# Scrape allowed
pass
Handling CAPTCHAs (Theory)
Options:
- Manual solving: Pause script, solve manually
- CAPTCHA solving services: 2Captcha, Anti-Captcha
- OCR libraries: pytesseract (for simple text CAPTCHAs)
- Browser automation: Selenium with manual intervention
Handling CAPTCHAs using input()
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://example.com')
# Wait for user to solve CAPTCHA manually
input("Please solve the CAPTCHA and press Enter...")
# Continue scraping
c 'content').text
print(content)
Handling CAPTCHAs using pytesseract & OpenCV
from selenium import webdriver
from PIL import Image
import pytesseract
import cv2
import numpy as np
driver = webdriver.Chrome()
driver.get('https://example.com')
# Find CAPTCHA image
captcha_img = driver.find_element(By.ID, 'captcha-image')
# Take screenshot
captcha_img.screenshot('captcha.png')
# Preprocess image
img = cv2.imread('captcha.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# OCR
text = pytesseract.image_to_string(thresh)
print(f"CAPTCHA text: {text}")
# Enter CAPTCHA
driver.find_element(By.ID, 'captcha-input').send_keys(text)
Note: OCR-based CAPTCHA solving has low success rate. Consider using CAPTCHA solving services for production.
Best Practices
1. Respect Rate Limits
import time
import random
def scrape_with_delay(urls):
for url in urls:
resp>
# Random delay between 1-3 seconds
time.sleep(random.uniform(1, 3))
2. Handle Errors Gracefully
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session():
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
3. Use Sessions
import requests
session = requests.Session()
session.headers.update({'User-Agent': 'MyBot'})
# Reuse session for multiple requests (faster)
for url in urls:
resp>
4. Cache Responses
import requests_cache
session = requests_cache.CachedSession('cache', expire_after=3600)
resp>'https://example.com') # Cached for 1 hour
5. Monitor and Log
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def scrape_with_logging(url):
try:
resp>
logger.info(f"Scraped {url}: {response.status_code}")
return response
except Exception as e:
logger.error(f"Error scraping {url}: {e}")
return None
6. Data Validation
def validate_scraped_data(data):
"""Validate scraped data before saving"""
required_fields = ['name', 'price', 'url']
for field in required_fields:
if field not in data or not data[field]:
return False
# Validate price format
try:
float(data['price'].replace('$', ''))
except ValueError:
return False
return True
Real-World Projects
Project 1: Yahoo Finance Stock Data
Goal: Scrape stock prices and financial data from Yahoo Finance.
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
def scrape_stock_data(symbol):
"""Scrape stock data for a given symbol"""
url = f'https://finance.yahoo.com/quote/{symbol}'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
resp headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
# Extract data (adjust selectors based on actual page structure)
data = {
'symbol': symbol,
'price': soup.find('span', {'data-field': 'regularMarketPrice'}).text,
'change': soup.find('span', {'data-field': 'regularMarketChange'}).text,
'volume': soup.find('span', {'data-field': 'regularMarketVolume'}).text
}
return data
# Scrape multiple stocks
symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN']
stocks_data = []
for symbol in symbols:
data = scrape_stock_data(symbol)
stocks_data.append(data)
time.sleep(1) # Be respectful
df = pd.DataFrame(stocks_data)
print(df)
Project 2: Real Estate Listings
Goal: Scrape property listings with Selenium for dynamic content.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pandas as pd
import time
def scrape_real_estate(url):
"""Scrape real estate listings"""
driver = webdriver.Chrome()
driver.get(url)
properties = []
# Wait for listings to load
wait = WebDriverWait(driver, 10)
listings = wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, 'property-item')))
for listing in listings:
try:
property_data = {
'title': listing.find_element(By.CLASS_NAME, 'title').text,
'price': listing.find_element(By.CLASS_NAME, 'price').text,
'location': listing.find_element(By.CLASS_NAME, 'location').text,
'bedrooms': listing.find_element(By.CLASS_NAME, 'bedrooms').text,
'bathrooms': listing.find_element(By.CLASS_NAME, 'bathrooms').text,
'area': listing.find_element(By.CLASS_NAME, 'area').text
}
properties.append(property_data)
except Exception as e:
print(f"Error extracting property: {e}")
continue
# Handle pagination
try:
next_button = driver.find_element(By.CLASS_NAME, 'next-page')
if next_button.is_enabled():
next_button.click()
time.sleep(2)
# Recursively scrape next page
properties.extend(scrape_real_estate(driver.current_url))
except:
pass
driver.quit()
return properties
# Usage
url = 'https://example-realestate.com/listings'
properties = scrape_real_estate(url)
df = pd.DataFrame(properties)
df.to_csv('real_estate.csv', index=False)
Project 3: News Article Aggregator
Goal: Scrape news articles from multiple sources.
import requests
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime
def scrape_news_article(url):
"""Scrape a single news article"""
headers = {'User-Agent': 'Mozilla/5.0...'}
resp headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
article = {
'title': soup.find('h1').text.strip(),
'author': soup.find('span', class_='author').text if soup.find('span', class_='author') else 'Unknown',
'date': soup.find('time').get('datetime') if soup.find('time') else None,
'content': ' '.join([p.text for p in soup.find_all('p', class_='article-content')]),
'url': url
}
return article
def scrape_news_site(base_url):
"""Scrape all articles from a news site"""
headers = {'User-Agent': 'Mozilla/5.0...'}
resp headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
articles = []
article_links = soup.find_all('a', class_='article-link')
for link in article_links[:10]: # Limit to 10 for demo
article_url = link['href']
if not article_url.startswith('http'):
article_url = base_url + article_url
article = scrape_news_article(article_url)
articles.append(article)
time.sleep(1)
return articles
# Scrape multiple news sites
news_sites = [
'https://news-site-1.com',
'https://news-site-2.com'
]
all_articles = []
for site in news_sites:
articles = scrape_news_site(site)
all_articles.extend(articles)
df = pd.DataFrame(all_articles)
df.to_csv('news_articles.csv', index=False)
Key Takeaways
- Start Simple: Use Requests + Beautiful Soup for static content
- Use Selenium: When content is loaded by JavaScript
- Use Scrapy: For large-scale, multi-page scraping
- Be Ethical: Respect robots.txt, add delays, use proper headers
- Handle Errors: Implement retries and error handling
- Validate Data: Check data quality before saving
- Monitor: Log your scraping activities
- Legal: Always check Terms of Service
Resources
- Requests Documentation
- Beautiful Soup Documentation
- Selenium Documentation
- Scrapy Documentation
- Web Scraping Best Practices
Try next: Scrape one allowed page with rate limiting and a robots.txt check. Store raw HTML before parsing.