简介面向需要批量获取 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微信countrycnentitysoftwareterm 传应用名关键词country 传商店地区码entity 固定传 software 表示只查 iOS 应用。返回结果是一个 JSON里面的 results 数组每个元素就是一个应用包含 trackId数字 ID和 bundleId包名。search 的价值在于你只知道名字、不知道 ID 时用它做初筛但要注意它单次最多返回 200 条且按相关性排序不是全量结果。再说 lookup这是整个脚本体系的核心端点https://itunes.apple.com/lookup?id284910350,362001281countrycnid 参数支持逗号拼接一次最多查 200 个 ID这是苹果文档里明确写过的上限。lookup 返回的结果和 search 结构一致但字段更完整包含版本号、价格、评分、文件大小、更新说明等详情。它和 search 的关系可以这样理解search 是搜索引擎给你一批候选lookup 是精确查询你给 ID 它给全部细节。对比项searchlookup核心用途按关键词找应用拿到 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, countrycn, langzh_cn, retries3): 按 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, paramsparams, timeout10) 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(fHTTP 错误: {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?id284910350countrycn \ | jq .results[0] | {trackId, trackName, bundleId, version}curl 的参数里必须注意两个地方。第一URL 要用双引号包起来否则 符号会被 shell 解释成后台执行符导致请求只发了一半第二id 参数如果是多值同样用逗号拼接但逗号在 shell 里不需要转义。jq 的用法是在管道后面接字段过滤器这里只取我们关心的四个字段输出是一行干净的 JSON比 Python 脚本适合快速确认某个应用的当前状态。场景推荐方式原因服务器无 Python 环境curl jq零依赖一条命令走完定时任务批量拉取Python 脚本可读写文件、可记录日志、可重试线上接口临时排查curl排除代码因素直接看接口返回值一次查 100 个 IDPython 脚本自动拼逗号处理 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, paramsparams, timeout10) 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_workers2) 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_workers2 是经验值不是拍脑袋。对这个接口来说2 并发能稳定维持的吞吐量大约在每秒 5 到 10 个请求这个速度对几千个 ID 的批量任务完全够用而且几乎不会触发限流。如果你把 max_workers 调大到 8速度不会线性提升反而会频繁进入退避等待整体耗时反而更长。指数退避里要留意两点重试次数的上限和退避时间的上限。我这边设定 4 次重试、最长等待 8 秒超过这个阈值就放弃这条数据等下一轮全量任务再补采。不要做无限重试因为当接口进入限流窗口时重试只会延长整个任务的时间而且 windows 里等待的请求会占用线程让后续任务全部积压。正确的做法是先放弃记一笔日志下一轮增量更新时自然会被补上。5. 必读避坑五类让脚本失效的边界情况5.1 返回空结果却误判为封禁现象lookup 请求返回 HTTP 200但 resultCount 是 0results 数组为空。脚本报“该应用不存在”。原因常见于 id 拼接错误或传入了 bundleId 而不是 trackId。你拿着 com.tencent.xin 去查trackId 那一位根本读不到接口不会报错只会安静地返回空。解决先在浏览器或 curl 里单查一次确认 ID 是否正确如果确认 ID 没问题再看 country 参数——部分应用在某个国家区未上架时lookup 也会返回空。善用这个行为它反而可以当“应用在哪个区上架了”的判断依据。5.2 trackId 与 bundleId 全都塞进主键现象数据库里出现重复行同一应用被插入了多次。原因第一次用 trackId 做唯一键第二次换了 bundleId 做查找条件两个 ID 格式完全不同去重逻辑失效。解决库里同时保留两列但主键只用 trackIdbundleId 单独建索引用于跨平台关联查询。脚本里要写死“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 数据管道就算落地了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?