新手避坑指南:用Requests+BeautifulSoup爬取豆瓣电影Top250,解决反爬与数据清洗难题
2026/6/2 6:52:21 网站建设 项目流程

从零到实战:Python爬虫新手攻克豆瓣电影Top250的完整避坑手册

当你第一次尝试用Python爬取豆瓣电影Top250时,是否遇到过这些场景?明明照着教程一步步操作,却在获取页面时突然被拒绝访问;好不容易拿到数据,却发现电影时长字段里混入了各种奇怪字符;兴冲冲准备可视化时,又因为制片国家字段中的多国混排而手足无措。本文将带你完整经历一个真实项目从爬取到可视化的全流程,特别聚焦那些教程里不会告诉你的"坑"和解决方案。

1. 环境准备与基础配置

1.1 工具选择与安装

对于刚接触爬虫的新手,我建议从这些工具开始搭建开发环境:

  • Python 3.8+:这是目前最稳定的版本,避免使用最新的3.11+版本,某些库可能兼容性不佳
  • VS Code:比PyCharm更轻量,配合Python插件足够完成这个项目
  • Jupyter Notebook:特别适合数据清洗和可视化阶段的交互式调试

安装核心库时要注意版本匹配问题:

pip install requests==2.28.1 beautifulsoup4==4.11.1 pandas==1.5.3 pyecharts==1.9.1

提示:实际项目中我发现,requests 2.28.1与BeautifulSoup 4.11.1的组合在反爬处理上表现最稳定

1.2 反爬策略基础配置

豆瓣对爬虫有一定防护,新手常在这里栽跟头。我们需要配置合理的请求头:

headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Referer': 'https://movie.douban.com/', 'DNT': '1' # 禁止追踪标识 }

关键技巧:

  • 不要直接复制别人的User-Agent,自己从浏览器开发者工具获取
  • 每30分钟更换一次User-Agent字符串中的版本号
  • 控制请求频率,每页间隔3-5秒是安全范围

2. 页面抓取中的常见陷阱

2.1 动态Cookie处理实战

很多教程会告诉你直接复制浏览器的Cookie,但实际使用时发现:

  1. Cookie会在几小时后失效
  2. 不同页面的Cookie可能需要更新
  3. 频繁更换IP会导致Cookie被标记

解决方案是使用会话(Session)对象并动态维护Cookie:

session = requests.Session() def refresh_cookie(): login_url = 'https://accounts.douban.com/passport/login' session.get(login_url) # 获取初始Cookie # 模拟登录流程(此处省略具体实现) def get_page(url): try: response = session.get(url, headers=headers) if '验证' in response.text: # 触发验证码 refresh_cookie() return get_page(url) # 重试 return response.text except Exception as e: print(f"请求失败: {str(e)}") time.sleep(10) return get_page(url)

2.2 页面解析的稳定性技巧

豆瓣页面结构偶尔会有微调,导致选择器失效。这是我总结的健壮解析方案:

电影信息提取的防御式编程

def safe_extract(element, selector, default=''): try: return element.select_one(selector).get_text().strip() except AttributeError: return default # 使用示例 movie_name = safe_extract(soup, 'h1 span:first-child')

对于可能变化的页面结构,建议准备多套选择器:

