欢迎光临
我们一直在努力

【steam接口】想用游戏名查 Steam AppID?我替你把这四个坑踩平了

事情是怎么开始的

需求很小:用户输入一个游戏名,我得给出它在 Steam 上的 AppID。

听起来一句话的事。AppID 和游戏名的对应关系,Steam 官方就有现成接口,把全量列表拉下来本地一查就完了。我当时也是这么想的,结果从「拉列表」这一步开始,一个接一个地翻车。

下面这四个坑,按我踩的顺序讲。代码是从一个 Electron + Node 项目里抠出来的,能直接抄。

坑零:你搜到的那个接口,已经没了

网上一搜「Steam 全部游戏列表」,十篇有九篇给你这个:

https://api.steampowered.com/ISteamApps/GetAppList/v2/

不用 Key,一把梭返回所有 app,简单到不行。很多老教程、老库都还在用它。

我也照着抄了,然后收到一句冷冰冰的回复:

Method 'GetAppList' not found in interface 'ISteamApps'

HTTP 404。换 v0002、换 v1、去掉结尾斜杠,全是 404。

一开始我怀疑是自己网络或者域名被墙,于是拿同一个接口下的另一个方法 ISteamApps/UpToDateCheck 试了试,它好好地返回了 200。说明服务器活得好好的,就是 GetAppList 这个方法被摘掉了。

再用 GetSupportedAPIList 查了下 ISteamApps 接口现在还剩哪些方法,清单里只有 GetSDRConfig、GetServersAtAddress、UpToDateCheck 三个,确实没有 GetAppList 的影子。官方文档上那行小字写的是「Deprecated」,但实际体验已经是「调用即 404」。弃用和下线之间,它选择了后者。

那现在该用哪个?只有这一个:

https://api.steampowered.com/IStoreService/GetAppList/v1/

新旧对比一眼看完:

维度ISteamApps/GetAppList/v2(已下线)IStoreService/GetAppList/v1(在用)
能不能调 404 Method not found 能
要不要 Key 曾经不要 要
能不能只要游戏 不能,全都混在一起 能,include_games/dlc/… 自己挑
一次给多少 全给 分页,单页上限约 5 万

记住一句话就行:别再用 ISteamApps/GetAppList,换 IStoreService 那个。代价是得带 Key、得自己翻页,下面就是翻页的坑。 在这里插入图片描述

坑一:一次请求,拿不全

新接口不会一口气把几十万个 app 倒给你,它分页。每页最多五万条左右,靠两个字段决定要不要翻下一页:

  • response.have_more_results 是 true,说明后面还有;
  • response.last_appid 是这一页最后一个 appid,把它当作下一页的起点传回去。

先把请求 URL 拼好。我只要游戏和 DLC,软件、视频、硬件这些一律不要,省得后面还得自己筛:

const APP_LIST_PAGE_SIZE = 50000

function buildStoreAppListUrl(baseUrl: string, apiKey: string, lastAppId: number): string {
const params = new URLSearchParams({
key: apiKey,
include_games: '1',
include_dlc: '1',
include_software: '0',
include_videos: '0',
include_hardware: '0',
max_results: String(APP_LIST_PAGE_SIZE),
format: 'json',
})
if (lastAppId > 0) params.set('last_appid', String(lastAppId))
return `${baseUrl}?${params.toString()}`
}

然后是翻页循环。这里我多写了两道看着没用、其实救命的保险:

const MAX_APP_LIST_PAGES = 20 // 页数硬上限

async function fetchSteamAppList(apiKey: string): Promise<SteamAppListItem[]> {
const apps: SteamAppListItem[] = []
let lastAppId = 0
for (let page = 0; page < MAX_APP_LIST_PAGES; page++) {
const json = await fetchSteamAppListJson(apiKey, lastAppId)
const pageApps = json?.response?.apps
if (!Array.isArray(pageApps)) {
throw new Error('Steam AppList 返回结构异常:response.apps 不是数组')
}
apps.push(…pageApps.map(parseSteamApp).filter(Boolean))

if (!json.response?.have_more_results) break
const next = json.response.last_appid ?? 0
if (!Number.isInteger(next) || next <= lastAppId) break // 不前进就停
lastAppId = next
}
return apps
}

