打造私人搜書系統之系統設計

  故事是這樣的,在沒有王者農藥之前,筆者大部分業餘時間都是靠着有毒的免費小說來打發的。那時都是先通過百度搜索小說名,然後進入相應的小說網站來看,有很多不爽的體驗:第一,每次都要百度,去找有這個小說的網站;第二,每家網站的更新速度不一樣,有時要切換很多次才能找到最新章節;第三,這些小說網站都有很多廣告,嚴重影響了閱讀體驗。
  大概在今年年初的時候,萌生了一個想法,就是打造一個自己的搜書系統,基本的思路是:從各個小說網站爬取相關的書籍信息,通過一個手機客戶端來閱讀小說。經過半年多的打造,目前該系統已經基本完成,這裏主要從技術角度來做些分享。
  整個系統架構如下圖所示,數據系統和Web服務組成了後端服務,客戶端採用Android開發(筆者屌絲,買不起iphone)。數據系統,一方面負責從各個小說網站採集數據,按照既定的格式進行存儲;另一方面將採集的原始數據進行異步加工,生成適合搜索、可以快速訪問的中間數據。Web服務,主要提供REST API供前端訪問,包括搜索服務、獲取書籍列表等等。值得一提的是,考慮到版權和存儲容量的問題,從外部小說網站採集過來的只是一些摘要信息和原始鏈接,不涉及具體的小說文章內容(後面會具體介紹)。因此,Andorid端就主要負責搜索、書籍列表展示和從原始鏈接的HTML中提取小說內容進行顯示即可。


  整個後端服務是系統的關鍵,Android App只是內容展現的一種方式而已,因此這裏將重點介紹後端服務。本文將從系統設計的角度,來談談設計的思路和踩過的坑,後面將另起一文來介紹如何基於Elasticsearch構建系統的搜索服務。

數據爬取

  數據的爬取工作,需要考慮這麼幾點:

  • 從哪裏爬取數據?
  • 爬取什麼數據?
  • 如何進行全量爬取和增量更新?
  • 數據如何存儲?
  • 採用何種爬蟲框架來實現?

  帶着這些問題,調研了幾家排名較前的免費小說網站,驚奇的發現各家的網站佈局結構幾乎一致,基本由四部分組成:
1)首頁,裏面是一些熱門和推薦的小數,以HTML表格形式呈現。
2)某本書的摘要信息頁面,從中可以提取出該小說的基本信息。另外,頁面URL的格式基本是http://domain/book/id,id就是這本書在數據庫中的存儲id,由此我們可以推斷出該網站總共擁有多少本書,在做全量爬取時,直接循環替換這個id就可以了。
3)某本書的目錄頁,從中可以提取出該小說的所有章節名稱和鏈接。該頁面的URL格式頁基本也是攜帶id的格式。
4)某本書某章節的具體內容頁面,從中可以提取出該章節的小說內容,HTML的格式也基本一致。
  下圖所示即爲某網站的一個摘要信息頁面。


  各個網站佈局的一致性讓人非常驚喜,這意味着可以用比較通用的一套代碼來完成所有的數據爬取工作。結合上面的網頁結構分析,基本可以確定數據爬取的實現方案。
1)爬取內容爲兩部分:一個是書的基本信息,另一個是書的所有章節信息。二者均爲格式化的數據,考慮使用MySQL來存儲,採用兩張關聯的表來存儲數據。前文也提到,這裏只存儲相應的小說章節的鏈接,並不斷更新,不會存儲具體的小說章節文字內容。


2)如前面分析所言,網站中某本書的摘要信息和目錄信息頁面的URL都是由id組成的,因此在全量爬取時,嘗試從id=0開始爬取,然後逐漸遞增id來構建新的URL,直到連續10個URL都是無效的爲止,如此便可以爬取該網站的全部小說信息了。全量爬取比較耗費時間,因此只做一次,在這之後就是每日的增量更新了。增量更新時選取最近15天內沒有更新過的小說的URL,重新去爬取它的章節信息即可。
3)關於爬蟲框架的選擇,因爲筆者對Python比較熟悉,所以優先考慮與之相關的框架。Scrapy是一個不錯的選擇,可以實現快速開發,抓取Web網站並從頁面中提取結構化的數據,能較好的滿足需求,具體如何使用Scrapy這裏不做詳細描述。至於在爬取過程中如何有效的避免反爬蟲,筆者的策略是具體問題具體分析,遇到一個解決一個,不要過多的提前設計。慶幸的是這些免費小說網站的反爬蟲策略都很弱,大部分沒有,小部分通過降低頻率和設置USER_AGENT也都解決了。
  值得提出的是,數據爬取的關鍵是對要爬取的網站進行詳細的分析,然後纔是實現。

