用Python实现网页抓取:从URL请求到数据入库的全流程

木白
木白
发布时间:2026-11-06| 9人浏览过
原创

本文系统拆解用Python实现网页抓取并写入数据库的端到端技术链路,包含URL请求与会话管理、HTML节点提取与清洗、SQLite建表与防重入库,并附带完整可运行的工程化代码。

实现网页数据抓取并持久化,本质是由“网络通信、文本解析、数据清洗、事务入库”四个严谨模块串联而成的工程流水线。本文以 Python 标准生态中的 requests、BeautifulSoup4 以及内置轻量级关系型数据库 sqlite3 为基础,完整讲解从发起 HTTP 请求、解析 HTML 节点、清洗提取目标字段,直到建立数据表与幂等写入的全套实现方法与防御机制。

用Python实现网页抓取:从URL请求到数据入库的全流程
图 0:Python 网页抓取工程全流程技术流水线概览

一、全流程架构:网页抓取的四个阶段

许多初学者在刚开始编写网页抓取脚本时,往往把请求、提取和写库混在一个扁平的脚本甚至单个循环中。一旦网络稍有波动或者某个网页缺少了某个标签,整个抓取进程就会直接崩溃退出,或者在数据库中留下大量格式混乱的重复脏数据。

要构建一个健壮且易于排查问题的抓取流程,必须将抓取任务清晰拆解为四个单一职责的阶段:

  • 阶段 1(URL 请求派发):使用 HTTP 客户端模拟真实浏览器发起请求,设置合规的请求头与明确的超时保护,并严格核验响应状态码。
  • 阶段 2(DOM 树结构解析):将获取到的 HTML 字符串加载进解析引擎,通过 CSS 选择器定位目标元素容器,提取原始文本或标签属性。
  • 阶段 3(字段提取与清洗):去除文本噪点、剥离多余符号、完成字符串到数值或日期的类型转换,并对空节点进行缺省值填充。
  • 阶段 4(事务持久化存储):设计结构严谨的数据库表,通过参数化 SQL 执行插入操作,并利用唯一索引保证任务重跑时的幂等性。
用Python实现网页抓取:从URL请求到数据入库的全流程
图 1:从 URL 请求到持久化入库的四个核心执行阶段与产出契约

二、第一步:构造与发送 HTTP 请求

网络请求是抓取的第一道关口。直接调用无参数的 requests.get(url) 很容易被服务器直接识别为自动化脚本并拦截(返回 403 Forbidden),甚至因为目标服务器响应缓慢导致 Python 进程无限期阻塞。

在发起请求时,有三个关键参数是保障稳定性的基础:

  1. User-Agent 伪装:目标站点的 Web 服务器通常会检查 User-Agent 头部,以区分浏览器访问还是脚本请求。
  2. timeout 超时阈值:网络请求必须设定超时时间(例如 timeout=10)。若服务器超过指定秒数未返回数据,即抛出超时异常,交由程序处理,避免线程卡死。
  3. 状态码校验:必须通过 response.status_code 判断是否为 200,并建议通过 response.raise_for_status() 主动暴露客户端或服务端错误。

HTTP 请求模块示例代码

import requests
from typing import Optional

def fetch_page(url: str, timeout: int = 10) -> Optional[str]:
    """
    负责向指定 URL 发送 HTTP GET 请求,并返回页面 HTML 文本。
    包含统一的请求头配置与超时异常捕获。
    """
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
    }
    
    try:
        response = requests.get(url, headers=headers, timeout=timeout)
        # 如果返回 4xx 或 5xx 状态码,直接抛出 HTTPError 异常
        response.raise_for_status()
        
        # 显式使用 UTF-8 或响应探测编码,防止中文网页乱码
        response.encoding = response.apparent_encoding or "utf-8"
        return response.text
    except requests.exceptions.Timeout:
        print(f"[超时告警] 请求 URL 超过 {timeout} 秒无响应: {url}")
    except requests.exceptions.HTTPError as err:
        print(f"[HTTP错误] 状态码异常: {err}")
    except requests.exceptions.RequestException as err:
        print(f"[网络异常] 请求发生致命错误: {err}")
        
    return None
执行输出示例(标准请求)
fetch_page("http://books.toscrape.com/catalogue/page-1.html")
[状态] 发起连接 -> HTTP 200 OK -> 接收到 51,280 字节 HTML 文档文本流。

三、第二步:HTML 解析与目标字段提取

获取到 HTML 字符串之后,抓取工作进入提取阶段。BeautifulSoup 是 Python 处理静态 HTML 最常用的库,它将嵌套复杂的 HTML 文本构建为一棵可检索的节点树。

