爬虫获取公司通讯录

 

什么是爬虫

一般的,平时说的爬虫总是指基于http(s)的爬虫. 其任督二脉就是http协议.

基于java开发,就是一个简单的java se 应用. 利用java 发http请求模拟真实用户的行为 然后假装自己是浏览器,解析html文档.抽取出有用的信息.根据业务需要,深度遍历或者广度遍历链接地址, 不停的抽取信息.达到目的. 常见的 浏览器各种比价插件,抢火车票等等.

开整

目标网站是http://erp.hfvast.com:888.

首先分析重点/难点

  1. 查看通讯录需要登录,需要验证码识别.
  2. 有状态http 需要维持cookie.
  3. 返回的html 需要 抽取信息.

初始解决方法

  1. 因为是爬虫,使用之前用过的webmagic开源框架(后续所有的开源框架都会给出github地址)进行页面抽取
  2. 验证码使用tess4j开源框架 获取识别的码 后来发现该开源项目依赖c的dll文件 不能做到跨平台, 在阿里找到的图片识别的api.
  3. 手动维持 cookie
  4. 利用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++;
    }
}

现在就可以获取到所有员工的信息 包括手机号,邮箱 做一个小程序,在手机上就可以直接点击拨打电话,发邮件.

总结及思考

  1. 根据业务选择合适的框架 有状态(无状态) 跑批(关键数据)
  2. http协议必须要很熟悉
  3. session和cookie
  4. 反爬虫 文件 /robots.txt eg: http://www.baidu.com/robots.txt http://www.taobao.com/robots.txt
  5. io流 base64
  6. Chrome DevTools Protocol 协议(headless chrome) 通过websocket协议 遥控 chrome浏览器 这里是各语言的实现
  7. 自动化测试要怎么搞

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();
})();

可以看到除了验证码外其他都不是问题了.

地址