豆瓣电影Top250榜单是中文互联网上最具公信力的电影评价榜单之一,包含了全球范围内最受观众喜爱的250部电影作品。作为一名数据分析爱好者,我一直想获取这个榜单的详细数据用于后续分析。经过多次尝试,我总结出了一套完整的Python爬虫解决方案,能够稳定高效地获取所有电影信息。
这个爬虫项目特别适合Python初学者作为第一个实战项目来练手。它涵盖了网页请求、页面解析、数据清洗和存储等爬虫开发全流程,但又不涉及过于复杂的反爬机制。通过这个项目,你可以掌握BeautifulSoup的基本用法,理解如何分析网页结构,以及如何处理常见的爬虫异常情况。
在开发这个爬虫时,我选择了以下几个Python库:
Requests:用于发送HTTP请求获取网页内容。相比urllib,它的API更加友好,支持连接池和会话保持。
BeautifulSoup4:HTML解析库。它比正则表达式更易用,支持多种解析器(我推荐使用lxml,速度快且容错性好)。
SQLAlchemy:ORM工具。它让我们可以用Python类操作数据库,避免了直接写SQL语句的繁琐。
Pandas:数据处理库。虽然本项目数据量不大,但Pandas的DataFrame可以方便地将数据导出到各种格式。
提示:安装这些库时建议使用虚拟环境,避免污染全局Python环境。可以使用
python -m venv venv创建虚拟环境,然后source venv/bin/activate激活(Linux/Mac)或venv\Scripts\activate(Windows)。
我的开发环境配置如下:
需要提前创建好数据库,我将其命名为douban_movie。创建用户的SQL语句如下:
sql复制CREATE DATABASE douban_movie CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'douban'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON douban_movie.* TO 'douban'@'localhost';
FLUSH PRIVILEGES;
豆瓣Top250的列表页URL为https://movie.douban.com/top250,采用分页加载方式,每页显示25部电影。通过分析可以发现:
?start=0表示第一页,?start=25表示第二页,以此类推。<li class="item">标签中。<div class="pic">内的<a>标签的href属性中。以《肖申克的救赎》为例,其详情页URL为https://movie.douban.com/subject/1292052/。关键数据分布如下:
<span class="top250-no">No.1</span><span property="v:itemreviewed">肖申克的救赎</span><div id="info">包含导演、编剧、主演等信息<strong property="v:average">9.7</strong>和<span property="v:votes">3213687人评价</span><div class="ratings-on-weight">中基于分析,我设计了19个字段来存储电影信息。特别说明几个字段的处理方式:
首先创建一个配置类,包含数据库连接和请求头设置:
python复制import requests
from bs4 import BeautifulSoup
from collections import OrderedDict
import pandas as pd
from sqlalchemy import create_engine
import time
class DouBanMovie:
def __init__(self, url, start_page=0, pages=10, page_size=25):
self.url = url
self.start_page = start_page
self.pages = pages
self.page_size = page_size
self.data_info = []
self.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',
'Accept-Language': 'zh-CN,zh;q=0.9'
}
# 数据库配置
self.db_engine = create_engine('mysql+pymysql://douban:your_password@localhost:3306/douban_movie?charset=utf8mb4')
实现两个核心解析方法:
python复制def get_mul_tag_info(self, soup_span):
"""处理多值字段,用/分隔"""
if not soup_span:
return ''
return '/'.join([span.get_text().strip() for span in soup_span if span.get_text().strip()])
def crawl_page_info(self, page):
"""获取单页电影链接"""
start_number = page * self.page_size
current_url = f"{self.url}?start={start_number}&filter="
try:
response = requests.get(current_url, headers=self.headers, timeout=10)
response.raise_for_status()
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'lxml')
return [a['href'] for div in soup.find_all('div', class_='pic')
for a in div.find_all('a', href=True)]
except Exception as e:
print(f"获取第{page+1}页失败: {str(e)}")
return []
python复制def crawl_detail_info(self, movie_href):
"""爬取单部电影详情"""
try:
response = requests.get(movie_href, headers=self.headers, timeout=15)
response.encoding = 'utf-8'
soup = BeautifulSoup(response.text, 'lxml')
movie_info = OrderedDict()
# 解析基础信息
movie_info['movie_rank'] = soup.find('span', class_='top250-no').get_text() if soup.find('span', class_='top250-no') else ''
movie_info['movie_name'] = soup.find('span', property='v:itemreviewed').get_text() if soup.find('span', property='v:itemreviewed') else ''
info_div = soup.find('div', id='info')
if not info_div:
raise ValueError("无法找到电影信息区域")
# 解析多值字段
movie_info['movie_director'] = self.get_mul_tag_info(info_div.find_all('span')[0].find_all('a'))
movie_info['movie_writer'] = self.get_mul_tag_info(info_div.find_all('span')[3].find_all('a'))
movie_info['movie_starring'] = self.get_mul_tag_info(info_div.find_all('span')[6].find_all('a'))
# 其他字段解析...
return movie_info
except Exception as e:
print(f"解析{movie_href}失败: {str(e)}")
return None
python复制def run(self):
"""执行爬虫"""
for page in range(self.start_page, self.pages):
print(f'正在处理第{page+1}页...')
movie_links = self.crawl_page_info(page)
for link in movie_links:
data = self.crawl_detail_info(link)
if data:
self.data_info.append(data)
print(f'已获取: {data["movie_name"]}')
time.sleep(1.5) # 礼貌爬取
# 存储数据
if self.data_info:
df = pd.DataFrame(self.data_info)
df.to_sql('douban_top250', self.db_engine, if_exists='replace', index=False)
print(f'成功存储{len(self.data_info)}条数据')
在实际运行中,我发现豆瓣有一些基础的反爬措施:
请求频率限制:连续快速请求会导致暂时封禁。我的解决方案:
User-Agent检测:固定UA容易被识别。改进方案:
python复制from fake_useragent import UserAgent
ua = UserAgent()
self.headers['User-Agent'] = ua.random
IP限制:长期运行可能导致IP被封。可以考虑:
原始数据中存在一些需要清洗的情况:
多语言名称处理:
python复制name = data['movie_name']
if ' ' in name: # 中英文名分离
ch_name, en_name = name.split(' ', 1)
data['ch_name'] = ch_name
data['en_name'] = en_name.strip(' ')
时长标准化:
python复制runtime = data['movie_run_time']
if '分钟' in runtime:
mins = int(runtime.replace('分钟', ''))
data['runtime_mins'] = mins
评分分布计算:
python复制def parse_star_ratio(ratio_str):
try:
return float(ratio_str.strip('%'))/100
except:
return 0.0
原始设计可以进一步优化:
sql复制CREATE TABLE `douban_top250` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rank` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '排名',
`ch_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '中文名',
`en_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '英文名',
`directors` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '导演',
`writers` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '编剧',
`actors` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '主演',
`types` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '类型',
`country` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '国家',
`language` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '语言',
`release_dates` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '上映日期',
`runtime_mins` int(11) DEFAULT NULL COMMENT '片长(分钟)',
`imdb_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'IMDb链接',
`rating` decimal(3,1) DEFAULT NULL COMMENT '评分',
`votes` int(11) DEFAULT NULL COMMENT '评价人数',
`five_star` decimal(3,2) DEFAULT NULL COMMENT '五星占比',
`four_star` decimal(3,2) DEFAULT NULL COMMENT '四星占比',
`three_star` decimal(3,2) DEFAULT NULL COMMENT '三星占比',
`two_star` decimal(3,2) DEFAULT NULL COMMENT '二星占比',
`one_star` decimal(3,2) DEFAULT NULL COMMENT '一星占比',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_rank` (`rank`),
KEY `idx_rating` (`rating`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='豆瓣Top250电影';
除了数据库存储,我还建议:
定期导出CSV备份:
python复制df.to_csv(f'douban_top250_{datetime.now().strftime("%Y%m%d")}.csv', index=False)
使用JSON格式保存原始数据:
python复制import json
with open('douban_raw.json', 'w', encoding='utf-8') as f:
json.dump(self.data_info, f, ensure_ascii=False, indent=2)
这个基础爬虫可以进一步扩展:
在实际开发中,我遇到了以下典型问题:
编码问题:确保所有环节使用UTF-8编码
python复制response.encoding = 'utf-8'
# 数据库连接字符串添加charset=utf8mb4
标签定位失败:使用更健壮的定位方式
python复制# 不推荐
soup.find('div', class_='info')
# 推荐
soup.find('div', attrs={'class': 'info'})
网络不稳定:增加重试机制
python复制from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def request_page(url):
return requests.get(url, headers=self.headers, timeout=10)
数据不一致:添加数据验证
python复制required_fields = ['movie_name', 'movie_rating']
if not all(field in data and data[field] for field in required_fields):
print(f"数据不完整: {data.get('movie_name','未知')}")
return None
经过多次迭代,这个豆瓣爬虫已经能够稳定运行。几点经验分享:
对于初学者,我建议:
这个项目虽然基础,但涵盖了爬虫开发的完整流程。掌握了这些技能后,你可以尝试更复杂的爬虫项目,如动态渲染页面、验证码识别、分布式爬取等。