python爬虫基本知识

前言


「爬虫基本知识」这个词很容易被理解偏。有人把它理解成一堆 API 的集合,背下来就能写;也有人把它理解成一套「反反爬技巧」。这两者都不对。真正的基本知识是网络与文本的常识:URL 是怎么组成的、HTTP 请求和响应各自长什么样、为什么会有乱码、Cookie 是用来干什么的、robots.txt 是给谁看的。这些常识不打牢,API 背得再多也只是在试错。


还要纠正一个流传很广的说法:「爬虫就是让程序伪装成浏览器」。伪装浏览器 UA 只是绕过服务器「拒绝非浏览器访问」判断的一种做法,而这件事本身未必合规。更正当的思路是:如实标明自己的身份(比如带联系方式的 UA),遵守目标站点的 robots.txt 与使用条款,控制请求频率。本文在这一前提下讲基础。


本文只使用 Python 标准库,所有代码在 Python 3 下可直接运行。Python 2.7 已于 2020 年 1 月 1 日停止维护,老教程里的print语句、dict.iteritems()、xrange都要按 Python 3 的写法重写。


一、URL 不只是「一串网址」


URL(Uniform Resource Locator,统一资源定位符)由若干段组成,urllib.parse.urlsplit会把它们拆开:




组成部分英文名例子常见错误



协议schemehttps写成http导致明文或 301 跳转

主机netlocshop.example.com漏掉端口

路径path/search与域名之间少了斜杠

查询串queryq=python&page=2参数没做百分号编码

片段fragmenttop误以为片段会发给服务器



最后一条要特别记住:片段(fragment,也就是#后面那段)不会发送给服务器。它是给浏览器自己定位用的。所以一个带#的地址,服务器看到的和你不带#时是一样的。


查询串里的中文、空格、&、=都必须经过百分号编码(percent-encoding,俗称 URL 编码)才能安全地放进 URL。手工用+或&拼字符串是最常见的错误来源;标准库的urlencode就是干这个的。


二、请求与响应的常识


HTTP 的请求方法里,采集最常用的是 GET,提交表单用 POST。POST 的请求体要放在data参数里,且必须是字节串,内容是application/x-www-form-urlencoded格式——urlencode生成的是字符串,要再.encode()一次。


响应里最需要看懂的是状态码:




状态码含义采集时该怎么处理



200成功正常解析

301 / 302永久 / 临时跳转交给库自动跟随,注意最终 URL 变了

403拒绝访问停止,检查自己的请求是否越界

404不存在放弃这条,不要重试

429请求过多退避后重试,并降低频率

5xx服务端错误值得退避重试,但不是你的问题



urllib.request.urlopen遇到 4xx / 5xx 会抛urllib.error.HTTPError(它是URLError的子类,同时也是类文件对象)。所以「拿到 403」在代码里表现为异常,而不是一个返回值为None的响应——这点常被忽略。


请求头里常打交道的几个:User-Agent(身份)、Referer(来源页)、Cookie(会话)、Accept-Language(语言偏好)。构造Request时用headers={"键": "值"},等价于逐个调用add_header(key, val)。


三、编码:乱码只有一个根源


响应体是字节串,不是字符串。乱码的唯一根源是「用了错误的字符集去解码这段字节」。正确流程是:先看响应头Content-Type里有没有charset=...,有就用它;没有才回退到utf-8。HTTPMessage对象提供了get_content_charset(failobj=None)做这件事,返回字符集名或你给的默认值。


另一个容易踩的点是压缩。urllib 默认不发Accept-Encoding头,所以服务器通常返回未压缩内容;但只要你手动加了Accept-Encoding: gzip,就必须自己判断Content-Encoding是不是gzip,是则用gzip.decompress()解压,否则拿到的字节串是压缩过的二进制,解码必然乱码。


写文件时同理:open()不写encoding就会用平台默认编码,Windows 上可能是 GBK,跨平台读回来就乱。读写文本文件一律显式写encoding="utf-8"。


四、抓到的可能是三种东西


不是所有页面内容都藏在 HTML 里:



  • HTML:静态渲染的页面,用解析器抽取。标准库有事件式的html.parser.HTMLParser;选择器式的方案多在第三方库,具体 API 以各库官方文档为准。

  • JSON:很多页面通过接口异步取数据,返回的是 JSON 文本。用json.loads(text)解析成字典/列表即可,前提是这个接口是公开允许调用的。不要为了拿到数据去逆向破解签名或绕过风控。

  • CSV / 纯文本:下载链接常常直接给一个文本文件,urllib.request.urlretrieve(url, filename)可以把远程文件直接落盘,它返回(本地文件名, 响应头)二元组。


五、Cookie 与会话


HTTP 是无状态的,服务器靠 Cookie 记住「你是谁」。要跨请求保持同一个会话,就得有一个 Cookie 容器加一个装了 Cookie 处理器的 opener。http.cookiejar.CookieJar()是内存容器,urllib.request.HTTPCookieProcessor(jar)负责在收发请求时自动读写它,两者通过build_opener()组装成 opener。


需要说明:会话保持本身是中性的技术,但用它去「维持登录态、绕过登录墙」不是本文的范围,本文只讲它在正常访问公开页面时的作用(比如承接站点自动下发的会话标识)。


实战:拆解 URL 与维持会话


先看 URL 拆解与参数编码:


# 适用于 Python 3.8+
from urllib.parse import parse_qs, quote, urlencode, urlsplit

url = "https://shop.example.com/search?q=python 教程&page=2#top"
parts = urlsplit(url)
print(parts.scheme) # https
print(parts.netloc) # shop.example.com
print(parts.path) # /search
print(parts.query) # q=python 教程&page=2
print(parts.fragment) # top

print(parse_qs(parts.query))
# {'q': ['python 教程'], 'page': ['2']}

print(urlencode({"q": "python 教程", "page": 2}))
# q=python+%E6%95%99%E7%A8%8B&page=2

print(quote("a b/c"))
# a%20b/c —— 默认 safe='/',斜杠不被编码

再看不带第三方库的会话维持:


# 适用于 Python 3.8+
import http.cookiejar
import urllib.request

UA = "cookie-session-bot/0.1 (contact: basics@example.net)"

jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))


