恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python爬虫实战:用requests和lxml抓取全站图书数据
首页
资讯中心
/
Python爬虫实战:用requests和lxml抓取全站图书数据
Python爬虫实战:用requests和lxml抓取全站图书数据
发布时间:2026/10/7 5:09:16
爬虫界也有“练习场”如果你还没听过 Books to Scrape那说明你的爬虫之路还少了最友好的一堂入门课。这个网站是专门给爬虫学习者准备的沙盒整个站点用静态 HTML 渲染没有登录墙没有验证码几乎没有反爬策略数据量又恰到好处——50 个分页、1000 本图书够你把一套完整的采集流程跑得明明白白又不会让你被海量数据淹没。这篇文章就是一次完整的 Python 爬虫实战复盘从环境准备、页面拆解到用 requests 和 lxml 把全站图书抓下来再用 csv 模块导出成结构化 CSV 文件确保 Excel 打开不乱码、列顺序稳定、字段齐全。适合刚刚学完 Python 基础、想找一个正经练手项目的人也适合想把 requests lxml CSV 这套经典组合彻底吃透的读者。1. 先搞清楚目标Books to Scrape 是什么、要抓什么1.1 为什么这个网站适合当爬虫练习场先说说我为什么推荐这个站。很多初学者一上来就直接盯上电商平台、社交平台或点评类网站结果要么被验证码拦在门外要么刚发几个请求就收到了 403连列表页都没解析明白就劝退了。Books to Scrape 不一样它的定位就是“爬虫练习专用”。站内图书信息全部写在 HTML 里不依赖 JavaScript 动态渲染这就意味着你不必上 Playwright、Selenium 这种重武器用最基础的 requests 就能拿到原始页面。更重要的是它的数据结构非常有代表性列表页是一行行的图书卡片详情页里还有一张标准的书籍信息表格。这种“列表页拿摘要、详情页拿补充字段”的架构恰恰是绝大多数真实网站的通用形态。你在这个小小练习站上练会的 XPath 技巧、分页处理、字段清洗、CSV 写入几乎可以无缝迁移到其他静态站点。所以我一直说别小看这个只有 1000 本书的网站它其实把爬虫流程里最容易翻车的几件事都替你安排好了。还有一点Books to Scrape 允许爬虫抓取robots.txt 对采集者非常友好。也就是说你在这个站上折腾可以把全部精力放在技术实现上不需要去考虑绕过验证码这类灰色操作也不存在踩踏目标站点服务条款的负担。对新手来说这种“放心大胆练”的体验比什么都重要。1.2 全站数据的字段构成从列表页到详情页在写任何代码之前先把“我要抓什么”想清楚。Books to Scrape 的数据分布在两个层级。列表页也就是每页 20 本的分页页面可以直接拿到图书标题、价格、评分、库存数量以及对应的详情页链接。但如果你只想做一次完整的数据采集光拿这些还不够——UPC、分类、是否含税价格、Review 数量、产品描述这些更有价值的信息全部藏在详情页里。这里我给出一份我这次采集用到的字段清单字段名来源说明title列表页书名price列表页展示价格带英镑符号需清洗rating列表页1-5 星评分availability列表页库存数量需从文本中提取数字detail_url列表页详情页链接用于二次请求upc详情页图书唯一编码Excel 中需当文本处理category详情页面包屑中的分类名称product_type详情页产品类型price_excl_tax详情页不含税价格price_incl_tax详情页含税价格tax详情页税额reviews详情页评论数量description详情页书的描述有的书没有所谓“全站图书数据”在这个项目里就是 50 个分页 × 每页 20 本 1000 条记录。我实测时站点就是这个量级如果以后页面结构有调整总数可能会变但采集逻辑不变。这里也顺便说一下“全站”对新手来说最容易理解成一个固定数字实际上对爬虫而言全站 你覆盖了站点上所有可入口数据页面而不是穷尽每一张页面里的每一个字符。分清主数据、补充数据和无关数据比如导航栏、页脚广告是设计字段清单的第一步这决定了你后续代码能写得多清晰。2. 工具选型与环境准备2.1 依赖就两个requests 和 lxml爬虫这个领域有个很有意思的现象工具越重上手越慢。很多人一上来就装 Scrapy结果被 Item Pipeline、Middleware、Selector 这些概念绕晕连第一个蜘蛛都没跑起来就放弃了。对这种一次性全站采集任务我的选择非常简单requests 负责发请求lxml 负责解析 HTMLcsv 是 Python 标准库直接用来写文件。pip install requests lxml就这两行。为什么用 lxml 而不是 BeautifulSoup我两个库都用过坦白说 BeautifulSoup 的 find 系列方法在文档结构混乱时确实更宽容但 Books to Scrape 的页面结构非常规整列表卡片、价格、评分都有清晰的 class 命名。这种场景下 lxml 的 XPath 优势很明显表达式稳定可复用解析速度比 bs4 快一个量级而且写出来的定位逻辑一眼就能看懂。等你遇到那种页面结构混乱的站点再切回 BeautifulSoup 也不迟。至于 Scrapy、Playwright、requests-html 这些我的建议是先别碰。这种轻量级练习任务的本质是让你理解 HTTP 请求、HTML 解析、数据落地这三件事requests lxml 组合就是最透明的教学载体。等你把流程吃透了再升级成 Scrapy 做分布式采集或者上 Playwright 处理动态页面都能自然衔接不会觉得跨度太大。2.2 页面结构拆解XPath 怎么定位每一条数据写解析代码之前先在浏览器里按 F12 打开开发者工具把页面结构摸清楚。Books to Scrape 的列表页里一本图书的基本单位是这样的article classproduct_pod h3a hrefcatalogue/xxx/index.html title书的名字书的名字/a/h3 p classprice_color£51.77/p p classinstock availabilityIn stock (19 available)/p p classstar-rating Three/p /article这个结构意味着你可以用几个非常直接的 XPath 把它们全部定位出来。取整个图书卡片用//article[classproduct_pod]在卡片内部继续取标题、价格、评分、库存、链接。注意评分那一项star-rating后面的类名才是真正的星级比如Three就是 3 星需要从 class 属性里提取出来而不是直接把整段 class 文本当成评分。我先教你在浏览器控制台验证 XPath 的小技巧打开开发者工具在 Console 里输入$x(//article[classproduct_pod])回车就能看到当前页命中了多少个元素。这个技巧能让你在不写 Python 代码的情况下先把选择器调通等所有表达式都验证正确之后再去写正式脚本效率会高出不少。很多新手对着文档写 XPath 要么漏了斜杠要么写错层级用控制台先验证是最稳的做法。2.3 合规与礼貌爬取的红线这部分必须单独说。爬虫本身不是原罪但一定要清楚边界在哪。这次练习我用 Books to Scrape是因为它本身就是官方允许爬取的教学站点robots.txt 里对采集非常宽容数据也全部是公开信息。即便如此我在代码里依然做了三件事设置正常的 User-Agent不伪装成搜索引擎也不是默认的 python-requests、请求之间加上 0.5 秒以上的间隔、每个页面最多重试三次。这三件事背后的逻辑很简单爬虫是在消费目标站点的服务器资源礼貌程度决定了你的练习是“学习”还是“骚扰”。我特别想提醒的是如果你以后要采集真实业务里的数据务必先阅读目标站点的服务条款和 robots.txt评估数据用途是否合规不要在练习阶段养成暴力请求的习惯。练手选 Books to Scrape 这种沙盒站生产环境选有授权的数据源这条原则值得刻在脑门上。另外别拿这个套路去随便爬电商巨头或者某些社交平台那些站点的反爬机制和法务风险都不是新手练手项目该碰的。爬虫技术没有好坏但使用技术的场景和方式分境界很多初学者就是没分清这个界限才把原本单纯的学习变成了一堆麻烦。3. 代码实现分步拆解采集全流程3.1 先写一个带重试的请求函数请求是整个流程的地基。我最开始写爬虫的时候直接requests.get(url)一把梭然后被各种连接超时、偶发的 SSL 错误教育了一整晚。后来才明白一个健壮的爬虫脚本第一件事就是把请求函数写好而不是把解析逻辑写得花里胡哨。import time import requests from lxml import etree BASE_URL https://books.toscrape.com/catalogue/ HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36 } def fetch_html(url, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return etree.HTML(resp.text) except Exception as e: print(f第 {attempt 1} 次请求失败: {url} - {e}) if attempt max_retries - 1: time.sleep(2) return None这里有两个细节值得说。第一个是timeout10如果不设置这个参数遇到连接卡死的情况脚本可能挂很久都不报错设了超时最多等 10 秒就放弃配合重试机制整个流程才不会因为个别网络抖动而中断。第二个是raise_for_status()它的作用是当服务器返回 4xx 或 5xx 状态码时直接抛异常避免你拿着一个错误页面当正常页面去解析最后解析出空数据还找不到原因。重试策略我用了最简单的三次线性重试第一次失败等 2 秒第二次失败再等 2 秒最多试三次。Books to Scrape 这种低压力站点三次基本足够。如果换成生产环境通常会改用指数退避比如 1 秒、2 秒、4 秒或者配合随机抖动那是后话但你现在就知道这个方向后面改起来也不陌生。3.2 列表页解析拿到 20 本书的入口解析函数的核心逻辑是先用etree.HTML把响应文本变成可以被 XPath 查询的文档对象然后定位所有product_pod卡片逐个提取字段。代码写出来大概是下面这样import re from urllib.parse import urljoin RATING_MAP {One: 1, Two: 2, Three: 3, Four: 4, Five: 5} def parse_list_page(doc, page_url): books [] for item in doc.xpath(//article[classproduct_pod]): title_nodes item.xpath(.//h3/a/title) price_nodes item.xpath(.//p[classprice_color]/text()) avail_nodes item.xpath(.//p[contains(class,instock)]/text()) rating_nodes item.xpath(.//p[contains(class,star-rating)]/class) link_nodes item.xpath(.//h3/a/href) if not title_nodes or not link_nodes: continue title title_nodes[0].strip() price_text price_nodes[0].strip() if price_nodes else 0 avail_text .join(t.strip() for t in avail_nodes if t.strip()) if avail_nodes else rating_cls rating_nodes[0].split()[-1] if rating_nodes else Zero match re.search(r\((\d)\savailable\), avail_text) books.append({ title: title, price: float(price_text.replace(£, )), availability: int(match.group(1)) if match else 0, rating: RATING_MAP.get(rating_cls, 0), detail_url: urljoin(page_url, link_nodes[0]), }) return books每个字段都值得展开说。价格文本里带的英镑符号必须去掉再转成 float否则数值比较和后续分析都会出问题。库存文本常见格式是In stock (19 available)我用正则把括号里的数字抠出来正则写错一步就可能拿到 None所以后面加了if match else 0兜底。评分是文本映射Three转成数字 3这里用字典最清晰后续不管是排序还是统计都比字符串好用。还有一个关键操作是用urljoin(page_url, link_nodes[0])拼接详情链接。列表页的 href 是相对的直接存下来后面没法请求用 urljoin 就能自动拼出完整的绝对地址。我特意把page_url传进函数就是因为有的页面目录层级不同拼出来的结果会不一样用当前页作为基准更稳妥这是我在别的项目里踩过坑之后才养成的习惯。3.3 详情页解析补齐 UPC、分类、描述这些关键字段详情页的数据集中在两个地方面包屑导航里有分类信息product information表格里有 UPC、价格、税额、库存、评论数这些字段。页面底部还有一个产品描述段落。解析详情页的时候我把上一个函数拿到的列表页数据作为基础再往字典里补充新字段。FIELD_MAP { upc: upc, product_type: product_type, price_(excl._tax): price_excl_tax, price_(incl._tax): price_incl_tax, tax: tax, availability: availability_detail, number_of_reviews: reviews, } def parse_detail_page(doc, base_book): book dict(base_book) crumbs doc.xpath(//ul[classbreadcrumb]//li/a/text()) book[category] crumbs[2].strip() if len(crumbs) 3 else desc_nodes doc.xpath(//*[idcontent_inner]/article/p/text()) book[description] .join(t.strip() for t in desc_nodes).strip() if desc_nodes else rows doc.xpath(//table[contains(class,table-striped)]//tr) for row in rows: cells row.xpath(./td/text()) if len(cells) 2: continue key cells[0].strip().lower().replace( , _).replace((, ).replace(), ) value cells[1].strip() mapped FIELD_MAP.get(key, key) book[mapped] value return book这里最值得学的是“按行遍历表格”而不是“按固定行号取值”。很多人第一次写的时候会直接用//table/tr[1]/td[2]/text()去取 UPC这种写法在页面结构不变时没问题但一旦网站新增了一行信息行号就全错位了。我这种写法把表格的每一行读出来用第一列文本做字段名再映射成统一字段无论表格行序怎么变代码都能稳定工作。这也是一种典型的“防御式解析”在生产级爬虫里非常常见。分类从面包屑第三个li里取这是 Books to Scrape 固定的结构Home Books 分类名。描述这里要注意有些书没有描述段落所以必须判空。如果不管三七二十一直接取第一个元素的下标遇到没有描述的书就会索引报错整个脚本直接崩掉后面的数据全白爬。判空这种写法看起来啰嗦但它能保证脚本在面对脏数据时依然走得下去。3.4 分页循环、数据汇总与 CSV 导出解析函数都写好了接下来就是组装主流程。Books to Scrape 的分页 URL 非常有规律从page-1.html到page-50.html直接一个 for 循环就能覆盖。循环里每解析完一个列表页就进去逐个请求详情页把补充字段加进来。全部塞进一个all_books列表最后用 csv 模块写文件。import csv def save_to_csv(books, filenamebooks_all.csv): fieldnames [ title, upc, category, price, price_excl_tax, price_incl_tax, tax, availability, rating, reviews, description, detail_url ] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames, extrasactionignore) writer.writeheader() writer.writerows(books) def main(): all_books [] for page in range(1, 51): page_url f{BASE_URL}page-{page}.html doc fetch_html(page_url) if doc is None: print(f第 {page} 页获取失败跳过) continue list_books parse_list_page(doc, page_url) for book in list_books: detail_doc fetch_html(book[detail_url]) if detail_doc is not None: all_books.append(parse_detail_page(detail_doc, book)) time.sleep(0.5) print(f第 {page} 页完成累计 {len(all_books)} 本) time.sleep(0.8) save_to_csv(all_books) print(f导出完成共 {len(all_books)} 条数据)关于 CSV 导出有两个坑是我亲测非常折磨人的。第一个是编码如果直接encodingutf-8生成的 CSV 用 Excel 打开就会乱码因为 Excel 默认按 ANSI 解析文件头。解决办法是用utf-8-sig它会在文件开头写入 BOMExcel 就能正确识别为 UTF-8。第二个是换行open()里必须加newline否则写出来的 CSV 每一行之间会多一个空行这是 csv 模块在 Windows 平台上著名的坑。字段顺序我固定成一个fieldnames列表这样导出的文件列顺序是稳定的不会因为字典插入顺序变化而导致每次导出列顺序不同。extrasactionignore则保证如果某个字典里混入了多余字段也不会报错只会被忽略。这两行代码看起来不起眼但对维护数据列格式非常有用尤其是后续要做跨批次数据合并的时候。4. 实测运行与踩坑记录4.1 实测结果全站跑完需要多久我本地实测跑了一轮单线程、每次请求后 sleep 0.5 秒、列表页翻页 sleep 0.8 秒全站 1000 本图书跑完大概花了 12 到 15 分钟生成的 books_all.csv 约 800 KB描述文本占了大头。这个速度对于一个练习项目来说完全够用比人手工复制粘贴不知道快了多少倍也能让你在等待过程中观察每一步日志输出。跑完第一件事永远是校验数据打开 CSV 数一下总行数是不是 1000不含表头再随便抽几本书对一下原始网站的信息。我在列表里检查过1000 条记录一条不多一条不少。如果发现数量对不上最可能的原因是某几个请求超时后被跳过了这时需要加大重试次数或者拉长间隔而不是急着去检查解析代码。打印日志的好处就在这你能看到是哪一页哪一步出了问题。这里特别想说的一点是跑爬虫和我平时做数据分析的心态一样结果出来第一眼不要看字段对不对先看条数对不对。条数对了再抽查几个字段条数不对就先看日志定位是哪个请求丢了。这个排查顺序能帮你省掉大量无头苍蝇式的调试时间我见过太多人一跑出数据就开始盯着某个字段纠结结果最后发现是缺了一整页白忙活半天。4.2 常见问题排查速查表我把这个项目里以及教别人写类似爬虫时最常遇到的问题整理成了一张表基本覆盖了绝大多数新手翻车现场问题现象可能原因解决办法请求超时或连接失败网络不稳定或并发过高加 timeout、重试和 sleep降低单次请求频率拿到 403 ForbiddenUA 被识别为爬虫设置浏览器 UA去掉默认 python-requests 指纹页面解析出来是空的XPath 写错或页面结构变化先在浏览器 Console 用$x()调通表达式再写代码中文乱码站点编码不是 UTF-8用 resp.encoding 识别或手动指定正确的编码CSV 用 Excel 打不开或乱码编码用了 utf-8改成 utf-8-sig导出的数字列变了UPC/ISBN 被当成数字处理导出时用字符串或 Excel 里设置成文本列某些书没有描述导致报错详情字段缺失解析时统一判空默认值兜底数据条数比预期少部分请求失败被跳过检查日志定位失败页增大重试次数价格转 float 报错货币符号或空文本混入先 strip 再去符号处理异常值兜底这张表基本就是我从“爬虫新手”到“能稳定写采集脚本”过程中踩过的所有坑的浓缩。每一个问题我都在 Books to Scrape 或者其他站上真实遇到过。特别是 csv 模块的 newline 参数和 utf-8-sig 编码这两件事几乎每个初学者都会踩一次踩完才会记住现在我直接把答案摆出来你就不用再受一遍这个罪了。4.3 让爬虫更稳的几个习惯说几个代码之外的习惯。第一个用 Session 而不是裸 requests.get。Session 会自动复用底层的 TCP 连接减少三次握手次数对服务器也更友好。对 Books to Scrape 这种小站效果不明显但等你爬数据量大的站点时快与稳的差距就出来了。session requests.Session() session.headers.update(HEADERS)第二个边爬边存不要爬完才落盘。我的主流程里虽然是最后统一写 CSV但如果你爬的是几千上万条数据强烈建议每处理完一页就增量写入一行到 CSV或者先用 JSON 缓存每个阶段的进度。不然你爬到第 900 条时进程崩了重启再爬又要从第 1 条开始那种绝望我经历过太多次。折中方案是每页解析完就 append 到本地临时文件里最后再统一合并成正式 CSV这样即使中途挂掉损失的也只是最后一页的数据。第三个把枚举范围、重试次数、延时这些值提取成变量。别小看这个习惯我刚学的时候直接把range(1, 51)写死在循环里后面想改成动态获取页数、想调大间隔就得来回改代码。把参数集中写在文件顶部维护成本会低很多。这个习惯用到后面做自动化任务时尤其值钱因为自动化最怕的就是“改一行代码要翻遍整个文件找位置”。5. 这套代码还能怎么扩展5.1 按分类爬取与增量更新Books to Scrape 的导航栏里有几十个分类入口每个分类对应一个独立列表页。上面这套代码稍作修改就能变成按分类爬取先请求首页解析出所有分类链接再对每个分类跑一遍分页循环最后在数据里额外记录分类字段。这个升级其实是电商类站点采集的常见需求不是要全站快照而是要按业务线更新。增量更新则是生产级爬虫绕不开的话题。全站重爬很快但代价是每次都请求所有页面对服务器不友好。更合理的做法是用 UPC 或 detail_url 作为主键先把历史数据加载进内存遇到已存在的记录就跳过只补充新增的图书。这个逻辑本质上是数据同步。我在写这个练习项目的时候顺手做了一个简化版把上一次导出的 CSV 里的 UPC 集合读进来新数据只有在 UPC 不在集合中时才写入。代码很简单但对理解“增量”这个概念非常有帮助。5.2 从 CSV 到数据分析与可视化辛辛苦苦抓下来 1000 条数据只放在 CSV 里躺着有点浪费。下一步完全可以把 CSV 交给 pandas 做分析比如统计不同类别的图书均价、看价格分布直方图、分析评分和价格之间的关系。Books to Scrape 的数据虽然简单用来练数据分析却非常合适。我只举一个例子用 pandas 读入 CSV 后按类别分组计算均价再排序输出就能一眼看出哪类书最贵、哪类书最便宜。import pandas as pd df pd.read_csv(books_all.csv) print(df.groupby(category)[price].mean().sort_values(ascendingFalse))这个扩展方向其实比爬虫本身更有价值。很多人的技术成长路径都是先学会采集数据再学会清洗数据最后学会用数据讲故事。爬虫只是基建分析才是决策依据。Books to Scrape 这 1000 条数据虽然不算海量但足够你练习数据透视、分组聚合、缺失值处理这些基本功练完再去碰真实业务数据心里会踏实很多。我自己平时练习爬虫时还有个习惯每写完一个采集脚本都会顺手做一个简单的校验脚本对比本地 CSV 和线上数据的抽样结果。这个习惯让我后来写真实项目的自动化采集任务时少踩了很多坑。所以最后想对正在入门的朋友说一句Books to Scrape 这个站练完不算完把数据抓下来之后试着用它试试清洗、建模、可视化你会发现爬虫的乐趣才刚刚开始。