Python 爬虫爬取应用商店数据:请求构造与数据解析

应用商店(如 Google Play、App Store、豌豆荚)的应用数据是互联网竞品分析、行业市场研究、ASO 优化的核心数据源。应用商店爬虫的核心爬取难点主要集中在两大模块:一是请求构造 ,多数平台存在接口参数加密、请求签名校验等反爬限制;二是数据解析,接口普遍存在 JSON 层级嵌套深、不同接口字段命名不规范、数据格式杂乱等问题。本文以应用搜索列表、应用详情页为采集主线,完整讲解抓包分析、合规请求构造、结构化数据解析、代理反爬优化全流程落地方案,并提供可直接复用的 Python 源码。

一、应用商店爬取的技术难点

主流应用商店均具备成熟的反爬风控体系,结合Web端接口特征,整理核心难点如下:

难点 详细说明
接口参数加密 部分应用商店接口增加签名校验、动态加密参数,无合法加密参数直接请求会返回 403 权限拒绝
多层反爬机制 包含请求频率限制、UA 请求头校验、IP 封禁、滑块验证码、会话校验等风控策略
非规范数据格式 接口返回 JSON 通常嵌套 3-5 层结构,不同分类、不同版本接口字段命名不统一,适配难度高
页面动态加载 搜索结果分页异步加载、评论数据懒加载,常规静态请求无法获取完整数据

本文实现方案:以国内主流应用商店(豌豆荚、应用宝等)Web 端公开接口为采集对象,基于 Requests 库直接请求结构化 JSON 接口,跳过 JS 渲染流程,降低爬虫开发难度,提升数据采集稳定性。

二、接口分析:抓包定位数据源

应用商店优先采集 XHR 异步接口数据,相较于 HTML 页面解析,JSON 结构化接口数据更规整、提取成本更低。抓包流程采用浏览器开发者工具完成,操作步骤如下:

打开浏览器开发者工具(F12)→ 切换至 Network 面板 → 筛选 XHR 异步请求 → 在应用商店执行搜索操作,筛选出返回 JSON 格式的核心业务接口。抓包过程重点观察以下要素:

  1. 请求 URL:区分搜索接口、详情接口、评论接口的 URL 规则,归纳接口通用模板;
  2. 请求方法:判定接口为 GET 静态请求或 POST 加密请求;
  3. 请求参数:区分固定静态参数、时间戳、签名等动态可变参数;
  4. 请求头:识别 Token、签名、校验字段等必备请求头部参数。

本文列举典型应用商店搜索接口示例:

python 复制代码
GET /api/search?keyword=微信&page=1&size=20
Host: appstore.example.com

接口标准返回 JSON 结构如下:

json 复制代码
{
    "code": 0,
    "data": {
        "list": [
            {
                "appId": "com.tencent.mm",
                "appName": "微信",
                "developer": "Tencent",
                "version": "8.0.44",
                "fileSize": "258MB",
                "score": 4.8,
                "downloadCount": "100亿次+",
                "description": "...",
                "iconUrl": "https://...",
                "downloadUrl": "https://..."
            }
        ],
        "total": 150,
        "page": 1
    }
}

结构化 JSON 接口无需编写复杂网页选择器,可直接通过字典路径精准提取字段,大幅简化数据解析逻辑。

三、完整代码实现

3.1 基础配置与代理通用配置

为规避单IP高频请求导致的封禁问题,本项目接入通用动态代理IP,通过代理池轮换请求出口IP,降低风控拦截概率。删除商业化代理冗余介绍,采用通用代理配置格式,适配各类隧道代理服务。

python 复制代码
import requests
import json
import time
import random
import re
import os
import pandas as pd
from urllib.parse import quote

# ==================== 基础配置区 ====================
# 应用商店 API 基础地址(替换为目标应用商店的实际接口)
BASE_URL = "https://appstore.example.com/api"

# 通用标准化请求头,模拟真实浏览器访问
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    'Referer': 'https://appstore.example.com/',
}

# 通用代理IP配置(动态隧道代理,可替换为任意代理服务商参数)
PROXY_CONFIG = {
    "host": "代理地址",
    "port": "代理端口",
    "user": "代理账号",
    "pass": "代理密码"
}

# 组装代理请求格式
proxy_meta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % PROXY_CONFIG
PROXIES = {"http": proxy_meta, "https": proxy_meta}

# 爬虫业务配置
MAX_PAGE = 5        # 每个关键词爬取的最大页数
PAGE_SIZE = 20      # 每页数据量
SAVE_DIR = "app_data"
# ================================================

