Java爬虫入门(六)——课程设计报告

Java高级程序设计课程设计任务书
一 题目
Java并发爬取静态小说系统
二 目的与要求:
2.1目的:
JAVA爬虫并发爬取静态小说网站的全部小说:
https://www.bookbao8.com/BookList-c_0-t_2-o_1.html

2.2要求:
(1)掌握Java高级程序设计的基础知识,爬虫知识,线程池连接池和正则表达式匹配以及相关多线程内容进行Java爬虫.
(2)将Java和爬虫的理论知识和实际项目结合起来,熟练掌握Eclipse等开发工具,锻炼应用开发能力.
要求:
(1):要求利用软件工程的方法来完成系统的设计
(2):要求学生掌握Java爬虫的方法,熟练使用正则表达式
(3):能够进行基本的多线程操作
(4):能够完成基本的网络通信
最终实现目标:使用JAVA爬虫并发爬取静态小说网站的全部小说

三 主要内容及技术要求
3.1 运行环境:

  1. Eclipse
    3.2所需知识:
  2. HttpClient请求
  3. 连接池并发
  4. 线程池并发
  5. 多线程并发
  6. 正则表达式
  7. IO流保存本地文件
  8. 需要的jar包管理:

四 主要参考资料

  1. Effective Java(第2版),(美)Joshua Bloch(约书亚•布洛赫), 电子工业出版社, 2016.3.

2.自己动手写网络爬虫, 罗刚 / 王振东 , 清华大学出版社, 2010.10
3.Java编程思想, (第4版) [美] Bruce Eckel, 机械工业出版社, 2007-6

概述:

网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动得抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
随着网络的迅速发展,万维网成为大量信息的载体,如何有效地提取并利用这些信息成为一个巨大的挑战。搜索引擎(Search Engine),例如传统的通用搜索引擎如AltaVista,Yahoo!和Google等,作为一个辅助人们检索信息的工具成为用户访问万维网的入口和指南。但是,这些通用性搜索引擎也存在着一定的局限性,为了解决上述问题,定向抓取相关网页资源的聚焦爬虫应运而生。

同学一:
五 需求分析:
5.1系统业务分析
爬取指定页面小说的小说信息是本系统的业务主线.业务目标是访问指定页面小说并且下载点击率最多板块的所有小说信息以及章节目录.本系统设计时访问静态页面并且爬取小说信息不需要会员登录,直接在浏览器输入网站地址即可.一个页面内有多本小说,而且分页.综合分析核心业务如图2-1所示。

图2-1系统核心业务主线图

六 系统分析:
6.1 分析系统功能:
6.1.1并发处理分页功能:
并发处理分页线程类AddBookUrlThread调用run方法,创建GetContent类,调用GetContent类的getCOntent方法获得分页信息.根据分页信息将爬取分页信息的URL地址传入到线程池中Pool的静态方法execute中,可以实现多并发处理分页小说信息.如图2-5所示 图2-5并发处理分页时序图

七 系统设计
7.1 类属性和方法的命名规则

7.1.1 类属性命名规则:

对于属性的命名,要求属性的名称简单易懂,做到见名知意。具体例子如下所示:

public class Book {
private String book_name; // 书本名字
private String author; // 作者
private String type; // 类型
private String status; // 连载状态
private String update_time; // 更新时间
private String book_Introduction; // 书本简介
private String url; // 书本链接
}

7.1.2 方法请求的命名规则:
public class AdBookChaptersThread implements Runnable {
public boolean getBookInformation(Book book) {}
}

7.2 系统类设计
7.2.1 实体类相关设计:

与小说相关的实体类是小说类,应该设置为Book,包含如下属性:
book_name; // 书本名字
author; // 作者
type; // 类型
status; // 连载状态
update_time; // 更新时间
book_Introduction; // 书本简介
url; // 书本链接
以上信息都可以通过String字符串存储信息,
为了满足面向对象编程的操作,应该将实体类属性设置为private访问属性,通过set和get方法获取和设置属性值.这样子有利于封装属性,外部无法轻易修改实体类的属性,保证了安全性和标准性.

7.2.2 并发处理分页信息类相关设计:

