基于 Selenium 的美团外卖动态数据爬虫实现方案

1 阅读9分钟

美团外卖平台的商家列表、菜品详情、订单数据等核心内容均采用 JavaScript 动态渲染加载,传统 requests 库仅能获取未渲染的空壳 HTML 文档,无法直接提取有效数据。针对该类动态页面,主流解决方案分为AJAX 接口逆向浏览器模拟渲染两类:接口逆向开发成本高、易受平台接口更新影响导致失效,因此本文采用 Selenium 自动化测试框架,通过模拟真实浏览器执行环境,等待 JavaScript 渲染完成后精准提取目标数据。

核心技术难点与应对策略

美团外卖平台具备完善的反爬机制,核心限制包括地理位置校验人机验证拦截登录态强制校验。针对性解决策略:

  1. 精准配置地理位置信息,匹配平台商家推荐规则;
  2. 接入代理 IP 池分散请求流量,规避单 IP 限流封禁;
  3. 模拟人类用户操作行为(页面滚动、随机请求延迟),降低反爬识别风险。

一、开发环境配置

通过 pip 安装项目核心依赖库,实现浏览器自动化、驱动管理与数据持久化:

  • selenium:主流浏览器自动化操作框架,支持模拟用户交互与页面渲染;
  • webdriver-manager:自动适配 Chrome 浏览器版本,管理 ChromeDriver 驱动,无需手动配置;
  • pymysql:Python 连接 MySQL 数据库的驱动库,实现爬取数据的结构化存储。

二、Selenium WebDriver 初始化

配置 Chrome 浏览器启动参数,构建稳定的自动化运行环境,核心采用显式等待保证页面元素加载完成:

python

运行

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import time
import random

# 配置Chrome启动选项
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')  # 无头模式,生产环境推荐启用
chrome_options.add_argument('--disable-gpu')  # 禁用GPU加速,适配服务器环境
chrome_options.add_argument('--no-sandbox')  # 关闭沙盒模式,Linux环境必备
chrome_options.add_argument('--disable-dev-shm-usage')  # 解决共享内存不足问题
# 配置真实User-Agent,伪装浏览器请求
chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')

# 初始化WebDriver,自动安装匹配的ChromeDriver
driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options)
# 显式等待:最长等待10秒,等待元素加载完成后执行操作
wait = WebDriverWait(driver, 10)

关键配置说明

  1. 无头模式:无界面运行浏览器,降低资源消耗,适合服务器部署;
  2. User-Agent 伪装:模拟真实浏览器请求头,减少被反爬识别的概率;
  3. 显式等待:替代固定休眠,高效等待页面元素加载,提升爬虫稳定性与效率。

三、地理位置限制解决方案

美团外卖基于用户地理位置实现商家精准推荐,未配置合法位置将无法获取有效数据,提供两种标准化实现方案:

方案一:Cookie 持久化复用(推荐生产环境使用)

首次手动登录美团外卖并设置位置,保存 Cookie 至本地文件,后续直接复用登录态与位置信息:

python

运行

import json

# 1. 首次手动登录后,获取并保存Cookie到本地文件
cookies = driver.get_cookies()
with open('meituan_cookies.json', 'w', encoding='utf-8') as f:
    json.dump(cookies, f, ensure_ascii=False)

# 2. 后续爬虫启动时,加载本地Cookie恢复登录状态与位置信息
driver.get('https://www.meituan.com/')
with open('meituan_cookies.json', 'r', encoding='utf-8') as f:
    cookies = json.load(f)
for cookie in cookies:
    driver.add_cookie(cookie)

方案二:前端 JS 注入经纬度信息

通过执行 JavaScript 代码,直接修改浏览器本地存储,强制设置目标地理位置:

python

运行

# 访问美团外卖首页
driver.get('https://www.meituan.com/')
# 注入经纬度与位置信息(以北京市朝阳区为例)
driver.execute_script('''
    localStorage.setItem('geohash', 'wx4g0e0e0');
    localStorage.setItem('location', '{"latitude":39.9042,"longitude":116.4074,"address":"北京市朝阳区"}');
''')
# 刷新页面生效配置
driver.refresh()

四、商家列表数据爬取

针对美团外卖无限滚动加载的商家列表,模拟用户滚动行为加载全量数据,结合随机延迟规避反爬:

python

运行

