ARTICLE DETAIL

资讯详情

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

Python爬虫实战:抓取豆瓣TOP250生成Excel书单

Python爬虫实战:抓取豆瓣TOP250生成Excel书单 搞爬虫的人应该有同感入门最难的不是语法而是不知道该拿什么网站练手。太简单的没意思太难的打击信心反爬太凶的劝退毫无反爬的又练不到东西。豆瓣读书 TOP250 算是这个需求下的一个标准答案——页面结构稳定、榜单公开透明、分页规律清清楚楚而且爬完之后生成的书单是真能拿去用的东西。这个项目能做什么一句话就是用 Python 把豆瓣读书 TOP250 的书名、评分、简介抓到本地再一键生成带格式的 Excel 书单。请求、解析、存储三个环节全都有是一个标准的数据采集闭环。适合谁来学Python 刚入门、还没独立写过爬虫脚本的人以及想顺便复习 requests 和 openpyxl 的老手。整个项目只需要三个第三方库不需要框架、不需要数据库、不需要前端展示把一件小事做到完整反而比囫囵吞枣学一堆概念更有价值。1. 项目拆解为什么入门爬虫首选豆瓣读书 TOP2501.1 三条理由说清楚这个项目的含金量第一数据结构清晰到可以当教材。豆瓣读书 TOP250 的页面是老式的 HTML 表格布局每一本书都规规矩矩地躺在 tr.item 节点里书名、评分、链接的定位方式几十年没大变过。这意味着你不需要和复杂的动态渲染页面搏斗把精力全部放在爬虫本身的核心逻辑上。第二分页规律是教科书级别。榜单共 10 页每页 25 本书翻页完全靠 URL 参数 start 控制。这个设计太适合教学了没有加密参数、没有异步加载、没有跳转陷阱你一眼就能看穿页码到 URL 参数这个映射关系。第三数据有真实筛选价值。TOP250 毕竟是豆瓣用户集体评出来的高分书单爬下来做成 Excel 之后你可以按评分排序、按简介关键词筛选未来想读的书。这个项目做完不是为了交作业而是真的产出了一个有用的个人书单工具。1.2 合规边界学习练手和恶意爬取是两码事聊爬虫这个话题必须放在最前面。很多人一提到爬虫就联想到违规、封 IP其实没那么玄乎。爬虫本身是一项中性的编程技术关键在于你怎么用、用来干什么。在这个项目里我们抓的是豆瓣公开页面上的榜单信息属于公开数据的简单采集我建议把它严格控制在练习用途。实操层面控制好三点第一设置合理的请求间隔不要让服务器感觉到你在扫库第二只爬榜单这 10 个列表页和 250 个对应的详情页不碰任何需要登录才能看到的数据第三数据不要用于任何商业用途。下面给的代码里都写了 sleep 延时这不是多余的这是爬虫的基本素养。练代码的目的是学会 requests 和 BeautifulSoup不是真的要把别人的整个数据库搬回家频率低一点反而跑得更稳。1.3 三个依赖库覆盖完整数据链路这个项目用到的第三方库非常克制就三个requests发 HTTP 请求拿网页 HTMLbeautifulsoup4解析 HTML定位书名、评分、链接等节点openpyxl生成带样式的 Excel 文件requests 是 Python 生态里最常用的 HTTP 客户端语法简洁到不需要翻文档BeautifulSoup 内置了 HTML 解析器和 CSS 选择器支持面对豆瓣这种老式页面解析体验非常顺手openpyxl 是操作 Excel 的老牌库单元格样式、列宽、冻结窗格都支持得很好。这三个库组合起来正好覆盖取数—解析—入表的全过程没有多余的花架子。2. 环境准备Python 环境与依赖安装2.1 先确认 Python 环境没问题写 Python 程序的前提是电脑上有可用的 Python 环境。建议直接用 Python 3.8 以上的版本太老的版本在语法特性和依赖兼容性上多多少少会有问题。怎么确认打开命令行工具Windows 上用 PowerShell 或 CMDmacOS 上用终端输入python --version如果输出了 Python 3.x 的版本号说明环境没问题。如果提示找不到命令多半是安装时没勾选Add Python to PATH。这种情况不用急去 Python 官网下载安装包重新装一遍安装向导的第一步务必勾选 Add Python to PATH后面就顺了。macOS 用户如果系统自带的是 Python 2 残留建议用 brew install python 装一个干净的新版本。2.2 安装 requests、beautifulsoup4、openpyxl环境就绪后安装依赖就一行命令pip install requests beautifulsoup4 openpyxl如果你身处网络环境不太理想的场景显式加一个国内镜像源能明显提升下载速度pip install requests beautifulsoup4 openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后可以用下面的命令验证版本顺便确认三个库都进入了可导入状态pip show requests beautifulsoup4 openpyxl或者直接在 Python 解释器里跑三行 import不报错就说明全部就位。2.3 建议用一个虚拟环境管理依赖我在实际项目里几乎不会直接往全局环境 pip install因为不同项目对库版本的要求经常打架。比如项目 A 要 openpyxl 3.0项目 B 要 openpyxl 3.1全局环境下升级一个就把另一个搞坏了。虚拟环境就是为这个场景生的。在项目目录下创建虚拟环境python -m venv venv然后激活它# Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活后命令行前缀会出现 (venv)此时再 pip install所有依赖都会被隔离在当前目录下不会污染全局环境。这个习惯我从第一天写爬虫就开始用回头来看是节省时间最多的一个决定。3. 页面分析URL、HTML 结构与数据定位3.1 URL 分页规律start 参数说了算豆瓣读书 TOP250 的地址是 https://book.douban.com/top25实际是 https://book.douban.com/top250首页没有额外的分页参数。翻到第二页URL 变成 ?start25第三页 start50依次类推。规律一句话就能讲明白每页展示 25 本书start 表示从榜单第几个位置开始取。第 1 页跳过 0 本start0第 2 页跳过 25 本start25第 10 页跳过 225 本start225。用代码生成这 10 个分页参数就是for start in range(0, 250, 25): print(start)输出恰好是 0、25、50 直到 225。这个offset 分页模式在老牌网站里非常普遍搞懂了它以后爬类似的分页列表都能直接套用。3.2 用开发者工具定位书名和评分写解析代码之前先动手在浏览器里看一遍页面结构。建议打开开发者工具按下 F12点击左上角的元素选择器图标再点击页面上的书名会自动帮你定位到对应的 HTML 代码。豆瓣读书 TOP250 的列表页每本书被包在一个 tr.item 节点里。书名在 div.pl2 下面的 a 标签的 title 属性里评分在 span.rating_nums 里。我画一个简化结构示意!-- 逻辑示意实际页面以浏览器渲染为准 -- tr classitem td封面区域.../td td div classpl2 a hrefhttps://book.douban.com/subject/xxx title百年孤独 百年孤独 /a /div span classrating_nums9.3/span span classpl(xxx人评价)/span /td /trBeautifulSoup 里两个选择器就能把这些信息都拿下来node.select_one(div.pl2 a)拿到书名链接title 属性是书名href 是详情页地址node.select_one(span.rating_nums)拿到评分文本这里容易踩的第一个坑是直接用 select_one(a) 有可能选到封面图的链接而不是书名链接。所以要加限定 div.pl2 a把范围缩到书名所在区块内部。3.3 简介不在列表页需要二次请求详情页这是整个项目最容易被新手忽略的一个事实。看项目名书名评分简介三个字段很多人默认列表页全都有。认真看一眼页面就会发现列表页包含书名、评分、评价人数、出版社、价格但让读者决定要不要读一本书的简介却并不在列表页里。简介藏在每本书的独立详情页。也就是说250 本书除了要爬 10 个列表页还需要额外发 250 次详情页请求。听到这里可能会觉得这不是加大工作量吗。但从学习角度讲这反而是这个项目真正的价值所在——它逼你掌握列表页详情页的两级页面采集模式。真实场景里绝大多数爬虫项目都不会在一个页面里把数据全部给全要么翻页要么跳详情要么接口分页。二级页面的处理迟早要学不如在入门阶段就练一次。4. 代码实现从请求到解析到入表4.1 请求封装Session、请求头、编码一个都不能少写爬虫的第一步是稳定地拿到 HTML。很多入门教程只让你带一个 User-Agent实际跑起来会频繁碰壁。豆瓣的防护策略是看人下菜碟裸 requests 访问直接给你一个 403带上完整的浏览器请求头访问就正常。一套可靠的请求头至少要有三项User-Agent、Accept-Language、Accept。User-Agent 用来表明我是一个浏览器Accept-Language 告诉服务器优先返回中文内容Accept 表明我能接收 HTML。最佳做法是挂在 requests.Session() 上这样整个会话内的所有请求都会自动携带这些头def build_session(): session requests.Session() session.headers.update({ 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: zh-CN,zh;q0.9,en;q0.8, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, }) return session拿到响应之后有两件必须做的事。第一用 raise_for_status() 主动抛出 HTTP 错误状态码不是 2xx 就直接报异常方便你第一时间发现问题。第二设置响应编码。requests 的 resp.encoding 有时会猜测错误豆瓣页面明确是 UTF-8所以 resp.encoding utf-8 这行要写死否则解析出来的书名可能是一堆乱码。4.2 列表页解析与详情页简介抓取页面解析是爬虫的第二课。BeautifulSoup 的 select 方法支持 CSS 选择器写起来直观和浏览器开发者工具里的选择器语法几乎一致所以我更推荐 select 而不是 find_all。列表页解析def parse_list_page(soup): book_nodes soup.select(tr.item) data [] for node in book_nodes: detail_link node.select_one(div.pl2 a) title detail_link.get(title, ).strip() href detail_link.get(href, ).strip() rating_node node.select_one(span.rating_nums) rating rating_node.text.strip() if rating_node else data.append({title: title, detail_url: href, rating: rating}) return data这段代码体现了爬虫容错的三件套get(title, ) 安全取属性、strip() 清除首尾空白、字段缺失时条件表达式给默认值。写爬虫最忌讳的就是假设每个字段都一定存在实际页面上总有例外。详情页的简介抓取需要单独处理。豆瓣详情页的简介通常放在 div.intro 节点里但有些书没有简介、有些书的节点结构略有差异。我习惯先取 div.intro取不到就退而取 meta[namedescription]再不行就返回暂无简介四个字保底def fetch_intro(session, detail_url): try: resp session.get(detail_url, timeout10) resp.raise_for_status() resp.encoding utf-8 detail_soup BeautifulSoup(resp.text, html.parser) intro_node detail_soup.select_one(div.intro) if not intro_node: meta detail_soup.select_one(meta[namedescription]) if meta and meta.get(content): return meta[content].strip()[:300] return 暂无简介 intro_text intro_node.get_text(separator\n, stripTrue) return intro_text[:300] except Exception: return 暂无简介这里我把简介截断到 300 字符原因是有些书的内容简介很长存进 Excel 之后会让单元格变得像一面墙不仅影响美观阅读体验也差。截断并不是丢失数据而是从存储原始数据变成了存储可读展示数据这是实际项目中很常见的一种取舍。4.3 完整主流程250 本书一条龙跑完爬虫的主流程就是三个动作循环翻页、解析列表、逐个抓详情。写成一个主函数串联起来便于整体控制节奏def scrape_top250(): session build_session() all_books [] rank 1 for start in range(0, 250, 25): page_url fhttps://book.douban.com/top250?start{start} resp session.get(page_url, timeout10) resp.raise_for_status() resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) books_on_page parse_list_page(soup) for book in books_on_page: book[rank] rank rank 1 print(f正在处理第 {book[rank]} 本: {book[title]}) book[intro] fetch_intro(session, book[detail_url]) all_books.append(book) time.sleep(random.uniform(0.5, 1.5)) time.sleep(random.uniform(2, 4)) return all_books细节都在节奏控制上。每抓完一本书随机休息 0.5 到 1.5 秒每翻完一页随机休息 2 到 4 秒。请求间隔模拟一个真人浏览的节奏既是对目标站点的尊重也是让自己代码运行更稳的实用手段。这里用 random.uniform 而不是固定 sleep是因为固定间隔更容易被反爬机制识别成机器行为随机化之后看起来更像人的操作节奏。5. Excel 书单美化指南5.1 openpyxl 三件套工作簿、工作表、单元格数据拿到手之后最后一步是入表。openpyxl 的对象体系刚好对应 Excel 的三层结构工作簿Workbook是一个 .xlsx 文件工作表Worksheet是文件里的一个 Sheet单元格Cell是最小的数据容器。创建文件的固定套路是先建工作簿再取活动工作表然后往里填数据from openpyxl import Workbook wb Workbook() ws wb.active ws.title 豆瓣读书TOP250 ws.append([排名, 书名, 评分, 简介])append 方法会把传入的列表当作一整行写入表格。我第一次用 openpyxl 的时候习惯性地想用 cell(row, column) 一个个填后面发现 append 在批量写入场景下方便太多了还能天然保持顺序。如果你想精确控制某个位置随时可以再用 ws.cell(row, column, value) 覆盖。5.2 五个细节让 Excel精美起来如果只是把数据写进表格pandas 一行 df.to_excel() 就搞定了但那个默认格式只能叫能用配不上精美二字。既然项目标题里承诺了精美 Excel 书单我就在样式上认真做了几个设计。第一表头醒目。深蓝底、白色加粗、微软雅黑字体再给表头行设置 32 像素行高。表格的第一印象完全由表头决定这个钱值得花。第二统一细边框。用极浅的蓝色细线给所有数据单元格加上边框整体视觉立刻从散装数据变成整齐表格。很多人导出的 Excel 没有边框在手机上看着就是一片毛坯房加一层 border 观感会好很多。第三列宽合理化。排名列 8 字符书名列 30 字符评分列 10 字符简介列 80 字符。这里有个非常重要的连带操作简介列必须设置自动换行wrap_textTrue否则列宽设置得再宽也不会出现你想要的整齐折行效果。这个细节我调试了很久才发现一开始只调列宽没开换行导出后简介全部挤在一行里朝右边伸出屏幕外。第四冻结首行。设置 freeze_panes A2往下滚动时表头始终钉在顶部。250 行长表不冻结表头的话翻到第 100 行你就忘了哪列是什么了。第五评分条件高亮。9.0 分以上的书用红色加粗标识8.5 到 9.0 之间用橙色标识这样打开 Excel 第一眼就能锁定书单里的高分王者比一溜黑色的默认字体多了一层信息层次。完整的保存函数示范from openpyxl.styles import Font, PatternFill, Alignment, Border, Side def save_to_excel(books, filename豆瓣读书TOP250书单.xlsx): wb Workbook() ws wb.active ws.title TOP250 headers [排名, 书名, 评分, 简介] ws.append(headers) header_font Font(name微软雅黑, size12, boldTrue, colorFFFFFF) header_fill PatternFill(solid, fgColor4472C4) header_alignment Alignment(horizontalcenter, verticalcenter) thin Side(stylethin, colorD9E2F3) border Border(leftthin, rightthin, topthin, bottomthin) for col_idx in range(1, len(headers) 1): cell ws.cell(row1, columncol_idx) cell.font header_font cell.fill header_fill cell.alignment header_alignment cell.border border ws.row_dimensions[1].height 32 for row_idx, book in enumerate(books, start2): row_values [book[rank], book[title], book[rating], book[intro]] for col_idx, value in enumerate(row_values, start1): cell ws.cell(rowrow_idx, columncol_idx, valuevalue) cell.font Font(name微软雅黑, size11) cell.alignment Alignment(verticalcenter, wrap_text(col_idx 4)) cell.border border if col_idx 3: try: score float(book[rating]) if score 9.0: cell.font Font(name微软雅黑, size11, boldTrue, colorC00000) elif score 8.5: cell.font Font(name微软雅黑, size11, colorED7D31) except ValueError: pass ws.column_dimensions[A].width 8 ws.column_dimensions[B].width 30 ws.column_dimensions[C].width 10 ws.column_dimensions[D].width 80 ws.freeze_panes A2 ws.auto_filter.ref ws.dimensions wb.save(filename) print(f已保存{filename}共 {len(books)} 本书)注意一个容易踩的坑openpyxl 里如果先设置样式再给单元格赋值部分版本的样式会被覆盖掉。稳妥的顺序永远是先写入 value再设置 font、fill、border 这些样式属性。评分单元格需要动态变色的时候赋值时先拿到 cell 对象再单独覆写 font就不会出错。6. 2026 避坑实录实测翻车点与排查技巧6.1 反爬拦截403、418 离你并不远最近重新跑了一遍这个项目明显感觉到前几年还能凑合的裸请求风格现在已经越来越难走通了。如果你发出去请求返回 403 Forbidden说明服务器根本不认你这个客户端是谁优先检查请求头是否完整。不要自己手写一个拼错的 UA直接复制浏览器开发者工具里完整的那串值最省事。如果你是请求头齐全但还是被拦看清楚状态码。418 Im a teapot 是那种很搞心态的响应——它来源于 HTTP 协议里的一个愚人节玩笑定义服务器真实意图是我知道你是爬虫但我不打算搭理你。这个状态码通常意味着访问频率太高触发了风控。解决办法很朴素停几秒再继续把延时拉长一倍不要死磕。技术层面的破解从来不是正确姿势降低频率反而能绕开大部分风控策略。6.2 编码、字段缺失与数据清洗爬取过程中最容易遇到的三类数据问题我在调试时全都碰到过中文乱码响应的 encoding 没设置成 utf-8解决方案就是加一行 resp.encoding utf-8。养成每个请求都写的习惯不要等出乱码了再检查。文本带着大量空格和换行书名、简介抽取出来之后是原始 HTML 里的样子可能带着奇怪的空白符。所有文本字段统一 .strip()能过滤掉大多数脏数据。简介获取失败有些书在详情页确实找不到 div.intro。兜底逻辑返回暂无简介就能保证主流程不中断最后 Excel 里这行依然存在只是简介字段填了占位文本。另外如果你发现某本书的评分解析出来是空字符串不要急着怀疑代码逻辑。豆瓣页面上那本书的评分可能真的缺失条件表达式的兜底正好派上用场。对数据字段做防御式处理不是过度设计而是写爬虫的基本修养。6.3 常见问题速查表我把这一路踩过的坑整理成了速查表方便你程序跑不通的时候快速定位问题阶段。现象可能原因处理方式403 Forbidden请求头不完整补全 User-Agent、Accept-Language、Accept 三项418 Im a teapot请求频率过高触发风控加大 sleep 间隔放慢节奏中文乱码响应编码识别错误设置 resp.encoding utf-8书名解析为空选择器范围太宽使用 div.pl2 a 精确定位简介全是暂无简介详情页结构变化换成 meta[namedescription] 兜底简介列不换行未开启自动换行Alignment(wrap_textTrue)同时加大列宽Excel 打开后空白忘记 save检查文件路径和 wb.save() 是否执行这些问题的排查思路有一个共同点先定位问题发生在请求、解析还是存储阶段再对症下药。不要一上来就怀疑代码逻辑先打印响应状态码和 HTML 片段亲眼看看服务器到底回了什么比我一句一句教你调试要快得多。最后再分享一个小技巧跑完这个项目数据不只可以停在 Excel。你可以继续用 openpyxl 给同一个工作簿加一个评分统计图表按评分段统计书籍数量或者把 9.0 分以上的书单独抽一个 Sheet 做成必读清单。爬虫到 Excel 只是起点把数据变成更方便看、更方便用的东西才是书单工具真正发挥价值的地方。
返回列表