AddBookUrlThread类实现Runnable接口,是一个线程类,设计为线程类的目的是为了并发操作.包含url使用String字符串存储,保存传递的分页URL地址.包含GetContent类可以获取分页中的小说息.AddBookUrlThread的构造器接收String的地址参数,保存传入的分页地址.重写run方法,其中调用content类的getContent方法获取分页信息,使用正则表达式获取该分页信息上的所有小说信息,如书名以及章节链接,通过循环将爬取具体小说信息的线程加入到线程池.
八 系统实现:
8.1 爬取指定url的html源码和总页数:

  1. 包含 BufferedReader缓冲流属性提取爬取的html内容.
  2. 包含 StringBuilder可修改的字符串动态追加提取到的html内容.
  3. getContent()方法通过 URI和HttpGet 链接指定网络,将连接HttpGet加入到连接池中以便下次使用.创建缓冲流 BufferedReader按行读取内容,循环读取内容并且 使用StringBuilder动态字符串追加,并返回指定url的html源码.
  4. PageAll()方法通过正则表达式:"/共(\d+?)页"返回指定url中html内容中的总页数.

public class GetContent {
private BufferedReader bufferedReader = null;
private StringBuilder conTent = null;
/**
* 爬取指定网页的全部内容
*
* @param urlin
* 传入地址
* @return 爬取指定网页的全部内容
* @throws URISyntaxException
* @throws IOException
/
public String getContent(String urlin) throws URISyntaxException, IOException {
// 获取爬取网络的地址
URI url = new URIBuilder().setScheme(“https”).setHost(urlin).build();
conTent = new StringBuilder();
HttpGet httpGet = new HttpGet(url);
/

* httpGet.setHeader(“User-Agent”,
* “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36”
* );
/
CloseableHttpResponse httpResponse = null;
try {
httpResponse = Pool.httpClient.execute(httpGet);
HttpEntity entity = httpResponse.getEntity();
// 获取编码信息
InputStream is = entity.getContent();
String line = “”;
// 转换为缓冲流,提高效率,可以按行读取
bufferedReader = new BufferedReader(new InputStreamReader(is, “utf-8”));
while ((line = bufferedReader.readLine()) != null) {
conTent.append(line);
}
is.close();
return conTent.toString();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
if (httpResponse != null) {
httpResponse.close();
}
}
}
/
*
* 爬取指定内容中的总分页数
*
* @param text
* 指定网页源码内容
* @return 总分页数
*/
public String PageAll(String text) {
String regex = “/共(\d+?)页”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
boolean is = matcher.find();
if (is) {
return matcher.group(1);
} else {
return null;
}
}
}

8.2 系统入口类:Main
Main类包含 GetAll方法()下载传入地址的全部书籍,GetAll方法()创建 GetContent实体类调用其 getContent()方法,再加入到并发爬取分页线程池.main()方法是系统入口,需要解决异常,抛出Exception,设置爬取的网页地址"www.bookbao8.com/BookList-c_0-t_2-o_1.html",通过 GetAll()方法将爬取小说的线程加入到线程池中,创建分页线程池和小说线程池,调用Pool.executorServicePage的shutdown()方法//等待加入线程全部执行完毕.通过每10秒判断是否全部结束,如果成功结束即显示”获取小说书名信息和链接成功!”反之则报错.

public class Main {

/** 下载urlFirst网页中的全部书籍
* @param urlFirst
* @throws URISyntaxException
* @throws IOException
* @throws InterruptedException
*/
public static void GetAll(String urlFirst) throws Exception {
GetContent content = new GetContent();
String text = content.getContent(urlFirst);
String pageTotalString = content.PageAll(text);

  if (pageTotalString != null) {
     Integer pageTotal = Integer.valueOf(pageTotalString);
     //值为1万多  为了测试起见  下面for循环的pageTotal可以改成1 表示一个分页 (也有十几本小说)
     for (int i = 1; i <= pageTotal; i++) {
        //小说的网址
        String url = "www.bookbao8.com/booklist-p_" + i + "-c_0-t_2-o_1.html";
        //加入线程池并发处理
        Pool.executorServicePage.execute(new AddBookUrlThread(url));
     }
  }

}
public static void main(String[] args) throws Exception {
String url = “www.bookbao8.com/BookList-c_0-t_2-o_1.html”;
GetAll(url);
ExecutorService executorServicePage = Pool.executorServicePage;
ExecutorService executorServiceBook = Pool.executorServiceBook;

  //等待加入线程全部执行完毕
  executorServicePage.shutdown();
  //awaitTermination限制每10秒循环一次是否全部结束,
  while (!executorServicePage.awaitTermination(5, TimeUnit.SECONDS));
  //如果线程全部结束isTerminated则为true
  boolean PageEnd = executorServicePage.isTerminated();
  
  if(PageEnd) {
     System.out.println("获取小说书名信息和链接成功!");
     executorServiceBook.shutdown();
  } 
  System.out.println("*************************************************");
  while (!executorServiceBook.awaitTermination(3, TimeUnit.SECONDS));
  
  //如果线程全部结束isTerminated则为true
  boolean BooksEnd = executorServicePage.isTerminated();
  if(BooksEnd) {
     System.out.println("获取小说成功!");
  } 

}
}

