☰
Python爬虫实战:TOP250电影数据采集与可视化分析
2026/10/20 22:16:53 网站建设 项目流程

简介:面向Python学习者与毕设学生的豆瓣TOP250电影爬虫与数据分析可视化项目,覆盖爬虫采集、数据清洗、数据库建表、可视化出图完整流程,适合作为课程设计、期末大作业或毕业设计的参考源码。资源共11个文件,包含3个Python程序文件,分别负责爬取入库、清洗处理和可视化生成;2个SQL脚本用于创建电影与评论明细数据库;同时提供清洗后数据Excel、可视化HTML报告、爬虫文档报告docx及讲解PPT,整包仅2.32MB,结构紧凑,便于下载后快速查阅与按需复用。目前已有571人学习使用,代码注释清晰,项目说明与PPT讲解能够帮助初学者快速理解从网络爬虫到数据分析可视化的各个环节。下载运行后即可看到豆瓣TOP250电影的排行、评分分布等可视化结果,适合具备基础编程能力并希望积累完整项目经验的学习者参考二次开发。

1. 把爬虫、数据分析可视化一次串完:这份TOP250资源适合谁

刚接触Python爬虫的开发者,最容易遇到的情况是:跟教程敲完了requests和BeautifulSoup,抓下来几十条数据,却不知道下一步能做什么。而真正要交毕业设计或课程设计时,光有爬虫又不够,还得有数据分析、可视化图表、一份能讲清楚的说明文档。这份基于某评分平台TOP250电影榜单的爬虫与数据分析可视化项目,恰好把整条链路补齐了:源码负责抓数据,pandas负责做统计,matplotlib和词云负责出图,说明文档和PPT负责把过程讲明白。适合正在找Python项目实例、需要完整源码做课设或毕设的开发者,也适合想看看“爬虫之后还能干什么”的初学者直接复现。

我把这个项目从下载到跑通的过程拆了一遍,包括页面字段怎么选、反爬怎么绕、哪些地方最容易翻车,以及最后怎么把它改造成一个可以反复使用的工具。下面按我实际的执行顺序来写。

2. 先摸清数据源:TOP250页面的结构、字段和反爬边界

2.1 榜单页与详情页的关系:字段从哪里取

TOP250榜单页面上,一部电影通常包含这些信息:片名、年份、地区、导演、主演、评分、评价人数、一句话短评。这8个字段基本覆盖了后续数据分析的所有维度,所以不需要再去逐一点开250个详情页,只抓榜单页就能满足需求。

我一般先把榜单页的HTML结构打印出来,确认每个字段所在标签的class属性。常见的做法是选中一个电影条目,观察它的<div class="item">结构,然后逐个定位。这个项目的爬虫源码里,用的就是基于item条目和info信息块的选择器写法。有一个细节要注意:同一个页面上“评分”和“评价人数”都在star这个class里,需要用span的序号来区分。选出第1个span是星星,第2个是评分,第4个是评价人数。这种细节最好先确认,代码写起来才不会有歧义。

数据字段的选型也要同步考虑后续分析。比如“年份”后面要做分布统计,就应该存纯数字(如2009),而不是“2009年”这种带单位字符串;地区字段会出现“中国大陆 / 香港”这种多值情况,落到CSV里后清洗时必须单独处理。项目里已经把这些字段存成了独立的列,省掉了重新拆分的麻烦。

2.2 反爬机制:UA、Cookie、限速和重试

TOP250的页面对爬虫不算特别严格,但设置得太粗糙照样会被拦。第一次尝试时我用默认的Python UA直接请求,很快返回了418状态码。之后把请求头补全,问题就消失了。常见做法是同时带上User-Agent、Accept、Accept-Language这几个请求头,其中UA建议填成浏览器的版本,而不是Python的默认值。

限速是另一个必须做的动作。榜单总共有10页,每页25条数据,如果3秒内连续请求完,即使不被封也会触发临时限制。项目源码里设置了一个随机延时,每次请求后time.sleep(random.uniform(1, 3)),这个1到3秒的随机区间在实际测试中效果不错。再配合重试机制:如果请求失败,间隔2秒后重试,最多重试3次。

我把反爬相关的参数都集中放在脚本开头的配置区,方便调整:

# config 区域 BASE_URL = "https://目标站点/top250?start={}&filter=" # 分页规律是每页25条 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } DELAY_RANGE = (1, 3) # 随机延时区间,单位秒 MAX_RETRIES = 3 # 单页最大重试次数 TIMEOUT = 10 # 请求超时时间

