目录
五、IDEA环境下将项目打成 war 包并部署到本地 tomcat 中
一、唐诗项目概述
1、项目简介
在大数据时代,信息的采集是一项非常重要的工作,如果单靠人力进行信息采集整理,不仅低效繁琐,搜索的成本也高。那么此时,我们就可以通过网络爬虫对数据信息进行采集。但在我们自己练习的过程中,可不要选个学校教务系统去爬,要不然还没爬到你就先没了~,网络安全也是非常重要的!所以,要找一个没有反爬虫技术且数据公开合法的,于是,我在这里选择了唐诗三百首进行爬虫,另一方面,web前端开发也是一个十分热门的行业,所以在爬取到数据后我又进行了可视化展示,通过图表更直观,好看,比一大堆文字获取信息更简洁方便,关键是赏心悦目呀。
2、设计思想
2.1、数据爬取模块
- 采集列表页,获取古诗文网首页的html文件
- 分析列表页,采集详情页,即从 html 文件中通过 href 标签拿到每首诗的链接
- 从详情页中提取诗词信息(标题、朝代、作者、正文)
- 计算 sha256(标题+正文),保证数据不会重复
- 调用第三方库对标题与正文进行分词
- 建立数据库与表,将数据存进去
2.2 可视化展示模块
- 浏览器发起 httpServlet 请求,Tomcat 将其作为静态资源处理,返回静态资源
- 浏览器因为 script 标签 GET 所有 js 文件
- js 中通过 jQuery 发起 ajax 请求
- Tomcat 将其作为 Servlet 处理
- 通过 JDBC 执行 sql 语句在数据库中取到数据后 Servlet 使用 FastJSON 把数据序列化为字符串
- 通过自己实现的函数把数据调整为 echarts 满足的格式后传给 echarts
- echarts 进行绘图展示
将数据库中的数据经过整理分别以一个柱形图来表示诗人及他们的创作数量,再对所有诗词的分词及使用频率通过一个词云来展示。
3、核心技术
- Java 操作 MySQL 数据库
- 数据库设计
- 第三方库的使用,比如HtmlUnit、ansj_seg等
- SHA256去重算法
- HTTP协议
- 多线程与线程池的使用
- echarts前端渲染、jQuery前后端交互、JSON响应字符串、ajax前端展示的使用
- 软件测试的方法
4、效果展示
诗人创作排行:
诗词云图展示:
二、数据爬取模块
1、技术选型
1.1 爬虫技术 htmlunit
网页抓取的第三方库有很多比如 HttpClient,HtmlUnit,在对比这两个的使用区别后我选择了 htmlunit
原因:HttpClient是用来模拟HTTP请求的,用的是socket通信,通过get方法来提交请求,只能获取html静态页面的源码,如果页面中有js部分,则不能获取到js执行后的源码。
HtmlUnit是一款无界面的浏览器程序库,它模拟用户去操作浏览器,允许调用页面,填写表单,点击链接等,还可以执行js,有很多的API用起来也非常方便。
引入依赖:
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.36.0</version>
</dependency>
1.2 分词技术 ansj_seg
展示页面涉及到了根据每一个词的出现频率来进行词图的展示。使用ansj_seg库对古诗的标题和正文进行分词,为词云做准备。这个中文分词器正确率高,不容易出做,分词速度也快,效果也比较高。
引入依赖:
<dependency>
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
1.3 JDBC 操作数据库 MySQL
这是一个轻量级的数据库,操作方便,且支持SQL语句。利用客户端我们可以方便的对数据进行存储与管理。
引入依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
1.4 项目管理工具 maven
为什么选择maven开发?
1、maven一个命令就可以完成项目构建过程
2、他可以进行强大的依赖管理
3、可以分阶段进行构建
4、提供web项目的模式
2、用法调研
2.1 请求解析列表页 HtmlUnitDemo
HtmlUnit是一个无界面的浏览器,它自带http客户端,给他一个url,它就会去请求页面。用它里面的webclient类,就可以完成请求和解析,从列表页里筛选我们需要的标签进入详情页。
过程中遇到的问题:抓取网页运行后会出现许多警告,这是因为 HtmlUnit 对 CSS 和 JavaScript 的支持不是很好,因此可以关闭 js 和 CSS 的执行引擎。
//1.请求过程
// 声明一个浏览器对象,版本为 CHROME,try 之后会自动关闭
try(WebClient webClient = new WebClient(BrowserVersion.CHROME)) {
//关闭浏览器的 js 与 css 执行引擎,即不再执行网页中的 js 和 css
//没有关闭 js 和 css 引擎的话会有很多警告
webClient.getOptions().setJavaScriptEnabled(false);
webClient.getOptions().setCssEnabled(false);
//对列表页进行请求,返回一个列表页的 dom 树
HtmlPage page = webClient.getPage("https://so.gushiwen.org/gushi/tangshi.aspx");
System.out.println(page);//HtmlPage(https://so.gushiwen.org/gushi/tangshi.aspx)@777341499
// 2.保存内容
// page.save(new File("唐诗三百首\\列表页.html"));
// 3.从 HTML 中提取需要的信息(解析 - 树的结点的查找)
HtmlElement body = page.getBody();// 取一个结点
System.out.println(body);//HtmlBody[<body onclick="closeshowBos()">]
//获取div标签中class属性为typecont的一组元素,由语法可知它返回的是一个 集合类型
List<HtmlElement> elements = body.getElementsByAttribute(
"div", "class", "typecont");
// //取出第一个就是唐诗三百首中的五言绝句部分
// HtmlElement divElement = elements.get(0);
// List<HtmlElement> aElement = divElement.getElementsByAttribute(
// "a", "target", "_blank");
// System.out.println(aElement.size());//五言绝句中的诗词数量
// System.out.println(aElement.get(0).getAttribute("href"));//打印第一首诗的地址
// 提取所有的
int count=0;
for(HtmlElement element:elements){
List<HtmlElement> list = element.getElementsByTagName("a");
for(HtmlElement e:list){
//获取 a 标签下所有属性为 href 的值
System.out.println(e.getAttribute("href"));
count++;
}
}
System.out.println(count);//诗词总数
2.2 请求解析详情页 XPathDemo
// 题目
String xpath = "//div[@class='cont']/h1/text()";
DomText domText = (DomText)(body.getByXPath(xpath).get(0));
System.out.println("题目:"+domText);
// 朝代
xpath = "//div[@class='cont']/p[@class='source']/a[1]/text()";
domText = (DomText)(body.getByXPath(xpath).get(0));
System.out.println("朝代:"+domText);
// 作者
xpath = "//div[@class='cont']/p[@class='source']/a[2]/text()";
domText = (DomText)(body.getByXPath(xpath).get(0));
System.out.println("作者:"+domText);
// 正文
xpath = "//div[@class='cont']/div[@class='contson']";
HtmlElement element = (HtmlElement)(body.getByXPath(xpath).get(0));
System.out.println("正文:"+element.getTextContent().trim());
2.3 sha256 算法去重
引入 sha256 是为了防止下载重复诗词
MessageDigest message = MessageDigest.getInstance("SHA-256");
String s = "验证sha256";
byte[] bytes = s.getBytes("UTF-8");
message.update(bytes);//加密
byte[] result = message.digest();//运算
for(byte b : result){
System.out.printf("%02x",b);
}
2.4 计算分词
给它一句话,可以帮你分成词,使用方便。NLP(Nature Language Process)自然语言识别。直接使用 NlpAnalysis.parse(sentence).getTerms();调用静态方法将要解析的字符串传入并调用getTerms方法返回一个Term的集合。
利用该方法可以对获取的诗的内容进行词语提取。
String sentence = "使用 NLP 自然语言识别来进行分词";
List<Term> list = NlpAnalysis.parse(sentence).getTerms();
for(Term term : list){
// 词性:分词
System.out.println(term.getNatureStr()+":"+term.getRealName());
}
2.5 存入数据库
(1)建库建表:
建库:create database tangshi charset utf8mb4;
使用该数据库:use tangshi;
建表:create table tangshi(
id INT PRIMARY KEY AUTO_INCREMENT,
sha256 char(100) not null unique,
dynasty varchar(10) not null,
title varchar(40) not null,
author varchar(10) not null,
content text not null,
words text not null
)charset utf8mb4;
显示建表信息: show create table tangshi;
清空表:truncate tangshi;
(2)通过 JDBC 连接数据库并存入数据
MysqlConnectionPoolDataSource ds = new MysqlConnectionPoolDataSource();
ds.setServerName("127.0.0.1");
ds.setPort(3306);
ds.setUser("root");
ds.setPassword("");
ds.setDatabaseName("tangshi");
ds.setUseSSL(false);
ds.setCharacterEncoding("UTF-8");
try(Connection con = ds.getConnection()){
String sql = "INSERT INTO tangshi " +
"(sha256, dynasty, title, author, content, words) " +
"VALUES (?, ?, ?, ?, ?, ?)";
try(PreparedStatement st = con.prepareStatement(sql)){
st.setString(1,"sha256");
st.setString(2,dynasty);
st.setString(3,title);
st.setString(4,author);
st.setString(5,content);
st.setString(6,"");
st.executeLargeUpdate();
}
}
3、整体代码
3.1单线程版本
全部由主线程完成,数据库存入操作慢,线程安全。
3.2 性能优化-多线程
(1)自己启动线程
方法:将 请求详情页,提取诗词信息,计算 SHA-256,计算分词,存数据库 操作放到多线程中。
发现问题:
- WebClient 、Connection、messageDigest 不是线程安全的,加锁互斥达不到多线程效果,每个线程创建自己的对象的话每次只有一个对象在跑
- "Too many connections"报错,查找原因发现是因为连接太多
(2)利用线程池 - Executors.newFixedThreadPool(int)方式创建的线程池。
发现问题:
- 忘记清理数据库上次存入的数据会出现报错,报错原因:重复插入,解决办法:忽略报错
catch(SQLException e){
if(!e.getMessage().contains("Duplicate entry"))){
e.printStackTrace();
}
}
- 爬取完成后进程并不会结束,JVM结束条件:所有非后台线程结束才会结束,但线程池中的线程是不会结束的,每个线程任务完成后,资源归还给线程池,线程池不停止,那么JVM就不会停止。
解决办法:在所有线程结束后调用 pool.shoudown(),那么怎么知道线程结束了呢?在这里有两种方法:
1.利用 CountDownLatch 类,计数 AtomicInteger可以加锁保证线程安全,但会引起线程的调度,成本较高。
在主线程中建立对象 countDownLatch,并将需要等待结束的线程数作为参数传给线程:
CountDownLatch countDownLatch=new CountDownLatch(detailUrlList.size()); //传入的参数是诗的个数( 320 )
然后在每个线程结束时调用一下 countDown() 方法
countDownLatch.countDown(); //个数减1(最初该对象里面的属性值为 320)
最后在主线程中调用 await() 方法,所有线程结束后该方法就会被执行通过,然后关闭线程池。
countDownLatch.await(); //等待 320 首诗都上传到数据库(一直等到 countDownLatch 对象里面的属性值为 0)
pool.shutdown(); //关闭线程池
2.利用原子类CAS(Compare And Swap),不会引起线程调度。
在类中定义变量:
private static AtomicInteger successCount=new AtomicInteger(0); //原子类
private static AtomicInteger failureCount=new AtomicInteger(0); //原子类
在每个线程(成功插入时)加入代码:
successCount.getAndIncrement(); //插入成功 successCount++;
在每个线程(插入失败时)加入代码:
failureCount.getAndIncrement(); //插入失败 failureCount++;
在主线程中加入代码:
while(successCount.get()+failureCount.get()<detailUrlList.size()){
// 没有加 \n 表示只回头,不换行,每次都覆盖上次的数据
System.out.printf("一共 %d 首诗,成功插入 %d 首诗\r",detailUrlList.size(),successCount.get());
TimeUnit.SECONDS.sleep(1); //1秒打印一次
}
System.out.println();
System.out.println("全部上传成功");
pool.shutdown();
3.3 总结
- 在创建数据库过程中,要合理设计每个字段的类型和大小,不要浪费资源但也不能不够用,可以在过程中多实践
- 在做项目前要有整体思路,由大到小,逐步实现,在遇到没有接触过的知识时要多查阅资料,技术选型时要根据考虑多种因素来选择,一般使用简单方便,易操作,文档完善或者周围人使用较多的合适,这样遇到问题也好请教或网上搜索来解决
- 多线程时一定要考虑线程安全
- 通过一次次实践来学会如何逐渐优化代码
三、可视化分析模块
1、技术选型
1.1 前端页面展示渲染工具( echarts )
要做一个可视化项目,就需要了解有哪些成熟的可视化第三方库可以使用,最后我选择了 echarts,原因如下:
- 中文文档、开源免费
- 使用方便,直接在网站上就可以进行调试来观察效果
- 使用人多,遇到问题方便提问
结合 echarts 官网文档,进行一些基础的开发,通过改代码观察效果来了解使用方法。
1.2 jQuery 发起的 ajax 请求
只用 echarts 数据是写死的,但我们的数据是要从数据库中取出来的,因此只用 echarts 是无法得到我们预期结果的。于是我选择使用 jquery 来进行 ajax 请求,再用 httpServlet 来处理请求,返回一个 json 格式的字符串,等加载成功后执行 success 方法去进行可视化,这样的数据就是动态的。
为什么选择 jquery:jquery 相对比较简单,文档比较成熟
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
1.3 JSON 第三方库的选择
返回Json格式的第三方库有很多,比如Gson(Google 速度快,BUG少),Jackson(速度快,BUG少),FastJson等,我选择FastJson主要是因为写起来简单,是阿里巴巴维护的,我这个应用小,也估计碰不到它的BUG。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
2、代码开发
2.1 JDBC 建立数据库连接
这里用的是饿汉的设计模式
2.2 整理作者诗词数量 RankServlet
首先,要知道每个作者写了多少首诗,就需要写一个 sql 聚合查询语句来统计作者诗词数量。然后 RankServlet 类继承自 HttpServlet ,通过 doGet 方法处理 ajax 发起的 http 请求,再将数据写入到 JSONArray 中,返回一个 Json 格式的字符串。
SELECT author, count(*) AS cnt FROM tangpoetry GROUP BY author HAVING cnt >=? ORDER BY cnt DESC;
- 用@WebServlet(“rank.json”)来配置path路径,就不需要去web.xml去配置servlet了。
- 发起ajax请求,收到响应后去执行success里面的方法,进行柱状图的渲染。
- 通过html页面的script标签找到js文件去执行。
@WebServlet("/rank.json") public class RankServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json; charset=utf-8"); String condition = req.getParameter("condition"); if (condition == null) { condition = "5"; } JSONArray jsonArray = new JSONArray(); try (Connection connection = DBConfig.getConnection()) { String sql = "SELECT author, count(*) AS cnt FROM tangshi GROUP BY author HAVING cnt >= ? ORDER BY cnt DESC"; //把作者全部统计出来 try (PreparedStatement statement = connection.prepareStatement(sql)) { statement.setString(1, condition); try (ResultSet rs = statement.executeQuery()) { while (rs.next()) { String author = rs.getString("author"); int count = rs.getInt("cnt"); JSONArray item = new JSONArray(); item.add(author); item.add(count); jsonArray.add(item); System.out.println(author+":"+count); } resp.getWriter().println(jsonArray.toJSONString()); } } } catch (SQLException e) { e.printStackTrace(); JSONObject object = new JSONObject(); object.put("error", e.getMessage()); resp.getWriter().println(object.toJSONString()); } } }
2.3 整理分词 WordsServlet
- 继承HttpServlet,通过doGet方法处理请求,将分词都先放入到list中,再用map来整理每个词出现的次数,最后将Key和Value来放入到JSONArray中,返回一个json格式的字符串。
- 同样用@WebServlet("/words.json")
@WebServlet("/word.json") public class WordsServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json; charset=utf-8"); Map<String,Integer> map = new TreeMap<>(); JSONArray jsonArray = new JSONArray(); try (Connection connection = DBConfig.getConnection()) { String sql = "SELECT words FROM tangshi"; try (PreparedStatement statement = connection.prepareStatement(sql)) { try (ResultSet rs = statement.executeQuery()) { while (rs.next()) { String words = rs.getString("words"); String[] wordList = words.split(","); for(int i = 0;i < wordList.length;i++){ if(map.containsKey(wordList[i])){ map.put(wordList[i],map.get(wordList[i])+1); }else{ map.put(wordList[i],1); } } } for(Map.Entry<String,Integer> entry : map.entrySet()){ JSONArray item = new JSONArray(); item.add(entry.getKey()); item.add(entry.getValue()); jsonArray.add(item); } resp.getWriter().println(jsonArray.toJSONString()); } } } catch (SQLException e) { e.printStackTrace(); JSONObject object = new JSONObject(); object.put("error", e.getMessage()); resp.getWriter().println(object.toJSONString()); } } }
2.4 前后端交互
利用$.ajax()发起一个HTTP请求,后台收到请求时返回给前端一个index.html文件,这个文件里面的Script标签又会主动向后台发送http请求,得到json格式的数据,js中的代码把数据写到echart中,页面就展示出来了也引入第三方库,js请求交给相对应的servlet去处理(创作数量排行榜用RankServlet()去处理,诗词用词云图用wordServlet()去处理)
四、项目测试
1、功能测试
- 网页爬取是否正常
- 数据清洗是否正常
- 数据爬取到数据库中是否正常
- 数据的正确性
- tomcat 未启动是否可以加载
- 运行过程中关闭 tomcat,是否会继续显示
- tomcat 8080端口被占用是否可以访问正常
- 电脑未联网,是否可以访问
- 页面中的按钮是否能跳转到各自对应的页面进行展示
- 点击一次后回退与前进按钮是否使用正常
- 创作排行中点击某一列是否会出现渐变颜色,是否显示诗人信息与诗词数量
- 词云界面展示方式是否从内到外
- 点击词云页面具体分词时是否会出现阴影
- 词云界面点击刷新是否更新词云
2、性能测试
- 输入地址点击回车后响应时间长短
- 单线程下数据爬取到存入数据库耗时55秒
- 多线程下需要 4 秒
- 多个用户同时点击,系统相应时间以及承载能力
3、界面测试
- 界面按钮位置是否合理
- 页面文字显示正确
- 界面色彩是否合适
- 界面操作是否便捷
- 界面缩小内容如何显示
4、兼容性测试
- 在不同浏览器上,界面显示是否正确
- 在不同操作系统上,界面是否显示正确
- 不同版本浏览器,是否正确显示
5、易用性测试
- 按钮简单易用,有提示
- 输入 url 就可以查看结果
- 回退与前进按钮使用正常
- 界面只有一页不需要上下滚动浏览
6、安全性测试
- 数据爬取是否有反爬取机制
- 数据库是否安全
五、IDEA环境下将项目打成 war 包并部署到本地 tomcat 中
1、打war包
1、点击左上角【File】-> 【Project Structure】菜单:
2、在【Project Structure】中选择左侧的【Artifacts】页签,点击【Web Application:Exploded】-> 【Empty】:
4、创建 web 项目时,【Eclipse】中自动生成目录是 WebContent,【myeclipse】中生成 WebRoot,【IDEA】中生成 webapp 目录,而此处我是建立的 maven 项目,目录中没有 webapp 目录,而我在编写前端代码时的目录起名为 web,因此这里会显示错误:
此时点击“+”号,选择【Directory Content】,选择需要的目录:
5、添加完对应的目录后,点击右下角【Apply】后点击【OK】
6、构建项目,点击菜单栏【Build】-> 【Build Artifacts】后点击刚才的 war 包,选择 【Build】
7、构建成功后就会在目录多出一个目录,因为刚才我没有改变 war 包路径,它默认会在这个项目文件下生成 out -> artifacts -> war包。
到了这里 war 包就打好了。接下来就是部署到 Tomcat 运行。
2、部署
1、找到 war 包所在的位置,并将它复制到 tomcat的 webapps 目录下:
2、进到 tomcat 安装目录下的【bin】文件夹,点击 【startup.bat】启动 tomcat
启动成功的话在浏览器输入【localhost:8080】就会出现以下界面:
3、去浏览器输入项目地址就可以查看自己的项目了:
4、如果项目代码出现了改动就需要重新构建部署:只需要在菜单栏选择 【Build】-> 【Build Artifacts】,然后选择 ReBuild ,接下来根据上述 2 的内容重新进行部署就可以了。