简介:面向需要批量获取 Apple Store 应用信息的开发者、市场分析师及 Python 爬虫爱好者,这套工具围绕 Apple 搜索 API 封装了一个轻量脚本,通过读取 CSV 输入文件中每行一个的应用程序 ID,自动请求并输出包含 iTunes ID、捆绑 ID、应用名称、发布者名称、主要类别等字段的 CSV,省去手动逐个查询的繁琐过程。资源压缩包仅 2KB,共包含 3 个文件:py 脚本负责核心抓取逻辑,csv 文件用于维护待查询的 App ID 列表,md 文档则对运行环境(Python 2.7)和操作流程做了说明,结构清晰。目前已有 469 人浏览学习,特别适合想快速上手 API 数据采集和小型爬虫项目的入门者。借助该工具包,读者既能获得可直接运行的抓取脚本,也能掌握 CSV 输入输出设计以及 Apple 搜索 API 的参数拼接思路,为后续扩展成更完整的应用市场数据收集工具打下基础。
1. 一键拉取 Apple Store 应用 ID:这个脚本解决什么问题
很多人以为 Apple Store 的应用程序 ID 是苹果后台下发的神秘编号,其实它只是一串公开可查的数字。做竞品分析、ASO 调研、版本排期核对时,第一步都是拿到这个 ID,再顺着它把应用名称、价格、评分、更新说明一起拉出来。手动去网页上复制一两个没问题,攒到几十上百个就彻底浪费时间了——好在苹果提供了公开的查询接口,要做的只是写一个脚本替我们完成重复劳动。applestoredata 这个方向,就是把“查 ID、按 ID 取元数据”这整件事脚本化,形成一条稳定可复现的数据链路。
这套脚本适合谁呢?移动端开发者做上架排期时,需要批量核对各版本号;运营盯竞品价格波动时,需要定期拉快照做对比;做行业盘点的人,需要一次性整理成百上千个应用。这篇文章会按一条完整路径讲:ID 从哪里来、最小脚本怎么写、批量场景怎么限速、哪些细节最容易让脚本翻车、最后怎么把它做成持续运行的数据管道。全程基于公开接口,不依赖任何私有协议,只要能发 HTTPS 请求就能跑。
2. Apple Store 应用 ID 从哪里来:公开 API 与核心字段拆解
2.1 search 与 lookup:两条获取 ID 的公开路径
苹果的 iTunes Search API 是整套脚本的地基,也是 Apple Store 应用 ID 最权威的来源。它有两条路径需要分清:search 负责“找 ID”,lookup 负责“取数据”。先说 search,它的请求形式是:
https://itunes.apple.com/search?term=微信&country=cn&entity=softwareterm 传应用名关键词,country 传商店地区码,entity 固定传 software 表示只查 iOS 应用。返回结果是一个 JSON,里面的 results 数组每个元素就是一个应用,包含 trackId(数字 ID)和 bundleId(包名)。search 的价值在于你只知道名字、不知道 ID 时,用它做初筛;但要注意它单次最多返回 200 条,且按相关性排序,不是全量结果。
再说 lookup,这是整个脚本体系的核心端点:
https://itunes.apple.com/lookup?id=284910350,362001281&country=cnid 参数支持逗号拼接,一次最多查 200 个 ID,这是苹果文档里明确写过的上限。lookup 返回的结果和 search 结构一致,但字段更完整,包含版本号、价格、评分、文件大小、更新说明等详情。它和 search 的关系可以这样理解:search 是搜索引擎,给你一批候选;lookup 是精确查询,你给 ID 它给全部细节。
| 对比项 | search | lookup |
|---|---|---|
| 核心用途 | 按关键词找应用,拿到 ID | 按 ID 查详情 |
| 单次上限 | 200 条结果 | 200 个 ID |
| 返回字段 | 基础信息 | 完整元数据 |
| 典型场景 | 新品调研、行业列表 | 批量更新、版本监控 |
我在实际使用中很少单独用 search,通常是把 search 的 results 里的 trackId 抽出来,再丢给 lookup 做二次拉取。原因很简单:search 返回的 releaseNotes、ratingCount 这些字段偶尔会被截断,而 lookup 给的是完整数据。脚本里做一个“先搜后查”的串联动作,拿到手的才是干净的数据。
2.2 响应里哪些字段值得存:唯一键、价格、评分与版本
以 lookup 为例,单个应用的返回结构大致长这样,我精简了无关字段,保留核心部分:
{ "resultCount": 1, "results": [ { "trackId": 284910350, "trackName": "Keynote", "bundleId": "com.apple.Keynote", "version": "14.2", "currentVersionReleaseDate": "2024-05-13T14:56:12Z", "price": 0, "formattedPrice": "免费", "averageUserRating": 4.5, "userRatingCount": 12840, "fileSizeBytes": "498515968", "releaseNotes": "新功能:支持协作", "sellerName": "Apple", "trackCensoredName": "Keynote" } ] }这里有几个字段需要特别说明。trackId 是应用的唯一数字标识,脚本里所有去重、归档、关联操作都应以它为主键;bundleId 是反向域名格式的包名,例如 com.tencent.xin,它在跨平台比对时更稳定,但同一个应用在部分市场可能包名不同,所以不能拿它当唯一键用。version 是当前商店版本,注意它是字符串,不同格式的版本号直接比较会出问题,这块后面避坑章细讲。
价格和评分相关字段也有坑。price 是数值,formattedPrice 是本地化字符串,两者建议都存,前者用于计算、后者用于展示。averageUserRating 是平均分,userRatingCount 是评分数,判断应用热度时更应看 count,两个都只有几百的“高分应用”参考价值远低于评分数量过万的应用。fileSizeBytes 是字符串,不是整数,要做体积排序时记得先 int() 转换,否则按字典序排出来的结果是乱的。
还有一个容易被忽略的字段是 trackCensoredName,它在敏感词过滤后会出现,平时和 trackName 一样,但在部分市场,应用名被苹果本地化改写后,两者会不一致。如果你拿 trackName 去和自家后台的名单做匹配,建议同时保留两个字段,匹配时优先用 trackId,避免白名单对不上。
3. 写能直接跑的 ID 查询脚本:Python 与 curl 两个版本
3.1 Python 版:单个查与批量查合一的实现
先给一个最小可用的 Python 脚本,核心是 lookup 请求的封装。用 requests 库,没有额外依赖,Python 3.6 以上就能跑:
import requests import time def lookup_app(app_ids, country="cn", lang="zh_cn", retries=3): """按 ID 查 Apple Store 应用信息,返回 results 列表""" id_str = ",".join([str(i).strip() for i in app_ids if str(i).strip()]) url = "https://itunes.apple.com/lookup" params = { "id": id_str, # 多个 ID 用逗号拼接,最多 200 个 "country": country, # 商店地区码,决定价格和评分地域 "lang": lang, # 返回文案语言,zh_cn 返回中文描述 } for attempt in range(retries): try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json().get("results", []) except requests.exceptions.HTTPError as e: if resp.status_code == 429: wait = 2 ** attempt # 429 限流时退避重试 print(f"触发限流,{wait}s 后重试") time.sleep(wait) continue print(f"HTTP 错误: {resp.status_code}") return [] except requests.exceptions.Timeout: print("请求超时,重试中") time.sleep(1) continue except Exception as e: print(f"请求失败: {e}") return [] return [] if __name__ == "__main__": ids = ["284910350", "362001281"] results = lookup_app(ids) for app in results: print( f"{app.get('trackId')} | {app.get('trackName')} | " f"{app.get('bundleId')} | {app.get('version')} | " f"{app.get('formattedPrice')}" )这段代码做了三件事:拼参数、发请求、拆结果。timeout 设 10 秒是因为苹果接口偶尔慢,超过这个时间直接当超时处理,而不是无限等;country 和 lang 建议固定传入,不传的话默认是美国区、英文文案,价格和货币符号都会偏;retries 做的是 HTTP 层重试,只对 429 做退避,这是为了避免把偶然的限流误判成永久失败。
这里要补充一个我在初期翻车过的点:requests 的 params 会自动做 URL 编码,所以 id 参数里即使拼了中文也无需手动处理,但当你把 URL 复制到浏览器或 curl 里调试时,要注意用百分号编码后的形式,否则可能查出来是空结果。另外,lookup 的 id 参数如果传了一个不存在的 ID,整个请求不会报错,只会返回 resultCount 为 0 的空数组,所以脚本里对空结果不要直接抛异常,先检查 ID 列表是否拼写正确。
3.2 curl + jq 轻量版与两个关键参数
Python 脚本适合放进定时任务,但你在服务器上临时验证一个 ID 时,用 curl 更快。下面这条命令我经常用来排查“是接口问题还是脚本问题”:
curl -s "https://itunes.apple.com/lookup?id=284910350&country=cn" \ | jq '.results[0] | {trackId, trackName, bundleId, version}'curl 的参数里必须注意两个地方。第一,URL 要用双引号包起来,否则 & 符号会被 shell 解释成后台执行符,导致请求只发了一半;第二,id 参数如果是多值,同样用逗号拼接,但逗号在 shell 里不需要转义。jq 的用法是在管道后面接字段过滤器,这里只取我们关心的四个字段,输出是一行干净的 JSON,比 Python 脚本适合快速确认某个应用的当前状态。
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 服务器无 Python 环境 | curl + jq | 零依赖,一条命令走完 |
| 定时任务批量拉取 | Python 脚本 | 可读写文件、可记录日志、可重试 |
| 线上接口临时排查 | curl | 排除代码因素,直接看接口返回值 |
| 一次查 100+ 个 ID | Python 脚本 | 自动拼逗号,处理 200 个上限 |
这两种版本没有优劣之分,我自己的习惯是:curl 版放在运维手册里,遇到“数据对不上”的反馈先手动跑一条,超过 50 个 ID 的批量操作全部交给 Python 版,然后在外面套一层按 ID 切片的循环,确保单次请求不超过 200 个上限。
4. Apple Store 应用 ID 批量拉取:并发、限速与重试
4.1 为什么并发一高就 429:接口限流的真实行为
单线程连续拉 1000 个 ID,每个之间隔 0.2 秒,大概率能顺畅跑完。但把同样的请求改成 10 个线程并发,通常跑不到第 100 个就开始报 HTTP 429。这不是你的 IP 被封了,而是苹果对这个公开接口做了按来源 IP 的隐式限流。它的特征很明显:某段时间内所有请求一律 429,持续几十秒到几分钟后自行恢复。这个行为在初期容易变成玄学——你以为是代码写错了,改来改去发现只要降低频率就好了。
我在实际项目中遇到过最典型的场景是:A 同学写了个多线程脚本,跑上架应用的全量快照,几十个线程一起发请求,跑完前几页之后整个脚本开始连环报错,而且后面的重试也全部失败。排查之后发现,问题不是请求头不对,也不是参数错误,纯粹是并发把自己的出口 IP 打进了限流名单。这个接口没有明文额度,也没有账号体系,所以它不会发 403 让你带鉴权重试,只会在压力过大时用 429 让你冷静。
理解这一点后,处理策略就不该是“加大并发抢时间”,而是“把并发压到接口能接受的水平”。判断标准很简单:连续跑 10 分钟,日志里 429 出现次数为 0,说明当前参数是安全的;一旦出现 429,先把并发数减半,而不是加重试次数。重试只解决偶发问题,不解决速度过快的问题。
4.2 线程数与退避策略怎么配:2 并发 + 指数退避的实测建议
基于上面的行为特征,我常用的批量拉取模板长这样。并发数固定为 2,每个失败请求按 1 秒、2 秒、4 秒的指数退避重试,最多 4 次,超时直接跳过:
import concurrent.futures import time import requests ID_LIST = [str(i) for i in range(284910350, 284910500)] # 模拟一批 ID def fetch_and_save(app_id): url = "https://itunes.apple.com/lookup" params = {"id": app_id, "country": "cn", "lang": "zh_cn"} for attempt in range(4): # 最多 4 次重试 try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json().get("results", []) except requests.exceptions.HTTPError as e: if resp.status_code == 429: wait = 2 ** attempt # 1s, 2s, 4s 指数退避 print(f"{app_id} 触发限流,等待 {wait}s") time.sleep(wait) continue return [] except requests.exceptions.Timeout: time.sleep(1) continue return [] with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: results = list(executor.map(fetch_and_save, ID_LIST)) for res in results: if res: app = res[0] print(f"{app.get('trackId')} | {app.get('trackName')} | {app.get('version')}")max_workers=2 是经验值,不是拍脑袋。对这个接口来说,2 并发能稳定维持的吞吐量大约在每秒 5 到 10 个请求,这个速度对几千个 ID 的批量任务完全够用,而且几乎不会触发限流。如果你把 max_workers 调大到 8,速度不会线性提升,反而会频繁进入退避等待,整体耗时反而更长。
指数退避里要留意两点:重试次数的上限和退避时间的上限。我这边设定 4 次重试、最长等待 8 秒,超过这个阈值就放弃这条数据,等下一轮全量任务再补采。不要做无限重试,因为当接口进入限流窗口时,重试只会延长整个任务的时间,而且 windows 里等待的请求会占用线程,让后续任务全部积压。正确的做法是:先放弃,记一笔日志,下一轮增量更新时自然会被补上。
5. 必读避坑:五类让脚本失效的边界情况
5.1 返回空结果却误判为封禁
现象:lookup 请求返回 HTTP 200,但 resultCount 是 0,results 数组为空。脚本报“该应用不存在”。原因:常见于 id 拼接错误或传入了 bundleId 而不是 trackId。你拿着 com.tencent.xin 去查,trackId 那一位根本读不到,接口不会报错,只会安静地返回空。解决:先在浏览器或 curl 里单查一次确认 ID 是否正确;如果确认 ID 没问题,再看 country 参数——部分应用在某个国家区未上架时,lookup 也会返回空。善用这个行为,它反而可以当“应用在哪个区上架了”的判断依据。
5.2 trackId 与 bundleId 全都塞进主键
现象:数据库里出现重复行,同一应用被插入了多次。原因:第一次用 trackId 做唯一键,第二次换了 bundleId 做查找条件,两个 ID 格式完全不同,去重逻辑失效。解决:库里同时保留两列,但主键只用 trackId;bundleId 单独建索引用于跨平台关联查询。脚本里要写死“lookup 请求只认 trackId”的规则,不要让调用方自由传参,否则后续维护时一定有人混着传。
5.3 country 参数不固定导致数据串区
现象:同一应用,今天查出来价格是 6 元,明天变成 0.99 美元。原因:country 缺省时,接口默认返回美国区数据;你手动指定了 cn,但脚本某个分支又没传。解决:把所有请求的 country 和 lang 都收拢到配置常量里,显式传参,不依赖接口默认值;需要多区对比时,在结果行上加上 country 字段,把不同地区的数据当成不同记录存,互不覆盖。这是做价格监控的前提,不然后续所有差值分析都是错的。
5.4 时区换算错误让版本时间错乱
现象:应用的 currentVersionReleaseDate 显示是北京时间 2024-05-14,但苹果商店页面写的是 5 月 13 日。原因:接口返回的日期是 UTC 格式,带一个 Z 后缀,直接入库时被当作本地时间用了。解决:入库前统一转成 ISO 格式带时区,或者全部转成 UTC 时间戳;展示层再做 +8 小时处理。如果只做“是否更新”的判断,建议保留 UTC 字符串直接比较,因为字符串的字典序就是时间序,不需要做换算;但要按天分组统计时,必须先把时区对齐,否则每天凌晨 8 点前发布的应用都会被划到前一天。
5.5 中文关键词在 curl 和 Python 里表现不一致
现象:Python 脚本用 search 搜中文关键词没问题,同样的关键词扔进 curl 却返回空。原因:curl 命令行里的中文没有做 URL 编码,接口收到的是乱码。解决:Python 的 requests 会自动处理编码,不需要干预;curl 调试时要手动把关键词转成百分号编码,或者用curl --data-urlencode配合-G参数让 curl 自动编码。这个坑最容易出现在“用线上日志复现问题”的场景,一个请求在代码里正常,复制到终端就失败,多半是编码环节差异导致的,不是接口问题。补充一句,search 的 term 支持中文,但结果覆盖度和英文关键词存在差异,做 ASO 调研时中英文关键词最好都跑一遍。
6. 把脚本升级成数据管道:缓存、增量与离线仓库
脚本能跑通只是第一步,实际维护中发现最大的问题是“重复拉取”。每次全量跑几千个 ID,等接口响应的时间足够静音喝杯水。后来我用 SQLite 做了本地缓存,把每次拉到的数据按 trackId 存成 JSON 快照,带一个拉取时间戳:
CREATE TABLE app_snapshot ( track_id INTEGER PRIMARY KEY, bundle_id TEXT, data_json TEXT, fetched_at TEXT DEFAULT (datetime('now')) );更新逻辑很简单:先查表,如果某 trackId 的 fetched_at 在配置的刷新周期内(比如 24 小时),就直接用本地数据,不发起网络请求;超过周期才走 lookup 拉取并覆盖旧快照。这一套做下来,增量更新的耗时会从“小时级”降到“秒级”,因为 90% 的应用数据根本没变化,不需要重复请求。用 JSON 存全量字段的好处是,即使以后要加新的元数据字段,也不需要改表结构,直接存新快照即可。
更进一步的用法是给快照加一个版本差量记录。每次拉到新 version 时,和旧快照对比,如果版本号变了,单独落一条变更记录,这条记录就可以对接你自己的告警或周报逻辑。版本号比较记住用专门的版本排序算法,不要把新版号当浮点数或整型比较,“9.0.0”在字符串序里排在“10.0.0”后面。我自己跑这套管道跑了小半年,一开始也是图省事直接用脚本裸跑,直到某天拉数拉出几十个 429 才静下心做缓存。现在整个流程只需要每天定时触发一次增量任务,配合 SQLite 里存的本地快照,一套 Apple Store 应用 ID 数据管道就算落地了。希望帮到你。
本文还有配套的精品资源,点击获取