1. 环境准备与工具选择

要开始我们的豆瓣电影TOP250数据抓取之旅,第一步就是把“厨房”收拾好,把需要的“厨具”备齐。这里说的“厨房”就是你的编程环境,“厨具”就是我们要用到的Python库。别担心,整个过程就像安装手机APP一样简单,我会带你一步步来。

首先,确保你的电脑上已经安装了Python。我推荐使用Python 3.7或以上的版本,兼容性更好。怎么检查呢?打开你的命令行(Windows上是CMD或PowerShell,Mac或Linux上是终端),输入 python --version 或者 python3 --version,看看版本号对不对。如果还没安装,直接去Python官网下载安装包,记得安装时勾选“Add Python to PATH”这个选项,这能省去后面配置环境变量的麻烦。

接下来,我们需要几个核心的“武器库”。我们用 pip 这个Python自带的包管理工具来安装。在命令行里,依次输入下面几条命令:

pip install requests
pip install lxml
pip install fake-useragent

我来解释一下这几个库是干嘛的。requests 库是我们的“网络信使”,负责向豆瓣的服务器发送请求,说“你好,请把网页内容给我看看”。lxml 库是一个强大的解析工具,当服务器把一堆HTML代码(也就是网页的骨架和血肉)扔回来时,lxml 能帮我们从中精准地找到电影名字、导演这些“宝藏信息”。而 fake-useragent 库则是一个“伪装大师”,它能帮我们生成千变万化的浏览器身份标识(User-Agent)。为什么要伪装呢?因为豆瓣这类网站会有反爬虫机制,如果它发现总是同一个“机器人”在访问,可能就会拒绝服务。用这个库,我们每次访问都能换个“马甲”,大大降低被识破的风险。

除了这些,我们可能还会用到Python标准库里的 re 模块,也就是正则表达式,它对于处理一些不规则的文本信息非常拿手。不过这个是Python自带的,不需要额外安装。好了,工具都备齐了,我们的“数据挖掘机”已经可以点火启动了。

2. 网页结构分析与目标锁定

工欲善其事,必先利其器。现在工具有了,我们得先搞清楚要去哪里“挖宝”,以及宝藏具体埋在哪块“地砖”下面。这意味着我们需要仔细分析豆瓣电影TOP250页面的结构。

打开你的浏览器,访问 https://movie.douban.com/top250。你会看到一个经典的榜单页面。我们的目标是抓取每部电影的:电影名称、导演、主演、上映年份、国家/地区、电影类型、评分和一句话评价。接下来,就要像侦探一样,在网页的源代码里找到这些信息藏身的标签。

在页面上右键点击,选择“检查”或“审查元素”(不同浏览器叫法略有不同),打开开发者工具。这里我们看到的是网页的“源代码”,也就是浏览器真正接收到并渲染出漂亮页面的原始代码。我们需要用到开发者工具左上角那个“箭头”图标(通常叫“选择元素”工具)。

点击这个箭头,然后把鼠标移动到网页上第一部电影《肖申克的救赎》的标题上点击。此时,开发者工具的高亮区域会自动定位到对应的HTML代码。你会发现,电影标题通常被包裹在类似 <span class="title">肖申克的救赎</span> 的标签里。这个 class="title" 就是它的关键特征,是我们后续用XPath定位的“路标”。

用同样的方法,我们去定位其他信息:

  • 导演/演员信息:一般在 <div class="bd"> 下的第一个 <p> 标签里,是一段混合的文本,比如“导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 /...”。
  • 评分:很容易找到 <span class="rating_num">9.7</span>
  • 一句话评价:在 <span class="inq">希望让人自由。</span> 里。
  • 年份、国家、类型:它们和导演信息混在同一个段落里,通常用 / 分隔,比如“1994 / 美国 / 犯罪 剧情”。