为什么要这两道保险?

一是那个 MAX_APP_LIST_PAGES。万一接口抽风,have_more_results 永远是 true,没有它你就是个无限请求机器,配额分分钟刷爆。

二是 next <= lastAppId 就 break。正常情况下 last_appid 应该一页比一页大,但你不能赌它一定如此。一旦某次返回的 last_appid 没往前走,循环就会卡在同一页反复拉同样的数据,外面看着像卡死,里面在空转。把「下一页起点必须严格变大」这个常识写成代码,比相信接口靠谱。

坑二:几十万条数据,每次都重拉就是自找麻烦

这个接口拉的是整个 Steam 商店目录。光是游戏加 DLC 就好几十万条,落到本地 JSON 文件能撑到九十万行。用户每查一个名字就重新拉一遍全量,慢不说,配额也扛不住。

所以正确姿势是落盘缓存,按需刷新:

平时查询直接读本地那份缓存来匹配,根本不碰网络。只有用户手动点「刷新」,或者本地压根没缓存的时候,才真去拉全量。拉回来之后也不是整个覆盖,而是和旧数据按 appid 合并,新的盖旧的,没动过的保留。

合并就用一个 Map 去重,appid 作 key:

function mergeSteamAppListCache(
current: SteamAppListCache | null,
remoteApps: SteamAppListItem[],
now: Date,
): SteamAppListCache {
const byAppId = new Map<number, SteamAppListItem>()
for (const app of current?.apps ?? []) {
if (isValidSteamApp(app)) byAppId.set(app.appid, { appid: app.appid, name: app.name })
}
for (const app of remoteApps) {
if (isValidSteamApp(app)) byAppId.set(app.appid, { appid: app.appid, name: app.name })
}
const apps = […byAppId.values()].sort((a, b) => a.appid – b.appid)
return { version: 1, sourceUrl: STEAM_APP_LIST_SOURCE_URL, updatedAt: now.toISOString(), total: apps.length, apps }
}

顺手提一个容易忽略的点:商店目录里混着不少 appid 是 0、或者名字干脆是空的垃圾条目,入库前得筛掉,不然查询结果里会冒出一堆「无名氏」:

function isValidSteamApp(item: SteamAppListItem): boolean {
return Number.isInteger(item.appid) && item.appid > 0 && item.name.trim() !== ''
}

还有一点关乎体验:刷新失败别让整个功能跟着崩。网络拉取挂了但本地有旧缓存,那就用旧的,顶多附一句提示告诉用户「数据没刷新成」。让用户拿着稍旧的数据照常查,总比甩他一个红色报错强:

} catch (err) {
if (!cache) throw new Error(`Failed to fetch Steam app list: ${err}`)
warning = `刷新 Steam AppList 失败,已使用本地缓存:${stringifyError(err)}`
}

坑三:浏览器能开,代码偏说证书不对

数据能拉了,换台机器又翻车,这回是这句:

unable to verify the first certificate

最让人挠头的是,同一台机器,浏览器打开这个接口好好的,PowerShell 也能通,唯独 Node 代码报证书错误。

原因在于信任源不一样。浏览器、PowerShell 这些走的是 Windows 的证书库,而 Node 用的是自己内置的一份 CA 名单,它压根不读 Windows 证书库。机器上一旦装了会拦截 HTTPS 的东西,比如公司代理或者某些杀毒软件,它们会往系统里塞一张自己的根证书。Windows 认这张证书,所以浏览器没事;Node 不认,于是验签到一半断了,报「第一张证书都验不过」。

如果你的程序是 Electron,最省事的办法是用它的 net 模块兜底。net.fetch 走的是 Chromium 的网络栈,跟浏览器一个待遇,读系统证书库、认系统代理。于是思路就清楚了:正常先用 Node 的 https 发请求,只有撞上证书错误时才退一步用 net.fetch 重发。