在提取数据时,推荐使用 CSS 选择器(select 与 select_one),相比繁琐的多层嵌套 find 调用,CSS 选择器语法简洁直观,且与浏览器控制台开发者工具完全兼容。

防御性解析与数据清洗规范

现实中的网页并非所有卡片都拥有完备的标签。例如某些商品可能缺少副标题,或者处于缺货状态缺少价格标签。如果在代码中直接调用 tag.text,一旦某一项为 None,就会引发 AttributeError: 'NoneType' object has no attribute 'text'。

因此,解析函数内部必须执行防御性提取与标准化清洗:

  • 提取前判断节点是否存在,若不存在则赋予安全默认值(如空字符串或 0.0)。
  • 提取纯文本后,立即调用 .strip() 消除首尾空格、制表符及换行符 。
  • 清洗价格或数字中的特殊字符(例如 £51.77 需要剥离货币符号 £,并转为浮点数 float)。
用Python实现网页抓取:从URL请求到数据入库的全流程
图 2:抓取系统分层架构设计与跨层协作契约

HTML 提取与清洗模块代码

from bs4 import BeautifulSoup
import re
from typing import List, Dict, Any

def parse_book_items(html_content: str) -> List[Dict[str, Any]]:
    """
    解析 HTML 页面,提取图书列表中的标题、价格、评分与库存状态。
    返回规范化的字典列表。
    """
    soup = BeautifulSoup(html_content, "html.parser")
    items = []
    
    # 定位每个商品的容器卡片
    product_cards = soup.select("article.product_pod")
    
    for card in product_cards:
        # 1. 提取标题:优先取 a 标签的 title 完整属性,不存在则取其可见文本
        title_tag = card.select_one("h3 > a")
        if not title_tag:
            continue  # 若关键标识缺失,跳过脏数据
        title = title_tag.get("title") or title_tag.get_text(strip=True)
        
        # 2. 提取价格:剥离货币符号,保留数值
        price_tag = card.select_one("p.price_color")
        price = 0.0
        if price_tag:
            raw_price = price_tag.get_text(strip=True)
            # 使用正则提取浮点数字
            price_match = re.search(r"[\d.]+", raw_price)
            if price_match:
                price = float(price_match.group())
                
        # 3. 提取详情相对链接并拼合
        detail_rel_url = title_tag.get("href", "").strip()
        
        # 4. 提取库存状态文本
        avail_tag = card.select_one("p.instock.availability")
        in_stock = True if avail_tag and "In stock" in avail_tag.get_text() else False

        # 组装为强类型结构化字典
        record = {
            "title": title.strip(),
            "price": price,
            "relative_url": detail_rel_url,
            "in_stock": 1 if in_stock else 0
        }
        items.append(record)
        
    return items
解析阶段清洗产出示例(字典列表)
[
  {"title": "A Light in the Attic", "price": 51.77, "relative_url": "a-light-in-the-attic_1000/index.html", "in_stock": 1},
  {"title": "Tipping the Velvet", "price": 53.74, "relative_url": "tipping-the-velvet_999/index.html", "in_stock": 1}
]

四、第三步:数据建模与 SQLite 入库存储

数据抓取的终点是持久化。许多人习惯直接将结果导出为 CSV 或 JSON 文件。但面对多轮抓取、增量更新和并发入库时,文本文件极易发生写入冲突,且无法防止重复录入。

Python 标配内置的 sqlite3 模块是爬虫轻量级存储的黄金搭档:无需配置数据库服务端,数据直接保存在单个 .db 磁盘文件中,具备完整的 ACID 事务支持。

数据库表设计与防重策略

为了保证抓取任务可以反复安全重跑(幂等性),我们必须在建表时为关键唯一标识设置 UNIQUE 约束。例如:通过 relative_url(页面唯一相对路径)作为业务唯一标识。

在执行 SQL 写入时,采用 INSERT OR IGNORE 语法:如果该条记录已存在于数据库中,SQLite 会自动忽略该条插入,而不会引发程序报错崩溃;若需要覆盖更新,则可使用 INSERT OR REPLACE。

用Python实现网页抓取:从URL请求到数据入库的全流程
图 3:网络请求状态决策、异常退避机制与数据入库流水线控制

SQLite 数据库管理模块代码

import sqlite3
from typing import List, Dict, Any

