
如果你问一个写过爬虫的Python开发者“第一个拿来练手的网站是什么”十有八九会听到同一个名字Books to Scrape。我这些年带新人入门每次都用它当靶场——页面结构规整、几乎没有反爬门槛、布局常年不变但该有的爬虫要素一个都不少列表页、详情页、分页、编码、请求头、数据落盘。跑通这一条链路你手里就有了一套能迁移到绝大多数静态站点的通用骨架。这篇文章就用手把手的方式把从零到一爬取 Books to Scrape 全站 1000 本书的完整过程拆开揉碎包括每一步为什么这么做、哪些地方容易踩坑、遇到故障怎么排查。适合刚学完 Python 基础、准备上手第一个正经爬虫项目的人也适合自学中途卡住、想看看别人怎么处理边界情况的朋友。我的目标很明确跟着这篇文走完你能把这站的书籍名称、价格、评分、库存、链接、描述全部存成一张干净的 CSV 表格。1. 认识Books to Scrape为什么它是练手首选1.1 目标站点长什么样Books to Scrape 是 Scrapinghub现在的 Zyte官方维护的一个练习型网站专门给爬虫学习者当靶子用。站点的定位写得很直白“我们是一个让你练爬虫的网站请不要太用力”——它内置了 50 页、每页 20 本、合计 1000 本书的数据每本书都有独立的详情页还贴心地做了模拟真实站点的交互元素比如一个“现在允许 cookies 吗”的小弹窗。弹窗本身只是在测试你对 cookie 处理的敏感度对于静态内容抓取而言没有实际影响知道有这个机制就行。为什么我坚持推荐这个站而不是直接拿淘宝或者京东练手第一是法律风险。电商平台有严格的访问控制常规爬虫去碰它们容易触发风控甚至给自己惹上麻烦。Books to Scrape 是官方明确允许爬虫访问的站点学习期间怎么折腾都行。第二是结构固定。它从 2016 年上线到现在页面结构几乎没有大改教程里写的选择器过了几年还能用这点对新手极其友好——你排查问题时不会怀疑是网站改版了。第三是数据形态丰富列表页里有书名、链接、价格、评分、库存详情页里还有产品描述这不就把爬虫最常遇到的几种数据提取场景全涵盖了。1.2 技术栈选择requests BeautifulSoup 的取舍确定了目标站点接下来是选工具。市面上爬虫工具五花八门我从学习路径的角度说句实话绝大多数人的第一套爬虫都会是requests BeautifulSoup而不是 Scrapy 或者 Selenium。为什么因为 Books to Scrape 是纯静态页面所有的数据都在 HTML 源码里浏览器不需要额外执行 JavaScript 去渲染内容。这意味着我们用 requests 发送 HTTP 请求拿到的 HTML和你在浏览器里右键查看源代码看到的东西是一致的直接在字符串层面解析即可。Scrapy 是个功能完备的爬虫框架但它的学习曲线体现在项目结构、下载中间件、管道等机制上对新手来说太重了Selenium 则是用来驱动真实浏览器的适合处理动态渲染和复杂交互的页面拿它爬一个纯静态站就像开着铲车去搬鸡蛋没必要。我还坚持用 requests 而不是 httpx虽然 httpx 更现代、支持异步但 requests 是生态最成熟、问题排查最方便的库——你遇到报错网上一搜一大把答案。解析层选了 BeautifulSoup 加 lxml 解析器Beautifulesoup 的 API 设计非常贴近人的直觉写 find、select 这类方法查元素时像在操作一个迷你 DOM而 lxml 作为底层解析器速度足够快C 语言核心在解析大量标签时不会拖后腿。数据落盘我选了 CSV 而不是数据库1000 条数据用文本文件就够了打开方便后续想导入 Excel 也顺手。这套组合拳的打法简单直接跑通了之后你再看 Scrapy 的架构会发现很多概念其实是相通的。先学会走再去跑这个顺序很关键。2. 环境准备从零搭建一个干净的爬虫工程2.1 创建虚拟环境与安装依赖很多新手会跳过虚拟环境这一步直接用全局 Python 装库我当时也这么干过。后果是所有项目的依赖堆在一起版本冲突之后哭都来不及。现在养成好习惯每个项目独立虚拟环境互不干扰。我先假设你已经装好了 Python 3.8 以上的版本。在项目目录下打开终端执行mkdir books-scraper cd books-scraper python -m venv venvWindows 下激活虚拟环境是venv\Scripts\activatemacOS 和 Linux 是source venv/bin/activate。激活后命令行前面会出现(venv)前缀这就说明当前已经切到了独立的 Python 环境里。紧接着安装依赖pip install requests beautifulsoup4 lxml这三个库的分工很明确requests 负责发请求拿网页源码beautifulsoup4 负责解析 HTMLlxml 是解析引擎。我提醒一句如果你在解析中文页面时遇到乱码lxml 对编码的推断通常比 Python 自带的 html.parser 更可靠这也是我坚持装它的原因之一。安装完成后可以用pip list检查版本确保没有装错。虚拟环境的好处说白了就是隔离。哪怕你电脑上还有别的项目在用某个库的老版本也不影响这个仓库里的独立空间这对爬虫这种要装很多第三方库的工种来说属于基本素养。2.2 发送第一个请求并确认页面编码环境搞定了先写一段代码探探路。我们的目标是请求 Books to Scrape 的主页面https://books.toscrape.com/把 HTML 拿回来看一眼。import requests url https://books.toscrape.com/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout10) print(resp.status_code)第一次跑你大概率会看到输出200。这就是“请求成功”的信号。这里有几个新手容易忽略的点。第一是timeout参数。requests 的 get 方法默认是没有超时限制的也就是说如果服务器不响应程序会一直傻等。给一个 10 秒的限制请求超时会抛出异常咱们在后续代码里统一捕获处理比僵死在那里强得多。第二是编码问题。虽然 Books to Scrape 的页面声明了utf-8requests 也能自动识别但保不齐有些站点的声明的 charset 是旧编码或者压根没声明。我习惯手动指定resp.encoding resp.apparent_encodingapparent_encoding是 requests 根据字节内容推断出的编码对中文站尤其好用。Books to Scrape 是纯英文站这行加不加效果一样但从一开始养成习惯后面遇到中文站你就知道省了多少事。拿到响应后把前 300 个字符打印出来看看或者保存到本地 HTML 文件里用编辑器打开确认内容完整再往下走。我看到很多人跳过这一步直接写解析代码结果 selector 怎么写都匹配不上排查半天发现是请求就失败了浪费时间。先确认原料没问题再谈加工。3. 单页解析把第一页的20本书榨干3.1 解剖书架页的DOM结构目标列表页的第一页既可以从https://books.toscrape.com/进入也可以直接用https://books.toscrape.com/catalogue/page-1.html两者内容等价。为了方便后续分页我统一用带 page-1 的 URL 作为起点。在浏览器里按 F12 打开开发者工具先用“查看元素”功能点一下任意一本书的标题你会看到每本书都躺在下面的结构里article classproduct_pod h3 a href../../../the-grand-design_405/index.html titleThe Grand DesignThe Grand Design/a /h3 p classstar-rating Three.../p p classprice_color£51.77/p p classinstock availabilityIn stock/p /articlearticle.product_pod就是每一本书的容器所有关键字段都能在这个容器里找到。解析的基本思路就是先找到所有article.product_pod元素再逐个字段从里面取值。这里要特别提醒一个细节你从开发者工具里看到的a标签的href是../../../the-grand-design_405/index.html这样的相对路径。直接用这个字符串拼 URL 是会出问题的因为浏览器会自动做路径归一化而 requests 不会。我经验里的做法是剥掉前面的../../前缀然后拼上目录根from urllib.parse import urljoin base https://books.toscrape.com/catalogue/ href the-grand-design_405/index.html # 剥掉了相对路径前缀 full_url base href如果你图省事直接urljoin(https://books.toscrape.com/catalogue/page-1.html, ../../../the-grand-design_405/index.html)解析出来的路径大多不是你想要的。不要在这个问题上纠结按照“剥前缀 拼绝对路径”的方式来简单可靠。3.2 字段解析书名、价格、评分、库存与详情链接现在写函数把单本书的解析逻辑封装起来。我之前学到的一个重要教训是解析代码不要写在零散的脚本里每个操作都要能单独测试。from bs4 import BeautifulSoup def extract_book(container): title container.h3.a[title] price_text container.find(p, class_price_color).text price float(price_text.replace(£, )) link container.h3.a[href] if link.startswith(../../../): link link.replace(../../../, ) detail_url https://books.toscrape.com/catalogue/ link rating_class container.find(p, class_star-rating)[class] rating rating_map.get(rating_class[1], 0) stock_text container.find(p, class_instock).text.strip() return { title: title, price: price, rating: rating, stock: stock_text, detail_url: detail_url, }这里面的几个解析细节都值得展开讲讲。价格字段在页面上显示为£51.77英镑符号前面还可能有空格我处理时先strip()再.replace(£, )最后转成float这样存进 CSV 之后就能直接做数值运算比如算平均价、找最贵的一本。如果留的是字符串£51.77后续排序分析还得二次清洗麻烦。评分字段是最容易踩坑的地方。HTML 结构里的p classstar-rating Three不会给你数字3而是把英文单词Three作为 CSS 类名。所以解析时要拿到类的列表第二个元素才是评分级别。rating_map { One: 1, Two: 2, Three: 3, Four: 4, Five: 5, }用字典做映射比一堆 if-else 干净得多。万一网站后续把评分类名改了查不到就返回 0程序不至于崩这个兜底逻辑很实用。库存字段是p classinstock availabilityIn stock/p取文本后.strip()就得到干净的库存状态。列表页直接暴露了库存信息不用进详情页省了一次请求。3.3 详情页里的产品描述要不要爬列表页的字段虽然够用但“产品描述”这个经典字段只在详情页里出现。Books to Scrape 的每个详情页都有类似下面的结构div idproduct_description h2Product Description/h2 /div pIt is a truth universally acknowledged.../p也就是说描述文本在div idproduct_description之后的第一个p标签里。解析代码是def extract_description(detail_soup): desc_tag detail_soup.find(div, idproduct_description) if desc_tag: return desc_tag.find_next_sibling(p).text return 这段代码我最想强调的是**用if desc_tag做判断而不是直接desc_tag.find_next_sibling(p)**。因为有些书可能没有描述块直接调find_next_sibling会抛 AttributeError。我刚入坑时在这里翻过车以为所有页面结构都一样。凡是解析外站数据你必须时刻假设“预期中的元素可能不存在”这既是对站点结构的敬畏也是让代码稳定的基本素养。单本书的解析逻辑到这里就完整了通过列表页的链接进详情页再提取描述然后把所有字段汇总。一个函数管一本书后面要做 1000 本书只要循环调用就行。4. 全站分页抓取50页1000本书落盘CSV4.1 摸清分页URL规律Books to Scrape 的分页 URL 是个很好找的规律。你打开第 1 页地址是https://books.toscrape.com/catalogue/page-1.html点下一页变成page-2.html再下一页变成page-3.html。这不是所有网站都这么规矩有的会用?page2这类查询参数有的会通过 POST 请求加载数据但找到规律的方法是通用的多翻几页观察 URL 的差异再用一个循环变量去代换。我写代码前习惯先手动验证两个 URL第一页和最后一页。Books to Scrape 总共 1000 本每页 20 本正好 50 页所以最后一页是page-50.html。手动访问确认都能打开再写循环就有底了。4.2 完整抓取脚本循环、限速、异常兜底把前面的单页解析逻辑铺开再加一个 for 循环全站抓取的脚本就成型了。下面是我在实际项目里用过的简化版本import time import requests from bs4 import BeautifulSoup all_books [] for page in range(1, 51): url fhttps://books.toscrape.com/catalogue/page-{page}.html resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f第{page}页请求失败状态码{resp.status_code}) continue soup BeautifulSoup(resp.text, lxml) articles soup.select(article.product_pod) print(f正在抓取第{page}页找到{len(articles)}本书) for article in articles: book extract_book(article) # 这里往下接详情页、描述等逻辑 detail_resp requests.get(book[detail_url], headersheaders, timeout10) detail_soup BeautifulSoup(detail_resp.text, lxml) book[description] extract_description(detail_soup) all_books.append(book) time.sleep(0.5) print(f全部完成共{len(all_books)}本)这段代码里有几个设计选择值得说道说道。异常兜底很重要。我在每页请求后都检查了status_code失败就跳过当前页而不是让整个程序崩溃。很多新手写的爬虫一旦遇到一个异常页面就整体挂掉前面的劳动全部作废。真实世界里网络不稳定是常态容错比完美更宝贵。限速不是可选项。time.sleep(0.5)是每次请求之间的间隔时间。有人觉得 1000 本书也就几十秒的事没必要睡。但你想想你访问的是一个专供学习的练习站我们完全可以一秒抓完。可如果面对的是一个真实的生产站点没有节制的请求频率就是对人家服务器的不负责轻则你的 IP 被封重则影响站点性能。做爬虫的人要有最基本的道德感哪怕是允许爬的站点也要控制频率不要给别人添麻烦。0.5 秒的间隔50 页也就多花 25 秒换来的是安全和体面。详情页要不要每个都请求。我这里选择每一本书都进详情页拿描述所以最终请求总数是列表页 50 次加详情页 1000 次共 1050 次请求。这也是爬虫设计里常做的取舍如果详情字段不是必须的可以只爬列表页速度翻倍如果需要完整描述那就要接受额外的请求量。Books to Scrape 的一次请求大约几十毫秒总共不到两分钟就能跑完完全可以接受。4.3 数据落盘UTF-8-sig与CSV字段设计全部数据装进all_books列表之后最后一步是落盘。CSV 是最通用的格式Excel 可以直接打开pandas 也能轻松读。import csv fields [title, price, rating, stock, detail_url, description] filename books.csv with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfields) writer.writeheader() writer.writerows(all_books)这段代码里最能帮你避坑的是encodingutf-8-sig。如果写成普通的utf-8Windows 用户用 Excel 打开 CSV 时中文大概率会乱码原因在于 Excel 默认按 GBK 或本地编码去解读文件头。utf-8-sig会在文件开头写入 BOM字节顺序标记Excel 一看到 BOM 就知道这是 UTF-8 编码乖乖把中文字符正常显示。这个细节我在早期项目里踩过当时还以为抓下来的数据是坏的后来才发现是编码没写对。newline也很关键。如果不加Windows 平台上写 CSV 时每行后面会多一个空行这是 Python 的 CSV 模块和 Windows 换行符共同作用的结果。加上就干净了。最终生成的文件包含 6 个字段书名、价格、评分、库存、详情链接、描述。1000 行数据用 Excel 打开后还能直接按价格排序、按评分筛选你会发现一张原本平平无奇的练习表能玩出很多花样。5. 上路之后踩过的坑问题排查与避坑技巧5.1 一个User-Agent引发的403新手最常见的报错不是代码逻辑错了而是请求直接被服务器拒绝。我第一次爬这个站时以为 requests 默认的请求头就够了结果一跑状态码清清楚楚写着403 Forbidden。原因很简单服务器会检查请求头里的User-Agent字段判断来访者是不是一个真实浏览器。缺了这个字段或者字段值是一个明显非浏览器的字符串就会被判定为可疑流量。解决办法是像我在 2.2 节写的那样伪造一个常见的浏览器 UAheaders { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }这个字段怎么找一个像样的打开浏览器按 F12切到 Network 面板随便点一个请求在 Request Headers 里就能看到真实的 UA 字符串直接复制过来用。这个经验适用于几乎所有站点不仅仅是练习站。5.2 解析不到数据的三种常见死因BeautifulSoup 解析结果为空这是所有爬虫学习者都经历过的崩溃时刻。按我的排查经验按顺序检查这三个原因。第一请求返回的源码里根本没有你要的元素。很多人直接对resp.text做 BeautifulSoup但从没打印过resp.text的开头部分。正确做法是先输出或保存 HTML查一下你找的目标标签是否真的存在。如果存在说明你选错选择器了如果不存在说明请求可能被反爬拦截返回的是一个错误页或者验证页。第二页面内容由 JavaScript 动态渲染。Books to Scrape 不存在这个问题但很多真实站点有。特征是浏览器里能看到数据源码里却找不到。这种情况 requests 拿不到得换 Selenium 这类浏览器自动化工具。第三解析器选择错了。BeautifulSoup 默认用 Python 标准库的html.parser有些复杂或非标准的 HTML 结构它解析不好导致某些节点找不到。我用的lxml解析器对标准 HTML 的容错能力更强选这个能少掉一半的解析问题。5.3 星级评分不是数字怎么转成数值我前面提过评分字段的类是star-rating Three直接拿文本取不到数字。这里再展开说一个细节用[class]取类名时得到的可能是一个列表也可能是一个字符串。BeautifulSoup 对 class 属性的处理是如果 class 有多个值返回列表只有一个值返回字符串。为了稳妥应该统一做兼容处理class_list container.find(p, class_star-rating)[class] rating_word class_list[1] if isinstance(class_list, list) else class_list别小看这个判断。如果你跳过isinstance直接取[1]在一个 class 只有一个值的页面上就会IndexError。真实站点千奇百怪这种防御性写法能撑住很多意想不到的边界情况。5.4 抓了一半断掉断点续抓的思路全站抓取最容易碰到的尴尬事是跑到第 37 页时网络抖了一下程序异常退出又要从头跑 50 页。虽然这个练习站数据量不大但养成断点续抓的习惯对真实项目很有价值。最简单的做法是边抓边写而不是把 1000 条数据全部攒到内存里最后一次性写文件。每处理完一本书立刻追加写入 CSV这样即使程序中途崩溃已经抓完的数据都安全落盘了with open(books.csv, a, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfields) for book in batch: writer.writerow(book)更进阶一点的做法是在文件里记录“当前抓到了第几页”下次启动时从断点处继续。思路是启动时读进度文件for 循环的起始值从1改成last_page 1。我以前爬过一个上百万条数据的站就是靠这种机制扛过了无数次网络中断这已经是生产级爬虫的基本素养了。写在最后的一点体会把 1000 本书完整跑下来的那一刻我看到终端输出“全部完成共1000本”心里其实没什么成就感因为我知道这个项目真正值钱的地方不在数据量而在于我完整经历了“请求—解析—落盘”这条主链路并且把每个环节的坑都踩了一遍。后面我学 Scrapy 时发现框架的很多概念——爬虫中间件、请求队列、Item Pipeline——全都能映射到我用 requests 写的那套逻辑上只是换了个更工程化的壳子而已。根据我个人带新人的经验爬虫入门最忌讳的就是贪多嚼不烂。今天学 Selenium明天学 Scrapy后天又去看分布式爬虫结果一个完整项目都没跑通。你不如把 Books to Scrape 这个站吃透把列表页、详情页、分页、编码、限速这些基本功练扎实再往深走就顺理成章了。项目跑完后你还可以试着加些功能比如把图书分类页也抓了统计不同评分区间的价格分布或者用 matplotlib 画一张 1000 本书的价格直方图——练习项目的天花板从来不是网站而是你的想象力。