def scrape_merchants(keyword: str = '美食', limit: int = 20) -> list:
    """
    爬取美团外卖商家列表
    :param keyword: 搜索关键词
    :param limit: 最大爬取商家数量
    :return: 商家信息列表
    """
    # 构造搜索URL
    search_url = f'https://www.meituan.com/s/{keyword}/'
    driver.get(search_url)
    time.sleep(3)  # 基础页面加载延迟

    merchant_list = []
    last_page_height = driver.execute_script('return document.body.scrollHeight')

    while len(merchant_list) < limit:
        # 模拟滚动至页面底部,加载更多商家数据
        driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
        # 随机延迟1.5-3秒,模拟真实用户操作
        time.sleep(random.uniform(1.5, 3.0))

        # 定位商家元素节点
        merchant_elements = driver.find_elements(By.CSS_SELECTOR, '.poi-item')
        
        # 增量提取新增商家数据
        for elem in merchant_elements[len(merchant_list):]:
            try:
                merchant_info = {
                    'name': elem.find_element(By.CSS_SELECTOR, '.poi-name').text,
                    'rating': elem.find_element(By.CSS_SELECTOR, '.star-rating').get_attribute('title'),
                    'address': elem.find_element(By.CSS_SELECTOR, '.address').text,
                    'link': elem.find_element(By.CSS_SELECTOR, 'a').get_attribute('href')
                }
                merchant_list.append(merchant_info)
                print(f"成功爬取商家:{merchant_info['name']} | 评分:{merchant_info['rating']}")
            except Exception as e:
                print(f"商家信息提取异常:{str(e)}")
                continue

        # 判断是否到达页面底部,无新数据则终止循环
        new_page_height = driver.execute_script('return document.body.scrollHeight')
        if new_page_height == last_page_height:
            print('已滚动至页面底部,无更多商家数据')
            break
        last_page_height = new_page_height

    return merchant_list[:limit]

核心实现要点

  1. 无限滚动处理:通过 JS 获取页面高度,循环滚动加载动态数据;
  2. 增量提取:仅处理新增渲染的商家元素,避免重复爬取;
  3. 异常捕获:单个商家提取异常不中断整体爬虫流程,提升鲁棒性。

五、商家详情与菜品数据爬取

进入商家详情页,加载全量菜品信息,提取商家基础信息与菜品数据:

python

运行

def scrape_merchant_detail(merchant_url: str) -> dict | None:
    """
    爬取商家详情及菜品列表
    :param merchant_url: 商家详情页链接
    :return: 商家详情+菜品列表字典,提取失败返回None
    """
    driver.get(merchant_url)
    time.sleep(2)

    # 提取商家核心基础信息
    try:
        shop_name = driver.find_element(By.CSS_SELECTOR, '.shop-name').text
        business_hours = driver.find_element(By.CSS_SELECTOR, '.business-hours').text
        shop_phone = driver.find_element(By.CSS_SELECTOR, '.phone').text
    except Exception:
        print('商家基础信息提取失败')
        return None

    # 滚动加载全部菜品数据
    last_height = driver.execute_script('return document.body.scrollHeight')
    while True:
        driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
        time.sleep(1)
        new_height = driver.execute_script('return document.body.scrollHeight')
        if new_height == last_height:
            break
        last_height = new_height

    # 提取菜品列表信息
    dish_list = []
    dish_elements = driver.find_elements(By.CSS_SELECTOR, '.dish-item')
    for elem in dish_elements:
        try:
            dish_info = {
                'name': elem.find_element(By.CSS_SELECTOR, '.dish-name').text,
                'price': elem.find_element(By.CSS_SELECTOR, '.dish-price').text,
                'sales': elem.find_element(By.CSS_SELECTOR, '.dish-sales').text
            }
            dish_list.append(dish_info)
        except Exception:
            continue

    return {
        'name': shop_name,
        'hours': business_hours,
        'phone': shop_phone,
        'dishes': dish_list
    }

六、代理 IP 配置(反封禁核心)

美团外卖对单 IP 请求频率严格限制,高频爬取会触发验证码或 IP 封禁,亿牛云爬虫代理接入方案如下:

标准代理配置(Selenium 原生)

python

运行

from selenium.webdriver.common.proxy import Proxy, ProxyType

# 亿牛云代理基础配置
PROXY_HOST = "t.16yun.cn"
PROXY_PORT = "31111"
PROXY_USER = "your_username"
PROXY_PASS = "your_password"

# 初始化代理对象
proxy = Proxy({
    'proxyType': ProxyType.MANUAL,
    'httpProxy': f'{PROXY_HOST}:{PROXY_PORT}',
    'sslProxy': f'{PROXY_HOST}:{PROXY_PORT}',
    'noProxy': 'localhost,127.0.0.1'
})