代理技术说明:本项目采用亿牛云隧道动态代理模式,依托代理池实现请求IP自动轮换,适配无状态的Requests接口请求;支持高并发采集,毫秒级切换出口IP,有效规避IP封禁、频率限制等反爬拦截。

3.2 请求构造:搜索应用列表

基于GET请求构造搜索接口请求,加入随机隧道参数、请求休眠策略,优化反爬效果;同时增加异常捕获,适配429限流、403拒绝等异常状态码。

python 复制代码
def search_apps(keyword, max_pages=5, use_proxy=True):
    """搜索应用商店,提取应用列表"""
    all_apps = []

    for page in range(1, max_pages + 1):
        # 构造请求参数
        params = {
            'keyword': keyword,
            'page': page,
            'size': PAGE_SIZE,
        }

        # 随机隧道标识,实现请求IP差异化
        req_headers = HEADERS.copy()
        if use_proxy:
            tunnel = random.randint(1, 10000)
            req_headers['Proxy-Tunnel'] = str(tunnel)

        try:
            resp = requests.get(
                f"{BASE_URL}/search",
                params=params,
                headers=req_headers,
                proxies=PROXIES if use_proxy else None,
                timeout=15
            )

            if resp.status_code == 429:
                print(f"  [{keyword}] 第{page}页请求过快 (429)")
                time.sleep(3)
                continue

            if resp.status_code == 403:
                print(f"  [{keyword}] 第{page}页访问拒绝 (403)")
                continue

            resp.raise_for_status()
            data = resp.json()

            # 解析应用列表
            app_list = data.get('data', {}).get('list', [])

            if not app_list:
                print(f"  [{keyword}] 第{page}页无数据,停止翻页")
                break

            for app in app_list:
                all_apps.append(parse_app_item(app, keyword))

            print(f"  [{keyword}] 第{page}页: {len(app_list)} 个应用")

        except requests.exceptions.JSONDecodeError:
            print(f"  [{keyword}] 第{page}页返回非 JSON")
            # 尝试 HTML 解析作为降级方案
            html_apps = parse_html_results(resp.text, keyword)
            all_apps.extend(html_apps)
        except Exception as e:
            print(f"  [{keyword}] 第{page}页异常: {e}")

        time.sleep(random.uniform(1, 3))

    print(f"[{keyword}] 搜索完成,共 {len(all_apps)} 个应用")
    return all_apps

请求构造核心要点

  1. 搜索接口采用 GET 请求,核心入参包含关键词、页码、分页数量;
  2. 生成随机隧道参数,配合代理池实现每次请求出口IP不同;
  3. 增设异常降级策略,接口返回非JSON格式时,自动切换HTML解析方案。

3.3 数据解析:JSON 结构化提取

针对应用商店字段杂乱、格式不统一的问题,封装通用解析函数,做字段兼容、数据标准化清洗,统一输出规范格式数据。

python 复制代码
def parse_app_item(app_data, keyword):
    """解析单个应用数据,统一字段格式"""
    return {
        'keyword': keyword,
        'app_id': app_data.get('appId', '') or app_data.get('package_name', ''),
        'app_name': app_data.get('appName', '') or app_data.get('title', ''),
        'developer': app_data.get('developer', '') or app_data.get('author', ''),
        'category': app_data.get('category', ''),
        'version': app_data.get('version', ''),
        'file_size': normalize_size(app_data.get('fileSize', '')),
        'score': safe_float(app_data.get('score', 0)),
        'download_count': normalize_download_count(app_data.get('downloadCount', '0')),
        'description': clean_text(app_data.get('description', '')),
        'icon_url': app_data.get('iconUrl', '') or app_data.get('icon', ''),
        'download_url': app_data.get('downloadUrl', '') or app_data.get('apk_url', ''),
        'update_time': app_data.get('updateTime', '') or app_data.get('update_date', ''),
    }

# --- 通用数据清洗工具函数 ---
def normalize_size(size_str):
    """统一文件大小格式:'258MB' → 258"""
    if not size_str:
        return 0
    match = re.search(r'([\d.]+)', str(size_str))
    return float(match.group(1)) if match else 0

