《Learning Scrapy》(中文版)第2章 理解HTML和XPath


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎
第4章 從Scrapy到移動應用
第5章 快速構建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析


為了從網頁提取信息,了解網頁的結構是非常必要的。我們會快速學習HTML、HTML的樹結構和用來篩選網頁信息的XPath。

HTML、DOM樹結構和XPath

從這本書的角度,鍵入網址到看見網頁的整個過程可以分成四步:

  • 在瀏覽器中輸入網址URL。URL的第一部分,也即域名(例如gumtree.com),用來搜尋網絡上的服務器。URL和其他像cookies等數據形成了一個發送到服務器的請求request。
  • 服務器向瀏覽器發送HTML。服務器也可能發送XML或JSON等其他格式,目前我們只關注HTML。
  • HTML在瀏覽器內部轉化成樹結構:文檔對象模型(DOM)。
  • 根據布局規范,樹結構轉化成屏幕上的真實頁面。

研究下這四個步驟和樹結構,可以幫助定位要抓取的文本和編寫爬蟲。

URL
URL包括兩部分:第一部分通過DNS定位服務器,例如當你在瀏覽器輸入https://mail.google.com/mail/u/0/#inbox這個地址時,產生了一個mail.google.com的DNS請求,后者為你解析了一臺服務器的IP地址,例如173.194.71.83。也就是說,https://mail.google.com/mail/u/0/#inbox轉換成了https://173.194.71.83/mail/u/0/#inbox。

URL其余的部分告訴服務器這個請求具體是關于什么的,可能是一張圖片、一份文檔或是觸發一個動作,例如在服務器上發送一封郵件。

HTML文檔

服務器讀取URL,了解用戶請求,然后回復一個HTML文檔。HTML本質是一個文本文件,可以用TextMate、Notepad、vi或Emacs等軟件打開。與大多數文本文件不同,HTML嚴格遵循萬維網聯盟(World Wide Web Consortium)的規定格式。這個格式超出了本書的范疇,這里只看一個簡單的HTML頁面。如果你打開http://example.com,點擊查看源代碼,就可以看到HTML代碼,如下所示:

<!doctype html>
<html>
  <head>
      <title>Example Domain</title>
      <meta charset="utf-8" />
      <meta http-equiv="Content-type"
              content="text/html; charset=utf-8" />
      <meta name="viewport" content="width=device-width,
              initial-scale=1" />
      <style type="text/css"> body { background-color: ... 
              } </style>
  <body>
      <div>
              <h1>Example Domain</h1>
              <p>This domain is established to be used for
                 illustrative examples examples in documents.
                 You may use this domain in examples without
                 prior coordination or asking for permission.</p>
              <p><a >
                 More information...</a></p>
      </div>
  </body>
</html>

為了便于閱讀,我美化了這個HTML文檔。你也可以把整篇文檔放在一行里。對于HTML,大多數情況下,空格和換行符不會造成什么影響。

尖括號里的字符稱作標簽,例如<html>或<head>。<html>是起始標簽,</html>是結束標簽。標簽總是成對出現。某些網頁沒有結束標簽,例如只用<p>標簽分隔段落,瀏覽器對這種行為是容許的,會智能判斷哪里該有結束標簽</p>。
<p>與</p>之間的內容稱作HTML的元素。元素之間可以嵌套元素,比如例子中的<div>標簽,和第二個<p>標簽,后者包含了一個<a>標簽。

有些標簽稍顯復雜,例如<a >,帶有URL的href部分稱作屬性。
最后,許多標簽元素包含有文本,例如<h1>標簽中的Example Domain。對我們而言,<body>標簽之間的可見內容更為重要。頭部標簽<head>中指明了編碼字符,由Scrapy對其處理,就不用我們浪費精力了。

樹結構

不同的瀏覽器有不同的借以呈現網頁的內部數據結構。但DOM樹是跨平臺且不依賴語言的,可以被幾乎所有瀏覽器支持。

