豆瓣Top250爬虫实战:Python解析HTML到CSV存储

发布时间:2026/10/13 6:48:34
豆瓣Top250爬虫实战:Python解析HTML到CSV存储
晚上想整理一份高分观影清单翻了五页豆瓣Top250就决定写脚本——手动复制片名、评分、评价人数再粘贴进表格这活儿重复性太强不如交给爬虫。豆瓣电影Top250这个榜单可以说是最适合当练手对象的数据源之一结构规整、字段齐全、单页信息密度高而且跑完一个完整爬虫就已经把请求、解析、存储这条链路全走了一遍。这篇不是甩一段代码就完事我会把从页面分析到代码实现、再到实际跑的时候踩的坑完整过一遍。项目整体就三件事搞清URL规律、解析HTML、把250条数据写成CSV。代码用Python的requests加BeautifulSoup不需要登录不需要处理异步接口半小时能跑完。适合刚学完Python基础、想找个真实网页练手的朋友也适合想批量拿电影数据做分析的人。1. 为什么拿Top250当爬虫入门项目先想清楚要抓什么很多人学爬虫一上来就抓电商、抓社交平台结果被验证码、登录、动态加载劝退。Top250这个榜单的好处在于它不要求登录页面数据直接渲染在HTML里翻页逻辑简单到一眼能看穿。更重要的是它的数据字段相当完整一部电影的信息足够撑起一次像样的数据分析。1.1 这个榜单的数据特点先说榜单本身。Top250一共250部电影分成10页每页25条。排序规则是豆瓣自己的一套综合评分机制简单理解就是评分和评价人数共同影响排名所以它不是单纯的“评分最高”列表而是“口碑好且讨论度高”的电影集合。页面上每一行条目能看到这些信息排名序号、海报、电影名中文名和外语名、导演和主演列表、上映年份、制片国家/地区、类型、评分、评价人数以及一句电影简介。有些电影会附带“其他译名”。这些字段对于做观影推荐、评分趋势分析来说基本够用了。我一开始的规划是只抓自己需要的核心字段不贪多。贪多的坏处是解析代码变复杂页面一改就容易报错。先跑通再扩展是爬虫项目里最实用的策略。1.2 规划字段与数据表结构动手写代码前我建议先建一张表把自己要的数据列清楚。这不只是写代码的规范问题也是后续做分析的依据。我当时定的字段是这样字段含义来源位置备注rank当前排名序号标签1到250title中文片名标题标签优先取第一个title_other外语片名或其他译名标题标签可能为空url豆瓣详情页链接链接标签作为唯一标识rating评分满分10评分标签如9.7people评价人数文本星级区域含“人评价”字样quote一句话简介简介标签有些电影没有info导演、演员、年份、类型等信息段落先整体保留后续再拆分info字段我先整段存不急着拆。原因很简单导演和主演的HTML结构不统一有的演员多有的机器翻译英文名硬拆容易出错。先拿到原始文本后面用正则或者人工清理都方便。把字段设计好之后下一步就是打开网页确认这些数据在HTML里长什么样。2. 动手前先解析网页结构看清URL规律与页面组成写爬虫最忌讳的事情是拿到一段代码就复制粘贴连页面长什么样都没看过。代码跑不通是小问题跑通了但抓错字段才是真麻烦。所以这一节我不写代码先带你把目标网页的结构摸清楚。2.1 翻页URL与start参数的逻辑打开Top250第一页地址栏大概是这样的https://movie.douban.com/top250?start0filter再点下一页地址变成https://movie.douban.com/top250?start25filter规律非常明显start从0开始每翻一页加25。因为每页固定显示25部第十页就是start225。filter参数可以留着不影响结果。这意味着爬虫只需要用一个循环把start依次设为0、25、50……225就能覆盖全部250条数据。最核心的URL规律就这么多没有动态token没有加密参数简单到不需要做任何逆向。2.2 打开开发者工具观察真实请求我建议你在浏览器里按F12切到Network面板刷新页面找到当前文档的请求记录。然后看两样东西请求头和响应内容。请求头里最关键的字段是User-Agent。这是服务器识别客户端身份的重要依据。很多新手写爬虫requests.get直接裸奔服务器一看就知道不是浏览器访问轻则不返回完整数据重则直接拒绝访问。响应内容里你需要确认数据是不是直接渲染在HTML里。办法很简单在Preview或者Response标签页里搜一个电影名如果能搜到说明是服务端渲染requests拿HTML之后就能解析。如果搜不到说明数据是异步接口加载的那就得另找接口。Top250属于前者这就是我说它适合入门的原因。2.3 选择解析工具的考量解析HTML的工具有很多正则表达式、BeautifulSoup、pyquery、lxml、XPath等。我平时在项目里经常混用但给新手推荐还是requests加BeautifulSoup这一套。工具优点缺点正则表达式灵活速度最快写起来繁琐容易漏匹配维护成本高BeautifulSoup容错性强语法简单适合绝大多数页面性能一般但本项目数据量小完全够用pyquery语法接近jQuery写选择器方便中文资料相对少出错时调试成本高lxml 直接解析性能好支持XPathXPath写起来不够直观对新手不友好BeautifulSoup的select方法支持CSS选择器比如ol.grid_view li就能选出榜单里所有条目。对HTML结构稳定的页面来说这套组合非常省心。再复杂的情况比如数据是动态加载的那才需要上Selenium或者Playwright显然这里用不上。3. 完整代码实现请求、解析、保存一条龙思路理清之后写代码就是水到渠成的事。我会把完整脚本拆成几个函数每个函数负责一个环节这样后续维护和排错都轻松。代码里我会加详细注释说明每一处在做什么、为什么这么写。3.1 环境准备与依赖开始之前先确认Python环境里有三个库。requests负责发请求BeautifulSoup负责解析HTMLlxml作为解析引擎。如果还没装执行pip install requests beautifulsoup4 lxmlPython版本建议3.8以上我用的3.11没遇到任何兼容问题。不需要额外安装pandas等后面做Excel导出时再装避免一上来依赖太重。3.2 核心代码全注释版下面是我实际跑通过的脚本整体结构是构造请求头、定义请求函数、定义解析函数、定义保存函数、最后main函数控制流程。import csv import re import time import requests from bs4 import BeautifulSoup # 请求头重点是User-Agent让自己看起来像一个真实浏览器 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://movie.douban.com/top250, } BASE_URL https://movie.douban.com/top250 def clean_text(value): 去掉多余空白把各种不可见字符统一清理干净 if not value: return return re.sub(r\s, , value).strip() def fetch_page(start): 根据起始偏移量请求一页HTML url f{BASE_URL}?start{start}filter resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text def parse_page(html): 解析一页HTML提取25部电影的信息 soup BeautifulSoup(html, lxml) rows [] for li in soup.select(ol.grid_view li): row {} # 排名div classpic 里的 em em li.select_one(div.pic em) row[rank] em.get_text(stripTrue) if em else # 链接标题区域的 a 标签 href 属性 link li.select_one(div.hd a) row[url] link.get(href, ) if link else # 中文名和外语名span classtitle 可能出现两个 titles li.select(span.title) if titles: row[title] clean_text(titles[0].get_text()) else: row[title] row[title_other] clean_text(titles[1].get_text()) if len(titles) 1 else # 导演、演员、年份、类型等div classbd 里的 p info_p li.select_one(div.bd p) if info_p: # 用换行符分割第一行通常是导演/主演第二行是年份/国家/类型 lines [ clean_text(line) for line in info_p.get_text(\n, stripTrue).splitlines() if clean_text(line) ] row[info] | .join(lines) else: row[info] # 评分 rating li.select_one(span.rating_num) row[rating] rating.get_text(stripTrue) if rating else # 评价人数星级区域里最后一个 span star_spans li.select(div.star span) if star_spans: row[people] clean_text(star_spans[-1].get_text()) else: row[people] # 一句话简介 quote li.select_one(p.quote span.inq) row[quote] clean_text(quote.get_text()) if quote else rows.append(row) return rows def save_to_csv(rows, filenamedouban_top250.csv): 把解析结果写入CSV文件 fieldnames [ rank, title, title_other, url, rating, people, quote, info ] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(rows) def main(): all_rows [] for page in range(10): start page * 25 print(f正在抓取第 {page 1} 页start{start}) html fetch_page(start) rows parse_page(html) all_rows.extend(rows) # 本页数据少于20条说明可能被限流或页面结构变了需要检查 if len(rows) 20: print(f警告第 {page 1} 页只解析到 {len(rows)} 条请检查页面或请求头) # 每页请求之间停顿2秒降低对目标网站的压力也降低被封风险 time.sleep(2) save_to_csv(all_rows) print(f完成共保存 {len(all_rows)} 条数据) if __name__ __main__: main()代码里几个细节我单独说明一下。读取排名用div.pic em而不是直接用li的索引是因为HTML里每个li还包含海报等元素按索引取不如按语义取稳定。链接取div.hd a的href这个链接是每部电影的详情页地址后续做扩展会用到。title_other可能为空所以判断了titles的长度再取值。info字段我用了换行符分割再合并目的是把“导演主演”和“年份国家类型”分到两行便于后续再细化处理。比如你想单独分析年份就可以在info里按“|”拆分后再取。这里没有把年份单独拆成字段因为不同条目的格式略有差异先保留原始文本更稳妥。3.3 运行效果与数据样例脚本跑起来之后控制台会依次打印每页的抓取状态最后显示总共保存了多少条。我一次正常运行的结果是10页全部解析成功每页25条加起来正好250条总耗时大概40秒左右主要时间花在sleep等待上。生成的douban_top250.csv用Excel或者WPS打开效果类似下面这样ranktitleratingpeoplequoteinfo1肖申克的救赎9.72973377人评价希望让人自由。导演: 弗兰克·德拉邦特2霸王别姬9.62193484人评价风华绝代。导演: 陈凯歌CSV文件使用utf-8-sig编码这是很多新手容易忽略的细节。直接用utf-8写CSV在Excel里打开中文会乱码utf-8-sig带BOM头Excel能正确识别编码。反正就是多写一个后缀的事能省掉后面所有乱码烦恼。4. 实测过程踩过的坑状态码、懒加载与数据清洗代码第一次跑通不代表万事大吉。我在实际运行过程中遇到过几个问题有的属于反爬限制有的属于页面结构理解偏差还有的是数据本身格式不干净。把这些问题整理出来比代码本身更有参考价值。4.1 频繁请求触发风控的表现我第一次跑的时候请求头只设置了一个User-Agent间隔时间也没控制连续跑了三页之后开始出现HTTP 418状态码。418是“我是一个茶壶”的状态码很多网站用它来表示“我不服务你这个客户端”。豆瓣返回418基本就是识别出你不是正常浏览器访问了。排查步骤是先把请求头补全不仅要有User-Agent还要加上Accept、Accept-Language、Referer等常见字段。然后放慢请求频率每页之间至少休眠2秒。这两步做完重新跑一遍就顺利通过了。不要为了追求速度去开多线程抓Top250完全没必要。250条数据串行抓一遍也就一分钟左右多线程只会更快触发风控得不偿失。4.2 封面图懒加载与src的真实来源如果只想抓文字信息这个问题可以无视但很多人会顺手把海报一起存下来那就容易踩坑。豆瓣列表页的海报img标签src属性指向的是一张占位图真实图片地址在data-src属性里。也就是说你直接取img[src]拿到的是统一占位图而不是电影海报。正确的写法是优先取data-src取不到再用src兜底poster img.get(data-src) or img.get(src)这是因为列表页为了加载性能使用了懒加载机制屏幕外的图片不会真实加载而是先放一张占位图再通过data-src记录真实地址。爬虫不是浏览器不会自动触发加载逻辑所以必须自己处理。4.3 年份、导演、片名的文本清洗细节解析出来的文本没有想象中干净主要遇到三类情况第一HTML里的 空格符号在解析后会变成\xa0导致字符串中间出现一堆奇怪的空格。我在代码里用正则re.sub(r\s, , value)统一处理简单粗暴有效。第二有的电影没有一句简介quote字段为空。写入CSV时直接留空就好不要因为它是空值就跳过整行否则250条会变成240条。第三评价人数字段是“2973377人评价”这样的文本。如果要做数据分析最好清洗成纯数字。可以用正则提取import re people_count int(re.sub(r\D, , row[people]))清洗完再做排序、做图表都方便。这个操作我在下一节会继续用到。5. 让数据落地更有价值CSV加工与Excel导出抓到250条数据只是第一步数据落盘后的加工整理才是真正有意思的部分。这一节我会讲怎么把CSV文件做基础清洗再导出成Excel顺便做一个简单的评分分布分析。5.1 用csv模块写出规范文件上面完整脚本里已经用了csv.DictWriter写文件几个细节值得单独强调。写文件时open函数要传入newline否则在Windows平台上每两行之间会多一个空行。编码选utf-8-sig原因前面说过是为了Excel打开不乱码。如果你要保留历史快照可以给文件名加上日期。我后来更新了代码用datetime生成带日期的文件名这样每次抓取都会留下一个独立文件方便对比榜单变化from datetime import datetime filename fdouban_top250_{datetime.now():%Y%m%d}.csv5.2 用pandas导出Excel并做简单分析CSV方便程序处理但给不懂技术的人看还是Excel更友好。用pandas做转换特别快。pip install pandas openpyxl转换代码import pandas as pd df pd.read_csv(douban_top250.csv) df[people_count] df[people].str.extract(r(\d)).astype(int) df.to_excel(douban_top250.xlsx, indexFalse)简单分析也很顺手。比如看看评分最高的十部电影或者评价人数最多的十部print(df.nlargest(10, rating)[[rank, title, rating]]) print(df.nlargest(10, people_count)[[rank, title, people_count]])我当时跑出来的结果评分最高的基本都是9.7、9.6分的老片而评价人数最多的则更多是近年热门电影说明评价人数和评分排名并不完全一致。这种差异本身就是很好的分析素材。5.3 增量更新与去重策略如果只是手动跑一两次不需要考虑去重。但如果你把爬虫挂到定时任务里每周跑一次就要处理重复数据问题。以url字段作为唯一标识最可靠因为豆瓣详情页链接不会变同一条电影的url一定相同。用pandas去重很简单df df.drop_duplicates(subseturl, keeplast)keeplast表示保留最新抓到的版本。这是因为榜单偶尔会小幅变动最新记录比旧记录更准确。想要更严谨可以写入SQLite把url设为唯一索引配合时间戳做增量。不过对Top250这个体量来说CSV加去重已经足够了。6. 从Top250还能继续做什么扩展思路与合规提醒很多新手跑完这个项目就结束了觉得“会爬了”。其实Top250只是个入口顺着url字段能延伸出不少有意思的方向。这一节聊聊扩展思路也聊一下边界问题。6.1 延伸方向详情页、短评词云、评分演变我们在列表页拿到了每条电影的详情页url这意味着可以进一步爬取单部电影的信息。比如导演的完整列表、片长、上映日期、剧情简介、短评等。这些数据组合起来可以做不少分析方向按年份统计Top250里哪个年代的电影最多看口碑佳作是否存在时代集中性。按类型统计剧情片、爱情片、科幻片的占比分析榜单口味偏好。抓取每部电影的热门短评做词云或者简单的情感分析。定期抓取评分和排名记录随时间的变化观察哪些电影掉榜了哪些新上榜。我自己后来做的是把评分变化做成了一张曲线图每个月跑一次记录每部电影在Top250里的排名和评分。跑了大半年能很直观地看到口碑电影的长尾效应以及新片上映后的冲击节奏。这个概念跟“股票走势”很像只不过标的变成了电影评分挺有意思的。当然扩展方向要控制节奏每增加一个页面抓取请求量就翻倍。短评页面比列表页更容易触发风控抓取的时候间隔要放得更长或者只在白天低速跑。6.2 合规使用与频率控制爬虫技术本身是中性的但使用方式要注意边界。这里我提几个自己一直踩守的底线第一控制请求频率。个人学习用途下单台机器串行请求、每页间隔2秒以上是比较稳妥的做法。不要写并发不要开几十个线程去短时间抓取大量页面既没必要也容易给对方服务器造成压力。第二只抓公开数据不碰登录后内容。Top250列表页无需登录属于公开可访问的数据。一旦涉及个人账号信息和隐私数据性质就完全不同了。第三抓取的数据仅用于个人学习分析和非商业用途。不要打包成公开数据集传播更不要拿来建站、卖钱。如果确实需要规模化使用优先寻找官方渠道或者申请授权。说白了爬虫项目的核心是学习技术不是钻漏洞。守住这三条用起来不会有心理负担。6.3 写在最后的个人体会这个爬虫项目我从第一次写到今天前后改了很多版。最初也是复制别人的代码跑通那一刻挺兴奋但换个网站就傻眼了。后来我意识到爬虫的价值不在于“能抓到”而在于“页面变了你能快速定位问题、修改选择器、调整策略”。豆瓣Top250之所以经久不衰地出现在各种教程里就是因为它能让你用最小的成本完整经历一次数据采集的全流程。代码里学到的requests用法、HTML解析思路、数据清洗套路换到其他静态网站也能复用。等你哪天回到这里看到榜单里多了一部自己看过的新片会觉得这份数据比单纯收藏片单来得更实在。