rating_selectors = [ '#interest_sectl .rating_num', # 新版选择器 '.rating_wrap .rating_num', # 旧版选择器 '.star_score .rating_num' # 移动端选择器 ] for selector in rating_selectors: rating = safe_extract(soup, selector) if rating: break

3. 数据清洗的典型问题

3.1 非结构化数据处理

从豆瓣获取的原始数据往往需要大量清洗:

字段常见问题解决方案
制片国家多国混合(如"美国 / 法国")用正则r'([^/]+)'分割
上映日期多个日期用逗号分隔取第一个日期作为主要上映日期
电影时长"120分钟"带单位re.sub(r'\D', '', text)
电影类型喜剧,爱情,奇幻连在一起字符串分割后转为JSON数组

时长字段清洗实例

import re def clean_duration(duration_str): # 处理"135分钟"、"2小时15分钟"等多种格式 if '小时' in duration_str: hours = re.search(r'(\d+)小时', duration_str) mins = re.search(r'(\d+)分钟', duration_str) total = (int(hours.group(1)) * 60) + (int(mins.group(1)) if mins else 0) else: total = int(re.sub(r'\D', '', duration_str)) return total

3.2 缺失值处理策略

检查数据质量时,常见的缺失模式:

  1. 整列缺失:某些电影可能缺少时长信息
  2. 部分缺失:独立电影可能没有制片国家信息
  3. 隐藏缺失:字段值为"暂无"或"未知"

我的处理流程通常是:

  1. 先用df.info()查看各列完整性
  2. 对数值型字段用中位数填充
  3. 对文本字段用"Unknown"标记而非直接删除
  4. 记录缺失处理日志供后续分析
# 创建缺失值报告 missing_report = pd.DataFrame({ '缺失数量': df.isnull().sum(), '缺失比例': df.isnull().mean().round(4) * 100 })

4. 存储与可视化进阶技巧

4.1 数据库存储优化

直接使用pymysql可能会遇到字符集问题,更健壮的方案:

import pymysql from sqlalchemy import create_engine # 创建连接引擎 engine = create_engine( 'mysql+pymysql://user:password@localhost/movie?charset=utf8mb4', pool_size=5, max_overflow=10 ) # 批量插入数据 df.to_sql('douban_movies', engine, if_exists='append', index=False, chunksize=100) # 分批插入避免超时

注意:一定要使用utf8mb4字符集,否则存储emoji等特殊字符会失败

4.2 可视化中的特殊处理

制片国家统计的复杂情况

由于一部电影可能属于多个国家,我们需要先展开再统计:

# 展开多国家字段 countries = df['制片国家'].str.split('/').explode() # 清洗国家名称 countries = countries.str.strip().str.replace(r'[^a-zA-Z\u4e00-\u9fa5]', '') # 统计前10 top_countries = countries.value_counts().head(10)

制作交互式可视化

使用pyecharts创建带筛选功能的图表:

from pyecharts import options as opts from pyecharts.charts import Bar, Tab # 创建分页仪表盘 tab = Tab() # 评分分布 hist = ( Bar() .add_xaxis(["9分以上", "8-9分", "7-8分", "6-7分", "6分以下"]) .add_yaxis("电影数量", [ len(df[df['评分'] >= 9]), len(df[(df['评分'] >= 8) & (df['评分'] < 9)]), # 其他区间... ]) .set_global_opts(title_opts=opts.TitleOpts(title="评分分布")) ) tab.add(hist, "评分分布") # 国家统计 country_chart = ( Bar() .add_xaxis(top_countries.index.tolist()) .add_yaxis("电影数量", top_countries.values.tolist()) .reversal_axis() .set_global_opts(title_opts=opts.TitleOpts(title="制片国家统计")) ) tab.add(country_chart, "国家统计") tab.render("douban_analysis.html")

5. 项目复盘与经验总结

在完成这个项目的过程中,我踩过三个最典型的坑:

  1. IP被封问题:最初没有控制请求频率,连续请求20页后IP被暂时封禁。解决方案是加入随机延迟:

    time.sleep(random.uniform(2, 5))
  2. 数据不一致:发现某些电影的评分在HTML中的位置不同。最终采用CSS选择器优先级方案解决。

  3. 编码问题:存储到MySQL时遇到emoji字符报错。改用utf8mb4字符集后解决。

对于想进一步优化的同学,可以考虑:

  • 使用Scrapy框架实现分布式爬取
  • 添加自动验证码识别模块
  • 将数据接入Elasticsearch实现全文搜索

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

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

立即咨询