唐詩可視化項目

一.項目簡介

本項目分爲兩個模塊,分別是:詩詞爬取模塊和數據可視化模塊。主要是通過對 古詩文網 的唐詩三百首的內容進行爬取,並將爬取到的內容存儲到 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.置空一張表(把表裏面的內容刪掉)
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章