數據融合

  有了爬取的數據後,就可以實現查找某本書並閱讀其內容了,但是這裏還有另外幾個問題:

  • 比如要看小說A,有多個小說網站都有這本書,但是每家的更新頻率不一樣,如何快速找出更新到最新的那家網站的書和相關章節信息?
  • 由於都是一些免費的小說網站,有時會很不穩定甚至無法訪問,如何及時的屏蔽這些無效網站的書源信息?

  解決這個問題有兩個思路,一個是在每次查詢時進行分析和信息篩選,另一個是針對爬取的數據進行分析和融合,生成中間數據作爲查詢的數據。第一個方案需要在每次查詢中都做一次,會影響查詢的響應時間,而第二個採用異步的方法來生成中間數據,相比而言,筆者更傾向於第二個方案。實施這個方案分兩步:
  第一步,建立vendor信息表(vendor表示小說網站),記錄某個小說網站是否有效,當前訪問延遲是多少等等。在做異步融合時,首先去探測該小說網站是否可以訪問以及訪問延遲,並進行更新。如果人爲覺得某個網站不穩定,也可以手動將其置爲無效。
  第二步,建立book_best_source表,記錄當前時刻每本書的優選書源是什麼。在做異步融合時,會遍歷上述的book表,爲每本書挑選出一個最佳書源和一個備用最佳書源,挑選的策略是按照三個條件進行排序:網站可訪問–>該書的章節數量–>網站的訪問延遲。


數據存儲水平拆分

  完成數據爬取和異步融合後,整個數據系統就趨於完善了。然而運行一段時間後,數據存儲就暴露出問題了,出問題的是chapter這張表。按照之前的設計,chapter表用來存儲所有書籍的所有章節信息,每章一條記錄。隨着爬取的書籍越來越多,該表也越來越大,查詢的效率開始下降,並且在有一天,這張表的id不夠大了(默認INT類型),導致新的數據無法寫入。
  針對這個問題,開始考慮對chapter表進行水平拆分,拆分的策略是按照書源來區分,即爲每個小說網站的書建立不同的表來存儲章節信息。拆分的工作分佈在三塊:
1)數據存儲,即MySQL中建立新的表和數據遷移;
2)數據爬取,需要根據當前爬取的是哪家網站來選取對應的章節表進行存儲;
3)Web服務,在REST API來查詢時,需要根據查詢的書所屬的書源來查找對應的章節信息。
  慶幸的是整個系統並不複雜,拆分工作很快就完成了。這件事也說明在早期進行數據庫設計時,要充分考慮數據的增長速率,並作出相應的策略。

Web服務

  Web服務就是提供Android客戶端所需要的後臺服務API,採用Python Django來搭建。在該系統中,Android客戶端比較簡單,主要包含下面四塊:
1)搜索頁,通過關鍵詞來搜索相關的書籍;
2)書籍列表頁,用來顯示搜索出來的書籍列表;
3)書籍章節列表頁,用來顯示某本書的章節列表,即目錄;
4)內容頁面,即用來顯示某本書某章節的小說內容。


  結合前端需求,需要開發兩個REST API來供前端調用:
1)搜索API

GET
/api/v1/book/search/?keyword=大主宰

Response:
{
    "count": 20,
    "results": [
        {
            "id": 694077,
            "name": "大主宰. ",
            "author": "天蠶土豆",
            "vendor": "網站1",
            "cover": "",
            "category": "玄幻奇幻",
            "brief": "大千世界,位面交匯,萬族林立,羣雄薈萃..."
        }
    ]
}

2)根據某本書的id,獲取其章節信息

GET
/api/v1/book/694077/chapters/

Response:
{
    "count": 70,
    "results": [
        {
            "name": "大主宰.",
            "link": "http://www.example.com/20296/779515.html",
            "id": 97416937
        },
        {
            "name": "第一章 北靈院",
            "link": "http://www.exapmle.com/20296/779516.html",
            "id": 97416938
        }
    ]
}


  整個系統設計大致如上所述,當然中間還有很多細節,比如開發中間出現阿里雲的磁盤空間不夠用,導致MySQL數據無法寫入等等。由於只是把它當做個人項目來鍛鍊,利用業餘時間來做,系統的實現歷時近半年,到目前爲止,系統已接近完成,運行也相對穩定。如前文所訴,後面還將另開一文來聊一聊期間搜索的事情。



(全文完,本文地址:http://blog.csdn.net/zwgdft/article/details/75209434
Bruce,2017/08/26


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