前言
很多人一上手就问「爬虫要装哪几个库」,然后照着某篇清单把 requests、BeautifulSoup、lxml、Scrapy、selenium 全装上,结果一个也没用明白。库不是越多越好,关键是搞清楚每一类库解决的是哪一步的问题:有的负责发请求,有的负责解析 HTML,有的负责调度和去重,有的专门对付 JavaScript 渲染的页面。
另一个常见误解是把标准库当成「玩具」。事实上urllib.request能发完整的 HTTP 请求,html.parser能可靠地遍历标签,urllib.robotparser能解析 robots.txt,json、csv、sqlite3、re、gzip、concurrent.futures覆盖了存取、清洗和并发。一个不需要安装任何依赖的爬虫,是真实可用的。
需要提前声明:本文涉及第三方库时,只讲它们在流程里的定位和大致用法,具体参数名和返回类型请以各库官方文档为准。因为环境里没有安装这些库,任何具体到参数名的调用都不应被当作可直接照抄的成品。另外,Python 2 已于 2020 年 1 月 1 日停止维护,本文一律用 Python 3 写法。
一、标准库:一行安装命令都不需要
标准库适合「抓取量不大、页面是服务端渲染、能自己控制流程」的场景。它最大的价值是零依赖、可移植、行为稳定。
# 适用于 Python 3.8+
import urllib.request
import urllib.parse
import urllib.error
import urllib.robotparser
import http.client
import gzip
import json
import csv
import sqlite3
import re
import time
import concurrent.futures
print("标准库导入完成,无需 pip 安装")| 模块 | 负责的环节 | 典型用途 |
|---|
urllib.request | 发请求 | urlopen、Request设置请求头 |
urllib.parse | URL 处理 | urljoin拼相对地址、urlencode编码查询串 |
urllib.error | 异常 | 区分HTTPError(带状态码)与URLError |
urllib.robotparser | 合规 | 解析 robots.txt,can_fetch判断 |
http.client | 底层 HTTP | 需要精细控制连接时用 |
html.parser | 解析 HTML | 事件驱动,覆写handle_starttag等回调 |
json/csv | 存数据 | 读写 JSON、CSV |
sqlite3 | 存数据 | 单文件数据库,天然适合去重和增量 |
re | 文本清洗 | 从已取出的文本里抠日期、数字 |
gzip | 解压 | 处理Content-Encoding: gzip的响应 |
concurrent.futures | 并发 | 线程池处理 I/O 密集抓取 |
注意re的位置:它适合清洗已经抽出来的纯文本,不适合直接套在整页 HTML 上。HTML 的嵌套和属性顺序不是正则能稳定描述的。
二、第三方 HTTP 客户端:requests / httpx / aiohttp
标准库的urllib.request能用,但接口偏底层:编码要自己解、会话(连接复用)要自己配、超时和重试要自己写。这类库的价值就是把这些样板收起来。
- requests:同步、接口直观,是事实上的入门首选。它的定位是「给人用的 HTTP 客户端」,会话复用用
Session,请求参数、表单、JSON 都有对应入口。具体参数以 requests 官方文档为准。 - httpx:接口风格接近 requests,同时提供同步和异步两套 API,适合既要简单又要异步的场景。
- aiohttp:
asyncio生态里的 HTTP 客户端,异步抓取大量页面时常用。它与asyncio事件循环绑定,写法与同步库差别较大。
这里只给出定位,不给出具体调用——因为记不准参数名而写一个「看起来对」的签名,比不写更危险。需要时请查对应库的官方文档。
三、解析库:BeautifulSoup / lxml / html.parser
解析层解决的是「从 HTML 里把字段抠出来」。选择取决于页面是否规整、你更习惯 CSS 选择器还是 XPath。
- html.parser:标准库,容错性尚可,事件驱动。适合结构简单或需要边下边解析的场景。
- BeautifulSoup:在某个底层解析器之上提供统一的树形接口,容错性强,对残缺 HTML 友好。用 CSS 选择器取元素很方便。以 BeautifulSoup 官方文档为准。
- lxml:底层用 libxml2,解析快、支持 XPath。缺点是它是 C 扩展,安装依赖较多。
- 浏览器自动化(selenium / playwright):页面内容由 JavaScript 动态渲染时才需要。代价是慢、重,只在确实必要(且目标站允许)时使用。
一个务实的原则:能用服务端渲染的静态 HTML 解决的,就不要上浏览器自动化。
四、工程化框架:Scrapy
当抓取规模变大,你要处理的就不只是「抓一个页面」,而是调度、去重、限速、失败重试、数据管道。这类需求由爬虫框架承担。
Scrapy是最常见的 Python 爬虫框架,把「引擎、调度器、下载器、爬虫、管道」拆成独立组件,用Request/Response和回调(callback)组织解析流程。适合规则明确、站点多、需要长期运行的采集任务;如果只是抓几十个页面,用它反而是过度设计。
调度与存储层面还会用到 Redis、消息队列、关系型数据库等通用组件,它们不是爬虫专用库,这里只提定位。
实战:只用标准库搭一个抓取小工具
下面这段不依赖任何第三方库,演示「请求—解码—解析—存库」这条链路的骨架。把数据落到 sqlite3,顺便用主键天然去重。
# 适用于 Python 3.8+
import sqlite3
import time
import urllib.error
import urllib.request
from html.parser import HTMLParser
class PageTitleScanner(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.in_title = False
self.title_parts = []
def handle_starttag(self, tag, attrs):
if tag == "title":
self.in_title = True
def handle_endtag(self, tag):
if tag == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.title_parts.append(data)
def title(self):
return "".join(self.title_parts).strip()
def fetch_title(url, timeout=10):
req = urllib.request.Request(url, headers={
"User-Agent": "LibSummaryBot/0.1 (contact: me@example.com)",
})
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
charset = resp.headers.get_content_charset() or "utf-8"
text = raw.decode(charset, errors="replace")
parser = PageTitleScanner()
parser.feed(text)
parser.close()
return parser.title()
def main():
urls = ["https://www.example.com/", "https://www.example.org/"]
conn = sqlite3.connect("titles.db")
conn.execute(
"CREATE TABLE IF NOT EXISTS pages ("
"url TEXT PRIMARY KEY, title TEXT)"
)
for url in urls:
try:
title = fetch_title(url)
except urllib.error.HTTPError as e:
print("HTTP 错误", e.code, url)
continue
except urllib.error.URLError as e:
print("网络错误", e.reason, url)
continue
conn.execute(
"INSERT OR REPLACE INTO pages (url, title) VALUES (?, ?)",
(url, title),
)
conn.commit()
print("已保存:", url, "->", title)
time.sleep(1) # 限速
conn.close()
if __name__ == "__main__":
main()INSERT OR REPLACE配合url主键,同一地址重复抓到也只保留一条,比手工维护去重集合更省事。
常见坑点
- 先装库再想需求
❌ 一次性装上 requests、Scrapy、selenium,却不知道该用哪层。 ✅ 先明确页面是否需要 JS 渲染、规模多大,再按「请求—解析—调度」分层选库。
- 把
re当成 HTML 解析器
❌re.findall(r'<a href="(.*?)">', html)—— 属性顺序、单双引号、换行一变就漏。 ✅ 用html.parser、BeautifulSoup 或 lxml 做结构解析,re只清洗抽出的纯文本。
- 用第三方库却硬记参数名
❌ 凭印象写一个「看起来合理」的调用,参数名或顺序其实是错的。 ✅ 记不准就只写用法思路,以该库官方文档为准。
html.parser子类忘记调用父类构造
❌def __init__(self): self.links = []—— 不解构内部状态,feed()会出问题。 ✅ 先super().__init__(convert_charrefs=True),再初始化自己的属性。
- 硬编码 UTF-8 解码第三方库返回的内容
❌ 拿到 bytes 就.decode("utf-8"),GBK 页面直接乱码。 ✅ 先取响应头的 charset,取不到再退回 UTF-8 并加errors="replace"。
- 忽略 gzip 压缩
❌ 直接解码响应体,得到一堆二进制乱码。 ✅ 检查Content-Encoding,用gzip模块解压后再解码。
- 把
iteritems()/xrange当成还能用的写法
❌for k, v in d.iteritems():、for i in xrange(10):—— Python 3 均已移除。 ✅ 用d.items()(返回视图)、range(本身就是惰性的)。
- 上多线程就以为能提速
❌ CPU 密集任务用ThreadPoolExecutor指望加速。 ✅ 爬虫主要受 I/O 限制,线程池有意义;CPU 密集要换多进程。
总结
| 层次 | 标准库方案 | 第三方方案 | 何时升级 |
|---|
| 发请求 | urllib.request | requests / httpx / aiohttp | 需要会话、重试、异步时 |
| 解析 HTML | html.parser | BeautifulSoup / lxml | 页面复杂、要 XPath/CSS 时 |
| 渲染 JS | 无 | selenium / playwright | 内容确实由 JS 生成时 |
| 调度工程化 | 自己写循环 | Scrapy | 站点多、需长期运行时 |
| 存储 | sqlite3/json/csv | 数据库、消息队列 | 数据量大、要并发写时 |
选库的顺序应该是「先让标准库跑通,再按瓶颈补库」。绝大多数入门任务,标准库就够了;真正需要第三方库的时候,你已经能说清自己缺的是哪一步。