1. 从零开始:为什么我们要爬取豆瓣影评?

如果你刚学Python,是不是总觉得那些教程里的例子有点“玩具感”?比如打印个九九乘法表,或者算个斐波那契数列,做完之后总觉得离“做点有用的事”还差那么一口气。我刚开始学的时候也是这种感觉,直到我尝试了第一个爬虫项目——爬豆瓣电影信息。那种感觉就像突然打开了一扇新世界的大门:原来我写的几行代码,真的能从互联网上抓取到活生生的、实时更新的数据。

今天我们要做的,就是这样一个“有用”且“有趣”的项目:用Python爬取任意一部豆瓣电影的影评,然后对这些文本数据做一个简单的情感分析,看看观众对这部电影的整体情绪是褒是贬。这整个过程,就像一次小型的“数据探险”。你不仅能学会如何从网页上“拿”数据,还能初步体验一下自然语言处理(NLP)的魅力,看看计算机是如何尝试理解人类文字的。

这个项目特别适合零基础的朋友。你不需要事先懂什么高深的算法,也不需要了解复杂的网络协议。我们需要的,只是一点Python基础(知道变量、循环、函数是什么就够了),一颗愿意动手尝试的心,以及大概一个下午的完整时间。我会把每一步都掰开揉碎了讲,包括我当初踩过的坑和绕过的弯路。相信我,当你看到自己爬下来的几百条影评,并生成第一张情感倾向分布图时,那种成就感是无可替代的。

那么,这个项目具体能做什么呢?简单来说,我们可以选一部你感兴趣的电影,比如最近热映的《流浪地球2》,或者经典的《肖申克的救赎》。然后,我们的程序会像一只勤劳的小蜘蛛,自动访问这部电影的影评页面,把成百上千条网友写的长评标题和正文内容“搬运”到我们自己的电脑里,存进数据库。接着,我们会用一个现成的、非常容易上手的中文情感分析工具库,给每一条影评打上一个“积极”或“消极”的标签。最后,我们可以用图表直观地展示出来:喜欢这部电影的人和吐槽它的人,大概各占多少比例。整个过程,从数据采集、清洗、分析到可视化,是一条完整的数据分析小流水线,麻雀虽小,五脏俱全。

2. 环境准备与工具安装:打造你的爬虫工作台

工欲善其事,必先利其器。在开始写代码之前,我们需要先把“厨房”收拾好,把必要的“厨具”准备好。别担心,这个过程非常简单,大部分都是一行命令搞定。

2.1 Python与Pip的确认

首先,确保你的电脑上已经安装了Python。打开你的命令行(Windows上是CMD或PowerShell,Mac或Linux上是终端),输入 python --version 或者 python3 --version。如果能看到像 Python 3.8.10 这样的版本号,并且数字是以3开头的,那就恭喜你,第一步已经完成了。如果提示“不是内部或外部命令”,那你需要先去Python官网下载并安装最新版本的Python。安装时,请务必勾选“Add Python to PATH”这个选项,这能省去后面很多配置的麻烦。

有了Python,通常也就自带了一个叫 pip 的工具,它是Python的包管理器,我们可以用它来安装别人写好的、功能强大的代码库。同样在命令行里输入 pip --version 检查一下。如果没问题,我们就可以进行下一步了。

2.2 安装核心“四大件”

我们的项目主要依赖四个Python库,它们各自扮演着关键角色:

  1. Requests:这是用来和网络打交道的库,比Python自带的 urllib 要简单友好得多。我们可以用它来向豆瓣服务器发送请求,说“你好,请把某某网页的内容给我看看”。
  2. BeautifulSoup4:网页抓下来是一大堆复杂的HTML标签代码,人眼很难直接阅读。BeautifulSoup(靓汤)就像一个智能解析器,能帮我们把HTML这锅“乱炖”解析得清清楚楚,让我们能轻松地找到并提取出里面的影评标题和正文。
  3. Jieba:这是一个中文分词工具。情感分析需要先理解句子是由哪些词组成的。英文有空格分隔单词,但中文没有。比如“我喜欢这部电影”,我们需要把它切成“我/喜欢/这/部/电影”这样的词语序列,Jieba就是干这个的。
  4. SnowNLP:这是我们情感分析的主力库。它内置了一个基于朴素贝叶斯算法训练好的中文情感分析模型。我们只需要把分好词的文本喂给它,它就能返回一个0到1之间的情感极性值,越接近1表示越积极,越接近0表示越消极。

