學校公衆號要用爬蟲查詢校園網流量,記錄一下實現這個簡易爬蟲的過程。
開發工具:
Eclipse,Chrome/Firefox
第三方庫”:
jsoup:用來解析網頁數據,用法傳送門:http://www.open-open.com/jsoup/,HttpClient用來連接web頁面,模擬get和post請求
Step 1:明確目標
簡單的理解爬蟲的過程就是模擬網頁操作的過程,GET網頁數據,POST數據請求的模擬。
So,第一部先明確查詢校園網流量的步驟:
1:確認目標網頁:http://zyzfw.xidian.edu.cn/ 我們查詢校園網流量用戶登錄界面
2:輸入 學號,密碼,驗證碼,作爲post的數據,然後點擊登錄
3:登錄後轉向頁面地址:http://zyzfw.xidian.edu.cn/home/base/index 流量信息查詢頁面
4:記錄所查看到的流量信息
Step 2:java編程
java文件列表
HttpClientManager.java: 獲取一個HttpClient的單例,通過這個單例來連接網站
HttpOperate.java: HttpClient相關網絡請求的函數
1,獲取網頁cookies信息與驗證碼的GET方法;
2,賬戶登錄POST請求方法;
3,獲取登錄後網頁中流量信息的get方法。
DocHandle.java : 對通過jsoup庫獲得到的網頁的html源碼的document類進行分析處理獲取網頁內容並保存
1,獲取網頁錯誤信息方法,
2,獲取流量信息方法,
3,獲取令牌token方法,
4,驗證碼獲取分析方法
ImageOP.java 根據url和cookies下載驗證碼圖片
NetConstans.java 網址等常量
PictureOperate.java 對下載來的驗證碼圖片進行操作
1,讀取圖片方法(返回int[][]二維數組);
2,裁剪圖片方法(使得驗證碼圖片的4個數字變成4張圖片可以單獨處理);
3,保存圖片方法
4,簡單的識別圖片數字方法
UserInfo.java 用戶信息的保存
1,用戶基本信息
2,cookies信息
3,圖片路徑信息
4,令牌信息
5,流量信息
6,錯誤信息
MainRunning.java 主程序
MainRunning.java
public class MainRunning {
public static void main(String[] args) {
// TODO Auto-generated method stub
HttpClientManager.init();
UserInfo user = new UserInfo("0000000001","000000001");
if(user.getUserName().equals(""))
{
System.out.println("輸入用戶名");
}
//login
boolean loginOk=false;
int cc=0;
do{
cc++;
if(cc>5)
{
System.out.println("login error!tyr late.");
break;
}
if(HttpOperate.getLoginInfo(user)){
loginOk=HttpOperate.loginFlowQuery(user);
if(!loginOk&&!user.codeError.equals("")){
System.out.println(user.userError);
break;
}
}
else{
System.out.println("getLoginInfo error!!");
break;
}
}while(!loginOk);
if(loginOk&&HttpOperate.getFlowInfo(user)){
user.printFlowInfo();
}
else{
System.out.println("get FlowInfo error!");
}
}
}</span>
從Main方法中可以發現,邏輯十分的簡單:
1,初始化:
HttpClientManager.init();
初始化HttpClient實例
UserInfo user = new UserInfo("0000000001","000000001");
初始化用戶的賬號和密碼
2,賬戶登錄:
do{……}while(……)嘗試5次登錄(原諒我的驗證碼識別函數有點low),如果5次都沒有登錄成功,提示用戶稍後再試。
HttpOperate.getLoginInfo(user)
獲取網頁cookies信息與驗證碼,模擬用戶在地址欄輸入了目標網頁:http://zyzfw.xidian.edu.cn/ 的GET請求,然後獲取目標網站的內容,主要是要獲取cookies,token令牌(稍後說明),驗證碼圖片
loginOk=HttpOperate.loginFlowQuery(user);
根據用戶名,密碼,驗證碼,token令牌 進行登錄,即模擬一次post請求
3,登錄成功,查詢登錄界面的流量信息
HttpOperate.getFlowInfo(user)
模擬對登錄後轉向頁面地址:http://zyzfw.xidian.edu.cn/home/base/index 的一個GET請求
三個步驟的詳細說明:
1,初始化
HttpClientManager.java的代碼
public class HttpClientManager {
private static HttpClient httpClient = null;
private HttpClientManager(){
}
public static void init(){
httpClient = null;
if(httpClient == null){
synchronized (HttpClientManager.class){
if (httpClient == null){
httpClient = HttpClients.createDefault();
}
}
}
}
public static HttpClient getInstance(){
return httpClient;
}
}
簡單的一個單例模式,把構造方法變成私有,僅在靜態方法init中創建唯一實例,僅通過靜態方法getInstance獲取這個實例,通過這個HttpClinet可以進行網站的GET和POST請求,如果要超時等待等設置也是在這邊設置。
UserInfo.java中的UesrInfo類沒什麼好說明的,就是簡單各種成員屬性,用來保存網頁中獲得的數據。
2,賬戶登錄
2-1:獲取網頁cookies信息與驗證碼,模擬用戶在地址欄輸入了目標網頁:http://zyzfw.xidian.edu.cn/ 的GET請求,然後獲取目標網站的內容,主要是要獲取cookies,token令牌(稍後說明),驗證碼圖片
先通過Chrome瀏覽器來看一看,瀏覽器瀏覽(GET)網頁內容是如何發送請求的。
打開Chrome瀏覽器,按F12打開 開發者工具,選擇NETWORK(可以查看所有來往的網絡數據包,裏面包含了請求信息和網頁的所有信息),可以看到來自地址http://zyzfw.xidian.edu.cn/的數據包,點擊選擇headers,查看http頭文件
可以看到Requst Headers 就是爲了打開一個web頁面,向目標地址服務器發送的請求的頭信息。可以看到頭信息裏有一些設置。
再點開Response Headers,
這個就是向服務器發送GET請求,服務器發送給一個response,Response主要包含兩個部分,一個是ResponseHeaders(http頭信息)和ResponseEntity(就是看到的網站頁面的html源碼)
然後看HttpOperate.getLoginInfo(user)方法如下:
HttpOperate.getLoginInfo(user)在HttpOperate.java中
public static boolean getLoginInfo(UserInfo user){
HttpGet httpGet = new HttpGet(NetConstans.LOGIN_URL);
httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:39.0) Gecko/20100101 Firefox/39.0");
httpGet.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
httpGet.setHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
httpGet.setHeader("Accept-Encoding", "gzip, deflate");
httpGet.setHeader("Connection", "keep-alive");
httpGet.setHeader("Host","zyzfw.xidian.edu.cn");
httpGet.setHeader("Cache-Control","max-age=0");
httpGet.setHeader("Referer","http://pay.xidian.edu.cn/");
try {
HttpResponse response = HttpClientManager.getInstance().execute(httpGet);
Header[] headers = response.getHeaders("Set-Cookie");
StringBuilder sb=new StringBuilder();
for(int i=0;i<headers.length;i++){
sb.append(headers[i].toString());
}
user.setCookiesString(sb.toString());
//System.out.println(user.getCookiesString());
//System.out.println(user.getUserCookies().toString());
String loginWebStr = EntityUtils.toString(response.getEntity());
Document document = Jsoup.parse(loginWebStr);
DocHandle.getCsrfToken(document, user);
DocHandle.getVerifyCode(document, user);
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
創建一個HttpGet請求,先設置目標地址,再根據Chrome瀏覽器中截取的數據包中所示的頭信息的方式,逐一設置頭信息。
設置完信息後,通過獲取HttpClient的實例,執行GET請求操作: HttpResponse response = HttpClientManager.getInstance().execute(httpGet);
然後這個執行結果返回值就是我們的Response了:
1,先獲取Response的headers部分,主要是要獲取headers中的Set-Cookie中的內容,之前可以看到有3個set-cookie項,所以循環處理一下並分別保存一下這3個cookie(不一定會用到,因爲你之後的操作已經默認你保存這些Cookies)
2,在獲取Response的Entity部分,即網頁內容。先把html內容保存成字符串的形式,然後通過Jsoup.parse(String)方法把html字符串轉換成可以識別的Document的形式。
然後我們主要獲取這個html頁面中的兩樣信息:驗證碼 和token令牌(其實我也不知道爲什麼我要叫這個是令牌。。總好像有點什麼印象。。)
1,token令牌,DocHandle.getCsrfToken(document, user);在文件DocHandle.java中
public static void getCsrfToken(Document document,UserInfo user){
Elements es=document.select("meta");
for (Element element : es) {
if(element.attr("name").equals("csrf-token"))
{
user.setCsrf_token(element.attr("content"));
return;
}
}
System.out.print("token error");
}
這個token令牌是等會POST提交表單數據時要用到的。剛開始,我發現提交POST時有一個_csrf參數,我一直不知道這個參數的數值是哪裏來。。。。所以一直無法成功提交post請求,因此我查看了一下http://zyzfw.xidian.edu.cn/的html的源代碼,我看到了html源代碼中<head>標籤中有一個name="csrf-token"的標籤。因此,我們需要獲取這個標籤的內容。
<metaname="csrf-token"content="YUt1dfdtgno4KDcyaRUHDhczTRBaIzIDFikXT0woDhEVAx0xXjAKDw==">
Document的select方法選擇所有的meta標籤,然後遍歷這個標籤數組,去meta標籤的name屬性,當這個meta的name屬性是"csrf-tosen"時,獲取content屬性中的內容 保存到user中,即獲得了這個token令牌。
Document的操作都是Jsoup中的內容,很簡單的一些操作 ,傳送門:http://www.open-open.com/jsoup/
2,驗證碼
DocHandle.getVerifyCode(document, user);在文件DocHandle.java中
public static void getVerifyCode(Document document,UserInfo user){
Element eee = document.getElementById(NetConstans.VERIFYCODEID);
String url = NetConstans.LOGIN_URL+eee.attr("src");
user.imagePath=eee.attr("src").split("=")[1];
user.imagePath=user.imagePath;
//System.out.println(url);
ImageOP.downloadImageByURL(url,user);
int[][] data=PictureOperate.readPic2IntArray(user.imagePath+".png");
//File f1 = new File(user.imagePath+".png");
//if(f1.exists())
//f1.deleteOnExit();
String newPath2=user.imagePath+"-";
PictureOperate.cutPicture(data,newPath2);
StringBuilder sb=new StringBuilder();
for(int i=0;i<4;i++){
String newPath3=user.imagePath+"-"
+Integer.toString(i+1)+".png";
data = PictureOperate.readPic2IntArray(newPath3);
float[] f=PictureOperate.changeDataToInt9(data);
double res;
double minRes=9999;
int val=-1;
//0..9 = 10numbers
for(int j =0;j<10;j++){
res=0;
//9 blocks
for(int k =0;k<9;k++){
res=res+Math.abs(f[k]-PictureOperate.training[j][k]);
}
if(res<minRes){
minRes=res;
val=j;
}
}
sb.append(Integer.toString(val));
}
//String s=HttpOperate.recognizeCodeByORCKingWebsite(url);
user.setCode(sb.toString());
System.out.println(sb.toString());
for(int i=1;i<=4;i++){
File ff=new File(user.imagePath+"-"+Integer.toString(i)+".png");
ff.delete();
}
File fx = new File(user.imagePath+".png");
if(fx.exists())
{
if(!fx.delete())
{
System.gc();
fx.delete();
}
}
}
File fx = new File(user.imagePath+".png");
if(fx.exists())
{
if(!fx.delete())
{
System.gc();
fx.delete();
}
}
這段代碼,是由於奇怪的佔用導致文件打開了無法順利刪除,保證能刪除掉這個圖片。
先是根據驗證碼圖片的<img>標籤的特定的id號NetConstans.VERIFYCODEID="loginform-verifycode-image"來得到這個標籤,然後獲取這個img的對應src地址,然後根據這個地址調用downloadImageByURL方法下載這個驗證碼圖片。
驗證碼下載到本地以後是一張圖片的形式,接下來就開始
門外漢的圖像識別驗證碼數字
簡單說一下處理思路,
1,訓練樣本------這個操作是提前完成的,只要做一次就夠了,以後不用做了
1-1,獲取足夠多驗證碼圖片,一直刷新目標網頁,並每次保存驗證碼圖片
1-2,分割圖片,把每張圖片變成4個單獨的數字圖片,(這裏有個難的地方在於很多數字是粘連在一起的。。不好分割。。反正我的分割思路很low。。)
1-3,然後按0-9每個數字單獨分類,每種數字有足夠多的樣本
1-4,接着就是樣本分析了。。提取0123456789他們分別的特徵,我是把每張圖片分成九宮格,找出九宮格每一格中黑點佔全部黑點的百分作爲一個數字的特徵。
1-5,把訓練得到的結果保存成一個數組9(格)*10(類)的數組
2:把下載來的驗證碼也劃分成4個單獨數字,然後進行比較,看數字最就近哪個樣本特徵,就是哪個數字。最後把數字連起來就得到了驗證碼了。。。。。。。。。。
ImageOP.downloadImageByURL(url,user);在文件ImageOP.java中
public static void downloadImageByURL(String s,UserInfo user){
URL url;
try {
url = new URL(s);
//HttpURLConnection uc= (HttpURLConnection)url.openConnection();
URLConnection uc = url.openConnection();
uc.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
uc.setRequestProperty("accept", "*/*");
uc.setRequestProperty("connection", "Keep-Alive");
uc.setRequestProperty("user-agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
uc.setRequestProperty("Cookie",user.getUserCookies().toString());
uc.connect();
File file = new File(user.imagePath+".png");
FileOutputStream out = new FileOutputStream(file);
int i=0;
InputStream is = uc.getInputStream();
while ((i=is.read())!=-1) {
out.write(i);
}
is.close();
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
PS。。。注意這個GET請求要設置cookie,不然,獲得到的驗證碼不是同一個驗證碼。。。似乎是兩種請求不方式不同,不共用cookie。。。。2-2:根據用戶名,密碼,驗證碼,token令牌 進行登錄,即模擬一次post請求
同樣先看看點擊 登錄 時 Chrome瀏覽器和網站服務器之間傳送的數據包
同樣的方式,可以看到來自目標網站的數據包我們先看看Request Headers和Response Headers
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:180
Content-Type:application/x-www-form-urlencoded
Cookie:safedog-flow-item=C864AD7C523216AFDE4807601B; lzstat_uv=118312236|3401870; PHPSESSID=21sa2hufe124312aswrwqhghvcxc3; _csrf=f4559712aba7b9sadasda5d7655fcf96ede5b1df95febc673124452ca%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22YcBDPpAtvx8fcFtywbb9uMHktHhGgULu%22%3B%7D; BIGipServerzyzfw.xidian.edu.cn=13412690.24610.0000
Host:zyzfw.xidian.edu.cn
Origin:http://zyzfw.xidian.edu.cn
Referer:http://zyzfw.xidian.edu.cn/
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.130 Safari/537.36
Response Headers
Cache-Control:no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Connection:Keep-Alive
Content-Length:0
Content-Type:text/html; charset=UTF-8
Date:Fri, 29 Apr 2016 12:47:32 GMT
Expires:Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive:timeout=1, max=250
Location:http://zyzfw.xidian.edu.cn/home/base/index
Pragma:no-cache
Server:Apache/2.4.12 (Unix) OpenSSL/1.0.1g-fips PHP/5.5.23
Set-Cookie:PHPSESSID=a3c2f1p3rnf94ktcvfgi0vrvs4; path=/; HttpOnly
X-Powered-By:PHP/5.5.23
Cookie會自動獲得,無須設置
Location:http://zyzfw.xidian.edu.cn/home/base/index代表了登錄成功後轉向的地址。即流量信息查看的地址。
除了Resquest和Response,我們繼續往下拉,我們發現Form Data 這就是POST請求時,所需要的傳遞的參數,
我們發現除了用戶名,密碼,和驗證碼,還有一個關鍵的_csrf參數,因此就有了之前的獲取這個參數需求
Form Data
_csrf:U3NMOVJpWFcKEA59AhkZIyULdF8xLywuJBEuACckEDwnOyR.NTwUIg==
LoginForm[username]:00000001
LoginForm[password]:00000001
LoginForm[verifyCode]:7567
login-button:
public static boolean loginFlowQuery(UserInfo user){
HttpPost httpPost = new HttpPost(NetConstans.LOGIN_URL);
httpPost.setHeader("Host", "zyzfw.xidian.edu.cn");
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0");
httpPost.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
httpPost.setHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");
httpPost.setHeader("Accept-Encoding", "gzip, deflate");
httpPost.setHeader("Referer", "http://zyzfw.xidian.edu.cn/");
httpPost.setHeader("Origin", "http://zyzfw.xidian.edu.cn");
httpPost.setHeader("Connection", "keep-alive");
// set param
List<BasicNameValuePair> formparams = new ArrayList<BasicNameValuePair>();
formparams.add(new BasicNameValuePair("_csrf", user.getCsrf_token()));
formparams.add(new BasicNameValuePair("LoginForm[username]", user.getUserName()));
formparams.add(new BasicNameValuePair("LoginForm[password]", user.getPassword()));
formparams.add(new BasicNameValuePair("LoginForm[verifyCode]", user.getCode()));
formparams.add(new BasicNameValuePair("login-button", ""));
UrlEncodedFormEntity encodedFormEntity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
httpPost.setEntity(encodedFormEntity);
//System.out.println("Bfore Login");
HttpResponse response = null;
try {
response = HttpClientManager.getInstance().execute(httpPost);
if(response == null){
System.out.println("null");
return false;
}
String loginWebStr = EntityUtils.toString(response.getEntity());
//System.out.println(loginWebStr);
if(loginWebStr.equals("")||loginWebStr==null)
return true;//無返回值代表登陸成功
else {
Document document = Jsoup.parse(loginWebStr);
DocHandle.getErrorInfo(document, user);
return false;//有返回值代表出錯,查看錯誤信息
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
3:登錄成功後,就是在轉向後的網址內容GET網頁內容,通過jsoup獲取內容,保存在user中即可。
Form Data
_csrf:U3NMOVJpWFcKEA59AhkZIyULdF8xLywuJBEuACckEDwnOyR.NTwUIg==
LoginForm[username]:00000001
LoginForm[password]:00000001
LoginForm[verifyCode]:7567
login-button: