1. Python爬虫的基本原理与HTTP基础

网络爬虫是一种自动获取网页数据的程序,其核心在于模拟浏览器发送HTTP请求并解析返回内容。一个完整的爬虫流程包含四个关键步骤: 发送请求 → 接收响应 → 解析内容 → 存储数据 。其中,HTTP协议是整个通信过程的基础,它定义了客户端与服务器之间的交互规则。

1.1 网络爬虫的工作机制

爬虫本质上是一个自动化脚本,通过向目标网站的URL发起HTTP请求,获取服务器返回的HTML、JSON或其他格式的数据。以Python为例,使用 requests 库可轻松实现请求发送:

import requests
response = requests.get("https://example.com")
print(response.status_code)  # 输出状态码,如200表示成功

该过程背后遵循标准的TCP/IP和HTTP协议栈。爬虫首先建立与服务器的连接,随后发送带有特定头部信息(如User-Agent)的请求报文,服务器处理后返回响应报文,包含状态码、响应头及主体内容(如HTML页面)。爬虫再利用解析工具(如BeautifulSoup)提取所需结构化数据。

1.2 HTTP协议基础详解

HTTP(HyperText Transfer Protocol)是应用层协议,采用“请求-响应”模式进行通信。常见的请求方法包括:
- GET :获取资源,最常用;
- POST :提交数据,如登录表单;
- PUT / DELETE :更新或删除资源(较少用于爬虫)。

服务器返回的状态码揭示了请求结果:
| 状态码 | 含义 |
|--------|------------------|
| 200 | 请求成功 |
| 404 | 页面未找到 |
| 500 | 服务器内部错误 |
| 403 | 禁止访问(可能被反爬) |

请求头(Headers)在爬虫中尤为重要,常用于伪装成真实浏览器,例如设置 User-Agent 避免被识别为机器人。此外,Cookie与Session机制帮助维持用户会话状态,在需要登录的场景中不可或缺。

1.3 合法性与伦理规范:robots.txt与合规抓取

在实践爬虫时,必须遵守网站的 robots.txt 协议(如 https://example.com/robots.txt ),它声明了哪些路径允许或禁止抓取。例如:

User-agent: *
Disallow: /admin/
Allow: /public/

这表示所有爬虫不得访问 /admin/ 目录。尽管技术上可以绕过限制,但从法律和道德角度出发,应尊重网站意愿,控制请求频率,避免对服务器造成负担。合理使用延时( time.sleep() )、IP轮换等策略,既能提升稳定性,也体现专业素养。

2. Python网络请求与HTML解析技术

在现代数据采集的实践中,掌握如何通过程序化方式从互联网获取网页内容是构建高效爬虫系统的第一步。这一过程的核心在于两个关键技术环节: 发送HTTP请求以获取原始HTML响应 ,以及 对返回的HTML文档进行结构化解析以提取所需信息 。本章将深入探讨这两个关键步骤的技术实现路径,重点介绍 requests 库作为主流的HTTP客户端工具,以及 BeautifulSoup lxml 两大HTML解析引擎的工作机制与最佳使用模式。通过系统学习这些基础但至关重要的组件,读者不仅能够理解网络通信的本质,还能建立起稳定、可扩展的数据抓取流程。

2.1 使用requests库发送HTTP请求

requests 是 Python 社区中最广泛使用的第三方 HTTP 客户端库,因其简洁直观的 API 设计被誉为“人类友好的HTTP库”。它封装了底层复杂的 socket 通信和协议处理逻辑,使得开发者可以用极少的代码完成常见的网页请求任务。无论是获取静态页面、提交表单数据,还是与 RESTful API 进行交互, requests 都提供了统一且强大的接口支持。

2.1.1 安装与基本用法:get()和post()方法

要开始使用 requests ,首先需要通过 pip 工具安装该库:

pip install requests

安装完成后即可导入并发起最基本的 GET 请求。GET 方法用于从服务器获取资源,是最常用的 HTTP 动作之一。以下是一个典型的示例,演示如何请求一个公开网页并检查其响应状态:

import requests

# 发起 GET 请求
response = requests.get("https://httpbin.org/get")

# 检查响应状态码
print(f"Status Code: {response.status_code}")

# 输出响应内容(JSON格式)
print(response.json())

逐行逻辑分析:

  • 第1行:导入 requests 模块,这是所有操作的基础。
  • 第4行:调用 requests.get() 方法,传入目标URL。该方法会阻塞执行直到收到服务器响应或超时。
  • 第7行:访问 .status_code 属性获取HTTP状态码,如200表示成功,404表示未找到等。
  • 第10行: .json() 方法自动解析响应体中的 JSON 数据,前提是 Content-Type 正确标识为 application/json。

对于需要向服务器提交数据的场景(例如登录、搜索),通常使用 POST 请求。POST 方法允许携带请求体数据,常见于表单提交或API调用。下面是一个模拟用户登录的示例:

import requests

# 要提交的数据
login_data = {
    "username": "testuser",
    "password": "secret123"
}

# 发起 POST 请求
response = requests.post("https://httpbin.org/post", data=login_data)

# 查看返回结果
print(response.status_code)
print(response.json())

在此例中, data 参数接收字典类型的数据,并将其编码为 application/x-www-form-urlencoded 格式发送。若需以 JSON 形式发送数据(常用于REST API),应使用 json= 参数:

response = requests.post("https://api.example.com/login", json=login_data)

此时, requests 会自动设置 Content-Type: application/json 并序列化数据为 JSON 字符串。

请求类型 使用场景 常用参数
GET 获取资源、浏览页面 params (查询参数)
POST 提交数据、创建资源 data (表单数据)、 json (JSON数据)
PUT 更新完整资源 data json
DELETE 删除资源 ——

参数说明:
- params : 接收字典,用于构造URL查询字符串(即 ?key=value 形式);
- data : 表单数据,适用于 content-type 为 application/x-www-form-urlencoded
- json : 自动序列化为 JSON 并设置正确的 headers;
- headers : 自定义请求头;
- timeout : 设置请求最长等待时间,避免无限挂起。

2.1.2 自定义请求头与模拟浏览器行为

许多网站会对非浏览器来源的请求进行限制或直接拒绝服务。这是因为默认情况下, requests 发出的请求缺少某些典型浏览器特征,比如 User-Agent 头部。因此,在实际爬虫开发中,必须通过自定义请求头来伪装成真实用户访问。

import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/124.0.0.0 Safari/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",
    "Upgrade-Insecure-Requests": "1"
}

