其实这个代码去年我就在项目里写好了,只是去年我并没有玩博客……现在想想挺有趣的,记录下来。当然了,我做了一些简化处理,比如不建立表,不保存入库,由各位读者根据实际情况自己去处理,反正在我这如果要加上保存入库的代码就是一两行的事情,前提是各种类要封装好。
我们来看一下小米网的所有收货地址是什么样的:传送门
看到了吧,除了开头那77个字符串是没用的以外,剩下的都是标准json字符串,那么我们就解析这个网页这个json就可以了。这里我先创建一个JavaBean:Address类,采用树形结构,这种树形结构比较新颖,根据parent、lft、rgt来构成,不过我这里因为不保存入库,这里不对lft和rgt两个字段设置值,读者只需关心id、name、parent就行。小米网的每一个收货地址就是一个Address类。
public class Address {
private String id;
private Address parent;
private Integer lft;
private Integer rgt;
private String name;
private Integer priority;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Address getParent() {
return parent;
}
public void setParent(Address parent) {
this.parent = parent;
}
public Integer getLft() {
return lft;
}
public void setLft(Integer lft) {
this.lft = lft;
}
public Integer getRgt() {
return rgt;
}
public void setRgt(Integer rgt) {
this.rgt = rgt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPriority() {
return priority;
}
public void setPriority(Integer priority) {
this.priority = priority;
}
}
接下来开始爬取,创建一个入口类,给定两个静态成员变量:
/**
* 导入 Jackson 库,用于 Java 对象和 json 字符串的互相转换
*/
static ObjectMapper objectMapper = new ObjectMapper();
static List<Address> addressList = new ArrayList<Address>();
main 方法,这里的链接就是我上文给的传送门:
public static void main(String[] args) {
try {
Document doc = Jsoup.connect("https://s1.mi.com/open/common/js/address_all_new.js").ignoreContentType(true).get();
Elements body = doc.select("body");
// outerHtml:将把 <body> 标签一并输出
// html:只输出 <body> 标签里面的内容
// 裁掉前面无用的 77 个字符串,得到所有地址的 json 字符串
String html = body.html().substring(77);
// 转换为集合
List list = objectMapper.readValue(html, List.class);
save(null, list);
System.err.println(addressList);
} catch (IOException e) {
e.printStackTrace();
}
}
save 方法,用到了递归的思想:
private static void save(Address parent, List<Map> list) {
for(Map map : list) {
Address address = new Address();
String id = map.get("id").toString();
String name = (String) map.get("name");
address.setId(UUID.randomUUID().toString());
address.setName(name);
if(parent != null) {
address.setParent(parent);
}
addressList.add(address);
List<Map> childList = (List<Map>) map.get("child");
if(childList != null) {
save(address, childList);
} else {
try {
String url = "https://order.mi.com/api/getAddressRegion.php?jsonpcallback=jQuery111305980845644014263_1505877765419&parent="+ id +"&_=1505877765434";
Document doc = Jsoup.connect(url)
.ignoreContentType(true)
.referrer("https://item.mi.com/product/10000057.html") // 必须设置请求来源,只要是小米网的地址就行
.get();
// 以下变量见下文讲解
Elements body = doc.select("body");
String html = body.html().substring(42);
html = html.substring(0, html.length() - 2);
Map _map = objectMapper.readValue(html, Map.class);
if (_map != null && !_map.isEmpty()) {
Map dataMap = (Map) _map.get("data");
if (dataMap != null && !dataMap.isEmpty()) {
Map<String, Map> regionsMap = (Map) dataMap.get("regions");
for(Map.Entry<String,Map> entry : regionsMap.entrySet()) {
String regionId = entry.getKey();
Map regionValue = entry.getValue();
Integer _regionId = (Integer) regionValue.get("region_id");
String _regionName = (String) regionValue.get("region_name");
Integer _sort = (Integer) regionValue.get("sort");
Address street = new Address();
street.setId(UUID.randomUUID().toString());
street.setParent(address);
street.setName(_regionName);
street.setPriority(_sort);
addressList.add(street);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
body变量:
利用 html() 转换成文本再裁掉无用的42个字符串:
只要再裁掉最后2个字符串就能构成一个标准json了,剩下不用我说也都知道了吧。
还有一点就是为什么必须设置请求来源,我们先看一下不设置就去请求小米的那个链接是什么效果:
它会直接告诉你请求来源不正确,简单来说就是小米认为你是通过非法途径来到我这个网址的,滚;那什么是合法的呢?就是我从小米的域名跳到这个网址它会认为是合法的,毕竟是自家人。那么我们就设置请求来源为小米的一个域名,什么都行,只要是小米的域名就行。
非法的请求来源,即在请求头中没有 Referer 属性,或者非小米网的网址
合法的请求来源,你再去Preview选项卡就能看到获取的数据,就是我上文贴的那些变量图片:
可能有些人有疑问,我那两个链接是怎么知道的,很简单啊,你随便找一个小米的商品,到购买页,随便点几下收货地址,注意看Network,从中找就行了:
最后我们看一下爬取成果,一共有48000多条数据啊,当初我存到数据库都存了好一会儿……