class StoragePipeline:
    def __init__(self, db_path: str = "books_data.db"):
        self.db_path = db_path
        self._init_table()

    def _get_connection(self):
        """获取 SQLite 连接,配置超时以防多线程锁库"""
        return sqlite3.connect(self.db_path, timeout=15)

    def _init_table(self):
        """初始化数据表与唯一索引"""
        create_sql = """
        CREATE TABLE IF NOT EXISTS books (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            price REAL NOT NULL,
            relative_url TEXT NOT NULL UNIQUE,
            in_stock INTEGER NOT NULL DEFAULT 1,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        """
        with self._get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute(create_sql)
            conn.commit()

    def save_records(self, records: List[Dict[str, Any]]) -> int:
        """
        批量持久化数据,使用参数化占位符防止注入,
        使用 INSERT OR IGNORE 确保数据幂等防重。
        返回实际成功入库的记录条数。
        """
        if not records:
            return 0

        insert_sql = """
        INSERT OR IGNORE INTO books (title, price, relative_url, in_stock)
        VALUES (?, ?, ?, ?);
        """
        
        # 将字典转换为参数元组序列
        param_tuples = [
            (r["title"], r["price"], r["relative_url"], r["in_stock"])
            for r in records
        ]

        with self._get_connection() as conn:
            cursor = conn.cursor()
            cursor.executemany(insert_sql, param_tuples)
            inserted_count = cursor.rowcount
            conn.commit()

        return inserted_count
核心安全提醒:严禁直接拼接 SQL 字符串
必须使用问号 ? 参数化占位符,不可使用 Python 的 f"INSERT VALUES ('{title}')"。直接拼接不仅会导致标题中含有单双引号(如英文缩写 It's)时出现 SQL 语法错误,更会带来注入风险。

五、第四步:工程化串联与全流程完整代码

掌握了请求、解析与写库三个基础模块后,我们需要通过一个调度器将它们组织起来。为了避免对目标站点造成过大压力,必须在循环间引入礼貌延迟(time.sleep),并加入进度日志打印。

以下是整套抓取工程的完整可执行代码。你可以在自己的 Python 环境中直接运行它:

# -*- coding: utf-8 -*-
"""
用Python实现网页抓取:从URL请求到数据入库的全流程完整实现
依赖库:requests, beautifulsoup4 (pip install requests beautifulsoup4)
"""
import time
import requests
import sqlite3
import re
from typing import List, Dict, Any, Optional
from bs4 import BeautifulSoup

# ======================= 1. 网络请求层 =======================
class WebFetcher:
    def __init__(self, timeout: int = 10):
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
        })

    def get(self, url: str) -> Optional[str]:
        try:
            resp = self.session.get(url, timeout=self.timeout)
            resp.raise_for_status()
            resp.encoding = resp.apparent_encoding or "utf-8"
            return resp.text
        except requests.RequestException as e:
            print(f"[请求失败] URL: {url} 异常信息: {e}")
            return None

# ======================= 2. 数据解析层 =======================
class ContentParser:
    @staticmethod
    def parse_books(html_text: str) -> List[Dict[str, Any]]:
        soup = BeautifulSoup(html_text, "html.parser")
        results = []
        for card in soup.select("article.product_pod"):
            title_el = card.select_one("h3 > a")
            if not title_el:
                continue
            title = title_el.get("title") or title_el.get_text(strip=True)
            
            price_el = card.select_one("p.price_color")
            price = 0.0
            if price_el:
                m = re.search(r"[\d.]+", price_el.get_text())
                if m:
                    price = float(m.group())

            rel_link = title_el.get("href", "").strip()
            avail_el = card.select_one("p.instock.availability")
            in_stock = 1 if (avail_el and "In stock" in avail_el.get_text()) else 0

            results.append({
                "title": title.strip(),
                "price": price,
                "relative_url": rel_link,
                "in_stock": in_stock
            })
        return results

# ======================= 3. 数据存储层 =======================
class SQLitePipeline:
    def __init__(self, db_file: str = "scraped_data.db"):
        self.db_file = db_file
        self._init_db()

    def _init_db(self):
        with sqlite3.connect(self.db_file) as conn:
            conn.execute("""
            CREATE TABLE IF NOT EXISTS scraped_books (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                price REAL NOT NULL,
                relative_url TEXT NOT NULL UNIQUE,
                in_stock INTEGER NOT NULL,
                scraped_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
            """)
            conn.commit()

    def insert_batch(self, items: List[Dict[str, Any]]) -> int:
        if not items:
            return 0
        sql = """
        INSERT OR IGNORE INTO scraped_books (title, price, relative_url, in_stock)
        VALUES (?, ?, ?, ?);
        """
        params = [(i["title"], i["price"], i["relative_url"], i["in_stock"]) for i in items]
        with sqlite3.connect(self.db_file) as conn:
            cursor = conn.cursor()
            cursor.executemany(sql, params)
            affected = cursor.rowcount
            conn.commit()
        return affected