response = requests.get("https://example.com", headers=headers)

上述代码设置了多个关键头部字段:
- User-Agent :声明客户端身份,防止被识别为爬虫;
- Accept :告知服务器能接受的内容类型;
- Accept-Language :指定偏好语言,增强地域真实性;
- Connection :启用持久连接以提高效率;
- Upgrade-Insecure-Requests :表示支持HTTPS升级。

为了进一步提升伪装效果,可以采用 随机化 User-Agent 策略,避免长时间使用同一标识导致封禁:

import random
from requests import Session

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36..."
]

def get_random_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,*/*;q=0.9",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
    }

# 使用示例
session = Session()
response = session.get("https://target-site.com", headers=get_random_headers())

使用 Session 对象还可以保持 Cookie 和连接复用,显著提升多请求场景下的性能。

graph TD
    A[发起HTTP请求] --> B{是否包含合法Headers?}
    B -->|否| C[被服务器拦截或返回错误]
    B -->|是| D[正常响应HTML内容]
    D --> E[解析并提取数据]
    C --> F[调整Headers策略重试]
    F --> B

该流程图展示了请求头合法性对请求成功率的影响路径。合理的头部配置不仅是技术需求,更是绕过反爬机制的重要手段。

2.1.3 处理重定向、超时与异常捕获

在网络请求过程中,经常遇到诸如网络延迟、目标不可达、服务器错误等问题。为此, requests 提供了完善的控制选项和异常处理机制。

重定向控制

默认情况下, requests 会自动跟随 HTTP 重定向(如301、302状态码)。虽然这简化了开发,但在某些调试场景下可能希望禁用此功能:

response = requests.get("https://httpbin.org/redirect/1", allow_redirects=False)
print(response.status_code)  # 输出 302
print(response.headers['Location'])  # 查看重定向目标

若需手动控制跳转流程,可结合 allow_redirects=False 与循环逻辑实现自定义跳转策略。

超时设置

长时间挂起的请求会影响程序整体稳定性,因此务必设置合理的超时时间:

try:
    response = requests.get("https://slow-site.com", timeout=(5, 10))
except requests.exceptions.Timeout:
    print("请求超时:连接或读取耗时过长")
except requests.exceptions.RequestException as e:
    print(f"请求发生异常: {e}")

timeout 参数接受元组形式 (connect_timeout, read_timeout) ,分别控制建立连接和接收响应的最长时间。

异常分类与捕获

requests 抛出的异常具有层次结构,便于精细化处理:

异常类 含义
requests.exceptions.ConnectionError 网络连接失败
requests.exceptions.Timeout 请求超时
requests.exceptions.HTTPError 响应状态码为4xx或5xx
requests.exceptions.TooManyRedirects 超过最大重定向次数
requests.exceptions.RequestException 所有请求相关异常的基类

推荐做法是使用统一的异常基类进行兜底捕获:

import requests
from requests.exceptions import RequestException

try:
    response = requests.get("https://api.example.com/data", timeout=10)
    response.raise_for_status()  # 显式抛出HTTP错误(如404、500)
except RequestException as e:
    print(f"请求失败: {e}")
else:
    print("请求成功,数据如下:")
    print(response.text)

其中 raise_for_status() 方法会在状态码表示错误时主动引发 HTTPError ,有助于及时发现服务端问题。

2.2 解析网页内容:BeautifulSoup与lxml

获取到原始 HTML 文本后,下一步是从中精准提取结构化信息。由于 HTML 是一种嵌套的标记语言,手动字符串匹配极易出错且难以维护。为此,Python 提供了多种高效的解析工具,其中 BeautifulSoup lxml 组合使用最为普遍。

2.2.1 HTML结构解析与标签定位

BeautifulSoup 是一个灵活的 HTML/XML 解析库,擅长处理不规范或残缺的文档。它依赖于底层解析器(如 html.parser lxml )来构建文档树结构,从而支持基于标签名、属性、层级关系等多种方式进行元素查找。

安装命令如下:

pip install beautifulsoup4 lxml

加载一个简单的 HTML 片段进行测试:

from bs4 import BeautifulSoup

html_doc = """
<html>
<head><title>示例页面</title></head>
<body>
    <div class="content">
        <h1 id="title">欢迎来到我的网站</h1>
        <p class="desc">这是一个用于教学的HTML样例。</p>
        <a href="/about">关于我们</a>
        <ul>
            <li><a href="/news">新闻</a></li>
            <li><a href="/contact">联系方式</a></li>
        </ul>
    </div>
</body>
</html>

soup = BeautifulSoup(html_doc, 'lxml')  # 使用lxml作为解析器
print(soup.prettify())  # 格式化输出HTML结构

逐行分析:
- BeautifulSoup(html_doc, 'lxml') :创建 Soup 对象,使用高性能的 lxml 解析器;
- .prettify() :美化输出整个DOM树,便于查看结构。

解析后的对象支持类似 DOM 的遍历方式:

title_tag = soup.find('h1')
print(title_tag.string)           # 输出文本内容
print(title_tag['id'])           # 访问属性值
print(title_tag.parent.name)     # 获取父节点标签名

2.2.2 使用CSS选择器与find()/find_all()方法提取数据

BeautifulSoup 支持两种主要的选择方式: 方法查找 CSS选择器

方法查找:find 与 find_all

# 查找第一个匹配项
first_link = soup.find('a')
print(first_link['href'])

# 查找所有链接
all_links = soup.find_all('a')
for link in all_links:
    print(link.get_text(), "->", link['href'])

find_all() 支持多种过滤条件:

# 按类名查找
descriptions = soup.find_all('p', class_='desc')

# 按ID查找
title = soup.find('h1', id='title')

# 组合条件
items = soup.find_all('li', limit=2)  # 仅取前两项

CSS选择器:select() 方法

更强大的是 select() 方法,支持完整的 CSS3 选择器语法:

# 选择所有class为'desc'的p标签
descs = soup.select('p.desc')

# 子代选择器
links_in_ul = soup.select('ul > li > a')

# 属性选择器
about_link = soup.select('a[href="/about"]')

# 伪类选择(部分支持)
first_item = soup.select('ul > li:first-child')
选择器语法 示例 说明
tag div 选择所有div元素
.class .content 类名为content的元素
#id #title ID为title的元素
[attr] [href] 具有href属性的元素
A > B ul > li B是A的直接子元素
A B div a B是A的任意后代

2.2.3 处理乱码与非标准HTML文档

实际爬取的网页常存在编码混乱或语法错误的问题。 BeautifulSoup 在这方面表现出较强容错能力。

当出现中文乱码时,可通过指定编码解决:

response = requests.get("https://example-chinese-site.com")
response.encoding = 'utf-8'  # 强制设定编码
soup = BeautifulSoup(response.text, 'lxml')

也可让 BeautifulSoup 自动检测编码:

soup = BeautifulSoup(response.content, 'lxml', from_encoding=response.apparent_encoding)

对于严重损坏的HTML,建议优先使用 lxml html5lib 解析器:

soup = BeautifulSoup(broken_html, 'html5lib')  # 最强容错性
graph LR
    A[原始HTML文本] --> B{是否有效?}
    B -->|是| C[使用lxml解析]
    B -->|否| D[使用html5lib修复并解析]
    C --> E[构建DOM树]
    D --> E
    E --> F[使用CSS选择器提取数据]

该流程图清晰表达了不同质量HTML文档的处理路径决策。

2.3 数据提取的最佳实践

成功的爬虫不仅仅是获取数据,更重要的是以 结构化、可维护、高鲁棒性 的方式组织提取逻辑。

2.3.1 提取文本、链接与属性值的通用模式

标准化提取函数有助于复用和调试:

def extract_links(soup):
    links = []
    for a_tag in soup.select('a[href]'):
        link = {
            'text': a_tag.get_text(strip=True),
            'url': a_tag['href'],
            'title': a_tag.get('title', ''),
        }
        links.append(link)
    return links

def extract_articles(soup):
    articles = []
    for item in soup.select('.article-item'):
        article = {
            'title': item.select_one('.title').get_text(),
            'summary': item.select_one('.summary').get_text(),
            'url': item.select_one('a')['href'],
            'pub_date': item.select_one('.date').get_text()
        }
        articles.append(article)
    return articles

要点:
- 使用 .get() 防止 KeyError;
- strip=True 去除首尾空白;
- select_one() 返回首个匹配元素,避免索引越界。

2.3.2 分页链接识别与多页面数据采集策略

多数网站采用分页展示内容,需自动识别并遍历后续页码:

base_url = "https://example.com/news?page={}"
all_articles = []

for page in range(1, 6):  # 爬取前5页
    url = base_url.format(page)
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.text, 'lxml')
    articles = extract_articles(soup)
    all_articles.extend(articles)
    # 可加入延时防反爬
    import time
    time.sleep(1)

进阶策略包括动态检测“下一页”按钮是否存在:

next_page = soup.select_one('a[rel="next"]')
while next_page:
    current_url = urljoin(base_url, next_page['href'])
    response = requests.get(current_url)
    soup = BeautifulSoup(response.text, 'lxml')
    # 提取数据...
    next_page = soup.select_one('a[rel="next"]')

2.3.3 结构化输出:将数据整理为字典或列表格式

最终数据应统一为标准容器格式,便于后续清洗与存储:

import json

# 示例数据
data = [
    {'name': '商品A', 'price': '¥299', 'rating': 4.8},
    {'name': '商品B', 'price': '¥199', 'rating': 4.5}
]

# 导出为JSON文件
with open('products.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

表格总结常用数据结构适用场景:

数据结构 优点 适用场景
列表[List] 有序、可重复 存储多个条目
字典[Dict] 键值映射、易读 表示单个记录
DataFrame[pandas] 支持清洗、统计 大规模数据分析
JSON 跨平台兼容 数据交换与存储

遵循以上最佳实践,不仅能提升数据提取的准确率,也为后续集成至自动化管道奠定坚实基础。

3. 动态网页处理与反爬机制应对

在现代互联网应用中,静态HTML页面已不再是主流。越来越多的网站采用前端框架(如Vue.js、React、Angular)构建单页应用(SPA),数据通过异步请求(AJAX/Fetch)动态加载,内容由JavaScript渲染后才出现在DOM中。传统的基于 requests BeautifulSoup 的解析方式无法获取这些动态内容,因此必须引入新的技术手段来应对这一挑战。与此同时,随着数据价值的提升,各大平台纷纷加强反爬机制,包括IP限制、验证码拦截、用户行为分析等,使得网络爬虫面临前所未有的复杂环境。本章将系统探讨如何识别并处理动态网页内容,并深入剖析常见反爬策略及其有效应对方法,最终介绍Scrapy框架在高效率、可扩展性方面的优势。

3.1 动态内容加载与Selenium工具应用

当今绝大多数大型网站都采用了前后端分离架构,前端负责展示逻辑,后端提供API接口返回JSON数据,页面内容由JavaScript动态插入到DOM结构中。这种设计提升了用户体验,但也给传统爬虫带来了巨大障碍——使用 requests.get() 获取的原始HTML源码中往往不包含实际显示的数据。例如,在淘宝商品详情页或微博信息流中,初始HTML可能仅包含一个空容器 <div id="app"></div> ,真正的内容是在浏览器运行JavaScript脚本之后才填充进去的。要解决这类问题,就需要能够执行JavaScript并等待页面完全加载的工具,Selenium正是为此而生。

3.1.1 识别JavaScript渲染的网页内容

判断一个网页是否依赖JavaScript进行内容渲染,是决定爬取策略的关键第一步。最直接的方法是对比“原始HTML”与“浏览器渲染后的DOM”。

方法一:使用开发者工具观察网络请求

打开Chrome开发者工具(F12),切换至 Network 标签页,刷新页面,查看是否有大量XHR/Fetch请求返回JSON格式数据。如果关键内容来自某个API端点(如 /api/products /data/feed.json ),则说明该页面为动态加载型。

sequenceDiagram
    participant Browser
    participant Server
    participant API
    Browser->>Server: GET /page.html
    Server-->>Browser: 返回基础HTML骨架
    Browser->>API: Fetch /api/content
    API-->>Browser: 返回JSON数据
    Browser->>Browser: 执行JS,渲染DOM

上述流程图清晰地展示了现代SPA的工作机制:浏览器先加载轻量级HTML文件,再通过JavaScript发起异步请求获取真实数据,最后完成页面渲染。

方法二:禁用JavaScript测试内容可见性

在Chrome设置中临时禁用JavaScript,重新访问目标页面。若页面为空白或仅显示“正在加载”,即表明其严重依赖JS执行。

一旦确认目标页面为动态渲染类型,就不能再依赖 requests + BeautifulSoup 组合,而应转向具备浏览器自动化能力的工具,如Selenium。

3.1.2 Selenium+ChromeDriver实现自动化浏览

Selenium 是一个用于Web应用程序测试的开源框架,但它也被广泛应用于爬虫领域,因其能模拟真实用户的操作行为,支持JavaScript执行、页面跳转、点击事件等复杂交互。

安装与配置

首先安装Python绑定库:

pip install selenium

然后下载对应版本的  ChromeDriver  并将其路径加入系统环境变量,或在代码中显式指定。

基础示例:抓取动态加载的商品列表

以某电商网站为例,商品信息通过滚动加载,需触发多次AJAX请求才能获取全部结果。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import time

# 配置Chrome选项
options = webdriver.ChromeOptions()
options.add_argument("--headless")  # 无头模式,后台运行
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")

# 启动驱动
service = Service("/usr/local/bin/chromedriver")  # 替换为你的chromedriver路径
driver = webdriver.Chrome(service=service, options=options)

try:
    driver.get("https://example-shop.com/products")
    # 模拟滚动到底部,触发懒加载
    last_height = driver.execute_script("return document.body.scrollHeight")
    while True:
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(2)  # 等待新内容加载
        new_height = driver.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            break
        last_height = new_height

    # 提取所有商品项
    products = driver.find_elements(By.CLASS_NAME, "product-item")
    for product in products:
        name = product.find_element(By.CLASS_NAME, "name").text
        price = product.find_element(By.CLASS_NAME, "price").text
        print(f"商品名: {name}, 价格: {price}")

finally:
    driver.quit()

代码逻辑逐行解读:

行号 代码片段 解释
1-4 from selenium import ... 导入Selenium核心模块,包括WebDriver、Service管理及定位器By类
7-12 options.add_argument(...) 设置Chrome启动参数:
--headless : 无界面运行,适合服务器部署
--disable-gpu : 提升稳定性
user-agent : 模拟正常用户请求头
15-16 service = Service(...) 指定ChromeDriver可执行文件路径,避免自动查找失败
18 driver = webdriver.Chrome(...) 初始化浏览器实例,传入选项和服务对象
21 driver.get(url) 访问目标URL,等待页面初步加载
25-32 while True: 循环 实现滚动加载检测:
获取当前页面高度 → 滚动到底部 → 等待2秒 → 再次获取高度
若高度未变,说明无更多内容,跳出循环
35 find_elements(By.CLASS_NAME, ...) 使用类名定位多个商品元素,返回WebElement列表
36-38 .find_element(...).text 在每个商品块内进一步定位子元素并提取文本内容

⚠️ 注意事项:
- time.sleep() 是简单粗暴的等待方式,生产环境中建议改用显式等待。
- driver.quit() 必须调用,否则可能导致Chrome进程残留。

3.1.3 等待机制:显式等待与隐式等待的使用场景

Selenium提供了三种等待方式: 隐式等待(Implicit Wait) 显式等待(Explicit Wait) 强制等待(sleep) 。合理选择可显著提高脚本稳定性和性能。

对比表格:三种等待方式特性比较

类型 设置方式 作用范围 是否阻塞 推荐程度
隐式等待 driver.implicitly_wait(10) 全局生效,对所有查找元素操作 否(最多等待设定时间) 中等
显式等待 WebDriverWait(driver, 10).until(...) 局部针对特定条件 强烈推荐
强制等待 time.sleep(5) 全局暂停执行 不推荐

示例:使用显式等待确保元素出现

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(
    EC.presence_of_element_located((By.ID, "dynamic-content"))
)
print(element.text)

参数说明:

  • WebDriverWait(driver, 10) :创建一个最大等待时间为10秒的等待器。
  • EC.presence_of_element_located(...) :期望条件为“指定定位器的元素存在于DOM中”。
  • 支持多种条件,如:
  • visibility_of_element_located : 元素可见
  • element_to_be_clickable : 元素可点击
  • text_to_be_present_in_element : 元素文本包含某字符串

优势分析:

相比 time.sleep() ,显式等待更加智能——只要条件满足就立即继续执行,无需耗尽预设时间。这在网速波动或服务器响应延迟时尤为重要,既能保证成功率,又不会浪费资源。

graph TD
    A[开始查找元素] --> B{元素是否存在?}
    B -- 是 --> C[立即返回]
    B -- 否 --> D[检查是否超时]
    D -- 未超时 --> E[继续轮询]
    E --> B
    D -- 已超时 --> F[抛出TimeoutException]

该流程图展示了显式等待的核心机制:持续轮询直到满足预期条件或超时。

综上所述,Selenium为处理动态网页提供了强大支持,但其资源消耗较高,速度较慢。在实际项目中应权衡需求:对于少量、复杂交互的页面优先选用Selenium;而对于大规模数据采集,更推荐尝试逆向工程API接口或结合Puppeteer/Playwright等现代工具。

3.2 常见反爬策略及其破解思路

随着数据竞争加剧,网站运营方不断升级防护体系,试图阻止非人类流量的非法抓取。常见的反爬手段涵盖网络层、会话层、行为层等多个维度。理解这些机制的本质,并采取合法合规的规避措施,是构建可持续爬虫系统的前提。

3.2.1 IP封禁与请求频率控制:添加延时与随机化策略

许多网站会对单位时间内来自同一IP地址的请求数进行监控,超过阈值即触发限流甚至永久封禁。例如,知乎可能每分钟允许最多20次请求,超出后返回 429 Too Many Requests 状态码。

应对方案:请求节流与随机延迟

最简单的防御方式是在每次请求之间加入随机延时,打破固定节奏,降低被识别为机器的可能性。

import time
import random
import requests

urls = ["https://api.example.com/data?page={}".format(i) for i in range(1, 101)]

for url in urls:
    try:
        response = requests.get(url, headers={"User-Agent": "Mozilla/5.0..."})
        if response.status_code == 200:
            process_data(response.json())
        elif response.status_code == 429:
            print("被限流,休眠60秒...")
            time.sleep(60)
        else:
            print(f"请求失败: {response.status_code}")
    except Exception as e:
        print(f"异常: {e}")
    # 随机等待1~3秒
    time.sleep(random.uniform(1, 3))

逻辑分析:

  • random.uniform(1, 3) 生成1到3之间的浮点数,使请求间隔不规律。
  • 当收到 429 响应时,主动进入长时间休眠,避免持续触发警报。
  • 可进一步优化:记录响应时间、错误率,动态调整休眠时长。

进阶策略:使用代理池轮换出口IP

单一IP极易被封,解决方案是构建 代理IP池 ,每次请求从中随机选取不同IP发送。

代理类型 特点 适用场景
透明代理 被目标服务器识别 不推荐
匿名代理 隐藏真实IP,但暴露代理特征 一般用途
高匿代理(Elite Proxy) 完全伪装,难以检测 高强度爬取

示例:通过第三方代理服务(如Luminati、ScraperAPI)转发请求

proxies = {
    'http': 'http://user:pass@proxy-server:port',
    'https': 'http://user:pass@proxy-server:port'
}

response = requests.get("https://ipinfo.io/json", proxies=proxies)
print(response.json())  # 输出应为代理IP

📌 提示:免费代理质量差且不稳定,企业级应用建议采购商业代理服务。

3.2.2 验证码识别基础:OCR与第三方服务接入简介

验证码(CAPTCHA)是最直观的反机器人手段,常见形式包括:
- 图像验证码(数字/字母扭曲)
- 滑动拼图(极验、腾讯防水墙)
- 点选文字(点击‘水果’相关图片)
- 行为验证(鼠标轨迹分析)

OCR识别图像验证码(简单场景)

对于纯文本类验证码,可使用Tesseract OCR引擎配合Pillow预处理。

import pytesseract
from PIL import Image
import requests
from io import BytesIO

# 下载验证码图片
response = requests.get("https://example.com/captcha.jpg")
img = Image.open(BytesIO(response.content))

# 预处理:灰度化、二值化、降噪
img = img.convert('L')  # 灰度
img = img.point(lambda x: 0 if x < 128 else 255, '1')  # 二值化

# OCR识别
captcha_text = pytesseract.image_to_string(img, config='--psm 8 digits')
print("识别结果:", captcha_text)

参数说明:

  • --psm 8 :假设输入为单行文本
  • digits :仅识别数字,提升准确率
  • 前期图像处理至关重要,直接影响识别精度

❗ 局限性:面对复杂字体、干扰线、旋转字符时效果急剧下降。

第三方服务集成(推荐)

专业平台如阿里云、百度AI、2Captcha提供高精度验证码识别API,支持滑块、点选等多种类型。

import base64
import json
import requests

def solve_captcha(image_path):
    with open(image_path, "rb") as f:
        img_b64 = base64.b64encode(f.read()).decode()

    data = {
        "image": img_b64,
        "type": "ReCaptchaV2"
    }

    response = requests.post(
        "https://api.captcha-solver.com/solve",
        json=data,
        headers={"Authorization": "Bearer YOUR_API_KEY"}
    )

    result = response.json()
    return result.get("solution", {}).get("gRecaptchaResponse")

此类服务通常按次计费,但识别率可达90%以上,适用于关键登录环节的自动化。

3.2.3 用户代理轮换与请求指纹伪装

除了IP和频率,网站还会通过分析HTTP请求头、TLS指纹、JavaScript行为等构建“设备指纹”,识别自动化工具。

User-Agent轮换表(部分常用UA)

浏览器 User-Agent 示例
Chrome Win10 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
Firefox Mac Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0
Safari iOS Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15...

Python实现轮换:

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36..."
]

headers = {
    "User-Agent": random.choice(USER_AGENTS),
    "Accept": "text/html,application/xhtml+xml,*/*;q=0.9",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1"
}

