電商系統常需完整的行政區劃資料支援收貨地址選擇。本文示範使用 Java 結合 Jsoup 解析 HTML、**Fastjson** 序列化 JSON、Apache Commons IO/Lang** 輔助工具,從京東收貨地址介面采集省、市、區/縣、街道/鄉鎮四級行政區劃資料,並輸出結構化 JSON 檔案供前端直接使用。
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
京東收貨地址介面採用級聯載入,四個端點:
| 層級 | API URL | 參數 |
|---|---|---|
| 省份 | https://trade.jd.com/dynamic/consignee/getProvinces.action | 無 |
| 城市 | https://trade.jd.com/dynamic/consignee/getCitys.action | provinceId |
| 區/縣 | https://trade.jd.com/dynamic/consignee/getCountys.action | cityId |
| 街道/鎮 | https://trade.jd.com/dynamic/consignee/getTowns.action | countyId |
回應格式為 HTML `` 列表。
package com.test.crawler;
import com.alibaba.fastjson.JSON;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class JdAddressCrawler {
private static final int TIMEOUT_MS = 300_000; // 5 分鐘超時
private static final int REQUEST_DELAY_MS = 100; // 請求間隔
private static int requestCount = 0;
// API 基礎 URL
private static final String PROVINCE_URL = "https://trade.jd.com/dynamic/consignee/getProvinces.action";
private static final String CITY_URL = "https://trade.jd.com/dynamic/consignee/getCitys.action?consigneeParam.provinceId=";
private static final String COUNTY_URL = "https://trade.jd.com/dynamic/consignee/getCountys.action?consigneeParam.cityId=";
private static final String TOWN_URL = "https://trade.jd.com/dynamic/consignee/getTowns.action?consigneeParam.countyId=";
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("開始采集京東行政區劃資料...");
// 1. 獲取省份列表
List provinces = fetchList(PROVINCE_URL);
System.out.println("省份數量: " + provinces.size());
// 2. 遞迴采集下級行政區
for (Address province : provinces) {
List cities = fetchList(CITY_URL + province.getId());
province.setChildren(cities);
System.out.printf(" 省份 [%s] 城市數: %d%n", province.getName(), cities.size());
for (Address city : cities) {
List counties = fetchList(COUNTY_URL + city.getId());
city.setChildren(counties);
for (Address county : counties) {
List towns = fetchList(TOWN_URL + county.getId());
county.setChildren(towns);
}
}
}
// 3. 序列化輸出 JSON 檔案
String json = "var addressData = " + JSON.toJSONString(provinces, true);
File outputFile = new File("jd_address_data.js");
FileUtils.writeStringToFile(outputFile, json, "UTF-8");
System.out.println("資料已輸出至: " + outputFile.getAbsolutePath());
System.out.println("總請求數: " + requestCount);
}
/** 通用列表采集方法 */
private static List fetchList(String url) throws IOException, InterruptedException {
List list = new ArrayList<>();
Document doc = Jsoup.parse(new URL(url), TIMEOUT_MS);
Elements options = doc.select("option[value~=\\d+]"); // value 為數字的 option
for (Element option : options) {
Address addr = new Address();
addr.setName(option.text().replace("*", StringUtils.EMPTY)); // 移除 * 標記
addr.setId(option.attr("value"));
list.add(addr);
}
// 請求頻率控制:每 100 次列印進度,每 500 次暫停 5 秒
requestCount++;
if (requestCount % 100 == 0) {
System.out.println("已完成請求: " + requestCount);
}
if (requestCount % 500 == 0) {
System.out.println("休眠 5 秒避免頻率限制...");
Thread.sleep(5000);
}
return list;
}
}
/** 行政區劃資料模型 (支援遞迴樹狀結構) */
class Address {
private String name; // 名稱
private String id; // 代碼
private List children; // 下級行政區
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public List getChildren() { return children; }
public void setChildren(List children) { this.children = children; }
}
var addressData = [
{
"id": "1",
"name": "北京",
"children": [
{
"id": "72",
"name": "朝陽區",
"children": [
{"id": "2799", "name": "三環以內", "children": []},
{"id": "4139", "name": "北苑", "children": []}
]
}
]
},
{
"id": "2",
"name": "上海",
"children": [...]
}
]
<!-- 引入產出的 JS 檔案 -->
<script src="jd_address_data.js"></script>
<script>
// addressData 變數即可用
console.log(addressData[0].name); // "北京"
console.log(addressData[0].children[0].name); // "朝陽區"
// 級聯選擇器範例 (Vue/React/原生 JS 均可)
function renderProvince() {
addressData.forEach(p => {
// 建立 option 元素...
});
}
</script>
| 策略 | 實作方式 |
|---|---|
| 請求間隔 | 每請求 `Thread.sleep(100)`,每 500 請求暫停 5 秒 |
| User-Agent | `Jsoup.connect(url).userAgent("Mozilla/5.0...").get()` |
| Cookie 維持 | 若需登入態,先手動登入複製 Cookie 加入請求頭 |
| IP 輪換 | 大量采集建議使用代理 IP 池 |
| 錯誤重試 | 網路異常/非 200 狀態碼指數退避重試 3 次 |
robots.txt 規範