# 启动带代理的浏览器
driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options, proxy=proxy)

七、MySQL 数据持久化

将爬取的结构化数据存储至 MySQL 数据库,实现数据永久保存与后续分析:

python

运行

import pymysql
from pymysql.err import OperationalError

def save_to_mysql(merchant_data: list):
    """
    将商家数据存储至MySQL数据库
    :param merchant_data: 商家详情数据列表
    """
    try:
        # 建立数据库连接
        conn = pymysql.connect(
            host='localhost',
            user='root',
            password='your_password',
            database='meituan_data',
            charset='utf8mb4'
        )
        cursor = conn.cursor()

        # 创建商家信息表(不存在则创建)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS merchants (
                id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
                name VARCHAR(255) NOT NULL COMMENT '商家名称',
                rating VARCHAR(50) COMMENT '商家评分',
                address TEXT COMMENT '商家地址',
                link TEXT COMMENT '商家链接',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '爬取时间'
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '美团外卖商家信息表';
        ''')

        # 批量插入数据
        for merchant in merchant_data:
            cursor.execute('''
                INSERT INTO merchants (name, rating, address, link)
                VALUES (%s, %s, %s, %s)
            ''', (merchant['name'], merchant['rating'], merchant['address'], merchant['link']))

        conn.commit()
        print(f"成功存储 {len(merchant_data)} 条商家数据至MySQL")
    except OperationalError as e:
        print(f"数据库连接/操作失败:{str(e)}")
    finally:
        # 确保资源释放
        if 'conn' in locals() and conn.open:
            cursor.close()
            conn.close()

八、爬虫完整执行流程

整合所有模块,实现初始化→登录态恢复→数据爬取→数据存储的全流程自动化:

python

运行

def main():
    # 1. 初始化WebDriver
    driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options)
    wait = WebDriverWait(driver, 10)

    # 2. 恢复登录Cookie
    driver.get('https://www.meituan.com/')
    try:
        with open('meituan_cookies.json', 'r', encoding='utf-8') as f:
            cookies = json.load(f)
            for cookie in cookies:
                driver.add_cookie(cookie)
        print("Cookie加载成功,登录态已恢复")
    except FileNotFoundError:
        print("Cookie文件不存在,请首次手动登录获取")

    # 3. 爬取商家列表
    merchants = scrape_merchants(keyword='美食', limit=10)
    
    # 4. 爬取商家详情数据
    detailed_merchant_data = []
    for merchant in merchants:
        detail = scrape_merchant_detail(merchant['link'])
        if detail:
            detailed_merchant_data.append(detail)
        # 商家间随机延迟,降低请求频率
        time.sleep(random.uniform(2, 5))

    # 5. 数据存储至MySQL
    save_to_mysql(detailed_merchant_data)

    # 6. 关闭浏览器,释放资源
    driver.quit()
    print("爬虫任务执行完成,浏览器已关闭")

if __name__ == '__main__':
    main()

九、常见问题排查与性能优化

表格

异常问题根本原因标准化解决方案
触发人机验证码请求频率过高 / 单 IP 异常访问接入代理 IP 池,增大随机延迟,优化用户行为模拟
Cookie 登录态失效平台会话超时 / 异地登录检测定时刷新 Cookie,新增自动重登录逻辑
页面元素定位失败前端页面结构更新实时维护 CSS 选择器 / XPath,兼容页面迭代
长时间运行内存泄漏WebDriver 未正常释放 / 页面缓存堆积定时重启浏览器实例,强制启用无头模式
爬取效率过低重复启动浏览器 / 固定休眠过长复用 WebDriver 实例,用显式等待替代固定休眠

十、技术方案边界与选型建议

Selenium 方案优缺点

  • 优势:可直接获取渲染后的完整页面,调试成本低,无需逆向加密接口,适合短期、小规模爬虫项目;
  • 劣势:浏览器资源占用高、爬取速度较慢,不适用超大规模分布式采集场景。

大规模采集替代方案

  1. AJAX 接口逆向:直接调用平台后端 API,爬取效率最高,但需攻克加密参数、签名算法,技术门槛高;
  2. requests + 代理池:轻量高效,仅适用于无强反爬、非动态渲染的简单页面;
  3. seleniumwire + 代理池:平衡开发成本与爬取稳定性,适合中规模数据采集。