全部 / 技术交流 · 2023年1月11日 0

Steam游戏信息查询程序设计(node.js)

本文最后更新于 415 天前,其中的信息可能已经有所发展或改变。

检索功能该怎么写?

示例

想做一个优秀的主机游戏bot,我认为游戏库查询功能必不可少,简单地发送游戏名就可以搜到所有的游戏信息。

以下是框架与源代码:

GitHub - koishijs/koishi: Cross-platform chatbot framework made with love
Cross-platform chatbot framework made with love. Contribute to koishijs/koishi development by creating an account on GitHub.
github.com
GitHub - CelestialRipple/Gameinfo: 游戏信息查询
游戏信息查询. Contribute to CelestialRipple/Gameinfo development by creating an account on GitHub.
github.com
GitHub - CelestialRipple/koishi-plugin-steaminfo: Koishi框架Steam游戏查询
Koishi框架Steam游戏查询. Contribute to CelestialRipple/koishi-plugin-steaminfo development by creating an account on GitHub.
github.com

目标

当收到指令“查询+游戏名”时,自动查询Steam游戏库并输出相关信息。

  • 模糊匹配,不需要输入完整的游戏名。
  • 中文检索,即使输入中文名也可以正常查找。
  • 允许限定搜索范围,例如是否包含DLC、特典等。
  • 支持显示不同地区的游戏售价,描述使用中文。
  • 受到DNS污染时,支持自动重试。
配置项

作为开源插件发布,它的可以让用户配置基本功能:

  • 访问Steam的Cookies
  • 过滤的关键词
  • 需要的游戏信息

设计思路

Steam官方api

查询Steam库的信息,可以解析html网页或是使用API接口,Steam是具有公共api的,显然选择后者更方便。

Steam Web API Documentation
An automatically generated list of Steam Web API interfaces, methods and parameters. Allows you to craft requests in the browser.
steamapi.xpaw.me

浏览这份API使用文档后,我从其中找到了几个可以实现目标的接口:“http://api.steampowered.com/ISteamApps/GetAppList/v0001”、“http://store.steampowered.com/api/appdetails/?appids=${appid}”。使用接口检索游戏的方式很简单,首先调取全部的游戏列表,在其中检索所需游戏的英文名称,获取其appid后作为第二个接口的请求参数。

接口文档

使用“https://api.steampowered.com/IStoreService/GetAppList/v1/?key=${steam_key}&max_results=50000&last_appid=${last_appid}”也可以很方便地对列表进行筛选。

英文检索方式

