網站樹代碼解析
- 代碼使用方法
- settings.py
- url_object.py
- traverse_sebsite.py
- traverse_result_1.py
- build_tree.py
- result_1.md
- result_1.md.svg
- 遇到的問題
-
- a标簽擷取超出統計
- 連結處理
- 檔案命名
- 靜态樹節點設計
- 異常問題
- 網頁加載過慢
- 重定向問題
- 深度問題
- 作者有話說
前些日子在github上釋出了一個項目,是用來為網站生成樹形結構的,我将其稱為“website_tree”。項目位址為website_tree,有感興趣的朋友可以clone一下,一起探讨。為了便于了解和防止忘記,特寫此部落格進行思路記錄以及代碼解析。
代碼使用方法
- 在github上将代碼clone到本地
- 修改
中的配置settings.py
- 運作
traverse_sebsite.py
- 将
從目标路徑copy至目前路徑中traverse_result_1.py
- 運作build_tree.py
此時其實樹形結構已經生成到
.md
檔案中了,為了友善檢視可以将
.md
檔案導入百度腦圖來檢視生成效果。
settings.py
settings.py
檔案中是我設定的一些全局應用的變量,為了防止使用時多處修改,遂将其寫入檔案中。下面對其中的變量進行介紹:
-
為我們所要周遊的整個網站的域名。其中以DOMAIN
開頭的和以http://
開頭的認為屬于同一網站,因為隻是使用不同協定來通路。而以https://
結尾和以.cn
結尾的則認為不是同一網站。此變量的類型為.com
型,不将其設定為list
型的原因是在此項目做泛化的過程中遇到了有網頁重定向問題,我們認為一個網頁如果被重定向到另一個網頁了,那麼新的網頁的域名也是目前網站中的域名。str
-
為爬蟲開始的起始點,我們的爬取行為其實是一種以ORIGIN
為起始節點的廣度優先周遊(**注意:**我們假設整個網站為一個連通的有向圖,即不存在孤立的不被其他任何網頁指向的網頁)。ORIGIN
-
為爬取的深度,也就是說我們從目前節點開始爬取幾層。這一變量的設定主要是為了适應對于某些網站中清單頁有多個網頁,而我們隻想爬取第一頁的情況。若設定為MAX_DEEP
則不考慮爬取深度,直接爬取整個網站。None
-
為了友善檢視,我将每次爬取到的網頁儲存成了WEBPAGE_SAVEPATH
檔案,儲存在此目錄下。.html
-
爬取完整個網站後,我們已經得到了全部網頁節點的資訊,我将其寫到此檔案中。需要注意的是,雖然設定了儲存路徑,但在生成樹形結構的時候需要将此檔案複制到代碼的運作目錄下。TRAVERSE_RESULT_PATH
-
生成的樹形結構我用markdown的标題結構來進行表示,儲存在此檔案中。MARKDOWN_PATH
url_object.py
此檔案中寫的是我對于網頁節點定義的資料結構,在開始先導入在
settings.py
中定義的
DOMAIN
和
WEBPAGE_SAVEPATH
,此外為了記錄因為通路原因漏爬的網頁,我使用
logging
子產品來進行記錄。
下面來對資料結構進行解析:
-
為保證爬取效果與人類點選高度一緻,我使用的是browser
而不是selenium
。那麼為了保證爬取速度,應該避免頻繁建立requests
對象,而是在主函數中建立好傳入資料結構。browser
-
和url
預設是一緻的,隻有在網頁發生重定向時,會有不同,而我們記錄的為重定向之後的url。origin_url
-
和referer
為指向目前網頁的網頁位址及總數。referer_count
-
為從其實網頁開始到目前網頁經曆了幾層。deep
-
記錄目前網頁是否被爬取過。crawled
-
和inner_link
為目前網頁中的内部網頁(即同一域名下網頁)位址及總數。inner_link_count
-
和outer_link
為目前網頁中的外部網頁(即不同域名下網頁)位址及總數。outer_link_count
-
為目前網頁儲存時的位址。filepath
在此資料結構中,主要有兩個操作:
- 若此網頁還未被爬取過:對網頁進行爬取,若網頁發生重定向,将重定向後的域名加入域名中。周遊次網頁中所有的
标簽,取出a
屬性,對其進行處理。處理後,判斷是否屬于此網站,若是,加入@href
,inner_link
加一。否則,加入inner_link_count
,outer_link
加一。對outer_link_count
标簽進行周遊之後,将網頁儲存起來,儲存的檔案名也需要處理,首先要split出最後一段不包含“/”的str,然後用“_”來代替所有非法字元,最後若檔案已存在,則認為是同名檔案,在檔案名後加“(1)”以示差別。儲存後,将a
記為crawled
,log中進行記錄。True
- 若此網頁以被爬取過,則将指向此網頁的網頁加入
中,referer
加一。referer_count
在此檔案的末尾我寫了一個demo來示範怎樣使用此資料結構。
traverse_sebsite.py
在此檔案中我寫了一個demo來示範怎樣使用上述資料結構來爬取網站。
- 在開始先導入在
中定義的settings.py
,ORIGIN
和TRAVERSE_RESULT_PATH
。MAX_DEEP
- 接下來,要建立browser對象,我使用的是selenium中的Chrome浏覽器,為提升速度使用無頭模式,禁用了gpu加速以及禁用圖檔加載。
- 我用一個
(list型)來做隊列來對網頁進行爬取,用index
(dic型)來記錄網頁是否被爬取過。document
- 首先要為爬取起始網頁建立執行個體,将其加入隊列。
- 周遊隊列,若此網頁的深度已經超過了
則不進行爬取了。否則,爬取網頁,在MAX_DEEP
記錄此網頁的document
并将其指向此對象。若恰達到爬取深度,不進行操作,否則周遊此網頁中的内部連結url
,若未爬取過,建立對象,加入隊列,記入inner_link
。若已爬取過,調用此内部連結的document
函數。referering()
- 當隊列為通路結束,則認為整個網站的爬取已經完成了,那麼隊列中記錄的就是網站中所有節點的資訊了。我們将其中每個對象的
(除__dict__
外的所有成員變量)寫到檔案中形成一個list。browser
- 當然最後要記得關閉浏覽器。
traverse_result_1.py
此檔案為周遊網站後的節點資訊,需要從目标路徑copy至代碼的運作目錄下。
build_tree.py
此檔案為利用
traverse_result_1.py
中的節點資訊生成一棵樹。
- 首先我們要找到整個網站的根,思想為
大的為樹根(在早期一些大的爬蟲引擎也用此規則來确定網站的根)。當網站中有不止一個網頁同時具有最大的referer_count
時,我們就選擇層級短的(即離根目錄近的)為根。若此時找到的網頁還不是唯一的,那麼就選擇referer_count
大的為根。要是還不唯一,我也黔驢技窮了,就選擇第一個入隊的為根吧。inner_link
- 找到根之後,就将根的
記為-1。parents
- 接下來就周遊全部節點,為他們找
即可。思想為:若此節點已有parents
樹形則不處理,否則找parents
中包含此網頁的網頁。若沒有找到,将其inner_link
記為-1(也就是變成一個森林了,但其實不會出現這種情況,因為在若沒有網頁的parents
中包含它,它就不會入隊)。若有多個網頁指向此網頁,用inner_link
和referer_count
來确定雙親。inner_link_count
此時,其實一個靜态的樹形結構已經被儲存下來了,那麼我們接下來要做的就是将其生成markdown檔案了。我的做法是用一個有序字典
d
來記錄節點的
url
和層級
level
接下來從根節點開始先序周遊即可。最後就将此有序字典
d
按層級加“#”寫入檔案即可生成樹形的markdown檔案。
result_1.md
此檔案即為生成的樹形的markdown檔案。
内容大緻如下:
# docs.scrapy.org/en/0.10.3/topics/settings.html
## docs.scrapy.org/en/latest
### docs.scrapy.org/en/latest/genindex.html
### docs.scrapy.org/en/latest/py-modindex.html
### docs.scrapy.org/en/1.6
## docs.scrapy.org/en/1.0
### docs.scrapy.org/en/1.0/genindex.html
### docs.scrapy.org/en/1.0/py-modindex.html
## docs.scrapy.org/en/0.24
### docs.scrapy.org/en/0.24/intro/overview.html
### docs.scrapy.org/en/0.24/intro/install.html
### docs.scrapy.org/en/0.24/intro/tutorial.html
### docs.scrapy.org/en/0.24/intro/examples.html
### docs.scrapy.org/en/0.24/topics/commands.html
### docs.scrapy.org/en/0.24/topics/spiders.html
### docs.scrapy.org/en/0.24/topics/items.html
### docs.scrapy.org/en/0.24/topics/shell.html
### docs.scrapy.org/en/0.24/topics/item-pipeline.html
### docs.scrapy.org/en/0.24/topics/feed-exports.html
### docs.scrapy.org/en/0.24/topics/link-extractors.html
### docs.scrapy.org/en/0.24/topics/exceptions.html
### docs.scrapy.org/en/0.24/topics/logging.html
### docs.scrapy.org/en/0.24/topics/stats.html
### docs.scrapy.org/en/0.24/topics/email.html
### docs.scrapy.org/en/0.24/topics/telnetconsole.html
### docs.scrapy.org/en/0.24/topics/webservice.html
### docs.scrapy.org/en/0.24/topics/debug.html
### docs.scrapy.org/en/0.24/topics/contracts.html
### docs.scrapy.org/en/0.24/topics/practices.html
### docs.scrapy.org/en/0.24/topics/broad-crawls.html
### docs.scrapy.org/en/0.24/topics/leaks.html
### docs.scrapy.org/en/0.24/topics/autothrottle.html
### docs.scrapy.org/en/0.24/topics/benchmarking.html
### docs.scrapy.org/en/0.24/topics/jobs.html
### docs.scrapy.org/en/0.24/topics/architecture.html
#### docs.scrapy.org/en/0.24/_images/scrapy_architecture.png
### docs.scrapy.org/en/0.24/topics/downloader-middleware.html
### docs.scrapy.org/en/0.24/topics/spider-middleware.html
### docs.scrapy.org/en/0.24/topics/api.html
### docs.scrapy.org/en/0.24/topics/signals.html
### docs.scrapy.org/en/0.24/topics/exporters.html
### docs.scrapy.org/en/0.24/contributing.html
### docs.scrapy.org/en/0.24/versioning.html
### docs.scrapy.org/en/0.24/genindex.html
### docs.scrapy.org/en/0.24/py-modindex.html
### docs.scrapy.org/en/0.24/topics/selectors.html
### docs.scrapy.org/en/0.24/topics/firefox.html
### docs.scrapy.org/en/0.24/topics/firebug.html
#### docs.scrapy.org/en/0.24/_images/firebug1.png
#### docs.scrapy.org/en/0.24/_images/firebug2.png
...
...
## docs.scrapy.org/en/0.10.3/contributing.html
## docs.scrapy.org/en/0.10.3/api-stability.html
## docs.scrapy.org/en/0.10.3/experimental/index.html
## docs.scrapy.org/en/0.10.3/experimental/djangoitems.html
## docs.scrapy.org/en/0.10.3/experimental/crawlspider-v2.html
## docs.scrapy.org/en/0.10.3/index.html
result_1.md.svg
為了友善檢視,我将其導入到了百度腦圖進行可視化,此檔案為圖檔效果。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TPRplcWJTYuVjMipHbHFmeW5WW2R2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLyAjNzMTOwcTMwIzNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
怎麼樣,看起來還plausible吧,哈哈哈~
遇到的問題
整體來說,這個項目難度并不大。但作者認為爬蟲的代碼難度本來就不高,難點基本都在分析與細節處理上。由于此項目不涉及反爬之後分析解密的問題,是以主要在此總結遇到的細節問題
a标簽擷取超出統計
作者一開始是用requests來做的爬蟲,這也是一般爬蟲首選的方法,很簡單。但是使用requests會遇到這樣的問題:我們都知道網頁一般都是html+css+js的結構,我們想要的a标簽其實都是html中的a标簽,但使用requests不僅會将html中的a标簽取出來,js中的a标簽也會不可避免的被取出來。
解決方法:使用selenium,由于selenium是模拟人類點選的,那麼它的行為會和人的行為有更大的相似度,結果也就越接近人觀察到的結果。
連結處理
作者對于url的存儲一開始未做任何處理,導緻的結果是
http://
開頭的網址和
https://
開頭的網址被認為是不同url(但其實是不對的,因為這隻是使用不同協定來通路同一url)。
對于網頁中擷取的連結,有的時候并不是完整連結,而是相對路徑,有的還加了錨點,對于這些情況也都需要将其處理成整齊的url。
此外,url的大小寫問題,作者遇到的第一個網站是不區分大小寫的,我們也預設假設沒有程式員會以大小寫來區分url,于是将所有的url轉換成小寫進行處理。
檔案命名
在爬取過程中,我會将爬取的網頁儲存下來,但儲存的時候命名上遇到了一些問題。我的命名方法是選取url中最後一段有效路徑進行命名。但首先是遇到了檔案名中包含不合法字元的問題,我用下劃線
_
來替換了所有不合法字元。
緊接着,發現檔案數目與爬取數目不符。檢查發現是檔案會出現重名問題,這樣的結果就是每次新的同名檔案替換舊的同名檔案。為解決這一問題,我每次儲存之前判斷檔案是否存在,若存在則在想要設定的檔案名後加
(1)
。這樣處理之後,檔案數目确實增加了,但還是與爬取數量不符,原因是上述解決方法隻能解決一次重名問題,當有多次重名時,則會一直替換加
(1)
的檔案。解決辦法是用while循環來判斷,隻要有重名就加
(1)
,這樣得到的檔案數目就正确了。
靜态樹節點設計
之前也有提到過,我們将所有網頁節點儲存下來之後,為其找parents,形成一個靜态樹結構。作者後來考慮到後期會将靜态樹生成為markdown檔案,若增加child域的話能将時間複雜度降低。不過這是一種犧牲空間換時間的方法,具體要不要采用還需要根據具體問題進行分析,是以在代碼中并未采用後者。
異常問題
異常問題是在實際工程中常遇到的,尤其是在爬蟲中網頁通路中,為了保證軟體的魯棒性,我們常在代碼中用
try catch
來處理異常。但對于爬蟲的需求方而言,常常要求資料的完整性。退一步說,哪怕不可避免的會有漏爬現象,也需要知道是那些網頁被漏爬了。
在這種要求下,我們再傳回去看輸出的方法就顯得很笨重了。正常做法是對于每個對象将爬取狀态和資訊寫入到log中,這樣不論是對需求方彙報還是進行補爬都友善得多。
網頁加載過慢
之前提到了requests和selenium。作者說selenium類人類,但又說requests是首選,這似乎有點沖突。為什麼呢,selenium又好用,代碼實作難也不高,配置也是一勞永逸,為什麼不選用selenium呢?
對作者而言,主要原因在于selenium的速度原因,對于一個網頁,selenium的速度往往會比requests慢不隻一倍。
為了解決這一問題,我們有三種方法同時作用:
- 不在對象中建立driver對象。頻繁建立對象當然會将速度拉下來,我們在主函數中建立好driver對象,然後傳入對象中,則會減少建立driver對象的次數。
- 使用無頭模式,禁用gpu加速。這是一種常用的加速方法,一般先将代碼調試好,然後再在代碼中加入無頭和禁用gpu的兩個參數即可。
- 禁用圖檔加載。由于我們在爬取中隻需要a标簽,那麼圖檔加載的耗時就應該避免,也隻是加參數的問題。
利用這三種方法能有效的對selenium的速度進行提升。
重定向問題
重定向問題是在爬蟲過程中經常遇到的,其實這已經不算問題了,常用的包都自動解決重定向問題。但是,由于我們的項目要在内部連結中進行爬取,若重定向的網站域名發生變化則會産生問題。具體的做法,前面已經講過,就不再贅述了。
深度問題
深度問題是代碼初步建構出來之後才有的需求。針對這一需求,其實是存在一點點問題的。當深度不夠的時候,可能遇不到重定向問題的解決方法,那麼其實會導緻一部分重定向網頁漏爬,不過這一問題個人認為不屬于缺陷,是可了解的。
還有就是對于加深度之後的葉子節點的處理也需要進行不同于非葉子節點的處理。具體實作可參閱代碼。
作者有話說
作者為研二程式媛一枚,在爬蟲方面也還是一個新手,自知技術上還有很大不足,望大家海涵。如果大家對本項目有任何建議,想法或者問題,歡迎交流和探讨。