前言 爬蟲要爬取的信息主要來自於網頁載入的內容,有必要瞭解一些網頁的知識。 當我們在瀏覽器網址欄輸入一個網址——URL,經過TCP/IP協議簇的處理,這個網址請求的信息就被髮送到URL對應的伺服器,接著伺服器處理這個請求,並將請求的內容返回給瀏覽器,瀏覽器便顯示或者下載URL請求相應的資源。這是前一 ...
前言
爬蟲要爬取的信息主要來自於網頁載入的內容,有必要瞭解一些網頁的知識。
當我們在瀏覽器網址欄輸入一個網址——URL,經過TCP/IP協議簇的處理,這個網址請求的信息就被髮送到URL對應的伺服器,接著伺服器處理這個請求,並將請求的內容返回給瀏覽器,瀏覽器便顯示或者下載URL請求相應的資源。這是前一篇博客所述。
在這一篇博客,筆者嘗試說明瀏覽器是如何顯示出這個頁面的。如下
HTML
HTML的含義
與超文本相對的是線性文本。線性,即直線關係,成比例。一本書,從第一頁到最後一頁,呈現直線關係;一本書的書簽,從第一章轉跳至第十章,呈現的是非線性關係。對於線性的電腦文件,不能直接從從一個位置的文件非線性地轉至另一個位置的文件,這中間是要經過一定的順序;相反,超文本之間的關係是非線性的,從一個HTML文件可以直接連接至另一個HTML文件。促成這種連接的正是是超文本鏈接,超文本鏈接就是超鏈接,上一篇的URL就是超鏈接的一種,電子書中的書簽也是超鏈接的一種。
HTML是一門語言,常用於編寫網頁,HTML文件是超文本的一種形式。以下是一些名稱的解釋,以輔助理解,不必太在意於嚴格的定義。
- HTML(HyperText Mark-up Language):超文本標記語言
- 超文本:HyperText,用超鏈接的方法,將不同空間的文字信息組織在一起的網狀文本
- 鏈接:link,從一個文檔指向其它文檔或從文本錨點(anchor)指向某已命名位置的鏈接
- 錨點:anchor,是網頁製作中超級鏈接的一種,又叫命名錨記。命名錨記像一個迅速定位器一樣是一種頁面內的超級鏈接
- 超鏈接:hyperlink,它是一種允許我們同其他網頁或站點之間進行連接的頁面元素
- 超文本鏈接:Hypertext link,就是超鏈接。是指用文字鏈接的形式來指向一個頁面
- 線性:linear,指量與量之間按比例、成直線的關係,在數學上可以理解為一階導數為常數的函數
樹
樹的概念
樹的結構是很簡單的,平時留心觀察即可知道樹為何是“直”的。從第一個分叉開始這樹就是由無數的“開叉”結構組成,直至最微小的枝芽。怎麼簡單怎麼來,數學上的描述不管。下麵的性質和定義來自《用Python解決數據結構和演算法》
樹的性質
相關術語在“定義1”裡面有解釋,以分類樹為例此處有圖片
- 樹是分層的,分層的意思是樹的頂層部分更加寬泛一般而底層部分更加精細具體。在圖1中,最上層是“界”,它下麵的一層(上層的子層)是“門”,然後是“綱”等等。
- 一個節點的子節點(node)和另一個節點的子節點(children)是完全獨立的。如圖1,“貓屬”有兩個子節點“家生”和“野生”,“蠅屬”中也有一個“家生”, 但它和“貓屬”中的“家生”是完全不同而且相互獨立的。
- 樹的每個葉節點(leaf)都是不同的。如圖1,對每一種動物,我們都可以從根節點(root)開始沿著一條特定的路徑找到它對應的葉節點,並把它和其他動物區分開, 例如對於家貓
- 樹下層的所有部分(子樹Subtree)移動到樹的另一位置而不影響更下層的情況。如圖2,我們可以將所有標註/etc的子樹從根節點下移動到usr/下麵但是對httpd的內容及其子節點的內容不會有影響。
圖1 一些動物的分類樹
圖2 一小部分Unix文件系統的分層情況
定義1
樹是節點和連接節點的邊的集合
這個定義簡單粗暴,但蘊含的東西不少。以下是一些相關的東西,都是些抽象的概念,將其類比成枝節葉可以吧
- 節點(Node):樹的基本組成
- 邊(Edge):樹的基本組成,連接兩個節點。。每個節點(除了根節點)都有且只有一條與其他節點相連的入邊(指向該節點的邊),每個節點可能有許多條出邊(從該節點指向其他節點的邊)。
- 根節點(Root):樹中唯一沒有入邊的節點
- 路徑(Path):路徑是由邊連接起來的節點的有序排列
- 子節點集(Childern):當一個節點的入邊來自於另外一個節點時,稱前者為後者的子節點。同一個節點的所有子節點構成子節點集
- 父節點(Parent):一個節點是它的所有出邊連接的節點的父節點。
- 兄弟節點(Sibling)同一節點的所有子節點胡偉兄弟節點
- 子樹(Subtree):子樹是一個父節點的某個子節點的所有邊和後代節點所構成的集合
- 葉節點(LeafNode):沒有子節點的節點稱為葉節點
- 層數(Level):一個節點的層數是指從跟節點到該節點的路徑的邊的數目,定義根節點層數為0
- 高度(Height):樹的高度等於所有節點層數的最大值
定義2
每棵樹為空,或者包含一個根節點和0個或多個子樹,其中每個子樹也符合這樣的定義
這個定義巧妙,用到遞歸只能“巧妙”了。
HTML的構成
HTML是由一系列的元素組成,元素由首尾標簽和其中的內容組成,學習HTML就要學習那一堆元素。標簽表示元素的起始和結束。下麵是一個簡單的HTML網頁。例如代下麵代碼中
<li>List item one</li>是元素,<li>是首標簽,</li>是尾標簽,'List item one'是內容。
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>simple</title> </head> <body> <h1>A simple web page</h1> <ul> <li>List item one</li> <li>List item two</li> </ul> <h2><a href="http://www.cs.luther.edu">Luther CS </a><h2> </body> </html>
代碼1
這個網頁也相當於一棵樹,樹的每一層都對應超文本標記符的一層嵌套。如圖3
圖3 與網頁的構成元素相對應的樹
DOM
DOM(Document Object Model),文檔對象模型。當瀏覽器要顯示HTML文檔網頁的時候,瀏覽器會創建這個網頁全部元素的內部表示體系——DOM,類似於地圖表示實際的地點一樣,DOM也可以看做是這個HTML網頁的“地圖”,我們可以通過JavaScript(例如父子對象的形式)去讀取DOM這張“地圖”。在DOM裡面,網頁的所有元素以父子對象等形式形成樹形結構,這棵樹最頂層的是瀏覽器window對象(如圖4),window對象的一個子對象是document對象,一個HTML文檔被載入到瀏覽器的時候,都會創建一個document對象,這個對象包含了HTML文檔的全部元素,同樣HTML的內容也會表示成樹形結構(如圖3)
當DOM把網頁表示成“樹”的形式(如圖3)時,每個元素都相當於樹的節點(元素節點),每個屬性也相當一個節點(屬性節點),文本也是(文本節點),屬性節點和文本節點包含在元素節點中。邊表示了元素間的關係。
圖4 window對象及其一些子對象
CSS
通過DOM模型,瀏覽器就知道如何去顯示一個HTML網頁的title,h1,body,ul······,但這並不是唯一的方式,我們同樣可以通過CSS(Cascading Style Sheets)層級樣式表去告訴瀏覽器該如何去顯示一個網頁文檔,實際上瀏覽器也會根據外部樣式表去構建一棵“樹”——CSSOM(CSS Object Model,CSS 對象模型)。
CSS是一種樣式表語言,用於為HTML文檔定義佈局。例如,設置字體、顏色、邊距、高度、寬度、背景圖像等等。爬蟲中經常用到CSS選擇器。
添加CSS的方法
行內樣式表
為HTML應用CSS的一種方法是使用HTML屬性style。例如下麵代碼,通過行內樣式表將頁面背景設為紅色,代碼如下:
<html> <head> <title>例子</title> </head> <body style="background-color: #FF0000;"> <p>這個頁面是紅色的</p> </body> </html>
內部樣式表
為HTML應用CSS的另一種方法是採用HTML元素style。代碼如下
<html> <head> <title>例子</title> <style type="text/css"> body {background-color: #FF0000;} </style> </head> <body> <p>這個頁面是紅色的</p> </body> </html>
外部樣式表
外部樣式表就是一個擴展名為css的文本文件。如何在一個HTML文檔里引用一個外部樣式表文件(style.css)呢?可以在HTML文檔里創建一個指向外部樣式表文件的鏈接(link)即可,就像下麵代碼那樣,其中href="style/style.css是CSS文件的路徑,要註意的就是外部樣式表的路徑問題,詳略。 代碼如下:
<link rel="stylesheet" type="text/css" href="style/style.css" />
CSS構造樣式規則
樣式表中包含了定義網頁外觀的規則,樣式表中的每條規則都有兩個主要部分:選擇器(selector)和聲明塊(declaration block)。選擇器的作用在於定位以及決定哪些元素受到影響;聲明塊由一個或多個屬性- 值對(每個屬性-值對構成一條聲明,declaration)組成,它們指定應該做什麼(參見圖5 ~圖6)。
構造樣式規則的步驟如下:
- 輸入selector ,這裡的selector 表示希望進行格式化的元素。
- 輸入{(前花括弧)開始聲明塊。
- 輸入property:value; ,其中property是CSS 屬性的名稱,描述要應用哪種格式;value 是該屬性允許的選項之一。
- 根據需要,重覆第(3) 步。通常一行輸入一個property: value(一條聲明),如圖6所示的那樣,但這並非強制要求。
- 輸入},結束聲明塊和樣式規則。
CSS選擇器
由於選擇器具有定位作用,例如所以利用選擇器就可以定位到我們想提取的數據,因此,CSS選擇器經常在爬蟲中出現。常見的CSS選擇器語法規則如圖7,見W3C鏈接:
圖7 一些CSS選擇器的語法規則
CSS選擇器的應用
在Beautiful Soup中的應用
例如如果爬取到下麵這段HTML代碼,就可以通過CSS選擇器去提取,如下:
html_doc = """ <html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>, <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; and they lived at the bottom of a well.</p> <p class="story">...</p> """ from bs4 import BeautifulSoup soup = BeautifulSoup(html_doc, 'lxml') # 選擇所有title標簽,結果是一個列表,可迭代 print(soup.select("title")) # 選擇body標簽下的所有a標簽,並獲取文本 results = soup.select("body a") for result in results: print(result.get_text()) # 通過id查找 選擇a標簽,其id屬性為link1的標簽 print(soup.select("a#link1")) # 選擇所有p標簽中的第三個標簽 print(soup.select("p:nth-of-type(3)")) # 相當於soup.select(p)[2] # 選擇a標簽,其href屬性以lacie結尾 print(soup.select('a[href$="lacie"]')) # 選擇a標簽,其href屬性包含.com print(soup.select('a[href*=".com"]')) # 通過【屬性】查找,選擇a標簽,其屬性中存在myname的所有標簽 a = soup.select("a[myname]") # 選擇a標簽,其屬性href=http://example.com/lacie的所有標簽 b = soup.select("a[href='http://example.com/lacie']") # 選擇a標簽,其href屬性以http開頭 c = soup.select('a[href^="http"]') print(a) print(b) print(c)
1 # 選擇body標簽下的直接a子標簽 2 print(soup.select("body > a")) 3 # 選擇id=link1後的所有兄弟節點標簽 4 print(soup.select("#link1~.mysis")) 5 # 選擇id=link1後的下一個兄弟節點標簽 6 print(soup.select("#link1 + .mysis")) 7 # 選擇a標簽,其類屬性為mysis的標簽 8 print(soup.select("a.mysis")) 9 # 從html中排除某標簽,此時soup中不再有script標簽 10 print([s.extract()for s in soup('script')]) 11 # 如果想排除多個呢 12 print([s.extract()for s in soup(['script', 'fram'])])View Code
在pyquery中的應用
例如如果爬取到下麵這段HTML代碼,就可以通過CSS選擇器去提取,如下:
html = ''' <div class="wrap"> <div id="container"> <ul class="list"> <li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li> </ul> </div> </div> ''' from pyquery import PyQuery as pq doc = pq(html) a = doc('.item-0.active a') # 先獲取class為item-0 且class為active的li標簽內的a標簽節點,再提取屬性 print(a, type(a)) print(a.attr('href')) # 獲取到的結果為鏈接路徑: link3.html print(a.attr.href) print(a.text()) # 獲取文本,獲得a節點的wb li = doc('.item-0.active') print(li.html()) # html()返回該節點的所有文本,包括標簽a的開始和結束 lt = doc('li') print(lt.html()) # 只返回第一個li的文本,欲獲取全部需要遍歷 print(lt.text()) # 返回所有li的文本,用空格隔開,結果是字元串類型 print(type(lt.text())) b = doc('a') print(b, type(b)) print(b.attr.href) # attr()方法只會得到第一個節點的屬性,這時,需要遍歷 for item in b.items(): print(item.attr.href)
html = ''' <div id="container"> <ul class="list"> <li class="item-0">first item</li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li> <li class="item-1 active"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a></li> </ul> </div> ''' from pyquery import PyQuery as pq doc = pq(html) print(doc('#container .list li')) print(type(doc('#container .list li'))) items = doc('.list') print(items.text()) print(type(items.text())) print(items) lis = items.find('li') print(lis) print(type(items)) print(type(lis)) ls = items.children() # 返回子節點 print(ls) print(type(ls))View Code
JavaScript
這裡只說兩點,ajax和渲染,因為爬蟲經常碰到
渲染——瀏覽器如何顯示頁面
到目前為止,已經瞭解到瀏覽器在載入HTML的時候,先解析HTML文檔,然後生成HTML樹——DOM,同時瀏覽器生成了另外一棵樹——CSSOM,這兩個模型共同創建“渲染樹”,之後瀏覽器就有了足夠的信息去進行佈局,併在屏幕上繪製頁面。如果這裡沒有外部樣式表也沒有行內或者內部樣式表(前面所述),也無需操心,因為瀏覽器本身也自帶了一個預設的CSS樣式表,只不過我們自定義的CSS樣式表會將它覆蓋而已。這裡的“繪製的頁面”就是要顯示的頁面,暫且理解成編程中的“print”吧,這裡的一些奇怪的問題(比如:“瀏覽器顯示HTML文檔首尾標簽去哪裡啦?)”都可以類比print函數中的一些問題(“引號去哪裡了?”)來看待,因為瀏覽器的顯示和print函數是的目的都是將內容顯示到電腦屏幕!只不過這裡的繪製不是普通列印而是“彩打”。
渲染的過程如下(圖片來自這裡):
為什麼渲染還和JavaScript有關呢?是的,單單是HTML和CSS就可以顯示出網頁,但JavaScript卻有更強大的功能,其實JavaScript就是網頁源代碼中的一個腳本,他在瀏覽器顯示頁面的時候可以改變這個頁面的佈局和內容,也就是改變DOM和CSSOM的能力,從而改變了網頁的顯示。
ajax
Ajax是一種無需刷新頁面即可從伺服器(或客戶端)上載入數據的手段,這裡的刷新是指重新請求,重新下載頁面。而Ajax卻可以在不刷新的情況下載入數據,從而給人一種“流暢”的感覺。但ajax只是其中的一種手段,例如上面提到的JavaScript渲染也是這樣的一種手段。那麼ajax是如何實現這種效果的呢?既然載入了數據那麼肯定是向伺服器發送了請求,那麼如何做到不顯示新的頁面呢?答案是XMLHttpRequest(XHR)對象,它可以實現這種方式。既然是對象當然就有類似於“send()”等方法向伺服器發送請求,然後接受到伺服器響應的內容,接下來avaScript就會解釋並處理這些內容,然後渲染網頁,繼而瀏覽器將數據顯示出來。因此在爬蟲的時候要想爬取這種動態載入的數據,就需要在開發者工具中去找尋這些新的URL請求,然後再在程式中模擬這種請求,再提取數據。就這樣先吧。代碼來自W3C如下:
<html> <head> <script type="text/javascript"> function loadXMLDoc() { var xmlhttp; if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari xmlhttp=new XMLHttpRequest(); } else {// code for IE6, IE5 xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { document.getElementById("myDiv").innerHTML=xmlhttp.responseText; } } xmlhttp.open("POST","/ajax/demo_post.asp",true); xmlhttp.send(); } </script> </head> <body> <h2>AJAX</h2> <button type="button" onclick="loadXMLDoc()">請求數據</button> <div id="myDiv"></div> </body> </html>
周末結束了!以上。