1. 项目背景
从小到大读过很多唐诗宋词,甚至在我们根本不能理解诗的含义的情况下我们也背过,我们背过抒情的、励志的、思乡的等等,那么在古代大多数诗人擅长用的词语是什么?是代表思乡的明月、还是抒情的词语,还是对官场厌恶。而在古代谁又作诗最多呢?到底是我们耳熟能详的李白还是杜甫或者说另有其人?
《寻古之明星,探古之热词》程序主要是通过抓取互联网上的诗集(互联网上的数据不是我们的,所以对我们来讲特别的混乱,必须进行处理),然后进行数据分析最终得出我们想要的答案。
2. 项目意义
这次项目的主要意义是通过对网上的数据采集了解数据分析的流程
数据采集来源: https://so.gushiwen.org/gushi/tangshi.aspx
上述网页都是诗的题目,所谓爬虫就是即使它是超链接我们也能爬进去获取诗的信息。
3. 项目功能
- 采集网页内容,利用HtmlUnit工具进行数据解析之后存入MySQL数据库中
- 通过分析数据得出诗人与创作数量的对应关系
- 利用ansj中文分词技术分析出古文中热度较高的词
4. 项目技术
- JavaSE知识(多线程)
- MySQL和JDBC编程
- 文本分析和解析(ansj中文分词算法)
- 网页解析工具(htmlunit) 解析的一种
- 对象工厂(Spring的原型)
5. 项目实现
- 爬虫的实现
因为这个项目主要是想分析网页上的内容,所以我们首要的步骤是将网页上的内容拿下来,这个只需要知道网页的url就可以通过一个网页解析工具htmlunit将网页上的内容拿到本地,但是此时拿下来的网页是十分杂乱的,并且在计算机看来就是大量的字符串
htmlunit是一个java页面解析工具,它不仅可以获取网页上的内容,还可以根据标签解析出我们想要的模块例如像getbody()是获取整个源码中body体的部分,getElementById("**")是根据你给的Id值寻找对应的模块,但是像我们的项目中用的古诗文网首页全部都是超链接,我们要分析网页不可能分析这一堆无用的超链接,我们需要进入超链接,所以在解析页面的时候如果发现这并非一个详情页我们就根据它的a标签,将所有子网页加入到一个集合当中,重新解析,如果是一个详情页我们就根据特定的方法将标题、作者、内容、朝代全部取出来,取出来之后就可以将这些内容加入到数据库中。
从采集数据,解析数据,到加入数据库,我用的是一个多线程技术,设置了一个调度器,先采集网页,采集之后放入队列,解析器从队列中取数据进行解析,解析之后又放入一个队列,清洗器从队列中取数据加入到数据库中,模块与模块之间各干各的事情,因为如果是单线程调度的话解析器只能采集器完成工作之后才能运行,清洗器也只有等解析器完成之后才能运行,效率会非常的差。
如果想要爬取其他的页面,只需要将网页地址替换为你想要解析的页面,更改数据库即可。 - 数据分析
数据分析分为两部分一部分是根据解析到的内容获得古代诗人作诗数量的排名,这部分就比较简单,就是简单的sql语句,以作者分组统计数量。查询语句的最终结果就是我们想要的答案。另一部分就是看哪个词语被用的次数最多,计算机获得字符串中的词语我用的是一个ansj中文分词工具、分词工具用四种调用方法基本分词,精准分词、nlp分词、和面向索引的分词我选用的分词方法是nlp分词,它是一个精度最高的分词技术调用parse方法传入字符串即可自动分析,但是由于nlp的分析比较强大,甚至能将标点符号分出来,所以在统计时,简单的对统计结果进行了过滤,最后将统计结果以key-value的形式存放。
6. 项目代码
https://github.com/123zhaomiao/-dynasty
7. 项目结果展示
程序对于上图中的链接均能抓取、在这里我们只运行了唐诗三百和小学文言
唐诗共319首 总共为75个诗人所写共4974个词
8. 项目测试
- 通过代码对数据库连接池的特性进行测试
package mytangshi;
import com.alibaba.druid.pool.DruidDataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
class ConnectionManager {
//使用单利模式创建数据库连接池
private static ConnectionManager instance;
private static DruidDataSource dataSource;
private ConnectionManager() {
dataSource = new DruidDataSource();
dataSource.setUsername("root"); //用户名
dataSource.setPassword("12345"); //密码
dataSource.setUrl("jdbc:mysql://localhost:3306/tangshi");//数据库地址
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setInitialSize(5); //初始化连接数
dataSource.setMinIdle(1);//最小连接数
dataSource.setMaxActive(20);//最大连接数
dataSource.setMaxWait(50);//最长等待时间
}
public static final ConnectionManager getInstance() {
if (instance == null) {
try {
instance = new ConnectionManager();
} catch (Exception e) {
e.printStackTrace();
}
}
return instance;
}
public synchronized final Connection getConnection() {
Connection conn = null;
try {
conn = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
public class TestHtmlUnit {
public static void main(String[] args) throws SQLException {
System.out.println("使用连接池................................");
for (int i = 0; i < 20; i++) {
long beginTime = System.currentTimeMillis();
Connection conn = ConnectionManager.getInstance().getConnection();
try {
PreparedStatement pstmt =
conn.prepareStatement("select * from poetry_info");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
//TODO
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("第" + (i + 1) + "次执行花费时间为:" + (endTime - beginTime));
}
System.out.println("不使用连接池................................");
for (int i = 0; i < 20; i++) {
long beginTime = System.currentTimeMillis();
MysqlDataSource mds = new MysqlDataSource();
mds.setURL("jdbc:mysql://localhost:3306/tangshi?useSSL=false");
mds.setUser("root");
mds.setPassword("12345");
Connection conn = mds.getConnection();
try {
PreparedStatement pstmt =
conn.prepareStatement("select * from poetry_info");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
//TODO
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("第" + (i + 1) + "次执行花费时间为:"
+ (endTime - beginTime));
}
}
}
由图可见,采用数据库连接池,只有第一次运行时比较耗时,数据源完成初始化之后,使用连接池进行数据库操作明显比不使用连接池花费的时间少。并且改变连接池的最大连接数、最小连接数等对代码的影响很大。
- 检测到数据库的插入效率为0.6首/秒 (总共320首,耗时3.2分钟)
- 利用方法对爬取到的详情页超链接信息进行校验
使用了java.net 下的URL和HttpURLConnection两个类来实现。
通常一个HttpURLConnection 的实例可以生成一个请求,它有个方法getResponseCode();可以得到请求的响应状态,该方法返回一个 int 分别是 200 and 404 如无法从响应中识别任何代码则返回 -1。 - 对数据库中的诗文信息运用手工测试方法抽取性测试,在抽取率为40%的情况下信息正确率为100%
具体是根据select提取数据库中的信息进行对比
9. 项目优点
- 使用Druid数据库连接池,在程序启动时就建立足够的数据库连接,将这些连接集中管理,减少了数据库连接和释放所耗费的时间,在使用时只需要向连接池申请,使用完毕归还连接即可。
- 有一个对象工厂模型类似于一个小型的Spring框架,在爬虫启动时总是会有很多的准备工作比如初始化数据源,new一个采集器,new一个解析器等等,我们往往不希望在主方法中有这么多复杂的操作,因为主方法是面向客户的,对于客户来说我只关心功能并不关心你是怎么实现的,所以我采用了一个对象工厂的形式,将所有要new的对象放入对象工厂中,如果某个地方需要使用利用反射机制将对象取出,这样既保证了所有对象全局唯一,还让客户端的操作变得简单。
10. 项目不足
- 爬虫时需要网络支持(我觉得可以在爬虫启动之前ping一个网络,如果网络状态不良好就 启动爬虫,避免资源的浪费)
11.项目延伸知识
通过对唐诗三百首和小学文言的解析,发现唐诗三百的数据最终为851条,而小学文言只有52条,这些都是很少量的数据,那么当数据量特别庞大的时候怎么处理呢?
- Spark 专为大数据处理而设计的快速通用的计算引擎、实时计算,主要在内存中操作
- Hadoop 分布式文件系统,为海量数据提供计算
区别:
Spark 主要在内存中处理数据 ,实时分析(来一个数据就分析)
Hadoop 磁盘上,离线分析(相当于将数据加载到数据库完毕后再进行解析)
所以当数据量庞大时,对实时性要求不高的用Hadoop对,实时性要求较高的用Spark