背景
以前用python做爬虫,就了解到scrapy框架,但是用了一会儿,总觉得用不明白。一直想做一个自己的爬虫,最近就用java自己diy了一个。为了不让自己忘了,就打算写一篇博客

爬虫基本结构

java反爬虫框架 java爬虫框架有哪些_ide


原谅我用画图画的。。。。。

主要分为五部分

  1. 调度器
  2. request请求器
  3. Parse解析器
  4. Save存储器
  5. Reader、Writer读取器
  6. url,html,item资源池

调度器
调度器包括CenterController,ParseController,RequestController以及SaveController四个。CenterController负责读取资源池中的三种数据,分发给其他三者,其他三者负责向资源池写入数据,并与各自的对应的功能模块交互。

request请求器

请求器结构

java反爬虫框架 java爬虫框架有哪些_java反爬虫框架_02


request请求器是多线程的模式,request通过读取器从urll资源池中读取访问地址。分配给管理器Manager,管理器分配给执行单元Bean。管理器和bean都采取线程池的模式。可以手动设置执行数目。Parse解析器

解析器结构

java反爬虫框架 java爬虫框架有哪些_ide_03


不同于请求器,解析器是单线程模式。管理器下的ParseBean其实任意时刻只有一个,但是可以自定义选取哪个解析器。这样可以针对不同的返回值处理。HtmlParsBean是处理html的请求返回值的。JsonParseBean是处理Json文本的返回值的。这两者都可以将请求器中获取的byte数组编码并封装进一个Item(Item见下面介绍)中。ByteParseBean是处理字节流文件比如图片文本的,它会将字节流直接封装进一个ByteItem中(也就是不会操作)。

这三个ParseBean是我预设的,如果有特别的需求,可以自己定义(如何定义使用见下方)。Save存储器

存储器结构

java反爬虫框架 java爬虫框架有哪些_html_04


和解析器一样是单线程的。主要用于将解析器中提取到的资源信息持久化存储,或者。大致可以分为3种,存入url池,存入磁盘,存入数据库。不过我没有提供具体的实现。

Reader、Writer读取器
这是用来访问资源池数据的。主要有三种用途。

  1. 在这里可以设置,一次访问的数据条数,存储进资源池的一批数据中数据的数目。同时为Reader设置了缓存。合理地设置读取器可以更好地适配请求器地多线程访问。
  2. 通过读取器我们可以设置资源池和磁盘的访问,当diskSave变量为true时,可以通过读取器每次访问资源池判断资源池数据是否过大,过大会写入磁盘,过少会从磁盘加载进数据。
  3. 可以通过读取器判断当前资源池中是否有资源。

读取器有六种,读取各3种。分别针对url资源池,html资源池和item资源池。可以通过一个IOFactory的工厂生产其代理对象,代理对象才有自动加载磁盘资源的功能。

资源池
url,html,item资源池在内存中对应三个ConcurrentLinkedQueue对象。为Source类的静态成员变量,当该类通过类加载器加载时,会自动识别扫描磁盘,是否有数据,有的话加载进内存。调用该类的close方法时,会自动将三个资源池写入磁盘。

控制
除了5个模块,还有两个重要的类,Template类和Item接口。

Item接口
Item是解析器最终传递给存储器的对象。主要用于保存解析器提取的信息。我定义了简单的规则,使其能够通过反射的方式,自动将网页或json中的需要的信息封装进Item中。

Template类
该类是控制整个爬虫的核心,用于编写爬虫的规则。里面有6个属性

  1. String urlReg 该模板对应的网址的正则
  2. HashMap<String,String> elementPath。html或json文件的需要提取元素的路径
  3. String charset 文本编码方式
  4. String item Item封装类的全类名
  5. String parseBean ParseBean的全类名
  6. String saveBean SaveBean的全类名

两个接口

  1. IParseBean 自定义ParseBean需要实现的接口,同时实现类需要有一个带Template参数的构造器
  2. ISaveBean 自定义SaveBean需要实现的接口,必须有空参构造器

以爬取笔趣阁的书籍目录为例,我要通过这个页面抓取到这三本书的目录

java反爬虫框架 java爬虫框架有哪些_ide_05


java反爬虫框架 java爬虫框架有哪些_ide_06


我定义第一个页面的存储信息类Item

