简介这份资源收录了豆瓣图书爬虫脚本与11个分类整理好的Excel数据文件面向正在学习Python网络爬虫和数据分析的初学者也适合需要图书榜单数据的个人开发者。爬虫部分基于requests与BeautifulSoup编写完整展示了从构造请求、处理响应、解析HTML到提取书名、作者、出版社、评分等字段的流程并针对豆瓣图书页面结构给出了定位关键节点的思路Excel数据按小说、历史、科学、编程、金融、心理等主题分表存储字段包含分类、图书名称、作者、出版社和平均评分便于直接进行统计分析与二次利用。脚本中还包含数据清洗与保存逻辑可利用pandas将抓取结果写入Excel并借助zipfile完成文件打包方便传输与归档。资源包共12个文件含1个.py源码和11个.xlsx数据集压缩后大小12.34MB目录结构清晰可快速定位到对应分类。目前已有2051人下载学习适合用来练习网页抓取、pandas数据处理以及Excel读写也可作为图书推荐、市场研究或个人收藏管理的基础数据。1. 豆瓣图书数据不好拿一个晚上输出一张干净的 xlsx你一定遇到过这种场景想按评分整理一份年度书单或者做个小课题需要几百条图书的出版社、作者、评分数据打开豆瓣图书页面一条条复制结果复制到第 40 条就乱了。这个项目就是干这件事的——把豆瓣图书的列表页和详情页数据爬下来清洗后直接输出成一张可以丢进 Excel 或者导入数据库的 xlsx 文件。它只依赖 Python 的几个基础库不装数据库、不启服务适合想快速拿数据而不是想研究分布式爬虫的开发者。某同学用它一个晚上跑完了 top250 全量数据拿到手就是一张带标题、作者、出版社、出版日期、评分、评论数的表格。2. 技术选型与工程结构Requests BeautifulSoup openpyxl 的最小可用组合2.1 技术选型为什么不用 Scrapy第一次看到这个项目不少人会问“怎么不用 Scrapy”。项目原版的定位很清楚目标只有一个站点、数据量几百条到几千条、跑完即止没有增量抓取和分布式调度的需求。这种情况下上 Scrapy 等于给自己找活干——要建工程目录、写 Item Pipeline、配置 Middleware光是把项目框架搭起来的时间就够把脚本写完两遍了。原版用的是 Requests 发请求、BeautifulSoup 解析 HTML、openpyxl 写 xlsx三者组合非常直接。Requests 管网络交互BeautifulSoup 处理页面结构openpyxl 负责把数据落成 Excel 能直接打开的电子表格。没有中间件、没有消息队列一个 for 循环加上异常处理就能跑通全流程。这种“最小依赖”的选择降低了上手的门槛也方便二次开发时逐段替换——比如想把解析层从 BeautifulSoup 换成 lxml只改一个函数就够。我一般会把这种项目称为“三明治架构”请求层在最上面负责拿页面解析层在中间负责把 HTML 变成结构化字段存储层在最底下负责写文件。清晰的分层让调试变得容易哪一层出了问题就单独测哪一层这也是这个项目值得拆开看的原因。2.2 目录结构与模块职责原版目录非常朴素核心文件就三个douban_books/ ├── crawler.py # 请求与解析主逻辑 ├── writer.py # xlsx 写入模块 └── config.py # 配置URL、Headers、延时参数crawler.py 是入口里面定义了抓取主函数。它从 config.py 读取目标 URL 和请求头请求页面后把 HTML 交给解析函数解析结果组装成一个字典列表最后调用 writer.py 的保存函数落地。config.py 虽然短但把 URL、请求头、延时时间这类“改了又改”的东西独立出来比写死在爬虫文件里好改一万倍——尤其是换数据源的时候只动 config 就够了。writer.py 单独存在的意义在于它处理了“写 Excel”这个容易被忽视的脏活列宽设置、日期格式、文本中的特殊字符处理。如果把这些逻辑混进爬虫文件代码会越来越难读。原版的这种拆分虽然简单但对刚想接触爬虫工程化的开发者来说是一个很好的示范哪怕只写一百行代码也要把“拿数据”和“存数据”分开。2.3 数据模型设计xlsx 表结构怎么定爬虫的输出结构直接决定后期用数据的效率。原版设计了一个扁平化的表结构每一行是一条图书的完整信息核心字段如下字段名类型说明titlestring书名authorstring作者多位作者用斜杠分隔publisherstring出版社pub_datestring出版日期保持原页面格式ratingfloat豆瓣评分保留一位小数comment_countint评论数量用于衡量热度introstring内容简介的前 200 字这个结构最明显的好处是“一行动一条”不需要二次透视或转置。author 用字符串而不是数组存多位作者是为了避免 xlsx 中嵌套数据处理麻烦intro 用前 200 字而不是全文是因为大多数分析场景只需要前几句判断图书主题全文会让 xlsx 文件体积膨胀好几倍。这里有个细节值得注意pub_date 保留为字符串。很多爬虫会把日期解析成 datetime 对象再写进 Excel但这会导致部分 Excel 版本显示成乱码或“########”而且不同语言 Excel 的日期解析规则不一致。原版选择直接保存原始格式省去一层烦恼等数据导入到统计软件时再统一转换。这是典型的“爬虫只管拿到正确的原值”思路格式化是消费数据的人的事。3. 核心模块拆解从请求到落盘的完整链路3.1 请求层无状态 Session 与限速逻辑原版请求层写得很克制没有用 requests.Session而是直接用 requests.get 每次独立请求。这个选择基于一个简单判断目标页面没有登录态维持的需求Cookie 只在请求头里带一个就行。Session 的持久连接在这种低频请求下收益不大反而会让代码多一层生命周期管理。import requests import time import random from config import HEADERS, BASE_URL, DELAY_RANGE def fetch_page(url, retry3): for attempt in range(retry): try: response requests.get(url, headersHEADERS, timeout10) if response.status_code 200: return response.text elif response.status_code 403: print(f请求被拒绝: {url}, 尝试 {attempt 1}) else: print(f异常状态码 {response.status_code}: {url}) except requests.RequestException as e: print(f请求异常: {e}, 尝试 {attempt 1}) time.sleep(random.uniform(*DELAY_RANGE)) return Nonefetch_page 有三个关键设计。第一是重试机制网络请求失败是常态重试 3 次能把大多数临时性问题过滤掉第二是超时限制timeout10 避免了某个页面卡住时整个脚本无限期等待第三是应答状态码的判断403 单独处理也方便后续观察是否触发了站点拦截。调用时会配合延时策略每次请求后随机休眠 DELAY_RANGE 区间内的秒数。随机延时比固定延时更有效固定间隔会被识别成机器行为而随机值模拟了人类的操作节奏。DELAY_RANGE 在 config.py 中定义为 (1.5, 3.5)即每次请求间隔平均两秒多跑完 250 条数据大约需要十分钟这个速度既不会让目标站点反感也不会让使用者等太久。3.2 解析层列表页与详情页的字段抽取解析是这个项目最有含金量的部分。豆瓣图书的列表页结构非常稳定每一条数据包含书名、评分、评价人数和引用语。但出版社、作者、出版日期这些信息隐藏在详情页如果每条都去请求详情页请求量会从 10 页暴增到 250 次对网络和站点压力都更大。from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, html.parser) items [] for tr in soup.select(table.item): title_elem tr.select_one(a[href*subject]) rating_elem tr.select_one(span.rating_nums) comment_elem tr.select_one(span.pl) link title_elem[href] if title_elem else None items.append({ title: title_elem.text.strip() if title_elem else , rating: float(rating_elem.text) if rating_elem else 0.0, comment_count: extract_comment_count(comment_elem), detail_url: link }) return itemsparse_list_page 的输入是列表页 HTML 字符串输出是一个字典列表。select 方法用 CSS 选择器定位元素table.item 对应列表中的一行这些选择器是原版项目中最宝贵的资产——改动任何一处 CSS 类名整个解析就会失效。extract_comment_count 是一个提取评价人数的小函数用正则从“(21345人评价)”这样的文本中抠出数字返回 int 类型方便后续排序。有了列表页数据之后还需要补充详情页的字段。这部分原版做了一个聪明的取舍对列表页中的每一条先检查数据是否已经完整只在缺少 publisher、pub_date、author 这三个字段时才发起详情页请求。这个条件判断减少了不少无效请求在实际运行时能把详情页请求量降低三到四成。3.3 存储层openpyxl 批量写入与错误回退存储层是原版里最有“工程感”的部分。很多爬虫教程喜欢每爬一条就写一次 Excel原版没有这么干而是先把所有数据追加到一个 list 里等全部抓取完成后再统一写入。原因是 openpyxl 每次打开和保存工作簿的耗时都在秒级如果爬取过程中断反复写入还可能产生临时文件残留。from openpyxl import Workbook from openpyxl.styles import Font, Alignment def save_to_xlsx(data, filenamebooks.xlsx): wb Workbook() ws wb.active ws.title 图书数据 headers [书名, 作者, 出版社, 出版日期, 评分, 评论数, 简介] ws.append(headers) for col, _ in enumerate(headers, start1): ws.cell(row1, columncol).font Font(boldTrue) for row in data: ws.append([ row[title], row[author], row[publisher], row[pub_date], row[rating], row[comment_count], row[intro] ]) ws.column_dimensions[A].width 30 ws.column_dimensions[G].width 60 ws.freeze_panes A2 wb.save(filename) print(f数据已保存到 {filename})save_to_xlsx 的逻辑分四步创建 Workbook 并激活默认工作表写入表头并加粗循环把所有数据 append 到工作表最后调整列宽并冻结首行。freeze_panes A2 让首行表头在滚动时保持可见这对后期在 Excel 里翻数据非常实用。这个函数有一个很多人忽略的设计表头用中文字段名用英文。英文 key 保证代码里处理数据结构时不会乱中文表头保证使用者打开 Excel 时不需要再做一次字段映射。这个“内外有别”的思路是原版在处理“面向程序的数据”和“面向人的数据”之间做出的平衡。4. 反爬应对与稳定性控制让脚本跑到天亮4.1 请求头替换与 Cookie 传递豆瓣对异常请求的识别不算苛刻但基础防护还是有的。原版在 config.py 中维护了一份请求头模板关键字段是 User-Agent 和 Referer。如果直接使用 Python 默认的 User-Agent服务器端很容易识别出这不是浏览器访问返回 403 的概率很高。HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.5, Connection: keep-alive, Referer: https://book.douban.com/ }这里的 User-Agent 只是一个基础标头没有包含 Chrome 的具体版本号。实际使用时如果你发现 403 频率仍然很高可以打开浏览器开发者工具在 Network 面板里复制自己当前浏览器的完整 User-Agent 字符串替换进去。Referer 字段的作用是告诉服务器“我是从豆瓣图书页面跳转过来的”很多站点的反爬规则会校验 Referer 是否合法不带的请求权重会被调低。Cookie 传递采用最朴素的方式在 config.py 中预留了 COOKIE 字段登录状态获取的数据会有额外字段比如“在读”“想读”数量。把浏览器中登录后的 Cookie 粘贴到该字段requests.get 时通过 cookiesCOOKIE 传入就能拿到登录后才能看到的数据。这个思路非常适合处理“需要登录但不想做登录流”的场景省去了模拟登录时验证码识别的麻烦。4.2 失败重试与断点续采爬虫跑三十分钟后因为一条网络错误中断又得从头再来是所有爬虫开发者都遇到过的“血泪经验”。原版对这个问题做了两层防护。第一层在请求函数内部也就是前面看到的 retry3 循环第二层在抓取主流程中每解析完一条数据就把当前进度记录到本地文件。def crawl(book_urls, checkpoint_fileprogress.txt): completed set() if os.path.exists(checkpoint_file): with open(checkpoint_file, r) as f: completed set(f.read().splitlines()) for url in book_urls: if url in completed: continue html fetch_page(url) if not html: continue data parse_detail_page(html) save_to_xlsx([data], modeappend) with open(checkpoint_file, a) as f: f.write(url \n) time.sleep(random.uniform(*DELAY_RANGE))checkpoint_file 保存的是已经处理过的详情页 URL下次启动时直接跳过这些链接实现断点续采。这个方案比“从第 N 页继续爬”更可靠因为列表页的排序可能随时间和登录状态变化而 URL 是稳定的唯一标识。配合前面批量写入的 save_to_xlsx每条数据在成功后立即追加写入单条数据的丢失风险降到了最低。注意这里的 save_to_xlsx modeappend 和上一章的批量保存是两个不同的函数——一个负责从头生成完整 Excel一个负责往已存在的文件里追加行。实际开发中我一般会把这两个函数拆成两个接口避免一个函数同时承担两种职责导致参数混乱。4.3 延时策略怎么限速不封号限速这件事没有标准答案原版的策略值得参考动态延时 错误退避。动态延时是随机区间 (1.5, 3.5) 秒错误退避是当某次请求失败后下一次请求前的延时自动拉长到 10 秒避免在站点可能已经注意到来异常流量时继续高频请求。def adaptive_sleep(base_delay, last_failed): if last_failed: time.sleep(base_delay * 3) else: time.sleep(base_delay * random.uniform(0.8, 1.5))adaptive_sleep 传入基准延时和控制最近一次是否失败的布尔值。失败后延时翻三倍给服务器一个“冷静期”。这个机制比单纯的交替快慢请求更贴心的地方在于它只在出错时主动降速正常时不牺牲采集效率。有些开发者一上来就固定 sleep(5)跑两个小时还没跑完一半这种做法其实是把“限速”理解成了“无脑慢”。限速的核心逻辑是让请求频率看起来像人而不是脚本人不可能每 1.5 秒一次精确点击也不会在页面打不开时继续以同样频率点。把这个理解内化到代码里限速策略就不会写成死板的一句话了。5. 避坑清单原版脚本最容易翻车的 5 个地方5.1 现象请求返回 418 而不是 200第一次运行原版时控制台打印的响应码 418 而不是 200页面内容为空。原因不是脚本错了而是目标服务器识别到“不是浏览器的请求”后拒绝了访问。这是最常见的反爬拦截方式直接抛给请求方一个状态码很多爬虫新手会误以为是 HTTP 协议问题。解决方法是更新 config.py 中的 User-Agent 字符串尤其注意不要用过于old的版本号——服务器端会维护一份“过期 User-Agent”黑名单Chrome 80 以下的版本号很容易被标记。把 UA 换成当前主流浏览器版本后问题解除。5.2 现象Excel 文件打不开提示“文件已损坏”排查很快发现了问题根源openpyxl 写入时如果在单元格里引入了非法字符——比如一些 HTML 实体字符转出来的内容会导致生成的 xlsx 文件损坏。原因是解析层从 HTML 中提取文本时部分字段保留了换行符\n或制表符Excel 单元格内可以包含换行但必须在字符串首尾做 strip 处理。解决办法是在写入前统一做一次清洗把所有字段的首尾空白字符剥掉把字段内的连续空白压缩成单个空格。这是爬虫转 Excel 时一个非常经典的翻车点任何 XML 格式都会对特殊字符敏感。5.3 现象评分列的数值变成文本无法排序抓下来的评分在 Excel 里是文本类型排序时只能按字典序排9.9 会排在 9.1 前面。原因是在解析层把 rating 先转成了字符串保存openpyxl 写入时检测到类型是 str就按文本存了。解决方法是让解析层返回浮点数save_to_xlsx 里判断类型并强制转换如果是字符串先用 float() 包一层再写入。这个问题的隐蔽之处在于很多解析库拿到的文本是“9.9分”这样带后缀的直接转 float 会报错必须先提取数字部分。处理建议是写一个小清洗函数把非数字字符剥离后再转换。5.4 现象爬到第 30 条时进程卡死无响应现象是进程不退出、不报错、没有任何日志输出。原因有两个一是 fetch_page 的 timeout 设得太大请求到了 60 秒还没有超时卡在了网络层的等待中二是没有设置 socket 层面的保活参数。解决方法是把 timeout 从默认值改成显式的 10 秒并在 requests.get 里加上 streamFalse 确保响应内容一次性读取完。第二个隐藏原因是目标站点对长时间不活动的连接执行了断开requests 库默认会重试但重试过程中不会打印日志看起来就像卡死了。加一条超时后的 print 日志能避免下次再被这个现象迷惑。5.5 现象第二次运行时数据翻倍抓取结果里出现了重复条目第一次跑完的 250 条和第二次的 250 条混在一起形成了 500 行。原因是 save_to_xlsx 在文件已存在时直接往原文件继续追加没有清空旧数据。解决方法是先检查目标文件是否存在存在则先删除再生成或者更简单地让主流程在启动时总是新建一个临时文件名全部跑完后再重命名成正式文件名这样即使中途失败也不会污染之前的产出。这个问题的本质是“写入逻辑没有幂等性”即同样的输入重复执行产生了不同的结果。对于爬虫项目每次全量更新都应该是“先清空后写入”增量更新才用追加模式。6. 二次开发把这个项目改造成自己的采集工具包原版的定位是“一把梭跑完 top250”但这个项目最大的价值不是那 250 条数据而是它的分层结构带来的改造成本很低。我实际改造中优先做了三件事这里按性价比排序给你参考。第一件是把请求层从 Requests 直接切换成 httpx 并加入 Async 支持。httpx 的接口和 Requests 几乎一样迁移只需要改 import 和个别参数名却能把同步阻塞变成异步并发。改造方法很简单把 fetch_page 改成 async 函数用 asyncio.Semaphore 限住并发数在 5 个以内整体采集速度大约能提升 3 到 4 倍同时不会触发站点的风控。这个改造的验证方法是先跑一个小规模样本比如 30 条对比改造前后的数据和耗时确认解析结果一致再全量跑。第二件是把解析层从 BeautifulSoup 换成 lxml 的 etree并在里面增加一个 schema 校验函数。BeautifulSoup 在遇到页面上某些嵌套到深层的异常结构时“能容忍”并返回空值但这掩盖了页面结构变化的问题而 lxml 的解析报错更直接能让你第一时间感知到目标站点的 HTML 结构调整了。每次跑完数据后校验函数检查每条记录的必填字段是否非空低于 95% 就直接结束流程而不是输出一份残缺的 Excel。这个方法帮我避过一次大坑——某次目标页面在夜间悄悄调整了 CSS 类名BeautifulSoup 依然正常返回但字段全空如果不是校验函数在跑完后发现异常那份文件就要被当成真实数据交出去了。第三件是给项目加一个增量更新模式。通过维护一个已采集明细的本地 SQLite 数据库每次启动时查询已有数据只对新增的条目发起请求。这个改造让“定时跑一次”成为可能数据量从一次性变成持续积累。从那以后我每次接手爬虫项目都会先问自己三个问题数据需要增量吗、请求需要并发吗、失败了能被发现吗。这三个问题想清楚了再回到这个项目的三层结构里去改代码思路基本不会跑偏。希望帮到你。本文还有配套的精品资源点击获取