更高级伪装:使用Playwright/Selenium模拟完整指纹

现代反爬系统(如Cloudflare、Imperva)会检测 navigator.webdriver window.chrome 等属性。Selenium默认暴露 navigator.webdriver=true ,易被发现。

可通过以下方式隐藏:

options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
    'source': '''
        Object.defineProperty(navigator, 'webdriver', {
            get: () => false
        });
    '''
})

此段代码注入JavaScript,在每个新文档加载前修改 navigator.webdriver 属性,使其返回 false ,从而绕过检测。

3.3 使用Scrapy框架提升爬取效率

当项目规模扩大,涉及多层级链接、分布式调度、数据管道处理时,手动编写requests循环已难以维护。Scrapy作为Python最强大的爬虫框架之一,提供了完整的组件化架构,极大提升了开发效率和系统稳定性。

3.3.1 Scrapy项目结构与核心组件(Spider、Item、Pipeline)

Scrapy遵循MVC思想,各模块职责分明。

创建项目

scrapy startproject tutorial
cd tutorial
scrapy genspider example example.com

生成的标准目录结构如下:

tutorial/
├── scrapy.cfg
├── tutorial/
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders/
│       └── example.py

核心组件详解

组件 作用
Spider 定义爬取起始URL、解析规则、生成Request/Item
Item 定义结构化数据字段,类似Django Model
Pipeline 数据清洗、验证、存储的处理链
Downloader Middleware 拦截请求与响应,实现代理、重试等功能
Spider Middleware 处理Spider输入输出,如修改Request元数据