public class BaseItem implements Item {
    private String url;//从哪个网址获取的资源
    private ArrayList<MyAtom> atoms = new ArrayList<>();
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public ArrayList<MyAtom> getAtoms() {
        return atoms;
    }
    public void setAtoms(ArrayList<MyAtom> atoms) {
        this.atoms = atoms;
    }
    @Override
    public String toString() {
        return "BaseItem{" +
                "url='" + url + '\'' +
                ", atoms=" + atoms +
                '}';
    }
}
public class MyAtom implements Atom {
    private String type;//第一个页面的作品类别。就是玄幻小说、修真小说那个元素
    private String title;//那三个类别里的推荐小说,就是仙武帝尊、三寸人间、最佳女婿
    private String chapterHref;//上面三本小说的网址
    private ArrayList<String> books;
    public MyAtom() {
    }
    public ArrayList<String> getBooks() {
        return books;
    }
    public void setBooks(ArrayList<String> books) {
        this.books = books;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getChapterHref() {
        return chapterHref;
    }
    public void setChapterHref(String chapterHref) {
        this.chapterHref = chapterHref;
    }
    @Override
    public String toString() {
        return "MyAtom{" +
                "author='" + author + '\'' +
                ", title='" + title + '\'' +
                ", chapterHref='" + chapterHref + '\'' +
                ", books=" + books +
                '}';
    }
}

第二个页面的存储Item

public class IntItem implements Item {
    private String url;//从哪个网址获取的资源
    private String title;//第二个页面的小说名称
    private ArrayList<String> chapters;//小说的各个目录,保存在数组中

    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public ArrayList<String> getChapters() {
        return chapters;
    }
    public void setChapters(ArrayList<String> chapters) {
        this.chapters = chapters;
    }
    @Override
    public void setUrl(String url) {
        this.url = url;
    }
    @Override
    public String getUrl() {
        return url;
    }
    @Override
    public String toString() {
        return "IntItem{" +
                "url='" + url + '\'' +
                ", title='" + title + '\'' +
                ", chapters=" + chapters +
                '}';
    }
}

设置第一个页面的SaveBean

public class DBSaveBean implements ISaveBean {
    @Override
    public ConcurrentLinkedQueue<String> start(Item item){
        if(item instanceof BaseItem){
            ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>();
            ArrayList<MyAtom> atoms = ((BaseItem) item).getAtoms();
            for (MyAtom atom : atoms) {
                urls.add(atom.getChapterHref());
            }
            return urls;//我这里直接返回了chapaterHref,这样就会把这个地址存入url池中
        }
        return null;
    }
}

设置第二个页面的SaveBean

public class DBSaveBean2 implements ISaveBean {

    @Override
    public ConcurrentLinkedQueue<String> start(Item item) throws IOException {
        if(item instanceof IntItem){
        //我将读取到的url写入到txt文件中
            item = (IntItem)item;
            File file = new File("D:\\webSpider\\source\\novel\\"+ UUID.randomUUID()+".txt");
            if(!file.exists()){
                file.createNewFile();
            }
            FileWriter fos = new FileWriter(file);
            fos.write(((IntItem) item).getTitle()+"\n");
            fos.write(((IntItem) item).getChapters().toString());
            fos.close();
        }
        //return null就不会将数据放入url池
        return null;
    }
}
public class SpiderTest {
	//创建模板
    private List<Template> getTemplate(){
        ArrayList<Template> lists = new ArrayList<>();
        //创建模板
        Template template = new Template();
        //设置第一个页面模板
        template.setCharset("utf-8");
        template.setUrlReg("http://www.xbiquge.la/");
        //Item中各个元素和页面中的css路径,如果提取的是属性值需要在元素css路径后加上;属性,比如这里的;href。
        HashMap<String, String> temp = new HashMap<>();
        //atoms是第一层路径
        temp.put("atoms","#main > div:nth-child(4) > div.content");
        //atoms.title后的css路径是在atoms筛选出的元素基础上再次筛选元素
        temp.put("atoms.title","h2");
        temp.put("atoms.chapterHref","div > dl > dt > a;href");
        temp.put("atoms.type","div > dl > dt > a");
        temp.put("atoms.books","ul > li > a");
        template.setElementPath(temp);
        template.setParseBean("com.wsf.parse.bean.impl.HtmlParseBean");//解析器的全路径,这里选择自带的HtmlParseBean
        template.setItem("com.itcast.item.BaseItem");//Item信息存储类的全路径
        template.setSaveBean("com.itcast.save.DBSaveBean");
        //处理器的全路径
        lists.add(template);


		//设置第二个页面模板
        Template template1 = new Template();
        template1.setCharset("utf-8");
        template1.setUrlReg("http://www.xbiquge.la/\\d+/\\d+/");
        template1.setParseBean("com.wsf.parse.bean.impl.HtmlParseBean");
        HashMap<String, String> temp2 = new HashMap<>();
        temp2.put("title","#info > h1");
        temp2.put("chapters","#list > dl > dd > a");
        template1.setItem("com.itcast.item.IntItem");
        template1.setSaveBean("com.itcast.save.DBSaveBean2");
        template1.setElementPath(temp2);
        lists.add(template1);
        return lists;
    }
    @Test
    public void testStartOneRequest() throws ClassNotFoundException {
        //加载配置文件
        Class.forName("com.wsf.config.Configure");
        //向url池中写入初始url地址
        WriteToUrl write = new WriteToUrl();
        //写入初始网址
        ConcurrentLinkedQueue<String> inBuffer = new ConcurrentLinkedQueue<>();
        inBuffer.add("http://www.xbiquge.la/");
        write.write(inBuffer);
        
        //创建中央控制器,导入模板规则
        CenterControllerImpl center = new CenterControllerImpl(getTemplate());
        //执行爬虫
        center.start();
        //关闭爬虫
        center.destroy();
    }
}

爬取完成

java反爬虫框架 java爬虫框架有哪些_html_07

再举个返回文件为Json的例子

以b站的这个json返回文件为例

java反爬虫框架 java爬虫框架有哪些_html_08


我们需要提取其中的typename,title和pic三个信息。

Item如下

public class BilibiliItem implements Item {
    private String url;
    private ArrayList<MyAtom2> atoms;
    public String getUrl() {
        return url;
    }
    public ArrayList<MyAtom2> getAtoms() {
        return atoms;
    }
    public void setAtoms(ArrayList<MyAtom2> atoms) {
        this.atoms = atoms;
    }
    @Override
    public void setUrl(String url) {
        this.url = url;
    }
    @Override
    public String toString() {
        return "BilibiliItem{" +
                "url='" + url + '\'' +
                ", atoms=" + atoms +
                '}';
    }
}
public class MyAtom2 implements Atom {
    private String typename;
    private String title;
    private String pic;
    public String getTypename() {
        return typename;
    }
    public void setTypename(String typename) {
        this.typename = typename;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getPic() {
        return pic;
    }
    public void setPic(String pic) {
        this.pic = pic;
    }
    @Override
    public String toString() {
        return "MyAtom2{" +
                "typename='" + typename + '\'' +
                ", title='" + title + '\'' +
                ", pic='" + pic + '\'' +
                '}';
    }
}

解析器选择自带的JsonParseBean
存储器设置如下

//图片网址对应的存储器
public class DBSaveBean3 implements ISaveBean {
    @Override
    public ConcurrentLinkedQueue<String> start(Item item) throws Exception {
        if(item instanceof ByteItem) {
            File file = new File("D:\\spiderTest\\images\\"+ UUID.randomUUID().toString()+".jpg");
            if(!file.exists()){
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(((ByteItem) item).getBytes());
            fos.close();
        }
        return null;
    }
}

//json网址对应的存储器
public class DBSaveBean4 implements ISaveBean {
    private Template template;