def normalize_download_count(count_str):
    """统一下载量:'100亿次+' → 10000000000"""
    if not count_str:
        return 0
    count_str = str(count_str).replace(',', '').replace(' ', '')
    if '亿' in count_str:
        num = safe_float(count_str.replace('亿', '').replace('次', '').replace('+', ''))
        return int(num * 100000000)
    elif '万' in count_str:
        num = safe_float(count_str.replace('万', '').replace('次', '').replace('+', ''))
        return int(num * 10000)
    else:
        return int(safe_float(count_str))

def safe_float(val, default=0.0):
    """安全转换为浮点数"""
    try:
        return float(val)
    except (ValueError, TypeError):
        return default

def clean_text(text):
    """清洗文本:去除 HTML 标签、多余空白"""
    if not text:
        return ''
    text = re.sub(r'<[^>]+>', '', str(text))
    text = re.sub(r'\s+', ' ', text).strip()
    return text

数据解析优化方案

数据问题 处理方式
字段名不统一 多字段兼容匹配,适配不同接口命名规则
下载量格式混乱 将万、亿等中文单位统一换算为纯数字
描述含HTML标签 正则表达式清洗冗余标签、空白字符
空值异常 设置默认值,避免程序报错中断

3.4 HTML 降级解析

部分接口风控拦截后会返回静态HTML页面,此时采用BeautifulSoup做降级解析,通过通用CSS选择器提取页面应用数据,保障爬虫稳定性。

python 复制代码
def parse_html_results(html, keyword):
    """HTML 降级解析:从页面中提取应用列表"""
    from bs4 import BeautifulSoup

    soup = BeautifulSoup(html, 'html.parser')
    results = []

    # 通用应用卡片容器选择器,适配多数应用商店页面结构
    cards = soup.select('.app-card, .app-item, [class*="appItem"]')

    for card in cards:
        try:
            app = {
                'keyword': keyword,
                'app_id': '',
                'app_name': '',
                'developer': '',
                'category': '',
                'version': '',
                'file_size': 0,
                'score': 0,
                'download_count': 0,
                'description': '',
                'icon_url': '',
                'download_url': '',
                'update_time': '',
            }

            # 提取应用名称
            name_elem = card.select_one('.app-name, .title, h2, h3')
            if name_elem:
                app['app_name'] = name_elem.get_text(strip=True)

            # 提取应用ID
            link = card.select_one('a[href*="detail"], a[href*="app"]')
            if link:
                href = link.get('href', '')
                app_id_match = re.search(r'([a-z]+\.[a-z]+\.[\w.]+)', href)
                if app_id_match:
                    app['app_id'] = app_id_match.group(1)
                app['download_url'] = href

            # 提取图标、评分、下载量
            icon = card.select_one('img')
            if icon:
                app['icon_url'] = icon.get('src', '')

            score_elem = card.select_one('.score, .rating, [class*="star"]')
            if score_elem:
                app['score'] = safe_float(score_elem.get_text(strip=True))

            download_elem = card.select_one('.download-count, .downloads')
            if download_elem:
                app['download_count'] = normalize_download_count(
                    download_elem.get_text(strip=True)
                )

            if app['app_name']:
                results.append(app)

        except Exception:
            continue

    return results

3.5 应用详情页爬取

基于应用唯一ID,请求详情接口,采集应用介绍、权限、更新日志等深度数据,完善数据维度。

python 复制代码
def fetch_app_detail(app_id, use_proxy=True):
    """获取应用详情页数据"""
    req_headers = HEADERS.copy()
    if use_proxy:
        tunnel = random.randint(1, 10000)
        req_headers['Proxy-Tunnel'] = str(tunnel)

    try:
        resp = requests.get(
            f"{BASE_URL}/detail",
            params={'appId': app_id},
            headers=req_headers,
            proxies=PROXIES if use_proxy else None,
            timeout=15
        )
        resp.raise_for_status()
        data = resp.json()

        detail = data.get('data', {})

        return {
            'app_id': app_id,
            'description_full': clean_text(detail.get('description', '')),
            'screenshots': detail.get('screenshots', []),
            'permissions': detail.get('permissions', []),
            'update_log': clean_text(detail.get('updateLog', '')),
            'min_os_version': detail.get('minOsVersion', ''),
        }

    except Exception as e:
        print(f"  详情获取失败 [{app_id}]: {e}")
        return None

3.6 批量采集与持久化存储

实现多关键词批量采集,完成数据去重、格式清洗,最终导出Excel、JSON两种通用格式文件,便于数据分析与二次使用。

