豆瓣影评数据分析实战:从爬取到SQLite存储的全流程解析

电影评论数据蕴含着丰富的观众情感和市场反馈信息,对于影视从业者、数据分析爱好者而言都是极具价值的原始素材。本文将手把手带你构建一个完整的豆瓣影评数据处理流程,涵盖网页爬取、数据清洗到结构化存储的全套技术方案。

1. 环境准备与基础工具链搭建

在开始爬取数据之前,需要配置好Python开发环境并安装必要的工具库。推荐使用Python 3.8+版本,它能提供更好的异步支持和类型提示功能。

核心依赖库安装

pip install beautifulsoup4 requests html5lib sqlite3

这些库各司其职:

  • beautifulsoup4:HTML文档解析神器
  • requests:人性化的HTTP请求库
  • html5lib:容错性强的HTML解析器
  • sqlite3:Python内置的轻量级数据库引擎

建议使用虚拟环境隔离项目依赖,避免不同项目间的库版本冲突。创建虚拟环境的命令:

python -m venv douban_venv
source douban_venv/bin/activate  # Linux/Mac
douban_venv\Scripts\activate  # Windows

2. 豆瓣影评页面结构分析与爬取策略

豆瓣电影评论页面采用分页加载机制,每页默认展示20条评论。通过分析URL规律可以发现,分页参数通过start查询字符串控制:

https://movie.douban.com/subject/30128916/reviews?start=20

关键爬取策略

  1. 模拟浏览器请求头,降低被封禁风险
  2. 实现自动翻页逻辑,遍历所有评论页面
  3. 控制请求频率,建议间隔2-3秒
  4. 处理反爬机制,实现异常重试

以下是一个健壮的请求函数实现:

import requests
import time
from fake_useragent import UserAgent

def fetch_html(url, retry=3):
    headers = {
        'User-Agent': UserAgent().random,
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }
    for attempt in range(retry):
        try:
            resp = requests.get(url, headers=headers, timeout=10)
            resp.raise_for_status()
            return resp.text
        except Exception as e:
            print(f"请求失败({attempt+1}/{retry}): {str(e)}")
            time.sleep(2 ** attempt)  # 指数退避
    return None

3. 数据解析与清洗技术详解

获取HTML源码后,需要从中提取结构化数据。豆瓣影评页面包含多种信息元素:

  • 评论标题
  • 评论正文
  • 评分信息
  • 有用数统计
  • 发布时间

使用BeautifulSoup解析的关键代码

from bs4 import BeautifulSoup

def parse_reviews(html):
    soup = BeautifulSoup(html, 'html5lib')
    reviews = []
    
    for item in soup.select('.review-item'):
        try:
            title = item.select_one('h3 > a').get_text(strip=True)
            content = item.select_one('.review-content').get_text(' ', strip=True)
            rating = item.select_one('.main-title-rating')['title'] if item.select_one('.main-title-rating') else None
            useful_count = int(item.select_one('.action-count').get_text(strip=True).split()[0])
            pub_date = item.select_one('.main-meta').get_text(strip=True)
            
            reviews.append({
                'title': title,
                'content': content,
                'rating': rating,
                'useful_count': useful_count,
                'pub_date': pub_date
            })
        except Exception as e:
            print(f"解析异常: {str(e)}")
            continue
            
    return reviews

数据清洗注意事项

  • 处理HTML实体编码(如 )
  • 统一日期时间格式
  • 过滤广告和无效评论
  • 处理emoji等特殊字符

4. SQLite数据库设计与高效存储方案

SQLite作为轻量级关系型数据库,非常适合个人项目和小型数据存储。我们需要设计合理的表结构来存储影评数据。

数据库表结构设计

字段名 类型 说明
id INTEGER 主键,自增
movie_id TEXT 电影唯一标识
title TEXT 评论标题
content TEXT 评论正文
rating TEXT 星级评价
useful_count INTEGER 有用数
pub_date TEXT 发布时间
crawl_time TEXT 爬取时间

数据库操作封装类

import sqlite3
from datetime import datetime