示例:定义商品Item与Spider

# items.py
import scrapy

class ProductItem(scrapy.Item):
    title = scrapy.Field()
    price = scrapy.Field()
    url = scrapy.Field()
    image_url = scrapy.Field()

# spiders/example.py
import scrapy
from tutorial.items import ProductItem

class ProductSpider(scrapy.Spider):
    name = 'product_spider'
    start_urls = ['https://shop.example.com/list']

    def parse(self, response):
        for sel in response.css('.product-item'):
            item = ProductItem()
            item['title'] = sel.css('.title::text').get()
            item['price'] = sel.css('.price::text').re_first(r'\d+\.?\d*')
            item['url'] = sel.css('a::attr(href)').get()
            yield item

        # 自动跟进分页
        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

代码解析:

  • css('.title::text') 提取文本内容
  • re_first() 使用正则提取数值
  • response.follow() 自动补全相对URL并生成新请求
  • yield 同时可用于返回Item或Request,实现递归抓取

3.3.2 中间件配置:Downloader Middleware与Spider Middleware

中间件是Scrapy的扩展点,允许在请求/响应生命周期中插入自定义逻辑。

自定义随机User-Agent中间件

# middlewares.py
import random

class RandomUserAgentMiddleware:
    def __init__(self):
        self.user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)...',
        ]

    def process_request(self, request, spider):
        ua = random.choice(self.user_agents)
        request.headers['User-Agent'] = ua