最重要的一步:分析分页逻辑。翻到页面底部点击“下一页”,观察浏览器地址栏URL的变化。你会发现URL从 ...top250?start=0&filter= 变成了 ...top250?start=25&filter=,再下一页是 start=50。规律很明显:start 参数控制了起始位置,每页展示25部电影。要抓取全部250部,我们只需要用一个循环,让 start 参数从0开始,每次增加25,直到225即可。这个发现让我们可以轻松地规划如何抓取所有页面,而不需要手动构造10个不同的URL。

3. 核心爬虫代码编写与解析

分析完毕,现在开始动手写代码。我会把完整代码分成几个逻辑块,并逐块详细解释,确保你能看懂每一行的作用。

3.1 基础请求与UA伪装

首先,我们导入必要的库,并设置请求头进行UA伪装。

import requests
from lxml import etree
import re
from fake_useragent import UserAgent

# 初始化一个UserAgent对象,用于随机生成浏览器标识
ua = UserAgent()

# 目标URL的模板。{start} 是占位符,我们会在循环中替换它
url_template = "https://movie.douban.com/top250?start={start}&filter="

# 打开(或创建)一个文本文件,用于存储爬取的数据。'w'模式表示写入,如果文件存在则覆盖。
# encoding='utf-8' 确保能正确保存中文。
with open('./douban_top250.txt', 'w', encoding='utf-8') as f:
    # 写入文件标题行,方便识别每一列是什么
    f.write("电影名称#导演#主演#年份#国家#类型#评分#短评\n")

    # 循环10次,分别获取第1页(start=0)到第10页(start=225)
    for start in range(0, 250, 25):
        # 构造当前页的完整URL
        current_url = url_template.format(start=start)
        
        # 准备请求头,使用随机生成的User-Agent进行伪装
        headers = {
            'User-Agent': ua.random
        }
        
        # 发送GET请求,获取网页内容。加入headers和设置一个合理的超时时间是个好习惯。
        print(f"正在抓取: {current_url}")
        response = requests.get(current_url, headers=headers, timeout=10)
        
        # 检查请求是否成功(状态码200表示成功)
        if response.status_code != 200:
            print(f"请求失败,状态码:{response.status_code}")
            continue  # 如果失败,跳过当前页,继续下一页
        
        # 将响应的文本内容(HTML代码)传递给lxml的etree进行解析,生成一个可以XPath查询的对象
        html = etree.HTML(response.text)

这部分代码搭建了爬虫的基本框架。with open... 语句是Python中处理文件的最佳实践,它确保文件在使用完毕后会被正确关闭,即使中间发生错误。循环 for start in range(0, 250, 25): 正是基于我们之前发现的分页规律。ua.random 每次都会生成一个不同的、常见的浏览器User-Agent字符串,这是绕过简单反爬的第一道防线。

3.2 XPath定位与数据提取

获取到单页的HTML树对象后,我们需要定位到所有电影条目所在的“容器”,然后遍历每个容器,提取具体信息。

        # 使用XPath定位到包含所有电影信息的列表项(li)的集合。
        # 路径含义:找到class属性为'grid_view'的ol有序列表,再找到其下所有的li子元素。
        movie_list = html.xpath("//ol[@class='grid_view']/li")
        
        # 如果当前页没有找到电影条目,可能页面结构有变或已抓取完,跳出循环
        if not movie_list:
            print("未找到电影列表,可能已到达末尾或页面结构变化。")
            break
        
        # 遍历当前页的每一个电影条目(li标签)
        for movie in movie_list:
            # 提取电影名称(默认取第一个中文标题)
            # .// 表示从当前节点(movie)下开始查找
            # span[@class='title'][1] 表示取class为title的第一个span标签
            # /text() 获取该标签内的文本内容
            # 由于结果可能是一个列表,用 ''.join() 安全地将其转换为字符串
            name = ''.join(movie.xpath(".//span[@class='title'][1]/text()")).strip()
            
            # 提取评分
            rating = ''.join(movie.xpath(".//span[@class='rating_num']/text()")).strip()
            
            # 提取一句话短评(有些电影可能没有,所以给个默认值)
            quote_elem = movie.xpath(".//span[@class='inq']/text()")
            quote = quote_elem[0] if quote_elem else "无"
            
            # 提取包含导演、演员、年份等信息的整个文本块
            info_block = ''.join(movie.xpath(".//div[@class='bd']/p[1]/text()"))
            # 这个info_block包含换行符和空格,需要先清洗
            info_block_clean = ' '.join(info_block.split()).strip()

