Python Web Scraping Guide: Legal, Reliable, and Maintainable Data Collection

编程语言

Python is excellent for collecting public web data, but production scraping is not just "send requests and parse HTML." Real projects need permission checks, polite traffic, stable selectors, retry logic, data validation, and monitoring.

This guide focuses on compliant and maintainable scraping. It does not cover bypassing access controls, defeating bot protection, solving CAPTCHAs, or collecting data behind login, paywalls, or explicit restrictions. When a site offers an API, data export, feed, or partnership channel, use that first.

Useful references:

Start With A Permission Check

Before writing code, answer these questions:

  • Is the data public and intended to be viewed without login?
  • Does the site provide an API, sitemap, RSS feed, or export?
  • Does robots.txt allow crawling for the paths you need?
  • Do the terms of service restrict automated access or reuse?
  • Could the data include personal information, copyrighted content, or regulated records?
  • Can you identify your crawler and provide contact information?

If the answer is unclear, do not start with scraping. Ask for permission or use an official data source.

Choose The Right Tool

Page type Recommended approach Why
Static HTML httpx or requests + BeautifulSoup/parsel Simple, fast, low overhead
Many similar pages Scrapy Scheduling, retries, pipelines, exports
JavaScript-rendered pages Playwright Executes client-side rendering
API-backed pages Call the public API if allowed More stable and efficient than HTML parsing
One-off data cleanup Pandas + saved HTML/CSV Avoid repeated requests

Use the simplest tool that can collect the data reliably and politely.

A Small Static Scraper

For static pages, start with explicit timeouts, headers that identify your crawler, and strict parsing.

from __future__ import annotations

import time
from dataclasses import dataclass
from urllib.parse import urljoin

import httpx
from bs4 import BeautifulSoup


BASE_URL = "https://example.com/articles/"


@dataclass
class Article:
    title: str
    url: str
    summary: str


def fetch_html(url: str) -> str:
    headers = {
        "User-Agent": "ExampleResearchBot/1.0 (+https://example.com/contact)"
    }
    with httpx.Client(timeout=15, follow_redirects=True, headers=headers) as client:
        response = client.get(url)
        response.raise_for_status()
        return response.text


def parse_articles(html: str) -> list[Article]:
    soup = BeautifulSoup(html, "html.parser")
    articles: list[Article] = []

    for card in soup.select("article.card"):
        title_el = card.select_one("h2 a")
        summary_el = card.select_one(".summary")
        if not title_el:
            continue

        title = title_el.get_text(" ", strip=True)
        url = urljoin(BASE_URL, title_el.get("href", ""))
        summary = summary_el.get_text(" ", strip=True) if summary_el else ""
        articles.append(Article(title=title, url=url, summary=summary))

    return articles


def main() -> None:
    html = fetch_html(BASE_URL)
    for article in parse_articles(html):
        print(article)
    time.sleep(2)


if __name__ == "__main__":
    main()

The selector choices should be easy to test. If a selector fails, return fewer records and log the problem rather than silently storing broken data.

Handling Dynamic Pages With Playwright

Use Playwright when the target content is created by JavaScript and there is no permitted API or feed.

from playwright.sync_api import sync_playwright


def collect_titles(url: str) -> list[str]:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url, wait_until="networkidle", timeout=30_000)

        titles = [
            item.inner_text().strip()
            for item in page.locator("article h2").all()
        ]

        browser.close()
        return titles

Playwright is heavier than a normal HTTP request. Use it only for pages that need rendering. If the same content is available in the HTML or a permitted public API, do not start a browser.

Scaling With Scrapy

Scrapy is a good fit when you have many pages, retries, exports, and data pipelines.

import scrapy