启用中间件需在 settings.py 中注册:

DOWNLOADER_MIDDLEWARES = {
    'tutorial.middlewares.RandomUserAgentMiddleware': 400,
}

数字表示优先级,越小越早执行。

3.3.3 数据持久化:导出为JSON、CSV及数据库存储

Scrapy内置支持多种导出格式:

scrapy crawl product_spider -o output.json
scrapy crawl product_spider -o output.csv

存入MySQL示例(Pipeline)

# pipelines.py
import pymysql

class MySQLPipeline:
    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host='localhost',
            user='root',
            password='123456',
            database='crawl_db',
            charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = """INSERT INTO products(title, price, url) VALUES(%s, %s, %s)"""
        self.cursor.execute(sql, (item['title'], item['price'], item['url']))
        self.conn.commit()
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

启用方式:

ITEM_PIPELINES = {
    'tutorial.pipelines.MySQLPipeline': 300,
}

Scrapy通过异步非阻塞I/O模型,单机可轻松达到数千请求/分钟,配合Redis实现分布式爬虫(Scrapy-Redis),更是能满足海量数据采集需求。

4. 实战案例与数据清洗存储

4.1 综合项目:爬取电商网站商品信息

在本节中,我们将以一个典型的电商平台(如京东或模拟测试站点 http://books.toscrape.com )为目标,完成一次完整的爬虫实战流程。该项目将涵盖从目标分析、请求发送、数据提取到去重与持久化保存的全流程。

4.1.1 目标网站分析与数据字段定义

我们选择  Books to Scrape  作为教学目标网站,其结构清晰且专为爬虫练习设计。通过浏览器开发者工具(F12),观察网页HTML结构:

  • 商品列表页:每本书位于 <article class="product_pod"> 标签内
  • 详情链接: <h3><a href="catalogue/...">
  • 价格: <p class="price_color">£51.77</p>
  • 评分:通过 <p class="star-rating Three"> 中的类名判断星级
  • 库存状态: <p class="instock availability">

定义需采集的数据字段如下表所示:

字段名 类型 来源说明
title str <a> 标签的 title 属性
price float 提取 price_color 文本并转为浮点数
rating int 星级(One=1, Two=2,…)
in_stock bool 是否包含 “In stock”
category str 书籍分类(从上级目录获取)
upc str 详情页中的表格第一行
url str 商品详情页绝对URL

4.1.2 构建完整的爬虫流程:请求→解析→去重→保存

使用 requests BeautifulSoup 实现主逻辑:

import requests
from bs4 import BeautifulSoup
import time
import logging
from urllib.parse import urljoin

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

BASE_URL = "http://books.toscrape.com/"
visited_urls = set()  # 去重集合
data = []

def parse_book_detail(detail_url):
    try:
        resp = requests.get(detail_url, timeout=10)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.text, 'lxml')
        # 提取UPC
        table = soup.find("table", class_="table")
        upc = table.find_all("tr")[0].find("td").text if table else None
        return {
            "upc": upc,
            "url": detail_url
        }
    except Exception as e:
        logging.error(f"解析详情页失败 {detail_url}: {e}")
        return {}

