前言学爬虫最怕的是「知识点是散的」今天记住urlopen的用法明天背会一个解析器的写法结果遇到一个新站点还是不知道从哪儿下手。这篇总结的目的就是把散落的知识点挂到一张地图上让你随时能回答「我现在卡在这一层的哪一步」。有一件事要先纠正不少教程把爬虫的知识点分成「基础篇」和「反爬篇」好像学会反爬才算完整。这个划分是错的。爬虫的知识点只分四层——请求、解析、调度、存储合规不是第五层而是贯穿四层的前提。至于「反爬」多数情况下它对应的正确结论是「对方不允许那就不抓」而不是「想个办法绕过去」。本文不涉及任何绕过登录墙、付费墙或风控的内容。本文所有代码以 Python 3 为基准只用标准库。Python 2.7 已于 2020 年 1 月 1 日停止维护print语句、dict.iteritems()、xrange、urllib2这些老写法现在都不能用文中会在对照表里一并点明。一、知识地图层次你要回答的问题常用标准库常见坑请求东西怎么拿下来urllib.request没设 timeout、字节没解码解析数据从哪儿抠html.parser嵌套结构提前收尾调度下一步抓哪儿、抓多少collections.dequeset环状链接无限抓存储数据怎么落盘json/csv编码没写、CSV 多空行合规允不允许抓urllib.robotparser跳过检查直接抓把这张表记住比记二十个 API 有用遇到任何问题先定位它在哪一层。二、请求层要点urllib.request.urlopen(url, dataNone, [timeout, ]*, contextNone)data不为None时变成 POST且必须是字节串。urllib.request.Request(url, dataNone, headers{}, origin_req_hostNone, unverifiableFalse, methodNone)headers等价于逐个add_header(key, val)。设 UA 的目的是表明身份不是伪装。响应对象status整数、headersHTTPMessage可.get()、read()返回字节串、url。它是上下文管理器用with自动释放。字符集resp.headers.get_content_charset(failobjNone)读不到再回退utf-8。压缩自己加了Accept-Encoding: gzip就要自己判断Content-Encoding并用gzip.decompress()。异常urllib.error.URLError是网络错误基类urllib.error.HTTPError是它的子类、代表错误状态码同时是类文件对象有code、reason。异常分类处理404 放弃、403 停止、429 退避、网络错误退避重试。三、解析层要点html.parser.HTMLParser(*, convert_charrefsTrue, scriptingFalse)事件式解析。convert_charrefs从 3.5 起默认True会帮你把字符引用转成对应字符script/style内的除外。三个回调handle_starttag(tag, attrs)、handle_data(data)、handle_endtag(tag)另外还有handle_startendtag(tag, attrs)、handle_comment(data)、handle_decl(decl)。attrs是「(名字, 值) 二元组」的列表不是字典。feed(data)之后一定要close()。官方文档明确它不检查结束标签是否与开始标签匹配也不会为「被外层闭合隐式关闭」的元素调用结束回调。所以嵌套结构要自己用计数器处理。对结构复杂的页面选择器式解析更省事那属于第三方库具体 API 以各库官方文档为准。四、调度与并发要点队列用collections.deque两端进出都是 O(1)去重用set。终止条件要双重既看有没有下一页也设页数/条数硬上限。相对链接用urllib.parse.urljoin(base, href)绝对化去掉片段用urldefrag()。爬虫是 I/O 密集任务。CPython 有全局解释器锁GILGlobal Interpreter Lock同进程内同一时刻只有一个线程执行字节码但等待 I/O 时会释放 GIL所以少量多线程有效CPU 密集要用多进程或 C 扩展。Python 3.13 起有实验性自由线程no-GIL构建但不是默认构建不要说「Python 已经没有 GIL 了」。并发要配限速并发上限保持很小 全局最小请求间隔。不要为了速度去压一个小站点。五、存储与清洗要点文本文件一律显式encodingutf-8。CSV 必须同时newline否则 Windows 上每行之间多一个空行csv文档要求。json.dump(..., ensure_asciiFalse)让中文以汉字写入而非\uXXXX转义。长文本拼接用.join(parts)不要在循环里s s piece。边抓边追加写别把几万条全堆在内存里。六、合规清单写在代码第一行读robots.txtRobotFileParser的set_url→read→can_fetch返回False就不抓。读不到 robots.txt 时按「不允许」处理稍后重试。UA 里写清程序名和联系方式。每次请求之间time.sleep并设总量上限。需要登录、付费或 robots 明令禁止的内容不抓、不绕。遵守著作权与用户协议不把抓来的内容原样再发布。七、Python 2 与版本差异对照老写法Python 2现行写法Python 3说明print htmlprint(html)3 里print是函数urllib2.urlopenurllib.request.urlopen模块在 3 里重组过d.iteritems()d.items()3 里返回视图iteritems已移除xrange(n)range(n)3 的range本身就是惰性的unicode/basestringstr3 里文本统一为str3 / 2 13 / 2 1.5整除写//除法语义变了版本内部的变化也要知道字典从 3.7 起保证插入顺序dict.keys()返回视图而非列表asyncio.get_event_loop()在 3.12 起没有运行中的事件循环时会发DeprecationWarning3.14 起直接抛RuntimeError——新代码应当用asyncio.run()或asyncio.get_running_loop()。实战把知识点串成一个小脚本# 适用于 Python 3.8import csvimport jsonimport timeimport urllib.parseimport urllib.requestimport urllib.robotparserfrom html.parser import HTMLParserUA tutorial-sampler/0.2 (contact: notesexample.org)DELAY 1.0URL https://quotes.toscrape.com/class TextSpanParser(HTMLParser):收集所有 class 含 text 的 span 的文字。def __init__(self):super().__init__()self._in Falseself._buf []self.texts []def handle_starttag(self, tag, attrs):if tag span and text in dict(attrs).get(class, ).split():self._in Trueself._buf []def handle_data(self, data):if self._in:self._buf.append(data)def handle_endtag(self, tag):if tag span and self._in:self._in Falseself.texts.append(.join(self._buf).strip())def fetch_text(url, timeout15):req urllib.request.Request(url, headers{User-Agent: UA})with urllib.request.urlopen(req, timeouttimeout) as resp:charset resp.headers.get_content_charset() or utf-8return resp.read().decode(charset, errorsreplace)def robots_allowed(url):parts urllib.parse.urlsplit(url)robots_url urllib.parse.urlunsplit((parts.scheme, parts.netloc, /robots.txt, , ))parser urllib.robotparser.RobotFileParser()parser.set_url(robots_url)try:parser.read()except Exception:return Falsereturn parser.can_fetch(UA, url)def save(records):with open(quotes.json, w, encodingutf-8) as f:json.dump(records, f, ensure_asciiFalse, indent2)with open(quotes.csv, w, encodingutf-8, newline) as f:writer csv.writer(f)writer.writerow([index, text])for i, rec in enumerate(records, 1):writer.writerow([i, rec[text]])def main():if not robots_allowed(URL):print(robots.txt 不允许抓取退出)returnparser TextSpanParser()parser.feed(fetch_text(URL))parser.close()records [{text: t} for t in parser.texts if t]save(records)print(已保存, len(records), 条)time.sleep(DELAY)if __name__ __main__:main()四十几行代码把请求、解析、限速、合规、存储全部串了一遍。把它当作模板换掉 URL、换掉解析器、换掉落盘格式就是另一个采集任务。常见坑点1. 知识点记住了顺序记反了❌ 先想怎么写并发和解析最后才想起来看 robots.txt。 ✅ 顺序永远是合规 → 请求 → 解析 → 调度 → 存储。2. 把「绕过反爬」当必修课❌ 认为学会验证码识别、指纹伪装才算入门。 ✅ 遇到需要登录/付费/robots 禁止的场景正确结论是不抓。3.convert_charrefs的默认值记错❌ 以为默认是False还自己手工处理字符引用。 ✅ 从 3.5 起默认True大部分实体字符已经帮你转好了。4. 把attrs当字典❌attrs[class]直接下标。 ✅dict(attrs).get(class, )。5. 忘了close()却怪解析器漏数据❌ 只feed()就取值。 ✅feed()后补close()。6. 只设「有没有下一页」这一个终止条件❌ 跟着 next 链接走到天荒地老。 ✅ 再加一个页数/条数硬上限。7. CSV 没写newline❌open(out.csv, w, encodingutf-8)。 ✅open(out.csv, w, encodingutf-8, newline)。8. 把 GIL 说反❌ 「多线程能让 CPU 密集的爬虫变快」。 ✅ 多线程对 I/O 密集有效等 I/O 时释放 GILCPU 密集要用多进程。总结你记住的这句它对应哪一层响应体是字节要按 charset 解码请求attrs是列表feed后要close解析队列 去重 硬上限调度显式编码CSV 加newline存储先can_fetch不允许就不抓合规多线程治 I/O多进程治 CPU并发爬虫的知识点不多难的是把它们组织成一张随时能查的地图。请求、解析、调度、存储四层加上贯穿其中的合规前提就是全部。真正拉开差距的不是 API 记得多而是边界处理得好不好异常怎么分类、循环怎么终止、字段取不到怎么办、什么时候该停手。最后再强调一次没有例外的规则——动手之前先读目标站的 robots.txt。