爬虫(五)——用户(所有)爬取+常用爬虫正则整理

说明

  • 开发环境:jdk1.7+myeclipse10.7+win74bit+mysql5.5+webmagic0.5.2+jsoup1.7.2

爬虫框架:webMagic

  • 建议:建议首先阅读webMagic的文档,再查看此系列文章,便于理解,快速学习:开发所需jar下载(不包括数据库操作相关jar包):点我下载
  • 该系列文章会省略webMagic文档已经讲解过的相关知识。

概述

  • 我们会从个人中心出发,首先爬取一个用户的个人信息。然后根据该用户的好友关系去爬取好友信息。依次类推,爬取所用用户。
  • 爬取所有用户是根据“粉丝、关注”去爬取“粉丝、关注”,必然会涉及到“死循环”。到后期肯定会出现大量“脏数据”(重复数据),就要考虑到过滤脏数据的问题。
  • 虽然用webMagic框架爬虫会用到大量的正则表达式,并且爬虫类的正则在网上也很少能找到资料,但是也是比较固定。

用户(所有)爬取代码预览

package com.wgyscsf.spider;

import java.util.List;

import org.jsoup.select.Elements;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;

import com.wgyscsf.utils.MyStringUtils;

/**
 * @author 高远</n>
 * 编写日期   2016-9-24下午7:25:36</n>
 * 邮箱  wgyscsf@163.com</n>
 * 博客  ;
 * TODO</n>
 */
public class CsdnMineSpider implements PageProcessor {
    private final String TAG = CsdnMineSpider.class.getSimpleName();
    private Site site = Site
            .me()
            .setDomain("my.csdn.net")
            .setSleepTime(1000)
            // 便于测试,休眠较长时间。
            .setUserAgent(
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");

    @Override
    public void process(Page page) {
        // 列表页: 这里进行匹配,匹配出列表页进行相关处理。
        if ((page.getUrl()).regex("\\w+").match()) {
            // 获取最外层节点
            // TODO:应该是get(1),不知道为什么
            Elements mainElements = page.getHtml().getDocument()
                    .getElementsByTag("div").get(2).children();
            // 个人资料
            Elements profileElements = mainElements.get(0).getElementsByTag(
                    "div");
            // 个人技能
            Elements skillElements = mainElements.get(1)
                    .getElementsByTag("div");
            // 关系模块:关注和被关注
            Elements relationElements = mainElements.get(2).getElementsByTag(
                    "div");

            // 获取用户id
            String id_mine = MyStringUtils.getLastSlantContent(skillElements
                    .get(0).getElementsByTag("a").get(0).attr("href"));
            // 开始获取个人资料
            String headImg = profileElements.get(0)
                    .getElementsByClass("person-photo").get(0)
                    .getElementsByTag("img").attr("src");
            String fansNums = profileElements.get(0)
                    .getElementsByClass("fans_num").get(0)
                    .getElementsByTag("b").get(0).text();
            String nickname = profileElements.get(0)
                    .getElementsByClass("person-nick-name").get(0)
                    .getElementsByTag("span").get(0).text();
            // 这里只能精确到个人资料,没法继续分,因为好多用户该栏目只填写部分内容
            String personDetail = profileElements.get(0)
                    .getElementsByClass("person-detail").get(0).text();

            // 开始组织个人资料,保存数据操作
            System.out.println(TAG + ":用户id:" + id_mine + ",昵称:" + nickname
                    + ",粉丝:" + fansNums + ",个人资料概述:"
                    + personDetail + ",其它信息....");

            // 当前爬取页面信息爬取结束,将当前页面设置为“跳过”,下次再加入爬虫队列,直接过滤,提高爬取效率。
            page.setSkip(true);
            // 测试,看是否再被爬取。
            page.addTargetRequest("wgyscsf");
            /*
             * 核心部分,从关系模块出发,去遍历所有相关用户!
             */
            // 开始获取关注与被关注以及访客的个人中心。同时加入爬虫队列。
            String html = relationElements.get(0).html();
            List<String> all = new Html(html)
                    .xpath("//div[@class=\"mod_relations\"]").links().all();
            // 加入到爬虫队列
            page.addTargetRequests(all);

        }
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        Spider.create(new CsdnMineSpider())
                .addUrl("wgyscsf").thread(1)// 便于测试,只开一个线程,正常爬起来,可以开15不成问题。
                .pipeline(null)
                .run();
    }

}

关键代码解释