8.3 AddBookUrlThread并发爬取分页信息线程类:

AddBookUrlThread类实现Runnable接口,是一个线程类,设计为线程类的目的是为了并发操作.包含url使用String字符串存储,保存传递的分页URL地址.包含GetContent类可以获取分页中的小说息.AddBookUrlThread的构造器接收String的地址参数,保存传入的分页地址.重写run方法,其中调用content类的getContent方法获取分页信息,使用正则表达式获取该分页信息上的所有小说信息,如书名以及章节链接,通过循环将爬取具体小说信息的线程加入到线程池.

public class AddBookUrlThread implements Runnable {
private String url;
private GetContent content = new GetContent();
//爬取全部小说的网址,书名
private String regex = “class=“bookname”>.*?href=”(.+?)".+?>(.+?)";

public AddBookUrlThread(String url) {
this.url = url;
}
@Override
public void run() {
System.out.println(“开启获取分页线程:” + Thread.currentThread().getName());
String text = null;
try {
text = content.getContent(url);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
Book book = null;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
String bookurl = “”;
//下载一个分页中的所有小说
while(matcher.find()) {
book = new Book();
bookurl = “www.bookbao8.com” + matcher.group(1);
book.setUrl(bookurl);
book.setBook_name(matcher.group(2));
// ReturnBooks.addBook(book);
//并发处理单个书籍信息读取线程
Pool.executorServiceBook.execute(new AdBookChaptersThread(book));
}
}
}

效果展示:
开启获取分页线程:pool-1-thread-1
获取小说书名信息和链接成功!


开启获取书籍线程:pool-2-thread-1
开启获取书籍线程:pool-2-thread-2
开启获取书籍线程:pool-2-thread-3
开启获取书籍线程:pool-2-thread-4
开启获取书籍线程:pool-2-thread-5
穿越六十年代之末世女王信息爬取成功
爬取穿越六十年代之末世女王信息成功
穿越1979信息爬取成功
爬取穿越1979信息成功
你的青梅,她的竹马信息爬取成功
爬取你的青梅,她的竹马信息成功
帝国甜宠:首席的秘密恋人信息爬取成功
爬取帝国甜宠:首席的秘密恋人信息成功
随身空间之叶莫信息爬取成功
爬取随身空间之叶莫信息成功
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!
帝国甜宠:首席的秘密恋人章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!

查看本地磁盘D:

查看小说内容:

九 结论及存在问题:
在这次实验项目中,我负责的是总体流程设计和并发线程的设计与实现。一个项目的重心是核心业务的分析考虑,在这次的项目中,我也是学到了很多关于分析流程的知识,也知道了百度真的是好帮手,这是我以前盲人摸象没有使用到的工具。过程中我也应用了软件工程的需求分析,系统设计之内的内容,刚好学以致用使得这学期学的内容进一步巩固。在这期间遇到的难题其实是分页之间的数据爬取,通过观察法,发现分页网页的地址其实和页数是有关系的,这也是项目的一个突破口,根据分页地址,就可以提取到不同分页内的多本小说信息。并且提高效率,结合了同学的分析帮助采用了多线程的思想,速度提高了好几倍。也认识到了团队协作的重要性。以前都是单打独斗一个人完全实现一整个小项目的时候并没有发现团队的重要性,在这次的三人小组中,我们交流思想和技术,共同完成了这次项目。我也学会了很多知识,如多线程,爬虫的知识。
感谢我的三人小组和老师!

同学二:
五 需求分析:
5.1系统业务分析
分析系统对象之间的交互,系统访问静态小说地址显示主页,系统获取页面html到内存,网页显示分页信息并且提取分页信息,系统提取小说信息,网页显示小说信息,系统保存小说到本地为TXT格式.如图2-2时序图所示。

图2-2系统核心对象交互活动图
六 系统分析:
6.1 分析系统功能:
6.1.1连接网络功能:
分析连接网络功能,系统调用GetAll(String)方法,将消息地址传GetContent类,GetContent类调用getContent(String)方法,传入访问静态小说地址,创建URI类获取到连接指定IP地址.为了实现分页爬虫效果需要连接每个分页的地址创建连接,GetContent类调用PageAll获取总页数.然后通过观察网站可知分页地址跟页数有关系,即可通过循环将分页地址传入.针对连接网络,原始方法即每次访问一次IP地址,效率低下,回馈缓慢.改进方法多次访问,即每次访问同一IP地址多次,可以模拟人的作息时间每两分钟访问一次,因为很多网站都采用了反爬虫机制,根据同一时间访问该网站的次数,如果超过阀值即阻止其请求.所以我们可以调用连接Pool类的静态属性Pool.executorServicePage,调用execute()方法传入创建的爬虫分页的线程AddBookUrlThread.如图2-4时序图所示.
图2-4连接网络时序图

6.1.2保存小说信息到本地功能:
Main系统类调用main()方法,调用GetAll()方法,创建AddBookUrlThread线程类对象加入到并发处理页面的线程池中,再调用AddBookUrlThread类的run方法,创建AdBookChaptersThread线程类对象加入到处理单个页面中小说并发处理的线程池当中,其中AdBookChaptersThread线程类的run方法调用FIleReaderWriter工具类的writeINtoFile静态方法将小说的简介信息写入到磁盘中,通过循环获取该小说的章节信息,再调用FIleReaderWriter工具类的writeINtoFile静态方法将将其章节正文写入到该小说指定位置.如图2-7时序图所示。

图2-7保存小说信息到本地时序图

七 系统设计:
7.1 连接池和线程池类相关设计:
通过池子,可以从池子中调用连接和线程来执行操作,当不使用的时候保留一定数量的线程,当需要的时候直接调用线程池,减少了创建线程的时间和性能消耗.当需要并发操作的时候,线程池和连接池是很好的选择.
连接池存在的目的就是为了并发访问指定IP地址,提高访问效率
线程池存在的目的就是为了并发爬取小说信息.连接池又可以分为两种连接池:

