刚学会浏览器插件挂刀那会刷饰品上瘾,感觉每买一个便宜饰品都是赚钱,闲下来没事就刷网易 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¤cy=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 上架交易的过程了,半自动程序个人使用也能获取足够的余额,是否要投入更多的时间,就要看兴趣和用途了。全自动程序可能就会被于其他的目的,而不仅仅是找点折扣了。后续流程比较复杂,本文也就是抛砖引玉,还需各位自己摸索研究。
如果实现了这些,再接入其他几家平台,增加游戏种类,扩大饰品来源,手握一个低价余额收割机,日子就越来越有判头了。
发表回复