python 复制代码
def crawl_keywords(keywords, max_pages=5, use_proxy=True):
    """批量采集多个关键词的应用数据"""
    all_apps = []

    for keyword in keywords:
        print(f"\n{'='*40}")
        print(f"关键词: {keyword}")
        apps = search_apps(keyword, max_pages, use_proxy)
        all_apps.extend(apps)

    # 根据app_id完成数据去重
    df = pd.DataFrame(all_apps)
    if not df.empty and 'app_id' in df.columns:
        before = len(df)
        df = df.drop_duplicates(subset=['app_id'], keep='first')
        print(f"\n去重: {before} → {len(df)}")

    # 创建保存目录
    os.makedirs(SAVE_DIR, exist_ok=True)

    # 导出Excel、JSON文件
    excel_path = os.path.join(SAVE_DIR, "app_store_data.xlsx")
    df.to_excel(excel_path, index=False)
    print(f"Excel 已保存: {excel_path}")

    json_path = os.path.join(SAVE_DIR, "app_store_data.json")
    df.to_json(json_path, orient='records', force_ascii=False, indent=2)
    print(f"JSON 已保存: {json_path}")

    return df

def main():
    # 自定义采集关键词
    keywords = ["社交", "短视频", "电商", "游戏"]

    df = crawl_keywords(
        keywords=keywords,
        max_pages=MAX_PAGE,
        use_proxy=True
    )

    # 基础数据统计
    print(f"\n{'='*40}")
    print(f"采集完成")
    print(f"应用总数: {len(df)}")
    if 'score' in df.columns:
        print(f"平均评分: {df['score'].mean():.1f}")
    if 'keyword' in df.columns:
        print(f"各关键词数量:")
        print(df['keyword'].value_counts().to_string())

if __name__ == '__main__':
    main()

四、常见异常错误处理方案

错误码/异常 报错原因 解决方案
403 接口签名缺失、权限校验失败 重新抓包补充加密请求头、签名参数
407 代理身份认证失败 核对代理账号、密码、端口配置
429 请求频率过高触发限流 增加随机休眠、降低单线程采集频率
JSON解析失败 风控拦截返回HTML页面 自动降级为网页静态解析
字段缺失 不同接口字段命名差异化 多候选字段兼容匹配,设置空值默认值

五、进阶优化方向与适用场景

5.1 技术优化方向

  1. 接口逆向加固:海外应用商店存在高强度签名加密,可通过Frida、Xposed客户端Hook技术或APK逆向工程,解析加密签名算法;
  2. 选择器维护:HTML降级解析选择器易随页面改版失效,可配置多组备用选择器,按优先级自适应匹配;
  3. 增量采集:本地存储已采集应用ID,二次运行时跳过重复数据,仅采集新增应用,提升采集效率。

5.2 业务适用场景

  1. 竞品分析:批量采集同类产品评分、下载量、更新节奏,分析竞品运营策略;
  2. 行业调研:按应用分类采集行业数据,统计应用分布、热度、用户评分;
  3. ASO优化:监控关键词搜索排名,优化应用商店上架权重。

总结

应用商店数据爬虫的核心技术逻辑为:抓包逆向定位接口、标准化构造请求、结构化清洗解析、风控策略优化。相较于传统网页爬虫,JSON接口采集方式代码简洁、稳定性更强。针对平台反爬机制,可通过动态代理池、随机请求间隔、请求头伪装等方式降低拦截概率。本文提供的通用代码无需复杂改造,替换目标接口地址与代理配置即可快速落地,适用于中小型数据采集、行业分析、技术学习等场景。同时采集过程需遵守网站robots协议,合法合规采集公开业务数据。

相关推荐
pkowner1 小时前
若依分页问题及解决方法
java·前端·算法
golang学习记1 小时前
Cursor官方团队的AI指南:Cursor Team Kit
前端·cursor
Lee川1 小时前
RAG 知识库问答:从概念到代码的完整实现
前端·人工智能·后端
2301_781571422 小时前
NumPy张量缩并怎么用_np.einsum()爱因斯坦求和约定高级索引魔法
jvm·数据库·python
Warson_L2 小时前
python集合类型-set和tuple
python
计算机安禾2 小时前
【c++面向对象编程】第22篇:输入输出运算符重载:<< 与 >> 的友元实现
java·前端·c++
zhangzhi19798155922 小时前
Agent Skills
开发语言·python
redreamSo2 小时前
14 小时烧光 200 美金:Codex 和 Claude 的 /goal 命令打开了"放手跑"模式
前端
爱码小白2 小时前
MySQL索引与SQL优化
大数据·数据库·python