{
	"appid": 444330,
	"name": "Battleships At Dawn!"
},
{
	"appid": 444350,
	"name": "HACK_IT"
},
{
	"appid": 444360,
	"name": "Industry Manager: Future Technologies - Awesome Products Pack"
},
{
	"appid": 444390,
	"name": "Neo Aquarium Soundtrack"
},
{
	"appid": 444410,
	"name": "Find Out"
},
{
	"appid": 444420,
	"name": "24 Hours 'til Rescue"
JSON

从Applist接口返回的json数据如上,程序的目标是找到需要游戏的appid。

ctx
.command("查询 <text:text>", "检索游戏名称")
    .action(async ({ session }, text) => {     
     let response;
  response = await axios.get('http://api.steampowered.com/ISteamApps/GetAppList/v0001');
      if (response.status === 200) {
    appList = response.data.applist.apps.app;
        let matchingApps = appList.filter((app) => app.name.match(new RegExp(`\\b(${text.split(' ').join('.*')})\\b`, 'i')));
if (config.keywords.length > 0) {
  // 过滤掉与关键词匹配的游戏信息
  const filteredApps = matchingApps.filter(app => !config.keywords.some(keyword => new RegExp(`\\b(${keyword.split(' ').join('.*')}).*\\b`, 'i').test(app.name)));
  matchingApps = filteredApps;
}
JavaScript

上述代码是查询apply函数的一个片段,难点在于如何对英文进行检索。理想中的检索目标不能规定必须输入完整名称,大小写区别,特殊符号等。例如输入“persona™”,可以查询到“Persona 5 Royal”。

RegExp(`\\b(${text.split(' ').join('.*')})\\b`, 'i'))
JavaScript

我的解决方式是先对输入的文本去除特殊符号,然后使用以上正则匹配进行检索,它可能无法做到词汇联想,但已经可以实现基本的查询功能。

数据处理

在上述代码中,程序已经从外部映入了数组config.keywords,使用这个数组对查询到的数据进行了第一层的过滤。

const filteredApps = matchingApps.filter(app => !config.keywords.some(keyword => new RegExp(`\\b(${keyword.split(' ').join('.*')}).*\\b`, 'i').test(app.name)));
matchingApps = filteredApps;
JavaScript

进一步对matchingApps进行处理:

if (matchingApps.length > 50) {
            const newMatchingApps = matchingApps.splice(0, 30);
          return `检索到的游戏过多,已为你返回前30个相关性最强的结果。\n${newMatchingApps.map((app) => `游戏名称:${app.name},appid:${app.appid}`).join('\n')}\n请输入你想要查询游戏的appid,例:“id 1941401”`;
        } else if (matchingApps.length > 0) {
          return `为实现最佳查询效果,请使用英文输入。\n查询到 ${matchingApps.length} 个游戏:\n${matchingApps.map((app) => `游戏名称:${app.name},appid:${app.appid}`).join('\n')}\n请输入你想要查询游戏的appid,例:“id 1941401”`;
        } else {
          return '为实现最佳查询效果,请使用英文输入。\n未查询到匹配的游戏';
        }
      } else {
        return `请求失败:${response.statusText}`;
      }
JavaScript

为防止游戏名过短而造成结果过多,首先需要判断matchingApps.length的值,如果结果过长必须进行切片处理,防止消息刷屏。

写到这里似乎只实现了返回游戏的appid,并不能获取任何直观的信息,继续:

const koishi_1 = require("koishi"); 
ctx
.command("id <appid>", "检索appid")
.alias('id:', 'ID' , 'appid')
JavaScript

新建“id”命令用于使用id获取详细信息。

let response;
      try {
    const link = `https://store.steampowered.com/api/appdetails/?appids=${appid}`;
    response = await axios.get(link, {
   headers: {
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Cookie' : config.Cookie
  },
});
} catch (error) {
  // 处理错误
  console.error(error);
  return '请求出现错误,无法获取数据,请检查bot所处网络环境,可以尝试访问https://store.steampowered.com/api/appdetails/?appids=620进行验证';
}
JavaScript

使用“https://store.steampowered.com/api/appdetails/?appids=”接口时,我遇到了几个问题:

  • 使用浏览器模拟get请求时,返回的结果为中文,但在主机上使用axios.get时需要加上请求头'Accept-Language': 'zh-CN,zh;q=0.9'。
  • 同样使用axios.get时,steam返回了美区售价。打开浏览器请求时,返回了国区售价,猜测是Cookie问题,打开无痕浏览后果然也返回了美区售价,因此我在请求头中加入了可自行配置的Cookie选项'Cookie' : config.Cookie。
  • 使用“https://store.steampowered.com/api/appdetails”接口时,遇到了DNS污染的情况,可能需要配置科学上网环境,对此程序使用了try方法返回报错。

后续收到插件使用者的反馈,查询游戏信息时偶尔会请求失败,对此引入axios-retry依赖:

const axiosRetry = require('axios-retry');

// Enable axios-retry
axiosRetry(axios, { retries: 3 });
JavaScript

请求成功后,继续处理数据:

if (response.status === 200) {
  if (response.data[appid] && response.data[appid].data) {
    const data = response.data[appid].data;
    const values = [];
    
    if (data.name) {
      values.push(`名称:${data.name}`);
    }

 if (data.type && config.type) {
      values.push(`类型:${data.type}`);
    }
    if (data.short_description && config.short_description) {
      values.push(`简介:${data.short_description}`);
    }
    if (data.detailed_description && config.detailed_description) {
      values.push(`详细介绍:${data.detailed_description.replace(/<br\/?>/g, '\n')}\n\n`);
    }
    if (data.genres && config.genres) {
  const genreDescriptions = data.genres.map(genre => genre.description).join(', ');
  values.push(`分类:${genreDescriptions}`);
}

if (data.categories && config.categories ) {
  const categoriesDescriptions = data.categories.map(genre => genre.description).join(', ');
  values.push(`标签:${categoriesDescriptions}`);
}

   if (data.supported_languages && config.supported_languages) {
  values.push(`语言:${data.supported_languages.replace(/<br\/?>/g, '。注:')}`);
}

    if (data.platforms && data.platforms.mac && config.platforms) {
      values.push(`Mac支持:${data.platforms.mac}`);
    }
if (data.pc_requirements.recommended && config.pc_requirements) {
      values.push(`PC配置:${data.pc_requirements.recommended.replace(/<br\/?>/g, ',')}\n\n`);
    }
    
if (data.price_overview && config.initial_formatted) {
    if(data.price_overview.initial_formatted){
      values.push(`原始售价:${data.price_overview.initial_formatted}`);
    }
    }
    
    if (data.price_overview && config.final_formatted) {
      values.push(`\n当前售价:${data.price_overview.final_formatted}`);
    }
    
    if (data.metacritic && config.metacritic) {
      values.push(`MC均分:${data.metacritic.score}`);
    }
    if (data.achievements  && config.achievements) {
      values.push(`成就总数:${data.achievements.total}`);
    }
    if (data.release_date && config.release_date) {
      values.push(`发售日期:${data.release_date.date}`);
    }
    if (response.status === 200) {
      values.push(`官方链接:https://store.steampowered.com/app/${appid}`);
    }

    let imageData = null;
    let image1 = null;
    let image2 = null;

    if (data.header_image) {
      imageData = await ctx.http.get(data.header_image, { responseType: 'arraybuffer' });
    }

    if (data.screenshots && data.screenshots.length >= 1 && data.screenshots[0].path_full) {
      image1 = await ctx.http.get(data.screenshots[0].path_full, { responseType: 'arraybuffer' });
}

if (data.screenshots && data.screenshots.length >= 2 && data.screenshots[1].path_full) {
image2 = await ctx.http.get(data.screenshots[1].path_full, { responseType: 'arraybuffer' });
}
// 将图片数据拼接在返回的字符串中
let result = values.join('\n');

if (imageData) {
  result += koishi_1.segment.image(imageData);
}

if (image1) {
  result += koishi_1.segment.image(image1);
}

if (image2) {
  result += koishi_1.segment.image(image2);
}

return result;
    
  
  }
    } else {
      return `请求失败:${response.statusText}`;
    }
  });
