java爬虫框架diy

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

爬虫基本结构
在这里插入图片描述
原谅我用画图画的。。。。。

主要分为五部分

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

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

request请求器
请求器结构
在这里插入图片描述
request请求器是多线程的模式,request通过读取器从urll资源池中读取访问地址。分配给管理器Manager,管理器分配给执行单元Bean。管理器和bean都采取线程池的模式。可以手动设置执行数目。

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

Save存储器
存储器结构
在这里插入图片描述
和解析器一样是单线程的。主要用于将解析器中提取到的资源信息持久化存储,或者。大致可以分为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需要实现的接口,必须有空参构造器

以爬取笔趣阁的书籍目录为例,我要通过这个页面抓取到这三本书的目录
在这里插入图片描述
在这里插入图片描述
我定义第一个页面的存储信息类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();
    }
}

爬取完成
在这里插入图片描述

再举个返回文件为Json的例子
以b站的这个json返回文件为例
在这里插入图片描述
我们需要提取其中的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();
    }
}

结果如下

在这里插入图片描述

程序我打包成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的映射规则。我感觉自己都看不太懂。。。。。算了,如果真有人问再讲把。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值