这里的start={}对应分页参数,第1页是0,第2页是25,依此类推。延时设置成随机区间而不是固定值,是为了避免形成固定频率的访问节奏,被识别成脚本特征。超时时间给到10秒,是为了在网络波动时不至于因为等待过久拖慢整个抓取流程。

2.3 数据落盘:CSV是最稳妥的中间格式

抓取完成后的数据,我建议先落到CSV而不是直接塞进数据库。原因有三个:一是CSV方便用Excel或WPS直接打开检查,中途出了问题很容易定位;二是后面做pandas分析时,read_csv一步就能加载;三是这个项目的可视化部分不依赖数据库,CSV足够支撑。

源码里落盘时用了一个容易踩坑的细节——编码。如果你用encoding='utf-8'写入CSV,Excel打开时大概率会乱码。项目里用的是utf-8-sig,这个格式在文件开头加了BOM标记,Excel能正确识别成UTF-8。数据库和统计分析读取时,也统一按utf-8-sig处理。如果你准备把这个CSV导入MySQL之类的数据库,注意先转成不带BOM的UTF-8,否则首字段名会带上不可见字符。

3. 爬虫代码落地:从请求到CSV的完整实现

3.1 请求模块:带重试的通用get函数

爬虫的骨架是请求函数,这个项目里没有把请求逻辑散落在各处,而是封装成了一个通用函数。这样单页请求失败时,重试逻辑只写一遍,代码结构也更干净。

import requests import time import random def fetch_page(url, headers=HEADERS, retries=MAX_RETRIES): for attempt in range(retries): try: resp = requests.get(url, headers=headers, timeout=TIMEOUT) if resp.status_code == 200: resp.encoding = resp.apparent_encoding # 动态识别页面编码 return resp.text else: print(f"第{attempt+1}次请求返回状态码: {resp.status_code}") except requests.RequestException as e: print(f"第{attempt+1}次请求异常: {e}") time.sleep(DELAY_RANGE[0] + random.random() * (DELAY_RANGE[1]-DELAY_RANGE[0])) return None

resp.encoding = resp.apparent_encoding这行很关键。目标页面返回的响应头里写的可能是老旧的字符集,但页面实际内容是UTF-8。用apparent_encoding让requests去探测真实编码,能避免后续解析时出现乱码。重试逻辑里,每次失败都会走随机延时,不会出现失败后立即猛冲的情况。

3.2 解析模块:BeautifulSoup提取8个字段

解析逻辑是爬虫的核心。用BeautifulSoup定位条目,然后对每个字段做提取和清洗。这里我按项目源码的写法,用select_one精准定位:

from bs4 import BeautifulSoup import csv def parse_page(html_text): if html_text is None: return [] soup = BeautifulSoup(html_text, "lxml") items = soup.select("div.item") # 每个电影条目 rows = [] for it in items: title = it.select_one("span.title").get_text(strip=True) info_lines = it.select_one("div.bd > p").get_text("\n", strip=True).split("\n") year = info_lines[0].split("/")[0].strip() # 年份在导演信息段的首位 rating = it.select_one("span.rating_num").get_text(strip=True) people_tag = it.select("div.star span")[3] # 第4个span是评价人数 people = people_tag.get_text(strip=True) quote_tag = it.select_one("p.quote") quote = quote_tag.get_text(strip=True) if quote_tag else "" rows.append([title, year, rating, people, quote]) return rows

字段提取时有两个地方容易出错:一是年份并不是单独放在一个标签里,而是混合在“导演、主演、年份、地区”的整段文字里,必须先按/拆分再取首段;二是quote(一句话短评)并不是每部电影都有,用if quote_tag else ""做兜底,避免None值导致写入CSV时报错。

解析完一页后,把数据追加到CSV里,边抓边存。这比全部抓完再一次性写入要稳得多,中途挂了还能保留已经抓到的数据。

3.3 主流程:翻页循环与断点续抓

主流程的核心是按分页参数循环抓取10页,每页25条。这里需要一个计数器来控制页数,同时判断当前是否已经有数据落盘,避免重复运行脚本时抓出250条以上的重复数据。

def main(): csv_path = "data/movie_top250.csv" seen_titles = set() # 如果CSV已存在,先读取已有片名,做到断点续抓 if os.path.exists(csv_path): with open(csv_path, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: seen_titles.add(row["title"]) with open(csv_path, "a", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) if not seen_titles: # 首次新建时写入表头 writer.writerow(["title", "year", "rating", "people", "quote"]) for page in range(10): url = BASE_URL.format(page * 25) html = fetch_page(url) rows = parse_page(html) for r in rows: if r[0] in seen_titles: continue writer.writerow(r) seen_titles.add(r[0]) print(f"第{page+1}页完成,当前累计 {len(seen_titles)} 条") time.sleep(random.uniform(DELAY_RANGE[0], DELAY_RANGE[1])) if __name__ == "__main__": main()