这里展示了XPath的强大与简洁。//ol[@class='grid_view']/li 这个表达式就像一张精确的地图,直接把250个“宝箱”(电影条目)的位置告诉了我们。在遍历每个“宝箱”时,我们再用相对路径(以 .// 开头)去里面寻找具体的“宝物”(数据)。.strip() 方法用于去除字符串首尾的空白字符,让数据更干净。

3.3 使用正则表达式处理复杂文本

info_block_clean 变量现在是一段像“导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 1994 / 美国 / 犯罪 剧情”这样的文本。我们需要用“手术刀”把它精细地拆分开。这里正则表达式(re模块)就是最合适的手术刀。

            # 初始化变量,防止后续引用未定义变量出错
            director, actor, year, country, genres = "", "", "", "", ""
            
            # 1. 提取导演 (格式固定为“导演: 某某某”)
            # re.search 在字符串中搜索匹配正则表达式的第一个位置
            # r"导演:\s*([\u4e00-\u9fa5A-Za-z·.]+)" 解释:
            # r 表示原始字符串,避免转义麻烦
            # 导演: 匹配文字
            # \s* 匹配0个或多个空白字符(空格、换行等)
            # ([\u4e00-\u9fa5A-Za-z·.]+) 这是一个捕获组,匹配一个或多个中文字符、英文字母、点或中间点,用来提取导演名字
            director_match = re.search(r"导演:\s*([\u4e00-\u9fa5A-Za-z·.]+)", info_block_clean)
            if director_match:
                director = director_match.group(1)  # group(1)获取第一个括号里匹配的内容
            
            # 2. 提取主演 (格式为“主演: 某某某 / 某某某”,且可能不存在)
            actor_match = re.search(r"主演:\s*([\u4e00-\u9fa5A-Za-z·./\s]+?)(?=\s+\d{4}|$)", info_block_clean)
            # 正则解释:匹配“主演: ”之后,到“四个连续数字(年份)”或字符串结尾之前的所有字符(惰性匹配)。
            if actor_match:
                actor = actor_match.group(1).strip()
            else:
                actor = "无"
            
            # 3. 提取年份、国家、类型 (它们通常被 '/' 分隔在字符串末尾)
            # 使用 split('/') 方法分割字符串,是最直接的方式
            # 我们先把整个info_block按'/'分割
            parts = info_block_clean.split('/')
            if len(parts) >= 3:
                # 年份通常是倒数第三个部分,去掉空格后,取其中的数字部分
                year_candidate = parts[-3].strip()
                year_match = re.search(r'(\d{4})', year_candidate) # 匹配4位数字
                year = year_match.group(1) if year_match else year_candidate
                
                # 国家/地区是倒数第二部分
                country = parts[-2].strip()
                
                # 电影类型是最后一部分
                genres = parts[-1].strip()

正则表达式看起来有点复杂,但它是处理非结构化文本的利器。我用了两种策略:对于格式固定的“导演”,直接用正则精准提取;对于末尾用/分隔的信息,先用简单的 split 分割,再从结果中提取。这种组合拳非常实用。if 判断是为了确保即使某部电影信息缺失(比如没有主演),程序也不会崩溃,而是赋予一个默认值(如“无”)。

3.4 数据存储与进度反馈