def get(url):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with opener.open(req, timeout=15) as resp:
charset = resp.headers.get_content_charset() or "utf-8"
return resp.read().decode(charset, errors="replace")


html = get("https://quotes.toscrape.com/")
print("页面长度:", len(html))
print("本次会话保存的 cookie 数:", len(jar))
for cookie in jar:
print(cookie.name, "->", cookie.domain)

第二段代码里,opener.open()的行为和urlopen()一致,区别是它会自动通过HTTPCookieProcessor处理 Set-Cookie 与后续请求的 Cookie 头。CookieJar可以迭代,每一项是一个http.cookiejar.Cookie,有name、value、domain、path等属性。


常见坑点


1. 查询参数手工拼字符串


❌url = base + "?q=" + keyword—— 关键词含空格、&、中文时 URL 结构就被破坏。 ✅urlencode({"q": keyword}),需要拼到已有查询串上时先parse_qs再合并。


2.data传了字符串而不是字节串


❌urlopen(req, data="a=1")—— POST 的data必须是字节串。 ✅urlopen(req, data=urlencode({"a": 1}).encode("utf-8"))。


3. 把 403 当成「没数据」静默跳过


❌ 用except Exception: pass把拒绝访问也吞掉,然后继续按原频率抓。 ✅HTTPError要按code分类处理,403 应当停下来反思,429 应当退避。


4. 直接对resp.read()做字符串操作


❌len(resp.read())之后又说「怎么字符数不对」 —— 字节数不等于字符数。 ✅ 先decode再数;len(b"中文".decode("utf-8"))才是 2 个字符。


5. 忘了urlopen返回的是上下文管理器


❌resp = urlopen(req)之后一直不关闭,连接资源迟迟不释放。 ✅with urlopen(req, timeout=15) as resp:。


6. 手动加了Accept-Encoding: gzip却不解压


❌ 加了压缩头就直接decode,得到乱码。 ✅ 判断Content-Encoding == "gzip"后gzip.decompress()。


7. 用 Python 2 的写法处理编码


❌text = resp.read().decode("utf-8").encode("gbk")再print text—— Python 2 的print是语句,Python 3 里必须print(...);unicode/basestring类型也已统一为str。 ✅ 全程用str表示文本、bytes表示字节,并在边界处显式编解码。


总结




基本知识一句话常见误区



URL 结构片段不发给服务器以为#后面会影响抓取结果

百分号编码中文/空格/符号必须编码手工拼查询串

状态码4xx/5xx 在 urllib 里是异常以为会返回一个空响应

编码响应体是字节,要按 charset 解码一律decode("utf-8")

压缩自己加了Accept-Encoding就得自己解压解压步骤漏掉

数据格式HTML / JSON / CSV 三种常见形态以为数据一定在 HTML 里

会话CookieJar + HTTPCookieProcessor手工拼Cookie头

合规先看 robots.txt 与条款把「伪装浏览器」当基本功



这些基础知识点看起来零散,其实都围绕一句话:HTTP 传的是字节,服务端认的是头信息,你要做的是把字节按正确的方式还原成数据。把这句想通,乱码、403、参数丢失这些「玄学问题」大多能找到明确的原因。动手采集前,先花五分钟读目标站的 robots.txt。