这个循环里有两个关键设计。第一,用seen_titles集合记录已抓片名,脚本重跑时不会产生重复数据;第二,用追加模式写入,而不是覆盖模式,配合断点集合实现“挂了接着跑”的效果。每次写入后也做了延时,而不是只在一页开始前延时一次,这样节奏更接近人工浏览。

4. 数据分析与可视化:pandas清洗加matplotlib出图

4.1 pandas清洗:处理年份、地区和多值字段

爬下来的CSV里,年份字段基本干净,但地区字段可能出现“中国大陆 / 香港”这种多值情况。做分析前先做一步拆分。常见做法是只取第一个地区作为主分类,后面做饼图时不会因为类别太多而显得拥挤。

import pandas as pd df = pd.read_csv("data/movie_top250.csv", encoding="utf-8-sig") df["year"] = pd.to_numeric(df["year"], errors="coerce") # 强制转数值 df["main_area"] = df["area"].str.split("/").str[0].str.strip() # 取首个地区

errors="coerce"的作用是,遇到无法解析成数字的年份时置为NaN,不会让整列崩溃。拆分地区时用str.split("/")再取第0个元素,能把“中国大陆 / 香港”简化为“中国大陆”。

处理后要顺手看一遍数据质量:检查是否有空值、是否有年份为0的记录、评分是否都在0到10之间。这些检查用df.info()和df.describe()就能做到。项目里的说明文档专门提醒过这一步,因为后续所有图表都建立在这个清洗结果之上,如果清洗时漏了脏数据,可视化阶段会看到各种离谱的突刺。

4.2 评分分布、年份趋势与地区占比三张图

拿到干净数据后,我做了三个维度的可视化,这三个图基本就是PPT里的核心内容。

第一个是评分分布直方图,直接把250部电影的评分切成10个区间,看集中在哪个分数段:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 解决中文显示问题 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 df["rating"].plot(kind="hist", bins=10, edgecolor="white") plt.xlabel("评分") plt.ylabel("电影数量") plt.title("TOP250电影评分分布") plt.savefig("output/rating_dist.png", dpi=200, bbox_inches="tight")

bins=10把评分区间切成10份,每份是1分。保存图片时dpi=200保证PPT里放大不糊,bbox_inches="tight"会自动裁掉多余的留白。

第二个是年份趋势折线图,统计每年上榜的电影数量:

year_count = df["year"].value_counts().sort_index() year_count.plot(kind="line", marker="o") plt.xlabel("年份") plt.ylabel("上榜数量") plt.title("TOP250电影年份分布趋势") plt.savefig("output/year_trend.png", dpi=200, bbox_inches="tight")

从这张图能明显看出哪些十年是经典电影的高产期。这里要注意,折线图适合看趋势,但年份跨度大时数据点会很密,建议在plot里加marker="o"让数据点可见。

第三个是地区占比饼图:

area_count = df["main_area"].value_counts().head(8) area_count.plot(kind="pie", autopct="%.1f%%") plt.ylabel("") plt.title("TOP250电影主要地区占比") plt.savefig("output/area_pie.png", dpi=200, bbox_inches="tight")

head(8)限制只显示前8个主要地区,其他归为“其他”,饼图不会出现几十个挤在一起的扇区。autopct="%.1f%%"控制显示一位小数的百分比,否则占比数字会挤成一团。

4.3 词云:从一句话短评中提取关键词

这个项目比普通爬虫项目进阶的地方,是还加了一个词云图。数据源不是片名,而是每部电影的一句话短评。先做中文分词,再生成词云:

import jieba from wordcloud import WordCloud comments = " ".join(df["quote"].dropna().tolist()) seg_list = jieba.cut(comments) seg_text = " ".join(seg_list) wc = WordCloud( font_path="fonts/simhei.ttf", # 中文字体路径,不给会出方块 width=800, height=600, background_color="white", max_words=150, ).generate(seg_text) wc.to_file("output/wordcloud.png")

font_path必须指定一个支持中文的字体文件,否则生成的词云图全是方块。max_words=150控制词云中显示的词数量,调大了会出现太多低频词,调小了会丢掉有意义的关键词,150是我试过几次后比较合适的值。dropna()是去掉没有短评的电影,直接拼接会出现None导致分词报错。

5. 避坑汇总:编码乱码、字段漏抓、频繁请求封禁的翻车现场

5.1 用Excel打开CSV全乱码:编码选错没有后悔药

现象:抓取正常,代码不报错,但用Excel打开生成的CSV文件,中文全部变成乱码。

原因:写入CSV时用了encoding="utf-8"。Excel对于没有BOM标记的UTF-8文件,会默认按本地编码(如GBK)解析,中文字符自然显示不对。

解决:写入时改用encoding="utf-8-sig"。这个编码会在文件开头写入一个不可见的BOM头,Excel识别到后就会按UTF-8处理。遇到之前已经生成的乱码CSV,不用重跑爬虫,用文本编辑器另存为UTF-8 with BOM格式即可。

5.2 评分和评价人数全是空值:标签层级搞反了

现象:电影标题都正常抓到,年份也正常,但评分和评价人数两列全是空。

原因:用了select去查找span,如果CSS选择器定位到了别的同名class,或者选择的元素比预期早了一级,找出来的就不是目标节点。特别是在div.star下,有多个span同时存在,下标没对上就会取到空值。

解决:先打印选中元素的get_text()确认内容,再决定下标。评分字段直接select_one("span.rating_num"),评价人数取select("div.star span")[3]。如果打印后发现下标对不上,用len()输出span的个数,逐个核对,不要靠猜。

5.3 抓了几页后请求返回403:请求太快触发临时限制

现象:前3页正常,第4页开始连续返回403或418,脚本报错退出。

原因:请求频率超过了页面服务的容忍阈值。虽然设置了延时,但延时区间是1到3秒,实际打印日志发现经常在1秒出头就发出下一次请求,整体节奏偏快。

解决:把延时区间放大到2到5秒,同时配合重试机制。我在这个项目里把DELAY_RANGE调整成(2, 3)之后,完整跑完10页没有触发过一次拦截。如果你需要频繁重跑,建议每页延时后再加上一个随机数抖动,进一步打乱访问节奏。

5.4 年份字段混进了导演名字:split切分时位置错位

现象:年份列里有部分行显示的不是“2009”这种数字,而是导演或主演的名字。

原因:榜单页“导演 / 主演 / 年份 / 地区”是整段文字放在同一个<p>标签里的。按/拆分后,年份可能不在第0位,因为有的条目没有导演信息或主演信息缺失,导致数组错位。

解决:不要用固定下标,改为在拆分后的列表中逐个匹配正则表达式,找到形如“2009”或“2008”的项作为年份,找到包含“中国大陆”“美国”等关键字的项作为地区。正则写法:re.search(r"(\d{4})", part)。这样即使字段顺序变化,也能准确提取。

5.5 词云图输出全黑方块:中文字体路径缺失

现象:代码不报错,但生成的词云图里所有文字都是黑色方块。

原因:WordCloud默认字体不支持中文字符,没有显式指定中文字体路径时,渲染出来的就是方框。

解决:从系统字体目录里找一个中文字体文件,比如simhei.ttf或msyh.ttf,复制到项目的fonts目录,并把font_path指向它。如果用的是服务器环境,先确认字体文件存在,不要直接给一个不存在的路径。

还有一个通用排查技巧:每次代码改动后,先只抓1页数据验证,再跑完整流程。10页全量抓取遇到问题再回头排查时,耗时是单页验证的10倍。

6. 进阶技巧:把一次性爬虫改造成可复用的工具包

这个项目跑通之后,我建议你做一件让代码价值翻倍的事:把脚本参数化,让它不再只是“TOP250专用爬虫”,而是能切换目标榜单、控制抓取范围、自动生成图表的工具包。

我在改造时加了一个config.py,把URL规则、字段映射、页面数量、延时区间都抽成了配置项。命令行入口用argparse支持三个参数:--pages控制抓取页数,--output指定CSV路径,--skip-viz跳过可视化直接只爬数据。这样同一个工具既能全量抓取250部电影,也能只抓前2页做快速验证。可视化部分也用脚本组合了,跑完爬虫后自动执行数据分析和出图,三个模块通过main.py串联。

# main.py 入口 python main.py --pages 10 --output data/top250.csv --with-viz

如果你的环境缺少依赖,项目说明文档里已经列出了requirements.txt的内容:requests、beautifulsoup4、lxml、pandas、matplotlib、jieba、wordcloud。用pip install -r requirements.txt安装后就能直接跑。

最后说一个我自己的习惯。每次拿到别人的爬虫项目,我不会直接跑完整代码,而是先看三样东西:请求头配置、解析函数里的字段下标、落盘时的编码。这三处是爬虫项目最容易“按教程写却跑不通”的位置。这个项目里三处都处理得比较规范,复现过程基本顺畅。从那以后,我每次交付给别人的爬虫项目,都强制自己走一遍“单页验证、限速检查、编码确认”这三个步骤,既是对代码负责,也是给接手的人少留一点坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询