  1. 分页线程池为了同时处理多个页面
  2. 小说线程池,为了同时处理一个页面上的多个小说

而章节的爬取不需要创建线程池,因为章节爬取需要有先后顺序,比如:第一章后是第二章,具有有序性.反之分页之间和小说之间可以具有无序性,因为分页和小说之间具有原子性,可以独立存在,彼此之间没有先后顺序之分.
既然是池子,那么就是工具类,里面的连接池和线程池都应该设置为静态属性:

  1. CloseableHttpClient httpClient
  2. ExecutorService executorServicePage
  3. ExecutorService executorServiceBook
    既然是静态属性,那么可以通过静态代码块设置静态属性,如设置连接池最大连接数,创建并发量,置请求超时后重试次数,线程池最大连接数等

7.2 数据操作类相关设计:

FileReaderWriter类是工具类,包含两个静态方法:

  1. createNewFile()方法创建一个存储小说的文件夹
  2. writeIntoFile()方法实现将传入的Book实体类中存储的信息存储到本地磁盘
    之所以设置为静态方法是因为该类是工具类,将常用方法定义为静态的好处是方便调用并且唯一性.

八 系统实现:

基于面向对象的设计,设计实体类存储系统涉及到的用例,即小说类,包含该小说的所有信息,如:

book_name; //书本名字
author; // 作者
type; // 类型
status; // 连载状态
update_time; // 更新时间
book_Introduction; // 书本简介
String url; // 书本链接

8.1 实体类:
package entity;
public class Book {
private String book_name; // 书本名字
private String author; // 作者
private String type; // 类型
private String status; // 连载状态
private String update_time; // 更新时间
private String book_Introduction; // 书本简介
private String url; // 书本链接
public String getBook_name() {
return book_name;
}
public void setBook_name(String book_name) {
this.book_name = book_name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getUpdate_time() {
return update_time;
}
public void setUpdate_time(String update_time) {
this.update_time = update_time;
}
public String getBook_Introduction() {
return book_Introduction;
}
public void setBook_Introduction(String book_Introduction) {
this.book_Introduction = book_Introduction;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return book_name + "\r\n类型: " + type + "\r\n作者: " + author + "\r\n状态: " + status + "\r\n最新更新时间: "
+ update_time + "\r\n书本简介: " + book_Introduction + “\r\n\r\n\r\n\r\n”;
}
}

8.2 连接池和线程池工具类:
通过池子,可以从池子中调用连接和线程来执行操作,当不使用的时候保留一定数量的线程,当需要的时候直接调用线程池,减少了创建线程的时间和性能消耗.当需要并发操作的时候,线程池和连接池是很好的选择.
连接池存在的目的就是为了并发访问指定IP地址,提高访问效率
线程池存在的目的就是为了并发爬取小说信息.连接池又可以分为两种连接池:

  1. 分页线程池为了同时处理多个页面
  2. 小说线程池,为了同时处理一个页面上的多个小说

而章节的爬取不需要创建线程池,因为章节爬取需要有先后顺序,比如:第一章后是第二章,具有有序性.反之分页之间和小说之间可以具有无序性,因为分页和小说之间具有原子性,可以独立存在,彼此之间没有先后顺序之分.
既然是池子,那么就是工具类,里面的连接池和线程池都应该设置为静态属性:

  1. CloseableHttpClient httpClient
  2. ExecutorService executorServicePage
  3. ExecutorService executorServiceBook

既然是静态属性,那么可以通过静态代码块设置静态属性,如设置连接池最大连接数,创建并发量,置请求超时后重试次数,线程池最大连接数等

package getContent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
public class Pool {
public static CloseableHttpClient httpClient = null;
public static ExecutorService executorServicePage = null;
public static ExecutorService executorServiceBook = null;
static {
PoolingHttpClientConnectionManager cm1 = new PoolingHttpClientConnectionManager();
cm1.setMaxTotal(200);// 设置最大连接数
cm1.setDefaultMaxPerRoute(200);// 对每个指定连接的服务器(指定的ip)可以创建并发200 socket进行访问
httpClient = HttpClients.custom().setRetryHandler(new DefaultHttpRequestRetryHandler())// 设置请求超时后重试次数
.setConnectionManager(cm1).build();
//设置处理分页的线程池
executorServicePage = Executors.newFixedThreadPool(100);
executorServiceBook = Executors.newFixedThreadPool(100);
}
}

8.3 数据操作类: FileReaderWriter

