豆瓣图书爬虫实战:requests+BeautifulSoup+pandas构建结构化Excel数据

发布时间:2026/10/14 19:53:53
豆瓣图书爬虫实战:requests+BeautifulSoup+pandas构建结构化Excel数据 简介本资源是一套完整的豆瓣图书数据采集与分析实践项目面向Python初学者及网络爬虫入门学习者解决图书信息自动化获取、结构化存储与基础统计分析的学习需求。压缩包共12个文件含11个按主题分类的Excelxlsx数据表如数学、科幻、计算机、心理、摄影等细分领域和1个核心爬虫脚本doubanSpider.py总大小12.34MBxlsx文件承载清洗后的图书元数据书名、作者、出版社、评分、分类py文件实现基于requestsBeautifulSoup的页面抓取与解析逻辑。已有2051人学习下载项目覆盖从HTTP请求、HTML解析、pandas数据处理到Excel导出的完整链路附带多维度分类数据集可直接用于推荐系统验证、图书市场分析或教学案例演示兼具实操性与扩展性。1. 豆瓣图书爬虫以及图书数据xlsx不是“一键采集”而是用合法边界结构化思维把公开书目变成可分析的 Excel 表格你是不是也试过在豆瓣搜《三体》《百年孤独》《思考快与慢》想批量导出评分、页数、出版社、标签、简介却发现网页没提供导出按钮点开几十本书手动复制粘贴翻到第5页就手酸眼花这不是效率问题是方法错位——豆瓣图书页面虽未开放 API但其 HTML 结构稳定、反爬强度适中、数据完全公开用合规手段抓取书目元数据并落地为标准 .xlsx 文件是当前中文图书数据轻量分析最可行的起点。这个方案不碰用户行为日志、不模拟登录、不高频刷接口只聚焦「图书详情页公开字段」的精准提取与结构化归档。适合高校课程设计、出版行业竞品调研、个人阅读库管理、小团队做图书推荐原型验证。它不是黑盒工具而是一套可审计、可调试、可按需增删字段的本地化数据流水线从请求调度、HTML 解析、字段清洗到 pandas 写入 Excel 并自动设置列宽、冻结首行、保留超链接——每一步都可控、可复现、可嵌入你的数据分析工作流。2. 为什么选 requests BeautifulSoup pandas 而非 Scrapy 或 Selenium2.1 选型逻辑轻量、确定、易调试拒绝过度工程化豆瓣图书详情页如https://book.douban.com/subject/1084336/是静态渲染的典型所有关键信息书名、作者、出版社、出版年、页数、ISBN、评分、评价人数、标签、内容简介都在初始 HTML 中无需 JavaScript 渲染。这意味着Selenium 太重启动浏览器、加载 JS、等待 DOM 就绪单页耗时 2–5 秒100 本书就是 5–8 分钟且内存占用高、容易被识别为自动化行为Scrapy 过度设计它面向大规模分布式爬取带中间件、管道、调度器而本任务本质是「顺序请求 单页解析 单文件写入」引入 Scrapy 反而增加配置复杂度、调试成本和部署门槛requests BeautifulSoup 是黄金组合requests控制请求头、会话复用、超时与重试BeautifulSoup对 HTML 结构容错强豆瓣页面偶尔有轻微 DOM 变动也不崩CSS 选择器直观可读pandas直接对接 Excel 写入支持样式、多 sheet、超链接保留——三者加起来不到 50 行核心代码新手改字段、调参数、查日志一目了然。提示本方案默认遵守robots.txt豆瓣允许/subject/路径抓取请求间隔设为 1.5 秒以上User-Agent 模拟主流浏览器不使用代理池完全符合公开数据合理使用原则。2.2 环境准备与最小依赖安装# 创建独立虚拟环境强烈建议 python -m venv douban_book_env source douban_book_env/bin/activate # Linux/macOS # douban_book_env\Scripts\activate # Windows # 安装核心三件套版本锁定避免兼容问题 pip install requests2.31.0 beautifulsoup44.12.2 pandas2.0.3 openpyxl3.1.2requests2.31.0稳定、SSL 兼容性好支持 session 复用beautifulsoup44.12.2对豆瓣 HTML 的span classpl标签解析鲁棒性强pandas2.0.3.to_excel()支持openpyxl引擎可写入超链接、设置列宽openpyxl3.1.2唯一能真正保留 Excel 超链接的引擎xlsxwriter不支持。注意不要用pip install pandas默认装最新版——pandas 2.1 对openpyxl的样式 API 有 breaking change会导致列宽设置失效。版本锁定是血泪经验。2.3 从一个图书 ID 开始手写解析逻辑确认字段可提取以《三体》ID1084336为例我们先手动请求并解析验证关键字段是否存在、路径是否稳定import requests from bs4 import BeautifulSoup url https://book.douban.com/subject/1084336/ headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 } response requests.get(url, headersheaders, timeout10) response.raise_for_status() soup BeautifulSoup(response.text, html.parser) # 书名位于 h1 span 标签内 title soup.select_one(h1 span).get_text(stripTrue) if soup.select_one(h1 span) else N/A # 作者/译者在 idinfo 的 div 内找第一个 span classpl 后的文本 info_div soup.select_one(#info) author N/A if info_div: author_span info_div.select_one(span.pl:contains(作者)) if author_span and author_span.next_sibling: author author_span.next_sibling.strip().replace(\n, ).replace( , ) # 出版社同理找 出版社 press N/A if info_div: press_span info_div.select_one(span.pl:contains(出版社)) if press_span and press_span.next_sibling: press press_span.next_sibling.strip().replace(\n, ).replace( , ) print(f书名{title}) print(f作者{author}) print(f出版社{press})逻辑说明soup.select_one(h1 span)精准定位主标题内的纯文本避开副标题或英文名#info是豆瓣图书信息区的固定 ID所有元数据都在此 div 下span.pl:contains(作者)是 CSS 伪类选择器直接匹配含“作者”二字的span classpl标签注意BeautifulSoup 4.12 原生支持:contains无需额外插件.next_sibling获取该span标签后的第一个文本节点再.strip()清除换行和空格——这是解析豆瓣“标签-值”结构最稳的方式比正则更可靠。参数说明timeout10防止网络抖动导致脚本卡死response.raise_for_status()主动抛出 HTTP 错误如 404、403便于后续统一捕获BeautifulSoup(..., html.parser)用内置解析器不依赖 lxml降低安装门槛。3. 构建可扩展的图书 ID 列表与请求调度器3.1 图书 ID 来源从搜索页提取而非硬编码手动维护 ID 列表不可持续。豆瓣搜索页如https://book.douban.com/subject_search?search_text人工智能cat1001返回的 HTML 中每本书的链接形如/subject/4913064/我们只需提取这些路径片段即可。def get_book_ids_from_search(keyword, max_pages3): 从豆瓣图书搜索页提取前 N 页的图书 ID 列表 book_ids [] headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } for page in range(max_pages): url fhttps://book.douban.com/subject_search?search_text{keyword}cat1001start{page * 15} try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() soup BeautifulSoup(response.text, html.parser) # 搜索结果项在 .item-root 下a 标签 href 包含 /subject/{id}/ for a_tag in soup.select(.item-root a[href*/subject/]): href a_tag.get(href, ) if /subject/ in href: # 提取 ID/subject/1234567/ → 1234567 import re match re.search(r/subject/(\d)/, href) if match: book_ids.append(match.group(1)) print(f第 {page1} 页提取到 {len(set(book_ids))} 个去重 ID) except Exception as e: print(f获取第 {page1} 页失败{e}) continue return list(set(book_ids)) # 去重 # 示例获取关键词“机器学习”的前 2 页图书 ID ids get_book_ids_from_search(机器学习, max_pages2) print(f共获取 {len(ids)} 个图书 ID{ids[:5]}...) # 打印前 5 个预览逻辑说明max_pages3控制抓取深度避免触发频率限制.item-root a[href*/subject/]是精准 CSS 选择器只匹配搜索结果中的图书链接排除广告、分页等干扰正则r/subject/(\d)/提取纯数字 ID健壮性远高于字符串切片如href.split(/)[-2]在 URL 变形时易崩list(set(...))去重同一本书可能在不同页重复出现。提示豆瓣搜索页无反爬 header 验证但需注意start参数是 0-based每页 15 条所以第 2 页start15第 3 页start30。3.2 请求调度带退避、会话复用、错误隔离的稳健循环单纯 for 循环请求极易因单页失败导致整个流程中断。我们封装一个带重试、延迟、错误跳过的fetch_book_data函数import time import random from urllib.parse import urljoin def fetch_book_data(book_id, session, base_urlhttps://book.douban.com/subject/): 安全获取单本图书数据失败返回 None不中断主流程 url urljoin(base_url, f{book_id}/) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } for attempt in range(3): # 最多重试 3 次 try: response session.get(url, headersheaders, timeout12) response.raise_for_status() # 检查是否为真实图书页防 302 跳转到首页或提示页 if 豆瓣读书 not in response.text or 该页面不存在 in response.text: print(fID {book_id} 页面异常跳过) return None soup BeautifulSoup(response.text, html.parser) data parse_book_page(soup, book_id, url) # 下节定义 print(f✓ 成功获取 {book_id}{data.get(title, N/A)}) return data except requests.exceptions.RequestException as e: wait (2 ** attempt) random.uniform(0, 1) # 指数退避 随机抖动 print(f⚠ 第 {attempt1} 次请求 {book_id} 失败{e}等待 {wait:.1f}s 后重试...) time.sleep(wait) except Exception as e: print(f❌ 解析 {book_id} 时发生未知错误{e}) return None return None # 主调度循环用 Session 复用 TCP 连接提升速度 def crawl_books(book_ids, output_filedouban_books.xlsx): session requests.Session() all_data [] for i, book_id in enumerate(book_ids): print(f\n--- 处理第 {i1}/{len(book_ids)} 本{book_id} ---) data fetch_book_data(book_id, session) if data: all_data.append(data) # 每请求完一本强制休眠 1.5–2.5 秒模拟人工节奏 if i len(book_ids) - 1: sleep_time 1.5 random.uniform(0, 1) time.sleep(sleep_time) # 写入 Excel if all_data: write_to_excel(all_data, output_file) print(f\n✅ 全部完成数据已保存至 {output_file}) else: print(❌ 未获取到任何有效数据请检查网络或豆瓣访问状态) # 调用示例 # ids get_book_ids_from_search(深度学习, max_pages1) # crawl_books(ids, deep_learning_books.xlsx)逻辑说明requests.Session()复用连接减少 TCP 握手开销100 本书可提速 30%for attempt in range(3)实现指数退避1s → 2s → 4s配合random.uniform(0,1)抖动避免集群请求同步urljoin确保 URL 拼接绝对安全处理 base_url 末尾斜杠与否if 豆瓣读书 not in response.text是关键兜底防豆瓣返回 302 跳转到首页常见于限流此时直接跳过不浪费重试次数。参数说明timeout12比单页解析预留更多缓冲防 CDN 延迟sleep_time 1.5 random.uniform(0,1)1.5–2.5 秒随机间隔比固定值更接近真实浏览行为print日志带符号✓ ⚠ ❌和上下文便于快速定位哪本失败。4. 字段解析与清洗覆盖豆瓣图书页全部公开元数据4.1 完整字段映射表从 HTML 节点到 Excel 列名豆瓣图书详情页结构稳定但部分字段存在「多值」「格式混乱」「缺失」三大问题。下表列出所有可提取字段、对应 HTML 路径、清洗规则及是否必填Excel 列名HTML 定位方式清洗规则是否必填说明book_idURL 路径提取直接传入✅唯一标识用于去重和关联url原始请求 URL保留完整链接✅Excel 中自动转为超链接titleh1 span.strip()✅主标题不含副标题sub_titleh1 span.pl.strip()❌副标题如英文名、丛书名author#info span.pl:contains(作者) text去换行、空格、多余标点✅多作者用/分隔translator#info span.pl:contains(译者) text同 author❌译者字段press#info span.pl:contains(出版社) text去空格、括号内容如(2020年版)✅出版社名称pub_year#info span.pl:contains(出版年) text提取 4 位数字如2020❌出版年份pages#info span.pl:contains(页数) text提取数字如320❌页数纯数字price#info span.pl:contains(定价) text提取¥后数字如69.00❌定价浮点数isbn13#info span.pl:contains(ISBN) text提取 13 位连续数字❌ISBN-13scorestrong[propertyv:average]float()如9.3✅豆瓣评分0–10rating_countspan[propertyv:votes]int()如12543✅评价人数tagsdiv.tags-body a用 连接如科幻小说summarydiv#link-report div.intro取第一个div.intro的文本.strip()❌内容简介前 200 字注意#info内字段用 text表示「紧邻的下一个文本节点」这是解析豆瓣「标签-值」布局的核心技巧比find_next_sibling(textTrue)更简洁可靠。4.2 解析函数parse_book_page一行一字段清晰可维护def parse_book_page(soup, book_id, url): 解析单个豆瓣图书页返回字典字段名与 Excel 列名严格一致 def safe_text(selector, defaultN/A): el soup.select_one(selector) return el.get_text(stripTrue) if el else default def extract_text_after_label(label_text): 通用函数找 span.pl 包含 label_text 的元素取其后文本 label soup.select_one(f#info span.pl:contains({label_text})) if label and label.next_sibling: return label.next_sibling.strip().replace(\n, ).replace( , ) return N/A # 基础字段 data { book_id: book_id, url: url, title: safe_text(h1 span), sub_title: safe_text(h1 span.pl), author: extract_text_after_label(作者), translator: extract_text_after_label(译者), press: extract_text_after_label(出版社), pub_year: extract_text_after_label(出版年).split(年)[0] if 年 in extract_text_after_label(出版年) else N/A, pages: .join(filter(str.isdigit, extract_text_after_label(页数))) or N/A, price: .join(filter(lambda x: x.isdigit() or x ., extract_text_after_label(定价))) or N/A, isbn13: .join(filter(str.isdigit, extract_text_after_label(ISBN))) or N/A, score: safe_text(strong[propertyv:average]), rating_count: safe_text(span[propertyv:votes]), tags: |.join([a.get_text(stripTrue) for a in soup.select(div.tags-body a)[:5]]) or N/A, summary: } # 简介取第一个 intro div 的文本截断到 200 字 intro_div soup.select_one(div#link-report div.intro) if intro_div: summary_text intro_div.get_text(stripTrue) data[summary] summary_text[:200] ... if len(summary_text) 200 else summary_text else: data[summary] N/A # 类型转换Excel 写入前预处理 try: data[score] float(data[score]) if data[score] ! N/A else 0.0 except ValueError: data[score] 0.0 try: data[rating_count] int(data[rating_count]) if data[rating_count] ! N/A else 0 except ValueError: data[rating_count] 0 return data逻辑说明safe_text封装基础选择器避免重复写if el: el.get_text()extract_text_after_label是豆瓣专用解析器统一处理「作者」「出版社」等字段代码复用率高pub_year用.split(年)[0]提取年份比正则更轻量如2020年1月→2020pages和price用filter(str.isdigit)和filter(... or x.)提取纯数字完美应对320页、¥69.00等混排summary截断到 200 字既保证可读性又防止单元格撑爆 Excel 列宽。参数说明所有defaultN/A统一占位避免None导致 pandas 写入报错[:5]限制 tags 数量防止单元格过长影响 Excel 查看体验float()/int()转换放在解析层而非写入层确保数据类型在内存中即正确。5. 避坑豆瓣图书爬虫的 4 个真实翻车现场与后悔药5.1 现象请求突然大量 403IP 被临时限制原因豆瓣对高频请求有 IP 级限流连续 10 次请求尤其无 User-Agent 或 User-Agent 过旧会被返回 403。这不是封禁而是临时冷却约 5–10 分钟。解决必须设置现代 User-Agent如 Chrome 115避免用python-requests/2.x将请求间隔从 1 秒拉长到1.8–2.5 秒实测阈值加入time.sleep(random.uniform(1.8, 2.5))随机化比固定值更有效若已触发限流立即停止脚本等待 10 分钟后再续跑不要暴力重试。5.2 现象author字段为空或提取到“[美]”“[日]”等国籍前缀原因豆瓣对作者字段做了结构化处理国籍前缀在span classpl内真实作者名在下一个a标签里而next_sibling只拿到空白符。解决改用label.find_next(a)获取作者链接文本author_a soup.select_one(#info span.pl:contains(作者)).find_next(a) author author_a.get_text(stripTrue) if author_a else N/A若需兼容无链接的作者如“编者XXX”则 fallback 到next_siblingnext_sibling.next_sibling多级查找。5.3 现象Excel 中url列显示为纯文本不是可点击超链接原因pandas.to_excel()默认不写入超链接即使url列存的是字符串Excel 也不会自动识别。解决必须用openpyxl引擎并在写入后手动设置超链接def write_to_excel(data_list, filename): df pd.DataFrame(data_list) with pd.ExcelWriter(filename, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_nameBooks) workbook writer.book worksheet writer.sheets[Books] # 为 url 列假设是第 2 列索引为 1添加超链接 for row in range(2, len(df) 2): # Excel 行号从 1 开始header 占第 1 行 cell worksheet.cell(rowrow, column2) # B 列 if cell.value and isinstance(cell.value, str) and cell.value.startswith(http): cell.hyperlink cell.value cell.style Hyperlink # 设置为超链接样式5.4 现象summary字段内容错乱出现大量\n、nbsp;、HTML 标签原因div.intro内可能嵌套p、brget_text()默认合并所有空白但nbsp;不被识别。解决用get_text(separator , stripTrue)显式指定分隔符替换nbsp;为普通空格text.replace(\xa0, )移除多余空格re.sub(r\s, , text)最终清洗链summary intro_div.get_text(separator , stripTrue).replace(\xa0, ) summary re.sub(r\s, , summary).strip()[:200] ...提示以上 4 条全是某开发者在真实项目中踩过的坑第 2 条导致 30% 的作者字段丢失第 4 条让 Excel 报表被业务方退回重做——字段清洗不是锦上添花而是数据可用性的生死线。6. 进阶技巧用 Excel 样式、条件格式与数据验证让输出即战力6.1 自动设置列宽与冻结窗格告别手动调整pandas 默认写入的 Excel 列宽极窄中文全被截断。我们用 openpyxl 手动优化def write_to_excel(data_list, filename): df pd.DataFrame(data_list) with pd.ExcelWriter(filename, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_nameBooks) workbook writer.book worksheet writer.sheets[Books] # 设置列宽按字段语义定制 col_widths { A: 8, # book_id B: 25, # url超链接需稍宽 C: 30, # title D: 20, # sub_title E: 25, # author F: 25, # translator G: 20, # press H: 10, # pub_year I: 10, # pages J: 12, # price K: 15, # isbn13 L: 10, # score M: 15, # rating_count N: 30, # tags O: 50, # summary最宽 } for col, width in col_widths.items(): worksheet.column_dimensions[col].width width # 冻结首行方便滚动查看 worksheet.freeze_panes A2 # 为 url 列添加超链接见 5.3 for row in range(2, len(df) 2): cell worksheet.cell(rowrow, column2) if cell.value and isinstance(cell.value, str) and cell.value.startswith(http): cell.hyperlink cell.value cell.style Hyperlink效果打开 Excel 即可直接筛选、排序、打印无需二次调整——这才是交付给同事或导师的体面姿态。6.2 用条件格式高亮低分/高评图书一眼定位价值点在 Excel 中我们可以为score和rating_count列添加条件格式让数据自己说话# 续写 write_to_excel 函数内部... from openpyxl.formatting.rule import ColorScaleRule # 为 score 列L 列添加三色刻度红色7.0→ 黄色7.0–8.5→ 绿色8.5 score_rule ColorScaleRule( start_typenum, start_value0, start_colorFFEE1111, mid_typenum, mid_value7.5, mid_colorFFFFFF00, end_typenum, end_value10, end_colorFF11EE11 ) worksheet.conditional_formatting.add(L2:L{}.format(len(df)1), score_rule) # 为 rating_count 列M 列添加数据条仅 top 20% from openpyxl.formatting.rule import DataBarRule count_rule DataBarRule( start_typenum, start_valuedf[rating_count].quantile(0.2), end_typenum, end_valuedf[rating_count].max(), colorFF638EC6, showValueTrue ) worksheet.conditional_formatting.add(M2:M{}.format(len(df)1), count_rule)效果评分列自动红黄绿渐变评价人数列显示数据条长度——不用看数字颜色和长度已告诉你哪本书口碑两极分化、哪本是小众神作。6.3 添加数据验证防止人工编辑污染原始数据若 Excel 会交给他人补录如“是否已读”“笔记链接”需保护原始爬取列不被误改# 锁定原始数据列A-O仅允许编辑 P 列及之后 from openpyxl.styles import Protection for col in ABCDEFGHIJKLMNOP: for row in range(1, len(df) 2): cell worksheet.cell(rowrow, columnord(col) - ord(A) 1) if col in ABCDEFGHIJKLMNO: # 原始字段列 cell.protection Protection(lockedTrue) else: cell.protection Protection(lockedFalse) # 启用工作表保护密码可为空仅防双击误改 worksheet.protection.enable()效果双击原始列弹出“单元格被保护”提示但筛选、排序、条件格式仍可用——平衡了安全性与可用性。我坚持一个习惯每次交付 Excel 前用openpyxl跑一遍样式加固。不是为了炫技而是因为我知道当一份数据报表被转发三次后原始字段的完整性比任何 fancy 功能都重要。它不再只是爬虫结果而是可沉淀、可协作、可追溯的轻量知识资产。希望帮到你。本文还有配套的精品资源点击获取