只需右鍵點擊,選擇查看元素,就可以在瀏覽器中查看網頁的樹結構。如果這項功能被禁止了,可以在選項的開發者工具中修改。

你看到的樹結構和HTML很像,但不完全相同。無論原始HTML文件使用了多少空格和換行符,樹結構看起來都會是一樣的。你可以點擊任意元素,或是改變屬性,這樣可以實時看到對HTML網頁產生了什么變化。例如,如果你雙擊了一段文字,并修改了它,然后點擊回車,屏幕上這段文字就會根據新的設置發生改變。在右邊的方框中,在屬性標簽下面,你可以看到這個樹結構的屬性列表。在頁面底部,你可以看到一個面包屑路徑,指示著選中元素的所在位置。


重要的是記住,HTML是文本,而樹結構是瀏覽器內存中的一個對象,你可以通過程序查看、操作這個對象。在Chrome瀏覽器中,就是通過開發者工具查看。

瀏覽器中的頁面

HTML文本和樹結構和我們平時在瀏覽器中看到的頁面截然不同。這恰恰是HTML的成功之處。HTML文件就是要具有可讀性,可以區分網頁的內容,但不是按照呈現在屏幕上的方式。這意味著,呈現HTML文檔、進行美化都是瀏覽器的職責,無論是對于功能齊備的Chrome、移動端瀏覽器、還是Lynx這樣的文本瀏覽器。

也就是說,網頁的發展對網頁開發者和用戶都提出了極大的開發網頁方面的需求。CSS就是這樣被發明出來,用以服務HTML元素。對于Scrapy,我們不涉及CSS。

既然如此,樹結構對呈現出來的網頁有什么作用呢?答案就是盒模型。正如DOM樹可以包含其它元素或是文字,同樣的,盒模型里面也可以內嵌其它內容。所以,我們在屏幕上看到的網頁是原始HTML的二維呈現。樹結構是其中的一維,但它是隱藏的。例如,在下圖中,我們看到三個DOM元素,一個<div>和兩個內嵌的<h1>和<p>,出現在瀏覽器和DOM中:

用XPath選擇HTML元素
如果你以前接觸過傳統的軟件工程,并不知道XPath,你可能會擔心,在HTML文檔中查詢某個信息,要進行復雜的字符串匹配、搜索標簽、處理特殊字符、解析整個樹結構等繁瑣工作。對于XPath,所有的這些都不是問題,你可以輕松提取元素、屬性或是文字。

在Chrome中使用XPath,在開發者工具中點擊控制臺標簽,使用$x功能。例如,在網頁http://example.com/的控制臺,輸入$x('//h1'),就可以移動到<h1>元素,如截圖所示:


你在控制臺中看到的是一個包含所選元素的JavaScript數組。如果你將光標移動到這個數組上,你可以看到被選擇的元素被高亮顯示。這個功能很有用。

XPath表達式
HTML文檔的層級結構的最高級是<html>標簽,你可以使用元素名和斜杠線選擇任意元素。例如,下面的表達式返回了http://example.com/上對應的內容:

$x('/html')
  [ <html>...</html> ]
$x('/html/body')
  [ <body>...</body> ]
$x('/html/body/div')
  [ <div>...</div> ]
$x('/html/body/div/h1')
  [ <h1>Example Domain</h1> ]
$x('/html/body/div/p')
  [ <p>...</p>, <p>...</p> ]
$x('/html/body/div/p[1]')
  [ <p>...</p> ]
$x('/html/body/div/p[2]')
  [ <p>...</p> ]

注意,<p>標簽在<div>標簽內有兩個,所以會返回兩個。你可以用p[1]和p[2]分別返回兩個元素。

從抓取的角度,文檔的標題或許是唯一讓人感興趣的,它位于文檔的頭部,可以用下面的額表達式找到:

$x('//html/head/title')
  [ <title>Example Domain</title> ]