def scrape_books_list(list_url):
    if list_url in visited_urls:
        return
    visited_urls.add(list_url)

    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
        resp = requests.get(list_url, headers=headers, timeout=10)
        resp.raise_for_status()

        soup = BeautifulSoup(resp.text, 'lxml')
        books = soup.select('article.product_pod')

        for book in books:
            title_tag = book.h3.a
            title = title_tag['title']
            price_text = book.find('p', class_='price_color').get_text()
            rating_class = book.find('p', class_='star-rating')['class'][1]  # 如 'Three'
            rating_map = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
            rating = rating_map.get(rating_class, 0)
            in_stock = 'In stock' in book.find('p', class_='instock').get_text()
            rel_url = book.h3.a['href']
            detail_url = urljoin(BASE_URL, rel_url)

            base_data = {
                'title': title,
                'price': float(price_text.replace('£', '')),
                'rating': rating,
                'in_stock': in_stock,
                'category': soup.find('ul', class_='breadcrumb').find_all('li')[2].get_text(strip=True),
                'url': detail_url
            }

            # 获取详情页补充信息
            extra_data = parse_book_detail(detail_url)
            base_data.update(extra_data)
            data.append(base_data)

        # 分页处理
        next_page = soup.select_one('li.next > a')
        if next_page:
            next_url = urljoin(list_url, next_page['href'])
            logging.info(f"发现下一页: {next_url}")
            time.sleep(1 + 0.5 * hash(next_url) % 2)  # 随机延时防反爬
            scrape_books_list(next_url)  # 递归抓取

    except requests.RequestException as e:
        logging.error(f"请求失败 {list_url}: {e}")
        time.sleep(5)  # 错误后等待重试
        if list_url not in visited_urls:  # 可再次尝试
            scrape_books_list(list_url)

