资讯动态

python爬虫实例详解

发布时间:2026/10/16 19:18:40 来源:尧图企业网站定制
前言看爬虫教程最容易产生一种错觉每一段代码单独看都懂合起来放到一个能跑的脚本里就乱了。「实例详解」要解决的就是这个落差——把一个真实可运行的小爬虫拆成几层逐层说清它为什么这样写。本文用一个具体例子把完整链路走一遍发请求带超时与重试、解析列表页、识别分页、限速、把结果写成 CSV。为了让例子在没有任何第三方包的环境里也能跑解析部分只用标准库的html.parser。但这同时带来一个要提前说清的局限HTMLParser是事件式的它不帮你把 HTML 修成规范的树所以下面这个解析器是针对某一个具体页面结构写的。换一个站点、甚至同一个站点改版都要重写解析逻辑这是正常且必然的。例子抓的是quotes.toscrape.com一个专门为练习爬虫搭建的站点。选它不是因为「大站不能抓」而是因为它结构稳定、明确用于教学比拿商业网站练手合适。真实项目里动手前请先读目标站的 robots.txt 与使用条款并控制请求频率。所有代码以 Python 3 为基准。Python 2.7 已于 2020 年 1 月 1 日停止维护print语句、dict.iteritems()、xrange这些写法现在都不能用了。一、先设计数据再写代码写爬虫最容易犯的错误是先动手抓抓到一半才发现「这个字段我忘了取」。正确顺序是先定下你要的条目item长什么样。这个例子要的每条记录有三个字段字段含义类型缺失时怎么办text名言正文str该条丢弃author作者str用空串占位tags标签list[str]用空列表用字典表示一条记录即可{text: ..., author: ..., tags: [...]}。字段定下来解析器的任务就变成「把 HTML 里的片段映射到这几个键」边界清晰。二、请求层超时与重试必须显式写urllib.request.urlopen的timeout参数不写就不会超时——网络卡住时程序会一直挂着。所以每次请求都要给超时。重试也要分清对象404页面不存在重试没有意义直接放弃这条。403对方明确拒绝重试只会加重问题应当停止并检查自己的行为。429请求过多应当退避后重试而不是立刻再来。网络层错误连接超时、DNS 失败值得重试但要等一会儿。urllib.error里有两个关键异常URLError是网络层错误的基类HTTPError是它的子类代表「服务器返回了错误状态码」——它同时也是类文件对象可以读取响应体。退避不要用固定间隔用指数退避第一次等 2 秒第二次等 4 秒以此类推。三、解析层用状态机把片段归位HTMLParser的用法是继承它、重写handle_starttag(tag, attrs)、handle_data(data)、handle_endtag(tag)然后feed(text)喂入文本。这里有两个细节必须记住attrs是「(属性名, 属性值) 二元组」组成的列表不是字典。想按名字取值要先dict(attrs)。同一个属性也可能出现多次用dict()会只留最后一个。feed()之后必须调close()否则文档末尾的缓冲数据不会处理完。这段 HTML 的骨架大致是「一个卡片 div里面包含正文 span、作者 small、若干 tag 链接卡片内还嵌套了一个标签 div」。嵌套的 div 是关键陷阱如果一见到/div就以为卡片结束了会在内层标签 div 处提前收尾。解决办法是用一个深度计数器进入卡片时置 1每遇到一个内层 div 加 1遇到/div减 1减到 0 才算卡片真正闭合。四、分页与终止条件列表页的「下一页」是一个链接。解析出它的href后用urllib.parse.urljoin(base, href)拼成绝对地址——不要手工拼接也不要假设href已经是完整的。终止条件不能只看「有没有下一页」还要设一个页数上限否则一旦页面链接出现环状结构循环就不会停。实战一个完整的采集脚本# 适用于 Python 3.8import csvimport timeimport urllib.errorimport urllib.parseimport urllib.requestimport urllib.robotparserfrom html.parser import HTMLParserSTART https://quotes.toscrape.com/UA quote-walkthrough/0.1 (contact: walkthroughexample.com)DELAY 1.0 # 每页之间的间隔秒MAX_PAGES 3 # 页数硬上限防止无限翻页class CardCollector(HTMLParser):把列表页解析成若干条 {text, author, tags}。def __init__(self):super().__init__()self.items []self.next_href Noneself._current None # 当前卡片self._tags []self._depth 0 # 卡片内的 div 嵌套深度self._field None # 正在收集的字段self._buf []self._in_next Falsedef handle_starttag(self, tag, attrs):classes dict(attrs).get(class, ).split()if tag li and next in classes:self._in_next Truereturnif tag a and self._in_next:href dict(attrs).get(href)if href:self.next_href hrefreturnif tag div:if self._current is None and quote in classes:self._current {}self._tags []self._depth 1elif self._current is not None:self._depth 1returnif self._current is None:returnif tag span and text in classes:self._field, self._buf text, []elif tag small and author in classes:self._field, self._buf author, []elif tag a and tag in classes:self._field, self._buf tag, []def handle_data(self, data):if self._field is not None:self._buf.append(data)def handle_endtag(self, tag):if tag li:self._in_next Falsereturnif self._field is not None and tag in (span, small, a):value .join(self._buf).strip()if self._field text:self._current[text] valueelif self._field author:self._current[author] valueelse:self._tags.append(value)self._field Nonereturnif tag div and self._current is not None:self._depth - 1if self._depth 0:self._current[tags] self._tagsself.items.append(self._current)self._current Nonedef fetch(url, retries3):取回文本对可重试的错误做指数退避。last_error Nonefor attempt in range(1, retries 1):try:req urllib.request.Request(url, headers{User-Agent: UA})with urllib.request.urlopen(req, timeout15) as resp:charset resp.headers.get_content_charset() or utf-8return resp.read().decode(charset, errorsreplace)except urllib.error.HTTPError as exc:if exc.code in (403, 404): # 明确答复重试无意义raiselast_error excexcept urllib.error.URLError as exc:last_error excif attempt retries:time.sleep(2 ** attempt) # 2 秒、4 秒……raise last_error if last_error else RuntimeError(请求失败)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 False # 拿不到 robots 时保守处理return parser.can_fetch(UA, url)def main():if not robots_allowed(START):print(robots.txt 不允许抓取退出)returnitems []url STARTfor page in range(1, MAX_PAGES 1):html fetch(url)parser CardCollector()parser.feed(html)parser.close()print(第 %d 页解析到 %d 条 % (page, len(parser.items)))items.extend(parser.items)if not parser.next_href:breakurl urllib.parse.urljoin(url, parser.next_href)time.sleep(DELAY) # 翻页前限速with open(quotes.csv, w, encodingutf-8, newline) as f:writer csv.writer(f)writer.writerow([text, author, tags])for item in items:writer.writerow([item[text], item[author], |.join(item[tags])])print(共写入, len(items), 条)if __name__ __main__:main()跑完之后得到一个quotes.csv。这里有几个刻意的设计next_href为None时提前跳出即使还有下一页MAX_PAGES也会兜底写文件用了encodingutf-8和newline——后者是csv模块官方文档明确要求的少了它会在 Windows 上多出空行。常见坑点1.open()写 CSV 时忘了newline❌open(out.csv, w, encodingutf-8)—— 在 Windows 上每行之间会多出一行空行。 ✅open(out.csv, w, encodingutf-8, newline)这正是csv文档的做法。2.open()不写encoding❌open(out.csv, w)—— 用的是平台默认编码Windows 上可能是 GBK中文与跨平台读取都会出问题。 ✅ 显式写encodingutf-8。3. 用dict(attrs)取重复属性❌ 以为dict(attrs)能拿到全部属性 —— 同名属性只保留最后一个。 ✅ 需要严格处理时遍历attrs列表本身多数场景dict(attrs)够用但要清楚它的取舍。4. 嵌套 div 导致卡片提前闭合❌ 一遇到/div就当作卡片结束结果标签 div 一闭合就把半成品条目丢进结果里。 ✅ 用深度计数器减到 0 才收尾。5.feed()之后忘了close()❌ 只parser.feed(html)就读parser.items。 ✅ 补上parser.close()让缓冲数据被处理完。6. 只判断「有没有下一页」不设页数上限❌while True:里跟着 next 链接走 —— 一旦链接成环就无限抓。 ✅ 加MAX_PAGES上限并同时判断 next 链接是否存在。7. 手工拼接分页 URL❌url page/ str(n) /—— 少个斜杠、多个斜杠都会 404。 ✅urllib.parse.urljoin(url, href)。8.timeout不写❌urlopen(req)—— 连接卡死时程序永远挂着。 ✅urlopen(req, timeout15)并按错误类型决定是否重试。总结层次要做的事关键写法数据设计先定字段再写解析字典 item 三字段请求超时 分类重试timeout15、指数退避解析片段归位处理嵌套深度计数器 close()分页绝对化 硬上限urljoinMAX_PAGES限速每页之间留间隔time.sleep(DELAY)落盘UTF-8 且不留空行encodingutf-8,newline合规先查 robotsRobotFileParser.can_fetch一个爬虫实例的难点从来不在「怎么发请求」而在边界处理嵌套结构怎么收尾、异常怎么分类、循环怎么终止、编码与换行怎么统一。把这几处想清楚代码自然就短。最后提醒一句页面结构会变别人站点的解析规则在你手里可能明天就失效写解析器时留好「取不到字段」的分支比写得漂亮更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