class ArticleSpider(scrapy.Spider):
    name = "articles"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com/articles/"]

    custom_settings = {
        "ROBOTSTXT_OBEY": True,
        "CONCURRENT_REQUESTS_PER_DOMAIN": 2,
        "DOWNLOAD_DELAY": 2,
        "AUTOTHROTTLE_ENABLED": True,
        "AUTOTHROTTLE_TARGET_CONCURRENCY": 1.0,
        "USER_AGENT": "ExampleResearchBot/1.0 (+https://example.com/contact)",
        "FEEDS": {
            "articles.jsonl": {"format": "jsonlines", "encoding": "utf8"}
        },
    }

    def parse(self, response):
        for card in response.css("article.card"):
            title = card.css("h2 a::text").get()
            href = card.css("h2 a::attr(href)").get()
            if title and href:
                yield {
                    "title": title.strip(),
                    "url": response.urljoin(href),
                    "source_url": response.url,
                }

        next_url = response.css("a.next::attr(href)").get()
        if next_url:
            yield response.follow(next_url, callback=self.parse)

The important parts are not just the spider. Settings such as ROBOTSTXT_OBEY, domain concurrency, download delay, and AutoThrottle help keep traffic predictable.

Data Quality Pipeline

Raw scraped data is rarely ready to use. Add validation and normalization early.

from itemadapter import ItemAdapter


class CleanArticlePipeline:
    def process_item(self, item, spider):
        adapter = ItemAdapter(item)

        title = (adapter.get("title") or "").strip()
        url = (adapter.get("url") or "").strip()

        if not title or not url:
            raise ValueError("Missing title or URL")

        adapter["title"] = " ".join(title.split())
        adapter["url"] = url
        return item

In production, store validation failures separately so selector changes are visible.

Polite Crawling Rules

Polite crawlers reduce load and are easier to operate:

  • Respect robots.txt and site terms.
  • Use low concurrency by default.
  • Add delay and AutoThrottle.
  • Cache responses during development.
  • Avoid repeated downloads of unchanged pages.
  • Stop on repeated errors instead of retrying aggressively.
  • Identify your crawler with a clear user agent.
  • Provide a contact address or page.

If a site blocks or challenges the crawler, treat that as a boundary. Do not build around it without permission.

Incremental Crawling

For recurring jobs, do not fetch everything every time.

Use:

  • Sitemaps or feeds when available.
  • Last seen URL or timestamp.
  • ETag and Last-Modified headers when supported.
  • Content hashes to detect changes.
  • A persistent seen-URL store.
import hashlib


def content_hash(text: str) -> str:
    normalized = " ".join(text.split())
    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()

Incremental crawling saves bandwidth, reduces load, and makes data changes easier to audit.

Monitoring

Production scrapers should report:

  • Pages fetched.
  • Records extracted.
  • Empty pages.
  • HTTP status distribution.
  • Retry count.
  • Parse failure count.
  • Duplicate rate.
  • Job duration.

A sudden drop in extracted records usually means the site changed its markup or your crawler reached a restriction.

Common Mistakes

Parsing With Fragile Selectors

Selectors like .container > div:nth-child(3) > span break easily. Prefer semantic selectors, stable attributes, or structured data embedded in the page.

Ignoring Character Encoding

Always test multilingual data. Broken encoding can silently corrupt titles, addresses, and names.

Mixing Collection And Business Logic

Keep crawling, parsing, validation, and storage separate. That makes it easier to fix one layer without changing everything.

Running Too Fast

High concurrency causes bans, incomplete data, and operational risk. A slower crawler that finishes reliably is better than a fast crawler that breaks or overloads a site.

No Reproducible Input

Save sample HTML responses for parser tests. When the site changes, you can compare old and new markup.

Final Checklist

Before running a crawler repeatedly, confirm:

  • You are allowed to collect the target data.
  • robots.txt and terms have been checked.
  • The crawler identifies itself.
  • Rate limits and delays are configured.
  • Parser tests cover representative pages.
  • Data validation catches empty or malformed records.
  • Incremental crawling avoids unnecessary requests.
  • Logs and metrics make failures visible.
  • Personal or sensitive data is handled according to applicable rules.

Reliable scraping is mostly engineering discipline: collect only what you are allowed to collect, request gently, validate everything, and watch the job after it runs.

Try these browser-local tools — no sign-up required →

#Python爬虫#反爬虫#Scrapy#Playwright爬虫#2026#编程语言