我只是想查个电费(2)

玄学人生就此开启:OkHttp的使用和网页数据提取

Posted by Donggu Ho on 2016-11-19

上接为了查电费,学会了 Chrome 和 Postman 的使用。Postman 中可以生成对应语言的 Code,其中 JAVA 可选的有Ok HttpUnirest两种 Http 组件。考虑到 Ok Http 代码风格感觉比较清晰,而且在 Android 开发中较为流行故使用这款。其实使用 Java 自己的 HttpClient 也是可以的,不过其实代码可读性没有 OkHttp 好啊。

OK Http的导入与基本使用

刚开始调试时并不准备丢到 Android 项目里面去做,每次编译时间也太长了。就在随手一个普通 Java 项目里面进行。

导入与配置

OkHttp包的导入有三种方法,包括直接下载导入、gradle构建和maven构建方法。OkHttp 官网上有相关说明。这里我直接导入JAR包:

  1. 分别下载 OkHttp-3.4.2 和它的依赖包 Okio;
  2. 在项目的Project Structure(IntelliJ IDEA的快捷键是Ctrl+Shift+Alt+S)中导入 jar 包。

基本使用

基本思路是,先新建一个OkHttpClient,然后构建请求,利用 Client 执行请求。
一个最简单的GET请求如下:

1
2
3
4
5
6
7
8
9
10
client = new OkHttpClient();                            //新建客户端
Request request = new Request.Builder() //构造请求
.url("http://202.120.163.129:88/")
.get()
.build();
try{
Response response = client.newCall(request).execute(); //执行请求
}catch(IOEception e){
//...
}

而当需要发出POST请求时,则需要首先构造请求体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//构造请求体
String post = "&__EVENTTARGET=&__VIEWSTATE=&drlouming=7";
RequestBody requestBody = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), post);
// 构造请求
Request request = new Request.Builder()
.url("http://202.120.163.129:88/")
.post(requestBody)
.build();
// 执行请求
try{
Response response = client.newCall(request).execute(); //执行请求
}catch(IOEception e){
//...
}

从前文Postman生成的语句而言,其实步骤已经很清晰了。但是考虑到每一步需要先获取前一步的__VIEWSTATE,所以请求之间需要加入解析 response 的部分。

网页数据的提取

通过发送 POST 获取了 response 之后,我们需要对 response 进行解析,以提取到想要的信息。response 和 request 类似,也包括headerbody两个部分。其中回应头是键值对,而body在本条件下为html文本。想要提取目标数据主要可以使用两种方法:正则表达式Jsoup解析

正则表达式

正则匹配逼格高呀~(大雾)为了顺便练习一下自己写正则的能力顺便试试 Java 的正则玩法,我基本上是用正则完成了所有数据的提取。正则匹配的思想其实挺简洁的,学完之后找个测试器试试基本就差不多了。Java 自带的正则匹配位于java.util.regex下,主要用到的有PatternMatcher两个类。为了方便复用我将正则提取的步骤写在了一个函数里面。

1
2
3
4
5
6
7
private static String getValueFromHtml(String html, String regexExp, int group) {
Pattern r = Pattern.compile(regexExp); //从正则表达式生成比较器
Matcher m = r.matcher(html); //匹配结果放入Matcher
if (m.find()) {
return m.group(group); //返回对应分组的匹配值
} else return "";
}

我的提取__VIEWSTATE的正则表达式为

1
(?:__VIEWSTATE" value=")(/\S+)(?:")

那么,通过以下步骤即可获得__VIEWSTATE

1
2
String html = response.body().string(); //获取返回的html文本字符串
String viewstate = getValueFromHtml(html, "(?:__VIEWSTATE\" value=\")(/\\S+)(?:\")", 1);

在获得结果页后提取电费额也是同理:

1
String balance = getValueFromHtml(html, "(?:orange\">)(\\S+)(?:<)", 1);

Jsoup解析

Jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。对于学过JQuery或者类似的DOM的人而言非常容易上手。使用方法也很简单,到 Jsoup 官网下载 jar 包后导入即可。
其实 Jsoup 就自带了网络客户端的功能,从发送请求到解析 html 一气呵成……连接流程和OkHttp也很类似。啊总之很方便啦【为什么我没有早一点知道【躺
Jsoup的优秀在于其selector可以直接使用 CSS 选择器文法,所以就很方便。比如从第一个 get 请求获得校区选项信息及其value的典型流程如下

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws IOException {
Document doc = Jsoup.connect(url).get(); //获取文档
Element element = doc.select("#drlouming").first(); //使用CSS选择器获得元素
for(Element e : element.children()){ //遍历子元素
if(e.hasAttr("selected"))continue; //跳过第一个无意义选项
xiaoquList.add(new ListItem(e.val(),e.text())); //加入列表
}
}
ArrayList<ListItem> xiaoquList = new ArrayList<>();

其中ListItem为自定义的一个简单的类,用来储存各个选项的信息不要问我为什么不用 Map 和 Pair

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ListItem{
String value;
String content;

public ListItem(String value, String content) {
this.value = value;
this.content = content;
}

public String getValue() {
return value;
}

public String getContent() {
return content;
}
}

参数转码

在提取到__VIEWSTATE后,进行第二次请求却发现并没有获得想要的结果。经研究,发现是__VIEWSATE参数内容含有的部分字符没有转码所致。由于设置的MediaTypex-www-form-urlencoded,所以键的值需要经过 URL 编码。

1
viewstate = URLEncoder.encode(viewstate, "utf-8");

使用 Java 自带的编码器即可转码完成。这个步骤非常简单,然而天知道我在这里卡了多久:)

小结

于是,在搞定以上问题后,选择校区、楼栋、楼层的操作应该已经可以完成了。就是发送请求-提取信息-修改请求体-再次发送-……这样的套路。但是离得到最后的结果还有一点距离,因为最后的结果页位于另一地址,跨域请求产生了Cookie和重定向,使问题变得复杂了一些。下次再讲吧。

天哪居然还连载起来了
其实最近在学安卓 感觉这个系列可以连载到地老天荒

相关:查电费系列Blog