  • 正则 \\w+"表示过滤出以"开头的网址链接。不过该正则存在一定的问题,就是只对网址前面部分进行了限制,并没有对后面进行限制。只要以"开头的网址全部会被加入到爬虫队列,存在大量的“脏数据”网址。比较合理的是:"后面可以出现除了“/”的任意字符。正则的准确性直接影响到爬虫的效率,越准确越好。这里不再修改。
  • profileElements.get(0).getElementsByClass("person-detail").get(0).text();该代码片段是为了获取用户的个人资料,该资料比较多,包括行业、职业、地区(国、省、市、区等)、姓名等信息。经过分析没有好的依据对信息进行归类,这里只是获取粗略信息。经过分析,甚至说官方最开始对该块没有进行合理的安排,导致用户的信息没有统一的格式。
  • 以下代码片段是该部分的核心代码,实现了递归式的爬取所有用户。relationElements元素来自于mainElements.get(2).getElementsByTag("div");,属于“关系模块”,包括关注的人和被关注的人以及访客信息。在这个模块中可以获取部分其它用户的id,只不过是部分的,最多只有6个人信息。官方没有提供获取所有粉丝或者关注者的信息。不过,只要这仅仅的用户信息,我们就可以爬取所有的用户。方便的是,顺便可以过滤掉“死鱼”用户(没有相互关系的用户)。
// 开始获取关注与被关注以及访客的个人中心。同时加入爬虫队列。
    String html = relationElements.get(0).html();
    List<String> all = new Html(html)
            .xpath("//div[@class=\"mod_relations\"]").links().all();
    // 加入到爬虫队列
    page.addTargetRequests(all);
  • 需要说明的是,爬取所有用户,其实不是一定要定位到“关系”模块。我们甚至可以简单粗暴的直接获取user_id"中所用有效链接,直接加入到爬虫队列。到时候直接通过正则\\w+"过滤出有效链接即可。代码如下:
// 直接取出该网页下的所有网址链接,简单粗暴,不用进行判断。
    // 只要在进入爬取用户信息的时候加正则匹配即可。
    // 但是会出现过多的“脏数据”,增加判断,影响爬取效率。
    List<String> all = page.getHtml().links().all();
    System.out.println(all);// 测试打印的网址链接
    // 加入到爬虫队列
    page.addTargetRequests(all);
  • 正如前文概述中所说,递归时的爬取用户信息,会出现大量已经爬取过的页面,我们需要“过滤”掉这些信息。核心的代码是page.setSkip(true);,爬取之后,直接设置为“跳过”,下次爬取会直接跳过该链接。webMagic的作者对该方法的解释如下:

/**

  • Set whether to skip the result.
  • Result which is skipped will not be processed by Pipeline.
    *
  • @param skip whether to skip the result
  • @return this
    */
  • 过滤代码如下:
// 当前爬取页面信息爬取结束,将当前页面设置为“跳过”,下次再加入爬虫队列,直接过滤,提高爬取效率。
    page.setSkip(true);
    // 测试,看是否再被爬取。
    page.addTargetRequest("wgyscsf");

爬取结果预览

  • 爬取所有用户
  • 爬取所有user_id"内网址

常用爬虫正则

  • \\w+" :过滤出所有以开头的网址。
  • ^((?!/).)*$:过滤出以开头,并且后面不能再出现“/”的所有网址链接。
  • ^\\w+/article/list/[0-9]*[1-9][0-9]*$:过滤出后面是任意字符,并且紧接着/article/list/,且/article/list/后面只能是数字的所有网址链接。
  • \\w+/article/details/\\w+:效果同上,只是最后允许任意字符,不仅仅限于数字。

测试正则的方式

Pattern pattern = Pattern
            .compile("^\\w+/article/list/[0-9]*[1-9][0-9]*$");
    Matcher matcher = pattern
            .matcher("wgyscsf/article/list/32423");
    boolean b = matcher.matches();
    // 当条件满足时,将返回true,否则返回false
    System.out.println(b);

操作代码(代码已全部迁移至github,欢迎star)