简介:这是一份面向Python初学者与数据采集爱好者的豆瓣读书网站实战爬虫项目,解决图书信息批量抓取与结构化存储问题,适用于课程设计、数据分析入门及个人书单整理等场景。资源包共5个文件,含2个核心Python脚本(douban_crawler.py与面向对象版本douban_crawler_oop.py)、1个依赖锁定文件(Pipfile.lock)、1个项目配置文件(Pipfile)及1个Git忽略配置(.gitignore),整体仅7KB,轻量易读,便于快速理解请求发送、HTML解析(基于Requests+BeautifulSoup)、数据清洗与Excel导出(pandas.to_excel)全流程。已有996人学习下载,提供开箱即用的可运行代码、清晰的模块划分(函数式与OOP双实现对比)及标准化Python项目结构,助读者掌握网络爬虫基础开发范式与反爬应对思路,同时规避常见编码、请求头缺失等实操坑点。 豆瓣读书的数据无论是做阅读记录、书单整理、年度盘点还是搞点数据分析小项目,都是非常理想的爬虫练手对象。页面结构相对规整,字段信息齐全,书籍标题、作者、出版社、评分、评价人数、简介都在那里,而且榜单和标签体系很适合做批量抓取。这篇文章我把自己把豆瓣读书的Top250榜单抓下来并保存到Excel的完整过程记录下来,代码在2021年6月28日跑通,这也是标题里那个日期的由来。爬虫这东西有个特点,页面一改版代码就失效,所以标题里带上日期其实是一种惯例,意思是“这个版本在当前时间点可用”。
这套东西适合谁参考?想入门Python爬虫但不知道从哪下手的初学者,想把豆瓣读书数据抓下来做个人分析的阅读爱好者,还有需要给团队做爬虫培训、想找一个完整案例讲清楚“请求-解析-存储”全流程的人。整个项目用到的技术栈很基础,requests发请求,BeautifulSoup做解析,pandas写Excel,没有任何高门槛的东西,跟着敲一遍基本都能跑通。
1. 项目背景与整体设计思路
1.1 为什么选豆瓣读书作为爬虫目标
豆瓣读书是我接触过最适合新手练手的数据源之一,原因很直接。第一,页面结构稳定,豆瓣的HTML代码多年来保持了一种“老派”的风格,没有像很多现代网站那样大量使用JavaScript动态渲染,这意味只要requests能拿到HTML,数据就基本都在里面。第二,数据字段丰富,一本书的完整元数据至少能拿到书名、作者、出版社、出版年、评分、评价人数、内容简介,这些字段组合起来足够做很多维度的分析了。第三,榜单逻辑简单,Top250这种榜单的分页通过URL参数就能控制,翻页就是在start参数上做累加,不需要处理复杂的翻页规则。
选这个目标还有一层考虑——爬虫最烦的就是要处理各种动态加载和接口签名,而豆瓣读书就像一个“老实的网页”,怎么请求就怎么返回,对于理解爬虫的核心逻辑非常有帮助。学会抓豆瓣读书之后,再去碰那些需要逆向JS、模拟登录的网站,至少你已经知道基础流程是什么样,剩下的就是针对性地补其他知识。
1.2 整体技术路线与方案选型
这个项目的技术方案可以概括成一句话:requests发起HTTP请求,BeautifulSoup解析HTML,pandas把数据写入Excel。三个库分别对应爬虫的三大核心环节,简单直接。
这里要解释一下各环节的选型理由。首先为什么用requests而不是Scrapy?因为数据量级和数据规模就那么多,Top250总共10页,拿requests写脚本几十行就能搞定,而Scrapy需要搭建工程结构、配置Item和Pipeline,属于“杀鸡用牛刀”但配置成本反而更高。等以后需要爬数十万级数据、要做分布式的时候再上Scrapy也不迟,新手阶段用requests把逻辑理清楚比用框架更重要。其次,为什么用BeautifulSoup而不是正则表达式?正则虽然性能好,但可读性极差,写大段复杂的匹配规则非常容易出错。BeautifulSoup把HTML解析成树状结构,通过CSS选择器或者find方法定位元素,代码写出来自己能看懂,后面想维护也方便。最后,Excel的写入我用的pandas的to_excel方法,因为pandas的DataFrame结构天然就是一张二维表,把抓到的数据构造成DataFrame再输出Excel,比用openpyxl一行一行写单元格方便得多。
这个设计还有一个很关键的考量——把请求、解析、存储做成三个相对独立的模块,即使后面豆瓣改了页面结构,只需要修改解析那一块的代码就行,请求和保存的代码基本不用动。项目看起来小,但结构上留了扩展空间。
1.3 爬虫的一般工作流程梳理
写爬虫和写普通的业务代码思维上有很大区别。普通程序的数据是从数据库或用户输入来的,而爬虫的数据是“从别人的网页里挖出来的”。整个流程可以拆成四步,本项目也是严格按这四步来走的:
- 第一步,构造请求。弄明白目标页面URL的规律,把请求头、参数、Cookie这些准备好,用requests把网页内容拿到手。
- 第二步,解析内容。把拿到的HTML字符串喂给BeautifulSoup,定位到包含目标数据的标签位置,把数据提取出来。
- 第三步,清洗数据。抓到的字符串可能带空格、换行、HTML实体这类杂质,需要统一处理成干净的结构化数据。
- 第四步,存储落地。按照需要的格式,把清洗后的数据写入文件或数据库,本项目就是写入Excel。
这四步是所有爬虫的通用骨架。换句话说,不管你最后爬的是电商、招聘网站还是社交媒体,脑子里都要有这条流水线。豆瓣读书的项目就是把这条流水线完整走了一遍,每一步都清晰可见。
2. 环境准备与依赖库选择
2.1 开发环境与Python版本说明
我在这个项目里用的Python版本是3.8,实际上Python 3.6以上的版本跑这套代码都没有问题。操作系统方面,Windows、macOS、Linux都可以,代码里没有涉及任何平台相关的操作,唯一区别是Windows下可能有个别编码问题,后面会专门讲到。
建议先用虚拟环境隔离依赖,避免把系统全局的Python环境搞乱。我之前踩过坑,直接在全局环境pip install了一堆库,后面某个项目需要某个库的旧版本,升级之后把另一个项目搞挂了。现在习惯新项目先建虚拟环境,磨刀不误砍柴工。创建虚拟环境就两条命令:
python -m venv douban_env # Windows激活 douban_env\Scripts\activate # macOS/Linux激活 source douban_env/bin/activate激活之后命令行前面会出现环境名称的提示,这时候pip安装的东西都只在这个虚拟环境里生效,和其他项目互不干扰。
2.2 依赖库清单与安装步骤
本项目依赖的第三方库有四个:requests、beautifulsoup4、pandas、openpyxl。其中openpyxl不是直接导入的,而是pandas写Excel文件时需要的引擎,不装的话会报错。
pip install requests beautifulsoup4 pandas openpyxl这四个库各有分工。requests负责HTTP请求,是整个爬虫的“脚”。beautifulsoup4负责HTML解析,是整个爬虫的“眼睛”。pandas负责数据结构化和数据输出,是整个爬虫的“手”。openpyxl是pandas和Excel之间的桥,具体来说是Excel写入引擎。
安装过程中如果遇到下载慢的问题,可以临时切换成国内镜像源,实测下载速度快好几倍:
pip install requests beautifulsoup4 pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 爬虫必备的基础知识储备
在开始写代码之前,有几个基础知识点必须要懂,不然代码跑出问题都不知道在哪里排查。
第一个是HTTP请求与响应。requests库做的事情本质上是模拟浏览器发HTTP请求,服务器返回HTML文本。要理解请求头(Headers)、User-Agent(用户代理)、响应状态码(200表示正常,403表示拒绝访问)、请求方法(GET和POST)这些概念。本项目只需要用GET方法,因为豆瓣读书的榜单页面是普通的网页链接,不需要提交表单。
第二个是HTML结构。BeautifulSoup解析的前提是你得能看懂HTML的标签嵌套关系。不需要精通前端开发,但至少要能看懂div、span、a、p这些常用标签是什么意思,class和id属性的作用是什么。这些知识在写选择器的时候直接决定你定位数据的速度。
第三个是编码处理。网站返回的内容默认可能是UTF-8编码,但豆瓣的页面Response里面带了charset信息,一般requests会根据响应头自动解码。真正容易出问题的是Windows系统下的中文编码,Excel文件写入和读取时容易发生乱码,后面排查部分会详细说。
3. 请求与解析:核心功能实现
3.1 分析目标页面URL规律
先打开豆瓣读书的Top250页面,网址是https://book.douban.com/top250,这是第一页,默认展示25本书。往下拉到最底部的翻页导航,点第二页看URL变化。
豆瓣的翻页逻辑是用start参数控制的,start值表示“跳过的记录条数”。第一页start=0,第二页start=25,第三页start=50,以此类推。到第十页start=225,加上每页25条,总共250条。URL规律可以用一个简单公式概括:
url = f"https://book.douban.com/top250?start={start}"这个规律在写循环的时候非常有用,整个爬虫的翻页逻辑就是0到225,步长25。需要注意的是,URL上可能还有其他参数,比如filter,但实测下来不加这个参数完全不影响数据获取。
还要搞清楚一个概念,Top250这个榜单的URL规则和豆瓣其他榜单(比如豆瓣读书“图书榜单”里的各个分类)不同,有些榜单的URL用的是另一种路径格式。所以写爬虫第一步永远是打开浏览器,手动观察URL长什么样,搞清楚规律再动手。
3.2 请求头配置与User-Agent伪装
这是整个项目里最容易被忽略、但直接关系能否成功访问的一环。豆瓣有一个基础的反爬策略:检查请求的User-Agent,如果发现是Python-requests的默认值,直接拒绝响应,返回418或者是403。第一次写爬虫的人最容易在这里栽跟头,打开浏览器明明能看到内容,代码请求却什么都拿不到。
解决方式很简单,把请求头里User-Agent伪装成一个正常浏览器的值。chrome浏览器在Windows下的User-Agent字符串大概长这样:
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"除了User-Agent,请求头里最好把Accept-Language也加上,否则部分情况下返回的页面可能是英文版,导致解析时找不到中文数据。实际项目中我把请求头写成一个字典变量,方便统一维护:
headers = { "User-Agent": UA, "Accept-Language": "zh-CN,zh;q=0.9", }这个headers字典每次构造请求时传进去即可。需要强调一点,不要每次都随机换一个User-Agent,那样反而容易触发更严格的反爬机制。用一个稳定的、真实存在的浏览器UA,效果反而好。
3.3 单页数据抓取与BeautifulSoup解析
拿到HTML之后的核心工作是解析。这一步的产出是一本单页25本书的数据,我封装成一个函数,传入一个start值,返回一个包含25本书信息的列表。
先看请求和构造BeautifulSoup对象的代码:
import requests from bs4 import BeautifulSoup def fetch_page(start): url = f"https://book.douban.com/top250?start={start}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9" } resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") return soup需要注意resp.encoding这一行。虽然requests理论上会根据响应头自动猜编码,但实测豆瓣的响应头里charset信息有时不明确,手动指定utf-8能避免后续解析出现乱码。
解析的核心逻辑是怎么定位每一本书。打开浏览器按F12,查看Top250页面中每一本书的HTML结构,会发现每本书被包在一个<tr class="item">标签里。这其实是表格布局,每本书一行。基于这个规律,先用soup.find_all("tr", class_="item")把所有书籍行全部找出来,然后遍历每一行提取字段。
以书名和评分信息为例,在每一行里,书名在class="pl2"的div下的第一个a标签的title属性里,评分在class="rating_nums"的span标签的文本里:
def parse_page(soup): books = [] items = soup.find_all("tr", class_="item") for item in items: title_tag = item.find("div", class_="pl2").find("a") title = title_tag.get("title", "").strip() rating_average = item.find("span", class_="rating_nums").text.strip() # 其他字段继续往下提取 books.append({ "书名": title, "评分": rating_average, }) return books这里用get("title")而不是text来取书名,是因为豆瓣的书名如果太长,a标签中间的文本会被截断加省略号,但title属性保存的永远是完整书名。这是抓数据时容易被忽略的细节,用text取出来后面清洗会很麻烦。
3.4 书籍字段完整提取与清洗逻辑
实际项目中,我从每一本书里提取了八个字段:书名、作者/出版社信息、评分、评价人数、出版社、出版年、价格、内容简介。后面五个字段其实都在同一个信息块里,需要做一些处理才能拆分开。
原著信息在class="pub"的p标签里,文本格式类似“作者名 / 出版社 / 出版年 / 价格”。下面是完整提取逻辑:
def parse_page(soup): books = [] items = soup.find_all("tr", class_="item") for idx, item in enumerate(items, start=1): # 书名 title_tag = item.find("div", class_="pl2").find("a") title = title_tag.get("title", "").strip() # 评分 rating_average = item.find("span", class_="rating_nums").text.strip() # 评价人数 rating_num_tag = item.find("span", class_="pl") rating_people = rating_num_tag.text.strip() # 类似 "(9641人评价)" 的格式,需要清洗成数字 rating_people = rating_people.replace("(", "").replace(")", "").replace("人评价", "").strip() # 原著信息块 pub_info = item.find("p", class_="pub").text.strip() parts = pub_info.split(" / ") # parts[0] 是作者,parts[1] 是出版社,parts[2] 是出版年,parts[3] 是价格 # 内容简介 quote_tag = item.find("p", class_="quote") quote = quote_tag.find("span", class_="inq").text.strip() if quote_tag else "" book = { "书名": title, "作者": parts[0], "出版社": parts[1] if len(parts) > 1 else "", "出版年": parts[2] if len(parts) > 2 else "", "价格": parts[3] if len(parts) > 3 else "", "评分": rating_average, "评价人数": rating_people, "内容简介": quote, } books.append(book) return books有几个细节注释说明一下。quote标签不是每本书都有,排行榜里大约有40%的书有内容简介,所以获取时用了条件表达式兜底,没有就填空字符串。pub信息拆分用split(" / "),注意斜杠前后各有一个空格,因为豆瓣页面源码里就是这种格式,如果不带空格直接split("/"),作者名里的斜杠会把数据拆错。
4. 数据落地:用pandas写入Excel
4.1 DataFrame构造与Excel导出实现
数据抓下来并清洗完成之后,就进入最后一环——保存到Excel。这一环节用pandas实现非常舒服。把所有书的字典列表直接传给DataFrame构造函数,DataFrame会自动把字典的键当成列名,值当成单元格内容。
import pandas as pd df = pd.DataFrame(all_books) df.to_excel("douban_top250.xlsx", index=False, engine="openpyxl")这里两个参数值得解释一下。index=False表示不把DataFrame的行索引写入Excel,否则表格第一列会多出0、1、2、3这种无意义的行号。engine="openpyxl"指定了Excel写入引擎,因为pandas默认的xlsxwriter不是必备依赖,提前装了openpyxl就明确指定用它。
如果想控制列的顺序,可以在创建DataFrame时传columns参数:
order = ["书名", "作者", "出版社", "出版年", "价格", "评分", "评价人数", "内容简介"] df = pd.DataFrame(all_books, columns=order)这样导出的Excel表格列顺序就是列表里的顺序,不会因为字典的键顺序影响展示。
4.2 完整主流程代码整合
把前面的所有部分拼接起来,就是整个项目的完整代码。主流程的逻辑非常清楚:循环10次,每次取一个start值,请求、解析、累积数据,请求之间sleep一下,最终把累积的数据统一写入Excel。
import time import requests from bs4 import BeautifulSoup import pandas as pd ALL_BOOKS = [] def fetch_and_parse(start): url = f"https://book.douban.com/top250?start={start}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9" } resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") items = soup.find_all("tr", class_="item") for item in items: title_tag = item.find("div", class_="pl2").find("a") title = title_tag.get("title", "").strip() rating_average = item.find("span", class_="rating_nums").text.strip() rating_num_tag = item.find("span", class_="pl") rating_people = rating_num_tag.text.strip() rating_people = rating_people.replace("(", "").replace(")", "").replace("人评价", "").strip() pub_info = item.find("p", class_="pub").text.strip() parts = pub_info.split(" / ") quote_tag = item.find("p", class_="quote") quote = quote_tag.find("span", class_="inq").text.strip() if quote_tag else "" book = { "书名": title, "作者": parts[0], "出版社": parts[1] if len(parts) > 1 else "", "出版年": parts[2] if len(parts) > 2 else "", "价格": parts[3] if len(parts) > 3 else "", "评分": rating_average, "评价人数": rating_people, "内容简介": quote, } ALL_BOOKS.append(book) for start in range(0, 250, 25): print(f"正在抓取 start = {start}") fetch_and_parse(start) time.sleep(3) df = pd.DataFrame(ALL_BOOKS) df.to_excel("douban_top250.xlsx", index=False, engine="openpyxl") print("抓取完成,数据已保存到 douban_top250.xlsx")这段代码跑完之后,当前目录下会生成一个douban_top250.xlsx文件,打开就能看到完整的250条数据。注意range(0, 250, 25)这个写法,步长是25,正好覆盖10页数据。之前在调试的时候,写成range(0, 250)忘加步长参数,结果请求了250个不同的URL,虽然页面一样但白费了大量时间。
4.3 数据质量检查的要点
保存完成后不要急着收工,先对数据做一次质量检查。常见问题包括:抓取总数是不是250条,有没有字段为空的记录,评分是否都在合理范围内,等等。可以用pandas快速做一次数据探查:
df = pd.read_excel("douban_top250.xlsx", engine="openpyxl") print(df.shape) # 查看行列数 print(df.isnull().sum()) # 查看各列空值数量 print(df["评分"].describe()) # 查看评分字段的统计信息检查完数据质量没问题,整个项目才算真正完成。我在实际项目中执行了上面三行检查,输出的数据规模和预期完全一致,五个核心字段没有任何缺失,评分的平均值和豆瓣官方展示的高分榜情况吻合,说明抓取是准确的。
5. 反爬应对与长期可用性调整
5.1 豆瓣反爬策略分析与应对实践
豆瓣的反爬机制在业界属于中等强度,不像小红书、抖音那些App一样要搞签名逆向,但也不是完全不设防。我的实测经验是,豆瓣主要从三个维度做限制,只要每一个都处理到位,脚本就能稳定运行。
第一个维度是User-Agent校验,前面已经说过,requests默认的UA会被直接拒绝,伪装成真实浏览器以后基本能解决。第二个维度是请求频率限制,短时间高频请求会触发封IP,常见表现是页面返回403或者直接弹出验证码。我的应对手段是在每次翻页之间加sleep,延时随机设在3到5秒之间,让整个抓取节奏接近人工浏览的速度。第三个维度是Cookie校验,个别情况下豆瓣会要求提供Cookie才能继续访问,这个一般发生在IP被风控之后,等一段时间就能自动恢复。
防封IP这件事要讲一个客观事实:写爬虫的人跟封IP的规则之间其实就是一场猫鼠游戏,但站在个人学习、小规模抓取的角度,频率克制一点基本不会碰到封IP的严重情况,没必要一上来就研究代理池。合理设置延时、控制抓取总量、只抓必要的数据,这三点做到位,就能跟目标网站和谐共处。
5.2 请求延时、容错重试与异常处理
我在代码里把time.sleep(3)写成了固定延时,但更稳健的做法是让延时在一个范围内随机波动,这样请求特征更接近人的点击行为。实际项目中我把这个逻辑封装成一个变量:
import random import time time.sleep(random.uniform(2, 4))random.uniform(2, 4)会在2到4秒之间随机取一个浮点数,每次请求的间隔都不一样。这个细节虽然不影响功能正确性,但能明显降低触发风控的概率。
网络请求还有一类典型问题——超时和偶发失败。requests的timeout参数我设置了10秒,意思是超过10秒没有响应就放弃。但对于偶发的网络抖动,直接放弃整页数据太可惜,比较合理的做法是捕获异常后延时重试。可以把请求部分改造成带重试逻辑的版本:
for attempt in range(3): try: resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" return resp except Exception as e: print(f"请求失败,重试第 {attempt + 1} 次: {e}") time.sleep(5) return None这层重试逻辑在真实环境中非常有价值。我在调试时遇到过请求超时、连接被重置、偶尔SSL证书错误等各类情况,有重试兜底的话整个脚本能自动恢复,不需要人工盯在终端前面。
5.3 爬虫代码为什么会失效以及应对策略
回到标题里的日期,爬虫代码确实有“保质期”的概念。网站前端改版是常态化的事情,某个class名字改了、HTML结构调整了、URL参数变化了,都可能导致代码失效。最典型的失效场景是class="item"改成class="book-item",或者某个span的嵌套层级变化。这种情况下代码不会报错,但解析出来的数据是空的,排查起来还比较费劲。
要让代码具备长期可用性,有两个思路。第一个思路是定期运行验证脚本,因为抓回来的数据结构是已经定义好的,直接检查有几条数据、数据有没有空值,就能快速判断页面结构是否发生变化。第二个思路是把解析逻辑与请求逻辑分离,页面结构变了只需要修改解析那一部分,不用动其他代码。如果页面结构变化太大,解析逻辑重写也没办法,那就需要重新分析页面,用新的选择器替代失效的旧选择器。写爬虫的人心里要有数:维护爬虫的时间往往比写爬虫的时间多,这不是一次性的工作,而是需要持续跟进的。
6. 常见问题与排查技巧
6.1 请求阶段问题速查
这一阶段最容易出现的报错就是403和418,出现这类状态码说明请求被服务器拒了,核心原因就是User-Agent未伪装或者访问频率太高。403状态码的排查逻辑是:先确认headers有没有带UA,再确认延时是否合理,最后确认是不是IP被封了需要等待解封。下面用表格把请求阶段的高频问题整理出来:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 返回403 Forbidden | User-Agent未伪装或频率过高 | 检查headers配置,增加延时 |
| 返回418 I'm a teapot | 被识别为自动化请求 | 更新UA为最新浏览器版本 |
| 请求超时 | 网络波动或代理问题 | 增加timeout值,添加重试逻辑 |
| 返回数据不完整 | 页面短时异常或接口响应被截断 | 查看resp.text长度,验证完整HTML |
| 中文乱码 | 编码识别错误 | 手动指定resp.encoding = "utf-8" |
6.2 解析阶段问题速查
解析阶段最常见的坑是Find方法返回None。这个问题一般有两种原因:要么是class名称写错了,比如把item写成了item1,Python不会报错,但find_all返回空列表,循环体压根不会执行;要么是页面结构变了,class名称在改版中被调整了。排查方法是打印soup.prettify()的输出,用肉眼确认目标标签的真实结构。
还有一个非常容易忽略的细节:有些className里包含空格,比如class="item special",虽然页面源码里确实写了这个class,但如果用class_="item special"去找,BeautifulSoup可能匹配不到。因为BeautifulSoup的class_参数匹配的是完整的class属性值,空格会拆分成多个class。遇到这种情况用chooseclass_="item",或者直接改用select方法配合CSS选择器会更稳。
6.3 Excel写入阶段问题速查
写入Excel时最常见的报错是ModuleNotFoundError: No module named 'openpyxl',解决办法很简单,pip install openpyxl。另一个坑是文件被占用,Excel里打开了同名的文件写入就会报PermissionError,把Excel关掉重新运行即可。还有Windows下中文路径的问题,如果保存路径包含中文且用了比较老的pandas版本,可能出现编码错误,升级pandas或者把路径改成英文即可。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| No module named 'openpyxl' | 缺少Excel写入引擎 | pip install openpyxl |
| PermissionError: [Errno 13] | 目标Excel文件正被打开 | 关闭Excel后重跑 |
| 保存路径含中文报错 | Windows编码兼容问题 | 升级pandas或使用英文路径 |
| Excel打开后中文乱码 | 编码指定不正确 | 请求时手动设置utf-8编码 |
6.4 高频异常实录与独家避坑经验
最后分享几个我在实际调试过程中遇到的真实问题和处理方式,这些经验花了不少时间才总结出来,值得特别留意。
第一个是“状态码正常但解析不到数据”的问题。有一次代码跑完,程序没报错,但Excel里只有几条数据,循环里的print也没输出任何异常。后来检查发现HTML里class的写法变了,format是class="item "多了个空格,导致find_all("tr", class_="item")完全匹配不到。这类问题不会抛异常,但结果就是数据量不对,所以每次抓完一定要检查数据条数。
第二个是“抓一半IP被限制”的问题。有一次为了测试把延时改成了0.5秒,结果爬到第二页就连续403了,后来把延时调回3到5秒等了几分钟,请求恢复正常。这个教训让我深深记住了“频率克制”这四个字——宁可跑得慢一点,也不要让整个IP被风控。
第三个是作者信息里含有“/”导致的解析错乱。某些双语作者的翻译名里会自带斜杠,比如“村上春树 / 林少华 / 译”这类内容,直接split("/")会把一条信息拆成两三条,导致字段错位。处理方式是改为split(" / "),先把空格加斜杠的组合作为分隔符,这样字段错位的概率大大降低。如果还有更复杂的作者名情况,就需要专门写清洗逻辑了。
7. 项目扩展思路与后续优化方向
7.1 从Top250扩展到标签页和搜索页
Top250只是豆瓣读书其中一个榜单,爬虫跑通之后最自然的扩展方向就是抓豆瓣读书其他分类的榜单,比如文学、小说、历史、哲学这些热门标签。标签页URL也有固定格式,以文学分类为例,https://book.douban.com/tag/文学,分页方式同样是start参数,解析页面的HTML结构和Top250稍有不同,但核心思路完全一样——先分析页面结构,再写选择器。有了Top250的代码打底,改造成本很低。
搜索页的抓取稍微麻烦一点,因为搜索结果页的URL带URL编码参数,而且需要处理空结果的边界情况。不过只要理解了“构造URL、发起请求、解析页面、存储结果”这个流程,换任何数据源都是套模板的事情。我自己的感受是,把一个项目彻底吃透比草草写十个爬虫项目效率高得多。
7.2 保存格式与存储方式升级
Excel适合数据量比较小、给人做分析的场景,但如果数据量上来了,或者希望数据能被程序频繁读取,MySQL、SQLite这类数据库是更好的选择。pandas写入数据库非常方便,一行代码就能完成:
import sqlite3 conn = sqlite3.connect("douban.db") df.to_sql("books", conn, if_exists="replace", index=False)如果希望把抓取做成每天的定时任务,还可以在数据表里加一个crawl_date字段,记录每次入库的时间,这样后续就能分析榜单变化趋势,比如哪本书评分在新的一周上涨最快。这种功能在只保存Excel的场景下不太好实现,但换成数据库就顺理成章了。从项目学习的角度看,先掌握Excel落地,再过渡到数据库存储,每一步的跨度都不大,学习曲线非常平滑。
7.3 爬虫学习的进阶路径
这个项目跑通后,如果想在爬虫方向继续深入,我的建议是按照这几个阶段往前推进。第一阶段是依赖定向请求加解析的纯静态页面项目,把requests和BeautifulSoup用到滚瓜烂熟。第二阶段接触需要处理Session、Cookie、模拟登录的网站,把“有状态请求”这个概念搞清楚。第三阶段学Scrapy框架,用框架重新实现之前用requests写过的项目,体会框架在工程化方面的优势。第四阶段才需要碰JavaScript渲染页面,这时候不用急着上Selenium,先用requests直接调接口、分析XHR请求,往往效率更高。
最后必须说一句关于合规的话。爬虫本身是一种中性的技术工具,但用的时候要遵守目标网站的robots协议和用户协议,抓取频率要克制,抓下来的数据不要用于商业用途,尤其不要涉及个人隐私数据和受版权保护的内容。做一个技术能力在线、同时也懂得尊重规则的人,这条路才能走得长远。
本文还有配套的精品资源,点击获取