一. 唐詩項目介紹
1.1項目背景
爬蟲技術一直處於風口浪尖,爬了不該爬的會引來一身官司,爬想爬取的數據又不會輕而易舉獲得,但是又想來玩玩爬蟲技術,怎麼辦呢?首選古詩文網,因爲據瞭解它是沒有設置任何反爬蟲機制的,數據都是公開合法的,這裏我就選擇了唐詩三百首來進行爬蟲,你們不好奇哪位唐代詩人寫的詩最多嗎?你們不好奇詩人們最喜歡用什麼詞去作詩嗎?如果你對唐詩也感興趣,那就跟我一起開啓這個奇妙的詩詞探險吧。
1.2項目需求
一.用柱狀圖來展示詩人們的姓名和詩詞數量,並按詩詞數量降序排序。
二.用詞雲來展示詞的使用頻度,使用最頻繁的詞應該一眼看出。
1.3項目設計
一.通過爬蟲機制把唐詩的數據爬取出來,對它進行解析,將每首詩的數據寫入到數據庫中。
二.通過發起請求來對數據進行分析篩選,最後渲染成可視化效果。
1.4項目工具選擇
這是一個Web項目,Java選擇用IDEA這個編譯器,選擇用maven來引進一些需要用到的第三方庫,通過tomcat這個Web應用服務器來執行。
二.唐詩數據爬取模塊
2.1技術選型環節
2.1.1爬蟲技術
我瞭解到的爬蟲技術棧有 HtmlUnit,HttpClient,這裏我選擇了HtmlUnit這個框架。
- 原因:HttpClient是用來模擬HTTP請求的,用的是socket通信,通過get方法來提交請求,只能獲取html靜態頁面的源碼,如果頁面中有js部分,則不能獲取到js執行後的源碼。
HtmlUnit是一款無界面的瀏覽器程序庫,它模擬用戶去操作瀏覽器,允許調用頁面,填寫表單,點擊鏈接等,還可以執行js,有很多的API用起來也非常方便。
2.1.2分詞技術
使用ansj_seg庫對古詩的標題和正文進行分詞,爲詞雲做準備。這個中文分詞器正確率高,不容易出做,分詞速度也快,效果也比較高。
2.2項目依賴環節
解析請求html頁面
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.36.0</version>
</dependency>
分詞
<dependency>
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
2.3預研環節
通過編寫一些Demo來熟悉這些技術的使用,看看它展示的效果是否滿意。
2.3.1解析列表頁Demo
WebClient client = new WebClient(BrowserVersion.CHROME);
client.getOptions().setJavaScriptEnabled(false);
client.getOptions().setCssEnabled(false);
String baseurl = "https://so.gushiwen.org";
String pathurl = "/gushi/tangshi.aspx";
List<String> detailUrlList = new ArrayList<>();//所有古詩的詳情頁的url
//列表頁的解析
{
String url = baseurl + pathurl;
HtmlPage page = client.getPage(url);//獲取詩詞頁面
List<HtmlElement> divs = page.getBody().getElementsByAttribute("div", "class", "typecont");
for (HtmlElement div : divs) {
List<HtmlElement> as = div.getElementsByTagName("a");
for (HtmlElement a : as) {
String detailUrl = a.getAttribute("href");
detailUrlList.add(baseurl + detailUrl);
}
}
}
2.3.2分詞Demo
public static void main(String[] args) {
String sentence="中華人民共和國成立了!中國人民從此站起來了";
List<Term> termlist=NlpAnalysis.parse(sentence).getTerms();
for(Term term:termlist){
System.out.println(term.getNatureStr()+":"+term.getRealName());
}
}
2.4具體開發環節
2.4.1數據庫的設計
數據庫要存取:自增主鍵id,標題,朝代,作者,正文,分詞,sha-256。
- SHA-256是爲了防止下載重複詩詞,所以用每首詩的標題和正文去計算SHA-256值。
CREATE TABLE `tangpoetry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sha256` char(64) NOT NULL,
`dynasty` varchar(10) NOT NULL,
`title` varchar(100) NOT NULL,
`author` varchar(10) NOT NULL,
`content` text NOT NULL,
`words` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `sha256` (`sha256`)
) ENGINE=InnoDB AUTO_INCREMENT=703 DEFAULT CHARSET=utf8mb4
2.4.2爬取,解析數據
- 獲取列表頁解析出每首詩的url
- 解析詳情頁得出要往數據庫中寫入的數據(標題,朝代,作者,正文,計算SHA-256,計算分詞(切記分詞要去除特殊字符,比如符號,null等)需要將分詞拼接成字符串格式,方便存儲)
2.4.3數據寫入到數據庫中
- 通過JDBC建立數據庫連接,將標題,朝代等信息插入到數據庫的表中。
2.4.4驗證並優化—多線程
*通過sql語句查看320首詩詞數據是否成功寫入到數據庫中。如果信息無誤,則插入成功。
- 單線程版本效率低,每次都是主線程去完成各項任務,在解析詳情頁的時候每次都是主線程,而總共有320首詩,就要挨個去解析320次,非常耗時。
- 引入多線程版本:讓主線程去獲取列表頁,解析列表頁,得出每首詩的url,開啓多個線程,去完成列表頁解析工作,並將數據寫入到數據庫中。
問題:1.webclient不是線程安全的,每個線程都得自己建一個webclient對象
2.connection不是線程安全的,需要通過傳
datasource這個參數去建立連接。
3.MessageDigest這個計算SHA-256的也不是線程安全的。
多線程每次都要創建320個子線程去執行任務,執行完再銷燬,顯然有點浪費資源。 - 線程池版本:這裏用的是Executors.newFixedThreadPool(30)這個固定數目的線程池,把多線程要處理的那些任務去交給線程池裏面的線程去完成,這裏我設置的是30個核心線程。
BUG: JVM結束是指所有非後臺線程執行結束後才關閉,而線程池一直在主線程中,根本不可能自己關閉,它會不斷地去執行任務,重複的死循環,永不停止。
解決辦法: 只要等所有子線程都完成任務,就可以手動關閉線程池。 - 如何判斷子線程任務是都都結束了呢?
1.CAS原子類
private static AtomicInteger successCount=new AtomicInteger(0);
private static AtomicInteger failureCount=new AtomicInteger(0);
private static class Job implements Runnable{
private void work() throws IOException, InterruptedException {
Random random=new Random();
int n=random.nextInt(5);
if(n<2){
throw new IOException();
}
Thread.sleep(5);
}
@Override
public void run() {
try{
work();
//successCount++;
successCount.getAndIncrement();
}catch (IOException e){
//failureCount++;
failureCount.getAndIncrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<COUNT;i++){
Thread thread=new Thread(new Job());
thread.start();
}
while(successCount.get()+failureCount.get()!=COUNT){
Thread.sleep(1000);
System.out.println("任務還沒完成");
}
System.out.println("任務全部完成");
}
2.CountDownLatch:開始會設定要等待的線程數,主線程阻塞,每執行完一個子線程就調用countDown(),此時計數器-1,直到計時器爲0時纔將主線程喚醒,不爲0 就一直await(),爲0說明子線程任務都執行完了,就可以關閉線程池了。
CountDownLatch countDownLatch = new CountDownLatch(detailUrlList.size());//指定countDownLatch要等待的線程數
for (String url : detailUrlList) {
pool.execute(new Job(url,dataSource2,countDownLatch));
}
countDownLatch.await();//如果沒處理結束,就等待
pool.shutdown();//最後關閉線程池
}
2.5總結問題
- 在創建數據庫的時候,要合理的設計每個字段的類型,不能盲目浪費資源。
- 多線程設計,一定要考慮線程安全問題,不然出大錯。
- 學會不斷地優化項目。
三.詩詞可視化分析模塊
3.1技術選型
3.1.1 echarts可視化
echarts是一個開源免費的javascript可視化庫,柱狀圖和詞雲都是來自於它。而且它是開源的,中文文檔,方便上手。
3.1.2 jquery的發起ajax請求
因爲echarts的數據是寫死的,所以使用jquery來發起ajax請求,用httpservlet來處理請求,返回一個json格式的字符串,等加載成功執行success方法去進行可視化,這樣數據就是從請求響應獲取的,不再是寫死的。
3.1.3 FastJson格式
返回Json格式的第三方庫有很多,比如Gson,FastJson等,我選擇FastJson主要是因爲寫起來簡單,是阿里巴巴維護的,我這個應用小,也估計碰不到它的BUG。
3.2導入依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
3.3代碼開發
3.3.1 JDBC建立數據庫連接
這裏用的是餓漢的設計模式,來進行數據庫連接。
3.3.2 整理詩的數量
- 繼承HttpServlet,通過doGet方法來處理ajax發起的http請求,用複合查詢sql語句就可以搞定,再將數據寫入到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裏面的方法,進行柱狀圖的渲染。
method: "get", // 發起 ajax 請求時,使用什麼 http 方法
url: "rank.json?condition=10", // 請求哪個 url
dataType: "json", // 返回的數據當成什麼格式解析
success: function (data) { // 成功後,執行什麼方法
通過html頁面的script標籤找到js文件去執行。
3.3.3 整理分詞
- 繼承HttpServlet,通過doGet方法處理請求,將分詞都先放入到list中,再用map來整理每個詞出現的次數,最後將Key和Value來放入到JSONArray中,返回一個json格式的字符串。
- 同樣用@WebServlet("/words.json")