什么是爬虫
一般的,平时说的爬虫总是指基于http(s)的爬虫. 其任督二脉就是http协议.
基于java开发,就是一个简单的java se 应用. 利用java 发http请求模拟真实用户的行为 然后假装自己是浏览器,解析html文档.抽取出有用的信息.根据业务需要,深度遍历或者广度遍历链接地址, 不停的抽取信息.达到目的. 常见的 浏览器各种比价插件,抢火车票等等.
开整
目标网站是http://erp.hfvast.com:888
.
首先分析重点/难点
- 查看通讯录需要登录,需要验证码识别.
- 有状态http 需要维持cookie.
- 返回的html 需要 抽取信息.
初始解决方法
- 因为是爬虫,使用之前用过的
webmagic开源框架(后续所有的开源框架都会给出github地址)
进行页面抽取 - 验证码使用
tess4j开源框架
获取识别的码 后来发现该开源项目依赖c的dll文件 不能做到跨平台, 在阿里找到的图片识别的api. - 手动维持 cookie
- 利用webmagic 集成的 xpath 进行员工信息抽取
总得思路就是 第一步发http获取验证码图片 交给tess4j识别出数字, 然后和 用户名 密码一起submit到后台.正常就应该登录成功了.然后再访问通讯录页面 使用xpath进行信息抽取,存到list中,最后打印.
开撸
新建 maven module hfvast-spider
pom.xml 添加webmagic依赖
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>${webmagic.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>${webmagic.version}</version>
<exclusions>
<exclusion>
<artifactId>jedis</artifactId>
<groupId>redis.clients</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-saxon</artifactId>
<version>${webmagic.version}</version>
</dependency>
抓包获取各主要的页面url
这里使用fiddler 进行抓包
- 验证码
/imageCode.code
- 登录
/loginAction.do?type=getClient
- 查看通讯录
/addressListAction.do?type=outprint
获取验证码图片
//修改UserAgent 模拟Chrome浏览器
private static Site site = Site.me()
.setRetryTimes(2)
.setSleepTime(100)
.setCharset("UTF-8")
.setUserAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36");
public static SimpleHttpClient simpleHttpClient = new SimpleHttpClient(site);
private static Request request = new Request();
/**
* @return 识别成功的验证码
* @throws Exception
*/
public static void login() throws Exception {
request.setCharset("gbk");
request.setUrl(SPIDER_CAPTCHA_URL);
//获取验证码图片
Page captchaPage = simpleHttpClient.get(request);
//图片转文本
String captchaString = getCaptcha(captchaPage.getBytes());
logger.debug("本次识别的验证码: " + captchaString);
request.setUrl(SPIDER_LOGIN_URL);
HttpRequestBody httpRequestBody = new HttpRequestBody();
httpRequestBody.setBody(("clientid=0001&userid=" + SPIDER_USERNAME + "&password=" + SPIDER_PASSWORD + "&yzm=" + captchaString + "&Submit=%CC%E1%BD%BB").getBytes("utf-8"));
request.setRequestBody(httpRequestBody);
request.setMethod("post");
Page loginResult = simpleHttpClient.get(request);
System.out.println(loginResult.getRawText());
}
run 之后 发现 一直登录失败. 猜想是cookie 出了问题. debug发现果然 登录时没有附带请求验证码的cookie,后台服务器 检测到验证码不匹配,登录失败.
突然想起来httpClient 是可以做到cookie保持的. 切换成httpClient.
切换到httpClient
<!--去除webmagic 依赖 添加httpclient依赖-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.4</version>
</dependency>
private static CloseableHttpClient httpClient = HttpClients.createDefault();
public static void login() throws Exception {
HttpGet httpGet = new HttpGet(SPIDER_CAPTCHA_URL);
CloseableHttpResponse response1 = null;
try {
response1 = httpClient.execute(httpGet);
HttpEntity entity1 = response1.getEntity();
byte[] bytes = EntityUtils.toByteArray(entity1);
//图片转文本
String captchaString = getCaptcha(bytes);
logger.debug("本次识别的验证码: " + captchaString);
EntityUtils.consume(entity1);
} finally {
try {
if (response1 != null) {
response1.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这次发现阿里的识图api不好使,返回无结果,少字段.果断换市场,找到了腾讯优图的通用印刷体识别api,进行集成,成功的返回了识别后的验证码.
现在已经有了验证码字符串,变量httpClient
持有了cookie,再结合用户名,密码,进行提交,应该就可以了
添加以下代码
HttpPost httpPost = new HttpPost(SPIDER_LOGIN_URL);
CloseableHttpResponse response2 = null;
try {
List<NameValuePair> parameters = new ArrayList<>();
parameters.add(new BasicNameValuePair("clientid","0001"));
parameters.add(new BasicNameValuePair("userid",SPIDER_USERNAME));
parameters.add(new BasicNameValuePair("password",SPIDER_PASSWORD));
parameters.add(new BasicNameValuePair("yzm",captchaString));
parameters.add(new BasicNameValuePair("Submit", new String("提交".getBytes("utf-8"),"gbk")));
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(parameters);
httpPost.setEntity(urlEncodedFormEntity);
response2 = httpClient.execute(httpPost);
HttpEntity entity2 = response2.getEntity();
Header[] allHeaders = response2.getAllHeaders();
for (Header allHeader : allHeaders) {
System.out.println(allHeader);
}
EntityUtils.consume(entity2);
if(response2.getStatusLine().getStatusCode()!=302){
throw new Exception("模拟登录失败");
}
logger.info("模拟登录成功!!");
} finally {
try {
if (response2 != null) {
response2.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
经过不断的调试修改 终于登录成功.要注意的是,erp系统登录后会立马302跳转两次,需要修改httpClient.
private static CloseableHttpClient httpClient = HttpClients.createDefault();
↓
private static CloseableHttpClient httpClient = HttpClients.custom()
.disableAutomaticRetries() //关闭自动处理重定向
.setRedirectStrategy(new LaxRedirectStrategy()).build();//利用LaxRedirectStrategy处理POST重定向问题
进行通讯录页面信息获取
上一节已经成功登录,接下来访问通讯录.涉及到xpath pom.xml
中添加jsoup依赖
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
//get方法 封装了每次发请求的细节 返回报文正文
String first = get(SPIDER_TEL_URL);
//使用jsoup解析 html
Document document = Jsoup.parse(first);
//使用xpath 定位下拉框 获取 option元素集合
Elements select = document.select("select[name='deptid'] > option");
List<Map> list = new ArrayList<>();
for (Element element : select) {
//根据每一个option的value 发请求 获取每个部门的员工信息 保存到list中
searchDept(element.val(),list);
}
list.forEach(System.out::println);
//使用gson 序列化list
String json = new Gson().toJson(list);
System.out.println(json);
private static void searchDept(String deptId,List list) throws Exception{
String html = get(SPIDER_TEL2_URL + deptId);
Document document = Jsoup.parse(html);
Elements select1 = document.select("table:has(table) table tbody tr");
for (Element element : select1) {
Map map = new LinkedHashMap();
Elements children = element.children();
for (int i = 1; i <= children.size(); i++) {
String s = mapTitle.get(i);
if (s != null) {
map.put(s, element.child(i - 1).text());
}
}
list.add(map);
}
}
优化
超时问题
private static RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(1000)//从连接池获取连接的超时时间
.setSocketTimeout(5000)//读超时时间
// .setProxy(new HttpHost("127.0.0.1",8888))//设置代理 fiddler 抓包调试
.setConnectTimeout(5000).build();//连接超时时间
httpGet.setConfig(requestConfig);
日志log4j2
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<!--替换使用log4j 升级版 log4j2-->
<Loggers>
<!--监控系统信息-->
<Logger name="com.youtu" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.apache" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="weihai4099.github.io" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<!--输出到NoSQL中-->
<!--<Logger name="mongoLog" level="trace" additivity="false">-->
<!--<AppenderRef ref="databaseAppender"/>-->
<!--</Logger>-->
<Root level="debug">
<!-- 这儿为trace表示什么都可以打印出来了,其他几个级别分别为:TRACE、DEBUG、INFO、WARN、ERROR和FATAL -->
<Appender-Ref ref="Console"/>
<Appender-Ref ref="INFO"/>
<!--<Appender-Ref ref="ERROR"/>-->
<!--<Appender-Ref ref="FATAL"/>-->
</Root>
</Loggers>
添加properties
将关键数据保存到properties文件,将此文件添加到ignore中
尝试机制
由于ocr技术不能保证100%,添加尝试机制,尝试五次
int i = 1;
while (i <= 5) {
try {
logger.info(String.format("开始第%d次 模拟登录", i));
login1();
break;
} catch (Exception e) {
e.printStackTrace();
i++;
}
}
现在就可以获取到所有员工的信息 包括手机号,邮箱 做一个小程序,在手机上就可以直接点击拨打电话,发邮件.
总结及思考
- 根据业务选择合适的框架 有状态(无状态) 跑批(关键数据)
- http协议必须要很熟悉
- session和cookie
- 反爬虫 文件
/robots.txt
eg: http://www.baidu.com/robots.txt http://www.taobao.com/robots.txt - io流 base64
- Chrome DevTools Protocol 协议(
headless chrome
) 通过websocket协议 遥控 chrome浏览器 这里是各语言的实现 - 自动化测试要怎么搞
eg: 以googleChrome官方的es6实现 demo
const puppeteer =require('puppeteer');
const CREDS = require('./creds');
const USERNAME_SELECTOR ='#userMail';
const PASSWORD_SELECTOR = '#userPassword';
const BUTTON_SELECTOR = '#account_login > form > div > div.form-item.form-button > button';
const TEXTAREA_SELECTOR = '#consulting_content';
const BUTTON2_SELECTOR = '#v-comment > section > div.box.com-item.com-form > div > div > button';
(async() => {
//以headless 模式启动一个浏览器
const browser = await puppeteer.launch({headless:true});
//打开一个新标签页
const page = await browser.newPage();
//访问此网址
await page.goto('https://www.oschina.net/home/login?goto_page=https%3A%2F%2Fwww.oschina.net%2F');
//点击用户名 input框
await page.click(USERNAME_SELECTOR);
//输入用户名
await page.type(CREDS.username);
//点击密码输入框
await page.click(PASSWORD_SELECTOR);
//输入密码
await page.type(CREDS.password);
//点击登录按钮
await page.click(BUTTON_SELECTOR);
//等页面跳转完成
await page.waitForNavigation();
//跳转到对应的页面
await page.goto('http://www.oschina.net/p/puppeteer-nodejs');
// await page.waitFor(10*1000);
//点击评论框
await page.click(TEXTAREA_SELECTOR);
//输入评论
await page.type('很好');
//点击提交评论按钮
await page.click(BUTTON2_SELECTOR);
//关闭浏览器
browser.close();
})();
可以看到除了验证码外其他都不是问题了.