)
一、前言为什么要写这个爬虫在数据驱动的时代图书推荐系统、阅读社区、知识管理平台往往需要高质量的图书数据。豆瓣读书作为国内最具影响力的图书评价平台其评分体系极具参考价值。而评分9.0以上的图书通常被公认为“神作”或“经典”。手动逐页筛选这些高分图书效率极低因此编写一个自动化爬虫不仅可以高效获取数据还能锻炼数据采集、解析、清洗和存储的全流程能力。本文将带领你使用最新的Python技术栈requests、BeautifulSoup、pandas、fake_useragent、retrying等爬取豆瓣读书中评分≥9.0的图书信息包括书名、作者、出版社、出版年份、评分、评分人数和一句话简介等。我们会重点讲解筛选与分页两大核心难点并给出完整的工程化代码最终将数据保存为CSV文件。声明本教程仅供学习交流使用请遵守robots.txt协议控制请求频率切勿对目标网站造成压力。目录一、前言为什么要写这个爬虫二、技术选型与环境搭建2.1 核心技术栈2.2 环境准备2.3 豆瓣读书页面分析三、爬虫设计思路3.1 工作流程3.2 关键难点与解决方案四、代码实现逐块详解4.1 导入库与配置4.2 随机User-Agent4.3 带重试机制的请求函数4.4 解析单页图书数据4.5 分页控制与主爬虫逻辑4.6 数据保存与主函数五、完整代码整合版六、运行测试与结果分析6.1 预期输出6.2 生成的CSV示例七、反爬虫策略与优化建议7.1 进阶防封措施7.2 数据清洗增强7.3 增量爬取八、总结与扩展8.1 核心收获8.2 可扩展方向二、技术选型与环境搭建2.1 核心技术栈库名作用requests发送HTTP请求获取网页HTMLBeautifulSoup4解析HTML提取所需数据pandas数据清洗与结构化存储fake_useragent随机生成User-Agent防止被封retrying请求失败自动重试lxml更快的HTML解析引擎可选re正则表达式辅助清洗time控制请求间隔模拟人类行为2.2 环境准备bash# 创建虚拟环境推荐 python -m venv douban_spider source douban_spider/bin/activate # Windows: douban_spider\Scripts\activate # 安装依赖库 pip install requests beautifulsoup4 pandas fake_useragent retrying lxml2.3 豆瓣读书页面分析目标URL模式texthttps://book.douban.com/tag/小说?start0typeT https://book.douban.com/tag/小说?start20typeT https://book.douban.com/tag/小说?start40typeT ...tag图书分类我们以“小说”为例可自定义start起始索引每页20条记录0,20,40...我们只提取评分 ≥ 9.0 的条目并持续跨页抓取直到某页完全没有高分图书。三、爬虫设计思路3.1 工作流程构造URL设置请求头。发送GET请求获取响应文本。使用BeautifulSoup解析每一页的图书列表。提取每本书的标题、链接、评分、评价人数、作者、出版社等信息。筛选评分 ≥ 9.0 的图书。自动翻页直到最后一页当前页没有任何评分≥9.0的图书。将所有数据存入DataFrame并导出CSV。3.2 关键难点与解决方案难点解决方案反爬虫User-AgentIP限制随机UA 请求延迟2-5秒 代理池可选评分筛选提取评分文本并转换为float比较分页终止条件检测当前页是否存在评分≥9.0的记录字段缺失部分图书无出版信息使用try-except或get方法设置默认值动态加载豆瓣图书列表是服务端渲染无需担心直接解析HTML四、代码实现逐块详解4.1 导入库与配置pythonimport requests import time import random import re import pandas as pd from bs4 import BeautifulSoup from fake_useragent import UserAgent from retrying import retry from typing import List, Dict, Optional # 全局配置 HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.5,en;q0.3, Connection: keep-alive, } TIMEOUT 10 MAX_RETRIES 3 REQUEST_INTERVAL (3, 6) # 随机间隔3~6秒4.2 随机User-Agentpythonua UserAgent() def get_random_headers(): 随机生成User-Agent降低封禁风险 headers HEADERS.copy() headers[User-Agent] ua.random return headers4.3 带重试机制的请求函数pythonretry(stop_max_attempt_numberMAX_RETRIES, wait_fixed2000) def fetch_page(url: str) - Optional[str]: 发送请求并返回HTML文本 重试最多3次每次间隔2秒 try: headers get_random_headers() response requests.get(url, headersheaders, timeoutTIMEOUT) response.encoding utf-8 # 豆瓣使用utf-8编码 if response.status_code 200: return response.text else: print(f请求失败状态码{response.status_code}URL{url}) return None except Exception as e: print(f请求异常{e}URL{url}) raise # 触发重试4.4 解析单页图书数据豆瓣图书列表页的HTML结构2025年最新版依然稳定每个图书条目位于li classsubject-item中评分位于span classrating_nums内评价人数span classpl内括号中的数字pythondef parse_book_item(item) - Optional[Dict]: 解析单个图书条目返回字典或None如果评分9.0 # 1. 标题和链接 title_tag item.find(div, class_info).find(h2).find(a) if not title_tag: return None title title_tag.get_text(stripTrue) book_url title_tag.get(href) # 2. 评分 rating_tag item.find(span, class_rating_nums) if not rating_tag: return None # 无评分则跳过 try: rating float(rating_tag.get_text(stripTrue)) except: rating 0.0 # 核心筛选只保留9.0分及以上 if rating 9.0: return None # 3. 评价人数 people_tag item.find(span, class_pl) rating_people 0 if people_tag: people_text people_tag.get_text(stripTrue) match re.search(r\((\d)人评价\), people_text) if match: rating_people int(match.group(1)) # 4. 图书信息作者/译者/出版社/出版年份 pub_tag item.find(div, class_pub) pub_info pub_tag.get_text(stripTrue) if pub_tag else # 示例格式[法] 阿尔贝·加缪 / 金祎 / 江苏凤凰文艺出版社 / 2022-12 / 45.00元 # 使用正则或简单split拆分这里为了通用性做粗略解析 parts pub_info.split(/) author parts[0].strip() if len(parts) 0 else 未知 translator # 如果存在译者一般含有译或位于第二个位置且非数字 if len(parts) 1 and (译 in parts[1] or len(parts[1].strip()) 10): translator parts[1].strip() publisher parts[2].strip() if len(parts) 2 else 未知 year parts[3].strip() if len(parts) 3 else 未知 else: translator 无 publisher parts[1].strip() if len(parts) 1 else 未知 year parts[2].strip() if len(parts) 2 else 未知 # 年份提取数字 year_match re.search(r\d{4}, year) year_num year_match.group(0) if year_match else 未知 # 5. 简介可选 abstract_tag item.find(p, class_detail) abstract abstract_tag.get_text(stripTrue) if abstract_tag else return { title: title, url: book_url, rating: rating, rating_people: rating_people, author: author, translator: translator, publisher: publisher, year: year_num, abstract: abstract }4.5 分页控制与主爬虫逻辑pythondef crawl_douban_books(tag小说, start0, max_pages50): 爬取豆瓣指定标签下的高分图书评分≥9.0 :param tag: 图书分类标签 :param start: 起始偏移量 :param max_pages: 最大翻页数防止无限循环 :return: 图书列表 base_url fhttps://book.douban.com/tag/{tag} all_books [] current_start start page_num 1 while page_num max_pages: url f{base_url}?start{current_start}typeT print(f正在抓取第{page_num}页: {url}) html fetch_page(url) if not html: print(获取页面失败停止爬取) break soup BeautifulSoup(html, lxml) book_items soup.find_all(li, class_subject-item) if not book_items: print(当前页没有找到任何图书条目停止翻页) break high_rating_count 0 for item in book_items: book_data parse_book_item(item) if book_data: all_books.append(book_data) high_rating_count 1 print(f第{page_num}页共{len(book_items)}本其中≥9.0分的有{high_rating_count}本) # 核心终止条件如果当前页没有任何一本评分≥9.0则停止爬取 if high_rating_count 0: print(当前页已无高分图书爬取结束) break # 翻页增加20 current_start 20 page_num 1 # 随机休眠模拟人类浏览行为 sleep_time random.uniform(*REQUEST_INTERVAL) print(f等待 {sleep_time:.2f} 秒后继续...) time.sleep(sleep_time) return all_books4.6 数据保存与主函数pythondef save_to_csv(books: List[Dict], filename: str douban_high_score_books.csv): 保存数据到CSV文件 if not books: print(无数据可保存) return df pd.DataFrame(books) # 去重根据URL df.drop_duplicates(subset[url], keepfirst, inplaceTrue) # 按评分降序排列 df.sort_values(by[rating, rating_people], ascending[False, False], inplaceTrue) df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f成功保存 {len(df)} 条记录到 {filename}) def main(): 主入口函数 print(豆瓣读书高分爬虫启动...) books crawl_douban_books(tag小说, max_pages30) print(f总共抓取到 {len(books)} 本评分≥9.0的图书) if books: # 展示前5本 for i, book in enumerate(books[:5], 1): print(f{i}.《{book[title]}》 评分{book[rating]} 评价人数{book[rating_people]}) save_to_csv(books) else: print(未获取到任何数据请检查网络或目标标签) if __name__ __main__: main()五、完整代码整合版python# douban_spider.py import requests import time import random import re import pandas as pd from bs4 import BeautifulSoup from fake_useragent import UserAgent from retrying import retry from typing import List, Dict, Optional # ---------- 配置 ---------- HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.5,en;q0.3, } TIMEOUT 10 MAX_RETRIES 3 REQUEST_INTERVAL (3, 6) ua UserAgent() def get_random_headers(): headers HEADERS.copy() headers[User-Agent] ua.random return headers retry(stop_max_attempt_numberMAX_RETRIES, wait_fixed2000) def fetch_page(url: str) - Optional[str]: try: response requests.get(url, headersget_random_headers(), timeoutTIMEOUT) response.encoding utf-8 if response.status_code 200: return response.text else: print(f请求失败状态码{response.status_code}) return None except Exception as e: print(f请求异常{e}) raise def parse_book_item(item) - Optional[Dict]: try: title_tag item.find(div, class_info).find(h2).find(a) if not title_tag: return None title title_tag.get_text(stripTrue) book_url title_tag.get(href) rating_tag item.find(span, class_rating_nums) if not rating_tag: return None rating float(rating_tag.get_text(stripTrue)) if rating 9.0: return None people_tag item.find(span, class_pl) rating_people 0 if people_tag: match re.search(r\((\d)人评价\), people_tag.get_text(stripTrue)) if match: rating_people int(match.group(1)) pub_tag item.find(div, class_pub) pub_info pub_tag.get_text(stripTrue) if pub_tag else parts [p.strip() for p in pub_info.split(/)] author parts[0] if len(parts) 0 else 未知 translator 无 publisher 未知 year 未知 if len(parts) 1: if 译 in parts[1] or len(parts[1]) 10: translator parts[1] publisher parts[2] if len(parts) 2 else 未知 year parts[3] if len(parts) 3 else 未知 else: publisher parts[1] year parts[2] if len(parts) 2 else 未知 year_num re.search(r\d{4}, year) year year_num.group(0) if year_num else 未知 abstract_tag item.find(p, class_detail) abstract abstract_tag.get_text(stripTrue) if abstract_tag else return { title: title, url: book_url, rating: rating, rating_people: rating_people, author: author, translator: translator, publisher: publisher, year: year, abstract: abstract } except Exception as e: print(f解析条目出错{e}) return None def crawl_douban_books(tag小说, start0, max_pages50): base_url fhttps://book.douban.com/tag/{tag} all_books [] current_start start page_num 1 while page_num max_pages: url f{base_url}?start{current_start}typeT print(f[第{page_num}页] 抓取 {url}) html fetch_page(url) if not html: break soup BeautifulSoup(html, lxml) items soup.find_all(li, class_subject-item) if not items: print(无图书条目停止翻页) break high_count 0 for item in items: data parse_book_item(item) if data: all_books.append(data) high_count 1 print(f本页高分图书数量{high_count}) if high_count 0: print(已无更多高分图书结束爬取) break current_start 20 page_num 1 sleep_time random.uniform(*REQUEST_INTERVAL) print(f休眠 {sleep_time:.2f} 秒\n) time.sleep(sleep_time) return all_books def save_to_csv(books, filenamedouban_top_books.csv): if not books: return df pd.DataFrame(books) df.drop_duplicates(subset[url], inplaceTrue) df.sort_values([rating, rating_people], ascendingFalse, inplaceTrue) df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f已保存 {len(df)} 条数据至 {filename}) def main(): books crawl_douban_books(tag小说, max_pages30) print(f总计获取{len(books)} 本) if books: save_to_csv(books) print(\n示例数据) for b in books[:3]: print(f《{b[title]}》 {b[rating]}分 {b[rating_people]}人评价) if __name__ __main__: main()六、运行测试与结果分析6.1 预期输出text[第1页] 抓取 https://book.douban.com/tag/小说?start0typeT 本页高分图书数量8 休眠 4.23 秒 [第2页] 抓取 https://book.douban.com/tag/小说?start20typeT 本页高分图书数量5 休眠 3.67 秒 ... [第6页] 抓取 https://book.douban.com/tag/小说?start100typeT 本页高分图书数量0 已无更多高分图书结束爬取 总计获取42 本 已保存 42 条数据至 douban_top_books.csv6.2 生成的CSV示例titleratingrating_peopleauthorpublisheryearabstract活着9.4385672余华作家出版社2012《活着》讲述了人如何去承受巨大的苦难...百年孤独9.5298451[哥伦比亚] 加西亚·马尔克斯南海出版公司2011魔幻现实主义文学代表作...七、反爬虫策略与优化建议7.1 进阶防封措施IP代理池使用免费或付费代理如requestsproxies参数。请求头轮转除UA外也随机更换Accept-Language等。Cookies维持从浏览器复制一个已登录的Cookie字符串提高信任度。异步爬取使用aiohttp提升效率但需更谨慎控制并发。7.2 数据清洗增强使用jieba分词对简介做关键词提取。通过isbnlib库根据书名自动补全ISBN号。出版社名称标准化“中信出版社”与“中信出版集团”合并。7.3 增量爬取记录上次爬取的最后一页URL下次启动时从该页开始避免重复。八、总结与扩展8.1 核心收获分页控制通过start参数递增并设计合理的终止条件当前页无高分图书。评分筛选在解析阶段进行浮点数比较提前丢弃低分数据节省内存。健壮性设计重试机制、异常捕获、随机延时保障长时间稳定运行。8.2 可扩展方向多标签并行爬取如“历史”“科幻”“哲学”等对比不同类别的高分密度。存入数据库使用SQLite或PostgreSQL方便后续查询分析。可视化分析用matplotlib/seaborn绘制评分分布、年份趋势图。构建推荐系统基于爬取的数据和协同过滤算法做简单的图书推荐。