博主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:如果需要的人多的话我就改改代码发个能运行的代码包。比较不能泄露公司机密嘛。
如有出数据库操作之外的遗漏请通知。