一、项目背景
说起唐诗,我想你第一个想起的就是李白,对他的诗也应该是耳熟能详,然而唐代的诗人可并不止他一人,那么在众多的诗人中,到底谁的创作数量更多呢?这是个值得深究的的问题。
此外,唐朝是个比较开明的王朝,每个诗人的作品也风格迥异,像诗仙李白之豪迈浪漫,诗圣杜甫之沉郁顿挫,他们每个人的生活背景不同,作品的风格也不同,那么,在这么多首诗中,到底哪种风格的作品较多呢?要了解这个问题,我们就需要对这些唐诗的词语进行分析,分析出这些诗中每个词语出现的频次,由此,我们就可以判断出唐朝诗歌的整体风格偏向了。到此,我们就有了两个任务了,分析唐代诗人每个诗人的创作数量以及所有唐诗中词语的出现频次。由此,我们的唐诗项目也就应运而生了。
二、项目简介:
唐诗分析程序是通过抓取互联网上的唐诗,对其网页上的数据进行解析、清洗、存储到数据库,然后再对内容进行分析,最后输出报告。
三、 数据来源
数据采集主要来自:古诗文网
四、 功能
实现数据采集、清洗、存储、分析、分析结果可视化展示。这几个功能
五、 所用技术
Java方面:面向对象(接口,类)、多线程、集合框架、设计模式、JDBC编程、常用类、Stream流、Lambda表达式
第三方的:htmlunit(网页分析工具)、Ansj(分词)、sparkjava、Druid(数据库连接池)
前端:HTML、JavaScript、jQuery、Echarts
工具:IDEA、Maven
六、 实现流程
整体流程概括:
(1)采集数据并解析
先从原始网页拿到古诗文网的URL地址,用htmlunit网页分析工具对其网页源码进行分析,用采集器(webClient)对网页的数据进行采集, 并对采集得到的page对象进行解析,解析的结果若是详情页则放在一个详情页的队列中,若是是非详情页则把它放在一个文档页的队列中,继续对文档队列中的page对象进行解析。
(2)对解析结果进行清洗并存储
从详情队列中取出一个详情页调用清洗的多线程调度器page对象进行清洗,将清洗的结果存到数据库中。同时在控制台打印。
(3)访问数据库中的数据并对其进行分析
采用JDBC编程对数据库进行操作,通过数据访问拿到数据库中的数据对其进行分析,其中包含作者的创作数量分析以及词云分析,分析的结果存储在其对应的集合中
(4)通过web接口将结果部署到服务器
通过web接口将其我们的结果序列化后部署到服务器上,再通过js可视化编程,将分析结果以图表的形式在网页上显示出来。至此就完成了我们的唐诗分析流程。
七、 代码模块功能介绍
每个模块的详细介绍及框架如图:
接下来我们对每一部分的代码进行展示说明:
八、项目源码
crawler爬虫类的包
该类是为采集器所抓取到的网页对象的封装
public class Page {
private final String base;// 数据网站的根地址
private final String path;//具体网页的路径
private HtmlPage htmlPage;//网页的DOM对象
private final boolean detail;//表示网页是否是详情页
private Set<Page> subPage=new HashSet<>();//子页面对象集合
private DataSet dataset=new DataSet();//数据对象
public Page(String base, String path, boolean detail) {
this.base = base;
this.path = path;
this.detail = detail;
}
public String getUrl(){
return this.base+this.path;
}
public void setHtmlPage(HtmlPage htmlPage) {
this.htmlPage = htmlPage;
}
}
该类采用Map集合存储清洗的数据
package com.symx.poetrytang.crawler.common;
import lombok.ToString;
import java.util.HashMap;
import java.util.Map;
/**
* 存储清洗的数据
* data 把DOM解析清洗之后存储的数据
* eg:
* 标题:送孟浩然之广陵
* 作者:李白
* 正文:***
*
**/
@ToString
public class DataSet {
private Map<String,Object> data=new HashMap<>();
public void putData(String key,Object value){
this.data.put(key,value);
}
//通过key值获取一个数据对象
public Object getData(String key){
return this.data.get(key);
}
//获取所有数据对象的集合
public Map<String,Object> getData(){
return new HashMap<>(this.data);
}
}
解析器相关类
package com.symx.poetrytang.crawler.parse;
import com.symx.poetrytang.crawler.common.Page;
/**
*解析页面
**/
public interface Parse {
void parse(final Page page);
}
对文档页面的解析
package com.symx.poetrytang.crawler.parse;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.symx.poetrytang.crawler.common.Page;
/**
* 文档页面
**/
public class DocumentParse implements Parse {
@Override
public void parse(final Page page) {
if (page.isDetail()) {
return;
}
HtmlPage htmlPage = page.getHtmlPage();
htmlPage.getBody()
.getElementsByAttribute("div", "class", "typecont")
.forEach(div -> {
DomNodeList<HtmlElement> aNodeList = div.getElementsByTagName("a");
aNodeList.forEach(
aNode -> {
String path = aNode.getAttribute("href");
Page subPage = new Page(page.getBase(), path, true);
page.getSubPage().add(subPage);
}
);
});
}
}
对详情页面的解析
package com.symx.poetrytang.crawler.parse;
import com.gargoylesoftware.htmlunit.html.*;
import com.symx.poetrytang.crawler.common.Page;
import com.symx.poetrytang.analyze.entity.PoetryInfo;
/**
* 详情解析页面
* */
public class DataPageParse implements Parse {
@Override
public void parse(final Page page) {
if (!page.isDetail()) {
return;
}
HtmlPage htmlPage = page.getHtmlPage();
HtmlElement body = htmlPage.getBody();
//标题
String titlePath = "//div[@class='cont']/h1/text()";
DomText titleDom = (DomText) body.getByXPath(titlePath).get(0);
String title = titleDom.asText();
//朝代
String dynastyPath = "//div[@class='cont']/p/a[1]";
HtmlAnchor dynastyDom = (HtmlAnchor) body.getByXPath(dynastyPath).get(0);
String dynasty = dynastyDom.asText();
//作者
String authorPath = "//div[@class='cont']/p/a[2]";
HtmlAnchor authorDom = (HtmlAnchor) body.getByXPath(authorPath).get(0);
String author = authorDom.asText();
//正文
String contentPath = "//div[@class='cont']/div[@class='contson']";
HtmlDivision contentDom = (HtmlDivision) body.getByXPath(contentPath).get(0);
String content = contentDom.asText();
page.getDataset().putData("title",title);
page.getDataset().putData("dynasty",dynasty);
page.getDataset().putData("author",author);
page.getDataset().putData("content",content);
//更多的数据
page.getDataset().putData("url",page.getUrl());
}
}
清洗器相关类
package com.symx.poetrytang.crawler.pipeline;
import com.symx.poetrytang.crawler.common.Page;
public interface Pipeline {
void pipeline(final Page page);
}
将清洗后的数据存放到数据库(采用JDBC编程)
package com.symx.poetrytang.crawler.pipeline;
import com.symx.poetrytang.crawler.common.Page;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
*将数据存储在数据库
**/
public class DatabasePipeline implements Pipeline {
private final Logger logger = LoggerFactory.getLogger(DatabasePipeline.class);
private final DataSource dataSource;
public DatabasePipeline(DataSource datasource){
this.dataSource=datasource;
}
@Override
public void pipeline(Page page){
String dynasty=(String)page.getDataset().getData("dynasty");
String author=(String)page.getDataset().getData("author");
String title=(String)page.getDataset().getData("title");
String content=(String)page.getDataset().getData("content");
String sql="insert into poetry(title,dynasty,author,content) values(?,?,?,?)";
try(Connection connection=dataSource.getConnection();
PreparedStatement statement=connection.prepareStatement(sql)){
statement.setString(1,title);
statement.setString(2,dynasty);
statement.setString(3,author);
statement.setString(4,content);
statement.executeUpdate();
}catch(SQLException e){
logger.error("Database insert occur exception {}.", e.getMessage());
}
}
public DataSource getDataSource() {
return dataSource;
}
}
将清洗后得到的数据存储到Map集合里,再将其打印输出到控制台。
package com.symx.poetrytang.crawler.pipeline;
import com.symx.poetrytang.crawler.common.Page;
import java.util.Map;
/**
* 将数据打印到控制台
**/
public class ConsolePipeline implements Pipeline {
@Override
public void pipeline(final Page page) {
Map<String,Object> data=page.getDataset().getData();
//存储
System.out.println(data);
}
}
爬虫类进行调度
package com.symx.poetrytang.crawler;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.symx.poetrytang.crawler.common.Page;
import com.symx.poetrytang.crawler.parse.Parse;
import com.symx.poetrytang.crawler.pipeline.Pipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
*WebClient(BrowserVersion.CHROME) */
public class Crawler {
private final Logger logger = LoggerFactory.getLogger(Crawler.class);
private final Queue<Page> docQueue=new LinkedBlockingDeque<>();//放置文档页面(超链接)
private final Queue<Page> detailQueue=new LinkedBlockingDeque<>();//放置详情页面
private final WebClient webclient;//采集器
private final List<Parse> parseList=new LinkedList<>();//所有解析器
private final List<Pipeline> pipelineList=new LinkedList<>();//所有清洗器
private final ExecutorService excutorservice; //线程执行器
public Crawler(){
this.webclient=new WebClient(BrowserVersion.CHROME);
this.webclient.getOptions().setJavaScriptEnabled(false);//设置js脚本无用
//创建线程执行器
this.excutorservice= Executors.newFixedThreadPool(8, new ThreadFactory() {
//采用自增长方式给爬虫的线程执行器的Id 赋值
private final AtomicInteger id=new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread=new Thread(r);
thread.setName("Crawler-Thread-"+id.getAndIncrement());
return thread;
}
});
}
public void start(){
this.excutorservice.submit(this::parse);//爬取解析
this.excutorservice.submit(this::pipeline);//清洗
}
//爬取解析
private void parse(){
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("Parse occur exception {} .", e.getMessage());
}
final Page page = this.docQueue.poll();
if (page == null) {
continue;
}
this.excutorservice.submit(() -> {
try {
//采集
HtmlPage htmlpage = Crawler.this.webclient.getPage(page.getUrl());
page.setHtmlPage(htmlpage);
for (Parse parse : this.parseList) {
parse.parse(page);
}
//是详情页面执行
if (page.isDetail()) {
Crawler.this.detailQueue.add(page);
} else {
Iterator<Page> iterator = page.getSubPage().iterator();
while (iterator.hasNext()) {
Page subPage = iterator.next();
Crawler.this.docQueue.add(subPage);
iterator.remove();
}
}
} catch (IOException e) {
logger.error("Parse task occur exception {} .", e.getMessage());
}
});
}
}
//清洗
public void pipeline(){
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.error("Parse task occur exception {} .", e.getMessage());
}
//detail队列的page对象
final Page page=this.detailQueue.poll();
if(page==null){
continue;
}
//清洗的多线程调度
this.excutorservice.submit(()->{
for(Pipeline pipeline:Crawler.this.pipelineList){
pipeline.pipeline(page);
}
});
}
}
public void addPage(Page page){
this.docQueue.add(page);
}
//添加解析器
public void addParse(Parse parse){
this.parseList.add(parse);
}
//添加管道
public void addPipeLine(Pipeline pipeline){
this.pipelineList.add(pipeline);
}
//爬虫停止
public void stop(){
if(this.excutorservice!=null&&this.excutorservice.isShutdown()){
this.excutorservice.shutdown();
}
logger.info("Crawler stopped...");
}
}
Dao包下的相关类:
package com.symx.poetrytang.analyze.dao.impl;
import com.symx.poetrytang.analyze.entity.PoetryInfo;
import com.symx.poetrytang.analyze.model.AuthorCount;
import java.util.List;
/**
* 分析唐诗中作者的创作数量
**/
public interface AnalyzeDao {
List<AuthorCount> analyzeAuthorCount();//分析作者的创作数量
List<PoetryInfo> queryAllPoetryInfo();//查询所有诗文
}
采用JDBC编程连接到数据库,对数据库中的数据进行查询操作,得到每首诗的信息以及每个作者的创作数量。
package com.symx.poetrytang.analyze.dao.impl;
import com.symx.poetrytang.analyze.entity.PoetryInfo;
import com.symx.poetrytang.analyze.model.AuthorCount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 将数据库中存储的诗取出来进行分类整理得到每个作者的创作数量
**/
public class AnalyzeDaoImpl implements AnalyzeDao {
private final Logger logger = LoggerFactory.getLogger(AnalyzeDaoImpl.class);
private DataSource dataSource; //数据源对象
public AnalyzeDaoImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
/**
*方法名:analyzeAuthorCount()
*功能:分析作者的创作数量
*返回值:List集合,里面存储每位作者及其对应的作品数量
**/
public List<AuthorCount> analyzeAuthorCount(){
List<AuthorCount> datas=new ArrayList<>();
String sql="select count(*) as count,author from poetry_info group by author;";
try (Connection connection=dataSource.getConnection(); //通过数据源连接到数据库
//返回sql语句执行结果的结果集
PreparedStatement statement=connection.prepareStatement(sql);
ResultSet rs=statement.executeQuery()){
while(rs.next()){
AuthorCount authorCount=new AuthorCount();
authorCount.setAuthor(rs.getString("author"));
authorCount.setCount(rs.getInt("count"));
datas.add(authorCount);
}
} catch (SQLException e) {
e.printStackTrace();
}
return datas;
}
/**
*方法名:queryAllPoetryInfo()
*功能:查询所有诗的相关信息,包括题目、朝代、作者、内容,将其所查结果保存到一个链表
* 中,链表中的每个元素为没收拾的相关信息
* 返回值:保存有所有诗的相关信息的链表
*/
public List<PoetryInfo> queryAllPoetryInfo(){
List<PoetryInfo> datas=new ArrayList<>();
String sql="select title,dynasty,author,content from poetry_info;";
try(Connection connection=dataSource.getConnection();
PreparedStatement statement=connection.prepareStatement(sql);
ResultSet rs=statement.executeQuery();){
while(rs.next()) {
PoetryInfo poetryInfo=new PoetryInfo();
poetryInfo.setTitle(rs.getString("title"));
poetryInfo.setDynasty(rs.getString("dynasty"));
poetryInfo.setAuthor(rs.getString("author"));
poetryInfo.setContent(rs.getString("content"));
datas.add(poetryInfo);
}
}catch(SQLException e){
logger.error("Database query occur exception{}.",e.getMessage());
}
return datas;
}
}
entity包下:
定义一首诗的相关信息
package com.symx.poetrytang.analyze.entity;
import lombok.Data;
/**
* 对诗的信息进行分类
**/
@Data
public class PoetryInfo {
private String title;
private String dynasty;
private String author;
private String content;
}
model包下:
定义作者及其创作数量相关的类
package com.symx.poetrytang.analyze.model;
import lombok.Data;
/**
* 有关作者创作信息的类,包括作者以及作者的创作数量
**/
@Data
public class AuthorCount {
private String author;
private Integer count;
}
定义词频相关的类
package com.symx.poetrytang.analyze.model;
import lombok.Data;
/**
* 词频相关类
**/
@Data
public class WordCount {
private String word;
private Integer count;
}
service包下:
定义作者创作分析以及词云分析相关的类,相当于对前面model包下两个类的一次封装。
package com.symx.poetrytang.analyze.service.impl;
import com.symx.poetrytang.analyze.model.AuthorCount;
import com.symx.poetrytang.analyze.model.WordCount;
import java.util.List;
public interface AnalyzeService {
List<AuthorCount> analyzeAuthorCount(); //作者创作分析
List<WordCount> analyzeWordCount();//词云分析
}
基于前面 AnalyzeDao 对作者创作数量的分析结果,该类对此结果进行整理并进行了排序,同时进行分词操作
package com.symx.poetrytang.analyze.service.impl;
import com.symx.poetrytang.analyze.dao.impl.AnalyzeDao;
import com.symx.poetrytang.analyze.entity.PoetryInfo;
import com.symx.poetrytang.analyze.model.AuthorCount;
import com.symx.poetrytang.analyze.model.WordCount;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.NlpAnalysis;
import java.util.*;
public class AnalyzeServiceImp implements AnalyzeService {
private final AnalyzeDao analyzeDao;
public AnalyzeServiceImp(AnalyzeDao analyzeDao) {
this.analyzeDao = analyzeDao;
}
@Override
public List<AuthorCount> analyzeAuthorCount(){
//此处结果并未排序
//排序方式
//1. DAO层SQL排序
//2. Service层进行数据排序
List<AuthorCount> authorCounts=analyzeDao.analyzeAuthorCount();
//按count升序排
authorCounts.sort(Comparator.comparing(AuthorCount::getCount));
return authorCounts;
}
public List<WordCount> analyzeWordCount(){
/**
* 1,查询出所有数据
* 2,取出title,content
* 3,分词(过滤w,null,length<2)
* 4,统计(key-value)
**/
Map<String,Integer> map=new HashMap<>();
List<PoetryInfo> poetryInfos=analyzeDao.queryAllPoetryInfo();
for(PoetryInfo poetryInfo:poetryInfos){
List<Term> terms=new ArrayList<>();
String title=poetryInfo.getTitle();
String content=poetryInfo.getContent();
terms.addAll(NlpAnalysis.parse(title).getTerms());
terms.addAll(NlpAnalysis.parse(content).getTerms());
Iterator<Term> iterator=terms.iterator();
while(iterator.hasNext()){
Term term=iterator.next();
//词性的过滤
if(term.getNatureStr()==null||term.getNatureStr().equals("w")){
iterator.remove();
continue;
}
//词的过滤
if(term.getRealName().length()<2){
iterator.remove();
continue;
}
//统计
String realName=term.getRealName();
int count=0;
if(map.containsKey(realName)){
count=map.get(realName)+1;
}else{
count=1;
}
map.put(realName,count);
}
}
List<WordCount> wordCounts=new ArrayList<>();
for(Map.Entry<String,Integer> entry:map.entrySet()){
WordCount wordCount=new WordCount();
wordCount.setCount(entry.getValue());
wordCount.setWord(entry.getKey());
wordCounts.add(wordCount);
}
return wordCounts;
}
}
config包下:
ConfigProperties 类用来把代码里的输入固化到属性文件中,提供了连接所需的准。备信息,方便之后的修改
package com.symx.poetrytang.config;
import lombok.Data;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 从外部文件获得一些配置信息在该类里进行处理
**/
@Data
public class ConfigProperties {
//爬虫相关配置变量的定义
private String crawlerBase;
private String crawlerPath;
private boolean crawlerDetail;
//数据库相关配置变量的定义
private String dbUsername;
private String dbPassword;
private String dbUrl;
private String dbDriverClass;
//控制台相关配置变量的定义
private boolean enableConsole;
public ConfigProperties(){
//从外部文件加载
InputStream inputStream=ConfigProperties.class.getClassLoader().getResourceAsStream("config.properties");
Properties p=new Properties();
try {
p.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
this.crawlerBase=String.valueOf(p.get("crawler.base"));
this.crawlerPath=String.valueOf(p.get("crawler.path"));
this.crawlerDetail=Boolean.parseBoolean(String.valueOf(p.get("crawler.detail")));
this.dbUsername=String.valueOf(p.get("db.username"));
this.dbPassword=String.valueOf(p.get("db.password"));
this.dbDriverClass=String.valueOf(p.get("db.url"));
this.dbUrl=String.valueOf(p.get("db.driver_class"));
this.enableConsole=Boolean.valueOf(String.valueOf(p.getProperty("config.enable_console","false")));
}
}
ObjectFactory是对象工厂类,用来实例化我们所需要的所有对象,如配置对象,数据源对象,爬虫对象,web对象。这里用到了工厂方法的设计模式,其目的是为了将所有对象的创建与初始化放到工厂类中完成,这样在主方法里只需创建一个工厂对象就可完成所有所需对象的创建与初始化,进行对象封装,简化了主方法的代码量,使客户端变得更加简单,降低了代码的耦合度。
同时用单例的设计模式使构造方法私有化,目的是因为我们的数据源对象和爬虫对象等只需要有一个,因此初始化一个对象就可以,单例模式正好满足了我们的要求,保证了外部无法产生新的实例化对象,一定程度上也保证了程序的安全性。
package com.symx.poetrytang.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.symx.poetrytang.analyze.dao.impl.AnalyzeDao;
import com.symx.poetrytang.analyze.dao.impl.AnalyzeDaoImpl;
import com.symx.poetrytang.analyze.service.impl.AnalyzeService;
import com.symx.poetrytang.analyze.service.impl.AnalyzeServiceImp;
import com.symx.poetrytang.crawler.Crawler;
import com.symx.poetrytang.crawler.common.Page;
import com.symx.poetrytang.crawler.parse.DataPageParse;
import com.symx.poetrytang.crawler.parse.DocumentParse;
import com.symx.poetrytang.crawler.pipeline.ConsolePipeline;
import com.symx.poetrytang.crawler.pipeline.DatabasePipeline;
import com.symx.poetrytang.web.WebController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class ObjectFactory {
private static final ObjectFactory instance=new ObjectFactory();
private final Logger logger = LoggerFactory.getLogger(ObjectFactory.class);
//存放所有对象
private static final Map<Class,Object> objectHashMap=new HashMap<>();
private ObjectFactory(){
//1.初始化配置对象
initConfigProperties();
//2,创建数据源对象
initDataSource();
//3,爬虫对象
initCrawler();
//4,web对象
initWebController();
//5.对象清单打印输出
printObjectList();
}
private void initWebController() {
DataSource dataSource=getObject(DataSource.class);
AnalyzeDao analyzeDao=new AnalyzeDaoImpl(dataSource);
AnalyzeService analyzeService=new AnalyzeServiceImp(analyzeDao);
WebController webController=new WebController(analyzeService);
objectHashMap.put(WebController.class,webController);
}
private void initCrawler() {
ConfigProperties configProperties=getObject(ConfigProperties.class);
DataSource dataSource=getObject(DataSource.class);
final Page page =new Page(configProperties.getCrawlerBase(),
configProperties.getCrawlerPath(),
configProperties.isCrawlerDetail());
Crawler crawler=new Crawler();
crawler.addParse(new DocumentParse());
crawler.addParse(new DataPageParse());
if(configProperties.isEnableConsole()){
crawler.addPipeLine(new ConsolePipeline());
}
crawler.addPipeLine(new DatabasePipeline(dataSource));
crawler.addPage(page);
objectHashMap.put(Crawler.class,crawler);
}
private void initDataSource() {
ConfigProperties configProperties=getObject(ConfigProperties.class);
DruidDataSource dataSource =new DruidDataSource();
dataSource.setUsername(configProperties.getDbUsername());
dataSource.setPassword(configProperties.getDbPassword());
dataSource.setDriverClassName(configProperties.getDbDriverClass());
dataSource.setUrl(configProperties.getDbUrl());
objectHashMap.put(DataSource.class,dataSource);
}
private void initConfigProperties() {
ConfigProperties configProperties=new ConfigProperties();
objectHashMap.put(ConfigProperties.class,configProperties);
//打印出配置信息
logger.info("ConfigProperties info:\n{}", configProperties.toString());
}
public <T> T getObject(Class classz){
if(!objectHashMap.containsKey(classz)){
throw new IllegalArgumentException("class"+classz.getName()+"noy found Object");
}
return (T)objectHashMap.get(classz);
}
public static ObjectFactory getInstance(){
return instance;
}
private void printObjectList(){
logger.info("====== ObjectFactory List =====");
for(Map.Entry<Class,Object> entry:objectHashMap.entrySet()){
logger.info(String.format("[%-5s] ==> [%s]", entry.getKey().getCanonicalName(),
entry.getValue().getClass().getCanonicalName()));
}
logger.info("================================");
}
}
唐诗分析的主调度类
package com.symx.poetrytang;
import com.symx.poetrytang.config.ObjectFactory;
import com.symx.poetrytang.crawler.Crawler;
import com.symx.poetrytang.web.WebController;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
/**
* 唐诗分析主类
* */
public class tangshiAnalyze {
//加日志
private static final Logger LOGGER = LoggerFactory.getLogger(tangshiAnalyze.class);
public static void main(String[] args) {
WebController webController = ObjectFactory.getInstance().getObject(WebController.class);
//运行了web服务,提供接口
LOGGER.info("Web Server launch ...");
webController.launch();
//启动爬虫
if (args.length == 1 && args[0].equals("run-crawler")) {
Crawler crawler = ObjectFactory.getInstance().getObject(Crawler.class);
LOGGER.info("Crawler started ...");
crawler.start();
}
}
}
web包
将程序部署到服务器上
package com.symx.poetrytang.web;
import com.google.gson.Gson;
import com.symx.poetrytang.analyze.model.AuthorCount;
import com.symx.poetrytang.analyze.model.WordCount;
import com.symx.poetrytang.analyze.service.impl.AnalyzeService;
import com.symx.poetrytang.config.ObjectFactory;
import com.symx.poetrytang.crawler.Crawler;
import spark.ResponseTransformer;
import spark.Spark;
import java.util.List;
/**
* Web API
* Sparkjava完成API的开发
* */
public class WebController {
private final AnalyzeService analyzeService;
public WebController(AnalyzeService analyzeService){
this.analyzeService=analyzeService;
}
public List<AuthorCount> analyzeAuthorCount(){
return analyzeService.analyzeAuthorCount();
}
public List<WordCount> analyzeWordCount(){
return analyzeService.analyzeWordCount();
}
public void launch(){
ResponseTransformer transformer=new JSONResponseTransformer();
//前端静态文件的目录
Spark.staticFileLocation("/static");
Spark.get("/analyze/author_count", ((request, response) -> analyzeAuthorCount()),transformer);
Spark.get("/analyze/word_count",((request, response) -> analyzeWordCount()),transformer);
Spark.get("/crawler/stop", ((request, response) -> {
Crawler crawler = ObjectFactory.getInstance().getObject(Crawler.class);
crawler.stop();
return "爬虫停止";
}));
}
//Gson() 方法用于将对象序列化为字符串
public static class JSONResponseTransformer implements ResponseTransformer{
private Gson gson=new Gson();
@Override
public String render(Object o)throws Exception{
return gson.toJson(o);
}
}
}
数据库的创建语句
create database if not exists `tangshi`;
use tangshi;
create table if not exists `poetry_info`(
title varchar(32) not null,
dynasty varchar(32) not null,
author varchar(12) not null,
content varchar(1024) not null
);
js编程让其在浏览器显示
function creationRanking(id) {
//HTTP Method : GET
//HTTP URL : 请求地址(服务端提供的API接口)
$.get({
url: "/analyze/author_count",
dataType: "json",
method: "get",
success: function (data, status, xhr) {
//echarts图表对象
var myChart = echarts.init(document.getElementById(id));
var options = {
//图标的标题
title: {
text: '唐诗创作排行榜'
},
tooltip: {},
//柱状图的提示信息
legend: {
data: ['数量(首)']
},
//X轴的数据:作者
xAxis: {
data: []
},
//Y轴的数据:创作的数量
yAxis: {},
series: [{
name: '创作数量',
type: 'bar',
data: []
}]
};
//List<AuthorCount>
for (var i=0; i< data.length; i++) {
var authorCount = data[i];
options.xAxis.data.push(authorCount.author);
options.series[0].data.push(authorCount.count);
}
myChart.setOption(options, true);
},
error: function (xhr, status, error) {
}
});
}
function cloudWorld(id) {
$.get({
url: "/analyze/word_cloud",
dataType: "json",
method: "get",
success: function (data, status, xhr) {
var myChart = echarts.init(document.getElementById(id));
var options = {
series: [{
type: 'wordCloud',
shape: 'pentagon',
left: 'center',
top: 'center',
width: '80%',
height: '80%',
right: null,
bottom: null,
sizeRange: [12, 60],
rotationRange: [-90, 90],
rotationStep: 45,
gridSize: 8,
drawOutOfBound: false,
textStyle: {
normal: {
fontFamily: 'sans-serif',
fontWeight: 'bold',
color: function () {
//rgb(r,g,b)
return 'rgb(' + [
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') + ')';
}
},
emphasis: {
shadowBlur: 10,
shadowColor: '#333'
}
},
// Data is an array. Each array item must have name and value property.
data: []
}]
};
for (var i=0 ;i<data.length; i++) {
var wordCount = data[i];
//wordCount => 词 : 词频
options.series[0].data.push({
name: wordCount.word,
value: wordCount.count,
textStyle: {
normal: {},
emphasis: {}
}
});
}
myChart.setOption(options, true);
},
error: function (xhr, status, error) {
}
});
}
九、运行结果
作者创作数量分析结果图:
词云分析结果图
十、项目测试
说到测试,我们首先要回到需求,最初我们的需求是分析唐诗中作者的创作数量以及词语的词频,因此我们很有必要对分析结果的可靠性进行测试,以下列出了一些测试点作为测试用例对该项目进行测试;
测试结果:
在对爬虫的正常功能进行测试时,我们先在古诗文网页里选了一篇超链接页和详情页查看它的网页源码, 通过对源码的分析,我们写程序来抓取它的Dom对象,并对其里面的内容进行解析清洗,将结果在控制台打印出来,打印结果无误,我们在采用多线程编程对整个网页的数据进行采集、解析、清洗与存储,然后在数据库里查看其存储的内容对其爬取结果的正确性进行分析。
在异常测试时发现,更改数据库的密码,则爬取结果不能存储到数据库中,更改网站的url地址,则爬虫不能爬取到我们需要的数据;在爬虫爬取时断网,则爬取的数据不完整,致使最终的分析结果不准确,影响功能的正常实现。
在性能测试方面我是采用手工测的,因为我们采用了多线程调度器来进行处理,因此处理速度还是较快的,差不多一分钟的时间就已经爬取到了所有的诗文。
十一、总结
这次项目的完成中,遇到了很多技术上的难题,比如爬虫那块,不知道如何爬取网页上的数据并进行解析,后经老师指导用htmlunit工具来实现爬虫部分,还有前端的js部分也不太会,后经过查找资料,了解它的用法,来帮助我们实现了前端的可视化页面部分。由此总结,做项目可能有的东西你并没有学过,但只要我们知道了它如何使用,解决了实际问题就行了。还有一点就是不言败喽。