ARTICLE DETAIL

资讯详情

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

Python爬虫实战:豆瓣电影数据采集、清洗与可视化全链路解析

Python爬虫实战:豆瓣电影数据采集、清洗与可视化全链路解析 这类项目最值得先看的不是爬虫代码怎么写也不是图表库怎么用而是如何在不触发反爬、不违反网站规则的前提下稳定、高效地获取数据并把零散的数据变成能讲故事的图表。很多人一上来就猛写requests和BeautifulSoup结果要么 IP 被封要么数据乱成一团根本没法用。这篇文章会围绕“豆瓣电影”这个经典场景拆解从数据采集、清洗、存储到可视化的完整链路重点不是给你一堆代码而是告诉你每个环节的关键判断、常见坑点和生产级考量。如果你正打算用 Python 做类似的数据项目或者爬虫总是跑不稳这篇经验能帮你避开 80% 的初期问题。1. 先想清楚你要爬什么、怎么爬、爬来干嘛在动手写一行代码之前必须把目标、边界和风险理清楚。豆瓣作为一个内容社区对自动化访问有明确的限制盲目爬取不仅效率低还可能带来法律风险。1.1 明确数据目标与范围豆瓣的数据维度很多对于爬虫项目我建议先从单一、明确、有价值的目标开始。不要试图“把豆瓣爬下来”。常见且相对稳妥的目标包括电影 Top 250 榜单数据规整是学习爬虫和可视化的绝佳起点。某一特定类型如科幻、悬疑的电影列表及详情用于类型片分析。某位导演或演员的全部作品信息用于影人作品分析。某部电影下的短评前N页用于情感分析或观点挖掘需特别注意频率和伦理。关键决策深度 vs 广度你是要爬很多部电影的基础信息广度还是针对少数电影爬取其详尽的评分、短评、影评深度后者请求量更大风险更高。静态 vs 动态豆瓣大部分列表页是静态的直接requests即可。但部分内容如无限加载的评论可能需要分析接口或使用Selenium/Playwright。优先选择静态页面。频率与总量心里要有预估。爬取 250 条数据和爬取 25 万条数据在策略上天差地别。1.2 理解反爬机制与伦理规范这是爬虫项目成败的关键也是很多教程轻描淡写的地方。豆瓣常见的反爬措施请求头User-Agent检查没有合法浏览器User-Agent的请求会被直接拒绝或返回错误页面。访问频率限制短时间内来自同一 IP 的过多请求会触发验证码或直接封禁。Cookie/Session 验证某些页面如查看更多评论需要登录后的 Cookie。动态参数与签名部分 API 接口的请求参数带有加密签名增加直接调用的难度。必须遵守的伦理与规范查看robots.txt访问https://www.douban.com/robots.txt。它会告诉你网站允许或禁止爬虫访问哪些路径。虽然这不是法律但遵循它是良好的网络公民行为。限制请求速率这是最重要的实操原则。不要在循环里不加延迟地疯狂请求。为每个请求添加随机延时例如time.sleep(random.uniform(1, 3))。识别并处理验证码一旦遇到验证码应立即停止当前 IP 的爬取切换代理或等待足够长的时间。尝试自动识别验证码在豆瓣这类站点上成本高且易失效。不爬取个人隐私数据如用户邮箱、电话号码等。明确数据用途爬取的数据应用于个人学习、分析研究切勿用于商业售卖、恶意攻击或骚扰用户。注意本文所有技术和讨论均基于公开、非隐私数据的合规学习与研究目的并强调遵守目标网站的访问协议。任何数据采集行为都应以不影响目标网站正常服务为前提。2. 搭建稳健的爬虫从单页到批量的工程化实践爬虫的核心是稳定和可维护而不是代码有多炫酷。下面以“豆瓣电影 Top 250”为例构建一个健壮的爬虫。2.1 环境准备与工具选型基础环境Python 3.7 推荐 3.8 或 3.9兼容性好必要的库通过pip安装。pip install requests beautifulsoup4 pandas lxmlrequests用于发送 HTTP 请求。beautifulsoup4和lxml用于解析 HTML提取数据。lxml解析速度更快。pandas用于数据清洗和初步整理方便后续存储。可选进阶库selenium/playwright仅在页面内容由 JavaScript 动态渲染且无公开接口时使用。它们重量级、速度慢应作为最后手段。redis用作分布式任务队列或缓存在大型爬虫中很有用。sqlalchemy用于将数据方便地存入关系型数据库。2.2 单页数据抓取与解析先从一页开始确保你的数据提取逻辑是准确的。步骤 1分析页面结构用浏览器打开豆瓣电影 Top 250 页面。按 F12 打开开发者工具。使用“检查元素”功能找到电影标题、评分、评价人数、简介等信息对应的 HTML 标签和 CSS 选择器。你会发现每部电影的信息都包裹在一个div class”item”…/div中。步骤 2编写单页爬虫脚本import requests from bs4 import BeautifulSoup import time import random def scrape_one_page(url): 抓取单页电影数据 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‘ } try: # 发送请求添加随机延时模拟人工 time.sleep(random.uniform(1, 2)) resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 检查请求是否成功 resp.encoding ‘utf-8‘ except requests.RequestException as e: print(f“请求失败: {url}, 错误: {e}“) return [] soup BeautifulSoup(resp.text, ‘lxml‘) movie_items soup.find_all(‘div‘, class_‘item‘) page_data [] for item in movie_items: # 提取电影详情页链接通常包含电影ID link_tag item.find(‘a‘) movie_url link_tag[‘href‘] if link_tag else ‘‘ # 提取电影标题 title_tag item.find(‘span‘, class_‘title‘) title title_tag.get_text(stripTrue) if title_tag else ‘‘ # 提取评分 rating_tag item.find(‘span‘, class_‘rating_num‘) rating rating_tag.get_text(stripTrue) if rating_tag else ‘0.0‘ # 提取评价人数 comment_tag item.find(‘div‘, class_‘star‘).find_all(‘span‘)[-1] if item.find(‘div‘, class_‘star‘) else None comment_count comment_tag.get_text(stripTrue).replace(‘人评价‘, ‘‘) if comment_tag else ‘0‘ movie_info { ‘title‘: title, ‘rating‘: float(rating) if rating else 0.0, ‘comment_count‘: int(comment_count) if comment_count.isdigit() else 0, ‘url‘: movie_url, ‘movie_id‘: movie_url.split(‘subject/‘)[-1].replace(‘/‘, ‘‘) if ‘subject/‘ in movie_url else ‘‘ # 提取电影ID } page_data.append(movie_info) # 打印单条记录便于调试 print(f“提取到: {title}, 评分: {rating}“) return page_data # 测试第一页 if __name__ ‘__main__‘: test_url “https://movie.douban.com/top250?start0“ data scrape_one_page(test_url) print(f“本页共提取 {len(data)} 条数据。“)关键点解析请求头务必设置真实的User-Agent。异常处理使用try…except包裹网络请求避免因单次请求失败导致整个程序崩溃。延时time.sleep(random.uniform(1, 2))是核心道德与防封策略。数据提取使用BeautifulSoup的find和find_all方法结合之前分析的选择器。提取后立即进行简单的清洗如strip()、类型转换。电影ID从详情页URL中提取电影ID这是数据的唯一标识符对后续去重、关联查询至关重要。2.3 实现多页爬取与数据存储单页跑通后扩展到多页并考虑数据如何持久化。步骤 1构建分页逻辑豆瓣 Top 250 的分页规则很简单start(page_num-1)*25。def scrape_top250(base_url, max_pages10): 爬取多页数据 all_data [] for page in range(max_pages): start page * 25 url f“{base_url}?start{start}“ print(f“正在爬取第 {page1} 页: {url}“) page_data scrape_one_page(url) if not page_data: print(f“第 {page1} 页未获取到数据可能已爬完或遇到限制。“) break all_data.extend(page_data) # 每爬完一页等待稍长时间 time.sleep(random.uniform(2, 4)) return all_data步骤 2选择数据存储方式CSV 文件初学者推荐简单直观适合数据量不大几千到几万条的情况。import pandas as pd def save_to_csv(data, filename‘douban_top250.csv‘): df pd.DataFrame(data) # 去重基于电影ID df.drop_duplicates(subset[‘movie_id‘], keep‘first‘, inplaceTrue) df.to_csv(filename, indexFalse, encoding‘utf-8-sig‘) # utf-8-sig 解决Excel中文乱码 print(f“数据已保存至 {filename}, 共 {len(df)} 条记录。“)SQLite 数据库项目推荐轻量级、无需单独服务支持查询适合中型项目。import sqlite3 def save_to_sqlite(data, db_name‘douban_movies.db‘): conn sqlite3.connect(db_name) cursor conn.cursor() # 创建表 cursor.execute(‘‘‘ CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id TEXT UNIQUE, title TEXT, rating REAL, comment_count INTEGER, url TEXT ) ‘‘‘) # 插入数据忽略重复基于movie_id for movie in data: cursor.execute(‘‘‘ INSERT OR IGNORE INTO movies (movie_id, title, rating, comment_count, url) VALUES (?, ?, ?, ?, ?) ‘‘‘, (movie[‘movie_id‘], movie[‘title‘], movie[‘rating‘], movie[‘comment_count‘], movie[‘url‘])) conn.commit() conn.close() print(f“数据已存入数据库 {db_name}。“)MySQL/PostgreSQL适合团队协作、数据量极大或需要复杂查询的生产环境。MongoDB适合文档结构灵活、可能频繁变更字段的场景。步骤 3组装完整流程if __name__ ‘__main__‘: base_url “https://movie.douban.com/top250“ all_movies scrape_top250(base_url, max_pages10) # 爬10页 # 存储 save_to_csv(all_movies) # 或者 save_to_sqlite(all_movies)2.4 应对反爬与提升健壮性当你的爬虫需要长时间运行或爬取更多数据时以下策略必不可少。代理IP池当单个IP被限制后切换代理IP是继续爬取的有效手段。可以使用付费代理服务或搭建自己的代理池难度较高。在requests中使用代理proxies { ‘http‘: ‘http://your-proxy-ip:port‘, ‘https‘: ‘https://your-proxy-ip:port‘, } resp requests.get(url, headersheaders, proxiesproxies, timeout10)请求重试机制对于网络波动导致的临时失败可以自动重试。from tenacity import retry, stop_after_attempt, wait_random_exponential retry(stopstop_after_attempt(3), waitwait_random_exponential(multiplier1, max10)) def request_with_retry(url, headers): return requests.get(url, headersheaders, timeout15)需要安装pip install tenacity分布式与任务队列使用Redis配合RQ或Celery将待爬取的 URL 放入队列由多个爬虫 worker 消费。这能显著提升爬取效率并便于管理。日志记录不要只用print。使用logging模块记录信息、警告和错误便于后期排查问题。import logging logging.basicConfig(levellogging.INFO, format‘%(asctime)s - %(levelname)s - %(message)s‘) logging.info(f“开始爬取页面: {url}“)3. 从原始数据到可视化故事清洗、分析与图表呈现爬下来的数据是“原材料”可视化是“成品”。中间必须经过数据清洗和转换。3.1 数据清洗与预处理用pandas加载你存储的数据以 CSV 为例。import pandas as pd import numpy as np # 加载数据 df pd.read_csv(‘douban_top250.csv‘) # 1. 查看基本信息 print(df.info()) print(df.head()) # 2. 处理缺失值本例中数据较干净但实际项目常有缺失 # 例如如果 rating 有缺失可以用均值填充或删除 # df[‘rating‘].fillna(df[‘rating‘].mean(), inplaceTrue) # 3. 数据类型转换确保后续计算正确 df[‘comment_count‘] pd.to_numeric(df[‘comment_count‘], errors‘coerce‘) # 4. 衍生新字段为分析做准备 # 例如根据评分划分等级 def rating_level(score): if score 9.0: return ‘神作 (≥9.0)‘ elif score 8.0: return ‘优秀 (8.0-8.9)‘ elif score 7.0: return ‘良好 (7.0-7.9)‘ else: return ‘一般 (7.0)‘ df[‘rating_level‘] df[‘rating‘].apply(rating_level) # 5. 保存清洗后的数据 df.to_csv(‘douban_top250_cleaned.csv‘, indexFalse, encoding‘utf-8-sig‘)3.2 选择可视化工具与图表类型工具选型Matplotlib基础、强大、高度定制但 API 稍显繁琐。适合需要精细控制的图表。Seaborn基于 Matplotlib统计图表更美观默认样式更好看API 更友好。Plotly/Plotly Express交互式图表库可以生成网页交互图表体验非常好。Pyecharts基于百度 ECharts图表类型丰富风格符合国内审美也支持交互。Pandas 内置绘图df.plot()系列适合快速探索但定制能力有限。对于博客或报告我推荐Seaborn静态出图美观或Pyecharts交互式效果炫酷。本文示例使用Seaborn和Matplotlib。图表类型选择指南分布情况评分分布 - 直方图Histogram、密度图KDE Plot。关系对比评分与评价人数的关系 - 散点图Scatter Plot。构成比例不同评分等级的电影数量占比 - 饼图Pie Chart或环形图Donut Chart。排名展示评分最高的前10部电影 - 条形图Bar Chart。趋势分析如果数据有时间维度电影上映年份与平均评分 - 折线图Line Chart。3.3 实战可视化案例假设我们已经有了清洗后的 DataFramedf。案例 1电影评分分布直方图import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体和图表样式 plt.rcParams[‘font.sans-serif‘] [‘SimHei‘, ‘Arial Unicode MS‘, ‘DejaVu Sans‘] # 解决中文显示问题 plt.rcParams[‘axes.unicode_minus‘] False # 解决负号显示问题 sns.set_style(“whitegrid“) plt.figure(figsize(10, 6)) # 绘制直方图与密度曲线 sns.histplot(df[‘rating‘], bins20, kdeTrue, color‘skyblue‘) plt.axvline(df[‘rating‘].mean(), color‘red‘, linestyle‘--‘, linewidth2, labelf‘平均分: {df[“rating“].mean():.2f}‘) plt.title(‘豆瓣电影Top250评分分布‘, fontsize16) plt.xlabel(‘评分‘, fontsize12) plt.ylabel(‘电影数量‘, fontsize12) plt.legend() plt.tight_layout() plt.savefig(‘rating_distribution.png‘, dpi300) # 保存图片 plt.show()解读这张图能立刻看出评分是集中在 8.5 分以上还是分布较广。红色虚线标出平均分便于对比。案例 2评分与评价人数关系散点图plt.figure(figsize(12, 8)) scatter sns.scatterplot(datadf, x‘rating‘, y‘comment_count‘, hue‘rating_level‘, palette‘viridis‘, s100, alpha0.7) plt.title(‘Top250电影评分 vs 评价人数‘, fontsize16) plt.xlabel(‘评分‘, fontsize12) plt.ylabel(‘评价人数‘, fontsize12) # 添加回归线看趋势 sns.regplot(datadf, x‘rating‘, y‘comment_count‘, scatterFalse, axscatter.axes, color‘grey‘, line_kws{“linestyle“: “:“}) plt.legend(title‘评分等级‘, bbox_to_anchor(1.05, 1), loc‘upper left‘) plt.tight_layout() plt.savefig(‘rating_vs_comments.png‘, dpi300) plt.show()解读高分电影是否一定评价人数多是否存在“叫好不叫座”或“大众爆米花”电影散点图加回归线能直观展示相关性。案例 3各评分等级电影数量占比环形图# 计算各等级数量 level_counts df[‘rating_level‘].value_counts() plt.figure(figsize(8, 8)) colors sns.color_palette(‘pastel‘)[0:len(level_counts)] wedges, texts, autotexts plt.pie(level_counts.values, labelslevel_counts.index, autopct‘%1.1f%%‘, colorscolors, startangle90, wedgepropsdict(width0.3)) # width控制环形粗细 # 美化文字 plt.setp(autotexts, size10, weight“bold“, color“black“) plt.setp(texts, size12) plt.title(‘Top250电影评分等级分布‘, fontsize16) plt.savefig(‘rating_level_donut.png‘, dpi300) plt.show()解读环形图比饼图更美观一眼看出“优秀”和“神作”级别电影占据了绝大部分。案例 4评分最高的前15部电影条形图top_n 15 top_movies df.nlargest(top_n, ‘rating‘)[[‘title‘, ‘rating‘, ‘comment_count‘]] top_movies top_movies.sort_values(‘rating‘, ascendingTrue) # 排序以便条形图从上到下显示 plt.figure(figsize(12, 10)) bars plt.barh(top_movies[‘title‘], top_movies[‘rating‘], colorsns.color_palette(“coolwarm“, top_n)) plt.xlabel(‘评分‘, fontsize12) plt.title(f‘豆瓣Top250中评分最高的前{top_n}部电影‘, fontsize16) # 在条形末端添加评分数值 for bar, rating in zip(bars, top_movies[‘rating‘]): plt.text(bar.get_width() 0.01, bar.get_y() bar.get_height()/2, f‘{rating:.1f}‘, va‘center‘, ha‘left‘, fontsize10) plt.tight_layout() plt.savefig(‘top_rated_movies.png‘, dpi300) plt.show()解读水平条形图适合展示排名特别是名称较长时。将评分数值标在条末信息更清晰。4. 项目进阶、常见问题与排查清单一个完整的爬虫可视化项目除了核心功能还需要考虑工程化和后期维护。4.1 从脚本到项目代码组织与配置管理不要把所有代码写在一个.py文件里。建议按模块拆分douban_project/ ├── config.py # 配置文件URL模板、请求头、数据库连接字符串等 ├── spider/ │ ├── __init__.py │ ├── crawler.py # 核心爬虫类/函数 │ ├── parser.py # 页面解析函数 │ └── scheduler.py # 任务调度与URL管理 ├── storage/ │ ├── __init__.py │ ├── csv_saver.py │ └── db_saver.py ├── analysis/ │ ├── __init__.py │ ├── cleaner.py # 数据清洗 │ └── visualizer.py # 可视化绘图 ├── utils/ │ ├── __init__.py │ ├── logger.py # 日志配置 │ └── proxy_pool.py # 代理工具 ├── main.py # 主程序入口 └── requirements.txt # 项目依赖使用配置文件管理易变参数如config.py# config.py DOUBAN_TOP250_URL “https://movie.douban.com/top250?start{start}“ HEADERS { ‘User-Agent‘: ‘Mozilla/5.0 ...‘ } REQUEST_DELAY (1, 3) # 随机延时范围 DB_PATH ‘data/douban_movies.db‘4.2 常见问题与排查顺序当爬虫不工作时按以下顺序排查现象请求返回 403 或 418 错误。排查首先检查User-Agent是否设置且有效。然后检查请求频率是否过高立即增加延时。最后考虑是否触发了更复杂的反爬如需要 Cookie尝试使用requests.Session()或手动添加简单 Cookie。现象能收到响应但解析不到数据soup.find返回空列表。排查首先打印resp.text的前几百字符看返回的是否是预期 HTML还是反爬提示如“检测到异常请求”。如果是反爬提示回到步骤1。如果是 HTML用浏览器开发者工具再次确认标签和选择器是否准确网站结构可能已更新。现象程序运行缓慢或卡住。排查检查网络连接。检查是否在循环内进行了同步的、耗时的 I/O 操作如频繁写入文件。考虑使用异步库如aiohttp提升效率或优化存储逻辑批量写入。现象数据库写入失败或数据重复。排查检查数据库连接字符串和表结构。检查INSERT语句的字段数与值的数量是否匹配。确保在插入前已根据唯一键如movie_id进行去重。现象可视化图表中文显示为方框。排查确保系统安装了中文字体并在 matplotlib 中正确设置字体路径。使用本文示例中的plt.rcParams设置是一种通用方法。4.3 进阶方向与扩展思考增量爬虫不是每次都全量爬取。记录已爬取的movie_id每次只爬取新电影。这需要设计一个记录爬取状态的机制。爬取更多字段导演、演员、类型、上映日期、片长、简介等。这需要深入到每部电影的详情页请求量会剧增务必控制好速率。情感分析如果爬取了短评可以使用snownlp或jieba 情感词典进行简单的情感分析可视化正面/负面评价比例。构建仪表盘使用Flask或Streamlit框架将爬虫和可视化整合成一个 Web 应用实现数据自动更新和图表交互。定时任务使用APScheduler或操作系统级的cron让爬虫定期自动运行更新数据。最后也是最关键的一点爬虫项目的价值最终体现在对数据的洞察上而不是爬取技术本身。在动手之前多花时间思考你想通过数据回答什么问题。是“高评分电影有哪些共同特点”还是“评价人数和评分有什么关系”。带着问题去爬取和分析你的项目才会更有深度可视化也才更有灵魂。先跑通最小闭环再逐步增加复杂度和稳定性这才是可持续的实践路径。
返回列表