索引擎和聚合類新聞App之所以有源源不斷的新內容供予用戶瀏覽,原因就在于有網絡爬蟲技術的加持。網絡爬蟲的應用對于用戶來說,是一大福利——我們可以從一個搜索引擎輕松搜索到各個領域的信息。但是,對于原創方來說,就涉及到版權被侵犯的問題了。工具理性,但不意味著操持工具的人就可以假借“工具理性”肆意侵犯他人的合法權益,網絡爬蟲技術的應用還應該要在法理之內。
工作的時候,想要查找“產品設計”,可以直接在搜索引擎上輸入內容,就可以直接找到數以百萬計的資料。
上下班路上,刷新聞類APP的時候,只要愿意,就會有源源不斷的新的信息,足夠刷一路的時間。
搜索引擎和(大多數)新聞類APP都不自己生產內容(雖然有些平臺孵化了自己的內容,但也只占整個平臺內容的很少的一部分,更重要的是,成本非常高)。
那么,他們的大量的內容從哪里來?
“我們不生產內容,只是內容的搬運工”,將互聯網上的內容“搬運”到自己的服務器上,這就是爬蟲。
首先,我們需要了解一下互聯網的結構。
互聯網上的內容數以億計,雖然很復雜,但說白了就是一張大網,網上的每個節點就是一個網頁,連接網頁的超鏈接(Hyperlinks)相當于線,線把所有的節點連接在一起,形成了一個復雜的網。
通過點擊超鏈接的文字或者圖片,就可以跳轉到對應的網頁。爬蟲可以自動訪問到每一個網頁,并把網頁的內容保存下來。
世界上第一個網絡爬蟲由麻省理工學院的學生馬修·格雷(Matthew Gray)在1993年寫成,之后的爬蟲盡管越來越復雜。
比如:可以實現更快的訪問速度、訪問更多的網頁、更好的將網站內容解析出來。但爬蟲的基本原理是一樣的,都主要包括三個部分:訪問網頁鏈接,下載網頁內容,解析網頁內容。
爬蟲的工作過程與我們查找網頁的過程是一樣的。
比如,我們想要查一下豆瓣上最新的電影:首先,在瀏覽器地址欄輸入網址鏈接https://movie.douban.com/,之后,瀏覽器會跳轉到豆瓣電影。最后,我們就可以找到當前熱映的電影。
同樣的,一個最簡單的爬蟲三步就可以爬取一個網頁——首先,訪問這個網頁,之后,把網頁內容下載下來,最后,對下載的內容進行解析。
最簡單的爬蟲三步就可以爬取一個網頁,那么要寫多少行代碼呢?
我們寫一個爬蟲,爬取豆瓣的“一周口碑榜”,只要7行代碼!
這里我們使用Python語言,至于為什么選擇Python語言,會在后面交代清楚,如果不懂Python也沒有關系,了解爬蟲是如何工作的就可以了。
代碼如下:
import requests from lxml
import html url=’https://movie.douban.com/’ # 1、需要爬數據的網址
page=requests.Session.get(url) # 2、訪問網頁
tree=html.fromstring(page.text) # 3、解析網頁的過程
result=tree.xpath(‘//td[@class=”title”]//a/text’) #3、解析網頁的過程
print(result) # 打印出結果
在Python環境中運行這幾行代碼,就可以獲取“一周口碑榜”了,結果如下:
[‘迦百農’, ‘綠皮書’, ‘馴龍高手3’, ‘速成家庭’, ‘阿麗塔:戰斗天使’, ‘膚色’, ‘死亡天使’, ‘黎明墻’, ‘小小巨人’, ‘出·路’]
其中最關鍵的是解析網頁內容,主要是(‘//td[@class=”title”]//a/text’)這行代碼,大多數人可能對比較困惑。
這涉及到HTML網頁的結構,可以把網頁理解成一個文件夾,打開一個文件夾,會發現子文件夾,子文件夾或許還有文件夾。通過打開一個個文件夾,最終找到需要的數據。
至于是怎么寫出來這行代碼的,可以通過在網頁空白處點擊右鍵,查看源代碼,就可以找到對應的td、class=”title”、a等標識符。
大多數程序員寫爬蟲選擇python的理由很簡單.
首先,python有很多的庫,可以直接調用,比如:上面的代碼就引入了requests、lxml庫,分別實現訪問網頁、對網頁結構解析。有開源的庫,就直接調用,避免重復造輪子。
其次,python寫起來很方便,配置也簡單,短短幾行的代碼,就可以直接運行了,如果使用C或者Java,可能配置環境就要老半天。
把上面的每個步驟分別實現(模塊化),就可以構成一個簡答的爬蟲系統。
使用URL(可以理解為網址鏈接)管理器管理所有的網址鏈接,使用HTML(可以理解為網頁內容)下載器下載網頁內容,使用HTML解析器對下載的內容解析,再加上數據存儲模塊、控制整個爬蟲的調度模塊,就構成了一個簡單的爬蟲系統。
更具體的說,URL管理器負責管理所有的網址鏈接,記錄下哪些URL已經爬取了,哪些還沒有爬取。如果爬取過了,就要避免再次下載,如果沒有,就要加入隊列,等HTML下載器下載。
HTML下載器可以從服務器下載整個網頁的內容,從URL管理器中獲取未爬取的網址鏈接,之后,訪問這些網頁鏈接,下載網頁。
HTML解析器負責解析下載好的網頁,主要有兩個任務:一方面,解析出需要的信息,比如上文的“一周口碑榜”;另一方面,解析出新的URL鏈接,交給URL管理器,繼續下載,這個功能在上面的“7行代碼”沒有實現。
數據存儲器實現存儲數據的功能,將HTML解析器解析出來的信息存儲起來,否則每次使用都要下載,會浪費大量的時間。圖片、文檔之類的文件可以直接保存到服務器上,文字類的可以通過數據庫存儲起來。
爬蟲調度器作為系統的大腦,負責統籌其他四個模塊的協調工作。
無論是大型的還是小型的爬蟲雖然在設計細節,性能上有所不同,但都不會脫離這五個模塊。
乍一看,每個模塊實現起來都很簡單,但細想,似乎每個模塊都要考慮很多東西。
7行代碼爬取豆瓣電影,直接訪問網址鏈接(https://movie.douban.com/)就可以爬取“一周口碑榜”。對稍大一些的爬蟲系統或者商用爬蟲,就要有更多的考慮了,在保證獲取充足信息的同時,也要保證下載的質量。
對搜索引擎公司而言,要盡可能包括互聯網所有的信息。對垂直領域,更多的偏向業務類信息,比如:對新聞類的APP,主要包括一些新聞網站、政府網站等,對Github這類的編程網站,他們可能就不感興趣。
巧婦難為無米之炊,初始的網址鏈接基本要靠人工憑經驗獲取,比如:新聞類的APP,他們的初始URL列表里可能就包括新浪、網易、搜狐等門戶網站,也包括各個級別的政府網站,還有人民網、新華社、人民日報等媒體的網站。
當一個頁面下載完成后,從這個網頁中提取出其中的網址鏈接,把它們添加到等待下載的隊列中,就可以獲得更多的網址鏈接。
如果一個網頁已經下載過了,重新下載,會消耗大量的時間,并占用存儲空間。更要命的是,如果一直重復下載,就可能陷入死循環。
那么,如何知道這網址鏈接是不是已經下載過了?
對于小型爬蟲,可以使用列表存儲下載過的網址鏈接,當有新的網址鏈接的時候,先查找這個列表中有沒有該網址鏈接。如果有的話,就不用插入,如果沒有的話,就插入列表,等待訪問下載。
對于大型爬蟲,有成百上千個“小爬蟲”(更加專業的名詞叫做分布式爬蟲),分布在不同的服務器上,同時爬取網址鏈接,就要考慮更多的東西。
比如:不同爬蟲之間的分工和通信,如何共同維護上述的列表。
當數據很大的時候,就要考慮分布式、通信、存儲、帶寬等每個環節的限制,無論哪個環節沒有做好,都有可能成為系統的瓶頸,這就像是木桶效應中的短板。
數據量增加10倍,之前的代碼可能要重寫了,工作量可能就要增加100倍,這也是量變引起質量的一個很好的例子。
在計算機領域,這樣的例子隨處可見,當數據增大到一定量級,原有的算法很可能無法繼續使用,需要重新開發,隨之而來的是加班、DEBUG以及延期上線。
爬取豆瓣電影的“一周口碑榜”,需要研究網頁的源代碼,并編寫對應的解析代碼。但是網頁的結構不同,用這個代碼爬取知乎,解析不到任何內容。
以新聞類的APP為例:一個好的新聞類APP需要爬蟲數以億計的網頁,并把里面的文字、視頻、圖片分別解析出來,難度可想而知。
好消息是一部分網站會遵守RSS規范(遵守RSS規范的網頁結構和代碼都有相似性,以便于訂閱器獲取主要信息),一種類型的爬蟲就可以爬取大量這種類似的網頁。但大部分的網站的結構,都是不同的,這需要算法工程師花費大量的時間和精力做解析工作。
新聞類APP通過爬蟲,獲得大量的優質資源,讀者也樂意在一個平臺上看到所有的內容,但“被爬取”的網站就不太高興了。對于大多數依靠廣告收入的網站,沒有了流量,連生存都成了問題,更別說盈利了。
一些自成體系的平臺,比如:大型電商平臺,他們希望所有的用戶在自己的平臺上查找信息,所有的商家在自己的平臺上吸引賣家(廣告費可不能付給搜索引擎),同樣不希望爬蟲的騷擾。
搜索引擎希望爬取更多的信息,優質的內容提供商又不希望被爬蟲騷擾,利益沖突難以調和,于是產生了Robots協議來解決這個問題。
Robots協議網站服務器的一個聲明,通常是保存在網站根目錄下的一個TXT格式的文件,網站通過Robots協議告訴搜索引擎:哪些頁面可以抓取?哪些頁面不能抓取?
當爬蟲訪問一個站點時,它會首先檢查該站點根目錄下是否存在robots.txt,如果存在,爬蟲就會按照該文件中的內容來確定訪問的范圍;如果該文件不存在,所有的爬蟲將能夠訪問網站上所有沒有被口令保護的頁面。
我們使用搜索引擎,經常會看到“由于該網站的robots.txt文件存在限制指令(限制搜索引擎抓取),系統無法提供該頁面的內容描述”,就是源于這個協議。
值得注意的是:Robots協議是國際互聯網界通行的道德規范,并沒有強制性約束力。
一些“沒有道德”的爬蟲同樣會爬取有robots.txt限制指令的網站,這時候就需要一些技術來實現反爬蟲了。
最常見的有三種方式:
每個電腦都有唯一的IP地址,每個爬蟲也有唯一的IP地址,當電腦或者爬蟲訪問網站的時候,網站會記錄這個IP地址。如果同一個IP短時間多次訪問同一個網站,這個網站可能會傾向于認為這是個爬蟲,會采取一些措施。
當然,這在反爬蟲的同時,也會給用戶帶來一些不好的體驗。
相比之下,一些比較優秀的網站或者APP,會根據用戶點擊頻率、時間間隔等信息,判斷是不是爬蟲或者誤點擊,之后再確定是否需要驗證。
更好的用戶體驗背后,是更大的開發成本,更長的開發周期。
當我們使用瀏覽器訪問網站的時候,瀏覽器會自動在訪問請求上添加一些信息,比如:瀏覽器采用的編碼方式、使用的操作系統、瀏覽器版本等信息放在訪問請求的最開始,作為Headers,但爬蟲一般不會附加這些信息。
網站會根據是否存在Headers信息以及Headers信息的內容,判斷對方是不是爬蟲,有必要的話,就拒絕訪問。
之前將的HTML網頁都是靜態的,隨著HTML代碼生成,頁面的內容和顯示效果就不會發生變化了。而動態網頁則不然,動態網站是腳本語言(比如PHP)生成的,一些內容不是直接可見的,而是要運行一些腳本,才能看到。
網址后綴為htm、html、shtml、xml的網頁是靜態網頁,而動態網頁是以·aspx、.asp、.jsp、.php、.perl、.cgi等形式為后綴,并且在動態網頁網址中有一個標志性的符號“?”,這些不同的后綴基本代表了網頁使用的語言。
訪問靜態網頁,只需要直接訪問鏈接就可以了,訪問動態網站,還需要執行一些特定的操作(比如點擊),才能顯示更多的內容,這就增加了爬取的難度,一些簡單的爬蟲就被拒之門外了。
介紹完三種主流的反爬蟲的方式,最后要說的是:反爬蟲技術也不是一勞永逸的,在反爬蟲的發展過程中,爬蟲也進化出了一系列反“反爬蟲”的方式。
針對反爬蟲驗證IP機制,爬蟲“進化”出了IP代理池,這樣,爬蟲就可以不斷變換自己的IP地址,迷惑反爬蟲。針對Headers驗證,爬蟲也會生成一個Headers信息,甚至針對動態頁面,也會模擬瀏覽器的行為。
雖然如此,反爬蟲在一定程度上提高了爬蟲的成本,降低了爬蟲的效率,就可以將一大部分爬蟲擋在門外。
從爬蟲與反爬蟲的例子也可以看出:大多數時候,沒有絕對的有效方式。提高對方的成本,讓對方覺得代價太大,得不償失,就是很好的解決問題的辦法。
上面講了爬蟲是怎么運行的,常見的反爬蟲機制。最后,我們再講一個爬蟲的應用場景的例子,可以幫助我們更好理解爬蟲。
冷啟動是每一個產品經理、運營人員和創業者面臨的重大問題。沒有優質的內容,就吸引不了用戶,沒有大量的用戶,就無法吸引優質的內容,就陷入了先有雞還是先有蛋的悖論。
爬蟲,低成本、快速地解決了這個問題!
“我們不生產新聞,我們只是新聞的搬運工”,通過爬蟲,低成本、快速地爬取整個互聯網的優質內容,并憑借海量數據,利用算法實現內容分類和個性推薦(個性推薦系統會在后序章節詳細介紹),吸引大量的用戶,最終通過廣告變現。
事實證明,這是個非常成功的商業模式。而媒體平臺和新聞網站雇傭大量編輯人員,花費大量時間、金錢寫成的高質量內容,連說一聲都沒有,就這樣被拿走了,這不是侵犯人家版權嘛!
于是,多家媒體聯合發起侵權訴訟或抗議聲討,最終迫使這家新聞巨頭支付版權費,但無論法律上、道德上有多少問題,都不影響這家公司商業成功的既定事實。
類似的事情同樣發生在其他垂直領域。
一家新成立的技術博客平臺,爬取競爭對手上的文章,迅速實現優質內容的聚合。如果原博客主發現了自己的文章被盜用了,新的平臺就移交賬號并看情況給予少許補償。如果對方不樂意,就注銷賬號,當一切都沒有發生過。憑借這種運營方式,順利實現了冷啟動。
短視頻APP的后來者,也可以通過類似的方式,實現用戶的積累和優質內容的聚合。
勝利即正義?
這似乎是過于武斷的一個評價。
上述的視頻APP做得太過分,引起公憤,最終不得不關閉自己的平臺。
對于通過爬蟲獲取內容的平臺而言,內容的獲取也只是萬里長征的第一步,通過運營手段減小生產內容的成本,通過利益共享激勵優質內容的產生,通過技術減小信息成本吸引用戶,更加任重而道遠。
而版權,也始終是懸于頭頂的達摩克利斯之劍。
本文由@linghu 原創發布于人人都是產品經理,未經許可,禁止轉載
題圖來自Unsplash, 基于CC0協議
一個用于提取簡體中文字符串中省,市和區并能夠進行映射,檢驗和簡單繪圖的python模塊。
舉個例子:
["徐匯區虹漕路461號58號樓5樓", "泉州市洛江區萬安塘西工業區"]
↓ 轉換
|省 |市 |區 |
|上海市|上海市|徐匯區|
|福建省|泉州市|洛江區|
如果你只是想快速實現以上類型的數據處理的話,那么只需要復制以下代碼,不需要過多閱讀本文檔的內容(復制代碼之前,先閱讀安裝說明將本模塊裝上):
location_str = ["徐匯區虹漕路461號58號樓5樓", "泉州市洛江區萬安塘西工業區", "朝陽區北苑華貿城"] #任意的可迭代類型,比如Series也可以
from chinese_province_city_area_mapper.transformer import CPCATransformer
from chinese_province_city_area_mapper import myumap
cpca = CPCATransformer(myumap.umap)
df = cpca.transform(location_str)
df
代碼目前僅僅支持python3
pip install chinese_province_city_area_mapper
本模塊中最主要的類是chinese_province_city_area_mapper.transformer.CPCATransformer(注:CPCA是Chinese Province City Area的縮寫),該類的transform方法可以輸入任意的可迭代類型(如list,Series等),然后將其轉換為一個DataFrame,示例代碼如下:
location_str = ["徐匯區虹漕路461號58號樓5樓", "泉州市洛江區萬安塘西工業區", "朝陽區北苑華貿城"]
from chinese_province_city_area_mapper.transformer import CPCATransformer
cpca = CPCATransformer()
df = cpca.transform(location_str)
df
輸出的結果為:
區 市 省
0 徐匯區 上海市 上海市
1 洛江區 泉州市 福建省
2 朝陽區
從上面的程序輸出中你會發現朝陽區并沒有被映射到北京市,這是因為在中國有多個同名的叫做朝陽區的區,并且他們位于不同的市,所以程序就不知道該映射到哪一個市了(舉個例子,南京市有一個鼓樓區,開封市有一個鼓樓區,福州市也有一個鼓樓區,這樣程序就不知道應該把鼓樓區映射到哪一個市了),因此就不對其進行映射,如果你確定你的數據中的朝陽區都是指北京市的那個朝陽區的話,可以在CPCATransformer的構造函數中傳一個字典(叫做umap參數,是user map的簡稱),指定朝陽區都要映射到北京市,注意只有區到市的這一級映射存在重名問題,中國的市的名稱都是唯一的,省的名稱也都是唯一的,示例代碼如下:
location_str = ["徐匯區虹漕路461號58號樓5樓", "泉州市洛江區萬安塘西工業區", "朝陽區北苑華貿城"]
from chinese_province_city_area_mapper.transformer import CPCATransformer
cpca = CPCATransformer({"朝陽區":"北京市"})
df = cpca.transform(location_str)
df
輸出結果為:
區 市 省
0 徐匯區 上海市 上海市
1 洛江區 泉州市 福建省
2 朝陽區 北京市 北京市
模塊中還內置了一個我推薦大家使用的umap,這個umap中我根據處理地址數據的經驗將那些重名的區映射到了它最常見的一個市,這個umap位于chinese_province_city_area_mapper.myumap.umap,使用如下:
location_str = ["徐匯區虹漕路461號58號樓5樓", "泉州市洛江區萬安塘西工業區", "朝陽區北苑華貿城"]
from chinese_province_city_area_mapper.transformer import CPCATransformer
from chinese_province_city_area_mapper import myumap
print(myumap.umap) #查看這個umap的內容
cpca = CPCATransformer(myumap.umap)
df = cpca.transform(location_str)
df
輸出和上一個程序一樣
模塊中還自帶一個簡單繪圖工具,可以在地圖上將上面輸出的數據以熱力圖的形式畫出來,代碼如下:
from chinese_province_city_area_mapper import drawers
#df為上一段代碼輸出的df
drawers.draw_locations(df, "df.html")
這一段代碼運行結束后會在運行代碼的當前目錄下生成一個df.html文件,用瀏覽器打開即可看到 繪制好的地圖(如果某條數據'省','市'或'區'字段有缺,則會忽略該條數據不進行繪制),速度會比較慢,需要耐心等待,繪制的圖像如下:
draw_locations函數還可以通過指定path參數來改變輸出路徑,示例代碼如下::
from chinese_province_city_area_mapper import drawers
#在當前目錄的父目錄生成df.html
drawers.draw_locations(df, "df.html", path="../")
到這里就你就已經知道了本模塊的基本使用了,接下來我會闡明更多細節。
本模塊自帶全國省市區的映射關系及其經緯度,如果你只是想使用這個數據的話可以使用如下代碼:
from chinese_province_city_area_mapper.infrastructure import SuperMap
#地區到市的映射數據庫,是一個字典類型(key為區名,value為其所屬的市名),注意其中包含重復的區名
SuperMap.area_city_mapper
#重復的區名列表,列表類型,如果區名在這個列表中,說明存在多個同名區,則area_city_mapper的映射是不準確的
SuperMap.rep_areas
#市到省的映射數據庫,字典類型(key為市的名稱,value為省的名稱)
SuperMap.city_province_mapper
#全國省市區的經緯度數據庫,字典類型(key為"省,市,區",value為(維度,經度))
SuperMap.lat_lon_mapper
#獲取北京市朝陽區的經緯度
SuperMap.lat_lon_mapper.get("北京市,北京市,朝陽區")
#獲得一個地名的級別(即省,市或者區)
SuperMap.getType("江蘇省") #返回"province",即常量SuperMap.PROVINCE
SuperMap.getType("南京市") #返回"city",即常量SuperMap.CITY
SuperMap.getType("海淀區") #返回"area",即常量SuperMap.AREA
#省略"省"字也能夠識別出來
SuperMap.getType("江蘇")
為了保證匹配與映射的正確性,我做了很多細節上的處理,如果在使用本模塊的過程中遇到困惑可以參考這里。
#測試數據
location_strs = ["江蘇省南京市鼓樓區256號", "江蘇南京鼓樓區256號"]
from chinese_province_city_area_mapper.transformer import CPCATransformer
cpca = CPCATransformer()
df = cpca.transform(location_strs)
df
輸出的結果為:
區 市 省
0 鼓樓區 南京市 江蘇省
1 鼓樓區 南京市 江蘇省
#測試數據,一些故意錯亂的地址描述
location_strs = ["靜安區南京西路30號", "南京市靜安區", "江蘇省上海市", "上海市靜安區南京西路"]
from chinese_province_city_area_mapper.transformer import CPCATransformer
cpca = CPCATransformer()
df = cpca.transform(location_strs)
df
輸出結果如下:
區 市 省
0 靜安區 上海市 上海市
1 南京市 江蘇省
2 江蘇省
3 靜安區 上海市 上海市
分析:第一個測試數據"靜安區南京西路"會同時匹配到"靜安區"和"南京"兩個地域名稱,但是靜安區是屬于上海的,和"南京"想矛盾,而且因為"南京"是"南京市"的縮寫,因此優先級比較低,故放棄"南京"這個地域名稱。
第二個測試數據匹配到"南京市"和"靜安區"兩個矛盾的地域名稱,而且這兩個名稱都是全稱,優先級相同,所以保留地域范圍比較大的,即保留"南京市"而放棄"靜安區"。第三個測試數據也是一樣的道理。
第四個測試數據中有兩個市的名稱會被匹配到,一個是"上海市",還有一個是"南京",但是因為"上海市"在前面被匹配到了,所以"南京"就會被忽略。
本倉庫放了一份大約一萬多條地址描述信息addr.csv,這是我當時測試與開發用的數據,目前的版本可以保證比較高的準確率,大家可以用這個數據繼續進行測試,測試代碼如下:
#讀取數據
import pandas as pd
origin = pd.read_csv("addr.csv")
#轉換
from chinese_province_city_area_mapper.transformer import CPCATransformer
from chinese_province_city_area_mapper import myumap
cpca = CPCATransformer(myumap.umap)
addr_df = cpca.transform(origin["原始地址"])
#輸出
processed = pd.concat([origin, addr_df], axis=1)
processed.to_csv("processed.csv", index=False, encoding="utf-8")
注意以上代碼會產生產生大量的warnning,這些warnning是因為程序無法確定某個區縣屬于哪個市(因為這些區縣存在重名問題而且在umap中又沒有指定它屬于哪一個市).
繪圖代碼:
from chinese_province_city_area_mapper import drawers
#processed為上一段代碼的processed
drawers.draw_locations(processed, "processed.html")
繪制的局部圖像如下:
(注意:本模塊在繪圖時,只繪制那些可以精確地匹配到省市區的地址,對于省市區有一個或多個字段缺失的則會直接忽略)
-----------------------------------以下為2.0版本新增的接口----------------------------------------------
通過pip list查看模塊版本,如果版本低于2.0,則應該使用如下的命令更新模塊:
pip install -U chinese_province_city_area_mapper
之前版本的繪圖接口是基于folium編寫的,但是在國內folium的地圖顯示速度太慢了,所以2.0版本在保留原本的folium繪圖接口的基礎上添加了echarts的繪圖接口.
第一個接口是echarts熱力圖繪制接口,代碼如下,仍然使用之前的測試數據生成的processed變量:
from chinese_province_city_area_mapper import drawers
drawers.echarts_draw(processed, "test.html")
該接口的更多參數及其含義如下:
def echarts_draw(locations, fileName, path="./", title="地域分布圖"
, subtitle="location distribute"):
"""
生成地域分布的echarts熱力圖的html文件.
:param locations: 樣本的省市區, pandas的dataframe類型.
:param fileName: 生成的html文件的文件名.
:param path: 生成的html文件的路徑.
:param title: 圖表的標題
:param subtitle: 圖表的子標題
"""
然后會在當前目錄下生成一個test.html文件,用瀏覽器打開后即可看到圖像:
第二個接口是樣本分類繪制接口,通過額外傳入一個樣本的分類信息,能夠在地圖上以不同的顏色畫出屬于不同分類的樣本散點圖,以下代碼以“省”作為類別信息繪制分類散點圖(可以看到,屬于不同省的樣本被以不同的顏色標記了出來,這里以“省”作為分類標準只是舉個例子,實際應用中可以選取更加有實際意義的分類指標):
from chinese_province_city_area_mapper import drawers
drawers.echarts_cate_draw(processed, processed["省"], "test2.html")
然后會在當前目錄下生成一個test2.html文件,用瀏覽器打開后即可看到圖像:
該接口更多的參數及其含義如下:
def echarts_cate_draw(locations, labels, fileName, path="./"
, title="地域分布圖", subtitle="location distribute",
point_size=7):
"""
依據分類生成地域分布的echarts散點圖的html文件.
:param locations: 樣本的省市區, pandas的dataframe類型.
:param labels: 長度必須和locations相等, 代表每個樣本所屬的分類.
:param fileName: 生成的html文件的文件名.
:param path: 生成的html文件的路徑.
:param title: 圖表的標題
:param subtitle: 圖表的子標題
:param point_size: 每個散點的大小,如果樣本數較少可以考慮設置的大一些
"""
給出的數據集考慮到了模塊給出的echarts繪圖接口都是基于本模塊自帶的經緯度數據集的,而不是pyecharts的數據集。如果想更加精細地控制pyecharts繪圖參數的話可以直接把本倉庫的drawers模塊的源碼復制過去修改。
項目地址:https://github.com/DQinYuan/chinese_province_city_area_mapper.git
喜歡的可以自己下載使用。
用過老版本UC看小說的同學都知道,當年版權問題比較松懈,我們可以再UC搜索不同來源的小說,并且閱讀,那么它是怎么做的呢?下面讓我們自己實現一個小說線上采集閱讀。(說明:僅用于技術學習、研究)
看小說時,最煩的就是有各種廣告,這些廣告有些是站長放上去的盈利手段,有些是被人惡意注入。在我的上一篇博客中實現了小說采集并保存到本地TXT文件 HttpClients+Jsoup抓取筆趣閣小說,并保存到本地TXT文件,這樣我們就可以導入手機用手機閱讀軟件看小說;那么我們這里實現一個可以在線看小說。
首頁:
頁面很純凈,目前有三種來源
搜索結果頁:
三個不同的來源,分頁用的是layui的laypage,邏輯分頁。(筆趣閣的搜索結果界面沒有書本的圖片)
翻頁效果:
縱橫網連簡介等都幫我們分詞,搞得數據量太大,速度太慢:books.size() < 888
書本詳情頁:
小說閱讀頁:
上、下一章:
項目是springboot項目,原理非常簡單,就是用httpclient構造一個請求頭去請求對應的來源鏈接,用jsoup去解析響應回來的response,
通過jsoup的選擇器去找到我們想要的數據,存入實體,放到ModelAndView里面,前端頁面用thymeleaf去取值、遍歷數據。
但是有一些書是要會員才能看,這種情況下我們需要做模擬登陸才能繼續采集,這里只是一個簡單的采集,就不做模擬登陸了。
采集過程中碰到的問題:
1、起點中文網采集書本集合時,想要的數據不在頁面源碼里面
起點中文網很機智,他在html代碼了沒有直接展示page分頁信息的鏈接
可以看到,httpClient請求回來的response里分頁信息標簽里面是空的,但用瀏覽器去請求里面有信息
這是因為httpClient去模擬我們的瀏覽器訪問某個鏈接,直接響應回這個鏈接對應的內容,并不會去幫我們觸發其他的ajax,而瀏覽器回去解析響應回來的html,當碰到img、script、link等標簽它會幫我們去ajax請求對應的資源。
由此推測,page相關的信息,起點中文網是在js代碼里面去獲取并追加,最后通過network找到它的一些蛛絲馬跡
既然他沒有寫在html里,那我們就自己去創建連接,可以看到html上有當前頁跟最大頁數
完美
2、筆趣閣查看書本詳情,圖片防盜鏈
筆趣閣有一個圖片防盜,我們在自己的html引入圖片路徑時,但當我們把鏈接用瀏覽器訪問時是可以的
對比一下兩邊的請求頭
首先我們要知道什么事圖片防盜鏈,猛戳這里 -->:圖片防盜鏈原理及應對方法 ;我們直接用大佬的反防盜鏈方法,并且針對我們的項目改造一下:
<div id="bookImg"></div>
/**
* 反防盜鏈
*/
function showImg(parentObj, url) {
//來一個隨機數
var frameid = 'frameimg' + Math.random();
//放在(父頁面)window里面 iframe的script標簽里面綁定了window.onload,作用:設置iframe的高度、寬度 <script>window.onload = function() { parent.document.getElementById(\'' + frameid + '\').height = document.getElementById(\'img\').height+\'px\'; }<' + '/script>
window.img = '<img src=\'' + url + '?' + Math.random() + '\'/>';
//iframe調用parent.img
$(parentObj).append('<iframe id="' + frameid + '" src="javascript:parent.img;" frameBorder="0" scrolling="no"></iframe>');
}
showImg($("#bookImg"), book.img);
效果最終:
3、采集書本詳情時,起點網的目錄并沒有在html里
起點網的目錄并沒有在html里,也不是在另一個鏈接里
通過瀏覽器頁面Elements的Break on打斷點
查看調用棧發現,它在js ajax請求數據,進行tab切換,就連總共有多少章,它都是頁面加載出來之后ajax請求回來的
看一下他的請求頭跟參數
只要我們弄懂_csrfToken參數就可以構造一個get請求
https://book.qidian.com/ajax/book/category?_csrfToken=LosgUIe29G7LV04gdutbSqzKRb9XxoPyqtWBQ3hU&bookId=1209977
通過瀏覽器查看可知,第一章對應的鏈接:https://read.qidian.com/chapter/2R9G_ziBVg41/MyEcwtk5i8Iex0RJOkJclQ2
這個就是我們想要的
https://read.qidian.com/chapter/ + cU章節鏈接
cN章節名稱
_csrfToken是cookie,而且多次刷新都不變,大膽猜測:起點為我們生成cookie并且攜帶請求ajax,攜帶與起點給我們的cookie不一致的時候返回失敗,
我們每次調用gather,都是一次新的httpclient對象,每次既然如此,那我們就先獲取cookie,在用同一個httpclient去請求數據即可 (詳情代碼已經貼出來,在BookHandler_qidian.book_details_qidian里面)
最終我們獲得了返回值,是一個json
同樣的,大部分邏輯都寫在注釋里面,相信大家都看得懂:
maven引包:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.9</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
書實體類:
/**
* 書對象
*/
@Data
public class Book {
/**
* 鏈接
*/
private String bookUrl;
/**
* 書名
*/
private String bookName;
/**
* 作者
*/
private String author;
/**
* 簡介
*/
private String synopsis;
/**
* 圖片
*/
private String img;
/**
* 章節目錄 chapterName、url
*/
private List<Map<String,String>> chapters;
/**
* 狀態
*/
private String status;
/**
* 類型
*/
private String type;
/**
* 更新時間
*/
private String updateDate;
/**
* 第一章
*/
private String firstChapter;
/**
* 第一章鏈接
*/
private String firstChapterUrl;
/**
* 上一章節
*/
private String prevChapter;
/**
* 上一章節鏈接
*/
private String prevChapterUrl;
/**
* 當前章節名稱
*/
private String nowChapter;
/**
* 當前章節內容
*/
private String nowChapterValue;
/**
* 當前章節鏈接
*/
private String nowChapterUrl;
/**
* 下一章節
*/
private String nextChapter;
/**
* 下一章節鏈接
*/
private String nextChapterUrl;
/**
* 最新章節
*/
private String latestChapter;
/**
* 最新章節鏈接
*/
private String latestChapterUrl;
/**
* 大小
*/
private String magnitude;
/**
* 來源
*/
private Map<String,String> source;
private String sourceKey;
}
小工具類:
/**
* 小工具類
*/
public class BookUtil {
/**
* 自動注入參數
* 例如:
*
* @param src http://search.zongheng.com/s?keyword=#1&pageNo=#2&sort=
* @param params "斗破蒼穹","1"
* @return http://search.zongheng.com/s?keyword=斗破蒼穹&pageNo=1&sort=
*/
public static String insertParams(String src, String... params) {
int i = 1;
for (String param : params) {
src = src.replaceAll("#" + i, param);
i++;
}
return src;
}
/**
* 采集當前url完整response實體.toString()
*
* @param url url
* @return response實體.toString()
*/
public static String gather(String url, String refererUrl) {
String result = null;
try {
//創建httpclient對象 (這里設置成全局變量,相對于同一個請求session、cookie會跟著攜帶過去)
CloseableHttpClient httpClient = HttpClients.createDefault();
//創建get方式請求對象
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Content-type", "application/json");
//包裝一下
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
httpGet.addHeader("Referer", refererUrl);
httpGet.addHeader("Connection", "keep-alive");
//通過請求對象獲取響應對象
CloseableHttpResponse response = httpClient.execute(httpGet);
//獲取結果實體
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
result = EntityUtils.toString(response.getEntity(), "GBK");
}
//釋放鏈接
response.close();
}
//這里還可以捕獲超時異常,重新連接抓取
catch (Exception e) {
result = null;
System.err.println("采集操作出錯");
e.printStackTrace();
}
return result;
}
}
Controller層:
/**
* Book Controller層
*/
@RestController
@RequestMapping("book")
public class BookContrller {
/**
* 來源集合
*/
private static Map<String, Map<String, String>> source = new HashMap<>();
static {
//筆趣閣
source.put("biquge", BookHandler_biquge.biquge);
//縱橫中文網
source.put("zongheng", BookHandler_zongheng.zongheng);
//起點中文網
source.put("qidian", BookHandler_qidian.qidian);
}
/**
* 訪問首頁
*/
@GetMapping("/index")
public ModelAndView index() {
return new ModelAndView("book_index.html");
}
/**
* 搜索書名
*/
@GetMapping("/search")
public ModelAndView search(Book book) {
//結果集
ArrayList<Book> books = new ArrayList<>();
//關鍵字
String keyWord = book.getBookName();
//來源
String sourceKey = book.getSourceKey();
//獲取來源詳情
Map<String, String> src = source.get(sourceKey);
// 編碼
try {
keyWord = URLEncoder.encode(keyWord, src.get("UrlEncode"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//searchUrl
src.put("searchUrl", BookUtil.insertParams(src.get("searchUrl"), keyWord, "1"));
//調用不同的方法
switch (sourceKey) {
case "biquge":
BookHandler_biquge.book_search_biquge(books, src, keyWord);
break;
case "zongheng":
BookHandler_zongheng.book_search_zongheng(books, src, keyWord);
break;
case "qidian":
BookHandler_qidian.book_search_qidian(books, src, keyWord);
break;
default:
//默認所有都查
BookHandler_biquge.book_search_biquge(books, src, keyWord);
BookHandler_zongheng.book_search_zongheng(books, src, keyWord);
BookHandler_qidian.book_search_qidian(books, src, keyWord);
break;
}
System.out.println(books.size());
ModelAndView modelAndView = new ModelAndView("book_list.html", "books", books);
try {
modelAndView.addObject("keyWord", URLDecoder.decode(keyWord, src.get("UrlEncode")));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
modelAndView.addObject("sourceKey", sourceKey);
return modelAndView;
}
/**
* 訪問書本詳情
*/
@GetMapping("/details")
public ModelAndView details(String sourceKey,String bookUrl,String searchUrl) {
Map<String, String> src = source.get(sourceKey);
src.put("searchUrl",searchUrl);
Book book = new Book();
//調用不同的方法
switch (sourceKey) {
case "biquge":
book = BookHandler_biquge.book_details_biquge(src, bookUrl);
break;
case "zongheng":
book = BookHandler_zongheng.book_details_zongheng(src, bookUrl);
break;
case "qidian":
book = BookHandler_qidian.book_details_qidian(src, bookUrl);
break;
default:
break;
}
return new ModelAndView("book_details.html", "book", book);
}
/**
* 訪問書本章節
*/
@GetMapping("/read")
public ModelAndView read(String sourceKey,String chapterUrl,String refererUrl) {
Map<String, String> src = source.get(sourceKey);
Book book = new Book();
//調用不同的方法
switch (sourceKey) {
case "biquge":
book = BookHandler_biquge.book_read_biquge(src, chapterUrl,refererUrl);
break;
case "zongheng":
book = BookHandler_zongheng.book_read_zongheng(src, chapterUrl,refererUrl);
break;
case "qidian":
book = BookHandler_qidian.book_read_qidian(src, chapterUrl,refererUrl);
break;
default:
break;
}
return new ModelAndView("book_read.html", "book", book);
}
}
三個不同來源的Handler處理器,每個來源都有不同的采集規則:
BookHandler_biquge
/**
* 筆趣閣采集規則
*/
public class BookHandler_biquge {
/**
* 來源信息
*/
public static HashMap<String, String> biquge = new HashMap<>();
static {
//筆趣閣
biquge.put("name", "筆趣閣");
biquge.put("key", "biquge");
biquge.put("baseUrl", "http://www.biquge.com.tw");
biquge.put("baseSearchUrl", "http://www.biquge.com.tw/modules/article/soshu.php");
biquge.put("UrlEncode", "GB2312");
biquge.put("searchUrl", "http://www.biquge.com.tw/modules/article/soshu.php?searchkey=+#1&page=#2");
}
/**
* 獲取search list 筆趣閣采集規則
*
* @param books 結果集合
* @param src 源目標
* @param keyWord 關鍵字
*/
public static void book_search_biquge(ArrayList<Book> books, Map<String, String> src, String keyWord) {
//采集術
String html = BookUtil.gather(src.get("searchUrl"), src.get("baseUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//當前頁集合
Elements resultList = doc.select("table.grid tr#nr");
for (Element result : resultList) {
Book book = new Book();
//書本鏈接
book.setBookUrl(result.child(0).select("a").attr("href"));
//書名
book.setBookName(result.child(0).select("a").text());
//作者
book.setAuthor(result.child(2).text());
//更新時間
book.setUpdateDate(result.child(4).text());
//最新章節
book.setLatestChapter(result.child(1).select("a").text());
book.setLatestChapterUrl(result.child(1).select("a").attr("href"));
//狀態
book.setStatus(result.child(5).text());
//大小
book.setMagnitude(result.child(3).text());
//來源
book.setSource(src);
books.add(book);
}
//下一頁
Elements searchNext = doc.select("div.pages > a.ngroup");
String href = searchNext.attr("href");
if (!StringUtils.isEmpty(href)) {
src.put("baseUrl", src.get("searchUrl"));
src.put("searchUrl", href.contains("http") ? href : (src.get("baseSearchUrl") + href));
book_search_biquge(books, src, keyWord);
}
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
}
/**
* 獲取書本詳情 筆趣閣采集規則
* @param src 源目標
* @param bookUrl 書本鏈接
* @return Book對象
*/
public static Book book_details_biquge(Map<String, String> src, String bookUrl) {
Book book = new Book();
//采集術
String html = BookUtil.gather(bookUrl, src.get("searchUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//書本鏈接
book.setBookUrl(doc.select("meta[property=og:url]").attr("content"));
//圖片
book.setImg(doc.select("meta[property=og:image]").attr("content"));
//書名
book.setBookName(doc.select("div#info > h1").text());
//作者
book.setAuthor(doc.select("meta[property=og:novel:author]").attr("content"));
//更新時間
book.setUpdateDate(doc.select("meta[property=og:novel:update_time]").attr("content"));
//最新章節
book.setLatestChapter(doc.select("meta[property=og:novel:latest_chapter_name]").attr("content"));
book.setLatestChapterUrl(doc.select("meta[property=og:novel:latest_chapter_url]").attr("content"));
//類型
book.setType(doc.select("meta[property=og:novel:category]").attr("content"));
//簡介
book.setSynopsis(doc.select("meta[property=og:description]").attr("content"));
//狀態
book.setStatus(doc.select("meta[property=og:novel:status]").attr("content"));
//章節目錄
ArrayList<Map<String, String>> chapters = new ArrayList<>();
for (Element result : doc.select("div#list dd")) {
HashMap<String, String> map = new HashMap<>();
map.put("chapterName", result.select("a").text());
map.put("url", result.select("a").attr("href"));
chapters.add(map);
}
book.setChapters(chapters);
//來源
book.setSource(src);
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
/**
* 得到當前章節名以及完整內容跟上、下一章的鏈接地址 筆趣閣采集規則
* @param src 源目標
* @param chapterUrl 當前章節鏈接
* @param refererUrl 來源鏈接
* @return Book對象
*/
public static Book book_read_biquge(Map<String, String> src,String chapterUrl,String refererUrl) {
Book book = new Book();
//當前章節鏈接
book.setNowChapterUrl(chapterUrl.contains("http") ? chapterUrl : (src.get("baseUrl") + chapterUrl));
//采集術
String html = BookUtil.gather(book.getNowChapterUrl(), refererUrl);
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//當前章節名稱
book.setNowChapter(doc.select("div.box_con > div.bookname > h1").text());
//刪除圖片廣告
doc.select("div.box_con > div#content img").remove();
//當前章節內容
book.setNowChapterValue(doc.select("div.box_con > div#content").outerHtml());
//上、下一章
book.setPrevChapter(doc.select("div.bottem2 a:matches((?i)下一章)").text());
book.setPrevChapterUrl(doc.select("div.bottem2 a:matches((?i)下一章)").attr("href"));
book.setNextChapter(doc.select("div.bottem2 a:matches((?i)上一章)").text());
book.setNextChapterUrl(doc.select("div.bottem2 a:matches((?i)上一章)").attr("href"));
//來源
book.setSource(src);
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
}
BookHandler_zongheng
/**
* 縱橫中文網采集規則
*/
public class BookHandler_zongheng {
/**
* 來源信息
*/
public static HashMap<String, String> zongheng = new HashMap<>();
static {
//縱橫中文網
zongheng.put("name", "縱橫中文網");
zongheng.put("key", "zongheng");
zongheng.put("baseUrl", "http://www.zongheng.com");
zongheng.put("baseSearchUrl", "http://search.zongheng.com/s");
zongheng.put("UrlEncode", "UTF-8");
zongheng.put("searchUrl", "http://search.zongheng.com/s?keyword=#1&pageNo=#2&sort=");
}
/**
* 獲取search list 縱橫中文網采集規則
*
* @param books 結果集合
* @param src 源目標
* @param keyWord 關鍵字
*/
public static void book_search_zongheng(ArrayList<Book> books, Map<String, String> src, String keyWord) {
//采集術
String html = BookUtil.gather(src.get("searchUrl"), src.get("baseUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//當前頁集合
Elements resultList = doc.select("div.search-tab > div.search-result-list");
for (Element result : resultList) {
Book book = new Book();
//書本鏈接
book.setBookUrl(result.select("div.imgbox a").attr("href"));
//圖片
book.setImg(result.select("div.imgbox img").attr("src"));
//書名
book.setBookName(result.select("h2.tit").text());
//作者
book.setAuthor(result.select("div.bookinfo > a").first().text());
//類型
book.setType(result.select("div.bookinfo > a").last().text());
//簡介
book.setSynopsis(result.select("p").text());
//狀態
book.setStatus(result.select("div.bookinfo > span").first().text());
//大小
book.setMagnitude(result.select("div.bookinfo > span").last().text());
//來源
book.setSource(src);
books.add(book);
}
//下一頁
Elements searchNext = doc.select("div.search_d_pagesize > a.search_d_next");
String href = searchNext.attr("href");
//最多只要888本,不然太慢了
if (books.size() < 888 && !StringUtils.isEmpty(href)) {
src.put("baseUrl", src.get("searchUrl"));
src.put("searchUrl", href.contains("http") ? href : (src.get("baseSearchUrl") + href));
book_search_zongheng(books, src, keyWord);
}
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
}
/**
* 獲取書本詳情 縱橫中文網采集規則
* @param src 源目標
* @param bookUrl 書本鏈接
* @return Book對象
*/
public static Book book_details_zongheng(Map<String, String> src, String bookUrl) {
Book book = new Book();
//采集術
String html = BookUtil.gather(bookUrl, src.get("searchUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//書本鏈接
book.setBookUrl(bookUrl);
//圖片
book.setImg(doc.select("div.book-img > img").attr("src"));
//書名
book.setBookName(doc.select("div.book-info > div.book-name").text());
//作者
book.setAuthor(doc.select("div.book-author div.au-name").text());
//更新時間
book.setUpdateDate(doc.select("div.book-new-chapter div.time").text());
//最新章節
book.setLatestChapter(doc.select("div.book-new-chapter div.tit a").text());
book.setLatestChapterUrl(doc.select("div.book-new-chapter div.tit a").attr("href"));
//類型
book.setType(doc.select("div.book-label > a").last().text());
//簡介
book.setSynopsis(doc.select("div.book-dec > p").text());
//狀態
book.setStatus(doc.select("div.book-label > a").first().text());
//章節目錄
String chaptersUrl = doc.select("a.all-catalog").attr("href");
ArrayList<Map<String, String>> chapters = new ArrayList<>();
//采集術
for (Element result : Jsoup.parse(BookUtil.gather(chaptersUrl, bookUrl)).select("ul.chapter-list li")) {
HashMap<String, String> map = new HashMap<>();
map.put("chapterName", result.select("a").text());
map.put("url", result.select("a").attr("href"));
chapters.add(map);
}
book.setChapters(chapters);
//來源
book.setSource(src);
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
/**
* 得到當前章節名以及完整內容跟上、下一章的鏈接地址 縱橫中文網采集規則
* @param src 源目標
* @param chapterUrl 當前章節鏈接
* @param refererUrl 來源鏈接
* @return Book對象
*/
public static Book book_read_zongheng(Map<String, String> src,String chapterUrl,String refererUrl) {
Book book = new Book();
//當前章節鏈接
book.setNowChapterUrl(chapterUrl.contains("http") ? chapterUrl : (src.get("baseUrl") + chapterUrl));
//采集術
String html = BookUtil.gather(book.getNowChapterUrl(), refererUrl);
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//當前章節名稱
book.setNowChapter(doc.select("div.title_txtbox").text());
//刪除圖片廣告
doc.select("div.content img").remove();
//當前章節內容
book.setNowChapterValue(doc.select("div.content").outerHtml());
//上、下一章
book.setPrevChapter(doc.select("div.chap_btnbox a:matches((?i)下一章)").text());
book.setPrevChapterUrl(doc.select("div.chap_btnbox a:matches((?i)下一章)").attr("href"));
book.setNextChapter(doc.select("div.chap_btnbox a:matches((?i)上一章)").text());
book.setNextChapterUrl(doc.select("div.chap_btnbox a:matches((?i)上一章)").attr("href"));
//來源
book.setSource(src);
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
}
BookHandler_qidian
/**
* 起點中文網采集規則
*/
public class BookHandler_qidian {
/**
* 來源信息
*/
public static HashMap<String, String> qidian = new HashMap<>();
static {
//起點中文網
qidian.put("name", "起點中文網");
qidian.put("key", "qidian");
qidian.put("baseUrl", "http://www.qidian.com");
qidian.put("baseSearchUrl", "https://www.qidian.com/search");
qidian.put("UrlEncode", "UTF-8");
qidian.put("searchUrl", "https://www.qidian.com/search?kw=#1&page=#2");
}
/**
* 獲取search list 起點中文網采集規則
*
* @param books 結果集合
* @param src 源目標
* @param keyWord 關鍵字
*/
public static void book_search_qidian(ArrayList<Book> books, Map<String, String> src, String keyWord) {
//采集術
String html = BookUtil.gather(src.get("searchUrl"), src.get("baseUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//當前頁集合
Elements resultList = doc.select("li.res-book-item");
for (Element result : resultList) {
Book book = new Book();
/*
如果大家打斷點在這里的話就會發現,起點的鏈接是這樣的
//book.qidian.com/info/1012786368
以兩個斜杠開頭,不過無所謂,httpClient照樣可以請求
*/
//書本鏈接
book.setBookUrl(result.select("div.book-img-box a").attr("href"));
//圖片
book.setImg(result.select("div.book-img-box img").attr("src"));
//書名
book.setBookName(result.select("div.book-mid-info > h4").text());
//作者
book.setAuthor(result.select("div.book-mid-info > p.author > a").first().text());
//類型
book.setType(result.select("div.book-mid-info > p.author > a").last().text());
//簡介
book.setSynopsis(result.select("div.book-mid-info > p.intro").text());
//狀態
book.setStatus(result.select("div.book-mid-info > p.author > span").first().text());
//更新時間
book.setUpdateDate(result.select("div.book-mid-info > p.update > span").text());
//最新章節
book.setLatestChapter(result.select("div.book-mid-info > p.update > a").text());
book.setLatestChapterUrl(result.select("div.book-mid-info > p.update > a").attr("href"));
//來源
book.setSource(src);
books.add(book);
}
//當前頁
String page = doc.select("div#page-container").attr("data-page");
//最大頁數
String pageMax = doc.select("div#page-container").attr("data-pageMax");
//當前頁 < 最大頁數
if (Integer.valueOf(page) < Integer.valueOf(pageMax)) {
src.put("baseUrl", src.get("searchUrl"));
//自己拼接下一頁鏈接
src.put("searchUrl", src.get("searchUrl").replaceAll("page=" + Integer.valueOf(page), "page=" + (Integer.valueOf(page) + 1)));
book_search_qidian(books, src, keyWord);
}
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
}
/**
* 獲取書本詳情 起點中文網采集規則
* @param src 源目標
* @param bookUrl 書本鏈接
* @return Book對象
*/
public static Book book_details_qidian(Map<String, String> src, String bookUrl) {
Book book = new Book();
//https
bookUrl = "https:" + bookUrl;
//采集術
String html = BookUtil.gather(bookUrl, src.get("searchUrl"));
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
//書本鏈接
book.setBookUrl(bookUrl);
//圖片
String img = doc.select("div.book-img > a#bookImg > img").attr("src");
img = "https:" + img;
book.setImg(img);
//書名
book.setBookName(doc.select("div.book-info > h1 > em").text());
//作者
book.setAuthor(doc.select("div.book-info > h1 a.writer").text());
//更新時間
book.setUpdateDate(doc.select("li.update em.time").text());
//最新章節
book.setLatestChapter(doc.select("li.update a").text());
book.setLatestChapterUrl(doc.select("li.update a").attr("href"));
//類型
book.setType(doc.select("p.tag > span").first().text());
//簡介
book.setSynopsis(doc.select("div.book-intro > p").text());
//狀態
book.setStatus(doc.select("p.tag > a").first().text());
//章節目錄
//創建httpclient對象 (這里設置成全局變量,相對于同一個請求session、cookie會跟著攜帶過去)
BasicCookieStore cookieStore = new BasicCookieStore();
CloseableHttpClient httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
//創建get方式請求對象
HttpGet httpGet = new HttpGet("https://book.qidian.com/");
httpGet.addHeader("Content-type", "application/json");
//包裝一下
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
httpGet.addHeader("Connection", "keep-alive");
//通過請求對象獲取響應對象
CloseableHttpResponse response = httpClient.execute(httpGet);
//獲得Cookies
String _csrfToken = "";
List<Cookie> cookies = cookieStore.getCookies();
for (int i = 0; i < cookies.size(); i++) {
if("_csrfToken".equals(cookies.get(i).getName())){
_csrfToken = cookies.get(i).getValue();
}
}
//構造post
String bookId = doc.select("div.book-img a#bookImg").attr("data-bid");
HttpPost httpPost = new HttpPost(BookUtil.insertParams("https://book.qidian.com/ajax/book/category?_csrfToken=#1&bookId=#2",_csrfToken,bookId));
httpPost.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
httpPost.addHeader("Connection", "keep-alive");
//通過請求對象獲取響應對象
CloseableHttpResponse response1 = httpClient.execute(httpPost);
//獲取結果實體(json格式字符串)
String chaptersJson = "";
if (response1.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
chaptersJson = EntityUtils.toString(response1.getEntity(), "UTF-8");
}
//java處理json
ArrayList<Map<String, String>> chapters = new ArrayList<>();
JSONObject jsonArray = JSONObject.fromObject(chaptersJson);
Map<String,Object> objectMap = (Map<String, Object>) jsonArray;
Map<String, Object> objectMap_data = (Map<String, Object>) objectMap.get("data");
List<Map<String, Object>> objectMap_data_vs = (List<Map<String, Object>>) objectMap_data.get("vs");
for(Map<String, Object> vs : objectMap_data_vs){
List<Map<String, Object>> cs = (List<Map<String, Object>>) vs.get("cs");
for(Map<String, Object> chapter : cs){
Map<String, String> map = new HashMap<>();
map.put("chapterName", (String) chapter.get("cN"));
map.put("url", "https://read.qidian.com/chapter/"+(String) chapter.get("cU"));
chapters.add(map);
}
}
book.setChapters(chapters);
//來源
book.setSource(src);
//釋放鏈接
response.close();
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
/**
* 得到當前章節名以及完整內容跟上、下一章的鏈接地址 起點中文網采集規則
* @param src 源目標
* @param chapterUrl 當前章節鏈接
* @param refererUrl 來源鏈接
* @return Book對象
*/
public static Book book_read_qidian(Map<String, String> src,String chapterUrl,String refererUrl) {
Book book = new Book();
//當前章節鏈接
book.setNowChapterUrl(chapterUrl.contains("http") ? chapterUrl : (src.get("baseUrl") + chapterUrl));
//采集術
String html = BookUtil.gather(book.getNowChapterUrl(), refererUrl);
try {
//解析html格式的字符串成一個Document
Document doc = Jsoup.parse(html);
System.out.println(html);
//當前章節名稱
book.setNowChapter(doc.select("h3.j_chapterName").text());
//刪除圖片廣告
doc.select("div.read-content img").remove();
//當前章節內容
book.setNowChapterValue(doc.select("div.read-content").outerHtml());
//上、下一章
book.setPrevChapter(doc.select("div.chapter-control a:matches((?i)下一章)").text());
String prev = doc.select("div.chapter-control a:matches((?i)下一章)").attr("href");
prev = "https:"+prev;
book.setPrevChapterUrl(prev);
book.setNextChapter(doc.select("div.chapter-control a:matches((?i)上一章)").text());
String next = doc.select("div.chapter-control a:matches((?i)上一章)").attr("href");
next = "https:"+next;
book.setNextChapterUrl(next);
//來源
book.setSource(src);
} catch (Exception e) {
System.err.println("采集數據操作出錯");
e.printStackTrace();
}
return book;
}
}
四個html頁面:
book_index
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>MY BOOK</title>
<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
<style>
body{
background-color: antiquewhite;
}
.main{
margin: auto;
width: 500px;
margin-top: 150px;
}
#bookName{
width: 300px;
}
#title{
text-align: center;
}
</style>
</head>
<body>
<div class="main">
<h2 id="title">MY BOOK</h2>
<form class="form-inline" method="get" th:action="@{/book/search}">
來源
<select class="form-control" id="source" name="sourceKey">
<option value="">所有</option>
<option value="biquge">筆趣閣</option>
<option value="zongheng">縱橫網</option>
<option value="qidian">起點網</option>
</select>
<input type="text" id="bookName" name="bookName" class="form-control" placeholder="請輸入..."/>
<button class="btn btn-info" type="submit">搜索</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>BOOK LIST</title>
<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="http://hanlei.online/Onlineaddress/layui/css/layui.css"/>
<style>
body {
background-color: antiquewhite;
}
.main {
margin: auto;
width: 500px;
margin-top: 50px;
}
.book {
border-bottom: solid #428bca 1px;
}
.click-book-detail, .click-book-read {
cursor: pointer;
color: #428bca;
}
.click-book-detail:hover {
color: rgba(150, 149, 162, 0.47);
}
.click-book-read:hover {
color: rgba(150, 149, 162, 0.47);
}
</style>
</head>
<body>
<div class="main">
<form class="form-inline" method="get" th:action="@{/book/search}">
來源
<select class="form-control" id="source" name="sourceKey">
<option value="">所有</option>
<option value="biquge" th:selected="${sourceKey} == 'biquge'">筆趣閣</option>
<option value="zongheng" th:selected="${sourceKey} == 'zongheng'">縱橫網</option>
<option value="qidian" th:selected="${sourceKey} == 'qidian'">起點網</option>
</select>
<input type="text" id="bookName" name="bookName" class="form-control" placeholder="請輸入..."
th:value="${keyWord}"/>
<button class="btn btn-info" type="submit">搜索</button>
</form>
<br/>
<div id="books"></div>
<div id="page"></div>
</div>
</body>
<!-- jquery在線版本 -->
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script src="http://hanlei.online/Onlineaddress/layui/layui.js"></script>
<script th:inline="javascript">
var ctx = /*[[@{/}]]*/'';
var books = [[${books}]];//取出后臺數據
var nums = 10; //每頁出現的數量
var pages = books.length; //總數
/**
* 傳入當前頁,根據nums去計算,從books集合截取對應數據做展示
*/
var thisDate = function (curr) {
var str = "",//當前頁需要展示的html
first = (curr * nums - nums),//展示的第一條數據的下標
last = curr * nums - 1;//展示的最后一條數據的下標
last = last >= books.length ? (books.length - 1) : last;
for (var i = first; i <= last; i++) {
var book = books[i];
str += "<div class='book'>" +
"<img class='click-book-detail' data-bookurl='" + book.bookUrl + "' data-sourcekey='" + book.source.key + "' data-searchurl='" + book.source.searchUrl + "' src='" + book.img + "'></img>" +
"<p class='click-book-detail' data-bookurl='" + book.bookUrl + "' data-sourcekey='" + book.source.key + "' data-searchurl='" + book.source.searchUrl + "'>書名:" + book.bookName + "</p>" +
"<p>作者:" + book.author + "</p>" +
"<p>簡介:" + book.synopsis + "</p>" +
"<p class='click-book-read' data-chapterurl='" + book.latestChapterUrl + "' data-sourcekey='" + book.source.key + "' data-refererurl='" + book.source.refererurl + "'>最新章節:" + book.latestChapter + "</p>" +
"<p>更新時間:" + book.updateDate + "</p>" +
"<p>大小:" + book.magnitude + "</p>" +
"<p>狀態:" + book.status + "</p>" +
"<p>類型:" + book.type + "</p>" +
"<p>來源:" + book.source.name + "</p>" +
"</div><br/>";
}
return str;
};
//獲取一個laypage實例
layui.use('laypage', function () {
var laypage = layui.laypage;
//調用laypage 邏輯分頁
laypage.render({
elem: 'page',
count: pages,
limit: nums,
jump: function (obj) {
//obj包含了當前分頁的所有參數,比如:
// console.log(obj.curr); //得到當前頁,以便向服務端請求對應頁的數據。
// console.log(obj.limit); //得到每頁顯示的條數
document.getElementById('books').innerHTML = thisDate(obj.curr);
},
prev: '<',
next: '>',
theme: '#f9c357',
})
});
$("body").on("click", ".click-book-detail", function (even) {
var bookUrl = $(this).data("bookurl");
var searchUrl = $(this).data("searchurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/details?sourceKey=" + sourceKey + "&searchUrl=" + searchUrl + "&bookUrl=" + bookUrl;
});
$("body").on("click", ".click-book-read", function (even) {
var chapterUrl = $(this).data("chapterurl");
var refererUrl = $(this).data("refererurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/read?sourceKey=" + sourceKey + "&refererUrl=" + refererUrl + "&chapterUrl=" + chapterUrl;
});
</script>
</html>
book_details
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>BOOK DETAILS</title>
<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="http://hanlei.online/Onlineaddress/layui/css/layui.css"/>
<style>
body {
background-color: antiquewhite;
}
.main {
margin: auto;
width: 500px;
margin-top: 150px;
}
.book {
border-bottom: solid #428bca 1px;
}
.click-book-detail, .click-book-read {
cursor: pointer;
color: #428bca;
}
.click-book-detail:hover {
color: rgba(150, 149, 162, 0.47);
}
.click-book-read:hover {
color: rgba(150, 149, 162, 0.47);
}
a {
color: #428bca;
}
</style>
</head>
<body>
<div class="main">
<div class='book'>
<div id="bookImg"></div>
<p>書名:<span th:text="${book.bookName}"></span></p>
<p>作者:<span th:text="${book.author}"></span></p>
<p>簡介:<span th:text="${book.synopsis}"></span></p>
<p>最新章節:<a th:href="${book.latestChapterUrl}" th:text="${book.latestChapter}"></a></p>
<p>更新時間:<span th:text="${book.updateDate}"></span></p>
<p>大小:<span th:text="${book.magnitude}"></span></p>
<p>狀態:<span th:text="${book.status}"></span></p>
<p>類型:<span th:text="${book.type}"></span></p>
<p>來源:<span th:text="${book.source.name}"></span></p>
</div>
<br/>
<div class="chapters" th:each="chapter,iterStat:${book.chapters}">
<p class="click-book-read" th:attr="data-chapterurl=${chapter.url},data-sourcekey=${book.source.key},data-refererurl=${book.bookUrl}" th:text="${chapter.chapterName}"></p>
</div>
</div>
</body>
<!-- jquery在線版本 -->
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script th:inline="javascript">
var ctx = /*[[@{/}]]*/'';
var book = [[${book}]];//取出后臺數據
/**
* 反防盜鏈
*/
function showImg(parentObj, url) {
//來一個隨機數
var frameid = 'frameimg' + Math.random();
//放在(父頁面)window里面 iframe的script標簽里面綁定了window.onload,作用:設置iframe的高度、寬度 <script>window.onload = function() { parent.document.getElementById(\'' + frameid + '\').height = document.getElementById(\'img\').height+\'px\'; }<' + '/script>
window.img = '<img src=\'' + url + '?' + Math.random() + '\'/>';
//iframe調用parent.img
$(parentObj).append('<iframe id="' + frameid + '" src="javascript:parent.img;" frameBorder="0" scrolling="no"></iframe>');
}
showImg($("#bookImg"), book.img);
$("body").on("click", ".click-book-read", function (even) {
var chapterUrl = $(this).data("chapterurl");
var refererUrl = $(this).data("refererurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/read?sourceKey=" + sourceKey + "&refererUrl=" + refererUrl + "&chapterUrl=" + chapterUrl;
});
</script>
</html>
book_read
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>BOOK READ</title>
<style>
body {
background-color: antiquewhite;
}
.main {
padding: 10px 20px;
}
.click-book-detail, .click-book-read {
cursor: pointer;
color: #428bca;
}
.click-book-detail:hover {
color: rgba(150, 149, 162, 0.47);
}
.click-book-read:hover {
color: rgba(150, 149, 162, 0.47);
}
.float-left{
float: left;
margin-left: 70px;
}
</style>
</head>
<body>
<div class="main">
<!-- 章節名稱 -->
<h3 th:text="${book.nowChapter}"></h3>
<!-- 章節內容 -->
<p th:utext="${book.nowChapterValue}"></p>
<!-- 上、下章 -->
<p class="click-book-read float-left"
th:attr="data-chapterurl=${book.nextChapterUrl},data-sourcekey=${book.source.key},data-refererurl=${book.nowChapterUrl}"
th:text="${book.nextChapter}"></p>
<p class="click-book-read float-left"
th:attr="data-chapterurl=${book.prevChapterUrl},data-sourcekey=${book.source.key},data-refererurl=${book.nowChapterUrl}"
th:text="${book.prevChapter}"></p>
</div>
</body>
<!-- jquery在線版本 -->
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script th:inline="javascript">
var ctx = /*[[@{/}]]*/'';
$("body").on("click", ".click-book-read", function (even) {
var chapterUrl = $(this).data("chapterurl");
var refererUrl = $(this).data("refererurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/read?sourceKey=" + sourceKey + "&refererUrl=" + refererUrl + "&chapterUrl=" + chapterUrl;
});
</script>
</html>
2019-07-17補充:我們之前三個來源網站的baseUrl都是用http,但網站后面都升級成了https,例如筆趣閣:
導致抓取數據時報錯
<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>BOOK LIST</title>
<!-- 新 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="http://hanlei.online/Onlineaddress/layui/css/layui.css"/>
<style>
body {
background-color: antiquewhite;
}
.main {
margin: auto;
width: 500px;
margin-top: 50px;
}
.book {
border-bottom: solid #428bca 1px;
}
.click-book-detail, .click-book-read {
cursor: pointer;
color: #428bca;
}
.click-book-detail:hover {
color: rgba(150, 149, 162, 0.47);
}
.click-book-read:hover {
color: rgba(150, 149, 162, 0.47);
}
</style>
</head>
<body>
<div class="main">
<form class="form-inline" method="get" th:action="@{/book/search}">
來源
<select class="form-control" id="source" name="sourceKey">
<option value="">所有</option>
<option value="biquge" th:selected="${sourceKey} == 'biquge'">筆趣閣</option>
<option value="zongheng" th:selected="${sourceKey} == 'zongheng'">縱橫網</option>
<option value="qidian" th:selected="${sourceKey} == 'qidian'">起點網</option>
</select>
<input type="text" id="bookName" name="bookName" class="form-control" placeholder="請輸入..."
th:value="${keyWord}"/>
<button class="btn btn-info" type="submit">搜索</button>
</form>
<br/>
<div id="books"></div>
<div id="page"></div>
</div>
</body>
<!-- jquery在線版本 -->
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script src="http://hanlei.online/Onlineaddress/layui/layui.js"></script>
<script th:inline="javascript">
var ctx = /*[[@{/}]]*/'';
var books = [[${books}]];//取出后臺數據
var nums = 10; //每頁出現的數量
var pages = books.length; //總數
/**
* 傳入當前頁,根據nums去計算,從books集合截取對應數據做展示
*/
var thisDate = function (curr) {
var str = "",//當前頁需要展示的html
first = (curr * nums - nums),//展示的第一條數據的下標
last = curr * nums - 1;//展示的最后一條數據的下標
last = last >= books.length ? (books.length - 1) : last;
for (var i = first; i <= last; i++) {
var book = books[i];
str += "<div class='book'>" +
"<img class='click-book-detail' data-bookurl='" + book.bookUrl + "' data-sourcekey='" + book.source.key + "' data-searchurl='" + book.source.searchUrl + "' src='" + book.img + "'></img>" +
"<p class='click-book-detail' data-bookurl='" + book.bookUrl + "' data-sourcekey='" + book.source.key + "' data-searchurl='" + book.source.searchUrl + "'>書名:" + book.bookName + "</p>" +
"<p>作者:" + book.author + "</p>" +
"<p>簡介:" + book.synopsis + "</p>" +
"<p class='click-book-read' data-chapterurl='" + book.latestChapterUrl + "' data-sourcekey='" + book.source.key + "' data-refererurl='" + book.source.refererurl + "'>最新章節:" + book.latestChapter + "</p>" +
"<p>更新時間:" + book.updateDate + "</p>" +
"<p>大小:" + book.magnitude + "</p>" +
"<p>狀態:" + book.status + "</p>" +
"<p>類型:" + book.type + "</p>" +
"<p>來源:" + book.source.name + "</p>" +
"</div><br/>";
}
return str;
};
//獲取一個laypage實例
layui.use('laypage', function () {
var laypage = layui.laypage;
//調用laypage 邏輯分頁
laypage.render({
elem: 'page',
count: pages,
limit: nums,
jump: function (obj) {
//obj包含了當前分頁的所有參數,比如:
// console.log(obj.curr); //得到當前頁,以便向服務端請求對應頁的數據。
// console.log(obj.limit); //得到每頁顯示的條數
document.getElementById('books').innerHTML = thisDate(obj.curr);
},
prev: '<',
next: '>',
theme: '#f9c357',
})
});
$("body").on("click", ".click-book-detail", function (even) {
var bookUrl = $(this).data("bookurl");
var searchUrl = $(this).data("searchurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/details?sourceKey=" + sourceKey + "&searchUrl=" + searchUrl + "&bookUrl=" + bookUrl;
});
$("body").on("click", ".click-book-read", function (even) {
var chapterUrl = $(this).data("chapterurl");
var refererUrl = $(this).data("refererurl");
var sourceKey = $(this).data("sourcekey");
window.location.href = ctx + "/book/read?sourceKey=" + sourceKey + "&refererUrl=" + refererUrl + "&chapterUrl=" + chapterUrl;
});
</script>
</html>
解決辦法:參考https://blog.csdn.net/xiaoxian8023/article/details/49865335,繞過證書驗證
在BookUtil.java中新增方法
/**
* 繞過SSL驗證
*/
private static SSLContext createIgnoreVerifySSL() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sc = SSLContext.getInstance("SSLv3");
// 實現一個X509TrustManager接口,用于繞過驗證,不用修改里面的方法
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
String paramString) throws CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] paramArrayOfX509Certificate,
String paramString) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sc.init(null, new TrustManager[]{trustManager}, null);
return sc;
}
然后在gather方法中改成這樣獲取httpClient
/**
* 采集當前url完整response實體.toString()
*
* @param url url
* @return response實體.toString()
*/
public static String gather(String url, String refererUrl) {
String result = null;
try {
//采用繞過驗證的方式處理https請求
SSLContext sslcontext = createIgnoreVerifySSL();
// 設置協議http和https對應的處理socket鏈接工廠的對象
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", new SSLConnectionSocketFactory(sslcontext))
.build();
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
HttpClients.custom().setConnectionManager(connManager);
//創建自定義的httpclient對象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connManager).build();
//創建httpclient對象 (這里設置成全局變量,相對于同一個請求session、cookie會跟著攜帶過去)
// CloseableHttpClient httpClient = HttpClients.createDefault();
//創建get方式請求對象
HttpGet httpGet = new HttpGet(url);
httpGet.addHeader("Content-type", "application/json");
//包裝一下
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");
httpGet.addHeader("Referer", refererUrl);
httpGet.addHeader("Connection", "keep-alive");
//通過請求對象獲取響應對象
CloseableHttpResponse response = httpClient.execute(httpGet);
//獲取結果實體
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
result = EntityUtils.toString(response.getEntity(), "GBK");
}
//釋放鏈接
response.close();
}
//這里還可以捕獲超時異常,重新連接抓取
catch (Exception e) {
result = null;
System.err.println("采集操作出錯");
e.printStackTrace();
}
return result;
}
這樣就可以正常抓取了
我們之前獲取項目路徑用的是
var ctx = /*[[@{/}]]*/'';
突然發現不行了,跳轉的路徑直接是/開頭,現在改成這樣獲取
//項目路徑
var ctx = [[${#request.getContextPath()}]];
2019-08-01補充:大家如果看到有這個報錯,連接被重置,不要慌張,有可能是網站換域名了比如現在我們程序請求的是http://www.biquge.com.tw,但這個網址已經不能訪問了,筆趣閣已經改成https://www.biqudu.net/,我們改一下代碼就可以解決問題,要注意檢查各個源路徑是否能正常訪問,同時對方也可能改頁面格式,導致我們之前的規則無法匹配獲取數據,這種情況只能重新編寫爬取規則了
2019-08-02補充:發現了個bug,我們的BookUtil.insertParams方法原理是替換#字符串
/**
* 自動注入參數
* 例如:
*
* @param src http://search.zongheng.com/s?keyword=#1&pageNo=#2&sort=
* @param params "斗破蒼穹","1"
* @return http://search.zongheng.com/s?keyword=斗破蒼穹&pageNo=1&sort=
*/
public static String insertParams(String src, String... params) {
int i = 1;
for (String param : params) {
src = src.replaceAll("#" + i, param);
i++;
}
return src;
}
但是我們在搜索的時候,調用參數自動注入,形參src的值是來自靜態屬性Map,初始化的時候有兩個#字符串,在進行第一次搜索之后,#字符串被替換了,后面再進行搜索注入參數已經沒有#字符串了,因此后面的搜索結果都是第一次的結果...
解決:獲取來源時不是用=賦值,而是復制一份,三個方法都要改
修改前:
//獲取來源詳情
Map<String, String> src = source.get(sourceKey);
修改后:
//獲取來源詳情,復制一份
Map<String, String> src = new HashMap<>();
src.putAll(source.get(sourceKey));
公司最近打算做手機端,學習了DCloud公司的uni-app,開發工具是HBuilderX,并用我們的小說爬蟲學習、練手,做了個H5手機端的頁面
DCloud公司官網:https://www.dcloud.io/
uni-app官網:https://uniapp.dcloud.io/
uni-app 是一個使用 Vue.js 開發所有前端應用的框架,開發者編寫一套代碼,可編譯到iOS、Android、H5、以及各種小程序等多個平臺。
效果圖:
代碼已經開源、托管到我的GitHub、碼云:
GitHub:https://github.com/huanzi-qch/spider
碼云:https://gitee.com/huanzi-qch/spider
作者:huanzi-qch
出處:https://www.cnblogs.com/huanzi-qch
若標題中有“轉載”字樣,則本文版權歸原作者所有。若無轉載字樣,本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,否則保留追究法律責任的權利.
*請認真填寫需求信息,我們會在24小時內與您取得聯系。