ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python爬虫实战:requests+lxml抓取豆瓣图书Top250评分信息

Python爬虫实战:requests+lxml抓取豆瓣图书Top250评分信息 写爬虫这件事我前前后后折腾了几年从最开始用正则硬抠 HTML 字符串到后来用 requests lxml 一套组合拳解决大部分静态页面中间踩过的坑真不少。这篇就聊聊怎么用 Python 爬虫实战抓取豆瓣图书评分信息。豆瓣图书 Top250 是很多爬虫学习者都会碰到的经典目标数据结构规整、字段丰富而且反爬相对温和拿来练手再合适不过。这里会讲清楚每一步的思路和代码细节——从分析页面结构、构造请求到用 XPath 提取书名、作者、评分和评价人数再到分页爬取和保存数据整个过程可以直接照着复现。适合刚学完 Python 基础、想找一个完整项目练手的同学也适合已经写过简单爬虫、想系统理解解析和反爬应对的人参考。1. 项目概述与目标拆解1.1 为什么偏偏选了豆瓣图书我见过不少人入门爬虫的第一站是爬豆瓣电影 Top250但图书版其实更适合新手。原因有这么几个图书页面是纯服务端渲染的 HTML数据直接嵌在文档里不需要处理 JavaScript 动态加载接口每一本书的信息字段非常完整书名、作者、出版社、出版年份、价格、评分、评价人数、简介全都有分页规则简单URL 参数干净一眼就能看穿翻页逻辑。更关键的是豆瓣的 robots 协议和数据接口设计相对规范你只要不去高频恶意请求基本不会触发强硬的封禁策略。从需求角度看图书评分数据的应用场景也很多。比如我想做一个简单的读书推荐工具需要把 Top250 的书名和评分拉下来做个排序或者想对比不同出版社的图书均分发现哪家出版社的书整体口碑更好再比如给团队内部做个图书墙自动更新热门书籍排名。这些场景的底层都绕不开一件事抓取结构化数据并保存下来做后续处理。完成这个项目后你会掌握一套完整的爬虫流程这套流程换个网站换个字段照样能跑。1.2 这个项目到底要解决什么问题很多人以为爬虫难在写代码其实真正难的是前期的分析和后期的数据清洗。我拆解一下这个项目的核心任务第一搞清楚数据从哪里来。打开豆瓣图书 Top250 页面表面上是浏览器里渲染出的一堆带样式的网页元素背后其实是一次 HTTP GET 请求拿到了 HTML 文档。数据的源头就是这份文档字符串。第二把文档里人类可读的信息转换成程序可操作的字段。这里牵扯到解析方案的选择你当然可以用正则但正则写起来像天书稍微复杂一点的嵌套结构就头大。更优雅的方式是 XPath——用路径表达式定位 HTML 节点规则清晰、容错率也高。第三应对各种意外情况。网络不稳、请求超时、页面改版、频率触发反爬这些都属于实战中一定会遇到的状况代码里必须有对应的容错机制。整个项目拆开就是四个模块发起请求拿到 HTML、解析 HTML 提取字段、翻页循环获取完整数据、保存结果。四个模块串起来就是一个稳定的爬虫管线任何一个环节出了问题后面都得跟着遭殃。2. 技术选型与核心原理2.1 请求库requests 凭什么比 urllib 好用Python 标准库里的 urllib 当然也能发请求但用起来太痛了。设个请求头得写一长串处理 Cookie 麻烦POST 表单又要手动编码拿到响应还要记得调用resp.read().decode(utf-8)这种繁琐步骤。requests 把这些东西全部封装成了近乎直觉的 API一行代码就能完成请求而且会自动帮你处理编码推断、连接池复用这些烦心事。在这个项目里requests 的重点用法是requests.get()带 headers、带 params。豆瓣对没有 User-Agent 的请求基本是秒拒连正常页面都可能返回 418。我在代码里固定设置一个模拟 Chrome 浏览器的 UA让服务器以为是个正常人访问。params 参数用来传分页参数start比如第一页是 0第二页是 25第三页是 50用循环拼进请求里就能轻松翻页。有些新手喜欢用 Scrapy 框架来做这类小项目说实话有点杀鸡用牛刀。Scrapy 的异步引擎、中间件、Item Pipeline 设计确实强大但学习曲线陡峭配置繁琐而且这类静态页面的小规模爬取根本用不上分布式能力。requests lxml 的组合足够应付 90% 的轻量爬虫需求逻辑直白出问题也好调试。2.2 解析库lxml 与 XPath 的战斗值拿到 HTML 之后有两个选择BeautifulSoup 和 lxml。BeautifulSoup 的语法更友好对新手特别亲和比如soup.find(div, class_pl2)这种写法很接近自然语言。但它背后的解析器性能一般处理大文档时速度明显慢。lxml 底层是 C 语言实现解析速度能快一个量级加上它的 XPath 语法在复杂结构下定位能力更强。爬虫永远要考虑效率尤其在分页爬取几十上百个页面时lxml 的优势非常明显。XPath 的思维方式和 jQuery 选择器有点像都是通过路径表达式告诉程序你要找的节点在哪。比如//div[classpl2]/a的意思是在整个文档里找所有 class 属性为 pl2 的 div然后取它的直接子节点 a。这种层级定位方式在处理重复结构时特别好用因为每一本书的信息都被包在同样 class 名的容器里一条 XPath 表达式就能批量提取 25 本书的同一字段。XPath 还有几个实用技巧。text()取当前节点的文本href取属性值string()用来取嵌套节点里的所有文本。豆瓣图书简介那块儿就常见这种嵌套结构简介文本在一个 span 里但内部还有子节点分割内容直接取 text() 只能拿到一半用string(.//div[classpub])这样的表达式反而干净利落。2.3 存储方案CSV 就够了这个项目的数据量也就两三百条根本用不上数据库。CSV 是最直观、最通用的存储格式Excel 能直接打开pandas 也能轻松读入做后续分析。写入时指定encodingutf-8-sig而不是utf-8否则用 Excel 打开 CSV 会出现中文乱码原因在于 Excel 默认用 GBK 编码读文件而 utf-8-sig 带了 BOM 头Excel 能识别出来并正确解码。这是个极容易忽略的细节我第一次跑的时候就被坑过。3. 实操全过程从分析到落地3.1 第一步分析豆瓣图书页面的结构动手写代码之前我习惯先在浏览器里观察目标页面。打开豆瓣图书 Top250 页面右键检查在开发者工具里定位到任意一本书的 DOM 结构。豆瓣图书的列表结构是这样的一个div classitem容器里面有左右两列左边是封面图右边是div.pl2里面包含书名链接下方有一个p.pl段落写着作者、出版社、出版年份等信息再下面评分是一个span.rating_nums评价人数是span.pl里面的带括号文本最后是摘要部分div.pub或span.inq存着书籍简介。这个结构是我最推荐新手去拆解的范本。它规律性极强25 个div.item平铺排列彼此之间没有任何嵌套交叉。你要做的就是写一条 XPath 表达式选中所有符合条件的节点然后循环取每个节点下的字段。这里有个核心技巧不要一次性写绝对路径而是先选到每一本书的祖先容器//div[classitem]再在这个节点的上下文里继续用.//取子元素。这样即使单个字段的表达式写错了最多损失该字段不会把整本书的数据搞丢。3.2 第二步构造请求头与基础请求函数请求头是爬虫和服务器打交道的身份证。豆瓣对爬虫的警惕性不比那些大厂低你用一个 Python 默认的 UA比如 python-requests/2.31.0直接访问很容易被识别为机器人。我在项目里内置了一个最常见的 Chrome UA同时把 Accept-Language 设为 zh-CN这样服务器返回的内容会更偏向中文站正常页面。为了代码清晰我封装了一个get_page(url)函数处理超时重试import requests import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def get_page(url, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: return resp.text else: print(f状态码异常: {resp.status_code}, 第 {attempt 1} 次尝试) except requests.RequestException as e: print(f请求异常: {e}, 第 {attempt 1} 次尝试) time.sleep(2) return None这里我没用 Session 是因为豆瓣这个页面不需要登录和 Cookie纯 GET 就能拿到公开数据。不过如果你未来要爬需要登录的站点就得用requests.Session()去维持会话状态它会在内部自动处理 Cookie 的传递把登录后的凭证带着走。3.3 第三步用 XPath 提取核心字段提取字段是整个项目最核心的部分。我每次写这种解析代码都遵循一个原则先从整体列表开始再处理单条数据。先用tree.xpath(//div[classitem])拿到所有书籍节点然后在循环里逐条提取字段。from lxml import etree html get_page(https://book.douban.com/top250?start0) tree etree.HTML(html) items tree.xpath(//div[classitem]) for item in items: title item.xpath(.//div[classpl2]/a/text())[0].strip() info item.xpath(.//p[classpl]/text())[0].strip() rating item.xpath(.//span[classrating_nums]/text())[0].strip() people item.xpath(.//span[classpl]/text())[1].strip() link item.xpath(.//div[classpl2]/a/href)[0]字段细节上要注意几点。书名在pl2容器里的 a 标签中但豆瓣的标题有时带前后空格所以.strip()这一步不能省不然存进 CSV 里后面做字符串匹配时容易出问题。p.pl这一段是作者、出版社、出版年份、价格连在一起的文本字段间用斜杠分隔后续如果要拆开可以split(/)但要特别注意作者名本身可能包含多个作者用逗号分隔不要跟字段分隔符混淆。评分简单直接取文本。评价人数比较坑因为同一个 class 的 span 在书籍卡片里可能出现了两次一次是评分旁边的人数一次是其他位置所以要靠索引确定是哪个节点我用[1]取第二顺位的那个。不过这里我被坑过一次如果某本书的评价人数缺失这个 span 可能不存在索引就会越界报错。稳妥的做法是先检查len(item.xpath(...))再取值或者用 Python 的异常捕获兜底。3.4 第四步翻页循环与数据存储豆瓣图书 Top250 的分页规则是每页 25 本从 start0 到 start225一共 10 页。我直接写个for i in range(10)每次请求start{i * 25}。翻页代码里最容易犯的错误是忘记控制请求间隔。豆瓣对频繁请求非常敏感我实测如果只带 UA 不设延时连续请求第 5、6 页就会被拦返回一个 HTML 里含有异常请求字样的校验页。我在每次请求之间加time.sleep(2)虽然慢一点但胜在稳定。存储部分的代码import csv with open(douban_books.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([书名, 作者信息, 评分, 评价人数, 链接]) for i in range(10): url fhttps://book.douban.com/top250?start{i * 25} html get_page(url) if html is None: print(f第 {i 1} 页失败跳过) continue tree etree.HTML(html) for item in tree.xpath(//div[classitem]): # ... 提取字段 ... writer.writerow([title, info, rating, people, link]) print(f第 {i 1} 页完成) time.sleep(2)这里newline是 Python 写 CSV 的一个大坑。如果不加在 Windows 平台上会由于换行符不同产生空白行看起来像数据被隔开了实际上是因为 csv 模块用\r\n写行而 open 默认又按系统换行符转换导致叠加。加了newline之后writer 自己控制换行问题彻底消失。4. 反爬机制应对与合规边界4.1 反爬的基本盘UA、Referer 和请求频率豆瓣很早就引入了反爬策略。我总结下来有三道坎第一道是基础请求头检测你访问时 UA 必须是正常浏览器第二道是请求频率监控短时间内的密集请求会触发临时封禁表现是你还能访问但从第 N 页开始页面里不再有书籍数据而是出现有异常请求的提示页第三道是 IP 级别的风控这个在普通练习中很难触发但如果触发了一般要等一段时间才能恢复。应对思路其实很简单模拟正常人。正常人不会 0.5 秒刷新一页不会每次请求的 UA 都不一样。最稳妥的做法是固定一个 UA、固定请求间隔中间加随机延时让节奏更接近真实用户。我一般会写time.sleep(random.uniform(1, 3))而不是固定的 2 秒这样时序上更自然也顺手降低出事的概率。4.2 异常重试与降级策略爬虫跑一跑就断是很正常的事关键是断了能不能自己站起来。我在前面写的get_page函数里已经内建了重试机制最多尝试 3 次每次间隔 2 秒。如果重试 3 次还是失败就把当前页跳过不要中断整个程序。这样设计的好处是就算某一页真出了硬性问题后面几页还能继续抓数据只是缺了一小段后期补跑即可。还有一类异常是解析层面的。比如某本书的简介缺失导致 XPath 索引报错或者评分文本里混进了异常字符。我的处理方式是在解析循环里套try...except单条数据解析失败就打印出来跳过不拖垮整体流程。有时候你以为数据规整实际抓出来才发现豆瓣有些老书的字段格式特别随意作者信息里既有繁体又有英文这种脏数据清洗工作跑不掉。4.3 合规提醒爬虫也有使用边界这个必须好好说清楚。爬虫本身是合法的技术工具但用在哪里、怎么用有明确的边界。豆瓣的 robots 协议里对爬虫访问有说明个人学习目的的少量请求是没有问题的但收集数据用于商业用途、转售数据、大规模抓取影响网站正常运行这些都是不合适的。我给自己定的底线是只爬公开数据、控制请求频率、不做商业化处理、不绕过登录墙或验证码。这个项目的所有请求都是豆瓣公开页面不涉及用户隐私数据也没有强制登录属于安全的学习案例。如果你以后要爬别的网站请先看 robots.txt再做技术方案评估。还有一个容易被忽略的点爬下来的数据如果要用在你的应用里最好标明数据来源别把第三方数据当成自己的成果。完成实操过程后用pandas.read_csv(douban_books.csv)就能读入数据做进一步分析了。我经常把这个项目当做一个数据管道的起点爬下来的数据清洗后可以做评分分布图、出版社排名、作者作品均分排名等分析也可以用来训练一个简单的推荐模型。爬虫永远只是第一步数据分析和应用才是让数据产生价值的地方。5. 常见问题与排查实录5.1 请求被拦截418 与 403 状态码这是我最常遇到的问题。如果你直接裸调用 requests没有设置 UA豆瓣大概率会返回 418Im a teapot这个反爬特有的状态码。面对 418唯一有效的方案就是补全请求头模拟真实浏览器的完整请求。有时候一个 UA 还不够某些站点会检测 Referer、Accept 等字段。建议用浏览器开发者工具里的网络面板把一次真实请求的 headers 完全复制下来。如果返回 403通常意味着更严格的反爬比如 IP 被临时封禁。403 的处理方式和 418 不同补请求头没用得先停下来等一段时间几分钟到几小时不等再恢复。我遇到过调试时手贱开了个循环忘了加 sleep几百个请求瞬间打过去过了十几个小时才恢复正常访问。这种情况下不要反复去试探越试探封禁时间越长。5.2 XPath 提取结果为空这个问题仅次于请求被拦。明明浏览器里能看到数据XPath 却提取不到任何内容。我的排查套路是先把html保存成本地debug.html文件再用编辑器打开查看实际拿到的内容。很多情况下你会发现你拿到的 HTML 里根本没有那些看起来像是数据的节点——因为你请求的页面是一个验证页、登录跳转页或者移动端重定向页面。如果 HTML 确实包含目标节点那就是表达式写错了。我建议从简单表达式开始逐步加条件验证。先写tree.xpath(//div[classitem])看数量是否等于 25如果这一步就失败说明 class 名字不对可能页面改版了如果这一步成功但某个字段提取不到那就是字段那层结构有问题。这种二分定位法几乎能解决所有解析问题。5.3 中文乱码问题乱码有两种情况。一种是请求阶段返回的 HTML 文本乱码原因可能是 requests 自动检测编码失败。豆瓣的页面用了 UTF-8正常情况下 requests 能识别对但个别页面可能在 meta 标签没写明 charset。处理方案是手动指定编码resp.encoding utf-8再重新解码。另一种是写 CSV 后打开 Excel 乱码这个前面说过用utf-8-sig编码写入即可解决。还有一种是控制台打印乱码。Windows 的终端默认编码是 GBK直接 print 中文可能全是问号。你可以在代码开头设置sys.stdout.reconfigure(encodingutf-8)或者干脆不要依赖 print 验证数据直接看 CSV 文件内容。5.4 数据缺失与重复爬取过程中偶尔会出现某一条数据解析失败导致字段缺失或者因为网络重试导致某页被重复解析并追加写入。我的解决方案是解析循环里逐条检查关键字段是否为空为空则跳过并记录日志写入 CSV 前保留一个内存中的集合记录书名如果检测到重复书名就跳过。这个逻辑不复杂放在一个大项目里却会给下游数据使用省掉大量清洗时间。6. 后续扩展方向与个人体会爬完豆瓣图书 Top250你手里的数据已经可以做很多有意思的事了。我建议不要停在爬下来了这一步继续往下推一步用 pandas 统计不同出版社的平均评分、用 wordcloud 做书名关键词词云、甚至把评分和评价人数的关系做成散点图观察口碑和热度的相关性。以我个人的经验从跑通爬虫到跑出分析结论这个跨越才真正让人意识到爬虫的价值不在于技术本身而在于它连接了网络数据和你的想法。这个项目练熟之后我个人推荐往两个方向进阶第一个方向是分布式爬虫把 requests 换成 Scrapy再加个 Redis 队列学习如何在大规模数据采集下做任务的调度与去重第二个方向是动态页面的爬取有些网站的数据藏在 Ajax 接口里需要分析 XHR 请求并模拟构造参数这比纯静态页面解析又多了一层技术含量。最后分享一个我自己的习惯写完爬虫后我会保留一份解析日志把每次请求的 URL、状态码、解析到的条数、耗时记录下来。这样遇到任何问题都能快速回溯到具体是哪个环节出了错。越往后做项目越会发现爬虫拼的不是技术技巧而是排查问题的耐心和严谨的工程习惯。
返回列表