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

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

检索功能该怎么写?

示例

想做一个优秀的主机游戏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