1. 项目整体设计与思路拆解1.1 为什么选豆瓣Top100当练手项目用 Python 爬豆瓣 Top100 电影名单存成 CSV是我接触过的最适合入门网络爬虫的实战项目之一。很多刚开始学爬虫的朋友第一个能完整跑完的项目往往就是它页面数据是服务端直接渲染好的静态 HTML不需要分析接口的加密参数不依赖登录态只要会用浏览器“查看源代码”就能看到完整数据。这就把难点集中在爬虫最基础的三个动作上发请求、解析内容、保存结果。标题里写的是 Top100本质上是豆瓣 Top250 列表的前 100 条。Top250 列表分 10 页展示每页 25 条4 页正好凑满 100。这个地方值得单独说一下很多人在网上抄代码时没有意识到“页码”和“start 参数”的关系爬来爬去都是第一页的数据输出的 CSV 只有 25 行。理解了 Top100 Top250 前 4 页之后后面想扩展到 Top250只需要把循环范围从 100 改成 250一行代码的事但你没搞懂原理就改不动。另外这个项目是典型的“投入产出比极高”的练手项目。写一遍大概 100 行代码但这 100 行里覆盖了网页结构分析、请求头设置、数据清洗、文件编码处理等一堆实用技能。很多人在课程里学完语法之后不知道怎么落地卡住的原因往往不是语法不会而是“不知道从哪一步开始想”。这个项目能帮你把零散的知识点串成完整的链路。1.2 技术选型背后的几个理由既然要爬静态页面技术栈无非就是那几样。我推荐的是 requests BeautifulSoup Python 内置的 csv 库。这套组合可以说是爬虫界的街车丑但稳人人都看得懂出了问题也特别好排查。requests 为什么比 urllib 好用直接说结论requests 对 Cookie、请求头、编码的处理比 urllib 人性化太多。urllib 是 Python 标准库自带的不假但用它可以写就是每次要处理Request对象、urlopen返回的响应对象还要手动read().decode()多了一层无意义的样板代码。requests 直接一句requests.get(url, headersheaders)就能拿到内容响应对象的.text属性还会自动根据响应头推断编码对新手来说少踩一半的坑。BeautifulSoup 则解决了“如何在 HTML 里定位目标数据”这个问题。对比一下用正则表达式去匹配电影信息写出来的 pattern 又长又脆页面稍微多个空格就匹配失败用 BeautifulSoup 的话本质上是“给你一段 HTML 文档你告诉我里面哪个节点是什么”API 设计得符合直觉。你可以在代码里把文档结构打印出来肉眼确认目标数据落在哪个标签里再写选择器这种交互方式对学习阶段特别友好。至于存储格式有人说为什么不直接存 Excel或者干脆用 pandas我的看法是CSV 是纯文本格式任何文本编辑器都能打开数据可以被 Excel、Numbers、pandas、SQLite 无痛地继续加工是跨工具兼容性最好的通用格式。用 Python 内置的 csv 模块不引入额外依赖这让学生能把注意力放在“爬虫本身”而不是“为了存数据还要再学一个 pandas”。1.3 整体流程拆解把整个项目拆开看其实就是一根线串了四件事分析豆瓣 Top250 的页面 URL 规律和 HTML 结构。用 requests 模拟浏览器发起 HTTP 请求拿到 HTML 文本。用 BeautifulSoup 解析 HTML按字段提取电影信息。用 csv 模块把数据写入本地文件并把编码设置正确。这四步每一环都有坑。第一步的坑是 URL 参数没搞明白导致翻不了页第二步的坑是请求头没带全被服务器拒绝或者没显式指定编码导致中文乱码第三步的坑是 HTML 结构判断失误提取出来的是空数据第四步的坑就更多了最典型的就是 CSV 用 Excel 打开后中文全部变成乱码。把项目划分为这四个阶段还有一个好处当程序报错时你可以准确判断“你现在卡在哪一环”而不是在整套代码里毫无头绪地打转。下面我按照这个流程把每一环节的关键细节都过一遍并给出可直接运行的完整代码。2. 核心细节解析与实操要点2.1 目标页面结构与字段分析先看目标地址。豆瓣 Top250 的页面地址和翻页逻辑非常规整首页是https://movie.douban.com/top250第二页以及后续页面的地址是这个结构https://movie.douban.com/top250?start25filter这里的start是“本页第一条电影在完整列表中的偏移量”。第一页相当于start0第二页是start25第三页是start50。每页展示 25 条所以 Top100 就是取start从 0 到 75 的 4 个页面。这里的filter参数有没有实际作用其实不重要保留它只是为了和浏览器的请求保持一致。接着看页面里的数据结构。定位到单条电影信息所在的区块你会看到一个class为item的div每个div代表一部电影。电影的信息分布大致如下电影标题在item下的.hd区块中class为title的链接文本。注意这地方有中英文两个标题实际取第 0 个标题也就是中文名。评分在classrating_num的span标签中。评价人数在.star区块下的第 4 个span中文本类似“1,235,456人评价”后续需要清洗掉中文部分。电影排名则直接体现在item下的em标签里从 1 排到 25每页重新计数所以存储时要用“当前页偏移量 排名”得到真正的 Top 排名。底部那句电影简介或经典台词放在classquote的span内这一项可能为空。这里强调一句豆瓣页面的 HTML 结构并不是永恒不变的类名的调整时有发生。所以正式写代码之前先打开页面按下 F12在开发者工具里确认一下上述选择器是否依然存在。我见过不少人从旧文章里抄了代码没跑通第一反应是怀疑库装错了其实大概率是页面结构微调了。养成“先查看再匹配”的习惯比记死一套选择器重要得多。2.2 请求头与访问频率控制爬虫请求被服务器拒绝是新手最容易碰到的第一个“硬墙”。直接写一句requests.get(https://movie.douban.com/top250)大概率会得到一个 418 或者 403 状态码——这里的 418 就是从反爬机制返回来的语义是“你是机器人我不给你”。豆瓣会通过请求头中的User-Agent等字段判断访问者是不是浏览器。因此请求头至少要带上User-Agent。最省事的做法是从浏览器复制一段当前版本的信息比如headers { 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: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }我不建议一上来就把所有请求头都复制粘贴一遍。多数情况下带个User-Agent就够用保留精简的请求头还能减少被识别的概率——请求头太“满”反而容易露出马脚。比请求头更关键的是访问频率。豆瓣的反爬并不是一次性拒绝你的请求而是基于“单位时间内请求次数”做限制。初学者最容易犯的错误是写个for循环连续请求 10 页中间不休息结果第 3 页开始就被拦截。解决办法就是显式地在每页请求之间加延时。time.sleep(1)到time.sleep(3)之间都可以宁肯慢一点保证能拿到完整数据。另外如果请求返回了包含“检测到有异常请求”字样的页面说明你访问太频繁了停个几分钟再继续。这里再多提一句看到请求被拒绝时不要第一反应去查“怎么换代理”。正规的练手项目遵守目标网站的访问规则本身就是爬虫开发者的基本素养。把频率控制好加足延时这个项目根本不需要什么特殊手段。通过设置合理的请求间隔来完成数据采集才是长久之计。2.3 CSV字段设计与文本清洗想清楚“CSV 里存哪些列”其实比写代码更值得花时间。Top100 电影数据建议至少包含这几列字段含义示例rank总排名1title电影中文名肖申克的救赎rating豆瓣评分9.7comment_num评价人数2,612,358人评价quote经典台词希望让人自由。这样的字段设计既覆盖了“能证明数据有意义”的核心信息又不会因为字段过多导致解析难度陡增。电影导演、主演、类型等信息也能抓到但它们的解析逻辑更复杂通常分布在classbd区域的不同标签中加进来会让代码量翻倍。用一个练手项目先把难度控制住等你把主流程走通了再回来扩展字段也不迟。文本清洗是另一个容易被忽略的环节。直接从 HTML 里拿出来的文本常常带着多余空格、换行符甚至夹杂着页面里的标签噪声。在代码里统一加上get_text(stripTrue)只保留可见文本并去掉首尾空白是一个实用的小技巧。评价人数“2,612,358人评价”这种字符串如果后续想用来排序通常需要把非数字字符清洗掉只保留数字部分。排名数字也要注意页面里的排名是每页从 1 到 25 的局部值保存时要加上当前页偏移量才能得到 Top100 里的真实排名。清洗这块不做到位后面读数据做分析时会有各种隐性问题。比如排名列混入了换行符、评分列解析成字符串而不是小数、引用台词里的引号破坏了 CSV 的列结构——一步没做好后续成本都会放大。3. 实操过程与核心环节实现3.1 完整可运行的参考代码基于上面的分析我给出一个完整版本。这段代码是经过多次测试的“稳”版本核心逻辑很直白适合复制下来一点一点读懂也可以直接跑通看到效果。import time import csv import requests from bs4 import BeautifulSoup BASE_URL https://movie.douban.com/top250 HEADERS { 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, } def fetch_page(start): 请求当前偏移量的页面返回 HTML 文本。 params {start: start, filter: } response requests.get(BASE_URL, headersHEADERS, paramsparams, timeout10) response.encoding utf-8 response.raise_for_status() return response.text def parse_page(html, start): 从 HTML 中解析出当前页的电影信息列表。 soup BeautifulSoup(html, lxml) movies [] for item in soup.select(.item): title item.select_one(.title) rating item.select_one(.rating_num) star_spans item.select_one(.star).find_all(span) quote item.select_one(.quote span) # 清洗标题里中英文并存只保留第一个 movie_title title.get_text(stripTrue) if title else # 去重HTML 中可能把中文和英文标题合并显示 movie_title movie_title.split(/)[0].strip() movie_rating rating.get_text(stripTrue) if rating else comment_num star_spans[-1].get_text(stripTrue) if star_spans else movie_quote quote.get_text(stripTrue) if quote else rank item.select_one(em).get_text(stripTrue) # 加上偏移量转为全榜排名 real_rank start int(rank) movies.append({ rank: real_rank, title: movie_title, rating: movie_rating, comment_num: comment_num, quote: movie_quote, }) return movies def save_to_csv(movies, filenamedouban_top100.csv): 将电影数据写入 CSV 文件使用 utf-8-sig 编码防止乱码。 with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[rank, title, rating, comment_num, quote]) writer.writeheader() writer.writerows(movies) def main(): all_movies [] # 每页25条取前100条 4页 for start in range(0, 100, 25): print(f正在抓取 start{start} 的页面...) html fetch_page(start) page_movies parse_page(html, start) all_movies.extend(page_movies) # 控制访问频率避免触发反爬 time.sleep(2) save_to_csv(all_movies) print(f抓取完成共保存 {len(all_movies)} 条数据到 douban_top100.csv) print(前3条数据预览) for movie in all_movies[:3]: print(movie) if __name__ __main__: main()这段代码的大致路径是主函数循环请求 4 个页码每页解析出 25 条电影数据列表汇总后一次性写入 CSV。代码里用到了lxml作为 BeautifulSoup 的解析器如果没有安装会报错运行前记得执行pip install lxml。3.2 重要代码分步解读先看fetch_page。这里我把请求方式写成了response.encoding utf-8。为什么需要这一行因为 requests 会根据响应头去推断页面编码但豆瓣页面的响应头有时并不会显式给出charsetrequests 可能会错误地猜测成ISO-8859-1之类的编码导致后面解析时中文乱码。手动指定为 utf-8 是从源头解决问题比事后清洗要省心得多。raise_for_status()的作用是当服务器返回 4xx 或 5xx 状态码时直接抛出异常避免拿到错误页面还继续解析。再看parse_page。soup.select(.item)返回当前页所有电影条目后面每条都用select_one去找子元素。这里使用.select_one而不是find是因为 CSS 选择器的表达更紧凑。标题多行时如果get_text(stripTrue)直接拼接中文和英文名会用/分隔因此我加了split(/)[0]只保留第一个名字。这个处理方式不一定对所有页面版本通用如果输出标题异常可以把这个逻辑去掉再看看。在解析评价人数时star_spans[-1]取的是.star区块的最后一个span正常情况下内容就是“xxx人评价”。如果 HTML 结构变了导致这个元素不存在取[-1]会直接抛 IndexError。稳妥的办法是加一个长度判断。这里不做过多防御是为了保持代码好读但你心里要有数。save_to_csv里最关键的两处newline和encodingutf-8-sig。newline是为了避免在 Windows 上写入 CSV 时每两行之间多出一个空行utf-8-sig则是乱码解决方案的核心后面我会专门用一个章节详细解释。3.3 运行结果与 CSV 内容预览代码跑完后控制台会打印出类似于下面前 3 条的数据{rank: 1, title: 肖申克的救赎, rating: 9.7, comment_num: 2,612,358人评价, quote: 希望让人自由。} {rank: 2, title: 霸王别姬, rating: 9.6, comment_num: 2,118,662人评价, quote: 风华绝代。} {rank: 3, title: 阿甘正传, rating: 9.7, comment_num: 2,345,981人评价, quote: 生活就像一盒巧克力。}对应的 CSV 文件内容用 Excel 正确打开后会是这样的表格ranktitleratingcomment_numquote1肖申克的救赎9.72,612,358人评价希望让人自由。2霸王别姬9.62,118,662人评价风华绝代。3阿甘正传9.72,345,981人评价生活就像一盒巧克力。值得说明的是以上三条数据只是示意实际运行时以当时页面的实时数据为准。豆瓣评分和评价人数会随着时间变化CSV 文件的内容也会随之更新这本来就是爬虫项目的一个特点——你抓的是“那一刻”的数据。3.4 从 Top100 扩展到 Top250 的参数调整如果你跑通了 Top100再想扩展成 Top250 其实只需要改main函数里的一处循环范围# 改前只取前100条4页 for start in range(0, 100, 25): # 改后取全部250条10页 for start in range(0, 250, 25):同时把time.sleep(2)的延时稍微加长到 3 秒左右因为页面数量变多访问频率更高被抓的风险也更高。其他逻辑一概不用动。这就是把“翻页逻辑”抽象成start参数的好处改动面被控制在最小范围。如果你还想把每条电影的详情页链接也保存下来可以在parse_page里从.hd a标签的href属性取值然后加一个字段。这一步的代码改动也不大但它会让 CSV 的意义上一个台阶——拿到链接后续就能继续爬取每部电影的更多资料。4. 常见问题与排查技巧实录4.1 CSV 乱码的根源编码不一致CSV 本质上是纯文本文件而文本文件在计算机里保存的是字节序列。同一个“肖申克的救赎”这串字符用 UTF-8 编码保存的字节和用 GBK 编码保存的字节完全不同。Excel 打开 CSV 时默认按系统本地语言对应的编码去解码——在中文 Windows 上这就是 GBK准确说是 GB2312 的扩展系统代码页 936。如果你的 CSV 文件是用 UTF-8 写入的而 Excel 用 GBK 去读两者对不上屏幕上自然就是“铏蹭紕鏂囩珷”这类鬼画符。所以乱码根本不是“文件损坏了”而是“写文件用的编码”和“读文件用的编码”不一致。理解这一点之后解决方案无非两个方向要么让写入的编码与 Excel 默认读取的编码一致要么让 Excel 告诉它改用什么编码去读。下面三种方案都是围绕这两条思路展开的。4.2 方案一写入时使用 UTF-8-SIG 编码最推荐的做法是写入 CSV 时把编码从常见的utf-8改成utf-8-sig。差一个-sig后缀效果完全不同utf-8是一种无 BOM 的编码形式utf-8-sig会在文件最开头写入一个字节顺序标记BOM也就是EF BB BF这三个字节。Excel 检查到 BOM 时就能据此判断“这个文件是 UTF-8 编码”自动切换解码方式中文正常显示。在代码里就是改动save_to_csv的 open 参数# 乱码写法 with open(filename, w, newline, encodingutf-8) as f: # 不乱码的写法 with open(filename, w, newline, encodingutf-8-sig) as f:这也是我写在完整代码里的方案。说实话多数人和我一样第一次打开 CSV 看到乱码后网上搜教程会得到“用 utf-8-sig 存就好了”的答案但如果不明白 BOM 的作用下次换个场景又会懵。记住一个判断原则如果你的 CSV 文件主要是给中文 Windows 用户用 Excel 打开的优先utf-8-sig如果你的文件是给其他程序员写代码读取的可以不用 BOM但从项目兼容性角度带 BOM 才能同时照顾到两边。4.3 方案二改用 GBK 编码写入第二种思路是让写入编码直接匹配 Excel 的默认编码也就是用 GBK 写文件with open(filename, w, newline, encodinggbk) as f:这样写出来的文件Excel 双击打开默认就是正常的不需要任何额外操作。这个方法胜在“彻底不需要用户操作”但我通常不推荐作为首选原因有两点。第一GBK 是中文环境下的编码它并不包含世界上所有的字符。电影标题里如果出现类似“嘅”“瀞”等生僻汉字编码时可能会抛出UnicodeEncodeError异常导致写文件中途失败。第二这个文件拿到非中文系统或非中文版本的 Excel 上又会因为编码不匹配出现乱码兼容性更差。相比之下utf-8-sig在跨平台共用方面明显更稳妥。顺带提一句如果你用 pandas 来写 CSV同样要指定编码df.to_csv(douban_top100.csv, indexFalse, encodingutf-8-sig)同一个原理只是换了 API。4.4 方案三文件已生成后用 Excel 手工导入如果你的 CSV 文件已经写成了无 BOM 的 UTF-8或者你在别人那里收到一个乱码 CSV文件已经生成不可能再回头改代码那就直接在 Excel 里操作打开一个空白 Excel。点击“数据”选项卡 → “自文本/CSV”入口。选择目标 CSV 文件。在弹出的导入向导里把“文件原始编码”选为“65001: Unicode (UTF-8)”。确认分隔符是逗号点击加载。这样就算文件本身没有 BOMExcel 也会按 UTF-8 去读乱码自然消失。这个方法同样适用于你已经关闭的、由别人发来的 UTF-8 文件是一招“事后急救”。4.5 其他爬虫与数据过程中的常见问题除了乱码整个项目里我被人问得最多的问题还有下面几个每个都附上排查思路。第一个是请求被拒返回状态码 418 或 403。排查顺序先看请求头里有没有User-Agent再看循环里的time.sleep延时是否足够最后确认是不是短时间内跑太多次程序。如果连续触发就先歇几分钟再继续。请求头伪装到位、频率控制好正常采集 Top100 不会有什么问题。第二个是解析结果全是空列表程序不报错但 CSV 里只有表头。这种情况九成是 HTML 结构与你写选择器时不一致最简单的方法是先把html保存成文件用文本编辑器打开搜索classitem看看还在不在。不要在程序里猜直接肉眼确认数据在哪再反向改代码。第三个是 CSV 里出现莫名其妙的换行每两行数据中间有个空行。这是open时没有指定newline造成的Windows 环境下 csv 模块写入时会发生额外的换行符转换。解决方案就是在open里加上newline我已写在示例代码中。第四个是排名是每页重新从 1 开始的合并后会出现多个排名为 1、2、3 的条目。解决办法在解析时加上偏移量real_rank start int(rank)也就是示例代码里的做法。第五个是评价人数带“人评价”三个字后续想转成纯数字做排序就不可能了。解决方法是正则替换掉非数字字符比如re.sub(r\D, , comment_num)如果需要再保留单位可以组合处理。还有一个容易踩的坑用 Excel 打开 CSV 后会发现某些字段的首尾空格被自动剪掉或者带逗号的字段被强制加上引号。这些其实是 Excel 显示 CSV 的正常行为不必惊慌具体的存储格式以文本编辑器打开为准。我在实际跑这个项目的过程中最深的一个体会是爬虫代码本身不难难的是“代码写完之后数据的价值有没有被正确释放”。一次 Top100 的抓取从 URL 分析到存储编码每一步都有人在同一个坑里跌倒。如果你把这份代码跑通了乱码也解决了我更建议你顺手做两件事第一把抓下来的 CSV 丢到 pandas 里算一算 Top100 评分的均值和分布第二试着把字段扩展到电影年份和导演当你能自由地改字段、调翻页、自适应页面变化时这个入门项目就算真正吃透了。