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