☰
Python爬虫技术
2026/10/17 15:15:33 网站建设 项目流程

前言


「爬虫技术」听起来像一个独立学科,拆开看其实就是三件事:发出 HTTP 请求拿到响应、从响应里抽出结构化数据、把数据存下来并决定下一次抓哪里。Python 之所以成了爬虫的主流语言,不是语言本身有什么魔法,而是标准库把这三件事的零件都备齐了:urllib.request负责发请求,urllib.parse负责拼 URL 与编码参数,html.parser负责解析 HTML,json与csv负责落盘,urllib.robotparser甚至帮你读 robots.txt。第三方库(requests、BeautifulSoup、lxml、Scrapy、aiohttp 等)只是把这些零件包装得更顺手。


最常见的误解有两个。第一个是「爬虫就是写个for循环,只要不被封 IP 就行」——把工程问题当成了对抗问题。实际上大多数抓取失败的原因不是被封,而是编码没处理对、翻页逻辑写错、把None当成字符串拼接。第二个是「数据在网页上显示出来,就说明可以抓」——法律与服务条款不这么认为。动手前先看目标站的 robots.txt 与使用条款,遵守它,给对方留出带宽,这是本文反复强调的前提。


本文讲的是通用技术骨架,示例只用 Python 标准库,让你在没装任何第三方包的环境里也能把流程跑通。所有代码以 Python 3 为基准:Python 2.7 已于 2020 年 1 月 1 日停止维护,老教程里的print语句、dict.iteritems()、xrange现在都跑不起来,本文一律用 Python 3 写法。


一、请求层:一次 HTTP GET 到底发生了什么


用urllib.request发一个请求,最短的写法是urllib.request.urlopen(url)。它的签名是urlopen(url, data=None, [timeout, ]*, context=None):url既可以是字符串,也可以是一个Request对象;data不为None时请求方法变为 POST,且必须是已经编码好的字节串;timeout是位置参数,单位秒。


只要你不是只抓一次给自己看,就应该用Request对象,因为要设置 User-Agent(用户代理)。Request的签名是Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None),headers里的每一项都等价于调用一次add_header(key, val)。设置 UA 的目的不是伪装成浏览器去骗过服务器,而是让服务器能从日志里认出你、找得到你、并决定是否给你开白名单。urllib 若不设置,默认会把自己标成Python-urllib/3.x这样的字符串。


拿到响应后,几个高频属性要认清:resp.status是整数状态码,resp.headers是http.client.HTTPMessage对象(可以像字典一样.get("Content-Type")取头),resp.read()返回字节串而不是字符串。字节串要解码成str才能做文本处理,而解码用的字符集应该从响应头里的Content-Type取,而不是想当然写utf-8。


二、编码层:乱码的根源


「爬下来的中文是乱码」几乎总是同一个原因:把字节串按错误的字符集解码。正确做法是从Content-Type头里读 charset,读不到再回退到utf-8。HTTPMessage继承自email.message.Message,提供了get_content_charset(failobj=None)方法,专门干这件事。


另一个容易忽略的点是响应可能是压缩的。urllib默认不会发送Accept-Encoding头,所以服务器通常不压缩;但如果你手动加了Accept-Encoding: gzip,就必须自己用gzip.decompress()解压resp.read()的结果,否则拿到的是二进制乱码。官方文档给出的就是这个模式:先看Content-Encoding是不是gzip,是则解压。


三、解析层:标准库能做到什么程度


html.parser.HTMLParser是一个基于事件的解析器:你继承它,重写handle_starttag(tag, attrs)、handle_endtag(tag)、handle_data(data)等方法,然后调用feed(text)把文档喂进去,最后close()收尾。attrs是「(名字, 值) 二元组」组成的列表,注意是列表不是字典,同一个属性理论上可以出现多次。


它的定位要摆正:HTMLParser只做词法层面的切分,不做容错修复,官方文档明确说它不检查结束标签是否与开始标签匹配。所以拿它写「某类新闻页正文抽取」可行,拿它当通用浏览器级 DOM 解析器会失望。要更方便的选择器语法,才轮到第三方库出场——但具体 API 请以各库官方文档为准,本文不给出记不准的调用。




环节标准库模块职责



发请求urllib.request构造 Request、发出去、拿响应

拼 URL / 编码参数urllib.parseurlencode、quote、urljoin

读 robots.txturllib.robotparserRobotFileParser判断某 URL 能不能抓

管理 Cookiehttp.cookiejar+HTTPCookieProcessor维持会话

解析 HTMLhtml.parser事件式解析,提取数据

解压gzip处理 gzip 压缩的响应体

落盘json、csv结构化存储



四、合规层:先问能不能抓,再问怎么抓


urllib.robotparser.RobotFileParser(url='')是标准库里的 robots.txt 解析器。用法是:set_url(robots_url)设定地址,read()抓取并解析,然后can_fetch(useragent, url)返回布尔值。它还有crawl_delay(useragent)(返回 Crawl-delay 的秒数或None)和site_maps()等方法。


有一个容易搞错的细节:服务器上根本没有 robots.txt 文件,和服务器拒绝提供 robots.txt,是两回事。标准库内部的read()已经按 RFC 9309 区分了这些情况——4xx(401、403 除外)表示「没有规则」,结果是允许访问一切;401、403 表示「整站禁止」;5xx 表示「服务器出错」,结果是必须假定完全禁止。真正会让read()抛异常的是网络层错误,你的代码遇到它时按保守策略当作「不允许」是稳妥的。此外,无论 robots.txt 怎么写,都应该在两次请求之间留出间隔(time.sleep),并且不要用能瞬间打满对方带宽的并发去压一个小站。


