天涯旅店

利用网易 BUFF 挂刀获取 Steam 折扣(爬虫篇)

刚学会浏览器插件挂刀那会刷饰品上瘾,感觉每买一个便宜饰品都是赚钱,闲下来没事就刷网易 BUFF。

周末公司培训,无聊的时候继续翻饰品列表,翻的时候就想,这么机械的行为,能不能改成自动的呢?

代码半自动挂刀


有想法就马上干,虽然 Python 是爬虫专用语言,但是个人 Python 水平太低,所以本文代码都是 Java 技术栈。等学会 Python 就更新 Python 源码。

因为本身这事有点灰色地带,就不贴完整代码了。代码片段主要提供思路,整体步骤其实挺简单的,主要用到的类库:

  • Apache HttpClient
  • Fastjson
  • Lombok
  • Google Guava

查询饰品列表

第一步,分页查询饰品列表,然后直接循环就行了。

private void listItem() throws IOException {
    //网易buff的饰品列表接口,page_size最大就80,再大也是返回80条
    String buffUrl = "https://buff.163.com/api/market/goods?game=csgo&page_num=1&page_size=80";
    //http库
    CloseableHttpClient httpClient = HttpClients.custom()
            .setDefaultRequestConfig(RequestConfig.custom()
                    .setCookieSpec(CookieSpecs.STANDARD).build()).build();
    //基本的http参数
    HttpGet httpGet = new HttpGet(buffUrl);
    httpGet.addHeader("Accept", "application/json, text/javascript, */*; q=0.01");
    httpGet.addHeader("Cache-Control", "no-cache");
    httpGet.addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36");
    //发起请求,得到返回的集合数据
    CloseableHttpResponse response = httpClient.execute(httpGet);
    JSONObject jsonObject = JSON.parseObject(EntityUtils.toString(response.getEntity()));
    List<JSONObject> itemList = jsonObject.getJSONObject("data").getJSONArray("items").toJavaList(JSONObject.class);
}

看到这里可能有疑问,为什么查询列表的参数是固定的?

不是代码省略了构造翻页的步骤,而是只能这么查询。因为 BUFF 网站有权限设计,未登录用户只能查列表的第一页,并且不能带多余的查询参数。

而一旦登录,正如前文手动教程所说,爬虫程序超高频率的请求速度和一天内超大的请求量,会直接被封号

所以不登录查询,是最保险的,换 IP 成本比换号低多了。如果想要查询全部列表,可以设置 Cookie 登录。

筛选查询结果

得到列表以后,开始按自己要求遍历,关键是筛选价格,太便宜的买卖时间成本太高,太贵的风险大。
确定一个最高挂刀比例,低于这个比例的都是我们的目标数据。

/**
 * 挂刀比例
 */
private static final Double SALE_PERCENT = 0.7;

private void dataProcessing(List<JSONObject> itemList) {
    for (JSONObject object : itemList) {
        //从json获取需要的参数
        double price = object.getDouble("sell_min_price");
        double steamPrice = object.getJSONObject("goods_info").getDouble("steam_price_cny");
        String id = object.getString("id");
        //筛掉价格过低和比例太高的,steamPrice除以0.87才是扣除手续费获得的余额
        if (price < 1 || price / (steamPrice * 0.87) > SALE_PERCENT) {
            continue;
        }
        //校验steam价格
        if (steamPercentTooExpensive(object)) {
            continue;
        }
        //校验buff真实价格
        if (buffPriceIsFake(object)) {
            continue;
        }
        //TODO 下单购买,没做这个
        buyItem(object);
        //发送提醒短信
        sendMail(object);
        log.info("find item id={}, name={}", id, object.getString("name"));
    }
}

得到的 JSON 串如下,只用到了很少一部分字段,里面的数据还有待发掘。省略了一些展示信息,有兴趣可以自己调接口查询。