安装它们只需要一行命令。打开你的命令行,输入:

pip install requests beautifulsoup4 jieba snownlp

静静等待它下载和安装完成即可。如果速度慢,可以考虑临时使用国内的镜像源,比如加上 -i https://pypi.tuna.tsinghua.edu.cn/simple

2.3 选择一个顺手的代码编辑器

写代码就像写文章,需要一个好用的编辑器。对于初学者,我强烈推荐 Visual Studio Code (VS Code)。它免费、轻量、功能强大,而且对Python的支持非常好。去官网下载安装后,记得安装官方的“Python”扩展插件。这样你的编辑器就会有代码高亮、智能提示、代码调试等功能,写起来会舒服很多。当然,如果你用PyCharm、Sublime Text或者Jupyter Notebook,也完全没问题,选一个你看着顺眼的就行。

3. 实战第一步:爬取豆瓣影评数据

好了,工具齐备,我们正式开工。爬虫工作可以形象地理解为“请求-解析-存储”三步走。我们以电影《流浪地球2》的影评页面为例,它的豆瓣ID是 35267208,影评列表页的URL模式是 https://movie.douban.com/subject/35267208/reviews?start=

3.1 分析网页结构,找到数据藏在哪里

写爬虫的第一步永远不是直接写代码,而是先“侦察”。用你的浏览器打开上面那个链接,然后右键点击页面,选择“检查”或“审查元素”,打开开发者工具。我们需要找到影评链接和标题在HTML代码中的“模样”。

你会发现,每一条影评摘要都被包裹在一个 <div class="main-bd"> 的标签里。在这个div里面,影评的标题链接藏在 <h2><a href="...">影评标题</a></h2> 这个结构里。我们的目标就是先把这个链接提取出来,因为完整的影评内容在链接指向的详细页面里。

同时,注意观察浏览器地址栏。当你点击“后页”时,URL最后面的 start= 参数会以20为单位增加(比如 start=20, start=40)。这证实了豆瓣影评是分页加载的,每页20条。这是我们设计循环爬取的关键。

3.2 编写爬虫核心代码

现在,打开你的代码编辑器,新建一个Python文件,比如叫做 douban_crawler.py。我们来一步步实现。

首先,导入我们安装好的库:

import requests
from bs4 import BeautifulSoup
import time
import sqlite3
import re

requests 用于网络请求,BeautifulSoup 用于解析HTML,time 用来在请求间加入延迟(这是对网站友好的行为,也是避免被反爬虫机制屏蔽的关键),sqlite3 用来把数据存到本地数据库,re 是正则表达式库,用来做更精细的文本清理。

接下来,我们定义一个函数来获取网页的HTML内容。这里有个非常重要的点:设置请求头(headers)。这相当于给你的请求披上一件“浏览器”的外衣,让豆瓣服务器以为是一个真实的用户在浏览,而不是一个程序在抓取。