對于大文檔,你可能要寫很長的XPath表達式,以獲取所要的內容。為了避免這點,兩個斜杠線//可以讓你訪問到所有的同名元素。例如,//p可以選擇所有的p元素,//a可以選擇所有的鏈接。

$x('//p')
  [ <p>...</p>, <p>...</p> ]
$x('//a')
  [ <a >More information...</a> ]

//a可以用在更多的地方。例如,如果要找到所有<div>標簽的鏈接,你可以使用//div//a。如果a前面只有一個斜杠,//div/a會返回空,因為在上面的例子中<div>標簽下面沒有<a>。

$x('//div//a')
  [ <a >More information...</a> ]
$x('//div/a')
  [ ]

你也可以選擇屬性。http://example.com/上唯一的屬性是鏈接href,可以通過下面的方式找到:

$x('//a/@href')
[]

你也可以只通過text( )函數選擇文字:

$x('//a/text()')
["More information..."]

可以使用*標志選擇某層下所有的元素,例如:

$x('//div/*')
[<h1>Example Domain</h1>, <p>...</p>, <p>...</p>]

尋找特定屬性,例如@class、或屬性有特定值時,你會發現XPath非常好用。例如,//a[@href]可以找到所有鏈接,//a[@href="http://www.iana.org/domains/example"]則進行了指定的選擇。
當屬性值中包含特定字符串時,XPath會極為方便。例如,

