经过前面的实战,我们已经能处理本地数据并可视化,而实际开发中更常见的需求是“获取网上的公开数据”——比如爬取新闻标题、天气信息、商品价格等。这篇我们进入Python热门应用领域:爬虫入门,用“Requests(发送网络请求)+ BeautifulSoup(解析网页)”这对入门友好的组合,从零实现一个“豆瓣电影Top250爬虫”,爬取电影名称、评分、简介等数据,最后保存到Excel。全程跟着做,你就能掌握爬虫核心逻辑,亲手从网页中提取数据!

一、爬虫基础:先搞懂3个核心问题

在写代码前,先明确爬虫的基本逻辑,避免踩坑:

1. 什么是爬虫?

爬虫本质是“模拟浏览器向网站发送请求,获取网页内容,再提取有用数据”的程序——比如你手动打开“豆瓣电影Top250”网页(https://movie.douban.com/top250),看到的文字、图片都是网页内容,爬虫就是用代码自动化完成“打开网页→复制数据”的过程。

2. 爬虫合法吗?

关键看“爬什么”和“怎么爬”:

  • ✅ 合法:爬取公开、非盈利的网页数据(如豆瓣公开的电影信息、新闻网站的公开文章);
  • ❌ 非法:爬取隐私数据(如用户手机号、身份证)、需要登录的非公开数据(如某些平台的会员内容),或频繁请求导致网站崩溃(恶意爬虫)。
    重要提醒:爬取前先看网站的「robots协议」(在网站域名后加/robots.txt,如https://movie.douban.com/robots.txt),协议会说明哪些内容可爬、哪些不可爬,遵守协议是基本准则。

3. 本次实战工具

我们用两个核心库实现爬虫,先安装(打开终端/命令提示符执行):

# 安装Requests(发送网络请求)、BeautifulSoup(解析网页)、openpyxl(保存Excel)
pip install requests beautifulsoup4 openpyxl lxml
  • requests:代替浏览器向网站发送请求,获取网页的HTML代码;
  • beautifulsoup4:解析HTML代码,提取目标数据(如电影名称、评分);
  • lxml:高效的HTML解析器,配合BeautifulSoup使用(比Python自带解析器更快);
  • openpyxl:将爬取的数据保存到Excel,之前数据可视化实战已用过。

二、实战步骤:豆瓣电影Top250爬虫开发

我们的爬虫流程分为四步,循序渐进实现:

  1. 发送请求:用Requests获取单页HTML代码;
  2. 解析数据:用BeautifulSoup提取电影信息;
  3. 多页爬取:循环获取10页数据(Top250共10页,每页25部电影);
  4. 保存数据:将所有数据存入Excel,方便后续分析。

步骤1:发送请求(获取单页HTML)

网站会通过“请求头(Headers)”识别请求来源,若直接用代码请求,可能被判定为“非浏览器访问”而拒绝(返回403错误)。因此需要先配置请求头,模拟浏览器访问。

如何获取浏览器请求头?

以Chrome浏览器为例:

  1. 打开豆瓣电影Top250第一页(https://movie.douban.com/top250?start=0&filter=);
  2. F12打开“开发者工具”→ 切换到“Network”标签 → 刷新页面;
  3. 在请求列表中找到第一个请求(名称为“top250?start=0&filter=”),点击进入;
  4. 下拉找到“Request Headers”,复制“User-Agent”字段(格式类似Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...)。
代码实现(单页请求)
import requests
from bs4 import BeautifulSoup
import pandas as pd
import logging
import time

# 1. 配置日志:记录爬取过程、错误,方便排查问题
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

# 2. 配置请求头:模拟浏览器,关键是User-Agent
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Accept-Language": "zh-CN,zh;q=0.9",  # 告诉网站优先返回中文内容
    "Referer": "https://movie.douban.com/"  # 模拟从豆瓣首页跳转,降低被拦截概率
}

def get_single_page_html(page_url):
    """
    发送GET请求,获取单页的HTML代码
    参数:page_url - 目标页面的URL(如豆瓣Top250第一页URL)
    返回:成功返回HTML文本字符串,失败返回None
    """
    try:
        logging.info(f"开始请求页面:{page_url}")
        # 发送请求:设置超时时间10秒,避免长时间等待
        response = requests.get(
            url=page_url,
            headers=HEADERS,
            timeout=10
        )
        
        # 检查请求是否成功:status_code=200表示正常响应
        response.raise_for_status()  # 若状态码非200(如403、404),会抛出HTTPError
        
        # 设置编码:豆瓣网页用UTF-8编码,避免中文乱码
        response.encoding = "utf-8"
        logging.info(f"成功获取页面:{page_url}(页面大小:{len(response.text)}字符)")
        return response.text
    
    except requests.exceptions.HTTPError as e:
        logging.error(f"请求页面{page_url}失败(HTTP错误):{e}")
        print(f"❌ HTTP错误:{e},可能被网站拒绝访问(检查User-Agent是否正确)")
        return None
    
    except requests.exceptions.Timeout as e:
        logging.error(f"请求页面{page_url}超时:{e}")
        print(f"❌ 超时错误:{e},请检查网络连接或稍后重试")
        return None
    
    except Exception as e:
        logging.error(f"请求页面{page_url}发生未知错误:{e}", exc_info=True)
        print(f"❌ 未知错误:{e},请查看日志获取详细信息")
        return None

# 测试:获取豆瓣Top250第一页HTML
if __name__ == "__main__":
    first_page_url = "https://movie.douban.com/top250?start=0&filter="
    html_content = get_single_page_html(first_page_url)
    if html_content:
        print("\n✅ 成功获取第一页HTML(前600字符预览):")
        print(html_content[:600])  # 打印前600字符,确认内容正确

运行代码后,若输出包含<html><head><title>等HTML标签,说明请求成功,已拿到网页原始内容。

步骤2:解析数据(从HTML中提取电影信息)

拿到HTML后,需要从杂乱的标签中提取目标数据。通过浏览器开发者工具观察豆瓣Top250页面结构:每部电影的信息都包裹在<div class="item">标签内,我们需要从中提取“排名、电影名称、评分、评价人数、简介”5个核心字段。

代码实现(单页数据解析)
def parse_single_page_data(html_text):
    """
    解析单页HTML文本,提取电影信息
    参数:html_text - 单页的HTML文本字符串
    返回:成功返回电影列表(每个元素为字典,包含单部电影信息),失败返回None
    """
    try:
        logging.info("开始解析页面数据")
        # 1. 创建BeautifulSoup对象,指定解析器为lxml(高效、容错性强)
        soup = BeautifulSoup(html_text, "lxml")
        
        # 2. 定位所有电影项:找到所有class="item"的div标签
        movie_items = soup.find_all("div", class_="item")  # class_加下划线,避免与Python关键字class冲突
        if not movie_items:
            logging.error("未找到电影项(可能网页结构更新或请求内容错误)")
            print("❌ 解析失败:未找到电影数据,请检查HTML内容或网页结构")
            return None
        
        # 3. 循环提取每部电影的信息
        movie_list = []
        for item in movie_items:
            # 3.1 排名:在class="pic"的div内,<em>标签的文本
            rank = item.find("div", class_="pic").find("em").text.strip()
            
            # 3.2 电影名称:取第一个class="title"的span标签(中文名称),排除外文名称
            title_tags = item.find_all("span", class_="title")
            movie_name = title_tags[0].text.strip()  # 第一个span是中文名称,第二个可能是外文名称
            
            # 3.3 评分:class="rating_num"且property="v:average"的span标签
            rating = item.find("span", class_="rating_num", property="v:average").text.strip()
            
            # 3.4 评价人数:class="star"的div内,最后一个span标签(格式如"123456人评价")
            star_div = item.find("div", class_="star")
            comment_num = star_div.find_all("span")[-1].text.strip()
            # 清理评价人数:去除"人评价"和逗号(如"1,234,567人评价"→"1234567")
            comment_num = comment_num.replace("人评价", "").replace(",", "")
            
            # 3.5 简介:class="inq"的span标签(部分电影无简介,用"无"填充)
            quote_tag = item.find("span", class_="inq")
            intro = quote_tag.text.strip() if quote_tag else "无"
            
            # 3.6 整理单部电影信息为字典,添加到列表
            movie_info = {
                "排名": rank,
                "电影名称": movie_name,
                "评分": rating,
                "评价人数": comment_num,
                "简介": intro
            }
            movie_list.append(movie_info)
        
        logging.info(f"页面数据解析完成,共提取{len(movie_list)}部电影信息")
        return movie_list
    
    except Exception as e:
        logging.error(f"解析页面数据发生错误:{e}", exc_info=True)
        print(f"❌ 解析错误:{e},请检查HTML格式或代码逻辑")
        return None

# 测试:解析第一页HTML并打印前3部电影信息
if __name__ == "__main__":
    first_page_url = "https://movie.douban.com/top250?start=0&filter="
    html_content = get_single_page_html(first_page_url)
    if html_content:
        movie_data = parse_single_page_data(html_content)
        if movie_data:
            print("\n✅ 第一页电影数据(前3部预览):")
            for idx, movie in enumerate(movie_data[:3], 1):
                print(f"\n第{idx}部电影:")
                for key, value in movie.items():
                    print(f"  {key}{value}")

运行后会输出前3部电影的结构化信息,类似:

第1部电影:
  排名:1
  电影名称:肖申克的救赎
  评分:9.7
  评价人数:2801234
  简介:希望让人自由。

第2部电影:
  排名:2
  电影名称:霸王别姬
  评分:9.6
  评价人数:2015678
  简介:风华绝代。

步骤3:多页爬取(循环获取10页数据)

豆瓣电影Top250共10页,每页25部电影,页面间的URL规律是**start参数**:

  • 第1页:start=0(从第0部开始,显示0-24部)→ URL:https://movie.douban.com/top250?start=0&filter=
  • 第2页:start=25(从第25部开始,显示25-49部)→ URL:https://movie.douban.com/top250?start=25&filter=
  • 第10页:start=225(从第225部开始,显示225-249部)→ URL:https://movie.douban.com/top250?start=225&filter=

我们通过循环生成10页URL,依次爬取并合并所有数据,同时加入“延迟爬取”(避免频繁请求被网站封禁IP)。

代码实现(多页爬取)
def crawl_douban_top250(total_pages=10):
    """
    爬取豆瓣电影Top250所有页面数据
    参数:total_pages - 总页数(默认10页,对应250部电影)
    返回:成功返回所有电影的列表(共250部),失败返回None
    """
    logging.info(f"开始爬取豆瓣电影Top250(计划爬取{total_pages}页)")
    all_movies = []  # 存储所有页面的电影数据
    
    for page in range(total_pages):
        # 1. 生成当前页的URL:start = 页码 * 25(页码从0开始)
        start = page * 25
        current_url = f"https://movie.douban.com/top250?start={start}&filter="
        
        # 2. 获取当前页HTML
        html_content = get_single_page_html(current_url)
        if not html_content:
            logging.warning(f"第{page+1}页爬取失败(未获取HTML),跳过该页")
            continue
        
        # 3. 解析当前页数据
        current_page_movies = parse_single_page_data(html_content)
        if not current_page_movies:
            logging.warning(f"第{page+1}页解析失败,跳过该页")
            continue
        
        # 4. 将当前页数据添加到总列表
        all_movies.extend(current_page_movies)  # extend:将列表元素逐个加入,区别于append(加入整个列表)
        
        # 5. 延迟爬取:每次请求后暂停1.5-2秒,模拟人工浏览,降低被封禁概率
        delay = 1.8
        logging.info(f"第{page+1}页爬取完成(获取{len(current_page_movies)}部电影),延迟{delay}秒后爬取下一页")
        time.sleep(delay)
    
    # 6. 检查爬取结果
    if len(all_movies) == 0:
        logging.error("所有页面爬取/解析失败,未获取任何电影数据")
        print("❌ 爬取失败:未获取到任何电影数据,请检查网络、请求头或网页结构")
        return None
    
    logging.info(f"豆瓣电影Top250爬取完成,共获取{len(all_movies)}部电影数据(目标250部)")
    print(f"\n✅ 所有页面爬取完成!共获取{len(all_movies)}部电影数据")
    return all_movies

# 测试:爬取所有10页数据并打印概览
if __name__ == "__main__":
    top250_movies = crawl_douban_top250(total_pages=10)
    if top250_movies:
        # 计算并打印关键统计信息
        highest_rating_movie = max(top250_movies, key=lambda x: float(x["评分"]))
        most_comments_movie = max(top250_movies, key=lambda x: int(x["评价人数"]))
        
        print(f"\n📊 爬取结果概览:")
        print(f"  最高评分电影:{highest_rating_movie['电影名称']}{highest_rating_movie['评分']}分)")
        print(f"  评价人数最多电影:{most_comments_movie['电影名称']}{most_comments_movie['评价人数']}人评价)")
        print(f"  评分≥9.5分的电影数量:{len([m for m in top250_movies if float(m['评分']) >= 9.5])}部")

运行后会循环爬取10页,每爬完一页暂停1.8秒,最终输出总数据量和关键统计信息,若数据量接近250部,说明爬取成功。

步骤4:保存数据(存入Excel)

爬取到数据后,用pandas将电影列表保存为Excel文件,方便后续筛选、分析(如用第十篇学的可视化方法生成评分分布图表)。

代码实现(数据保存)
def save_movies_to_excel(movie_list, excel_path="豆瓣电影Top250.xlsx"):
    """
    将爬取的电影数据保存到Excel文件
    参数:movie_list - 电影数据列表,excel_path - 保存路径(默认当前目录)
    返回:成功返回True,失败返回False
    """
    try:
        logging.info(f"开始将数据保存到Excel:{excel_path}")
        # 1. 将电影列表转换为DataFrame(pandas的表格数据结构)
        df = pd.DataFrame(movie_list)
        
        # 2. 调整列顺序(按我们希望的展示顺序排列)
        column_order = ["排名", "电影名称", "评分", "评价人数", "简介"]
        df = df[column_order]  # 重新排列列
        
        # 3. 保存到Excel(index=False:不保存默认索引列)
        df.to_excel(
            excel_path,
            index=False,  # 不写入行索引
            engine="openpyxl"  # 指定引擎,支持.xlsx格式
        )
        
        logging.info(f"数据成功保存到Excel:{excel_path}{len(df)}行数据)")
        print(f"\n✅ 数据已保存到Excel文件:{excel_path}")
        print(f"  包含{len(df)}部电影数据,{len(df.columns)}个字段(排名/电影名称/评分/评价人数/简介)")
        return True
    
    except Exception as e:
        logging.error(f"保存Excel失败:{e}", exc_info=True)
        print(f"❌ 保存失败:{e},请检查文件路径是否有权限")
        return False

# 完整流程:爬取→保存(最终可直接运行这段代码)
if __name__ == "__main__":
    print("="*60)
    print("📽️  豆瓣电影Top250爬虫工具")
    print("="*60)
    
    # 1. 爬取所有数据
    movies_data = crawl_douban_top250(total_pages=10)
    
    # 2. 保存到Excel(若爬取成功)
    if movies_data:
        save_movies_to_excel(movies_data, excel_path="豆瓣电影Top250.xlsx")
    
    print("\n🎉 程序执行完毕!")

运行后,项目文件夹中会生成“豆瓣电影Top250.xlsx”文件,打开后可看到结构化的电影数据,包含排名、名称、评分等字段,方便后续分析。

三、实战拓展:让爬虫更完善

基础功能实现后,可尝试以下拓展,巩固知识点:

1. 增加数据去重

若爬取过程中因网络问题重复获取某页数据,可在crawl_douban_top250函数中添加去重逻辑:

# 在all_movies.extend(current_page_movies)前添加
for movie in current_page_movies:
    # 按“排名”去重(排名唯一)
    if not any(m["排名"] == movie["排名"] for m in all_movies):
        all_movies.append(movie)

2. 爬取更多字段

尝试提取“导演、主演、上映年份、国家/地区”等信息(需通过开发者工具定位对应标签),例如:

# 提取导演和主演(在parse_single_page_data函数的循环中)
info_div = item.find("div", class_="bd").find("p", class_="")
info_text = info_div.text.strip().replace("\n", "").replace("  ", " ")
# 解析格式如“导演: 弗兰克·德拉邦特 Frank Darabont 主演: 蒂姆·罗宾斯 Tim Robbins... 1994 / 美国 / 剧情 犯罪”

3. 代理IP(应对封禁)

若频繁爬取被豆瓣封禁IP,可使用代理IP(需自行获取免费/付费代理),修改get_single_page_html函数:

proxies = {
    "http": "http://代理IP:端口",
    "https": "https://代理IP:端口"
}
# 在requests.get中添加proxies参数
response = requests.get(url=page_url, headers=HEADERS, proxies=proxies, timeout=10)

总结

这篇实战串联了前序核心知识点,并新增爬虫专属技能:

  1. 第三方库使用:用requests发送网络请求,beautifulsoup4解析HTML;
  2. 函数封装:将“请求、解析、保存”拆分为独立函数,逻辑清晰;
  3. 异常处理:捕获网络错误、解析错误,确保程序稳定运行;
  4. 反爬基础:设置请求头、延迟爬取,避免被网站封禁。

掌握这些后,你可以尝试爬取其他公开网站(如新闻网站、天气网站),核心逻辑相通——“请求→解析→存储”。下一篇我们将学习“API接口调用”,这是比爬虫更规范的数据获取方式,敬请期待!

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