GetAppList 这里我还多加了一层。因为除了证书问题,v1 这个路径偶尔会在某些代理或网关后面被改写、变得不可达,所以再准备一个老版本 v0001 的端点垫底。连起来是三级:

const PRIMARY = 'https://api.steampowered.com/IStoreService/GetAppList/v1/'
const FALLBACK = 'https://api.steampowered.com/IStoreService/GetAppList/v0001/'

async function fetchSteamAppListJson(apiKey: string, lastAppId: number) {
const primaryUrl = buildStoreAppListUrl(PRIMARY, apiKey, lastAppId)
const fallbackUrl = buildStoreAppListUrl(FALLBACK, apiKey, lastAppId)
try {
return await fetchJson(primaryUrl) // 1) 先用 Node https
} catch (err) {
if (!isTlsCertificateError(err)) throw err // 不是证书错,照常抛
try {
return await fallbackFetchJson(primaryUrl, err) // 2) 证书错,net.fetch 重发同一个 URL
} catch (fallbackErr) {
if (!isHttp404Error(fallbackErr)) throw new Error(/* 两步都挂 */)
return await fallbackFetchJson(fallbackUrl, err) // 3) 还 404,换 v0001 端点
}
}
}

判断「这是不是证书错误」要精准,别把超时、404 也当证书问题给吞了:

export function isTlsCertificateError(err: unknown): boolean {
const code = isNodeError(err) ? err.code : undefined
if (code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
code === 'SELF_SIGNED_CERT_IN_CHAIN' ||
code === 'DEPTH_ZERO_SELF_SIGNED_CERT') return true
return /unable to verify the first certificate/i.test(String(err))
}

顺带提一句反面教材:网上有人教你设 rejectUnauthorized: false 把证书校验直接关掉。这等于把家门口的报警器拆了图清静,真有中间人攻击你也察觉不到。能用 net.fetch 就别走这条路。

最后一步:名字怎么查才不死板

列表到手,剩下就是拿用户输入的名字去匹配。

直接 === 比是不行的。大小写、空格、各种标点和商标符号都会让「Half-Life」匹配不上「half life」。所以先把名字归一化:统一小写,把所有非字母数字的字符都替换成空格再合并:

function normalizeSteamAppName(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[^\\p{L}\\p{N}]+/gu, ' ') // 非字母数字 → 空格
.replace(/\\s+/g, ' ')
.trim()
}

归一化之后再分三档打分:精确相等的排最前,前缀命中的次之,包含关系的垫后;同一档里名字越短越靠前。这样查「Portal」,「Portal」本体稳稳排第一,「Portal 2」「Portal Knights」乖乖跟在后面,不会喧宾夺主。

写在最后

回头看,这个「用名字查 AppID」的小需求,真正花时间的全是接口和环境的脾气,跟业务逻辑没多大关系。把几个要点收一收:

老接口 ISteamApps/GetAppList 别再碰了,调用就是 404,认准 IStoreService/GetAppList/v1。它要 Key、要翻页,翻页循环记得加「页数上限」和「last_appid 必须变大」两道保险。几十万条数据务必本地缓存、增量合并、筛掉垃圾条目,刷新失败也要能退回旧缓存继续用。证书报错八成是 Node 不读系统证书库撞上了代理或杀软,Electron 里用 net.fetch 兜底最干净。查名字之前先归一化,再分精确、前缀、包含三档排。

参考文档

  • Steamworks 官方文档 · ISteamApps 接口(GetAppList 的弃用说明就在这):https://partner.steamgames.com/doc/webapi/isteamapps
  • Steam Web API 接口速查(社区维护,能直接看每个接口的参数和返回):https://steamapi.xpaw.me/
赞(0)
未经允许不得转载:171主机测评 » 【steam接口】想用游戏名查 Steam AppID?我替你把这四个坑踩平了
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址