Table of contents
1 關於 Retrofit
Retrofit 係一個 HTTP client 既 Java library,可以幫我地做到 HTTP call,有啲似 Spring Cloud OpenFeign,都係行 declarative approach,透過 interface methods 既 annotations 以及 parameters 黎定義 HTTP 請求。
2 動手使用 Retrofit 下載資料
2.1 需求情境
試想像我地而家想寫一個程式,可以幫我地喺 Jumbo Computer 網站獲取電腦零件報價資訊。
透過 Chrome developer tools 既 Network 分頁,我地可以見到,當我地喺頁面選擇左「產品類型」之後,Jumbo Computer 會喺 XHR 分類下有個 getprod.php
既 HTTP POST request,而 response body 係 JSON 格式。
2.2 分析
我地可以見到 Jumbo Computer 拎資料用既 HTTP POST request 係需要一個 session
既 form data entry。
經 Postman 一試,我地會發現如果個 HTTP request 缺少左 session
,又或者個 session
值係我地自己改出黎既話,都會收到 error response,咁係因為 Jumbo Computer 既 server 會驗證呢個 session
值。
至於呢個 session
值要點樣先可以拎到?實驗話畀我地知,呢個 session
值好似唔會 expire,咁即係我地可以直接用已知既 session
值。不過,穩陣起見,我地都係睇下佢係點樣攞到或者計到呢個 session
值比較好。
2.2.2 如何取得新的 session
值
喺 Chrome developer tools 既 Application 分頁下面,我地可以見到 Local Storage 分類下,shoponjc.com
有一個 Key
為 Session ID
既 entry。
另外,喺 Chrome developer tools 既 Sources 分頁下面,我地可以見到 shoponjc.com
有一個 JavaScript 檔叫 quo.js?d83scwsoejkx6
,裡面有以下一段:
if (window.localStorage) {
storage = window.localStorage;
storage.setItem("Version", "1.0");
storage.setItem("Session ID", document.getElementById("sid").value);
}
咁即係我地可以用 sid
作為 DOM element ID 黎喺個網頁上搵 DOM element,而結果如下:
<input type="hidden" id="sid" value="wpjfi557nqf75" />
解讀: 每當我地瀏覽呢個網頁既時候,佢 server 會返回一個帶有隨機 session ID 既 DOM element 既 HTML 檔,然後 JavaScript 會令 Local Storage 裡面多左一個名為 Session ID
既 entry。
另外,我地都可以從個 JavaScript 檔見到個 session
值係黎自於 this.$refs.sid
。因為個網頁係用 Vue.js library 黎寫,而 this.$refs.sid
就即係指緊 DOM element ID 為 sid
既 DOM element。
1var vm = new Vue({
2 // ... 省略 500 幾行
3 mounted: function () {
4 this.session = this.$refs.sid.value;
5 // ... 省略幾行
6 },
7});
解讀: 喺個 sid
DOM element mount 左之後,Vue.js 既 session
(this.session
)就會變成 sid
DOM element 既 value
。
再睇下佢係點樣組裝 POST request 同 call 出去:
1actualSearch: function (pnum) {
2 /*
3 if (this.ptype == '') {
4 alert("Please select 產品類型 Product Type first.");
5 return;
6 }
7 if (this.ptypeval.length > 40) {
8 alert("關鍵字 Key words are too long, please re-enter first.");
9 return;
10 }
11 */
12 if (this.searchFired == false) {
13 this.searchFired = true;
14 var form_data = new FormData();
15 form_data.append('session', this.session);
16 form_data.append('sort', this.nsort);
17 form_data.append('link', this.linkpicks);
18 form_data.append('ptype', this.ptype);
19 form_data.append('pmin', this.range[0]);
20 form_data.append('pmax', this.range[1]);
21 form_data.append('pwords', this.ptypeval);
22 form_data.append('prow', this.pagerow);
23 form_data.append('pnum', pnum);
24 ajax({
25 method: 'post',
26 url: 'getprod.php',
27 data: form_data
28 }).then(response => {
29 this.searchFired = false;
30 this.$nextTick(function () {
31 let json = response.data;
32 this.havedata = false;
33 if (json.status == 'Error') {
34 alert("No product found!!");
35 } else {
36 this.plist = json.plist;
37 if (json.plist.length >= 10)
38 this.havedata = true;
39 }
40 });
41 });
42 }
43},
解讀: 可以見到拎資料既 POST request 既 session
係黎自於 Vue.js 既 session
(this.session
)。
成件事就係咁簡單!我地每次用 jsoup 下載個新既 HTML 落黎就可以拎到新既 session ID 值。
2.3 添加 Maven dependencies
我地會用到以下 Maven dependencies,當中包括 jsoup:
1<!-- ##### jsoup ##### -->
2<dependency>
3 <groupId>org.jsoup</groupId>
4 <artifactId>jsoup</artifactId>
5 <version>1.13.1</version>
6</dependency>
7
8<!-- ##### retrofit ##### -->
9<dependency>
10 <groupId>com.squareup.retrofit2</groupId>
11 <artifactId>retrofit</artifactId>
12 <version>2.9.0</version>
13</dependency>
14<dependency>
15 <groupId>com.squareup.retrofit2</groupId>
16 <artifactId>converter-scalars</artifactId>
17 <version>2.9.0</version>
18</dependency>
19<dependency>
20 <groupId>com.squareup.retrofit2</groupId>
21 <artifactId>converter-gson</artifactId>
22 <version>2.9.0</version>
23</dependency>
2.4 寫 Java code
ProductsResponse.java
:
1@Data
2@Accessors(chain = true)
3@FieldDefaults(level = PRIVATE)
4public class ProductsResponse {
5 String status;
6 String session;
7
8 @SerializedName("plist")
9 List<Product> products;
10}
JumboClient.java
:
1public interface JumboClient {
2
3 @FormUrlEncoded
4 @POST("getprod.php")
5 Call<ProductsResponse> getProductsByCategory(
6 @Field("session") String session,
7 @Field("sort") Integer sortType,
8 @Field("link") Boolean link,
9 @Field("ptype") Integer productType,
10 @Field("pmin") Long priceMin,
11 @Field("pmax") Long priceMax,
12 @Field("pwords") String productKeywords,
13 @Field("prow") Long pageRowCount,
14 @Field("pnum") Long pageNum
15 );
16}
RetrofitHelper.java
:
1public final class RetrofitHelper {
2
3 private static RetrofitHelper retrofitHelper;
4
5 private RetrofitHelper() {
6
7 }
8
9 public static RetrofitHelper getInstance() {
10 return retrofitHelper==null ? (retrofitHelper = new RetrofitHelper())
11 : retrofitHelper;
12 }
13
14 public JumboClient createJumboClient() {
15 return createRetrofit("https://shoponjc.com").create(JumboClient.class);
16 }
17
18 private Retrofit createRetrofit(String baseUrl) {
19 return new Retrofit.Builder()
20 .baseUrl(baseUrl)
21 .client(new OkHttpClient.Builder()
22 .readTimeout(10, SECONDS)
23 .connectTimeout(10, SECONDS)
24 .build())
25 .addConverterFactory(ScalarsConverterFactory.create())
26 .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))
27 .build();
28 }
29}
Main.java
:
1@Slf4j
2public class Main {
3
4 public static void main(String[] args) throws Exception {
5
6 final JumboClient jumboClient = RetrofitHelper.getInstance().createJumboClient();
7
8 // 拎資料
9 jumboClient.getProductsByCategory(
10 getSessionIdWithJsoup(), // sessionId
11 3, // sortType
12 true, // link
13 5, // productType
14 1L, // priceMin
15 200000L, // priceMax
16 "", // productKeywords
17 10000L, // pageRowCount
18 0L // pageNum
19 ).execute().body();
20 }
21
22 private static String getSessionIdWithJsoup() {
23
24 try {
25 final String html = Jsoup.connect("https://shoponjc.com/quote.php").get().html();
26
27 final Matcher matcher = Pattern.compile("<input type=\"hidden\" id=\"sid\" ref=\"sid\" value=\"(?<sessionId>\\w+)\"").matcher(html);
28 final String sessionId = matcher.find() ? matcher.group("sessionId") : null;
29
30 return sessionId;
31 } catch (Exception e) {
32 log.error("Failed to download Jumbo webpage.", e);
33 }
34
35 return null;
36 }
37}