{
  "appid": 730,
  "bookmarked": false,
  "buy_max_price": "900",
  "buy_num": 11,
  "can_bargain": true,
  "can_search_by_tournament": false,
  "description": null,
  "game": "csgo",
  "goods_info": {
    "icon_url": "https://g.fp.ps.netease.com/market/file/5b66b88ba7f252ad93695704qVT0q4d0",
    "item_id": null,
    "original_icon_url": "https://g.fp.ps.netease.com/market/file/5b66b88ba7f252918362aa98pQW49zw7",
    "steam_price": "179.2",
    "steam_price_cny": "1158.04"
  },
  "has_buff_price_history": true,
  "id": 759454,
  "market_hash_name": "★ Stiletto Knife | Crimson Web (Field-Tested)",
  "market_min_price": "0",
  "name": "短剑(★) | 深红之网 (久经沙场)",
  "quick_price": "939",
  "sell_min_price": "939.5",
  "sell_num": 174,
  "sell_reference_price": "939.5",
  "short_name": "短剑(★) | 深红之网",
  "steam_market_url": "https://steamcommunity.com/market/listings/730/%E2%98%85%20Stiletto%20Knife%20%7C%20Crimson%20Web%20%28Field-Tested%29",
  "transacted_num": 0
}

到 Steam 和 BUFF 详情页校验数据

得到列表的数据了,可以直接使用,但是强烈建议到 Steam 获取求购比例,到 buff 详情页校验数据真实性。
这两个步骤可以用两个线程处理,实际上 BUFF 的处理很快,Steam 要慢不少,聊胜于无。

Steam 查询求购比例

从 BUFF 的数据里得到 Steam 市场的地址,获取网页的 html。 因为求购列表价格是独立加载的,无法从 html 里直接得到,必须还要再查询求购价格。

从 html 文件里得到查询求购价的 id,调接口获取求购价格。示例只查询了最高价,正如前篇的经验所说,求购价经常有人设套,所以如果要求数据准确的话,可以多查几个。

private static final Double BUY_PERCENT = 0.75;

private static boolean steamPercentTooExpensive(JSONObject jsonObject) throws IOException {
    //http库,可以复用
    CloseableHttpClient httpClient = HttpClients.custom()
            .setDefaultRequestConfig(RequestConfig.custom()
                    .setCookieSpec(CookieSpecs.STANDARD).build()).build();
    //从json获取steam市场链接
    HttpGet httpGet = new HttpGet(jsonObject.getString("steam_market_url"));
    //steam市场必须用代理才能访问
    httpGet.setConfig(RequestConfig.custom()
            .setProxy(new HttpHost("127.0.0.1", 1087, "HTTP"))
            .build());
    CloseableHttpResponse response;
    response = httpClient.execute(httpGet);
    String html = EntityUtils.toString(response.getEntity());
    //从市场页面的html里获取itemNameId,用于查询价格;这段方法比较简陋,可以优化
    String steamContent = html.substring(html.indexOf("Market_LoadOrderSpread( "));
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < 40; i++) {
        if (Character.isDigit(steamContent.charAt(i))) {
            stringBuilder.append(steamContent.charAt(i));
        }
    }
    String itemNameId = stringBuilder.toString();
    //调接口获取卖盘最高价格,也就是求购价
    String steam = "https://steamcommunity.com/market/itemordershistogram?" +
            "country=CN&language=schinese&currency=23&item_nameid=%s&two_factor=0";
    httpGet.setURI(URI.create(String.format(steam, steamId)));
    response = httpClient.execute(httpGet);
    JSONObject result = JSON.parseObject(EntityUtils.toString(response.getEntity()));
    //根据求购价,得到求购比例
    double buyPrice = Double.parseDouble(result.getString("highest_buy_order")) / 100;
    return price / (buyPrice * 0.87) > BUY_PERCENT;
}

BUFF详情页确认价格真实性

BUFF 列表的价格,因为缓存或者其他一些原因,并不是实际的值。所以以详情页的价格为准,还需要查询详情页价格列表,获取最低价格,如果差别不大,视为准确数据。

如果要做全自动程序,在这里就应该下单购买了。