所有信息都提取出来后,我们需要把它保存到文件,并给用户一个进度反馈。

            # 将所有字段用'#'连接,构成一行数据,并写入文件
            # 这是一种简单的结构化方式,后续可以用Excel或文本编辑器轻松分列查看
            data_line = f"{name}#{director}#{actor}#{year}#{country}#{genres}#{rating}#{quote}\n"
            f.write(data_line)
            
            # 在控制台打印当前抓取到的电影信息,提供实时反馈
            print(f"  已抓取: 《{name}》 - 评分: {rating}")
        
        # 每抓取完一页,暂停一小段时间,模拟人类浏览,避免请求过于频繁给服务器造成压力
        # time.sleep(random.uniform(1, 3))  # 如果需要可以取消注释,并导入time和random模块

print("所有数据抓取完成!已保存到 douban_top250.txt 文件中。")

将数据用 # 这样的分隔符写入文本文件,是一种轻量级且通用的数据存储方式。你之后可以用任何文本编辑器打开,或者轻松导入到Excel、数据库中进行进一步分析。在控制台打印进度,能让你实时了解爬虫的工作状态,做到心中有数。最后那个被注释掉的 time.sleep 是“礼貌性爬虫”的体现,适当延迟请求是对目标网站的尊重,也能进一步降低被封IP的风险。

4. 反爬策略应对与代码优化

虽然我们的代码已经具备了一定的反爬能力(随机UA),但面对更严格的网站,我们还需要更多策略。这里分享几个我实战中总结的要点和优化方向。

1. 请求头(Headers)的完善: 我们只设置了 User-Agent,但一个真实的浏览器请求会携带很多其他信息。你可以通过开发者工具的“Network”面板,查看一个正常请求的所有Headers,并挑选一些关键的添加到代码中,比如 Referer(来源页面)、Accept-Language(接受语言)等。这会让你的请求看起来更“像”真人。

headers = {
    'User-Agent': ua.random,
    'Referer': 'https://movie.douban.com/',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Accept-Encoding': 'gzip, deflate, br', # 注意:requests自动处理解码,这里写不写均可
    'Connection': 'keep-alive'
}

2. 频率控制与代理IP: 这是中级爬虫必须考虑的。即使有UA伪装,如果一秒钟请求几十次,还是会被识别出来。一定要在循环中加入随机延时,比如 time.sleep(random.uniform(2, 5))。如果项目规模大,可能需要使用代理IP池来轮换IP地址,避免单个IP被限制。免费的代理IP不稳定,可以考虑一些可靠的付费服务。

3. 异常处理与代码健壮性: 网络请求充满不确定性,超时、连接错误、页面结构临时变动都可能发生。用 try...except 语句包裹核心请求和解析代码,记录错误日志,并设计重试机制,能让你的爬虫在无人值守时运行得更稳健。

try:
    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()  # 如果状态码不是200,主动抛出异常
except requests.exceptions.RequestException as e:
    print(f"请求发生错误: {e}")
    # 这里可以加入重试逻辑或记录到日志文件
    continue

4. 解析策略的灵活性: 不要过度依赖单一的XPath路径。网站的HTML结构可能会改版。一个更安全的方法是同时准备几个备用的XPath,或者使用更宽泛的定位方式,再通过文本内容进行过滤。例如,找评分时,除了用 class='rating_num',也可以试试找包含“评分”文字的span标签。

5. 数据清洗的强化: 我们提取的数据里可能混入不必要的空格、换行符,甚至一些不可见的Unicode字符。在写入文件前,进行更彻底的清洗是必要的。可以使用 .strip()replace(‘\u200b’, ‘’)(删除零宽空格)等方法。

把这些策略融入到你的爬虫中,它就能从一个简单的脚本,进化成一个健壮、可靠、可持续运行的数据采集工具。爬虫的编写就像一场与网站设计者之间的“博弈”,理解规则、保持礼貌、灵活应变,才能长久地获取到你想要的数据。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