实战:一个纯标准库的最小爬虫


下面这个例子抓的是quotes.toscrape.com——它是专门为练习爬虫搭建的站点,页面结构稳定且明确用于教学,比拿真实商业网站开刀合适得多。


# 适用于 Python 3.8+
import gzip
import time
import urllib.parse
import urllib.request
import urllib.robotparser
from html.parser import HTMLParser

UA = "stdlib-tech-demo/0.1 (contact: tech@example.com)"
DELAY = 1.0 # 两次请求之间的间隔(秒)


class QuoteTextExtractor(HTMLParser):
"""从列表页里抽出每条名言的正文。"""

def __init__(self):
super().__init__()
self._in_quote = False
self._depth = 0 # 卡片内 div 的嵌套深度
self._in_text = False
self._buf = []
self.quotes = []

def handle_starttag(self, tag, attrs):
classes = dict(attrs).get("class", "").split()
if tag == "div":
if not self._in_quote and "quote" in classes:
self._in_quote = True
self._depth = 1
elif self._in_quote:
self._depth += 1
elif tag == "span" and "text" in classes and self._in_quote:
self._in_text = True
self._buf = []

def handle_data(self, data):
if self._in_text:
self._buf.append(data)

def handle_endtag(self, tag):
if tag == "span" and self._in_text:
self._in_text = False
self.quotes.append("".join(self._buf).strip())
elif tag == "div" and self._in_quote:
self._depth -= 1
if self._depth == 0:
self._in_quote = False


def fetch(url):
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read()
if resp.headers.get("Content-Encoding") == "gzip":
raw = gzip.decompress(raw)
charset = resp.headers.get_content_charset() or "utf-8"
return raw.decode(charset, errors="replace")


def robots_allow(url, user_agent):
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 False # 保守策略:读不到就当不允许,稍后重试
return parser.can_fetch(user_agent, url)


def main():
index = "https://quotes.toscrape.com/"
if not robots_allow(index, UA):
print("robots.txt 不允许抓取,退出")
return

html = fetch(index)
parser = QuoteTextExtractor()
parser.feed(html)
parser.close()

for i, quote in enumerate(parser.quotes[:3], 1):
print(i, quote[:40])

time.sleep(DELAY) # 下一次请求前先等一会儿


if __name__ == "__main__":
main()

这段代码把前面四层都串了一遍:先问 robots 能不能抓,再发请求,按 charset 解码,用事件解析器抽数据,最后 sleep 限速。它没有翻页、没有并发、没有存储,因为那些是后续要补的工程细节;这里只要保证骨架正确。


常见坑点


1. 把resp.read()的返回值直接当字符串用


❌html = resp.read(); if "关键词" in html:—— 字节串里不能直接和str比较,要么TypeError,要么判断永远为假。 ✅html = resp.read().decode("utf-8"),或者按Content-Type里的 charset 解码。


2. 忽略Content-Encoding直接 decode


❌ 手动加了Accept-Encoding: gzip,然后对resp.read()直接 decode,得到一屏乱码。 ✅ 先判断resp.headers.get("Content-Encoding") == "gzip",是则gzip.decompress()再解码。


3.HTMLParser.attrs当字典用


❌self.attrs["class"]—— 它其实是「(名字, 值) 元组」的列表,下标是整数,TypeError。 ✅attrs = dict(attrs)之后再.get("class", "");注意一个属性名可能重复出现。


4. 忘了调用feed()之后还要close()


❌ 只parser.feed(html)就取结果 —— 文档末尾若有未闭合的结构,数据还缓冲在内部没吐出来。 ✅parser.feed(html)之后补一句parser.close()。


5.time.sleep写成了sleep


❌sleep(1)——sleep不在内置命名空间里,直接NameError。 ✅import time后写time.sleep(1),或from time import sleep再sleep(1)。


6. 抓不到 robots.txt 就默认「随便抓」


❌except Exception: pass之后继续猛抓目标站。 ✅ 取不到时按保守策略暂停,并在日志里记下来,下次重试成功再继续。


7. 用 Python 2 的写法解码


❌print html、for k, v in d.iteritems()—— Python 2.7 已于 2020 年 1 月 1 日停止维护,print语句、iteritems、xrange在 Python 3 里全部不可用。 ✅print(html)、d.items()、range(...)。


总结




你要做的事标准库方案关键点



发请求urllib.request.urlopen/Request设 UA、设 timeout

编码参数urllib.parse.urlencode/quotedata必须是 bytes

判断能否抓urllib.robotparser.RobotFileParser先set_url再read再can_fetch

解析 HTMLhtml.parser.HTMLParser事件式,不修复标签

处理乱码get_content_charset()回退utf-8

限速time.sleep(seconds, /)两次请求之间留间隔

落盘json/csv文本文件记得指定encoding



爬虫技术的地基就是「请求—解析—存储」这三块,标准库已经把零件给齐,把每一块的边界和错误处理弄清楚,比急着上手第三方框架更划算。最后一句仍然是老话:先读目标站的 robots.txt 与条款,再动手写代码。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询