一、开场故事:运营小哥的周五17:30
杭州某摄影器材公司,每周要汇报「闲鱼佳能AE-1均价」:
- 人工滑屏300条 → 眼花了
- Excel手填 → 错了
- 周五熬夜 → 秃了
- 后端同事甩给他一个60行JAR:
- 输入关键词 → 自动抓500条
- 一键导出CSV → Excel透视图直接出
- 周五17:30下班 → 老板夸「高效」
- 今天把完整思路开源,复制即可跑。
二、先选赛道:为什么选「公开接口」而不是「登录爬虫」
方案 | 封号风险 | 维护成本 | 数据完整度 | 选用 |
---|---|---|---|---|
模拟登录+滑块 | 高 | 极高 | 100% | ❌ |
无头浏览器 | 中 | 高 | 95% | ❌ |
公开游客接口 | 极低 | 低 | 80%(够用) | ✅ 结论:只爬“游客可见”信息——标题、价格、所在地、浏览量、想要数,不碰聊天记录,才能长期安稳。 |
三、5分钟环境:一个Maven命令全搞定
xml
<!-- pom.xml 只加两个依赖 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.47</version>
</dependency>
四、核心思路:两步拿到商品JSON
- 搜索页→拿itemId(正则即可)
- 详情页→/json.htm→结构化数据
- 示例URL(官方公开,无需Cookie):https://g-acs.m.goofish.com/h5/mtop.taobao.idle.awesome.itemdetail/1.0/?data={"itemId":"776721774669"}
- 返回片段:
JSON
{"data":{"components":{"itemInfo":{"title":"佳能 AE-1 胶片机","price":"1180","location":"杭州","viewCount":"1283","wantCnt":"23"}}}}
五、60行完整源码:搜索+详情+CSV一条龙
java
package com.xianyu;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import okhttp3.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class XianyuSpider {
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
private static final Semaphore SEMA = new Semaphore(10); // polite限速
private static final String SEARCH_URL = "https://s.goofish.com/h5/searchPub/data?query=%s&page=%s&pageSize=20";
private static final String DETAIL_URL = "https://g-acs.m.goofish.com/h5/mtop.taobao.idle.awesome.itemdetail/1.0/?data=%s";
public static void main(String[] args) throws Exception {
String kw = "佳能AE-1";
int maxPage = 3; // 先跑3页约60条
List<Item> items = new ArrayList<>();
for (int p = 1; p <= maxPage; p++) {
List<String> idList = search(kw, p);
for (String id : idList) {
Item it = detail(id);
if (it != null) items.add(it);
}
Thread.sleep(1000); // 翻页间隔
}
// 导出CSV(Excel可直接打开)
StringBuilder sb = new StringBuilder("itemId,title,price,location,want,view,updated\n");
items.forEach(i -> sb.append(i.toCsv()).append("\n"));
Files.write(Paths.get(kw + "_" + LocalDateTime.now().toLocalDate() + ".csv"), sb.toString().getBytes());
System.out.println("共导出 " + items.size() + " 条 → " + kw + ".csv");
}
/* -------------------- 搜索 -------------------- */
private static List<String> search(String kw, int page) throws IOException {
Request req = new Request.Builder()
.url(String.format(SEARCH_URL, URLEncoder.encode(kw, "UTF-8"), page))
.header("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)")
.build();
try (Response resp = CLIENT.newCall(req).execute()) {
JSONObject root = JSON.parseObject(resp.body().string());
List<String> ids = new ArrayList<>();
root.getJSONObject("data").getJSONArray("listItem").forEach(o -> {
ids.add(((JSONObject) o).getString("itemId"));
});
return ids;
}
}
/* -------------------- 详情 -------------------- */
private static Item detail(String itemId) throws IOException {
SEMA.acquireUninterruptibly();
try {
String param = URLEncoder.encode(JSON.toJSONString(new ItemId(itemId)), "UTF-8");
Request req = new Request.Builder()
.url(String.format(DETAIL_URL, param))
.header("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)")
.build();
try (Response resp = CLIENT.newCall(req).execute()) {
JSONObject root = JSON.parseObject(resp.body().string());
JSONObject d = root.getJSONObject("data").getJSONObject("components").getJSONObject("itemInfo");
return new Item(
itemId,
d.getString("title"),
Double.parseDouble(d.getString("price").replace(",", "")),
d.getString("location"),
d.getIntValue("wantCnt"),
d.getIntValue("viewCount"),
LocalDateTime.now().toString()
);
}
} finally {
SEMA.release();
}
}
/* -------------------- POJO -------------------- */
private static class ItemId { private String itemId; public ItemId(String id){this.itemId=id;} }
private static record Item(String itemId, String title, double price, String location, int want, int view, String updated) {
String toCsv(){ return itemId+","+title.replace(","," ")+","+price+","+location+","+want+","+view+","+updated; }
}
}
运行结果:共导出 60 条 → 佳能AE-1_2025-09-29.csv
Excel打开→透视表→均价/区域热度一目了然。
六、提速&稳速:并发+重试+礼貌
技巧 | 实现 | 效果 |
---|---|---|
并发 | Semaphore(10) | 60条≈8秒 |
重试 | okhttp3.RetryInterceptor | 网络抖动成功率99% |
限速 | 翻页Thread.sleep(1000) | 单IP安全 |
代理 | OkHttpClient.Builder().proxy() | 分布式再+代理 |
七、三行代码看行情(Excel透视)
- 均价 =AVERAGE(C:C)
- 最低地区透视图 → 低价收货
- 浏览/想要比 → 判断“真假热销”
八、常见问题(FAQ)
- 403怎么办?→把Semaphore调到5,或加Accept-Language
- 想要图文详情?→游客接口无图,需登录版(高风险,本文不展开)
- 一天能采多少?→单IP1w次+无封号;分布式再+代理
- 能商用吗?→只采“游客可见”字段,无明文禁止;对外展示请脱敏itemId后三位
九、把脚本升级成“副业现金流”
需求 | 工具 | 成本 |
---|---|---|
定时 | Linux cron / Win任务计划 | 0元 |
通知 | 企业微信机器人+http POST | 0元 |
前端 | Streamlit共享网页 | 0元 |
订阅 | 知识星球/小报童 | 29元/月 已有读者把“闲鱼行情”做成星球,200会员×50元=月入1w |
十、合规再提醒:只挖公开矿,不碰隐私矿
- 不登录、不破解、不存储聊天记录
- 不公开卖家手机号、地址、真实昵称
- 对外展示脱敏itemId,避免黄牛精准狙击
- 商用前阅读《闲鱼用户协议》,必要时咨询法律顾问
十一、结语:让技术“温柔”地赚钱
今天这篇软文,没有对抗、没有炫技,只有:
- 公开接口→低风险
- 60行代码→可复制
- CSV输出→能落地
- 把JAR丢给cron,每天一杯咖啡的时间,就能生成一份“二手行情报告”。