1. 程式人生 > >爬蟲基礎(二)——網頁

爬蟲基礎(二)——網頁

前言

  爬蟲要爬取的資訊主要來自於網頁載入的內容,有必要了解一些網頁的知識。

  當我們在瀏覽器網址欄輸入一個網址——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. 樹是分層的,分層的意思是樹的頂層部分更加寬泛一般而底層部分更加精細具體。在圖1中,最上層是“界”,它下面的一層(上層的子層)是“門”,然後是“綱”等等。
  2. 一個節點的子節點(node)和另一個節點的子節點(children)是完全獨立的。如圖1,“貓屬”有兩個子節點“家生”和“野生”,“蠅屬”中也有一個“家生”, 但它和“貓屬”中的“家生”是完全不同而且相互獨立的。
  3. 樹的每個葉節點(leaf)都是不同的。如圖1,對每一種動物,我們都可以從根節點(root)開始沿著一條特定的路徑找到它對應的葉節點,並把它和其他動物區分開, 例如對於家貓
  4. 樹下層的所有部分(子樹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)。

  構造樣式規則的步驟如下:

  1. 輸入selector ,這裡的selector 表示希望進行格式化的元素。
  2. 輸入{(前花括號)開始宣告塊。
  3. 輸入property:value; ,其中property是CSS 屬性的名稱,描述要應用哪種格式;value 是該屬性允許的選項之一。
  4. 根據需要,重複第(3) 步。通常一行輸入一個property: value(一條宣告),如圖6所示的那樣,但這並非強制要求。
  5. 輸入},結束宣告塊和樣式規則。

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>

  

  週末結束了!以上。