private static boolean buffPriceIsFake(JSONObject jsonObject){
    CloseableHttpClient httpClient = HttpClients.custom()
            .setDefaultRequestConfig(RequestConfig.custom()
                    .setCookieSpec(CookieSpecs.STANDARD).build())
            .build();
    //buff详情页的售卖列表
    String url = "https://buff.163.com/api/market/goods/sell_order?" +
            "game=csgo&goods_id=%s&page_num=1&sort_by=default&mode=&allow_tradable_cooldown=1&_=%s";
    HttpGet httpGet = new HttpGet(
            String.format(url, jsonObject.getString("id"), System.currentTimeMillis()));
    CloseableHttpResponse response;
    response = httpClient.execute(httpGet);
    JSONObject jsonObject = JSON.parseObject(EntityUtils.toString(response.getEntity()));
    JSONObject item = (JSONObject) jsonObject.getJSONObject("data").getJSONArray("items").get(0);
    //比对价格真实性
    return item.getDouble("price") / jsonObject.getDouble("sell_min_price") > 1.01;
}

发邮件提醒手动下单购买

程序本身是个半自动的,所以没有自动购买和后续的能力了,需要发消息提醒自己,到 App 上去买 BUFF 的饰品。我这里用的是比较常规的邮件提醒,也可以使用短信、即时通讯软件等等。

邮件发送是个比较常见和通用的功能,实现方式就看个人了。

private static void sendMail(String content) {
    try {
        Properties mailProp = getDefaultMailProp();
        Session session = Session.getDefaultInstance(mailProp);
        Transport transport = session.getTransport();
        // 连接邮件服务器:SMTP服务器,帐号,授权码代替密码(更安全)
        transport.connect(MAIL_HOST, MAIL_SENDER, AUTHORIZATION_CODE);
        // 创建邮件对象
        MimeMessage message = new MimeMessage(session);
        // 指明邮件的发件人
        message.setFrom(new InternetAddress(MAIL_SENDER));
        // 指明邮件的收件人
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(Test.MAIL_SENDER));
        // 邮件的标题
        message.setSubject("buff mail");
        message.setContent(content, "text/html;charset=UTF-8");
        // 发送邮件
        transport.sendMessage(message, message.getAllRecipients());
        transport.close();
    } catch (Exception e) {
        log.error("send mail error ", e);
    }
}

private static Properties getDefaultMailProp() throws GeneralSecurityException {
    Properties prop = new Properties();
    prop.setProperty("mail.smtp.auth", "true");
    prop.setProperty("mail.transport.protocol", "smtp");
    prop.put("mail.smtp.ssl.enable", "true");
    MailSSLSocketFactory sf = new MailSSLSocketFactory();
    sf.setTrustAllHosts(true);
    prop.put("mail.smtp.ssl.socketFactory", sf);
    return prop;
}

另外感慨下 Python 发邮件比 Java 代码简单太多了,即便是配置更多的标准库 smtplib,也没有多少代码,跟别说封装好的 yagmail

import yagmail

## 邮箱配置
conn = yagmail.SMTP( user="user@qq.com", password="1234", host='smtp.qq.com')

## 正文
content = ['This is the body']

## 发送
conn.send('user@qq.com', 'subject', content)

后续流程

后面就是 BUFF 交易和 Steam 上架交易的过程了,半自动程序个人使用也能获取足够的余额,是否要投入更多的时间,就要看兴趣和用途了。全自动程序可能就会被于其他的目的,而不仅仅是找点折扣了。后续流程比较复杂,本文也就是抛砖引玉,还需各位自己摸索研究。

如果实现了这些,再接入其他几家平台,增加游戏种类,扩大饰品来源,手握一个低价余额收割机,日子就越来越有判头了

没有标签
首页      未分类      利用网易 BUFF 挂刀获取 Steam 折扣(爬虫篇)

发表回复

textsms
account_circle
email

天涯旅店

利用网易 BUFF 挂刀获取 Steam 折扣(爬虫篇)
刚学会浏览器插件挂刀那会刷饰品上瘾,感觉每买一个便宜饰品都是赚钱,闲下来没事就刷网易 BUFF。 周末公司培训,无聊的时候继续翻饰品列表,翻的时候就想,这么机械的行为,能不能改…
扫描二维码继续阅读
2021-04-20