4.1.3 日志记录与错误重试机制设计

上述代码中已集成基本的日志系统和异常捕获。为进一步增强稳定性,可引入 tenacity 库实现自动重试:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def safe_request(url):
    return requests.get(url, timeout=10)

同时建议将关键状态写入日志文件,并定期输出进度统计,便于监控长时间运行任务。

4.2 数据清洗与预处理

原始爬取数据常包含噪声,需进行标准化清洗。

4.2.1 去除HTML标签与空白字符规范化

虽然本例中未涉及富文本,但在真实场景中常见描述字段含HTML标签。可使用以下函数清理:

import re

def clean_html(text):
    if not text:
        return ""
    clean_text = re.sub(r'<[^>]+>', '', text)  # 移除标签
    clean_text = re.sub(r'\s+', ' ', clean_text)  # 多空格合并
    return clean_text.strip()

对价格等数值字段做前后空格去除:

df['price'] = df['price'].astype(str).str.strip().str.replace('£', '').astype(float)

4.2.2 类型转换与缺失值处理

利用 pandas 统一类型管理:

import pandas as pd

df = pd.DataFrame(data)
df['rating'] = pd.to_numeric(df['rating'], errors='coerce')
df['in_stock'] = df['in_stock'].astype(bool)
df.fillna({'upc': 'UNKNOWN'}, inplace=True)