$x('//a[@href]')
[<a >More information...</a>]
$x('//a[@)
[<a >More information...</a>]
$x('//a[contains(@href, "iana")]')
[<a >More information...</a>]
$x('//a[starts-with(@href, "http://www.")]')
[<a >More information...</a>]
$x('//a[not(contains(@href, "abc"))]')
[ <a >More information...</a>]

http://www.w3schools.com/xsl/xsl_functions.asp在線文檔中你可以找到更多類似的函數,但并非都常用。

在Scrapy終端中可以使用同樣的命令,在命令行中輸入

scrapy shell "http://example.com"

終端會向你展示許多寫爬蟲時碰到的變量。其中最重要的是響應,在HTML中是HtmlResponse,這個類可以讓你在Chrome使用xpath( )方法$x。下面是一些例子:

response.xpath('/html').extract()
  [u'<html><head><title>...</body></html>']
response.xpath('/html/body/div/h1').extract()
  [u'<h1>Example Domain</h1>']
response.xpath('/html/body/div/p').extract()
  [u'<p>This domain ... permission.</p>', u'<p><a >More information...</a></p>']
response.xpath('//html/head/title').extract()
  [u'<title>Example Domain</title>']
response.xpath('//a').extract()
  [u'<a >More information...</a>']
response.xpath('//a/@href').extract()
  [u'http://www.iana.org/domains/example']
response.xpath('//a/text()').extract()
  [u'More information...']
response.xpath('//a[starts-with(@href, "http://www.")]').extract()
  [u'<a >More information...</a>']

這意味著,你可用Chrome瀏覽器生成XPath表達式,以便在Scrapy爬蟲中使用。

使用Chrome瀏覽器獲得XPath表達式

Chrome瀏覽器可以幫助我們獲取XPath表達式這點確實對開發者非常友好。像之前演示的那樣檢查一個元素:右鍵選擇一個元素,選擇檢查元素。開發者工具被打開,該元素在HTML的樹結構中被高亮顯示,可以在右鍵打開的菜單中選擇Copy XPath,表達式就復制到粘貼板中了。


你可以在控制臺中檢測表達式:

$x('/html/body/div/p[2]/a')
[<a >More information...</a>]

常見工作

下面展示一些XPath表達式的常見使用。先來看看在維基百科上是怎么使用的。維基百科的頁面非常穩定,不會在短時間內改變排版。

  • 取得id為firstHeading的div下的span的text:
//h1[@id="firstHeading"]/span/text()
  • 取得id為toc的div下的ul內的URL:
//div[@id="toc"]/ul//a/@href
  • 在任意class包含ltr和class包含skin-vector的元素之內,取得h1的text,這兩個字符串可能在同一class內,或不在。
//*[contains(@class,"ltr") and contains(@class,"skin-vector")]//h1//text()

實際應用中,你會在XPath中頻繁地使用class。在這幾個例子中,你需要記住,因為CSS的板式原因,你會看到HTML的元素總會包含許多特定的class屬性。這意味著,有的<div>的class是link,其他導航欄的<div>的class就是link active。后者是當前生效的鏈接,因此是可見或是用CSS特殊色高亮顯示的。當抓取的時候,你通常是對含有某個屬性的元素感興趣的,就像之前的link和link active。XPath的contains( )函數就可以幫你選擇包含某一class的所有元素。

  • 選擇class屬性是infobox的table的第一張圖片的URL:
//table[@class="infobox"]//img[1]/@src
  • 選擇class屬性是reflist開頭的div下面的所有URL鏈接:
//div[starts-with(@class,"reflist")]//a/@href
  • 選擇div下面的所有URL鏈接,并且這個div的下一個相鄰元素的子元素包含文字References:
//*[text()="References"]/../following-sibling::div//a
  • 取得所有圖片的URL:
//img/@src

提前應對網頁發生改變

爬取的目標常常位于遠程服務器。這意味著,如果它的HTML發生了改變,XPath表達式就無效了,我們就不得不回過頭修改爬蟲的程序。因為網頁的改變一般就很少,爬蟲的改動往往不會很大。然而,我們還是寧肯不要回頭修改。一些基本原則可以幫助我們降低表達式失效的概率:

  • 避免使用數組序號
    Chrome常常會在表達式中加入許多常數
//*[@id="myid"]/div/div/div[1]/div[2]/div/div[1]/div[1]/a/img

如果HTML上有一個廣告窗的話,就會改變文檔的結構,這個表達式就會失效。解決的方法是,盡量找到離img標簽近的元素,根據該元素的id或class屬性,進行抓取,例如:

//div[@class="thumbnail"]/a/img
  • 用class抓取效果不一定好
    使用class屬性可以方便的定位要抓取的元素,但是因為CSS也要通過class修改頁面的外觀,所以class屬性可能會發生改變,例如下面用到的class:
//div[@class="thumbnail"]/a/img

過一段時間之后,可能會變成:

//div[@class="preview green"]/a/img
  • 數據指向的class優于排版指向的class
    在上一個例子中,使用thumbnail和green兩個class都不好。thumbnail比green好,但這兩個都不如departure-time。前面兩個是用來排版的,departure-time是有語義的,和div中的內容有關。所以,在排版發生改變的情況下,departure-time發生改變的可能性會比較小。應該說,網站作者在開發中十分清楚,為內容設置有意義的、一致的標記,可以讓開發過程收益。

  • id通常是最可靠的
    只要id具有語義并且數據相關,id通常是抓取時最好的選擇。部分原因是,JavaScript和外鏈錨點總是使用id獲取文檔中特定的部分。例如,下面的XPath非??煽浚?/p>

//*[@id="more_info"]//text( )

相反的例子是,指向唯一參考的id,對抓取沒什么幫助,因為抓取總是希望能夠獲取具有某個特點的所有信息。例如:

//[@id="order-F4982322"]

這是一個非常差的XPath表達式。還要記住,盡管id最好要有某種特點,但在許多HTML文檔中,id都很雜亂無章。

總結
編程語言的不斷進化,使得創建可靠的XPath表達式從HTML抓取信息變得越來越容易。在本章中,你學到了HTML和XPath的基本知識、如何利用Chrome自動獲取XPath表達式。你還學會了如何手工寫XPath表達式,并區分可靠和不夠可靠的XPath表達式。第3章中,我們會用這些知識來寫幾個爬蟲。


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎
第4章 從Scrapy到移動應用
第5章 快速構建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容