Python 网络爬虫完全指南:从基础到企业级应用



Python 网络爬虫完全指南:从基础到企业级应用

  • 摘要
  • 目录
  • 一、网络爬虫核心原理与技术栈
    • [1.1 什么是网络爬虫](#1.1 什么是网络爬虫)
    • [1.2 Python 爬虫技术生态全景](#1.2 Python 爬虫技术生态全景)
    • [1.3 企业级架构设计原则](#1.3 企业级架构设计原则)
    • [1.4 合规性与法律边界](#1.4 合规性与法律边界)
  • 二、开发环境搭建
    • [2.1 Python 环境配置](#2.1 Python 环境配置)
    • [2.2 虚拟环境管理](#2.2 虚拟环境管理)
    • [2.3 核心库安装](#2.3 核心库安装)
    • [2.4 项目结构设计](#2.4 项目结构设计)
  • 三、基础爬虫实现:请求与解析
    • [3.1 HTTP 协议基础](#3.1 HTTP 协议基础)
    • [3.2 使用 Requests 发送请求](#3.2 使用 Requests 发送请求)
    • [3.3 使用 BeautifulSoup 解析 HTML](#3.3 使用 BeautifulSoup 解析 HTML)
    • [3.4 使用 lxml 与 XPath 解析](#3.4 使用 lxml 与 XPath 解析)
    • [3.5 数据提取实战](#3.5 数据提取实战)
  • 四、数据存储与格式转换
    • [4.1 CSV 格式存储](#4.1 CSV 格式存储)
    • [4.2 JSON 格式存储](#4.2 JSON 格式存储)
    • [4.3 Excel 格式存储](#4.3 Excel 格式存储)
    • [4.4 数据库存储(SQLite/MySQL)](#4.4 数据库存储(SQLite/MySQL))
    • [4.5 使用 Pandas 处理数据](#4.5 使用 Pandas 处理数据)
  • 五、高级爬虫功能与策略
    • [5.1 分页处理](#5.1 分页处理)
    • [5.2 增量爬取与去重机制](#5.2 增量爬取与去重机制)
    • [5.3 请求伪装与身份管理](#5.3 请求伪装与身份管理)
    • [5.4 robots.txt 合规检查自动化](#5.4 robots.txt 合规检查自动化)
    • [5.5 Session 与会话管理](#5.5 Session 与会话管理)
  • 六、反爬机制应对与合规策略
    • [6.1 常见反爬机制概述](#6.1 常见反爬机制概述)
    • [6.2 频率控制与并发调度](#6.2 频率控制与并发调度)
      • [6.2.1 同步爬虫频率控制](#6.2.1 同步爬虫频率控制)
      • [6.2.2 令牌桶限流器](#6.2.2 令牌桶限流器)
    • [6.3 User-Agent 轮换](#6.3 User-Agent 轮换)
    • [6.4 代理 IP 池管理](#6.4 代理 IP 池管理)
    • [6.5 验证码处理技术](#6.5 验证码处理技术)
    • [6.6 动态内容抓取方案](#6.6 动态内容抓取方案)
      • [6.6.1 分析 API 接口](#6.6.1 分析 API 接口)
      • [6.6.2 使用 Playwright 渲染](#6.6.2 使用 Playwright 渲染)
  • 七、异步爬虫与高性能采集
    • [7.1 同步 vs 异步爬虫](#7.1 同步 vs 异步爬虫)
    • [7.2 aiohttp 基础用法](#7.2 aiohttp 基础用法)
    • [7.3 并发控制与限流策略](#7.3 并发控制与限流策略)
    • [7.4 异步爬虫实战](#7.4 异步爬虫实战)
  • 八、自动化调度与分布式架构
    • [8.1 定时任务配置](#8.1 定时任务配置)
      • [8.1.1 使用 APScheduler](#8.1.1 使用 APScheduler)
      • [8.1.2 与 Shell 协同调度](#8.1.2 与 Shell 协同调度)
    • [8.2 Scrapy 框架入门](#8.2 Scrapy 框架入门)
      • [8.2.1 安装与创建项目](#8.2.1 安装与创建项目)
      • [8.2.2 编写 Spider](#8.2.2 编写 Spider)
      • [8.2.3 配置 Pipeline](#8.2.3 配置 Pipeline)
      • [8.2.4 运行爬虫](#8.2.4 运行爬虫)
    • [8.3 Scrapy-Redis 分布式爬虫](#8.3 Scrapy-Redis 分布式爬虫)
    • [8.4 错误处理与重试机制](#8.4 错误处理与重试机制)
    • [8.5 日志与监控](#8.5 日志与监控)
  • 九、实战案例:电商价格监控系统
    • [9.1 需求分析与系统设计](#9.1 需求分析与系统设计)
    • [9.2 核心代码实现](#9.2 核心代码实现)
    • [9.3 监控报警与运维优化](#9.3 监控报警与运维优化)
      • [9.3.1 Systemd Timer 配置](#9.3.1 Systemd Timer 配置)
      • [9.3.2 数据清理](#9.3.2 数据清理)
  • 十、总结
    • [10.1 核心技术要点](#10.1 核心技术要点)
    • [10.2 合规红线](#10.2 合规红线)
    • [10.3 学习路径建议](#10.3 学习路径建议)
  • 附录
    • [A. 常用 Python 爬虫库速查表](#A. 常用 Python 爬虫库速查表)
    • [B. HTTP 状态码与处理策略](#B. HTTP 状态码与处理策略)
    • [C. 合规爬虫 Checklist(开发前必读)](#C. 合规爬虫 Checklist(开发前必读))
    • [D. 学习资源推荐](#D. 学习资源推荐)

一套完整、合规、高可用的现代数据采集解决方案


摘要

本文提供了一套完整、合规、高效的现代网络爬虫技术体系。针对企业级数据采集需求,文章以 Python 为核心开发语言,结合 Shell 进行系统级调度,全面覆盖了从基础 HTTP 请求、DOM 解析到动态内容渲染、分布式调度的全链路技术。

Python 凭借其庞大的第三方库生态,已成为爬虫开发的首选语言。文章系统性地讲解了爬虫开发环境配置、Python 基础语法、爬虫原理、爬虫合法性与道德规范、爬虫基本库使用、静态与动态网页爬取、API 数据调用等核心内容。

文章特别强调合规性 ,深入探讨了如何遵守法律法规、尊重 robots.txt 协议以及保护用户隐私。通过丰富的 Python 代码示例和架构设计最佳实践,帮助开发者构建稳定、高并发、可维护的爬虫系统,适用于市场调研、竞品分析、公开数据采集等企业级应用场景。全文共分九大章节,包含 100+ 技术要点和 50+ 可运行代码示例,是一份面向专业开发者的权威技术指南。

目录

一、网络爬虫核心原理与技术栈

  • 1.1 什么是网络爬虫
  • 1.2 Python 爬虫技术生态全景
  • 1.3 企业级架构设计原则
  • 1.4 合规性与法律边界

二、开发环境搭建

  • 2.1 Python 环境配置
  • 2.2 虚拟环境管理
  • 2.3 核心库安装
  • 2.4 项目结构设计

三、基础爬虫实现:请求与解析

  • 3.1 HTTP 协议基础
  • 3.2 使用 Requests 发送请求
  • 3.3 使用 BeautifulSoup 解析 HTML
  • 3.4 使用 lxml 与 XPath 解析
  • 3.5 数据提取实战

四、数据存储与格式转换

  • 4.1 CSV 格式存储
  • 4.2 JSON 格式存储
  • 4.3 Excel 格式存储
  • 4.4 数据库存储(SQLite/MySQL)
  • 4.5 使用 Pandas 处理数据

五、高级爬虫功能与策略

  • 5.1 分页处理
  • 5.2 增量爬取与去重机制
  • 5.3 请求伪装与身份管理
  • 5.4 robots.txt 合规检查自动化
  • 5.5 Session 与会话管理

六、反爬机制应对与合规策略

  • 6.1 常见反爬机制概述
  • 6.2 频率控制与并发调度
  • 6.3 User-Agent 轮换
  • 6.4 代理 IP 池管理
  • 6.5 验证码处理技术
  • 6.6 动态内容抓取方案

七、异步爬虫与高性能采集

  • 7.1 同步 vs 异步爬虫
  • 7.2 aiohttp 基础用法
  • 7.3 并发控制与限流策略
  • 7.4 异步爬虫实战

八、自动化调度与分布式架构

  • 8.1 定时任务配置
  • 8.2 Scrapy 框架入门
  • 8.3 Scrapy-Redis 分布式爬虫
  • 8.4 错误处理与重试机制
  • 8.5 日志与监控

九、实战案例:电商价格监控系统

  • 9.1 需求分析与系统设计
  • 9.2 核心代码实现
  • 9.3 监控报警与运维优化

十、总结

附录

  • A. 常用 Python 爬虫库速查表
  • B. HTTP 状态码与处理策略
  • C. 合规爬虫 Checklist
  • D. 学习资源推荐

一、网络爬虫核心原理与技术栈

1.1 什么是网络爬虫

网络爬虫(Web Crawler/Spider)是一种按照一定规则自动抓取互联网信息的程序。它模拟浏览器向服务器发送 HTTP 请求,获取网页内容后从中提取所需数据。

爬虫的工作流程

复制代码
目标 URL → 发送 HTTP 请求 → 获取响应内容 → 解析提取数据 → 存储数据 → 继续下一个 URL

爬虫的典型应用场景

  • 搜索引擎索引(Google、百度)
  • 市场调研与竞品分析
  • 价格监控与价格比较
  • 舆情监测与新闻聚合
  • 学术研究与数据分析
  • 人才招聘信息采集

1.2 Python 爬虫技术生态全景

Python 凭借其庞大的第三方库生态,已成为爬虫开发的首选语言。下表列出了各层级的推荐工具及其核心优势:

层级 组件功能 推荐 Python 库 核心优势
网络层 HTTP/HTTPS 请求 requests, httpx, aiohttp 支持异步、连接池、自动重试、HTTP/2
解析层 HTML/XML/JSON 解析 BeautifulSoup4, lxml, parsel XPath/CSS 选择器支持,解析速度极快
渲染层 动态 JS 内容抓取 Playwright, Selenium 无头浏览器控制,完美模拟真实用户
数据层 数据清洗与存储 pandas, SQLAlchemy, pymongo 强大的 DataFrame 操作,ORM 支持
调度层 任务管理与并发 Celery, Scrapy-Redis, APScheduler 分布式任务队列,灵活调度
框架层 全栈爬虫框架 Scrapy 完整的异步处理、中间件和管道机制

Python 爬虫技术体系已形成从基础库到全栈框架的完整生态。requests + BeautifulSoup 组合足以应对大部分静态网站;Scrapy 框架提供了完整的异步处理、中间件和管道机制;Playwright 则彻底解决了动态渲染的难题。

1.3 企业级架构设计原则

企业级爬虫系统需要遵循以下架构原则:

1. 高内聚低耦合

将下载器(Downloader)、解析器(Parser)、管道(Pipeline)严格分离。每个模块可独立演进,便于单元测试和替换。

2. 容错与幂等性

网络请求必然会遇到超时或失败,系统必须具备自动重试能力,且重复执行不会产生脏数据。利用数据库唯一约束或分布式锁保证幂等。

3. 资源隔离

使用虚拟环境(venv/conda)和容器化(Docker)部署,避免依赖冲突。生产环境建议使用 Kubernetes 管理爬虫 Pod。

4. 可观测性

结构化日志(JSON 格式)、指标采集(请求成功率、响应时间、抓取数量)和分布式追踪,确保问题可快速定位。

5. 配置外部化

所有可变参数(URL、延迟、重试次数、数据库连接串)通过环境变量或配置中心管理,避免硬编码。

1.4 合规性与法律边界

合规是爬虫的生命线。在编写任何爬虫代码前,必须明确以下红线:

  • 不爬取个人隐私数据:严格遵守《个人信息保护法》,不采集未授权的身份证号、手机号、私人通讯录等。
  • 不干扰目标系统正常运行:控制并发量,避免演变成 DDoS 攻击。
  • 不突破技术防护措施:严禁使用恶意手段破解验证码、绕过付费墙或入侵内网。
  • 遵守 robots.txt:尊重网站所有者的爬虫协议。
  • 明确数据使用目的:采集的数据仅用于合法合规的内部研究或公开数据分析。

根据《中华人民共和国网络安全法》第四十四条,任何个人和组织不得非法获取他人信息系统数据。这一条款明确划定了爬虫技术的使用红线。

二、开发环境搭建

2.1 Python 环境配置

推荐使用 Python 3.8 及以上版本。检查 Python 版本:

bash 复制代码
python --version
# 或
python3 --version

如果未安装 Python,请从 python.org 下载安装包进行安装。安装时务必勾选"Add Python to PATH"选项。

2.2 虚拟环境管理

虚拟环境可以隔离不同项目的依赖,避免版本冲突。

使用 venv(Python 内置)

bash 复制代码
# 创建虚拟环境
python -m venv crawler_env

# 激活虚拟环境(Windows)
crawler_env\Scripts\activate

# 激活虚拟环境(macOS/Linux)
source crawler_env/bin/activate

# 退出虚拟环境
deactivate

使用 conda(推荐数据分析场景)

bash 复制代码
# 创建虚拟环境
conda create -n crawler_env python=3.10

# 激活虚拟环境
conda activate crawler_env

# 退出虚拟环境
conda deactivate

2.3 核心库安装

在激活的虚拟环境中安装以下核心库:

bash 复制代码
# 基础请求库
pip install requests

# HTML 解析库
pip install beautifulsoup4 lxml

# 异步请求库
pip install aiohttp

# 数据处理库
pip install pandas openpyxl

# 数据库 ORM
pip install sqlalchemy

# 动态渲染库(按需安装)
pip install playwright
playwright install

# 爬虫框架
pip install scrapy

# 重试与工具库
pip install tenacity fake-useragent

# 定时任务
pip install apscheduler

验证安装

python 复制代码
import requests
import bs4
import lxml
import aiohttp
import pandas
import scrapy
print("所有核心库安装成功!")

2.4 项目结构设计

一个规范的爬虫项目应采用以下目录结构:

复制代码
my_crawler/
├── README.md                 # 项目说明文档
├── requirements.txt          # 依赖清单
├── .env                      # 环境变量(不提交到 Git)
├── .gitignore               # Git 忽略文件
├── config/
│   ├── __init__.py
│   └── settings.py          # 全局配置
├── crawler/
│   ├── __init__.py
│   ├── downloader.py        # 下载器
│   ├── parser.py            # 解析器
│   ├── pipeline.py          # 数据管道
│   └── spider.py            # 爬虫主逻辑
├── data/
│   ├── raw/                 # 原始数据
│   └── processed/           # 处理后数据
├── logs/                    # 日志文件
├── tests/                   # 单元测试
│   ├── __init__.py
│   └── test_parser.py
└── utils/
    ├── __init__.py
    ├── database.py          # 数据库工具
    └── logger.py            # 日志工具

三、基础爬虫实现:请求与解析

3.1 HTTP 协议基础

HTTP(HyperText Transfer Protocol)是爬虫与网站交互的基础协议。理解以下核心概念至关重要:

HTTP 请求方法

方法 用途 说明
GET 获取资源 最常用的请求方式,参数在 URL 中
POST 提交数据 用于登录、表单提交等
PUT 更新资源 完整更新
DELETE 删除资源 删除指定资源

HTTP 状态码

状态码 含义 处理策略
200 成功 正常解析数据
301/302 重定向 自动跟随(requests 默认)
403 禁止访问 检查 User-Agent/Cookie
404 未找到 记录并跳过
429 请求过多 降低频率,增加延迟
500/502 服务器错误 重试或跳过

HTTP 请求头

python 复制代码
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
    'Referer': 'https://www.google.com/',
}

3.2 使用 Requests 发送请求

requests 是 Python 最流行的 HTTP 客户端库。

python 复制代码
import requests
from requests.exceptions import RequestException, Timeout, ConnectionError
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def fetch_page(url, max_retries=3, timeout=(5, 10)):
    """
    发送 GET 请求,带重试机制
    :param url: 目标 URL
    :param max_retries: 最大重试次数
    :param timeout: (连接超时, 读取超时)
    :return: 响应文本或 None
    """
    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-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        'Accept-Encoding': 'gzip, deflate',
        'Connection': 'keep-alive'
    }

    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, timeout=timeout)
            response.raise_for_status()
            response.encoding = response.apparent_encoding or 'utf-8'
            logger.info(f"成功抓取 {url},状态码 {response.status_code}")
            return response.text
        except Timeout:
            logger.warning(f"第 {attempt+1} 次请求超时: {url}")
        except ConnectionError:
            logger.warning(f"第 {attempt+1} 次连接错误: {url}")
        except RequestException as e:
            logger.warning(f"第 {attempt+1} 次请求失败: {e}")
        if attempt < max_retries - 1:
            import time
            time.sleep(2 ** attempt)  # 指数退避
    logger.error(f"所有重试失败: {url}")
    return None

POST 请求示例

python 复制代码
def login_and_fetch(login_url, data, target_url):
    """登录后获取数据"""
    session = requests.Session()

    # 登录
    login_response = session.post(login_url, data=data)
    if login_response.status_code != 200:
        logger.error("登录失败")
        return None

    # 获取目标页面
    response = session.get(target_url)
    return response.text

3.3 使用 BeautifulSoup 解析 HTML

BeautifulSoup 提供了友好的 API,适合快速开发。

python 复制代码
from bs4 import BeautifulSoup

def parse_with_beautifulsoup(html_content):
    """使用 BeautifulSoup + CSS 选择器解析"""
    soup = BeautifulSoup(html_content, 'lxml')

    # 提取所有商品
    products = []
    for item in soup.select('div.product-item'):
        title_tag = item.select_one('h2.title')
        price_tag = item.select_one('span.price')
        rating_tag = item.select_one('div.rating')
        url_tag = item.select_one('a')

        products.append({
            'title': title_tag.text.strip() if title_tag else '',
            'price': price_tag.text.replace('¥', '').strip() if price_tag else '0',
            'rating': rating_tag.text.strip() if rating_tag else '0',
            'url': url_tag.get('href') if url_tag else ''
        })

    return products

BeautifulSoup 常用方法

python 复制代码
# 按标签名查找
soup.find('h1')
soup.find_all('a')

# 按 CSS 类查找
soup.find_all('div', class_='product')

# 按属性查找
soup.find_all('a', href=True)

# CSS 选择器
soup.select('div.product-item')
soup.select('.title')
soup.select('#main-content')

# 提取文本和属性
element.text
element.get('href')
element['class']

3.4 使用 lxml 与 XPath 解析

lxml 是性能最高的 Python HTML/XML 解析库。

python 复制代码
from lxml import etree

def parse_with_xpath(html_content):
    """使用 lxml + XPath 解析"""
    tree = etree.HTML(html_content)

    products = []
    for product in tree.xpath('//div[@class="product-item"]'):
        title = product.xpath('.//h2[@class="title"]/text()')
        price = product.xpath('.//span[@class="price"]/text()')
        rating = product.xpath('.//div[@class="rating"]/text()')
        url = product.xpath('.//a/@href')

        products.append({
            'title': title[0].strip() if title else '',
            'price': price[0].replace('¥', '').strip() if price else '0',
            'rating': rating[0].strip() if rating else '0',
            'url': url[0] if url else ''
        })

    return products

XPath 常用语法

表达式 含义
// 从任意位置选取节点
/ 从根节点选取
@ 选取属性
text() 选取文本内容
//div[@class="product"] 选取 class 为 product 的 div
//a/@href 选取所有 a 标签的 href 属性
//h2/text() 选取所有 h2 的文本内容

3.5 数据提取实战

python 复制代码
#!/usr/bin/env python3
"""
完整的数据提取示例
"""

import requests
from bs4 import BeautifulSoup
import json

def scrape_product_page(url):
    """抓取商品页面数据"""
    # 1. 发送请求
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
    response = requests.get(url, headers=headers, timeout=10)

    if response.status_code != 200:
        print(f"请求失败: {response.status_code}")
        return None

    # 2. 解析 HTML
    soup = BeautifulSoup(response.text, 'lxml')

    # 3. 提取数据
    data = {
        'title': soup.select_one('h1.product-title').text.strip() if soup.select_one('h1.product-title') else '',
        'price': soup.select_one('span.price').text.strip() if soup.select_one('span.price') else '',
        'description': soup.select_one('div.description').text.strip() if soup.select_one('div.description') else '',
        'images': [img.get('src') for img in soup.select('div.gallery img')],
        'specs': {}
    }

    # 提取规格参数
    for row in soup.select('table.specs tr'):
        cells = row.select('td')
        if len(cells) >= 2:
            data['specs'][cells[0].text.strip()] = cells[1].text.strip()

    return data

# 使用示例
if __name__ == "__main__":
    result = scrape_product_page("https://example.com/product/12345")
    if result:
        print(json.dumps(result, ensure_ascii=False, indent=2))

四、数据存储与格式转换

4.1 CSV 格式存储

python 复制代码
import csv

def save_to_csv(data, filename="output.csv", encoding='utf-8-sig'):
    """
    保存数据为 CSV 文件
    encoding='utf-8-sig' 可防止 Excel 打开时乱码
    """
    if not data:
        print("没有数据可保存")
        return

    with open(filename, 'w', newline='', encoding=encoding) as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)

    print(f"成功保存 {len(data)} 条记录到 {filename}")

4.2 JSON 格式存储

python 复制代码
import json

def save_to_json(data, filename="output.json"):
    """保存数据为 JSON 文件"""
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    print(f"成功保存 {len(data)} 条记录到 {filename}")

4.3 Excel 格式存储

python 复制代码
import pandas as pd

def save_to_excel(data, filename="output.xlsx"):
    """保存数据为 Excel 文件"""
    if not data:
        print("没有数据可保存")
        return

    df = pd.DataFrame(data)
    df.to_excel(filename, index=False)
    print(f"成功保存 {len(data)} 条记录到 {filename}")

4.4 数据库存储(SQLite/MySQL)

python 复制代码
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime

Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(255))
    price = Column(Float)
    url = Column(String(500))
    created_at = Column(DateTime, default=datetime.now)

def save_to_sqlite(data, db_path="crawler.db"):
    """保存数据到 SQLite 数据库"""
    engine = create_engine(f'sqlite:///{db_path}')
    Base.metadata.create_all(engine)

    Session = sessionmaker(bind=engine)
    session = Session()

    for item in data:
        product = Product(
            title=item.get('title', ''),
            price=float(item.get('price', 0)),
            url=item.get('url', '')
        )
        session.add(product)

    session.commit()
    session.close()
    print(f"成功保存 {len(data)} 条记录到数据库")

4.5 使用 Pandas 处理数据

pandas 是数据处理的首选工具。

python 复制代码
import pandas as pd

def process_with_pandas(data):
    """使用 Pandas 处理数据"""
    df = pd.DataFrame(data)

    # 查看数据概览
    print(df.head())
    print(df.info())
    print(df.describe())

    # 数据清洗
    # 1. 删除价格为空的行
    df = df.dropna(subset=['price'])

    # 2. 转换价格类型
    df['price'] = pd.to_numeric(df['price'], errors='coerce')

    # 3. 过滤异常值
    df = df[(df['price'] > 0) & (df['price'] < 100000)]

    # 4. 去重
    df = df.drop_duplicates(subset=['url'])

    # 5. 排序
    df = df.sort_values('price', ascending=False)

    return df

五、高级爬虫功能与策略

5.1 分页处理

python 复制代码
def crawl_paginated(base_url, total_pages=10):
    """分页抓取"""
    all_data = []

    for page in range(1, total_pages + 1):
        url = f"{base_url}?page={page}"
        print(f"抓取第 {page} 页: {url}")

        html = fetch_page(url)
        if not html:
            break

        page_data = parse_with_beautifulsoup(html)
        if not page_data:
            break

        all_data.extend(page_data)
        print(f"第 {page} 页抓取 {len(page_data)} 条数据")

        import time
        time.sleep(1)  # 礼貌延迟

    return all_data

5.2 增量爬取与去重机制

python 复制代码
import hashlib
import sqlite3
from datetime import datetime

class IncrementalCrawler:
    """增量爬虫 - 基于 URL 哈希去重"""

    def __init__(self, db_path="crawler_state.db"):
        self.conn = sqlite3.connect(db_path)
        self.cursor = self.conn.cursor()
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS visited_urls (
                url_hash TEXT PRIMARY KEY,
                url TEXT,
                timestamp DATETIME
            )
        ''')
        self.conn.commit()

    def _hash_url(self, url):
        return hashlib.md5(url.encode('utf-8')).hexdigest()

    def is_visited(self, url):
        url_hash = self._hash_url(url)
        self.cursor.execute("SELECT 1 FROM visited_urls WHERE url_hash = ?", (url_hash,))
        return self.cursor.fetchone() is not None

    def mark_visited(self, url):
        url_hash = self._hash_url(url)
        self.cursor.execute(
            "INSERT OR IGNORE INTO visited_urls (url_hash, url, timestamp) VALUES (?, ?, ?)",
            (url_hash, url, datetime.now())
        )
        self.conn.commit()

    def get_unvisited_urls(self, urls):
        return [url for url in urls if not self.is_visited(url)]

    def close(self):
        self.conn.close()

5.3 请求伪装与身份管理

python 复制代码
import random
from fake_useragent import UserAgent  # pip install fake-useragent

class RequestManager:
    """请求管理器 - 伪装与身份管理"""

    def __init__(self):
        self.session = requests.Session()
        self.ua = UserAgent()
        self._setup_session()

    def _setup_session(self):
        # 设置默认请求头
        self.session.headers.update({
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
        })

        # 设置重试策略
        from requests.adapters import HTTPAdapter
        from urllib3.util.retry import Retry

        retry_strategy = Retry(
            total=3,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504],
        )
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount('http://', adapter)
        self.session.mount('https://', adapter)

    def get_headers(self, custom_headers=None):
        """生成随机请求头"""
        headers = {
            'User-Agent': self.ua.random,
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        }
        if custom_headers:
            headers.update(custom_headers)
        return headers

    def get(self, url, **kwargs):
        headers = self.get_headers(kwargs.pop('headers', None))
        return self.session.get(url, headers=headers, **kwargs)

    def post(self, url, data=None, json=None, **kwargs):
        headers = self.get_headers(kwargs.pop('headers', None))
        return self.session.post(url, data=data, json=json, headers=headers, **kwargs)

5.4 robots.txt 合规检查自动化

使用 Python 标准库 urllib.robotparser 自动检查目标网站是否允许爬取:

python 复制代码
from urllib.robotparser import RobotFileParser
from urllib.parse import urlparse

class RobotsChecker:
    """robots.txt 合规检查器"""

    def __init__(self, domain, user_agent="MyBot"):
        self.domain = domain.rstrip('/')
        self.user_agent = user_agent
        self.parser = None
        self._fetch_robots()

    def _fetch_robots(self):
        """获取并解析 robots.txt"""
        robots_url = f"{self.domain}/robots.txt"
        self.parser = RobotFileParser()
        self.parser.set_url(robots_url)

        try:
            self.parser.read()
            print(f"成功读取 robots.txt: {robots_url}")
        except Exception as e:
            print(f"警告: 无法读取 robots.txt: {e}")

    def can_fetch(self, url):
        """检查是否允许爬取指定 URL"""
        if self.parser is None:
            return True  # 无法读取时默认允许(需谨慎)

        return self.parser.can_fetch(self.user_agent, url)

    def get_crawl_delay(self):
        """获取建议的爬取延迟"""
        if self.parser is None:
            return None

        # 尝试获取 Crawl-delay
        try:
            # RobotFileParser 没有直接获取 Crawl-delay 的方法
            # 可以通过解析原始内容获取
            return None
        except:
            return None

# 使用示例
def check_before_crawl(url):
    checker = RobotsChecker("https://example.com")
    if checker.can_fetch(url):
        print(f"✅ 允许爬取: {url}")
        return True
    else:
        print(f"❌ 禁止爬取: {url}")
        return False

5.5 Session 与会话管理

python 复制代码
class SessionManager:
    """会话管理器 - 处理登录态和 Cookie"""

    def __init__(self):
        self.session = requests.Session()
        self.cookie_file = "cookies.json"

    def login(self, login_url, username, password):
        """登录并保存 Cookie"""
        data = {
            'username': username,
            'password': password
        }

        response = self.session.post(login_url, data=data)
        if response.status_code == 200:
            self._save_cookies()
            print("登录成功")
            return True
        else:
            print(f"登录失败: {response.status_code}")
            return False

    def _save_cookies(self):
        """保存 Cookie 到文件"""
        import json
        cookies = self.session.cookies.get_dict()
        with open(self.cookie_file, 'w') as f:
            json.dump(cookies, f)

    def _load_cookies(self):
        """从文件加载 Cookie"""
        import json
        try:
            with open(self.cookie_file, 'r') as f:
                cookies = json.load(f)
                self.session.cookies.update(cookies)
            return True
        except FileNotFoundError:
            return False

    def fetch_with_auth(self, url):
        """使用认证状态抓取"""
        if not self._load_cookies():
            print("未找到 Cookie,请先登录")
            return None

        response = self.session.get(url)
        return response.text

六、反爬机制应对与合规策略

6.1 常见反爬机制概述

网站常见的反爬机制包括:

反爬机制 原理 合规应对策略
User-Agent 检测 检查请求头中的 User-Agent 轮换 User-Agent
IP 频率限制 单 IP 请求频率过高时封禁 控制请求频率,使用延迟
验证码 要求输入验证码 降低频率,使用合规打码服务
动态内容加载 数据通过 JavaScript 异步加载 使用 Playwright/Selenium
Cookie/Session 检测 检查请求是否携带有效 Cookie 维护 Session 状态

⚠️ 合规提醒:通过破解技术手段绕过网站反爬机制获取数据,或对目标系统造成实质性干扰(如高频请求导致服务崩溃),均构成违法行为。

6.2 频率控制与并发调度

严禁使用高并发恶意请求。应使用信号量(Semaphore)控制并发数,并加入随机延迟。

6.2.1 同步爬虫频率控制

python 复制代码
import time
import random

def crawl_with_delay(urls, min_delay=1, max_delay=3):
    """带随机延迟的爬取"""
    for url in urls:
        html = fetch_page(url)
        if html:
            # 处理数据...
            pass

        # 随机延迟,模拟人类行为
        delay = random.uniform(min_delay, max_delay)
        time.sleep(delay)

6.2.2 令牌桶限流器

python 复制代码
import time
import threading

class RateLimiter:
    """令牌桶限流器"""

    def __init__(self, rate: float, burst: int = 1):
        """
        rate: 每秒令牌数
        burst: 最大突发请求数
        """
        self.rate = rate
        self.burst = burst
        self.tokens = burst
        self.last_refill = time.time()
        self.lock = threading.Lock()

    def acquire(self) -> bool:
        """获取令牌,成功返回 True"""
        with self.lock:
            now = time.time()
            elapsed = now - self.last_refill
            self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
            self.last_refill = now

            if self.tokens >= 1:
                self.tokens -= 1
                return True
            else:
                wait_time = (1 - self.tokens) / self.rate
                time.sleep(wait_time)
                self.tokens = 0
                self.last_refill = time.time()
                return True

# 使用示例
limiter = RateLimiter(rate=1.0)  # 每秒 1 个请求
for url in urls:
    limiter.acquire()
    response = requests.get(url)

6.3 User-Agent 轮换

使用 fake-useragent 库随机生成浏览器标识:

python 复制代码
from fake_useragent import UserAgent

def get_random_headers():
    ua = UserAgent()
    return {
        'User-Agent': ua.random,
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    }

6.4 代理 IP 池管理

当遇到 IP 限制时,可使用代理 IP 池轮换 IP 地址。

python 复制代码
class ProxyManager:
    """代理 IP 管理器"""

    def __init__(self, proxy_list=None):
        self.proxies = proxy_list or []
        self.current_index = 0

    def add_proxy(self, proxy):
        self.proxies.append(proxy)

    def get_next_proxy(self):
        """轮询获取下一个代理"""
        if not self.proxies:
            return None
        proxy = self.proxies[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy

    def get_random_proxy(self):
        """随机获取一个代理"""
        if not self.proxies:
            return None
        return random.choice(self.proxies)

# 使用示例
proxy_manager = ProxyManager([
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
])

def fetch_with_proxy(url):
    proxy = proxy_manager.get_next_proxy()
    if proxy:
        proxies = {'http': proxy, 'https': proxy}
        response = requests.get(url, proxies=proxies, timeout=10)
    else:
        response = requests.get(url, timeout=10)
    return response

6.5 验证码处理技术

遇到验证码时,合规的做法是降低请求频率,或者接入正规的第三方打码平台(如 2Captcha),严禁使用恶意脚本暴力破解。

python 复制代码
# 概念示例:接入合规的第三方打码服务
class CaptchaSolver:
    def __init__(self, api_key):
        self.api_key = api_key

    def solve_image(self, image_path):
        """调用第三方打码 API"""
        # 实际应用中调用第三方 API
        # 这里仅作流程演示
        print("正在请求合规打码平台识别验证码...")
        # response = requests.post('https://api.2captcha.com/in.php', ...)
        return "captcha_result"

    def solve_recaptcha(self, site_key, page_url):
        """解决 reCAPTCHA"""
        print("正在请求合规打码平台识别 reCAPTCHA...")
        return "recaptcha_token"

6.6 动态内容抓取方案

对于 Vue/React 等单页应用(SPA),数据通过 Ajax 异步加载。首选方案是逆向分析 API 接口

6.6.1 分析 API 接口

python 复制代码
# 使用浏览器开发者工具分析网络请求,找到数据接口
API_URL = "https://api.example.com/v1/products?page=1&limit=20"

def fetch_api_data():
    headers = {
        'Accept': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
    }
    response = requests.get(API_URL, headers=headers)
    return response.json()

6.6.2 使用 Playwright 渲染

若接口加密复杂,则使用 Playwright 进行无头浏览器渲染:

python 复制代码
from playwright.sync_api import sync_playwright

def scrape_dynamic_content(url):
    """使用 Playwright 抓取动态内容"""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()

        # 拦截特定的 XHR/Fetch 请求
        response_data = []

        def handle_response(response):
            if "/api/v1/products" in response.url:
                try:
                    response_data.append(response.json())
                except:
                    pass

        page.on("response", handle_response)
        page.goto(url)

        # 等待特定元素加载完成
        page.wait_for_selector('.product-list', timeout=30000)

        # 滚动加载更多
        for _ in range(3):
            page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
            page.wait_for_timeout(1000)

        browser.close()
        return response_data

七、异步爬虫与高性能采集

7.1 同步 vs 异步爬虫

传统的同步爬虫在面对大规模数据采集时往往力不从心。测试数据显示,在 4 核 8G 服务器上,aiohttp 可维持 3000+ 并发连接,而传统 requests 库超过 500 连接就会出现性能断崖式下跌。

同步爬虫:发送请求 → 等待响应 → 处理数据 → 发送下一个请求(串行)

异步爬虫:同时发送多个请求 → 谁先响应谁先处理(并发)

7.2 aiohttp 基础用法

python 复制代码
import aiohttp
import asyncio

async def fetch_one(session, url):
    """异步获取单个页面"""
    async with session.get(url) as response:
        return await response.text()

async def fetch_multiple(urls):
    """异步获取多个页面"""
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_one(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return results

# 运行异步任务
if __name__ == "__main__":
    urls = ["https://example.com/page1", "https://example.com/page2", "https://example.com/page3"]
    results = asyncio.run(fetch_multiple(urls))
    for url, result in zip(urls, results):
        if isinstance(result, str):
            print(f"成功: {url} (长度: {len(result)})")
        else:
            print(f"失败: {url} - {result}")

7.3 并发控制与限流策略

使用 asyncio.Semaphore 控制最大并发量,建议设置在 3-10 之间较为稳妥:

python 复制代码
import asyncio
import aiohttp
import random

class AsyncCrawler:
    """异步爬虫 - 带并发控制"""

    def __init__(self, max_concurrent=5, delay_range=(1, 3)):
        self.max_concurrent = max_concurrent
        self.delay_range = delay_range
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def fetch_with_limit(self, session, url):
        """带限流的异步请求"""
        async with self.semaphore:
            # 合规的随机延迟
            await asyncio.sleep(random.uniform(*self.delay_range))

            try:
                async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as response:
                    return await response.text()
            except Exception as e:
                return f"Error: {e}"

    async def crawl(self, urls):
        """批量爬取"""
        async with aiohttp.ClientSession() as session:
            tasks = [self.fetch_with_limit(session, url) for url in urls]
            return await asyncio.gather(*tasks, return_exceptions=True)

7.4 异步爬虫实战

python 复制代码
#!/usr/bin/env python3
"""
异步爬虫实战 - 批量抓取商品信息
"""

import asyncio
import aiohttp
import json
from typing import List, Dict

class AsyncProductCrawler:
    def __init__(self, max_concurrent=5):
        self.max_concurrent = max_concurrent
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.results = []

    async def fetch_product(self, session, product_id: int) -> Dict:
        """抓取单个商品"""
        url = f"https://api.example.com/products/{product_id}"

        async with self.semaphore:
            await asyncio.sleep(1)  # 礼貌延迟

            try:
                async with session.get(url) as response:
                    if response.status == 200:
                        data = await response.json()
                        return {'id': product_id, 'data': data, 'status': 'success'}
                    else:
                        return {'id': product_id, 'status': 'failed', 'code': response.status}
            except Exception as e:
                return {'id': product_id, 'status': 'error', 'error': str(e)}

    async def crawl_products(self, product_ids: List[int]) -> List[Dict]:
        """批量抓取商品"""
        async with aiohttp.ClientSession() as session:
            tasks = [self.fetch_product(session, pid) for pid in product_ids]
            return await asyncio.gather(*tasks)

# 使用示例
async def main():
    crawler = AsyncProductCrawler(max_concurrent=10)
    product_ids = list(range(1, 101))  # 抓取 1-100 号商品
    results = await crawler.crawl_products(product_ids)

    success = [r for r in results if r.get('status') == 'success']
    failed = [r for r in results if r.get('status') != 'success']

    print(f"成功: {len(success)}, 失败: {len(failed)}")

if __name__ == "__main__":
    asyncio.run(main())

八、自动化调度与分布式架构

8.1 定时任务配置

8.1.1 使用 APScheduler

python 复制代码
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def daily_crawler():
    """每日爬虫任务"""
    logger.info("开始执行每日爬虫")
    # 执行爬虫逻辑
    logger.info("每日爬虫执行完成")

def hourly_crawler():
    """小时级爬虫任务"""
    logger.info("开始执行小时级爬虫")

def main():
    scheduler = BlockingScheduler()

    # 每天凌晨 2:00 执行
    scheduler.add_job(
        daily_crawler,
        CronTrigger(hour=2, minute=0),
        id='daily_crawler'
    )

    # 每小时执行
    scheduler.add_job(
        hourly_crawler,
        CronTrigger(minute=0),
        id='hourly_crawler'
    )

    # 每 30 分钟执行
    scheduler.add_job(
        hourly_crawler,
        IntervalTrigger(minutes=30),
        id='interval_crawler'
    )

    try:
        scheduler.start()
    except KeyboardInterrupt:
        logger.info("调度器停止")

if __name__ == "__main__":
    main()

8.1.2 与 Shell 协同调度

Python 负责核心逻辑,Shell(Cron)负责系统级唤醒:

bash 复制代码
#!/bin/bash
# run_crawler.sh - Shell 调度脚本

# 激活虚拟环境并执行 Python 爬虫
source /opt/crawler/venv/bin/activate
python /opt/crawler/main.py >> /var/log/crawler/cron.log 2>&1

Crontab 配置

bash 复制代码
# 每天凌晨 2:00 执行
0 2 * * * /opt/crawler/run_crawler.sh

# 每小时执行
0 * * * * /opt/crawler/run_crawler.sh

8.2 Scrapy 框架入门

Scrapy 是 Python 领域最强大的异步爬虫框架。

8.2.1 安装与创建项目

bash 复制代码
# 安装 Scrapy
pip install scrapy

# 创建项目
scrapy startproject myproject

# 进入项目目录
cd myproject

# 生成爬虫模板
scrapy genspider example example.com

8.2.2 编写 Spider

python 复制代码
# myproject/spiders/example.py
import scrapy

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

    def parse(self, response):
        """解析商品列表页"""
        for product in response.css('div.product-item'):
            yield {
                'title': product.css('h2.title::text').get(),
                'price': product.css('span.price::text').get(),
                'url': product.css('a::attr(href)').get(),
            }

        # 处理分页
        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

8.2.3 配置 Pipeline

python 复制代码
# myproject/pipelines.py
import json

class JsonWriterPipeline:
    def open_spider(self, spider):
        self.file = open('items.json', 'w')

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(line)
        return item

8.2.4 运行爬虫

bash 复制代码
scrapy crawl example -o products.json

8.3 Scrapy-Redis 分布式爬虫

Scrapy 本身不直接支持分布式爬虫,但可以借助 Scrapy-Redis 库来实现。

Scrapy-Redis 核心原理

  • 使用 Redis 作为共享任务队列
  • 多个爬虫节点共享待爬 URL 队列
  • 使用 Redis 实现分布式去重
python 复制代码
# settings.py - 分布式配置

# 使用 Redis 调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True

# 使用 Redis 去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# Redis 连接配置
REDIS_HOST = 'localhost'
REDIS_PORT = 6379

8.4 错误处理与重试机制

使用 tenacity 库实现优雅的指数退避重试:

python 复制代码
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import requests

@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type((requests.RequestException, ConnectionError))
)
def fetch_with_retry(url):
    """带自动重试的请求"""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.text

8.5 日志与监控

python 复制代码
import logging
import sys

def setup_logger(name, log_file=None, level=logging.INFO):
    """配置日志系统"""
    logger = logging.getLogger(name)
    logger.setLevel(level)

    # 控制台输出
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(level)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)

    # 文件输出
    if log_file:
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setLevel(level)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

    return logger

# 使用示例
logger = setup_logger('crawler', 'crawler.log')
logger.info("爬虫启动")
logger.warning("请求失败,正在重试")
logger.error("爬取失败", exc_info=True)

九、实战案例:电商价格监控系统

9.1 需求分析与系统设计

场景需求

  • 监控自有电商平台商品价格变动
  • 前提:拥有该电商平台的管理权限或 API 访问权限
  • 按小时采集价格数据
  • 当价格波动超过 10% 时发送告警
  • 输出价格趋势报告

系统设计

复制代码
┌─────────────────────────────────────────────────────┐
│                  定时触发器                         │
│            (APScheduler / Cron)                     │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                 数据采集层                          │
│    请求 API → 提取价格数据                          │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                 数据处理层                          │
│    价格验证 → 波动计算 → 告警判断                   │
└─────────────────┬───────────────────────────────────┘
                  ▼
┌─────────────────────────────────────────────────────┐
│                 存储与告警                          │
│    存储历史数据 → 触发告警邮件                      │
└─────────────────────────────────────────────────────┘

9.2 核心代码实现

python 复制代码
#!/usr/bin/env python3
"""
电商价格监控系统 - 合规版本
此脚本仅适用于自有平台或获得授权的平台
数据仅用于内部商业分析,不对外公开
"""

import requests
import sqlite3
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
import logging
import json
from dataclasses import dataclass, asdict
from typing import Optional, List

# ============ 配置 ============
API_URL = "https://your-own-store.com/api/products/12345"
API_TOKEN = "YOUR_API_TOKEN"
HISTORY_DB = "price_history.db"
THRESHOLD = 10.0  # 波动阈值(百分比)
SMTP_HOST = "smtp.yourcompany.com"
SMTP_PORT = 587
SMTP_USER = "alert@yourcompany.com"
SMTP_PASS = "your_password"
ALERT_EMAIL = "admin@yourcompany.com"

# ============ 日志配置 ============
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('monitor.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# ============ 数据模型 ============
@dataclass
class PriceRecord:
    timestamp: str
    price: float
    product_id: str
    product_name: str = ''

@dataclass
class PriceAlert:
    timestamp: str
    product_id: str
    old_price: float
    new_price: float
    fluctuation: float

# ============ 数据库操作 ============
class PriceDatabase:
    def __init__(self, db_path=HISTORY_DB):
        self.conn = sqlite3.connect(db_path)
        self.cursor = self.conn.cursor()
        self._init_table()

    def _init_table(self):
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS price_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                product_id TEXT,
                product_name TEXT,
                price REAL,
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        self.conn.commit()

    def save_record(self, record: PriceRecord):
        self.cursor.execute(
            "INSERT INTO price_history (product_id, product_name, price, timestamp) VALUES (?, ?, ?, ?)",
            (record.product_id, record.product_name, record.price, record.timestamp)
        )
        self.conn.commit()

    def get_last_price(self, product_id: str) -> Optional[float]:
        self.cursor.execute(
            "SELECT price FROM price_history WHERE product_id = ? ORDER BY timestamp DESC LIMIT 1",
            (product_id,)
        )
        row = self.cursor.fetchone()
        return row[0] if row else None

    def get_history(self, product_id: str, limit: int = 100):
        self.cursor.execute(
            "SELECT timestamp, price FROM price_history WHERE product_id = ? ORDER BY timestamp DESC LIMIT ?",
            (product_id, limit)
        )
        return self.cursor.fetchall()

    def close(self):
        self.conn.close()

# ============ 价格获取 ============
class PriceFetcher:
    def __init__(self, api_url, api_token):
        self.api_url = api_url
        self.api_token = api_token
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {api_token}',
            'Accept': 'application/json'
        })

    def fetch_price(self) -> Optional[dict]:
        """获取当前价格数据"""
        try:
            response = self.session.get(self.api_url, timeout=10)
            response.raise_for_status()
            data = response.json()

            return {
                'product_id': data.get('id', ''),
                'product_name': data.get('name', ''),
                'price': float(data.get('current_price', 0))
            }
        except requests.RequestException as e:
            logger.error(f"获取价格失败: {e}")
            return None
        except (ValueError, KeyError) as e:
            logger.error(f"解析数据失败: {e}")
            return None

# ============ 告警发送 ============
class AlertSender:
    def __init__(self, smtp_host, smtp_port, smtp_user, smtp_pass):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
        self.smtp_user = smtp_user
        self.smtp_pass = smtp_pass

    def send_alert(self, alert: PriceAlert):
        """发送告警邮件"""
        subject = f"价格告警: {alert.product_id} - 波动 {alert.fluctuation:.1f}%"
        body = f"""
        商品 ID: {alert.product_id}
        旧价格: {alert.old_price}
        新价格: {alert.new_price}
        波动率: {alert.fluctuation:.1f}%
        时间: {alert.timestamp}

        请及时关注价格变化。
        """

        msg = MIMEMultipart()
        msg['Subject'] = subject
        msg['From'] = self.smtp_user
        msg['To'] = ALERT_EMAIL

        msg.attach(MIMEText(body, 'plain', 'utf-8'))

        try:
            with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
                server.starttls()
                server.login(self.smtp_user, self.smtp_pass)
                server.send_message(msg)
            logger.info(f"告警已发送: {subject}")
        except Exception as e:
            logger.error(f"发送告警失败: {e}")

# ============ 主程序 ============
class PriceMonitor:
    def __init__(self):
        self.db = PriceDatabase()
        self.fetcher = PriceFetcher(API_URL, API_TOKEN)
        self.alerter = AlertSender(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS)

    def calculate_fluctuation(self, old: float, new: float) -> float:
        """计算波动率"""
        if not old or old == 0:
            return 0.0
        return ((new - old) / old) * 100

    def run(self):
        """执行监控"""
        logger.info("开始监控商品价格...")

        # 获取当前价格
        data = self.fetcher.fetch_price()
        if not data:
            logger.error("无法获取价格数据")
            return

        current_price = data['price']
        product_id = data['product_id']
        product_name = data['product_name']

        logger.info(f"商品: {product_name}, 当前价格: {current_price}")

        # 获取历史价格
        last_price = self.db.get_last_price(product_id)

        if last_price is not None:
            fluctuation = self.calculate_fluctuation(last_price, current_price)
            logger.info(f"价格波动: {fluctuation:.1f}%")

            # 检查是否超过阈值
            if abs(fluctuation) > THRESHOLD:
                alert = PriceAlert(
                    timestamp=datetime.now().isoformat(),
                    product_id=product_id,
                    old_price=last_price,
                    new_price=current_price,
                    fluctuation=fluctuation
                )
                self.alerter.send_alert(alert)

        # 保存记录
        record = PriceRecord(
            timestamp=datetime.now().isoformat(),
            price=current_price,
            product_id=product_id,
            product_name=product_name
        )
        self.db.save_record(record)

        logger.info("监控完成")

    def generate_report(self):
        """生成价格趋势报告"""
        history = self.db.get_history('', limit=50)
        if not history:
            logger.info("暂无历史数据")
            return

        report = "价格趋势报告\n"
        report += "=" * 40 + "\n"
        for timestamp, price in history:
            report += f"{timestamp}: {price}\n"

        with open('price_report.txt', 'w', encoding='utf-8') as f:
            f.write(report)

        logger.info("报告已生成: price_report.txt")

    def close(self):
        self.db.close()

# ============ 主入口 ============
if __name__ == "__main__":
    monitor = PriceMonitor()
    try:
        monitor.run()
        monitor.generate_report()
    finally:
        monitor.close()

9.3 监控报警与运维优化

9.3.1 Systemd Timer 配置

bash 复制代码
# /etc/systemd/system/price-monitor.service
[Unit]
Description=Price Monitor Service

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/monitor/monitor.py
User=monitor

# /etc/systemd/system/price-monitor.timer
[Unit]
Description=Price Monitor Timer

[Timer]
OnCalendar=*-*-* *:00:00
Persistent=true

[Install]
WantedBy=timers.target

9.3.2 数据清理

python 复制代码
def cleanup_old_data(days=90):
    """清理旧数据"""
    import sqlite3
    from datetime import datetime, timedelta

    conn = sqlite3.connect(HISTORY_DB)
    cursor = conn.cursor()

    cutoff = (datetime.now() - timedelta(days=days)).isoformat()
    cursor.execute("DELETE FROM price_history WHERE timestamp < ?", (cutoff,))
    conn.commit()
    conn.close()

    logger.info(f"已清理 {days} 天前的数据")

十、总结

本文全面介绍了 Python 网络爬虫的核心技术体系:

10.1 核心技术要点

技术领域 核心内容 关键工具
请求与网络 HTTP 协议、请求头伪装、会话管理 requests, aiohttp
数据解析 HTML 解析、XPath、CSS 选择器 BeautifulSoup, lxml
动态渲染 无头浏览器、JavaScript 执行 Playwright, Selenium
数据存储 CSV、JSON、Excel、数据库 pandas, SQLAlchemy
并发采集 异步 IO、并发控制、限流 asyncio, aiohttp
分布式 任务队列、去重、多节点协同 Scrapy-Redis
合规性 robots.txt、频率控制、隐私保护 urllib.robotparser

10.2 合规红线

  1. 遵守法律法规:《网络安全法》《数据安全法》《个人信息保护法》
  2. 尊重 robots.txt :使用 RobotFileParser 检查
  3. 控制请求频率:避免对服务器造成过大压力
  4. 保护个人隐私:不采集敏感信息,数据脱敏处理
  5. 合法使用数据:仅用于合规的商业分析或学术研究

10.3 学习路径建议

第一阶段:基础入门(1-2 周)

  • 掌握 Python 基础语法和虚拟环境
  • 学习 requests + BeautifulSoup 基本用法
  • 完成一个简单的静态页面爬虫

第二阶段:实践进阶(2-4 周)

  • 学习 lxml + XPath 高级解析
  • 掌握数据存储(CSV、JSON、数据库)
  • 实现分页和增量爬取

第三阶段:高级开发(4-8 周)

  • 学习 aiohttp 异步爬虫
  • 掌握 Playwright 动态渲染
  • 学习 Scrapy 框架和分布式部署

第四阶段:工程化(持续)

  • 学习 Scrapy-Redis 分布式爬虫
  • 掌握监控告警和运维优化
  • 深入了解合规与安全

附录

A. 常用 Python 爬虫库速查表

库名 核心用途 安装命令
requests 同步 HTTP 请求 pip install requests
aiohttp 异步 HTTP 请求 pip install aiohttp
beautifulsoup4 HTML 解析 pip install beautifulsoup4
lxml 高性能解析 pip install lxml
pandas 数据清洗与处理 pip install pandas
playwright 无头浏览器自动化 pip install playwright
scrapy 爬虫框架 pip install scrapy
tenacity 重试机制 pip install tenacity
fake-useragent 随机 User-Agent pip install fake-useragent
apscheduler 定时任务 pip install apscheduler

B. HTTP 状态码与处理策略

状态码 含义 爬虫处理策略
200 成功 正常解析数据
301/302 重定向 自动跟随(requests 默认)
403 禁止访问 检查 User-Agent/Cookie,降低频率
404 未找到 记录死链,从任务队列中移除
429 请求过多 必须立即停止,增加休眠时间
500/502 服务器错误 加入重试队列,延迟后重试

C. 合规爬虫 Checklist(开发前必读)

  • 是否已读取并遵守目标网站的 robots.txt
  • 是否在 User-Agent 中留下了可联系的标识?
  • 请求频率是否已限制在合理范围(如单 IP 每秒不超过 2 次)?
  • 是否避免了在目标网站业务高峰期进行大规模抓取?
  • 采集的数据中是否包含个人隐私?若有,是否已做脱敏处理?
  • 是否仅采集公开可见的数据,未尝试绕过登录墙或付费墙?
  • 是否有数据安全和隐私保护措施?

D. 学习资源推荐

官方文档

合规指南

  • 《中华人民共和国网络安全法》
  • 《中华人民共和国数据安全法》
  • 《中华人民共和国个人信息保护法》
  • 互联网爬虫合规操作指南

推荐书籍

  • 《Python 网络爬虫开发从入门到精通》
  • 《Python 商业大数据网络爬虫技术与实战》