ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python爬虫入门实战:抓取豆瓣电影Top250并做数据分析

Python爬虫入门实战:抓取豆瓣电影Top250并做数据分析 1. 项目背景与选题思路1.1 第一次作业到底该做什么这段时间有不少朋友问我编程入门第一个作业到底做什么比较好。我自己带过几个零基础的朋友他们遇到的情况几乎一模一样语法书翻了不少视频课也刷了一堆但真要交作业的时候就懵了——不知道做什么题目怕做太难完不成又怕做太简单显得敷衍。这次我们要聊的“第一次作业”我给它定了一个非常具体的方向用 Python 写一个简单的爬虫程序抓取豆瓣电影 Top250 的基础信息然后做一次简单的数据统计和排序。选这个题目有三个理由。第一爬虫能让你把“请求网页、解析内容、提取字段、保存结果”这一整条链路跑通技术点非常综合但不复杂第二豆瓣 Top250 页面结构规整数据字段完整非常适合用来练习数据定位第三抓下来的数据还能顺手做点可视化和分析整个作业的完成度会特别高拿去交差也拿得出手。这个项目对零基础的人完全友好有一点 Python 语法基础就能上手同时又有足够的扩展空间让有基础的人玩出花样。1.2 为什么选豆瓣电影Top250作为作业目标选数据源是很多新手容易忽略的一步却是整个作业里最影响体验的决策。我当时帮朋友定这个题目之前也对比过几个常见的数据源电商网站反爬太强动不动就弹出验证码新闻网站字段太乱标题、时间、来源混在一起解析起来非常痛苦政府公开数据平台倒是友好但接口文档已经劝退了一大批小白。豆瓣电影 Top250 几乎是教科书级别的练手页面。它的 HTML 结构非常稳定每个电影条目都放在类名为item的div里里面包含排名、片名、评分、评价人数、导演主演、年份、制片地区、类型等字段。顶层导航的翻页规则也很单纯start0对应第一页start25对应第二页每页固定 25 条共 10 页。这种“按指定参数递增就能翻页”的规则非常符合新手的理解方式。再加上它不是那种需要登录才能访问的接口型数据直接发一个带基本请求头的 GET 请求就能拿回完整 HTML 页面门槛低到几乎为零。当然门槛低不意味着没有坑这个后面我会专门讲那几个容易翻车的细节。1.3 技术栈选型与整体架构我最终确定的技术栈是requests负责发送网络请求BeautifulSoup4负责解析 HTMLpandas做数据整理和统计matplotlib做可视化输出。这套组合是 Python 数据采集领域最经典、最不容易出错的搭配每个库各管一件事边界清晰出了问题也容易定位。为什么不选 Selenium因为 Selenium 是模拟浏览器操作虽然能解决 90% 的动态渲染页面问题但启动浏览器、等待页面加载这些环节对新手来说不稳还会被豆瓣的反爬策略盯上。为什么不直接调 API豆瓣的公开 API 早就关闭了现在能找到的大多数接口都是非官方的调用也有各种限制。相比之下直接解析静态 HTML 是最可控、最不依赖外部条件的方案拿来交作业也最稳妥。整体的处理流程就是一个流水线发起请求拿到 HTML → 解析页面提取电影数据 → 逐页抓取直到 250 条凑齐 → 用 pandas 做清洗和统计 → 把结果输出成表格和图表。整个流程每个环节都可以独立测试出了问题可以把问题精确缩小到某一个环节而不是在乱糟糟的一大坨代码里找 bug。2. 基础准备与环境搭建2.1 Python 版本选择与虚拟环境动手之前先把环境准备好。建议直接用 Python 3.8 及以上版本我自己用的是 3.10兼容性没什么问题。如果你机器上装了多个 Python 版本建议在项目目录下单独建一个虚拟环境避免把全局环境搞乱。虚拟环境的操作非常简单在终端里进入你的项目目录执行python -m venv venv然后激活它。Windows 下是venv\Scripts\activatemacOS 和 Linux 下是source venv/bin/activate。激活之后终端前面会出现(venv)的提示符说明你已经在这个独立环境里了。这个步骤虽然看起来多此一举但实际工作中非常实用。我经常遇到的情况是某个库升级之后其他项目整个不能跑了有了虚拟环境就完全不用慌每个项目锁在自己的一套依赖里互不干扰。2.2 依赖库安装虚拟环境激活之后直接通过 pip 安装依赖pip install requests beautifulsoup4 pandas matplotlib安装过程可能会因为网络原因比较慢可以指定国内镜像源速度会快很多pip install requests beautifulsoup4 pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后可以验证一下安装结果在 Python 交互环境里执行import requests import bs4 import pandas import matplotlib print(requests.__version__, bs4.__version__, pandas.__version__, matplotlib.__version__)每个库都能正常打印出版本号就说明环境没问题。这里啰嗦一句安装库的时候不建议一次性装很多用不到的东西够用就好后面如果确实需要再补装这样能省去很多依赖冲突的麻烦。2.3 开发工具的选择开发工具我推荐用 VS Code 或者 PyCharm。VS Code 轻量、插件生态丰富配合 Python 插件使用体验很好PyCharm 社区版对做这类小项目来说也完全够用调试功能对新手更友好。如果你还在犹豫那直接选 VS Code资源占用小打开速度快不容易劝退。调试思路也要在动手之前想清楚。这个项目虽然小但它涉及网络请求天然带着不确定性最常见的情况是代码逻辑没问题但请求失败了或者页面结构和你预期不一样导致解析不到数据。所以在每个环节都要养成“打印验证”的习惯。发完请求先打印响应状态码解析完 HTML 先打印提取到的字段数量数据保存之后先打印读取结果的前几行。每一步都确认无误再进入下一步问题就能被快速隔离。3. 爬虫核心代码实现3.1 发送请求与请求头伪装第一步是发起请求。豆瓣对 User-Agent 有一定校验直接使用默认的python-requests/2.x.x请求头特别容易被拦截所以我们要伪装成一个正常的浏览器。import requests 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, } url https://movie.douban.com/top250?start0 response requests.get(url, headersheaders) print(response.status_code)如果状态码是 200说明请求成功了。如果出现 418 或者 403说明你的请求头不够“像浏览器”需要对照上面的格式检查一下。还有一个经常被忽略的细节Accept-Language这个请求头对返回内容的语言有影响不设置的话可能返回英文页面那后面解析中文电影名的时候就会对上不。另外要注意response.text是 requests 根据响应头猜测编码后得到的文本豆瓣页面用的是 UTF-8所以默认没什么问题。但为了保险建议手动指定一下编码response.encoding utf-83.2 用BeautifulSoup解析HTML并提取电影字段拿到 HTML 之后解析的核心逻辑是找到整个电影条目容器再从每个容器里提取具体字段。用 Chrome 按 F12再选中任意一部电影的标题可以在 Elements 面板里看到它的页面结构。对应的解析代码如下from bs4 import BeautifulSoup soup BeautifulSoup(response.text, html.parser) items soup.select(.grid_view .item) print(f本页共提取到 {len(items)} 条电影数据)每条item里排名在em标签中电影名称在.title中导演演员信息在.bd p的第一行评分在.rating_num中评价人数在.star span:last-child中经典台词在.inq中详情页链接在a标签的href属性中。提取字段的完整代码如下for item in items: rank item.select_one(em).get_text() title item.select_one(.title).get_text() info_p item.select_one(.bd p).get_text().strip().split(\n)[0].strip() rating item.select_one(.rating_num).get_text() people_count item.select_one(.star span:last-child).get_text() quote_tag item.select_one(.inq) quote quote_tag.get_text() if quote_tag else link item.select_one(a)[href] print(rank, title, rating, people_count, quote, link)这里有个容易踩的坑item.select_one(.inq)可能返回None因为有的电影没有经典台词这一栏。在取文本前必须先判断是否为None否则直接调用get_text()会抛AttributeError。这种“字段缺失导致崩溃”的问题在真实数据里非常常见养成加判断的习惯后面能少很多麻烦。3.3 多页抓取与代码封装单页数据抓下来之后剩下的就是循环翻页。豆瓣 Top250 的翻页规则是每一页的start参数增加 25从 0 到 225一共 10 页。实现起来很直接all_movies [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start} response requests.get(url, headersheaders) response.encoding utf-8 soup BeautifulSoup(response.text, html.parser) items soup.select(.grid_view .item) for item in items: # 同上一节的字段提取逻辑 movie { ranking: item.select_one(em).get_text(), title: item.select_one(.title).get_text(), rating: item.select_one(.rating_num).get_text(), people_count: item.select_one(.star span:last-child).get_text(), quote: item.select_one(.inq).get_text() if item.select_one(.inq) else , link: item.select_one(a)[href], } all_movies.append(movie) print(f总共抓取到 {len(all_movies)} 条数据)跑完之后如果终端输出“总共抓取到 250 条数据”那这个作业的核心任务就完成一大半了。有一点要特别提醒翻页循环里要控制请求频率每页之间最好time.sleep(1)间隔一下。这不是故意拖慢速度而是在学习阶段就要养成“不给目标网站制造压力”的习惯。爬虫写得好不好不只是看能不能抓到数据还要看请求是否克制、代码是否有容错、日志是否清晰这些都属于作业的加分项。3.4 异常处理与重试机制网络请求是天然不可靠的随时可能超时、断连、被拒。如果代码里完全不做异常处理遇到一次请求失败整个程序就直接崩溃数据收集了一大半却前功尽弃。所以我在代码里加了基本的异常捕获和重试。完整版如下import time import requests from bs4 import BeautifulSoup 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, } def fetch_page(url, retries3): for attempt in range(retries): try: response requests.get(url, headersheaders, timeout10) response.encoding utf-8 if response.status_code 200: return response.text else: print(f第 {attempt 1} 次请求失败状态码{response.status_code}) except requests.RequestException as e: print(f第 {attempt 1} 次请求异常{e}) time.sleep(2) return None all_movies [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start} html fetch_page(url) if html is None: print(f页面 {url} 抓取失败跳过) continue soup BeautifulSoup(html, html.parser) items soup.select(.grid_view .item) for item in items: movie { ranking: item.select_one(em).get_text(), title: item.select_one(.title).get_text(), rating: item.select_one(.rating_num).get_text(), people_count: item.select_one(.star span:last-child).get_text(), quote: item.select_one(.inq).get_text() if item.select_one(.inq) else , link: item.select_one(a)[href], } all_movies.append(movie) print(f已抓取 {start // 25 1}/10 页累计 {len(all_movies)} 条数据) time.sleep(1)timeout10是设置单次请求的最长等待时间防止某个请求卡住之后程序一直挂着不动。重试次数设为 3 次重试间隔 2 秒既保证了稳定性又不会在失败时无脑狂刷。这里体现的是一个很重要的工程思维程序不仅要处理“顺利的情况”更要处理“不顺利的情况”。作业里如果能体现出这个意识答辩时老师问起来你也能答得头头是道。4. 数据清洗、统计与可视化4.1 数据存储到CSV数据抓完之后下一步是存储。最方便的方式是存成 CSV 文件既可以用 Excel 直接打开也可以用 pandas 继续处理分析。import pandas as pd df pd.DataFrame(all_movies) df.to_csv(douban_top250.csv, indexFalse, encodingutf-8-sig) print(df.head())这里有一个细节非常重要编码必须用utf-8-sig。如果直接用默认的utf-8生成的 CSV 文件用 Excel 打开时中文会乱码因为 Excel 默认按 GBK 或 ANSI 编码读取文件。utf-8-sig会在文件开头写入一个 BOM 标记Excel 识别到这个标记之后就能正确以 UTF-8 解码中文显示就不会出问题。这是我第一次写爬虫存储时踩过的坑后来每次写 CSV 都会习惯性加上这个参数。4.2 用pandas做数据统计数据落到 Excel 里之后细看可能觉得信息太杂。这时用 pandas 做一些统计分析就能提炼出有价值的结论作业的档次也一下子不一样了。首先把评分和评价人数转成数值类型方便统计df[rating] df[rating].astype(float) df[people_count] df[people_count].str.replace(人评价, ).str.strip().astype(int)能看到people_count字段原来的样子是“2124541人评价”这种文本直接转整数会失败所以先做了文本清洗。接着可以统计评分的一些关键指标print(最高评分, df[rating].max()) print(最低评分, df[rating].min()) print(平均评分, round(df[rating].mean(), 2)) print(评分中位数, df[rating].median())还有一个特别有意思的统计维度评价人数和评分的关系。通常我们认为评分高、评价人数多的电影更受欢迎那么这两者之间到底有没有关联可以用排序来验证top_10_popular df.nlargest(10, people_count)[[ranking, title, rating, people_count]] print(top_10_popular)跑完之后你会发现评价人数排名靠前的电影不一定评分最高评分和热度之间存在一定的错位。这种观察就属于“超出作业基本要求”的思考是加分项。4.3 用matplotlib绘制图表文字统计看得不过瘾再画一张图出来作业的直观性就更强了。可以画“评分分布直方图”和“评价人数 Top10 条形图”。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 解决中文显示问题 plt.rcParams[axes.unicode_minus] False # 解决负号显示问题 df[rating].hist(bins20, edgecolorblack) plt.xlabel(评分) plt.ylabel(电影数量) plt.title(豆瓣电影Top250评分分布) plt.show()这里也要提醒一句matplotlib 默认字体不支持中文如果不设置simhei或Microsoft YaHei之类的字体图表里的中文全部会变成方块。如果你用的系统是 macOS字体名要换成Arial Unicode MS或者PingFang SC。这是可视化环节最常见的翻车点好在解决办法非常简单两行配置就搞定了。再画一个评价人数 Top10 的条形图top10 df.nlargest(10, people_count) plt.figure(figsize(10, 6)) plt.barh(top10[title], top10[people_count], colorskyblue) plt.xlabel(评价人数) plt.ylabel(电影名称) plt.title(豆瓣电影Top250评价人数最高的10部电影) plt.gca().invert_yaxis() plt.show()图表的意义不只是让作业看起来更“高级”更重要的是它能帮你发现数据里的隐藏信息。评价人数靠前的电影往往代表了大众热度最高的那批片子它们和评分最高榜单的重合度并不高这种对比本身就可以成为作业总结里面一个很有意思的观察点。5. 易踩的坑与排查技巧5.1 请求被拦截怎么办这是爬虫新手遇到最多的问题。明明代码没问题requests 返回的状态码却是 418 或 403。418 是网站识别出你是“机器人”后返回的搞笑状态码本质是告诉你不欢迎这次请求。解决办法主要有三个方向检查 User-Agent 是否完整且接近真实浏览器加上Accept、Accept-Language等请求头降低请求频率不要在短时间内连续猛刷。按照这个顺序逐项排查绝大多数拦截问题都能解决。如果上面这些方法都不管用还有一个后备方案给 requests 加上 Session 对象保持连接因为 Session 可以复用底层的 TCP 连接同时在多次请求之间保持相同的 Cookie整体行为更接近正常浏览器。session requests.Session() session.headers.update(headers) response session.get(url, timeout10)5.2 解析结果为空时如何定位解析结果为空是最容易让人抓狂的问题。代码写了一大堆结果打印出来的列表是空的或者某个字段全是空值。这时候第一反应应该不是怀疑代码写错了而是先把页面内容打印出来看看确认 HTML 结构和你想的到底一不一样。具体做法是print(response.text[:2000])如果返回的内容是空的说明请求就没成功问题出在上面一层的环节。如果返回内容里能看到 “请打开豆瓣App” 或者 “检测到有异常请求” 之类的文案说明已经触发了反爬。如果返回的 HTML 里能搜到.item关键词那说明解析器选错了可能你用的是html.parser但页面里有些标签没正常闭合这时候可以换成lxml解析器再试一次。5.3 字段缺失和编码乱码的处理前文提到.inq字段有缺失风险代码里已经做了if ... else None的判断。处理原则是先假设字段可能存在再去取它的内容不能默认所有字段都齐全。这种思维在做真实项目时特别重要因为真实数据远比赛博玩具干净得多。编码乱码则分为两种一种是响应文本本身的乱码解决办法是主动设置response.encoding utf-8不要依赖 requests 自动判断另一种是 CSV 文件用 Excel 打开的乱码解决办法是存文件时用utf-8-sig编码。这两类问题都在前面代码里给出了可复制的解决方案照做就行。5.4 常见问题速查表问题现象可能原因解决方法状态码 418 / 403请求头不够完整或请求频率过高补充 User-Agent增加 sleep 间隔返回内容为空请求被拦截或应对反爬机制打印响应文本定位问题检查请求头提取到的列表为空页面结构变化或 CSS 选择器写错用浏览器开发者工具核对实际页面结构.inq报错 AttributeError部分电影没有该字段取文本前先判断是否 NoneCSV 用 Excel 打开中文乱码编码问题存文件时用 utf-8-sig图表中文显示为方块matplotlib 默认字体不支持中文设置rcParams[font.sans-serif]请求卡住不返回没有设置超时时间请求时加timeout105.5 作业提交的一些经验作业做完不等于万事大吉。我建议提交前检查三样东西代码能不能从头到尾一次跑通CSV 文件是否生成且数据完整最终结果有没有简单的小结或可视化。有很多朋友在本地改了半天代码最后发现 Python 路径有问题或者依赖库没有安装到当前的虚拟环境里导致演示时当场翻车。提前把运行环境和启动命令写在 README 里既能帮助你自己复现也是作业专业度的体现。代码组织这一块我也给一个模板把请求封装成fetch_page函数把解析封装成parse_item函数把数据保存封装成save_data函数最后在main里依次调用。这种“函数模块化”的做法从一开始就养成后续做任何项目都会受益。6. 基于这个作业的扩展方向6.1 加一个数据库存储如果觉得 CSV 太简陋可以试试把数据存进 SQLite。SQLite 是 Python 内置支持的轻量级数据库不需要单独安装服务非常适合做练习。存进去之后可以用 SQL 查询做一些更复杂的过滤比如“评分大于 9 分且评价人数超过 50 万的电影有哪些”。import sqlite3 conn sqlite3.connect(douban.db) df.to_sql(movies, conn, if_existsreplace, indexFalse) result pd.read_sql_query(SELECT title, rating, people_count FROM movies WHERE rating 9.0 AND people_count 500000 ORDER BY rating DESC, conn) print(result)这个扩展方向的难点在于理解“数据库表和 DataFrame 的关系”但好处是能让你提前接触数据持久化的概念为以后做 Web 项目打基础。6.2 扩充数据维度现在抓取的是 Top250 列表页的简化信息。如果想继续深入可以进入每部电影的详情页抓取更丰富的数据比如剧情简介、导演信息、上映日期、片长、语言等。这样下来每个电影的字段从 6 个扩展到 12 个以上数据分析能做的方向就更多了比如“不同国家/地区电影的平均评分对比”、“年代与评分的关系”。注意进入详情页意味着请求量一下子从 10 页变成了 250 个页面抓取时间明显变长更需要控制频率和做好断点续传。这也是一个很实际的工程问题能亲自处理一次成长速度比看十篇教程都快。6.3 加入自动化调度如果想让这个程序自动化运行可以配上调度任务每天定时抓一次把结果按日期归档。Windows 上可以用任务计划程序macOS 和 Linux 上可以用 cron。让脚本自动跑起来之后你就能维护一个长期的数据集观察榜单的变化趋势这就已经是一个真正意义上的数据监控小项目了。我第一次带着朋友做到这一步的时候他特别感慨原来一个小小的“第一次作业”认真做下来能牵出这么多知识点。这也正是我把整个过程完整写下来的原因希望有需要的朋友能顺着这个思路把第一次作业做得比自己预想的更出色。根据我个人的经验这个项目做完之后收获最大的不是那几分而是你终于有了一套属于自己的“从零到一的项目流程感”下次再拿到任何作业或需求思路都会清晰很多。
返回列表