零基础实战:Python爬取豆瓣影评并构建简易情感分析模型
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库,它们各自扮演着关键角色:
- Requests:这是用来和网络打交道的库,比Python自带的
urllib要简单友好得多。我们可以用它来向豆瓣服务器发送请求,说“你好,请把某某网页的内容给我看看”。 - BeautifulSoup4:网页抓下来是一大堆复杂的HTML标签代码,人眼很难直接阅读。BeautifulSoup(靓汤)就像一个智能解析器,能帮我们把HTML这锅“乱炖”解析得清清楚楚,让我们能轻松地找到并提取出里面的影评标题和正文。
- Jieba:这是一个中文分词工具。情感分析需要先理解句子是由哪些词组成的。英文有空格分隔单词,但中文没有。比如“我喜欢这部电影”,我们需要把它切成“我/喜欢/这/部/电影”这样的词语序列,Jieba就是干这个的。
- 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实体字符(如 )、甚至是一些广告文本。直接把这些数据扔给情感分析模型,效果会大打折扣。所以,我们需要做一个“数据清洗”的步骤。
4.1 常见的文本“脏数据”有哪些?
打开我们数据库里的几条影评看看,你可能会发现这些问题:
- 无关字符:比如“展开全文”、“收起”、“举报”等页面功能性的文字被混了进来。
- 特殊符号和空格:大量的换行符
\n、制表符\t,或者连续多个空格。 - HTML残留:虽然我们用
get_text()提取了文本,但有时可能还会漏掉一些注释或脚本内容。 - 非常短的无效内容:有些影评可能只有“好”、“顶”等几个字,或者干脆是空内容,这些样本对分析价值不大。
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虽然方便,但我们要知道它的局限性。它是一个通用模型,并非专门为影评训练。所以有时可能会误判,比如把“这部电影烂得很有特色”这种反讽句判断为积极。对于初学者项目,这个精度完全够用。如果你想让结果更准一点,可以尝试以下小技巧:
- 构建领域词库:影评有一些特定词汇。比如“演技炸裂”、“票房毒药”。我们可以用Jieba添加自定义词典,确保这些词能被正确分出来,可能对后续如果有更复杂的分析有帮助。
import jieba jieba.add_word('演技炸裂', freq=2000, tag='n') jieba.add_word('票房毒药', freq=2000, tag='n') - 结合规则:对于某些明确的关键词,我们可以用规则来覆盖模型。例如,如果文本中出现“千万别看”、“避雷”等词,可以直接标记为消极。
在实际使用时,可以先调用SnowNLP得到基础分,再用这个函数进行微调。这属于简单的“规则+模型”混合方法,在实践中往往能快速提升效果。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
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协议),分析不同的文本(新闻、社交媒体评论、电商评价),甚至结合更多可视化手段。最重要的就是保持动手的热情,遇到问题就查资料、调试代码,每一个你解决的问题,都会成为你宝贵的经验。好了,代码都在这里了,赶紧打开你的编辑器,复制粘贴,然后运行起来吧。亲眼看到结果的那一刻,你会觉得这一切都非常值得。
更多推荐

所有评论(0)