背景
以前用python做爬虫,就了解到scrapy框架,但是用了一会儿,总觉得用不明白。一直想做一个自己的爬虫,最近就用java自己diy了一个。为了不让自己忘了,就打算写一篇博客
爬虫基本结构
原谅我用画图画的。。。。。
主要分为五部分
- 调度器
- request请求器
- Parse解析器
- Save存储器
- Reader、Writer读取器
- 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读取器
这是用来访问资源池数据的。主要有三种用途。
- 在这里可以设置,一次访问的数据条数,存储进资源池的一批数据中数据的数目。同时为Reader设置了缓存。合理地设置读取器可以更好地适配请求器地多线程访问。
- 通过读取器我们可以设置资源池和磁盘的访问,当diskSave变量为true时,可以通过读取器每次访问资源池判断资源池数据是否过大,过大会写入磁盘,过少会从磁盘加载进数据。
- 可以通过读取器判断当前资源池中是否有资源。
读取器有六种,读取各3种。分别针对url资源池,html资源池和item资源池。可以通过一个IOFactory的工厂生产其代理对象,代理对象才有自动加载磁盘资源的功能。
资源池
url,html,item资源池在内存中对应三个ConcurrentLinkedQueue对象。为Source类的静态成员变量,当该类通过类加载器加载时,会自动识别扫描磁盘,是否有数据,有的话加载进内存。调用该类的close方法时,会自动将三个资源池写入磁盘。
控制
除了5个模块,还有两个重要的类,Template类和Item接口。
Item接口
Item是解析器最终传递给存储器的对象。主要用于保存解析器提取的信息。我定义了简单的规则,使其能够通过反射的方式,自动将网页或json中的需要的信息封装进Item中。
Template类
该类是控制整个爬虫的核心,用于编写爬虫的规则。里面有6个属性
- String urlReg 该模板对应的网址的正则
- HashMap<String,String> elementPath。html或json文件的需要提取元素的路径
- String charset 文本编码方式
- String item Item封装类的全类名
- String parseBean ParseBean的全类名
- String saveBean SaveBean的全类名
两个接口
- IParseBean 自定义ParseBean需要实现的接口,同时实现类需要有一个带Template参数的构造器
- 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的映射规则。我感觉自己都看不太懂。。。。。算了,如果真有人问再讲把。。。