  1. createNewFile()方法创建一个存储小说的文件夹
  2. writeIntoFile()方法实现将传入的Book实体类中存储的信息存储到本地磁盘
    之所以设置为静态方法是因为该类是工具类,将常用方法定义为静态的好处是方便调用并且唯一性.

public class FileReaderWriter {
public static boolean createNewFile(String filePath) {
boolean isSuccess = true;
//如有则将“\”转换成“/”,没有则不产生任何变化
String filePathTurn = filePath.replaceAll("\\", “/”);
//先过滤掉文件名
int index = filePathTurn.lastIndexOf("/");
String dir = filePathTurn.substring(0, index);
//再创建文件夹
File fileDir = new File(dir);
isSuccess = fileDir.mkdirs();
//创建文件
File file = new File(filePathTurn);
try {
isSuccess = file.createNewFile();
} catch (IOException e) {
isSuccess = false;
e.printStackTrace();
}
return false;
}
public static boolean writeIntoFile(String content, String filePath, boolean isAppend) {
boolean isSuccess = true;
//先过滤掉文件名
int index = filePath.lastIndexOf("/");
String dir = filePath.substring(0, index);
//创建文件路径
File fileDir = new File(dir);
fileDir.mkdirs();
//再创建路径下咋文件
File file = null;
try {
file = new File(filePath);
file.createNewFile();
} catch (IOException e) {
isSuccess = false;
e.printStackTrace();
}
//写入文件
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(file, isAppend);
fileWriter.write(content);
fileWriter.flush();
} catch (IOException e) {
isSuccess = false;
e.printStackTrace();
} finally {
try {
if (fileWriter != null) {
fileWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}

效果展示:
开启获取分页线程:pool-1-thread-1
获取小说书名信息和链接成功!


开启获取书籍线程:pool-2-thread-1
开启获取书籍线程:pool-2-thread-2
开启获取书籍线程:pool-2-thread-3
开启获取书籍线程:pool-2-thread-4
开启获取书籍线程:pool-2-thread-5
穿越六十年代之末世女王信息爬取成功
爬取穿越六十年代之末世女王信息成功
穿越1979信息爬取成功
爬取穿越1979信息成功
你的青梅,她的竹马信息爬取成功
爬取你的青梅,她的竹马信息成功
帝国甜宠:首席的秘密恋人信息爬取成功
爬取帝国甜宠:首席的秘密恋人信息成功
随身空间之叶莫信息爬取成功
爬取随身空间之叶莫信息成功
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!
帝国甜宠:首席的秘密恋人章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!

查看本地磁盘D:

查看小说内容:

九 结论及存在问题:
在这次报告中我负责实体类的设计以及连接网络,保存到本地的设计与代码实现。在这里我先总结下这次报告中遇到的问题:
问题1:一开始因为爬虫的速度过慢而设计了连接池,使得1秒内可以用访问该小说网站20次,提高了访问html的速度,而又遇到了爬取小说速度过慢的问题,又设计了线程池,速度提高了好几倍。
小结1:平时处理的数据都是几百,几千没有到达一定的量级,这次实验中我们处理的数据量达到了千级甚至万级,单线程已经远远无法达到我们的期望值,所以当数据量巨大时要考虑多并发处理,但这里要考虑数据不一致的问题,即多线程处理同一变量如全局或者静态,这里没有涉及到这方面也就不考虑了。
问题2:内存保存爬取内容的选择,一开始我使用String来保存内容,然后循环添加导致内存爆出。
小结2:百度了String字符串每次追加都会创建一个新的字符串,导致内存爆出,这个时候可以考虑StringBuilder类型。当频繁追加修改字符串的时候,应该使用StringBuilder,但是它是线程不安全的,这方面也没有涉及到所以不考虑。
问题3:内存中数据的保留,一开始我使用FileOutStream流保存到本地,不过发现效果不佳。
小结3:后来使用FileWriter效果提高了不少。
在这次项目的参与中,对软件开发设计结合了软件工程的思想,使得项目更加有计划目的,开发效率也提高了很多。也从这次项目中应用了爬虫的知识,对爬虫有了更加深入的了解。不仅知识上获取了很多,也在团队协作上和别人有了更多的交流和合作,发现团队的力量是无限的。
在这里也要感谢其他同学的帮助和老师的指导,谢谢!

同学三:
五 需求分析:
5.1业务需求分析
5.1.1系统爬虫需求分析
通过分析可以得出系统爬虫功能:连接网络管理,下载指定html页面管理,提取指定内容管理,保存到本地等功能.系统有获取信息操作,进行保存,提取关键内容操作,保存到数据库或者本地 操作.分析图2-3 用例图所示.
分析系统爬虫业务,其中一个业务是连接网络管理,通过传入URL地址,访问目标地址内容,并且保存到系统内存.如图2-4时序图所示.

图2-3 系统爬虫需求分析用例图
图2-4系统爬虫业务时序图

六 系统分析:
6.1 分析系统功能:
6.1.1处理单本小说信息功能:
AdBookChaptersThread处理单本小说信息线程类调用run方法类,调用getInformation方法判断是否爬取小说信息成功,如果该小说简介等信息已爬取则调用FileReaderWriter工具类(存储类)的writeIntoFile静态方法(写入方法),写入本地磁盘,然后反馈给系统是否存储成功的信息.run方法调用getTextAll方法获取到该小说章节信息,循环写入磁盘,并且提示写入成功或者失败.如图2-6所示.

						图2-6处理单本小说信息时序图

七 系统设计
7.1系统命名规则
7.1.1 包命名有如下规则:

实体包:

获取信息包:

爬取分页和小说包:

下载到本地包:

7.2 小说处理爬取信息类相关设计:

AdBookChaptersThread 实现Runnable接口,是一个线程类,设计为线程类的目的是为了并发操作.属性包含实体类Book存储小说信息,包含 GetContent类方法处理指定网页的html信息.方法包括:
1.getBookInformation()方法爬取该小说信息,
2.getInformation()方法下载该小说简介等信息,
3.getChapterUrl()方法获取小说章节链接信息,
4.getTextAll()方法爬起小说的章节列表区域源代码,
5.getText()方法爬取指定章节链接的内容,
6.重写 run方法实现下载该小说信息到本地磁盘

public class AdBookChaptersThread implements Runnable {
private Book book;
private GetContent content = new GetContent();
public AdBookChaptersThread(Book book) {
this.book = book;
}
public boolean getBookInformation(Book book) {}
public boolean getInformation() {}
public String getChapterUrl(String ulString) {}
public String getTextAll() {}
public String getText(String url) {}
@Override
public void run() { }
}

7.3 系统入口类相关设计:
Main类包含 GetAll方法()下载传入地址的全部书籍,GetAll方法()创建 GetContent实体类调用其 getContent()方法,再加入到并发爬取分页线程池.main()方法是系统入口,需要解决异常,抛出Exception,设置爬取的网页地址"www.bookbao8.com/BookList-c_0-t_2-o_1.html",通过 GetAll()方法将爬取小说的线程加入到线程池中,创建分页线程池和小说线程池,调用Pool.executorServicePage的shutdown()方法//等待加入线程全部执行完毕.通过每10秒判断是否全部结束,如果成功结束即显示”获取小说书名信息和链接成功!”反之则报错.

public class Main {
public static void GetAll(String urlFirst) throws Exception {
GetContent content = new GetContent();
String text = content.getContent(urlFirst);
}
public static void main(String[] args) throws Exception{ String url = “www.bookbao8.com/BookList-c_0-t_2-o_1.html”;
GetAll(url);
ExecutorService executorServicePage = Pool.executorServicePage;
ExecutorService executorServiceBook = Pool.executorServiceBook;executorServicePage.shutdown();
}

7.4获取类相关设计:

GetContent类获取网页html信息:

设计成public公有类方便调用
包含BufferedReader类存储爬取的信息,和getContent()方法爬取指定URI
网页的全部内容以及PageAll()方法爬取指定内容中的总分页数.比如getContent()方法使用URI获取网络连接,使用StringBuilder缓冲流的append追加内容,返回传入url地址的全部html源码.PageAll()方法通过正则表达式返回内容中的总页数信息.
public class GetContent {
private BufferedReader bufferedReader = null;
private StringBuilder conTent = null;
public String getContent{}
public String PageAll(String text) {}
}

八 系统实现:

8.1 线程类:

  1. AdBookChaptersThread爬取指定小说信息
  2. AddBookUrlThread 爬取指定分页信息

8.2 AdBookChaptersThread爬取指定小说线程类:

AdBookChaptersThread 实现Runnable接口,是一个线程类,设计为线程类的目的是为了并发操作.属性包含实体类Book存储小说信息,包含 GetContent类方法处理指定网页的html信息.方法包括
1.getBookInformation()方法爬取该小说信息,
2.getInformation()方法下载该小说简介等信息,
3.getChapterUrl()方法获取小说章节链接信息,
4.getTextAll()方法爬起小说的章节列表区域源代码,
5.getText()方法爬取指定章节链接的内容,
6.重写 run方法实现下载该小说信息到本地磁盘

public class AdBookChaptersThread implements Runnable {
private Book book;
private GetContent content = new GetContent();
public AdBookChaptersThread(Book book) {
this.book = book;
}
/**
* 爬取该书本信息
*
* @param book
* @return
/
public boolean getBookInformation(Book book) {
// FileReaderWriter.writeIntoFile(“zz1”, “D:/知乎-编辑推荐.txt”, false);
String xx;
try {
xx = content.getContent(book.getUrl());
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println(“获取书本信息失败”);
e.printStackTrace();
throw new RuntimeException(e);
}
// book中目前只有地址和书名
String regex = "id=“info”>.
?(.+?).?" + "(.?).?" + "

状态:(.+?)

.?"
+ “

更新时间:(.+?)

.*?class=“infocontent”>(.+?)”;
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(xx);
boolean is = matcher.find();
String information = “”;
if (is) {
book.setAuthor(matcher.group(1));
book.setType(matcher.group(2));
book.setStatus(matcher.group(3));
book.setUpdate_time(matcher.group(4));
information = matcher.group(5);
information = information.replaceAll("
", “\r\n”);
book.setBook_Introduction(information);
return true;
}
return false;
}
/**
* 下载该书籍简介等信息
*
* @param bookName
* @return
/
public boolean getInformation() {
if (getBookInformation(book)) {
FileReaderWriter.writeIntoFile(book.toString(), “D:/书籍/” + book.getBook_name() + “.txt”, true);
System.out.println(book.getBook_name() + “信息爬取成功”);
return true;
}
return false;
}
/
*
* @return 获取章节页面链接
/
public String getChapterUrl(String ulString) {
String url = book.getUrl();
int index = url.indexOf("/");
// 截取首页www.bookbao8.com
StringBuilder dir = new StringBuilder(url.substring(0, index));
dir.append(ulString);
return dir.toString();
}
/
*
* @return 爬起小说的章节列表区域源代码
/
public String getTextAll() {
String text = “”;
String url = book.getUrl();
try {
text = content.getContent(url);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 获取章节的列表内容,而不是其他如热门排行榜
String regex = "class=“wp b2 info_chapterlist”>.
";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
boolean is = matcher.find();
if (is) {
String ulString = matcher.group();
return ulString;
}
return null;
}
/**
* 爬取指定章节链接的内容
*
* @param url
* 章节链接
* @return 返回的文章内容
/
public String getText(String url) {
// http://www.bookbao8.com/views/201708/29/id_XNTg3MTc5_7.html
String text = “”;
try {
text = content.getContent(url);
} catch (URISyntaxException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String regex = "id=“contents”.
?>(.+?)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
boolean is = matcher.find();
if (is) {
String One = matcher.group(1);
One = One.replace("
", “\r\n”);
return One;
}
return null;
}
@Override
public void run() {
System.out.println(“开启获取书籍线程:” + Thread.currentThread().getName());
String bookName = book.getBook_name();
if (getInformation()) {
System.out.println(“爬取” + bookName + “信息成功”);
}
// 获取该小说的章节源内容
String ulString = getTextAll();
// 获取标题内容 和链接
String regex = “href=”(.+?)".+?>(.+?)";
String chapter = “”;
String contentText = “”;
StringBuilder All = new StringBuilder();
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(ulString);
while (matcher.find()) {
// 获取章节链接
chapter = getChapterUrl(matcher.group(1));
// 爬取文章内容
contentText = getText(chapter);
// 下载标题
All.append(matcher.group(2));
All.append("\r\n\r\n");
//下载章节内容
All.append(contentText);
All.append("\r\n\r\n\r\n");
FileReaderWriter.writeIntoFile(All.toString(), “D:/书籍/” + bookName + “.txt”, true);
System.out.println(book.getBook_name() + “章节爬取成功!”);
}
System.out.println(bookName + “全部爬取成功!”);
}
}

效果展示:
开启获取分页线程:pool-1-thread-1
获取小说书名信息和链接成功!


开启获取书籍线程:pool-2-thread-1
开启获取书籍线程:pool-2-thread-2
开启获取书籍线程:pool-2-thread-3
开启获取书籍线程:pool-2-thread-4
开启获取书籍线程:pool-2-thread-5
穿越六十年代之末世女王信息爬取成功
爬取穿越六十年代之末世女王信息成功
穿越1979信息爬取成功
爬取穿越1979信息成功
你的青梅,她的竹马信息爬取成功
爬取你的青梅,她的竹马信息成功
帝国甜宠:首席的秘密恋人信息爬取成功
爬取帝国甜宠:首席的秘密恋人信息成功
随身空间之叶莫信息爬取成功
爬取随身空间之叶莫信息成功
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!
帝国甜宠:首席的秘密恋人章节爬取成功!
穿越1979章节爬取成功!
你的青梅,她的竹马章节爬取成功!
穿越六十年代之末世女王章节爬取成功!
随身空间之叶莫章节爬取成功!

查看本地磁盘D:

查看小说内容:

九 结论及存在问题:
我在项目的开发过程中遇到了挺多问题,也学到了挺多知识,比如对于爬虫有了更深入的了解。这次爬虫题目我们考虑了好久,最终定为爬取静态网站的小说原因如下:
网页最终显示的页面源码是经过浏览器解释后的,当get或者post请求到的源码是服务器直接返回的,需要浏览器js渲染解释后正常显示。最基础的爬虫只能爬取没有动态加载的纯静态网页,而目前主流的网站都是有反爬虫的措施,以及各种验证措施。就比如2017年知乎就已经改版了,爬虫更加困难,之前的关于知乎的爬虫项目也都是无效的,因为爬虫模式不一样的,也就是说更难了。所以对于我们初学者来说,选择一个静态网站是一个好的选择。虽然这个所谓的”小”项目也是折磨了我们挺长一段时间。
这次项目中我和我的两个队友都采用了软件工程的思想,按照规范使用需求分析,系统实现等,使得项目具有扩展性和强壮性。应用了软件思想的项目,开发起来确实得心应手,有了一个目标和具体的规划让这次的爬虫的编写更有规范性。
除外,我还负责类名等名字设计,这方面虽然小,但是也不容忽视,好的方法命名可以让人一目了然,省去了很多不必要的交流。
当然,主线还是负责小说信息的爬取,这里用到了大量的正则表达式,为也是google了大量内容学习,一步步测试才最终完成了这一块内容。但是问题也是显而易见的,那就是耦合性太强,只适合这一个项目,这也是我目前还没有解决的问题,我的目的是想写出一个可扩展的爬虫,不过现在的技术还远远不够,不过在以后的学习中,我会加强爬虫这方面的学习。
不仅仅是技术上的不断尝试和提升,这次的三人团队协作也让我学到了很多东西,有句话说的好”三个臭皮匠,顶个诸葛亮”。就我一个人的努力是很难单独完成的,队友的弥补与意见给了我很多的帮助。

在这里也感谢一直帮助我们的老师,谢谢!

你可能感兴趣的:(JAVA爬虫系列)