检测并处理重复项:

duplicates = df.duplicated(subset=['title', 'upc'])
logging.info(f"发现 {duplicates.sum()} 条重复记录")
df.drop_duplicates(subset=['upc'], keep='first', inplace=True)

4.2.3 利用pandas进行结构化清洗与初步分析

执行基础数据分析:

summary = df.describe(include='all')
print(summary)

# 按类别统计平均价格
price_by_cat = df.groupby('category')['price'].mean().sort_values(ascending=False)
print(price_by_cat.head())

可视化分布情况(见后续章节)前先检查异常值:

outliers = df[(df['price'] > df['price'].quantile(0.95))]
logging.warning(f"高价值书籍候选: \n{outliers[['title', 'price']]}")

mermaid格式流程图展示数据清洗流程:

graph TD
    A[原始爬取数据] --> B{是否存在HTML标签?}
    B -- 是 --> C[正则去除标签]
    B -- 否 --> D[进入下一步]
    C --> E[空白字符规范化]
    D --> E
    E --> F[缺失值填充]
    F --> G[类型强制转换]
    G --> H[去重处理]
    H --> I[结构化DataFrame]
    I --> J[输出清洗后数据]

4.3 数据存储方案对比与选择

不同业务需求对应不同的存储方式。

4.3.1 文件存储:CSV与JSON格式适用场景

导出为 CSV(适合表格分析):

df.to_csv('books.csv', index=False, encoding='utf-8-sig')

导出为 JSON(适合嵌套结构):

df.to_json('books.json', orient='records', ensure_ascii=False, indent=2)
存储方式 优点 缺点 推荐场景
CSV 轻量、兼容Excel 不支持复杂嵌套 简单报表、导入数据库
JSON 支持层级结构 文件体积大 API接口、配置数据
Pickle 保留pandas类型 不跨语言 Python内部缓存

4.3.2 关系型数据库:MySQL/MariaDB写入实践

创建表结构:

CREATE TABLE books (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    price DECIMAL(6,2),
    rating TINYINT,
    in_stock BOOLEAN,
    category VARCHAR(100),
    upc VARCHAR(50) UNIQUE,
    url TEXT
);

使用 SQLAlchemy 写入:

from sqlalchemy import create_engine

engine = create_engine('mysql+pymysql://user:pass@localhost/scraping_db')
df.to_sql('books', engine, if_exists='append', index=False, method='multi')

注意:设置唯一约束防止重复插入。

4.3.3 NoSQL方案:MongoDB存储非结构化爬虫数据

对于字段动态变化的数据,MongoDB 更灵活:

from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017/')
db = client['scraping']
collection = db['books']

# 插入前确保唯一性
for record in data:
    collection.update_one(
        {'upc': record['upc']},
        {'$set': record},
        upsert=True
    )

该模式无需预定义 schema,适用于多源异构数据聚合。

4.4 可视化展示与后续应用展望

4.4.1 使用Matplotlib或Plotly生成数据图表

使用 Plotly 绘制交互式价格分布图:

import plotly.express as px

fig = px.histogram(df, x='price', nbins=30, title='图书价格分布')
fig.show()

fig2 = px.box(df, x='category', y='price', title='各类别价格箱线图')
fig2.show()

或使用 Matplotlib 输出静态图用于报告:

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
df['category'].value_counts().head(10).plot(kind='bar')
plt.title('Top 10 图书类别')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('top_categories.png', dpi=150)

4.4.2 将爬虫数据应用于舆情监控与市场分析

清洗后的电商数据可用于:
- 竞品分析 :监控同类商品价格波动趋势
- 用户偏好挖掘 :高评分商品特征聚类分析
- 库存预警 :长期缺货商品识别

结合 NLP 技术进一步提取评论情感倾向,构建完整的产品画像系统。

4.4.3 自动化调度:结合cron或APScheduler实现定时抓取

Linux 下使用 cron 每日凌晨执行:

0 2 * * * /usr/bin/python3 /path/to/book_scraper.py >> /var/log/scraper.log 2>&1

Python 中使用 APScheduler 实现更精细控制:

from apscheduler.schedulers.blocking import BlockingScheduler

sched = BlockingScheduler()
sched.add_job(scrape_books_list, 'interval', days=1, args=[BASE_URL])
sched.start()

支持动态启停、任务持久化等高级功能,适用于企业级部署环境。

 

Logo

更多推荐