class DoubanDB:
    def __init__(self, db_path='douban_reviews.db'):
        self.conn = sqlite3.connect(db_path)
        self._create_table()
        
    def _create_table(self):
        sql = """
        CREATE TABLE IF NOT EXISTS reviews (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            movie_id TEXT NOT NULL,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            rating TEXT,
            useful_count INTEGER DEFAULT 0,
            pub_date TEXT,
            crawl_time TEXT,
            UNIQUE(movie_id, title) ON CONFLICT IGNORE
        )
        """
        self.conn.execute(sql)
        self.conn.commit()
        
    def insert_review(self, movie_id, review):
        sql = """
        INSERT INTO reviews 
        (movie_id, title, content, rating, useful_count, pub_date, crawl_time)
        VALUES (?, ?, ?, ?, ?, ?, ?)
        """
        crawl_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        params = (
            movie_id,
            review['title'],
            review['content'],
            review['rating'],
            review['useful_count'],
            review['pub_date'],
            crawl_time
        )
        self.conn.execute(sql, params)
        self.conn.commit()
        
    def close(self):
        self.conn.close()

性能优化技巧

  • 使用事务批量插入
  • 建立合适索引
  • 定期执行VACUUM优化

5. 完整爬虫系统实现与异常处理

将各个模块组合起来,构建完整的爬虫系统。以下是主控程序的实现框架:

import time
from urllib.parse import urljoin

class DoubanSpider:
    def __init__(self, movie_id):
        self.movie_id = movie_id
        self.base_url = f"https://movie.douban.com/subject/{movie_id}/reviews"
        self.db = DoubanDB()
        
    def crawl(self, max_pages=50):
        page = 0
        while page < max_pages:
            url = f"{self.base_url}?start={page*20}"
            print(f"正在爬取: {url}")
            
            html = fetch_html(url)
            if not html:
                print(f"获取页面失败: {url}")
                break
                
            reviews = parse_reviews(html)
            if not reviews:
                print("未解析到评论数据,可能已达末页")
                break
                
            for review in reviews:
                self.db.insert_review(self.movie_id, review)
                
            page += 1
            time.sleep(3)  # 遵守爬虫礼仪
            
    def close(self):
        self.db.close()

# 使用示例
if __name__ == '__main__':
    spider = DoubanSpider('30128916')  # 电影《八佰》ID
    try:
        spider.crawl(max_pages=10)
    finally:
        spider.close()

关键异常处理点

  1. 网络请求超时重试
  2. 页面解析容错
  3. 数据库写入冲突处理
  4. 反爬识别与规避

6. 数据质量检查与后续分析准备

存储到数据库后,需要进行数据质量检查,确保后续分析的可靠性。

常见数据问题检查清单

  • 重复评论检测
  • 缺失值处理
  • 异常值识别
  • 文本编码统一

使用SQL查询进行基础检查:

-- 检查数据完整性
SELECT COUNT(*) AS total, 
       COUNT(DISTINCT title) AS unique_titles,
       COUNT(rating) AS rated_count
FROM reviews;

-- 查看评分分布
SELECT rating, COUNT(*) AS count
FROM reviews
WHERE rating IS NOT NULL
GROUP BY rating
ORDER BY count DESC;

对于文本数据,建议进行以下预处理:

  1. 去除HTML残留标签
  2. 统一全角/半角字符
  3. 标准化日期格式
  4. 分词处理(中文评论)

7. 扩展思路:从数据存储到价值挖掘

构建好影评数据库只是第一步,真正的价值在于后续的数据分析和可视化呈现。几个值得尝试的方向:

情感分析: 使用NLP技术分析评论情感倾向,了解观众对电影的整体评价。

主题建模: 通过LDA等算法发现评论中的主要讨论话题。

时间序列分析: 观察评论数量和情感随时间的变化趋势。

关联分析: 研究评分、有用数和评论长度之间的关系。

实现基础情感分析的代码示例:

from snownlp import SnowNLP

def analyze_sentiment(content):
    s = SnowNLP(content)
    return s.sentiments  # 返回0-1之间的情感分值

# 为数据库添加情感分析字段
def add_sentiment_analysis(db_path):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    # 添加新字段
    try:
        cursor.execute("ALTER TABLE reviews ADD COLUMN sentiment REAL")
    except sqlite3.OperationalError:
        pass  # 字段已存在
        
    # 更新已有数据
    cursor.execute("SELECT id, content FROM reviews WHERE sentiment IS NULL")
    for row in cursor.fetchall():
        doc_id, content = row
        sentiment = analyze_sentiment(content)
        cursor.execute("UPDATE reviews SET sentiment=? WHERE id=?", (sentiment, doc_id))
        conn.commit()
        
    conn.close()

在实际项目中,建议将爬虫任务分解为多个阶段执行,并做好日志记录。可以尝试以下优化:

  1. 实现增量爬取,只获取新增评论
  2. 添加代理IP支持,提高爬取稳定性
  3. 使用任务队列管理爬取流程
  4. 添加监控告警机制
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