一.項目簡介
本項目分爲兩個模塊,分別是:詩詞爬取模塊和數據可視化模塊。主要是通過對 古詩文網 的唐詩三百首的內容進行爬取,並將爬取到的內容存儲到 MySQL 數據庫當中,並將相關的數據內容進行統計,最終將其以圖表等形式展示出來的一個 JavaWeb 項目。(其中對詩人以及他們分別創作的著作數量使用柱狀圖來進行展示,對所有詩詞的所有分詞以及它們的使用頻率使用詞雲來進行展示)
核心技術:
- Servlet 的使用
- Java 操作 MySQL 數據庫
- 數據庫設計
- 響應風格
- gson 的使用
- HTTP 協議的理解
- Servlet 的使用
- HtmlUtil 第三方庫的使用
- ansj_seg 第三方庫的使用
- sha-256 算法
- 多線程技術
- 軟件測試的基本策略和方法
二.技術選型
1.前端頁面展示渲染工具( echarts )
做一個可視化項目:調研有哪些成熟的可視化第三庫類庫可以使用,最後選擇echarts。
echarts官網
echarts 它是一個開源免費的 javascript 可視化庫,本項目中前端頁面展示中用到的柱狀圖和雲圖皆來源於它。
選擇第三方庫 echarts 的原因:
- 開源免費
- 使用方便:可以直接在其網站上進行調試,得到自己想要的效果,然後進行下載
- 功能豐富
- 社區活躍,使用人多,方便詢問
2.前後端交互技術( jQuery)
利用 $.ajax() 發起一個 HTTP 請求,進行前後端數據的交互。
3.列表頁和詳情頁的請求和解析 ( HtmlUtil )
利用 HtmlUtil 第三方庫中的相應方法,來進行一個 Html 頁面的請求和解析工作。
在 pom.xml 中加入相應依賴:
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.36.0</version>
</dependency>
通過 HtmlUnit 庫,我們可以很輕易的模擬一個瀏覽器,發起對一個網頁的請求,並獲得相應的頁面元素 HtmlPage .
4. Java 語言操作數據庫( JDBC )
在 pom.xml 中加入相應依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
5.響應字符串( fastjson )
控制自己編寫的每一個 Servlet 類實現的方法中的每一個返回值(響應數據)都是 JSON 格式的字符串。
使用 fastjson 第三方類庫的原因:
- 使用方便
- 支持國產
在 pom.xml 中加入相應依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
6.分詞功能( ansj_seg )
展示頁面涉及到了根據每一個詞的出現頻率來進行詞圖的展示。這裏根據每一首詩的標題和內容來進行分詞。我們採用第三方類庫 ansj_seg 來完成分詞功能。
在 pom.xml 中加入相應依賴:
<dependency>
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
7.Maven 來進行項目管理
使用原因:幫助我們完成項目構建解決一切繁瑣事宜
Maven 的好處:
- 提供一個標準的項目工程目錄
- 提供項目描述
- 提供強大的版本管理工具
- 可以分階段的進行構建過程
- 提供了豐富的插件庫使用
- 幫助我們進行依賴管理
三.選型技術的簡單使用Demo
1.使用 HtmlUtil
package lab;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlBody;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class HtmlUtilDemo {
public static void main(String[] args) throws IOException {
//無界面的瀏覽器(HTTP客戶端)
WebClient webClient=new WebClient(BrowserVersion.CHROME);
//關閉了瀏覽器的js執行引擎,不再執行網頁中的js腳本
webClient.getOptions().setJavaScriptEnabled(false);
//關閉了瀏覽器的css執行引擎,不再執行網頁中的css佈局
webClient.getOptions().setCssEnabled(false);
HtmlPage page = webClient.getPage("https://so.gushiwen.org/gushi/tangshi.aspx");
System.out.println(page);
File file=new File("唐詩三百首\\列表頁.html");
file.delete();
page.save(new File("唐詩三百首\\列表頁.html"));
//如何從html 中提取我們需要的信息
HtmlElement body= page.getBody();
List<HtmlElement> elements=body.getElementsByAttribute(
"div",
"class",
"typecont");
for(HtmlElement element:elements){
System.out.println(element);
}
/* 打印結果:
HtmlPage(https://so.gushiwen.org/gushi/tangshi.aspx)@1424108509
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont">]
HtmlDivision[<div class="typecont" style="border:0px;">]
*/
System.out.println("----------------------------------------");
HtmlElement divElement=elements.get(0); //取第一個模塊
List<HtmlElement> aElements=divElement.getElementsByAttribute(
"a",
"target",
"_blank");
for(HtmlElement element:aElements){
System.out.println(element);
}
System.out.println(aElements.size());
System.out.println(aElements.get(0).getAttribute("href"));
}
}
2.計算 sha-256 的值
package lab;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class 求SHA256Demo {
public static void main(String[] args) throws NoSuchAlgorithmException, UnsupportedEncodingException {
// MD5 (快被淘汰,因爲沒有 SHA-256安全)
// SHA-256
MessageDigest messageDigest=MessageDigest.getInstance("SHA-256");
String s="你好世界";
byte[] bytes=s.getBytes("UTF-8");
messageDigest.update(bytes); //先放進去(此方法要求傳入一個字節數組,所以要把字符串進行轉換)
byte[] result=messageDigest.digest(); //再取出來(加密完算出來的值)
System.out.println(result.length);
for(byte b:result){
System.out.printf("%02x",b); //UTF-8一個字節佔兩位,把數據一個字節一個字節往出打
}
//哈希:單向的 不能返回去
//beca6335b20ff57ccc47403ef4d9e0b8fccb4442b3151c2e7d50050673d43172
}
}
3.計算分詞
package lab;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.NlpAnalysis;
import java.util.List;
public class 分詞Demo {
public static void main(String[] args) {
String sentence="中華人名共和國成立了!中國人民站起來了";
//一個Term就是一個單詞
List<Term> termList=NlpAnalysis.parse(sentence).getTerms();
for(Term term:termList){
// 詞性:詞
System.out.println(term.getNatureStr()+":"+term.getRealName());
}
/*
ns:中華人名共和國
v:成立
u:了
w:!
ns:中國
n:人民
v:站
v:起來
u:了
*/
}
}
NlpAnalysis.parse(String s).getTerms();
// 調用靜態方法將要解析的字符串傳入並調用 getTerms() 方法返回一個Term 的集合(其中:一個 Term 就是一個詞)
4.將數據插入數據庫
package lab;
import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.*;
public class 插入詩詞Demo {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String 朝代="唐代";
String 作者="白居易";
String 標題="問劉十九";
String 正文="綠蟻新醅酒,紅泥小火爐。晚來天欲雪,能飲一杯無?";
/* 獲取Connection的第一種方法
//1.註冊 Driver
Class.forName("com.mysql.jdbc.Driver");
//2.通過 DriverManger 獲取 Connection
String url="jdbc:mysql://127.0.0.1/tangshi?useSSL=false&characterEncoding=utf8";
Connection connection=DriverManager.getConnection(
url,
"root",
"123456"
);
System.out.println(connection);
Statement statement=connection.createStatement();
String sql="insert into tangshi(sha256,dynasty,author,content,words) values()";
statement.executeUpdate(sql);
*/
// 獲取 Connection 的第二種方法(通過 DataSource 獲取 Connection )
Class.forName("com.mysql.jdbc.Driver");
// DataSource dataSource=new MysqlDataSource(); //不帶有連接池
MysqlConnectionPoolDataSource dataSource=new MysqlConnectionPoolDataSource(); // 這個帶有連接池(有利於管理連接)
dataSource.setServerName("127.0.0.1");
dataSource.setPort(3306);
dataSource.setUser("root");
dataSource.setPassword("123456");
dataSource.setDatabaseName("tangshi");
dataSource.setUseSSL(false);
dataSource.setCharacterEncoding("UTF-8");
try(Connection connection=dataSource.getConnection()) { //拿到連接
String sql="insert into tangshi(sha256,dynasty,author,title,content,words) values(?,?,?,?,?,?)"; //佔位符
try(PreparedStatement statement=connection.prepareStatement(sql)) {
statement.setString(1, "qazwsxedcrfvtgbyhnujmikolp");
statement.setString(2, 朝代);
statement.setString(3, 作者);
statement.setString(4, 標題);
statement.setString(5, 正文);
statement.setString(6, "");
statement.executeUpdate(); //插入
}
}
}
}
四.數據庫表的設計
1.建表語句
CREATE DATABASE tangshi CHARSET utf8mb4;
use tangshi;
CREATE TABLE tangshi(
id INT PRIMARY KEY AUTO_INCREMENT,
sha256 CHAR(64) NOT NULL UNIQUE,
dynasty VARCHAR(20) NOT NULL,
title VARCHAR(30) NOT NULL,
author VARCHAR(10) NOT NULL,
content TEXT NOT NULL,
words TEXT NOT NULL
);
爲什麼要引入 sha-256?
使用 sha256 ,爲每首詩生成一個唯一標識符,可以(標題 +正文)來計算 sha-256 的值,保證同一首詩不會被重複插入。
項目中使用的 SQL 語句:
SELECT author, count(*) cnt FROM tangshi WHERE cnt >= 5 GROUP BY author;
五.項目編寫思路
模塊1. 從古詩文網爬取數據保存到數據庫
模塊2. 從後端(數據庫)獲取數據來進行最終頁面的展示
先定義一下兩個頁面:
列表頁:
詳情頁:
模塊 1 的實現思路:
1.請求和解析列表頁
- 列表頁中包含了每個詳情頁的後半部分 url
2.請求和解析詳情頁
- 進入到詳情頁頁面可以獲取詩詞的相關信息(標題,作者,朝代,內容等信息),這裏採用 XPath 來獲取這些信息。
XPath的簡單介紹:
3.計算 sha-256 的值,目的是爲了使數據庫中不存儲同一首詩,不同詩的 sha-256 的值不同(利用標題+內容計算每一首詩的 sha-256的值)
4.計算分詞(只選取標題和正文的內容來進行分詞,並且長度小於 1 的詞不算,標點符號也不算一個詞,爲 null 也不算一個分詞)。由於分詞之後的詞有可能會重複情況發生,所以要進行統計,存儲的時候就是以 key-value 格式存儲到 Map 集合當中去的。( key是詞,value是詞頻 )
5.所有數據就緒完畢,將數據插入到數據庫
6.循環執行序號 2-6 的操作,直至所有的古詩都已經全部插入到數據庫
模塊 2 的實現思路:
1.編寫兩個 Servlet ,一個是 RankServlet(用來從數據庫中獲取作者和作者相對應的詩詞數量),一個是 WordsServlet (用來從數據庫中獲取每一首詩的分詞情況)兩個 Servlet 的響應內容都爲 JSON 格式的字符串。
2.通過 $.ajax() 發起一個 HTTP 請求,從服務器後端獲取相應數據,用來填充 echarts 圖表中的相關內容
3.提取數據庫中的信息選擇合適的圖形界面來展示
- 各個詩人以及其對應的詩詞創作數量(柱狀圖);
- 根據詞的出現頻率展示圖片(雲圖)。
六.詩詞爬取模塊代碼實現
1.單線程版本
所有內容全部由主線程完成,無線程安全問題,但是速度最慢。
2.多線程版本
列表頁的請求和解析主線程去做,詳情頁的請求和解析交給線程去做。目的是爲了提高效率。
(1)出現問題:
WebClient,Connection,PreparedStatement ,生成SHA-256的MessageDigest 都不是線程安全的。
(2)解決方案:
讓上述這些對象在每一個線程中都有一個自己的對象,這樣就不會出現線程安全問題了。
3.線程池版本一
採用 Executors.newFixedThreadPool(int) 方式創建一個固定大小的的線程池
(1)出現問題:
程序(進程)不能自己停止,就算所有詩詞已經全部放入數據庫也不會停止。
(2)出現原因:
因爲 JVM 在所有的非後臺線程都結束後纔會結束,而線程池中的線程是永遠不會停止的(每個線程執行完任務後,自己又返回到線程池當中)那麼JVM 就不會停止。
(3)解決方法:
待所有的詩都成功上傳到數據庫當中時,顯示調用 pool.shutdown(); 方法即可讓程序停止執行。
4.線程池版本二
改進線程池版本一(採用兩種辦法去解決)
(1)通過 Atomic 原子類
在類中定義變量:
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();
(2)通過 CountDownLatch
在主線程中建立對象 countDownLatch,並作爲參數傳給線程:
CountDownLatch countDownLatch=new CountDownLatch(detailUrlList.size()); //傳入的參數是詩的個數( 320 )
當每個線程任務結束的時候,加入代碼:
countDownLatch.countDown(); //個數減1(最初該對象裏面的屬性值爲 320)
最後在主線程中加入:
countDownLatch.await(); //等待 320 首詩都上傳到數據庫(一直等到 countDownLatch 對象裏面的屬性值爲 0)
pool.shutdown(); //關閉線程池
七.數據可視化模塊代碼實現
使用技術:Servlet,JSON,jQuery,ajax ,echarts
index.html 代碼:
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<meta charset="UTF-8">
<!-- 網頁的標題-->
<title>唐詩可視化</title>
</head>
<body>
<!-- 網頁醒目的標題-->
<h1>唐詩可視化</h1>
<div>
<input type="button" value="著作數量排行榜" onclick="rank()">
<input type="button" value="分詞雲圖" onclick="words()">
</div>
<div id="main" style="width: 800px;height:500px; position: absolute; "></div> <!-- id="main" 一定要在 src=js/index.js 前-->
<script src="js/jquery-3.3.1.min.js"></script>
<script src="js/echarts.min.js"></script>
<script src="js/echarts-gl.min.js"></script>
<script src="js/echarts-wordcloud.min.js"></script>
<script src="js/index.js"></script>
</body>
</html>
解釋:
八.效果展示
過程中實踐的頁面:
九.項目附加
1.怎樣根據域名,得到一個網站的 IP 地址
2.列表頁的結構
3.查看數據庫的字符集編碼
4.置空一張表(把表裏面的內容刪掉)