1. 程式人生 > >打造私人搜書系統之系統設計

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

  故事是這樣的,在沒有王者農藥之前,筆者大部分業餘時間都是靠著有毒的免費小說來打發的。那時都是先通過百度搜索小說名,然後進入相應的小說網站來看,有很多不爽的體驗:第一,每次都要百度,去找有這個小說的網站;第二,每家網站的更新速度不一樣,有時要切換很多次才能找到最新章節;第三,這些小說網站都有很多廣告,嚴重影響了閱讀體驗。
  大概在今年年初的時候,萌生了一個想法,就是打造一個自己的搜書系統,基本的思路是:從各個小說網站爬取相關的書籍資訊,通過一個手機客戶端來閱讀小說。經過半年多的打造,目前該系統已經基本完成,這裡主要從技術角度來做些分享。
  整個系統架構如下圖所示,資料系統和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資料無法寫入等等。由於只是把它當做個人專案來鍛鍊,利用業餘時間來做,系統的實現歷時近半年,到目前為止,系統已接近完成,執行也相對穩定。如前文所訴,後面還將另開一文來聊一聊期間搜尋的事情。