# ======================= 4. 流程调度中枢 =======================
def run_crawler():
    base_url = "http://books.toscrape.com/catalogue/page-{}.html"
    total_pages = 2  # 示例演示抓取前 2 页
    
    fetcher = WebFetcher(timeout=10)
    pipeline = SQLitePipeline(db_file="books_warehouse.db")

    print("[任务启动] 开始执行抓取入库流水线...")
    total_saved = 0

    for page in range(1, total_pages + 1):
        target_url = base_url.format(page)
        print(f"
-> 正在抓取第 {page}/{total_pages} 页: {target_url}")

        # 1. 抓取
        html = fetcher.get(target_url)
        if not html:
            print(f"[跳过] 第 {page} 页拉取失败,继续下一页")
            continue

        # 2. 解析
        items = ContentParser.parse_books(html)
        print(f"   提取到有效卡片数: {len(items)} 条")

        # 3. 入库
        saved = pipeline.insert_batch(items)
        total_saved += saved
        print(f"   成功写入数据库记录: {saved} 条 (去重跳过: {len(items) - saved} 条)")

        # 4. 礼貌延迟,保护目标站点
        time.sleep(1.0)

    print(f"
[任务完成] 全流程执行完毕,累计新增入库: {total_saved} 条记录。")

if __name__ == "__main__":
    run_crawler()
执行输出示例(实际运行终端日志)
[任务启动] 开始执行抓取入库流水线...
-> 正在抓取第 1/2 页: http://books.toscrape.com/catalogue/page-1.html
   提取到有效卡片数: 20 条
   成功写入数据库记录: 20 条 (去重跳过: 0 条)
-> 正在抓取第 2/2 页: http://books.toscrape.com/catalogue/page-2.html
   提取到有效卡片数: 20 条
   成功写入数据库记录: 20 条 (去重跳过: 0 条)
[任务完成] 全流程执行完毕,累计新增入库: 40 条记录。

六、结果验证与数据校验

任务执行完成后,必须对生成的文件与入库记录进行校验,确保数据既没有丢失,也没有发生字段串位或乱码。

可以通过 Python 的内置交互终端或一段简单的验证脚本快速查看刚刚生成的数据库:

import sqlite3

conn = sqlite3.connect("books_warehouse.db")
cursor = conn.cursor()

# 查询总记录数
cursor.execute("SELECT COUNT(*) FROM scraped_books;")
total_rows = cursor.fetchone()[0]
print(f"当前数据库累计总行数: {total_rows}")

# 抽查前 3 条具体数据
cursor.execute("SELECT id, title, price, in_stock, scraped_at FROM scraped_books LIMIT 3;")
rows = cursor.fetchall()
for row in rows:
    print(f"ID: {row[0]} | 价格: ¥{row[2]} | 有货: {bool(row[3])} | 书名: {row[1]}")

conn.close()
数据库抽样校验输出示例
当前数据库累计总行数: 40
ID: 1 | 价格: ¥51.77 | 有货: True | 书名: A Light in the Attic
ID: 2 | 价格: ¥53.74 | 有货: True | 书名: Tipping the Velvet
ID: 3 | 价格: ¥50.10 | 有货: True | 书名: Soumission
用Python实现网页抓取:从URL请求到数据入库的全流程
图 4:全流程数据形态演进图(从网络目标到关系型数据行)

七、常见排查与工程防坑指南

在将上述方案应用到不同类型的网站时,通常会遇到以下三个典型技术边界问题:

问题现象 根本原因 规范解决方案
抓取返回 200 但没有目标数据 网页内容是通过前端 JavaScript 异步请求(AJAX/Fetch)动态渲染出来的,静态 HTML 中只包含空壳标签。 在浏览器开发者工具 Network 栏查找真实的 JSON 数据接口直接请求;或者改用浏览器自动化工具(如 Playwright / Selenium)进行无头渲染。
程序运行中偶发 429 报错 抓取请求频率过高,触发了目标站点的限流防火墙(Rate Limit)。 调大 time.sleep 间隔;在请求头中引入随机延迟;利用指数退避算法进行重试。
中文内容显示为乱码字符 服务端未显式声明 charset,或者使用了 GB2312/GBK 编码,requests 默认按 ISO-8859-1 误解码。 在解析前通过 response.apparent_encoding 自动探测编码,或手动指定 response.encoding = 'utf-8'。
重复运行后数据库记录翻倍 数据表缺少唯一业务主键,直接使用常规 INSERT 盲目追加。 为商品链接或唯一业务编号建立 UNIQUE 索引,统一使用 INSERT OR IGNORE 或 INSERT OR REPLACE 保持幂等。
合规与礼貌爬取准则
所有自动化抓取程序均应自觉遵守站点的 robots.txt 协议,合理控制并发与请求间隔,绝不对目标服务器造成性能负担;抓取的数据应仅用于学习研究或合法数据分析场景。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。