JavaScript

对数据进行处理时,最常碰见的就是undefined报错,因为并未所有游戏都包含了配置项,例如没有打折的游戏就不存在“data.price_overview.initial_formatted”,因此我使用values.push方法,先判断数据是否存在然后再拼接字符串。

据反馈,当配置国区Cookies后,调用某些锁区游戏(点名科乐美)数据时,steam接口可能会直接拒绝。因此加上报错:

else {
    return 'appid不存在或该游戏锁区,请检查您配置的Cookie是否有权限访问该游戏。';
    }
JavaScript

至此id命令编写完毕,但到此为止的话,查一个游戏岂不是还要使用两个命令?那为何不打开steam自己看...必须想办法精简这些步骤!

为此,我使用了koishi的库函数session.prompt、session.execute。

session.prompt((session) => {
    const appid = session.content.match(/\d+/);
if (appid) {
  return session.execute(`id ${appid[0]}`);
} else {
  return session.send("请输入正确的appid");
}
JavaScript

当用户发送查询命令后,无数再输入id指令,输入纯数字即可完成查询。

else if (matchingApps.length === 1) {
  const appid = matchingApps[0].appid;
  session.execute(`id ${appid}`);
JavaScript

如果筛选后的结果只有一个,那么自动执行id指令,配合上关键词过滤,一行指令即可完成游戏的查询!

文章分页: 1 2 3 下一页
页面: 1 2 3