    @Override
    public ConcurrentLinkedQueue<String> start(Item item) throws Exception {
        if(item instanceof BilibiliItem){
            ConcurrentLinkedQueue<String> urls = new ConcurrentLinkedQueue<>();
            for (MyAtom2 atom : ((BilibiliItem) item).getAtoms()) {
                //将全部的图片地址存储
                urls.add(atom.getPic());
            }
            //加入到url资源池
            return urls;
        }
        return null;
    }
}

执行testStartOneRequest()方法

public class SpiderTest {
private List<Template> getTemplate(){
    ArrayList<Template> lists = new ArrayList<>();
    //创建模板
    Template template = new Template();
    template.setCharset("utf-8");
    template.setUrlReg("https://api.bilibili.com/x/web-interface/ranking/region\\?rid=33&day=3&original=0");
    HashMap<String, String> temp = new HashMap<>();
    temp.put("atoms","data");
    temp.put("atoms.typename","typename");
    temp.put("atoms.title","title");
    temp.put("atoms.pic","pic");
    template.setElementPath(temp);
    template.setParseBean(JsonParseBean.class.getName());
    template.setItem("com.itcast.item.BilibiliItem");
    template.setSaveBean("com.itcast.save.DBSaveBean4");
    lists.add(template);


    Template template1 = new Template();
    template1.setCharset("utf-8");
    template1.setUrlReg("http://i\\d.hdslb.com/bfs/archive/.*?.jpg");
    template1.setParseBean(ByteParseBean.class.getName());
    template1.setItem(ByteItem.class.getName());
    template1.setSaveBean("com.itcast.save.DBSaveBean3");
    lists.add(template1);
    return lists;
}
    @Test
    public void testStartOneRequest() throws ClassNotFoundException {
        //加载匹配
        Class.forName("com.wsf.config.Configure");
        WriteToUrl write = new WriteToUrl();
        //写入初始网址
        ConcurrentLinkedQueue<String> inBuffer = new ConcurrentLinkedQueue<>();
        inBuffer.add("https://api.bilibili.com/x/web-interface/ranking/region?rid=33&day=3&original=0");
        write.write(inBuffer);
        CenterControllerImpl center = new CenterControllerImpl(getTemplate());
        center.start();
        center.destroy();
    }
}

结果如下

java反爬虫框架 java爬虫框架有哪些_html_09

程序我打包成jar传到guthub上了。有兴趣可以看看,应该还有许多bug,如果有人发现bug,希望能有反馈。emmm,大概也没人用吧。

https://github.com/yyyhah/java-spider/tree/parse/java%E7%88%AC%E8%99%ABjar%E5%92%8C%E7%9B%B8%E5%85%B3%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6

ps:虽然做出了个多线程的爬虫,但是大部分情况下,爬取少量数据时(爬少量数据时,可以调整请求器Manager和Bean的线程池,剩下创建线程池的时间),并快不了多少。而且爬太快访问的网站受不了。像笔趣阁同时访问同一个页面10多次就会出现访问失败的情况了。还有也不知道有没有讲清楚template中elementPath的映射规则。我感觉自己都看不太懂。。。。。算了,如果真有人问再讲把。。。