def get_html(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:
        response = requests.get(url, headers=headers)
        response.raise_for_status() # 如果状态码不是200,抛出异常
        response.encoding = 'utf-8'
        return response.text
    except requests.RequestException as e:
        print(f"请求出错:{e}")
        return None

然后,我们写主爬取逻辑。思路是:先爬取列表页,提取出本页所有影评的详细页面链接,然后逐个访问这些链接,抓取完整的标题和正文。

def parse_list_page(html):
    """解析影评列表页,提取详情页链接"""
    soup = BeautifulSoup(html, 'html.parser')
    review_links = []
    # 找到所有包含影评的div块
    for item in soup.find_all('div', class_='main-bd'):
        link_tag = item.find('h2').find('a') if item.find('h2') else None
        if link_tag and link_tag.has_attr('href'):
            review_links.append(link_tag['href'])
    return review_links

def parse_detail_page(html):
    """解析影评详情页,提取标题和正文"""
    soup = BeautifulSoup(html, 'html.parser')
    # 提取标题
    title_tag = soup.find('h1')
    title = title_tag.get_text(strip=True) if title_tag else '无标题'

    # 提取正文 - 找到id为‘link-report’的div,这里存放正文
    content_div = soup.find('div', id='link-report')
    content = ''
    if content_div:
        # 找到所有的段落<p>标签,提取文本
        for p in content_div.find_all('p'):
            content += p.get_text(strip=True) + '\n'
    else:
        content = '内容未找到'

    return title, content.strip()

注意,在 parse_detail_page 函数中,我们用了 .get_text(strip=True) 来直接获取标签内的纯文本,并去掉首尾空白。这比原始文章里用复杂正则表达式去替换HTML标签要简洁和稳定得多,也是BeautifulSoup的优势所在。

3.3 遵守规则,设置访问延迟与数据存储

豆瓣有比较完善的反爬虫机制,如果我们请求得太快,IP地址很快就会被暂时限制访问。根据我的实测,一个比较安全的间隔是 3到4秒。我们在爬取每一个详情页之前,用 time.sleep(3.5) 让程序休息一下。这既是道德要求(不过度消耗对方服务器资源),也是保证我们爬虫能长期稳定运行的实用技巧。

数据爬下来,我们得存起来。这里我们用Python内置的 sqlite3 数据库,它不需要安装任何服务器,所有数据存在一个本地文件里,非常轻便。

def init_database():
    """初始化数据库和表"""
    conn = sqlite3.connect('douban_reviews.db')
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS reviews (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            movie_title TEXT,
            review_title TEXT,
            review_content TEXT,
            crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

def save_to_db(movie_title, review_title, review_content):
    """保存一条影评到数据库"""
    conn = sqlite3.connect('douban_reviews.db')
    cursor = conn.cursor()
    cursor.execute('''
        INSERT INTO reviews (movie_title, review_title, review_content)
        VALUES (?, ?, ?)
    ''', (movie_title, review_title, review_content))
    conn.commit()
    conn.close()

最后,我们把所有函数组装到主逻辑里,控制爬取的页数(比如先爬5页,100条影评试试水):

def main():
    movie_id = '35267208' # 《流浪地球2》的ID
    movie_title = '流浪地球2'
    base_url = f'https://movie.douban.com/subject/{movie_id}/reviews?start='
    
    init_database() # 初始化数据库
    
    total_pages_to_crawl = 5 # 计划爬取5页
    for page in range(total_pages_to_crawl):
        print(f'正在爬取第 {page+1} 页...')
        list_url = base_url + str(page * 20)
        list_html = get_html(list_url)
        
        if not list_html:
            print(f'第 {page+1} 页列表获取失败,跳过。')
            continue
            
        detail_links = parse_list_page(list_html)
        
        for link in detail_links:
            print(f'  正在抓取详情页:{link}')
            detail_html = get_html(link)
            time.sleep(3.5) # 关键延迟,避免被封
            
            if detail_html:
                title, content = parse_detail_page(detail_html)
                save_to_db(movie_title, title, content)
                print(f'    已保存:《{title[:20]}...》')
            else:
                print(f'    详情页抓取失败。')
                
    print('爬取任务完成!')

if __name__ == '__main__':
    main()

运行这个脚本,泡杯茶,等上几分钟,你就能在本地得到一个名为 douban_reviews.db 的数据库文件,里面整整齐齐地躺着爬取下来的影评数据了。第一次看到自己抓取的数据成功入库,感觉非常美妙。

4. 数据清洗与预处理:为分析做好准备

直接从网上爬下来的数据,我们称之为“原始数据”或“脏数据”。它里面可能包含很多我们不需要的东西,比如多余的空白符、HTML实体字符(如 &nbsp;)、甚至是一些广告文本。直接把这些数据扔给情感分析模型,效果会大打折扣。所以,我们需要做一个“数据清洗”的步骤。

4.1 常见的文本“脏数据”有哪些?

打开我们数据库里的几条影评看看,你可能会发现这些问题:

  1. 无关字符:比如“展开全文”、“收起”、“举报”等页面功能性的文字被混了进来。
  2. 特殊符号和空格:大量的换行符 \n、制表符 \t,或者连续多个空格。
  3. HTML残留:虽然我们用 get_text() 提取了文本,但有时可能还会漏掉一些注释或脚本内容。
  4. 非常短的无效内容:有些影评可能只有“好”、“顶”等几个字,或者干脆是空内容,这些样本对分析价值不大。

4.2 编写清洗函数

我们可以写一个专门的函数来处理这些情况。这里会用到Python的字符串方法和正则表达式。

import re

def clean_text(text):
    """
    清洗单条影评文本
    """
    if not text:
        return ''
    
    # 1. 去除常见的无关短语(根据实际情况增减)
    irrelevant_phrases = ['展开全文', '收起', '举报', '来自豆瓣App']
    for phrase in irrelevant_phrases:
        text = text.replace(phrase, '')
    
    # 2. 使用正则表达式移除URL链接(有些影评里会贴链接)
    text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', '', text)
    
    # 3. 将多个换行符、空格合并为一个空格
    text = re.sub(r'\s+', ' ', text)
    
    # 4. 去除首尾空白字符
    text = text.strip()
    
    return text

4.3 从数据库读取并清洗全部数据

清洗完成后,我们通常会把清洗好的数据存到一个新的地方,或者直接覆盖原字段(建议先备份)。这里我们选择新建一个列表来存放清洗后的、用于分析的文本。

def load_and_clean_data_from_db():
    """从数据库加载影评,并清洗内容"""
    conn = sqlite3.connect('douban_reviews.db')
    cursor = conn.cursor()
    cursor.execute('SELECT review_title, review_content FROM reviews')
    rows = cursor.fetchall()
    conn.close()
    
    cleaned_data = []
    for title, content in rows:
        full_text = title + '。' + content # 把标题和正文合并成一个文档
        cleaned_text = clean_text(full_text)
        if len(cleaned_text) > 10: # 过滤掉太短的文本
            cleaned_data.append(cleaned_text)
    
    print(f'共加载并清洗了 {len(cleaned_data)} 条有效影评。')
    return cleaned_data

这里我把标题和正文用句号连起来,是因为标题往往包含了作者的核心观点(如“年度最佳!”“大失所望”),对情感分析非常重要。同时,我们过滤了长度小于10个字符的文本,以排除无效数据。

5. 构建简易情感分析模型:让机器读懂情绪

数据洗干净了,重头戏来了——情感分析。我们不用自己从零开始训练模型,那太复杂。我们利用 SnowNLP 这个强大的轮子。它的情感分析功能是基于大量中文评论语料训练好的,对于影评这种主观文本,效果通常不错。

5.1 使用SnowNLP进行情感打分

SnowNLP的使用简单到不可思议。它的核心原理是计算一段文本属于“积极”情感的概率。这个值在0到1之间。

from snownlp import SnowNLP

def analyze_sentiment_snownlp(text):
    """
    使用SnowNLP分析单条文本情感
    返回情感极性值 (0~1,越接近1越积极)
    """
    try:
        s = SnowNLP(text)
        return s.sentiments
    except Exception as e:
        print(f"分析文本时出错:{e},文本内容:{text[:50]}...")
        return 0.5 # 出错时返回中性值

我们来测试一下:

test_texts = [
    "这部电影太棒了,特效震撼,剧情感人!",
    "非常失望,逻辑漏洞百出,浪费时间。",
    "演员演技在线,但故事节奏有点慢。",
]
for txt in test_texts:
    score = analyze_sentiment_snownlp(txt)
    print(f'文本:“{txt}”')
    print(f'  情感得分:{score:.4f},倾向:{"积极" if score > 0.6 else "消极" if score < 0.4 else "中性"}')
    print()

运行后,你会看到第一句得分很高(可能0.9以上),第二句很低(可能0.1以下),第三句可能在0.5左右。这说明模型基本能区分出明显的褒贬。

5.2 批量处理所有影评并分类

现在,我们把清洗好的几百条影评,一条条地交给SnowNLP去打分,然后根据得分进行分类。通常我们可以设定两个阈值:得分大于0.6的算“积极”,小于0.4的算“消极”,介于两者之间的算“中性”。

def batch_sentiment_analysis(cleaned_texts):
    """
    批量情感分析,返回分类结果
    """
    positive = []
    negative = []
    neutral = []
    
    for idx, text in enumerate(cleaned_texts):
        score = analyze_sentiment_snownlp(text)
        
        if score > 0.6:
            positive.append((idx, text, score))
        elif score < 0.4:
            negative.append((idx, text, score))
        else:
            neutral.append((idx, text, score))
    
    print(f'分析完成!')
    print(f'积极影评:{len(positive)} 条')
    print(f'消极影评:{len(negative)} 条')
    print(f'中性影评:{len(neutral)} 条')
    
    return positive, negative, neutral

5.3 模型局限性讨论与简单优化

SnowNLP虽然方便,但我们要知道它的局限性。它是一个通用模型,并非专门为影评训练。所以有时可能会误判,比如把“这部电影烂得很有特色”这种反讽句判断为积极。对于初学者项目,这个精度完全够用。如果你想让结果更准一点,可以尝试以下小技巧:

  1. 构建领域词库:影评有一些特定词汇。比如“演技炸裂”、“票房毒药”。我们可以用Jieba添加自定义词典,确保这些词能被正确分出来,可能对后续如果有更复杂的分析有帮助。
    import jieba
    jieba.add_word('演技炸裂', freq=2000, tag='n')
    jieba.add_word('票房毒药', freq=2000, tag='n')
    
  2. 结合规则:对于某些明确的关键词,我们可以用规则来覆盖模型。例如,如果文本中出现“千万别看”、“避雷”等词,可以直接标记为消极。
    def rule_based_boost(text, snownlp_score):
        negative_keywords = ['千万别看', '避雷', '烂片', '圈钱']
        positive_keywords = ['力荐', '神作', '二刷', '回味无穷']
        
        for kw in negative_keywords:
            if kw in text:
                return max(0, snownlp_score - 0.3) # 大幅降低分数
        for kw in positive_keywords:
            if kw in text:
                return min(1, snownlp_score + 0.3) # 大幅提高分数
        return snownlp_score
    
    在实际使用时,可以先调用SnowNLP得到基础分,再用这个函数进行微调。这属于简单的“规则+模型”混合方法,在实践中往往能快速提升效果。

6. 结果可视化:用图表讲好数据故事

分析出来一堆数字,远不如一张图表来得直观。我们最后一步,就是把情感分析的结果用图形展示出来。这里我们用 matplotlib 这个经典的绘图库。如果你还没安装,用 pip install matplotlib 安装一下。

6.1 绘制情感分布饼图

饼图非常适合展示构成比例。我们来画一张图,看看积极、消极、中性影评各占多少。

import matplotlib.pyplot as plt

def plot_sentiment_pie(positive_count, negative_count, neutral_count):
    """
    绘制情感分布饼图
    """
    labels = ['积极', '消极', '中性']
    sizes = [positive_count, negative_count, neutral_count]
    colors = ['#66b3ff', '#ff9999', '#99ff99'] # 蓝,红,绿
    explode = (0.05, 0.05, 0) # 让前两部分“突出”一点
    
    plt.figure(figsize=(8, 6))
    plt.pie(sizes, explode=explode, labels=labels, colors=colors, autopct='%1.1f%%', shadow=True, startangle=90)
    plt.axis('equal') # 保证饼图是正圆
    plt.title('豆瓣影评情感分布', fontsize=15)
    plt.show()

6.2 绘制情感得分分布直方图

直方图能让我们看到所有影评情感得分的分布情况,是集中在中性区域,还是两极分化?

def plot_sentiment_histogram(all_scores):
    """
    绘制情感得分分布直方图
    """
    plt.figure(figsize=(10, 6))
    plt.hist(all_scores, bins=20, color='skyblue', edgecolor='black', alpha=0.7)
    plt.axvline(x=0.5, color='red', linestyle='--', label='中性线 (0.5)')
    plt.xlabel('情感得分 (0:消极 -> 1:积极)')
    plt.ylabel('影评数量')
    plt.title('影评情感得分分布直方图')
    plt.legend()
    plt.grid(axis='y', alpha=0.3)
    plt.show()

6.3 生成词云图(进阶可选)

词云能直观地显示哪些词语在影评中出现得最频繁。我们可以分别为积极影评和消极影评生成词云,对比看看观众夸的时候爱用什么词,吐槽的时候又爱用什么词。

from wordcloud import WordCloud
import jieba

def generate_wordcloud(texts, title):
    """
    生成词云图
    """
    # 将所有文本合并,并用jieba分词
    all_text = ' '.join(texts)
    word_list = jieba.lcut(all_text)
    word_text = ' '.join(word_list)
    
    # 设置停用词,过滤“的”、“了”、“电影”等无意义高频词
    stopwords = set(['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '电影'])
    
    wc = WordCloud(
        font_path='simhei.ttf', # 指定中文字体路径,否则会乱码
        width=800,
        height=600,
        background_color='white',
        stopwords=stopwords,
        max_words=200
    )
    wc.generate(word_text)
    
    plt.figure(figsize=(10, 8))
    plt.imshow(wc, interpolation='bilinear')
    plt.axis('off')
    plt.title(title)
    plt.show()

注意,运行词云代码需要安装 wordcloud 库 (pip install wordcloud),并且需要准备一个中文字体文件(如 simhei.ttf),放在项目目录下或指定系统字体路径。

7. 项目总结与避坑指南

走完整个流程,你应该已经成功爬取了数据、完成了情感分析并生成了图表。回顾一下,我们其实完成了一个微型的、完整的数据分析项目。在这个过程中,我猜你可能会遇到,或者未来可能会遇到下面这几个“坑”,我提前给你提个醒:

第一大坑:请求被拒绝(403错误)。 这是最常见的反爬虫响应。除了我们之前提到的加 User-Agent 请求头和设置 time.sleep 延迟之外,还有几个小技巧:一是可以尝试在headers里加入 Referer 字段(通常设为豆瓣电影主页URL),模拟从站内跳转过来的行为。二是如果爬取量真的很大,可以考虑使用 requests.Session() 来保持会话,有时比单次请求更稳定。

第二大坑:HTML结构变化导致爬取失败。 网站前端可能会改版。今天能用的 class="main-bd",明天可能就变了。所以,写爬虫代码要有“容错”思维。多用 try...except,在 find 不到标签时返回默认值或跳过,不要让单条数据的失败导致整个程序崩溃。定期运行一下你的爬虫,确认它还能工作。

第三大坑:情感分析结果不准。 正如前面所说,通用模型有局限。如果你对这个分析环节特别感兴趣,想提升准确率,那你的学习路径可以往自然语言处理(NLP)方向深入了。下一步可以学习如何使用 sklearn 库,自己尝试从清洗好的文本中提取特征(比如TF-IDF),然后手动标注几百条影评(标上积极/消极),训练一个属于自己的简单的分类模型(如逻辑回归)。这个过程会比直接用SnowNLP复杂,但可控性和可解释性会强很多,你会对“机器学习”有更切身的体会。

关于数据使用伦理。 最后多唠叨一句,我们爬取数据是为了学习和技术实践。请尊重豆瓣网的服务条款,不要对它们的服务器造成压力。我们爬取的数据,不要用于商业用途或公开大量传播,在自己的电脑上做分析和学习就好。

这个项目就像一把钥匙,帮你打开了“用Python解决实际问题”的大门。爬虫和数据分析的技能树非常庞大,你可以沿着这个方向,去尝试爬取不同的网站(注意法律法规和Robots协议),分析不同的文本(新闻、社交媒体评论、电商评价),甚至结合更多可视化手段。最重要的就是保持动手的热情,遇到问题就查资料、调试代码,每一个你解决的问题,都会成为你宝贵的经验。好了,代码都在这里了,赶紧打开你的编辑器,复制粘贴,然后运行起来吧。亲眼看到结果的那一刻,你会觉得这一切都非常值得。

更多推荐