2026/10/7 18:06:37

Python爬虫实战:用requests+XPath采集全站图书数据并导出CSV

Python爬虫实战:用requests+XPath采集全站图书数据并导出CSV 直接说结论这个项目是练习爬虫最合适的入门实战之一。Books to Scrape 是专门为爬虫学习设计的演示站点站内图书结构规整、分页清晰、无复杂登录几乎没有任何反爬门槛而且官方允许采集。我前后用 requests 加 XPath 的组合从零开始到跑完全站 50 页、导出干净可用的 CSV总共花了不到一个晚上。如果你刚开始学 Python 爬虫或者想找一个完整的数据采集项目写在简历里这个项目值得完整做一遍。这个过程中你会踩到几个非常典型的坑比如编码导致的中文乱码、XPath 表达式写错取不到数据、列表页数据与详情页字段对不上。我都会在下面展开讲并把可直接复制的代码和踩坑经验一并放出来。1. 项目剖析与全站数据结构拆解1.1 为什么选 Books to Scrape 作为爬虫练手项目这个站是国外一个专注于爬虫教学的实验性网站它的定位就是给爬虫学习者一个干净、稳定的数据源。对比其他真实网站它有几个非常明显的优势。第一站点是纯静态 HTML 渲染所有数据都直接写在响应体里不需要处理 JavaScript 动态加载也不需要分析异步接口。你打开页面右键查看源代码书名、价格、评分、库存、链接全部都能看到。这对新手极其友好因为你可以先用浏览器验证自己的 XPath 表达式再放进代码里跑。第二全站数据量适中1000 本书每页 20 本共 50 页。这个规模既不会因为数据量太少导致练手不充分也不会因为数据量太大导致采集时间过长。我实测跑完全站只需要几十秒完全不用担心被封。第三页面结构高度规整。每本书的信息都包在article classproduct_pod这个标签里字段位置一致写一套 XPath 就能批量提取所有图书。真实网站很少有这么统一的结构所以这个站特别适合用来建立模板化解析的思维。我自己做爬虫项目时有个习惯接到一个需求先花 20 分钟手工浏览目标网站的几个关键页面搞清楚数据长在哪里、分页规则是什么、字段之间怎么关联然后才写代码。这个项目同样如此建议你也不急着写爬虫先打开首页看看结构。1.2 全站图书数据到底包含哪些字段很多人做爬虫项目时对全站数据的理解比较模糊往往是爬到什么算什么。但真正的项目中需求方一定会要求你明确字段清单。这个项目里的全站图书数据我建议至少包含以下字段字段名来源说明book_id详情页 URL 末尾图书唯一标识用于去重和后续关联title列表页或详情页书名含出版年份后缀price列表页英镑价格需清洗掉符号转成浮点数rating列表页 class 属性评分分为 One 到 Five 五档stock列表页库存状态通常是 In stock 或 Out of stockcategory详情页侧边栏图书所属分类product_url列表页链接详情页完整链接description详情页图书简介清洗后较干净这里要特别提一下 book_id。Books to Scrape 的详情页 URL 尾部通常是一个数字 ID比如a-light-in-the-attic_1000/index.html里的 1000。这个 ID 在全站范围内是唯一的非常适合做主键。如果你后面要做增量采集或者多批次合并数据这个字段就派上大用场了。1.3 技术选型requests XPath 组合的合理性这个项目技术选型上有三种主流方案requests 正则、requests XPath/CSS选择器、Scrapy 框架。还有更重的 Selenium。我最终选择的是 requests lxml 的 XPath 解析组合。requests 负责发起 HTTP 请求并获取响应lxml 负责把 HTML 文档转成可查询的树状结构XPath 表达式负责精确提取节点。这套组合的优势是轻量、直观、调试方便完全覆盖了这个项目的数据采集需求。对比一下用正则表达式解析 HTML 虽然不需要额外依赖但写起来非常痛苦。图书列表页的 HTML 标签嵌套复杂正则表达式面对这种半结构化文本很容易写出漏洞比如误匹配、跨行匹配失败。Scrapy 框架功能强大但对新手来说异步调度、中间件、Item Pipeline 这些概念过于庞大做这个小项目反而有点杀鸡用牛刀。Selenium 就更不必说了这个站不需要浏览器渲染用它是在浪费资源。等到你把这个项目跑通再根据自己的需求去进阶学习 Scrapy那时你对框架的理解会比直接上手深得多。爬虫的核心能力是看清数据在哪、想清楚怎么取工具只是手段。2. 环境准备与请求层实现细节2.1 环境安装与项目初始化这个项目只需要三个核心依赖requests、lxml、pandaspandas 用于便捷的 CSV 导出如果你不想装Python 自带的 csv 模块也能完成同样的事后面我会给出两种写法。pip install requests lxml pandas如果安装过程比较慢可以临时切换国内镜像源pip install requests lxml pandas -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后建议先用一个简短的脚本来验证请求链路确认网络环境和响应状态都正常。这一步虽然简单但能帮你提前把环境问题排查干净免得写了一大段代码才发现 requests 都装不上。import requests from lxml import html url https://books.toscrape.com/ resp requests.get(url) print(f状态码: {resp.status_code}) print(f页面大小: {len(resp.text)} 字符) doc html.fromstring(resp.text) book_count len(doc.xpath(//article[contains(class,product_pod)])) print(f首页图书数量: {book_count})如果看到状态码 200、页面大小正常、图书数量为 20说明链路已经通了。我实际调试的时候这一步几乎没出过问题但多花两分钟验证一下能省下后面的排查时间。2.2 请求头的伪装与约定虽然 Books to Scrape 官方设计为允许爬取但真实项目中请求头Headers的控制依然是基本功。我习惯在请求前设置 User-Agent、Accept-Language 和 Connection 这几个关键字段。HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: en-US,en;q0.9, Connection: keep-alive, }为什么 User-Agent 这么重要因为服务器会通过这个字段识别客户端的身份。requests 默认的 User-Agent 是python-requests/x.x.x有些服务器看到这个标识直接拒绝响应甚至可能把非法请求当成攻击行为。换成主流浏览器的标识可以大大降低请求被拦截的概率。这里要提醒一个容易被忽视的点Accept-Language 字段。我用这个站测试时不设置这个字段的话部分环境下的响应内容可能不是你预期的版本。设成en-US,en;q0.9可以确保服务器返回标准的英文页面内容方便你后续用 XPath 匹配类名。2.3 用 Session 复用连接我建议你使用requests.Session()而不是每次请求单独requests.get()。Session 对象会自动管理 Cookie并复用底层的 TCP 连接减少握手开销。虽然这个站在多页采集场景下压力不大但这是个值得养成的习惯。session requests.Session() session.headers.update(HEADERS)做好请求层之后你就可以放心地进入解析环节了。请求层的核心经验就是先确认你能稳定拿到 HTML再考虑解析顺序不能乱。3. XPath 解析与关键字段提取实战3.1 XPath 语法速览与调试方法XPath 是一种在 XML/HTML 文档中查找信息的查询语言在爬虫领域里比正则更直观。它的核心语法只有几个//表示在整个文档中查找不考虑层级位置/表示直接子节点[]表示条件过滤表示选取属性text()表示选取文本内容比如//article[contains(class, product_pod)]的含义是在整个文档中查找所有 class 属性包含 product_pod 的 article 节点。在这个站里每本图书就是一个这样的 article定位了这个节点后再在当前节点范围内用.//h3/a这样的相对路径提取具体字段。调试 XPath 表达式有一个很实用的方法直接在浏览器里按 F12 打开开发者工具在 Console 面板里用$x(你的表达式)来测试。比如输入$x(//article[contains(class,product_pod)]//h3/a)浏览器会直接返回匹配到的节点列表。先在浏览器里验证表达式能取到正确节点再写进 Python 代码能节约大量调试时间。3.2 核心字段的 XPath 表达式与原理我直接给出实战中最常用的一套 XPath 表达式以及对应的提取逻辑。# 书名取 h3 下 a 标签的 title 属性 title book.xpath(.//h3/a/title)[0].strip() # 详情页链接取 h3 下 a 标签的 href 属性并拼接完整 URL relative_url book.xpath(.//h3/a/href)[0] product_url https://books.toscrape.com/catalogue/ relative_url # 价格取价格节点文本此时的文本类似于 £51.77 price_text book.xpath(.//div[contains(class,product_price)]//p[contains(class,price_color)]/text())[0] price float(price_text.replace(\u00a3, )) # 评分取星级节点 class 属性类似 star-rating Three空格分割后取最后一项 rating_class book.xpath(.//p[contains(class,star-rating)]/class)[0].split()[-1] rating_map {One: 1, Two: 2, Three: 3, Four: 4, Five: 5} rating rating_map.get(rating_class, 0) # 库存状态 stock_text book.xpath(.//p[contains(class,instock)]/text())[0].strip()这里有几个非常容易踩的坑。价格的文本里包含的是英镑符号£它的 Unicode 编码是\u00a3。直接对它做float()转换会报错必须先把符号替换掉。有些人习惯用正则去匹配数字但这里价格格式非常规整直接用 replace 更简洁可靠。评分字段不在文本里而是藏在 class 属性中。这个设计比较特殊你得先定位到p节点的 class 属性再取空格分割后的最后一个词最后通过映射字典转成数字。如果直接取文本内容你会得到空字符串因为星级评分的图案是 CSS 生成的HTML 里根本没有对应的文本。库存状态text()获取到的文本里往往带有换行和空格需要 strip。这个站的库存状态比较规整基本都是 In stock 或 Out of stock清洗成本很低。3.3 单页解析函数的封装把上述逻辑封装成函数输入是 HTML 字符串输出是图书字典列表。这个设计思路是把请求和解析解耦方便后续调试和复用。def parse_book_list(html_text): doc html.fromstring(html_text) books doc.xpath(//article[contains(class,product_pod)]) results [] for book in books: try: relative_url book.xpath(.//h3/a/href)[0] product_url https://books.toscrape.com/catalogue/ relative_url book_id relative_url.split(_)[-1].split(/)[0] title book.xpath(.//h3/a/title)[0].strip() price_text book.xpath(.//p[contains(class,price_color)]/text())[0] price float(price_text.replace(\u00a3, )) rating_class book.xpath(.//p[contains(class,star-rating)]/class)[0].split()[-1] rating rating_map.get(rating_class, 0) stock_text book.xpath(.//p[contains(class,instock)]/text())[0].strip() stock In stock if In stock in stock_text else Out of stock results.append({ book_id: book_id, title: title, price: price, rating: rating, stock: stock, product_url: product_url, }) except Exception as e: print(f解析单本书失败: {e}) return results封装时我特意加了 try/except。这个习惯对爬虫项目非常重要。真实网站的页面结构不是百分之百稳定的某个节点为空或结构有细微变化都会导致 XPath 取不到值如果你不捕获异常整个脚本就会中断。加了异常处理后最多是跳过一条数据并打印日志采集流程不会断。如果你希望字段更完整可以在这里对每本书的详情页发一次请求补充 category 和 description 字段。但这是可选项我建议你先跑通列表页基础版本再考虑升级到详情页增强版。3.4 从详情页补充完整字段的进阶方案如果你决定爬取详情页这里有一个值得注意的细节Books to Scrape 的详情页 URL 结构是https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html而列表页里提取到的相对链接是a-light-in-the-attic_1000/index.html。所以拼接 URL 时需要在前面补上catalogue/这个前缀不能漏。详情页中 title 和 description 的 XPath 分别是# 详情页中的图书描述 desc detail_doc.xpath(//div[contains(id,product_description)]/following-sibling::p/text()) # 详情页侧边栏的分类信息 category detail_doc.xpath(//ul[contains(class,breadcrumb)]/li[last()]/a/text())这些表达式在浏览器里验证一下就能理解。完整跑一遍详情页会让总请求量从 50 次变成 1050 次注意控制一下并发别把压力一次性打上去。4. 分页循环与全站数据组装4.1 分页 URL 规则与构造Books to Scrape 的分页 URL 非常有规律。首页也就是第一页的 URL 是https://books.toscrape.com/从第二页开始格式为https://books.toscrape.com/catalogue/page-2.html第三页是page-3.html依此类推直到page-50.html。注意第一页和后续页的 URL 格式不一致这是个容易踩的坑。有人看到第二页的格式后想当然地用page-1.html去构造第一页结果发现打不开或拿到 404。实际处理中我的做法是先把第一页单独处理好然后从 2 到 50 循环拼接。你也可以先检查页面底部的 next 按钮来推断是否存在下一页next_link doc.xpath(//li[contains(class,next)]/a/href) if next_link: next_url https://books.toscrape.com/catalogue/ next_link[0]这个判断方法更通用因为真实网站的翻页机制比这个站复杂得多有时候是跳页、有时候是加载更多按钮。掌握了判断下一页是否存在的思路你面对动态站点时也更有头绪。4.2 完整采集主流程我给出的完整主流程是这样的建立 Session循环请求各页每页解析并追加到总列表最后统一导出 CSV。加上一个time.sleep(0.5)控制请求频率。import requests import time from lxml import html session requests.Session() session.headers.update(HEADERS) all_books [] # 第一页 resp session.get(https://books.toscrape.com/, timeout10) all_books.extend(parse_book_list(resp.text)) # 第 2 到 50 页 for page_num in range(2, 51): url fhttps://books.toscrape.com/catalogue/page-{page_num}.html resp session.get(url, timeout10) if resp.status_code ! 200: print(f第 {page_num} 页请求失败状态码 {resp.status_code}) continue books parse_book_list(resp.text) all_books.extend(books) print(f第 {page_num} 页完成当前累计 {len(all_books)} 本) time.sleep(0.5)这里有几个细节要说明。timeout 参数非常重要如果你不设置它请求一旦挂起脚本就会卡住无法继续。time.sleep 是控制采集频率的关键手段虽然这个站允许爬取但保持一个合理的频率是基本的采集礼仪也是专业爬虫工程师的习惯。我在跑这个项目时还发现了一个细节个别页面的响应可能因为网络波动导致解析为空所以我会在 parse_book_list 里检查 books 列表是否为空如果为空就打印警告日志。这能帮你快速定位是哪一页出了问题。4.3 边采集边保存与中断恢复在真实项目中数据量一旦大起来如果程序跑到第 40 页突然因为网络中断之前爬的都白费了那会很被动。所以我养成了一个习惯边采集边增量写入 CSV而不是全部爬完再一次性导出。具体的做法是在每一页解析完成后立即追加写入 CSV 文件。这样即使程序中断之前已经采集到的数据都安全落盘下次从中断的页码继续即可。import csv csv_file open(books_data.csv, w, newline, encodingutf-8-sig) writer csv.DictWriter(csv_file, fieldnamesFIELDS) writer.writeheader() for page_num in range(1, 51): # ... 请求与解析逻辑 ... for book in books: writer.writerow(book) csv_file.flush()这里用encodingutf-8-sig是个关键细节后面我会详细解释它和乱码问题的关系。5. CSV 结构化导出与编码避坑5.1 为什么选 CSV 作为导出格式CSV 是表格类数据的通用交换格式几乎任何数据分析工具Excel、Tableau、pandas、数据库导入工具都能直接读取。对于 1000 条规模的图书数据CSV 文件大小通常只有几百 KB用 Excel 打开也很流畅。如果数据量达到几十万条我会考虑转成 Parquet 或 SQLite但那不是这个项目的范畴。5.2 标准库 csv 模块写法的完整方案Python 标准库的 csv 模块足够应付大部分场景。下面是一份可以直接运行的完整导出代码import csv FIELDS [book_id, title, price, rating, stock, product_url] with open(books_data.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesFIELDS) writer.writeheader() for book in all_books: writer.writerow(book)我特别提醒一下newline这个参数。在 Windows 环境下如果你不指定它为csv 模块写入每一行时会在行尾多出一个空行。这个细节不写代码时不觉得真跑完一打开 CSV 看到满屏空行你会怀疑自己代码写错了。5.3 pandas 便捷导出方式如果你已经装了 pandas导出代码会更简洁而且数据类型转换更方便import pandas as pd df pd.DataFrame(all_books) df.to_csv(books_data.csv, indexFalse, encodingutf-8-sig)不过 pandas 方式也有个潜在问题当值里包含非常规字符时它的默认转义规则可能和你预期的不完全一致。测试下来纯文本场景没问题但如果你后面爬的是含大量特殊符号的网站建议还是用 csv 模块的自定义 quoting 参数来严格控制转义行为。5.4 用 Excel 打开 CSV 中文乱码的全套解法这是这个项目里最容易让新手崩溃的问题明明爬回来的数据在 Python 里打印出来是正常中文导出成 CSV 后用 Excel 一打开全是乱码。原因其实很简单标准 CSV 文件默认是不带编码标识的Excel 在 Windows 下打开 CSV 时默认按 ANSI 编码比如 GBK去解码而 Python 写入时一般用 UTF-8两边不一致就会出现乱码。解决方案就是写入文件时指定encodingutf-8-sig。utf-8-sig会在文件开头自动加入一个 BOM字节序标记Excel 看到 BOM 就能正确识别为 UTF-8 编码。我在项目里统一用这个编码从源头上杜绝了乱码问题。顺便说一句如果你用 pandas 导出也要同样处理df.to_csv的 encoding 参数传utf-8-sig即可。这个坑不踩一次真的不会长记性踩完之后你以后写所有爬虫导出都会记得加这个参数。6. 常见问题排查与合规提醒6.1 高频问题速查表我按实际调试经验整理了下面这些问题几乎覆盖了这个项目所有常见的卡点。问题表现可能原因解决方案请求返回 403User-Agent 太暴露设置浏览器级别的请求头XPath 取到空列表表达式路径有误用浏览器$x()验证表达式价格字符串无法转 float英镑符号未替换先replace(\u00a3, )评分取不到值没从 class 属性里取用class取属性再处理CSV 打开中文乱码编码不匹配写入时用utf-8-sig运行到某页中断网络不稳定加异常处理 边采集边保存总页数变多新书入库用 book_id 去重重复跳过6.2 调试技巧与日志设计爬虫调试的核心思路是逐步缩小范围。遇到解析结果不对先打印出整个 HTML 段落看看结构遇到请求失败先打印出状态码和响应头而不是急着改代码。我习惯在关键节点加上带页码的日志输出例如第 12 页完成累计 240 本。这样跑完一遍就能快速判断数据是否按预期增长如果某页数量异常日志会直接给出线索。6.3 爬虫的边界与合规意识这个项目完结后我需要认真提醒一件事Books to Scrape 是一个允许爬取的练习站它可以让你放心地练习所有技能。但真实的网站不是这样。做任何爬虫项目都要先检查对方的 robots.txt 文件、了解网站的服务条款并且控制好采集频率不要影响对方网站的正常运行。采集到的数据如果涉及个人信息或版权内容使用前也要审慎评估。合规不是一句空话而是这一行能长期做下去的基础。能力越强边界意识越重要。7. 项目扩展与个人体会如果这个项目你已经完整跑通了可以试着在它的基础上做几件有意思的扩展。第一把 CSV 导入数据库比如 SQLite 或 MySQL写几条 SQL 查询试试比如按评分排序找出所有五星图书第二给爬虫加一个简单的代理池和异常重试机制模拟真实生产环境的稳定性要求第三用 pandas 对结果做一次数据分析算一算全站图书的平均价格、评分分布这些都是很自然的下滑方向。我个人实际跑这个项目最深的一个体会是爬虫项目的成败并不在于用了多少高级框架而在于对目标站点的结构理解是否透彻。你花 20 分钟用浏览器看清楚页面布局比闷头写 2 小时代码有用得多。Books to Scrape 的价值就在于它让你在低风险的环境里把这个先观察、再动手的流程刻进习惯里。最后再分享一个小技巧把解析函数和请求逻辑分开写会让这个项目的代码结构清晰很多也为后续扩展留好余地。保持这个习惯你后面接更大的采集任务时会感谢现在的自己。