關於java實現需要登入且帶驗證碼的定時網路爬蟲(爬取的資料存庫)
博主6月初的時候換了個工作,剛進來的時候什麼事沒有,愣是上班喝茶逛網站渡過了一週。那週週五的boss突然問我會不會爬蟲。
作為一個才工作一年的javaer表示根本沒接觸過,但是那種情況下你還敢說不會麼,但是當時也不敢說的很絕對,因此就和boss就會一點。
當時就隱隱約約有爬蟲任務了,感覺週末去突擊了一下。果不其然,下週一的時候給我一個賬號和密碼,讓我每隔5分鐘爬取該網站的客戶
資訊資料存到自己的資料庫,當時接到任務的時候一懵,和想象的不一樣啊,週末還要登入,**的還是要驗證碼的登入,並且還是定時的抓資料,
當時就一臉懵逼,還好咱會碼農最基本的技能--百度。經過幾天的各種百度之後,終於給做出來了。
由於公司主體框架是基於註解(mybatis也是註解)的SSM框架,資料庫Mysql,因此博主也是採用了這套框架,由於是獨立發到一個伺服器上,
不影響其他的程式碼,因此博主就盡情發揮了,有些程式碼寫的的確很爛,並且還遺留了幾個問題,什麼問題結尾再說,希望大家諒解,也看看能不能碰到大神幫我解決下。
這裡先介紹下具體思路,寫一個登入頁面,登入之後爬蟲系統就自動執行,然後每隔5分鐘自抓取一次資料,一直死迴圈(想不到其他方法了...)第一個問題是解決怎麼模擬登入目標網站的問題,
博士採用的是htmlunit這個框架,htmlunit可以模擬出瀏覽器頁面,使用它模擬出目標網站的登入頁面,然後抓取它的登入表單,獲取賬號、密碼、驗證碼輸入框和提交按鈕。
這樣就可以實現模擬登入。對於驗證碼的策略是當你進入登入頁面的時候就先發一個請求去獲取目標網站的驗證碼(博主的是圖片)存到伺服器,並顯示在自己登入頁的驗證碼框。
然後輸入賬號密碼和驗證碼登入即可。下面附程式碼。
專案結構
maven依賴
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>3.2.8.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>3.2.8.RELEASE</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.8.2</version> </dependency> <dependency> <groupId>net.sourceforge.htmlunit</groupId> <artifactId>htmlunit</artifactId> <version>2.16</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version> </dependency> </dependencies>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd"> <!-- 配置元件掃描 --> <context:component-scan base-package="com.jds"/> <!-- 配置mvc註解掃描,識別@RequestMapping --> <mvc:annotation-driven/> <!-- 配置檢視解析器 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- 在WEB-INF下 --> <property name="prefix" value=""/> <property name="suffix" value=".html"/> </bean> <!-- 讀取db.properties --> <util:properties id="jdbc" location="classpath:application.properties" /> <!-- 配置連線池 --> <bean id="ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="#{jdbc.driver}"/> <property name="url" value="#{jdbc.url}"/> <property name="username" value="#{jdbc.username}"/> <property name="password" value="#{jdbc.password}"/> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="ssfb" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 指定連線池 --> <property name="dataSource" ref="ds"/> <!-- 指定對映檔案 <property name="mapperLocations" value="classpath:mapper/*.xml "/> --> </bean> <!-- 配置MapperScannerConfigurer --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 指定對映器所在包 --> <property name="basePackage" value="com/jds/repository"/> </bean> <!-- 配置springmvc的value註解 --> <bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="locations"> <list> <value>classpath:application.properties</value> </list> </property> </bean> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer"> <property name="properties" ref="configProperties"/> </bean> </beans>
application.properties
#資料庫連線
driver=com.mysql.jdbc.Driver
url=你的資料庫地址
username=你的賬號
password=你的密碼
#連線池
initSize=5
maxSize=10
#延時時間(秒)
delayTimes=300
#首次讀取頁數
firstPage=3
#除首次每5分鐘讀取頁數
pages=1
#種類
type=AA22001,AA22002,AA22003,AA22004,AA22005,AA22006,AA22007,AA22008,AA22009
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<display-name>jds_pachong</display-name>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:conf/applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
工具類
package com.jds.util;
public class StringUtil {
/**
* 判斷空
* @param value
* @return
*/
public static boolean isEmpty(String value) {
return value == null || "".equals(value.trim());
}
}
package com.jds.util;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
public class TimeUtils {
/**
* 獲得指定時間的時間戳
* @param data
* @return
*/
public static int getTimeStampByDate(String data,String pattern){
Date date = format(data, pattern);
Long tm = date.getTime()/1000;
return tm.intValue();
}
private static Date format(String str, String pattern) {
DateFormat formatter = new SimpleDateFormat(pattern, Locale.ENGLISH);
Date date = null;
try {
date = (Date) formatter.parse(str);
} catch (ParseException e) {
return null;
}
return date;
}
/**
* 取得當前系統時間戳
* @param pattern eg:yyyy-MM-dd HH:mm:ss,SSS
* @return
*/
public static int getSysTimeStamp(String pattern) {
return getTimeStampByDate(formatSysTime(new SimpleDateFormat(pattern)),pattern);
}
private static String formatSysTime(SimpleDateFormat format) {
String str = format.format(Calendar.getInstance().getTime());
return str;
}
/**
* 獲取指定時間 之前或之後 幾分鐘的時間
* @param startTime 指定時間
* @param minute 分鐘
* @param type -1 之前的時間 ,1 之後的時間
* @return
*/
public static String getTimeByMinute(String startTime,int minute, String type ) {
//String startTime = "2018-05-10 11:10:50";
Date format = format(startTime, "yyyy-MM-dd HH:mm:ss");
long time = format.getTime();
if(type.equals("-1")){
time = time - (minute * 60 * 1000);
}else{
time = time + (minute * 60 * 1000);
}
String resultTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time);
return resultTime;
}
}
關於資料庫操作的實體類和service這裡不不加了,畢竟每個人的業務都不一樣,日誌相關可忽略。下面上核心程式
package com.jds.controller;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlImage;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
import com.jds.model.CoreThirdpartyCusEv;
import com.jds.service.CoreThirdpartyCusService;
import com.jds.service.SysConfigService;
import com.jds.util.TimeUtils;
@Controller
public class PaChongController {
/** 日誌*/
private static final Logger logger = LoggerFactory.getLogger(PaChongController.class);
final private static String PATTERN = "yyyy-MM-dd HH:mm:ss";
final private static int FiveMinutes = 300;
/** 模擬頁面*/
//private HtmlPage page;
/** 模擬瀏覽器*/
//private WebClient webClient;
/** 延時時間(秒)*/
@Value("${delayTimes}")
private int delayTimes;
/** 首次讀取頁數*/
@Value("${firstPage}")
private int firstPage;
/** 除首次每10分鐘讀取頁數*/
@Value("${pages}")
private int pages;
/** 種類*/
@Value("${type}")
private String[] type;
/** 儲存資料service*/
@Autowired
private CoreThirdpartyCusService coreThirdpartyCusEvService;
@Autowired
private SysConfigService sysConfigService;
@RequestMapping(value = "/login")//進入自己登入頁面的控制器
public String login(HttpServletRequest request , HttpServletResponse response){
logger.info("進入.................");
//建立一個webclient,指定火狐
WebClient webClient = new WebClient(BrowserVersion.FIREFOX_31);
request.getSession().setAttribute("webClient", webClient);
//引數設定
// 1 啟動JS
webClient.getOptions().setJavaScriptEnabled(true);
// 2 禁用Css,可避免自動二次請求CSS進行渲染
webClient.getOptions().setCssEnabled(false);
//3 啟動客戶端重定向
webClient.getOptions().setRedirectEnabled(true);
// 4 執行錯誤時,是否丟擲異常
webClient.getOptions().setThrowExceptionOnScriptError(false);
// 5 設定超時
webClient.getOptions().setTimeout(600000);
//6 設定忽略證書
//webClient.getOptions().setUseInsecureSSL(true);
//7 設定Ajax
webClient.setAjaxController(new NicelyResynchronizingAjaxController());
//8設定cookie
webClient.getCookieManager().setCookiesEnabled(true);
//設定js超時時間
webClient.setJavaScriptTimeout(30000);
try {
//先判斷是否已經登入 已經登入跳錯誤頁面
String path = request.getServletContext().getRealPath("/");
HtmlPage page = webClient.getPage("你的目標網站登入頁面");
request.getSession().setAttribute("page", page);
File file = new File(path+"image/yzm.png");//你的伺服器圖片地址
file.createNewFile();
HtmlImage vaCode=(HtmlImage) page.getElementById("captchaImage");//驗證碼圖片
//儲存圖片
vaCode.saveAs(file);
} catch (IOException e) {
e.printStackTrace();
}
return "html/login";
}
@RequestMapping(value = "/index")//點選登入的控制器
public String index(HttpServletRequest request , HttpServletResponse response){
try {
request.setCharacterEncoding("utf-8");
WebClient webClient = (WebClient) request.getSession().getAttribute("webClient");
HtmlPage page = (HtmlPage) request.getSession().getAttribute("page");
HtmlForm form = (HtmlForm) page.getElementById("loginForm");//登入表單
HtmlInput username = page.getHtmlElementById("username");//使用者名稱
HtmlInput pwd = page.getHtmlElementById("password");//密碼
HtmlInput captcha = page.getHtmlElementById("captcha");//驗證碼輸入框
HtmlSubmitInput btn = form.getInputByValue("登入");//登入按鈕
username.setAttribute("value", request.getParameter("username"));//賬號
pwd.setAttribute("value", request.getParameter("password"));//密碼
captcha.setAttribute("value", request.getParameter("captcha"));//驗證碼
// 等待JS驅動dom完成獲得還原後的網頁
HtmlPage page2 = btn.click();//登入之後的頁面
webClient.waitForBackgroundJavaScriptStartingBefore(20000);//設定js載入時間
DomNodeList<DomElement> Anchors = page2.getElementsByTagName("a");
HtmlAnchor btn2 = (HtmlAnchor) Anchors.get(7);//客戶檔案按鈕
System.out.println("登入成功................."+"時間:"+new Date());
HtmlPage page3 = btn2.click();//點選客戶檔案後的頁面
webClient.waitForBackgroundJavaScript(10000);
DomNodeList<DomElement> Anchors2 = page3.getElementsByTagName("a");
boolean flag = true;
//處理資料
List<Element> lists = new ArrayList<Element>();//第一次
List<Element> lists2 = new ArrayList<Element>();//增量
List<Element> listsTemp2 = new ArrayList<Element>();//臨時資料
ArrayList<CoreThirdpartyCusEv> cs = null;
ArrayList<CoreThirdpartyCusEv> cs2 = null;
while(true){
List<Element> listsTemp = new ArrayList<Element>(listsTemp2);//臨時資料
int count = flag==true?firstPage:pages;
for(int i=0;i<count;i++){//迴圈儲存資料
String info = null;
info = page3.asXml();
if(flag){
dealInfo(info,lists);//解析成html格式
}else{
dealInfo(info,lists2);//解析成html格式
}
if(i<count-1){
HtmlAnchor btn3 = (HtmlAnchor)Anchors2.get(Anchors2.size()-2);//">"按鈕
btn3.click();
webClient.waitForBackgroundJavaScriptStartingBefore(15000);
}
}
listsTemp2 = new ArrayList<Element>(lists2);//臨時資料
CoreThirdpartyCusEv c = null;//資訊實體
//迴圈的時候去重
if(lists2!=null && lists2.size()!=0){
for(Element e : listsTemp.size()!=0?listsTemp:lists){
String id = e.child(0).text();//id
for(int i=0;i<lists2.size();i++){
if(id.equals(lists2.get(i).child(0).text())){
lists2.remove(i);
}
}
}
}
// System.out.println("和上個頁面去重後資料(tr)"+lists2.size());
// if(lists2.size()>0){
// for(Element e :lists2){
// System.out.println(e);
// }
//
// }
cs = new ArrayList<CoreThirdpartyCusEv>();//資訊實體資料集合 第一次
cs2 = new ArrayList<CoreThirdpartyCusEv>();//增量資料
cs2 = flag == true ? dealList(lists,c,cs):dealList(lists2,c,cs);
System.out.println("當前時間段查詢數量(物件)"+cs2.size()+"時間:"+new Date());
if(cs2!=null && cs2.size()!=0){
//重啟的時候去重
if(flag){
ArrayList<CoreThirdpartyCusEv> CoreThirdpartyCusEvList = coreThirdpartyCusEvService.findThirdpartyCusList();
System.out.println("存量資料"+CoreThirdpartyCusEvList.size()+"時間:"+new Date());
if(CoreThirdpartyCusEvList !=null && CoreThirdpartyCusEvList.size()!=0){
for(CoreThirdpartyCusEv coreThirdpartyCus : CoreThirdpartyCusEvList){
for(int i=0;i<cs2.size();i++){
if(cs2.get(i).getCusName().equals(coreThirdpartyCus.getCusName()) && cs2.get(i).getCusMobile().equals(coreThirdpartyCus.getCusMobile()) && cs2.get(i).getCreateTime().equals(coreThirdpartyCus.getCreateTime())
&& cs2.get(i).getCusIdcard().equals(coreThirdpartyCus.getCusIdcard())){
cs2.remove(i);
}
}
}
}
}
System.out.println("儲存資料"+cs2.size()+"條");
for(CoreThirdpartyCusEv cc : cs2){
//存資料庫
coreThirdpartyCusEvService.insertThirdpartyCus(cc);
}
System.out.println("儲存成功...");
}
System.out.println("開始休眠"+delayTimes * 1000/60000+"分鐘");
Thread.sleep(delayTimes * 1000);//休眠5分鐘
flag = false;
page3 = btn2.click();//返回客戶檔案頁面
webClient.waitForBackgroundJavaScriptStartingBefore(15000);
}
}catch (Exception e) {
logger.info("程式出錯----------------------------------------"+new Date());
System.out.println("登入失敗"+new Date());
e.printStackTrace();
return "html/error2";
}
}
/**
* 儲存資料
* @param e
* @param c
* @param cs
* @return
* @throws ParseException
*/
public ArrayList<CoreThirdpartyCusEv> dealList(List<Element> lists,CoreThirdpartyCusEv c, ArrayList<CoreThirdpartyCusEv> cs) throws ParseException{
if(lists.size() == 0){
return cs;
}
for(Element e : lists){
c = new CoreThirdpartyCusEv();
String createTime = e.child(1).text();//建立時間
createTime = TimeUtils.getTimeByMinute(createTime , 25 , "1");
String nameAndMobile = e.child(2).text();
String[] strs = nameAndMobile.split("\\s+");//根據空格拆分
//拆分名字和手機號,如果沒有名字則放棄這條資料
if(strs.length > 1){
String name = strs[0];//
if("-".equals(name)){
continue;
}
String mobile = strs[1];//
c.setCusName(name);//
c.setCusMobile(mobile);//
}else{
continue;
}
String idcard = e.child(3).text();//
Date date = null;
SimpleDateFormat sdf = new SimpleDateFormat(PATTERN);
date = sdf.parse(createTime);
c.setCreateTime(date);//
c.setCusIdcard(idcard);//
c.setStatus("0");//
c.setThiridpartyCode(getRandomType(type));//
c.setZhimaScore(zhimaScore());
cs.add(c);
}
return cs;
}
/**
* 解析、封裝資料
* @param info
*/
public void dealInfo(String info,List<Element> lists){
Document document = Jsoup.parse(info);//轉成DOM格式方便解析
Element table = document.getElementById("query-table");//資料節點id
Element tbody = table.select("tbody").first();
Elements trs = tbody.select("tr");
for(Element tr : trs){
if((TimeUtils.getSysTimeStamp(PATTERN)-TimeUtils.getTimeStampByDate(tr.child(1).text(), PATTERN))>FiveMinutes){
lists.add(tr);
}
}
}
/**
* 隨機生成600-700的整數
* @return
*/
public int zhimaScore(){
Random r = new Random();
int score = r.nextInt(100)+601;
return score;
}
/**
* 生成隨機的種類({"AA22001","AA22002","AA22003","AA22004","AA22005","AA22006","AA22007","AA22008","AA22009"})
* @return
*/
public String getRandomType(String[] type){
Random r = new Random();
int num = r.nextInt(type.length);
return type[num];
}
}
請大家忽略有關資料庫操作的一切程式碼。
大體就是這樣的邏輯了,登入頁面還是自己寫吧。最後說下存在的問題,第一個就是死迴圈的問題,這樣會導致記憶體一直增加(不過到現在運行了1個多月了似乎還沒出現),第二個就是作用域的問題,如果重複登入的話會導致伺服器執行2個該程式,導致原本5分鐘的迴圈時間會縮減(5分鐘內爬存2次)。有沒有大神能提點下。。。
這篇也是博主的第一個個人部落格,上面關於html物件的選擇器還能優化下,建議去看看htmlunit對於html節點物件的選擇器,其中很多地方寫的不好(個人感覺就思路可以借鑑下,程式碼看看就好),敬請見諒。PS:如果需要的人多的話我就改改程式碼發個能執行的程式碼包。比較不能洩露公司機密嘛。
如有出資料庫操作之外的遺漏請通知。