非結構化數據和結構化數據提取

頁面解析和數據提取javascript

通常來說對咱們而言,須要抓取的是某個網站或者某個應用的內容,提取有用的價值。內容通常分爲兩部分,非結構化的數據 結構化的數據php

  • 非結構化數據:先有數據,再有結構,
  • 結構化數據:先有結構、再有數據
  • 不一樣類型的數據,咱們須要採用不一樣的方式來處理。

非結構化的數據處理

文本、電話號碼、郵箱地址
  • 正則表達式
HTML 文件
  • 正則表達式
  • XPath
  • CSS選擇器

結構化的數據處理

JSON 文件
  • JSON Path
  • 轉化成Python類型進行操做(json類)
XML 文件
  • 轉化成Python類型(xmltodict)
  • XPath
  • CSS選擇器
  • 正則表達式

爲何要學正則表達式

實際上爬蟲一共就四個主要步驟css

  1. 明確目標 (要知道你準備在哪一個範圍或者網站去搜索)
  2. 爬 (將全部的網站的內容所有爬下來)
  3. 取 (去掉對咱們沒用處的數據)
  4. 處理數據(按照咱們想要的方式存儲和使用)

咱們在昨天的案例裏實際上省略了第3步,也就是"取"的步驟。由於咱們down下了的數據是所有的網頁,這些數據很龐大而且很混亂,大部分的東西使咱們不關心的,所以咱們須要將之按咱們的須要過濾和匹配出來。html

那麼對於文本的過濾或者規則的匹配,最強大的就是正則表達式,是Python爬蟲世界裏必不可少的神兵利器。java

什麼是正則表達式

正則表達式,又稱規則表達式,一般被用來檢索、替換那些符合某個模式(規則)的文本。node

正則表達式是對字符串操做的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個「規則字符串」,這個「規則字符串」用來表達對字符串的一種過濾邏輯。python

給定一個正則表達式和另外一個字符串,咱們能夠達到以下的目的:程序員

  • 給定的字符串是否符合正則表達式的過濾邏輯(「匹配」);
  • 經過正則表達式,從文本字符串中獲取咱們想要的特定部分(「過濾」)。

正則表達式匹配規則

Python 的 re 模塊

在 Python 中,咱們可使用內置的 re 模塊來使用正則表達式web

有一點須要特別注意的是,正則表達式使用 對特殊字符進行轉義,因此若是咱們要使用原始字符串,只需加一個 r 前綴,示例:正則表達式

r'chuanzhiboke\t\.\tpython'

re 模塊的通常使用步驟以下:

  1. 使用 compile() 函數將正則表達式的字符串形式編譯爲一個 Pattern 對象

  2. 經過 Pattern 對象提供的一系列方法對文本進行匹配查找,得到匹配結果,一個 Match 對象

  3. 最後使用 Match 對象提供的屬性和方法得到信息,根據須要進行其餘的操做

compile 函數

compile 函數用於編譯正則表達式,生成一個 Pattern 對象,它的通常使用形式以下:

import re

# 將正則表達式編譯成 Pattern 對象
pattern = re.compile(r'\d+')

在上面,咱們已將一個正則表達式編譯成 Pattern 對象,接下來,咱們就能夠利用 pattern 的一系列方法對文本進行匹配查找了。

Pattern 對象的一些經常使用方法主要有:

  • match 方法:從起始位置開始查找,一次匹配
  • search 方法:從任何位置開始查找,一次匹配
  • findall 方法:所有匹配,返回列表
  • finditer 方法:所有匹配,返回迭代器
  • split 方法:分割字符串,返回列表
  • sub 方法:替換

match 方法

match 方法用於查找字符串的頭部(也能夠指定起始位置),它是一次匹配,只要找到了一個匹配的結果就返回,而不是查找全部匹配的結果。它的通常使用形式以下:

match(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可選參數,指定字符串的起始和終點位置,默認值分別是 0 和 len (字符串長度)。所以,當你不指定 pos 和 endpos 時,match 方法默認匹配字符串的頭部。

當匹配成功時,返回一個 Match 對象若是沒有匹配上,則返回 None

>>> import re
>>> pattern = re.compile(r'\d+')  # 用於匹配至少一個數字
>>> m = pattern.match('one12twothree34four')  # 查找頭部,沒有匹配
>>> print m
None 
>>> m = pattern.match('one12twothree34four', 2, 10) # 從'e'的位置開始匹配,沒有匹配
>>> print m
None
>>> m = pattern.match('one12twothree34four', 3, 10) # 從'1'的位置開始匹配,正好匹配
>>> print m                                         # 返回一個 Match 對象
<_sre.SRE_Match object at 0x10a42aac0> 
>>> m.group(0)   # 可省略 0
'12' 
>>> m.start(0)   # 可省略 0
3 
>>> m.end(0)     # 可省略 0
5 
>>> m.span(0)    # 可省略 0
(3, 5) 

在上面,當匹配成功時返回一個 Match 對象,其中:

  • group([group1, …]) 方法用於得到一個或多個分組匹配的字符串,當要得到整個匹配的子串時,可直接使用 group() 或 group(0);

  • start([group]) 方法用於獲取分組匹配的子串在整個字符串中的起始位置子串第一個字符的索引),參數默認值爲 0;

  • end([group]) 方法用於獲取分組匹配的子串在整個字符串中的結束位置子串最後一個字符的索引+1),參數默認值爲 0;
  • span([group]) 方法返回 (start(group), end(group))。

再看看一個例子:

>>> import re
>>> pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I)  # re.I 表示忽略大小寫
>>> m = pattern.match('Hello World Wide Web')

>>> print m     # 匹配成功,返回一個 Match 對象
<_sre.SRE_Match object at 0x10bea83e8> 
>>> m.group(0)  # 返回匹配成功的整個子串
'Hello World' 
>>> m.span(0)   # 返回匹配成功的整個子串的索引
(0, 11) 
>>> m.group(1)  # 返回第一個分組匹配成功的子串
'Hello' 
>>> m.span(1)   # 返回第一個分組匹配成功的子串的索引
(0, 5) 
>>> m.group(2)  # 返回第二個分組匹配成功的子串
'World' 
>>> m.span(2)   # 返回第二個分組匹配成功的子串
(6, 11) 
>>> m.groups()  # 等價於 (m.group(1), m.group(2), ...)
('Hello', 'World') 
>>> m.group(3)   # 不存在第三個分組 
Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: no such group 
------------------------------------------------------------------------------------------------------

search 方法

search 方法用於查找字符串的任何位置,它也是一次匹配,只要找到了一個匹配的結果就返回,而不是查找全部匹配的結果,它的通常使用形式以下:

search(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可選參數,指定字符串的起始和終點位置,默認值分別是 0 和 len (字符串長度)。

當匹配成功時,返回一個 Match 對象,若是沒有匹配上,則返回 None

讓咱們看看例子:

>>> import re
>>> pattern = re.compile('\d+')
>>> m = pattern.search('one12twothree34four')  # 這裏若是使用 match 方法則不匹配
>>> m
<_sre.SRE_Match object at 0x10cc03ac0>
>>> m.group()
'12'
>>> m = pattern.search('one12twothree34four', 10, 30)  # 指定字符串區間
>>> m
<_sre.SRE_Match object at 0x10cc03b28>
>>> m.group()
'34'
>>> m.span()
(13, 15) 

再來看一個例子:

# -*- coding: utf-8 -*-

import re
# 將正則表達式編譯成 Pattern 對象
pattern = re.compile(r'\d+')
# 使用 search() 查找匹配的子串,不存在匹配的子串時將返回 None
# 這裏使用 match() 沒法成功匹配
m = pattern.search('hello 123456 789')
if m:
    # 使用 Match 得到分組信息
    print 'matching string:',m.group()
    # 起始位置和結束位置
    print 'position:',m.span()

執行結果:

matching string: 123456
position: (6, 12)
------------------------------------------------------------------------------------------------------

findall 方法

上面的 match 和 search 方法都是一次匹配,只要找到了一個匹配的結果就返回。然而,在大多數時候,咱們須要搜索整個字符串,得到全部匹配的結果。

findall 方法的使用形式以下:

findall(string[, pos[, endpos]])

其中,string 是待匹配的字符串,pos 和 endpos 是可選參數,指定字符串的起始和終點位置,默認值分別是 0 和 len (字符串長度)。

findall 以列表形式返回全部能匹配的子串若是沒有匹配,則返回一個空列表

看看例子:

import re
pattern = re.compile(r'\d+')   # 查找數字

result1 = pattern.findall('hello 123456 789')
result2 = pattern.findall('one1two2three3four4', 0, 10)

print result1
print result2

執行結果:

['123456', '789']
['1', '2']

再先看一個栗子:

# re_test.py

import re

#re模塊提供一個方法叫compile模塊,提供咱們輸入一個匹配的規則
#而後返回一個pattern實例,咱們根據這個規則去匹配字符串
pattern = re.compile(r'\d+\.\d*')

#經過partten.findall()方法就可以所有匹配到咱們獲得的字符串
result = pattern.findall("123.141593, 'bigcat', 232312, 3.15")

#findall 以 列表形式 返回所有能匹配的子串給result
for item in result:
    print item

 

運行結果:

123.141593
3.15

 

------------------------------------------------------------------------------------------------------

finditer 方法

finditer 方法的行爲跟 findall 的行爲相似,也是搜索整個字符串,得到全部匹配的結果。但它返回一個順序訪問每個匹配結果(Match 對象)的迭代器

看看例子:

# -*- coding: utf-8 -*-

import re
pattern = re.compile(r'\d+')

result_iter1 = pattern.finditer('hello 123456 789')
result_iter2 = pattern.finditer('one1two2three3four4', 0, 10)

print type(result_iter1)
print type(result_iter2)

print 'result1...'
for m1 in result_iter1:   # m1 是 Match 對象
    print 'matching string: {}, position: {}'.format(m1.group(), m1.span())

print 'result2...'
for m2 in result_iter2:
    print 'matching string: {}, position: {}'.format(m2.group(), m2.span())

執行結果:

<type 'callable-iterator'>
<type 'callable-iterator'>
result1...
matching string: 123456, position: (6, 12)
matching string: 789, position: (13, 16)
result2...
matching string: 1, position: (3, 4)
matching string: 2, position: (7, 8)
------------------------------------------------------------------------------------------------------

split 方法

split 方法按照可以匹配的子串將字符串分割後返回列表,它的使用形式以下:

split(string[, maxsplit]) 

其中,maxsplit 用於指定最大分割次數,不指定將所有分割。

看看例子:

import re
p = re.compile(r'[\s\,\;]+')
print p.split('a,b;; c   d')

執行結果:

['a', 'b', 'c', 'd']
------------------------------------------------------------------------------------------------------

sub 方法

sub 方法用於替換。它的使用形式以下:

sub(repl, string[, count])

 

其中,repl 能夠是字符串也能夠是一個函數:

  • 若是 repl 是字符串,則會使用 repl 去替換字符串每個匹配的子串,並返回替換後的字符串,另外,repl 還可使用 id 的形式來引用分組,但不能使用編號 0;

  • 若是 repl 是函數,這個方法應當只接受一個參數(Match 對象),並返回一個字符串用於替換(返回的字符串中不能再引用分組)。

  • count 用於指定最多替換次數,不指定時所有替換。

看看例子:

import re
p = re.compile(r'(\w+) (\w+)') # \w = [A-Za-z0-9]
s = 'hello 123, hello 456'

print p.sub(r'hello world', s)  # 使用 'hello world' 替換 'hello 123' 和 'hello 456'
print p.sub(r'\2 \1', s)        # 引用分組

def func(m):
    return 'hi' + ' ' + m.group(2)

print p.sub(func, s)
print p.sub(func, s, 1)         # 最多替換一次 

執行結果:

hello world, hello world
123 hello, 456 hello
hi 123, hi 456
hi 123, hello 456
------------------------------------------------------------------------------------------------------

匹配中文

在某些狀況下,咱們想匹配文本中的漢字,有一點須要注意的是,中文的 unicode 編碼範圍 主要在 [u4e00-u9fa5],這裏說主要是由於這個範圍並不完整,好比沒有包括全角(中文)標點,不過,在大部分狀況下,應該是夠用的。

假設如今想把字符串 title = u'你好,hello,世界' 中的中文提取出來,能夠這麼作:

import re

title = u'你好,hello,世界'
pattern = re.compile(ur'[\u4e00-\u9fa5]+')
result = pattern.findall(title)

print result

 

注意到,咱們在正則表達式前面加上了兩個前綴 ur,其中 r 表示使用原始字符串,u 表示是 unicode 字符串。

執行結果:

[u'\u4f60\u597d', u'\u4e16\u754c']

 

注意:貪婪模式與非貪婪模式

  1. 貪婪模式:在整個表達式匹配成功的前提下,儘量多的匹配 ( * );
  2. 非貪婪模式:在整個表達式匹配成功的前提下,儘量少的匹配 ( ? );
  3. Python裏數量詞默認是貪婪的。

示例一 : 源字符串:abbbc

  • 使用貪婪的數量詞的正則表達式 ab* ,匹配結果: abbb。

    * 決定了儘量多匹配 b,因此a後面全部的 b 都出現了。

  • 使用非貪婪的數量詞的正則表達式ab*?,匹配結果: a。

    即便前面有 *,可是 ? 決定了儘量少匹配 b,因此沒有 b。

示例二 : 源字符串:aa<div>test1</div>bb<div>test2</div>cc

  • 使用貪婪的數量詞的正則表達式:<div>.*</div>

  • 匹配結果:<div>test1</div>bb<div>test2</div>

這裏採用的是貪婪模式。在匹配到第一個「</div>」時已經可使整個表達式匹配成功,可是因爲採用的是貪婪模式,因此仍然要向右嘗試匹配,查看是否還有更長的能夠成功匹配的子串。匹配到第二個「</div>」後,向右再沒有能夠成功匹配的子串,匹配結束,匹配結果爲「<div>test1</div>bb<div>test2</div>


  • 使用非貪婪的數量詞的正則表達式:<div>.*?</div>

  • 匹配結果:<div>test1</div>

正則表達式二採用的是非貪婪模式,在匹配到第一個「</div>」時使整個表達式匹配成功,因爲採用的是非貪婪模式,因此結束匹配,再也不向右嘗試,匹配結果爲「<div>test1</div>」。

正則表達式測試網址

 

案例:使用正則表達式的爬蟲

如今擁有了正則表達式這把神兵利器,咱們就能夠進行對爬取到的所有網頁源代碼進行篩選了。

下面咱們一塊兒嘗試一下爬取內涵段子網站: http://www.neihan8.com/article/list_5_1.html

打開以後,不難看到裏面一個一個灰常有內涵的段子,當你進行翻頁的時候,注意url地址的變化:

  • 第一頁url: http: //www.neihan8.com/article/list_5_1 .html

  • 第二頁url: http: //www.neihan8.com/article/list_5_2 .html

  • 第三頁url: http: //www.neihan8.com/article/list_5_3 .html

  • 第四頁url: http: //www.neihan8.com/article/list_5_4 .html

這樣咱們的url規律找到了,要想爬取全部的段子,只須要修改一個參數便可。 下面咱們就開始一步一步將全部的段子爬取下來吧。

第一步:獲取數據

1. 按照咱們以前的用法,咱們須要寫一個加載頁面的方法。

這裏咱們統必定義一個類,將url請求做爲一個成員方法處理。

咱們建立一個文件,叫duanzi_spider.py

而後定義一個Spider類,而且添加一個加載頁面的成員方法

import urllib2

class Spider:
    """
        內涵段子爬蟲類
    """
    def loadPage(self, page):
        """
            @brief 定義一個url請求網頁的方法
            @param page 須要請求的第幾頁
            @returns 返回的頁面html
        """

    url = "http://www.neihan8.com/article/list_5_" + str(page)
+ ".html"
    #User-Agent頭
    user_agent = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT
6.1; Trident/5.0'

    headers = {'User-Agent': user_agent}
    req = urllib2.Request(url, headers = headers)
    response = urllib2.urlopen(req)
    html = response.read()
    print html

    #return html

 

以上的loadPage的實現體想必你們應該很熟悉了,須要注意定義python類的成員方法須要額外添加一個參數self.

  • 那麼loadPage(self, page) 中的page是咱們指定去請求第幾頁。

  • 最後經過 print html打印到屏幕上。

  • 而後咱們寫一個main函數見到測試一個loadPage方法

2. 寫main函數測試一個loadPage方法
if __name__ == '__main__':
    """
        ======================
            內涵段子小爬蟲
        ======================
    """
    print '請按下回車開始'
    raw_input()

    #定義一個Spider對象
    mySpider = Spider()
    mySpider.loadpage(1)

 

  • 程序正常執行的話,咱們會在屏幕上打印了內涵段子第一頁的所有html代碼。 可是咱們發現,html中的中文部分顯示的多是亂碼 。

那麼咱們須要簡單的將獲得的網頁源代碼處理一下:

def loadPage(self, page):
    """
        @brief 定義一個url請求網頁的方法
        @param page 須要請求的第幾頁
        @returns 返回的頁面html
    """

    url = "http://www.neihan8.com/article/list_5_" + str(page)
+ ".html"
    #User-Agent頭
    user_agent = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT
6.1; Trident/5.0'
    headers = {'User-Agent': user_agent}
    req = urllib2.Request(url, headers = headers)
    response = urllib2.urlopen(req)
    html = response.read()
    gbk_html = html.decode('gbk').encode('utf-8')
    # print gbk_html
    return gbk_html

 

注意 :對於每一個網站對中文的編碼各自不一樣,因此html.decode(‘gbk’)的寫法並非通用寫法,根據網站的編碼而異

  • 這樣咱們再次執行如下duanzi_spider.py ,會發現以前的中文亂碼能夠正常顯示了。

第二步:篩選數據

接下來咱們已經獲得了整個頁面的數據。 可是,不少內容咱們並不關心,因此下一步咱們須要進行篩選。 如何篩選,就用到了上一節講述的正則表達式。

  • 首先
import re

 

  • 而後, 在咱們獲得的gbk_html中進行篩選匹配。

咱們須要一個匹配規則:

咱們能夠打開內涵段子的網頁,鼠標點擊右鍵 「 查看源代碼 」 你會驚奇的發現,咱們須要的每一個段子的內容都是在一個 <div>標籤中,並且每一個div都有一個屬性class = "f18 mb20"

因此,咱們只須要匹配到網頁中全部<div class="f18 mb20"> 到 </div> 的數據就能夠了。

根據正則表達式,咱們能夠推算出一個公式是:
<div.*?class="f18 mb20">(.*?)</div>

 

  • 這個表達式實際上就是匹配到全部divclass="f18 mb20 裏面的內容(具體能夠看前面正則介紹)

  • 而後將這個正則應用到代碼中,咱們會獲得如下代碼:

def loadPage(self, page):
    """
        @brief 定義一個url請求網頁的方法
        @param page 須要請求的第幾頁
        @returns 返回的頁面html
    """

    url = "http://www.neihan8.com/article/list_5_" + str(page)
+ ".html"
    #User-Agent頭
    user_agent = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT
 
6.1; Trident/5.0'
    headers = {'User-Agent': user_agent}
    req = urllib2.Request(url, headers = headers)
    response = urllib2.urlopen(req)
    html = response.read()
    gbk_html = html.decode('gbk').encode('utf-8')

    #找到全部的段子內容<div class = "f18 mb20"></div>
    #re.S 若是沒有re.S 則是隻匹配一行有沒有符合規則的字符串,若是沒有則下一行從新匹配
    # 若是加上re.S 則是將全部的字符串將一個總體進行匹配
    pattern = re.compile(r'<div.*?class="f18 mb20">(.*?)</di
v>', re.S)
    item_list = pattern.findall(gbk_html)

    return item_list


def printOnePage(self, item_list, page):
    """
        @brief 處理獲得的段子列表
        @param item_list 獲得的段子列表
        @param page 處理第幾頁
    """

    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        print "================"
        print ite
 
  • 這裏須要注意一個是re.S是正則表達式中匹配的一個參數。

  • 若是 沒有re.S 則是 只匹配一行 有沒有符合規則的字符串,若是沒有則下一行從新匹配。

  • 若是 加上re.S 則是將 全部的字符串 將一個總體進行匹配,findall 將全部匹配到的結果封裝到一個list中。
  • 而後咱們寫了一個遍歷item_list的一個方法 printOnePage() 。 ok程序寫到這,咱們再一次執行一下。
Power@PowerMac ~$ python duanzi_spider.py
咱們第一頁的所有段子,不包含其餘信息所有的打印了出來。
  • 你會發現段子中有不少 <p> , </p> 非常不舒服,實際上這個是html的一種段落的標籤。
  • 在瀏覽器上看不出來,可是若是按照文本打印會有<p>出現,那麼咱們只須要把咱們不但願的內容去掉便可了。

  • 咱們能夠以下簡單修改一下 printOnePage().

def printOnePage(self, item_list, page):
    """
        @brief 處理獲得的段子列表
        @param item_list 獲得的段子列表
        @param page 處理第幾頁
    """

    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        print "================"
        item = item.replace("<p>", "").replace("</p>", "").repl
ace("<br />", "")
        print item

 


第三步:保存數據

  • 咱們能夠將全部的段子存放在文件中。好比,咱們能夠將獲得的每一個item不是打印出來,而是存放在一個叫 duanzi.txt 的文件中也能夠。
def writeToFile(self, text):
'''
    @brief 將數據追加寫進文件中
    @param text 文件內容
'''
    myFile = open("./duanzi.txt", 'a') #追加形式打開文件
    myFile.write(text)
    myFile.write("---------------------------------------------
--------")
    myFile.close()

 

  • 而後咱們將print的語句 改爲writeToFile() ,當前頁面的全部段子就存在了本地的MyStory.txt文件中。
def printOnePage(self, item_list, page):
'''
    @brief 處理獲得的段子列表
    @param item_list 獲得的段子列表
    @param page 處理第幾頁
'''
    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        # print "================"
        item = item.replace("<p>", "").replace("</p>", "").repl
ace("<br />", "")
        # print item
        self.writeToFile(item)

 

第四步:顯示數據

  • 接下來咱們就經過參數的傳遞對page進行疊加來遍歷 內涵段子吧的所有段子內容。

  • 只須要在外層加一些邏輯處理便可。

def doWork(self):
'''
    讓爬蟲開始工做
'''
    while self.enable:
        try:
            item_list = self.loadPage(self.page)
        except urllib2.URLError, e:
            print e.reason
            continue

        #對獲得的段子item_list處理
        self.printOnePage(item_list, self.page)
        self.page += 1 #此頁處理完畢,處理下一頁
        print "按回車繼續..."
        print "輸入 quit 退出"
        command = raw_input()
        if (command == "quit"):
            self.enable = False
            break

 

  • 最後,咱們執行咱們的代碼,完成後查看當前路徑下的duanzi.txt文件,裏面已經有了咱們要的內涵段子。

以上即是一個很是精簡使用的小爬蟲程序,使用起來非常方便,若是想要爬取其餘網站的信息,只須要修改其中某些參數和一些細節就好了。

有同窗說,我正則用的很差,處理HTML文檔很累,有沒有其餘的方法?

有!那就是XPath,咱們能夠先將 HTML文件 轉換成 XML文檔,而後用 XPath 查找 HTML 節點或元素。

什麼是XML

  • XML 指可擴展標記語言(EXtensible Markup Language)
  • XML 是一種標記語言,很相似 HTML
  • XML 的設計宗旨是傳輸數據,而非顯示數據
  • XML 的標籤須要咱們自行定義。
  • XML 被設計爲具備自我描述性。
  • XML 是 W3C 的推薦標準

W3School官方文檔:http://www.w3school.com.cn/xml/index.asp

XML 和 HTML 的區別

數據格式 描述 設計目標
XML Extensible Markup Language (可擴展標記語言) 被設計爲傳輸和存儲數據,其焦點是數據的內容。
HTML HyperText Markup Language (超文本標記語言) 顯示數據以及如何更好顯示數據
HTML DOM Document Object Model for HTML (文檔對象模型) 經過 HTML DOM,能夠訪問全部的 HTML 元素,連同它們所包含的文本和屬性。能夠對其中的內容進行修改和刪除,同時也能夠建立新的元素。
XML文檔示例
<?xml version="1.0" encoding="utf-8"?>

<bookstore> 

  <book category="cooking"> 
    <title lang="en">Everyday Italian</title>  
    <author>Giada De Laurentiis</author>  
    <year>2005</year>  
    <price>30.00</price> 
  </book>  

  <book category="children"> 
    <title lang="en">Harry Potter</title>  
    <author>J K. Rowling</author>  
    <year>2005</year>  
    <price>29.99</price> 
  </book>  

  <book category="web"> 
    <title lang="en">XQuery Kick Start</title>  
    <author>James McGovern</author>  
    <author>Per Bothner</author>  
    <author>Kurt Cagle</author>  
    <author>James Linn</author>  
    <author>Vaidyanathan Nagarajan</author>  
    <year>2003</year>  
    <price>49.99</price> 
  </book> 

  <book category="web" cover="paperback"> 
    <title lang="en">Learning XML</title>  
    <author>Erik T. Ray</author>  
    <year>2003</year>  
    <price>39.95</price> 
  </book> 

</bookstore>
HTML DOM 模型示例

HTML DOM 定義了訪問和操做 HTML 文檔的標準方法,以樹結構方式表達 HTML 文檔。

XML的節點關係

1. 父(Parent)

每一個元素以及屬性都有一個父。

下面是一個簡單的XML例子中,book 元素是 title、author、year 以及 price 元素的父:

<?xml version="1.0" encoding="utf-8"?>

<book>
  <title>Harry Potter</title>
  <author>J K. Rowling</author>
  <year>2005</year>
  <price>29.99</price>
</book>

2. 子(Children)

元素節點可有零個、一個或多個子。

在下面的例子中,title、author、year 以及 price 元素都是 book 元素的子:

<?xml version="1.0" encoding="utf-8"?>

<book>
  <title>Harry Potter</title>
  <author>J K. Rowling</author>
  <year>2005</year>
  <price>29.99</price>
</book>

3. 同胞(Sibling)

擁有相同的父的節點

在下面的例子中,title、author、year 以及 price 元素都是同胞:

<?xml version="1.0" encoding="utf-8"?>

<book>
  <title>Harry Potter</title>
  <author>J K. Rowling</author>
  <year>2005</year>
  <price>29.99</price>
</book>

4. 先輩(Ancestor)

某節點的父、父的父,等等。

在下面的例子中,title 元素的先輩是 book 元素和 bookstore 元素:

<?xml version="1.0" encoding="utf-8"?>

<bookstore>

<book>
  <title>Harry Potter</title>
  <author>J K. Rowling</author>
  <year>2005</year>
  <price>29.99</price>
</book>

</bookstore>

5. 後代(Descendant)

某個節點的子,子的子,等等。

在下面的例子中,bookstore 的後代是 book、title、author、year 以及 price 元素:

<?xml version="1.0" encoding="utf-8"?>

<bookstore>

<book>
  <title>Harry Potter</title>
  <author>J K. Rowling</author>
  <year>2005</year>
  <price>29.99</price>
</book>

</bookstore>

什麼是XPath?

XPath (XML Path Language) 是一門在 XML 文檔中查找信息的語言,可用來在 XML 文檔中對元素和屬性進行遍歷。

W3School官方文檔:http://www.w3school.com.cn/xpath/index.asp

XPath 開發工具

  1. 開源的XPath表達式編輯工具:XMLQuire(XML格式文件可用)
  2. Chrome插件 XPath Helper
  3. Firefox插件 XPath Checker

選取節點

XPath 使用路徑表達式來選取 XML 文檔中的節點或者節點集。這些路徑表達式和咱們在常規的電腦文件系統中看到的表達式很是類似。

下面列出了最經常使用的路徑表達式:

表達式 描述
nodename 選取此節點的全部子節點。
/ 從根節點選取。
// 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。
. 選取當前節點。
.. 選取當前節點的父節點。
@ 選取屬性。

在下面的表格中,咱們已列出了一些路徑表達式以及表達式的結果:

  路徑表達式 結果
bookstore 選取 bookstore 元素的全部子節點。
/bookstore 選取根元素 bookstore。註釋:假如路徑起始於正斜槓( / ),則此路徑始終表明到某元素的絕對路徑!
bookstore/book 選取屬於 bookstore 的子元素的全部 book 元素。
//book 選取全部 book 子元素,而無論它們在文檔中的位置。
bookstore//book 選擇屬於 bookstore 元素的後代的全部 book 元素,而無論它們位於 bookstore 之下的什麼位置。
//@lang 選取名爲 lang 的全部屬性。

謂語(Predicates)

謂語用來查找某個特定的節點或者包含某個指定的值的節點,被嵌在方括號中。

在下面的表格中,咱們列出了帶有謂語的一些路徑表達式,以及表達式的結果:

路徑表達式 結果
/bookstore/book[1] 選取屬於 bookstore 子元素的第一個 book 元素。
/bookstore/book[last()] 選取屬於 bookstore 子元素的最後一個 book 元素。
/bookstore/book[last()-1] 選取屬於 bookstore 子元素的倒數第二個 book 元素。
/bookstore/book[position()<3] 選取最前面的兩個屬於 bookstore 元素的子元素的 book 元素。
//title[@lang] 選取全部擁有名爲 lang 的屬性的 title 元素。
//title[@lang=’eng’] 選取全部 title 元素,且這些元素擁有值爲 eng 的 lang 屬性。
/bookstore/book[price>35.00] 選取 bookstore 元素的全部 book 元素,且其中的 price 元素的值須大於 35.00。
/bookstore/book[price>35.00]/title 選取 bookstore 元素中的 book 元素的全部 title 元素,且其中的 price 元素的值須大於 35.00。

選取未知節點

XPath 通配符可用來選取未知的 XML 元素。

通配符 描述
* 匹配任何元素節點。
@* 匹配任何屬性節點。
node() 匹配任何類型的節點。

在下面的表格中,咱們列出了一些路徑表達式,以及這些表達式的結果:

路徑表達式 結果
/bookstore/* 選取 bookstore 元素的全部子元素。
//* 選取文檔中的全部元素。
//title[@*] 選取全部帶有屬性的 title 元素。

選取若干路徑

經過在路徑表達式中使用「|」運算符,您能夠選取若干個路徑。

實例

在下面的表格中,咱們列出了一些路徑表達式,以及這些表達式的結果:

路徑表達式 結果
//book/title | //book/price 選取 book 元素的全部 title 和 price 元素。
//title | //price 選取文檔中的全部 title 和 price 元素。
/bookstore/book/title | //price 選取屬於 bookstore 元素的 book 元素的全部 title 元素,以及文檔中全部的 price 元素。

XPath的運算符

下面列出了可用在 XPath 表達式中的運算符:

這些就是XPath的語法內容,在運用到Python抓取時要先轉換爲xml。

lxml庫

lxml 是 一個HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 數據。

lxml和正則同樣,也是用 C 實現的,是一款高性能的 Python HTML/XML 解析器,咱們能夠利用以前學習的XPath語法,來快速的定位特定元素以及節點信息。

lxml python 官方文檔:http://lxml.de/index.html

須要安裝C語言庫,可以使用 pip 安裝:pip install lxml (或經過wheel方式安裝)

初步使用

咱們利用它來解析 HTML 代碼,簡單示例:

# lxml_test.py

# 使用 lxml 的 etree 庫
from lxml import etree 

text = '''
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a> # 注意,此處缺乏一個 </li> 閉合標籤
     </ul>
 </div>
'''

#利用etree.HTML,將字符串解析爲HTML文檔
html = etree.HTML(text) 

# 按字符串序列化HTML文檔
result = etree.tostring(html) 

print(result)

 

輸出結果:

<html><body>
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
 </div>
</body></html>

 

lxml 能夠自動修正 html 代碼,例子裏不只補全了 li 標籤,還添加了 body,html 標籤。

文件讀取:

除了直接讀取字符串,lxml還支持從文件裏讀取內容。咱們新建一個hello.html文件:

<!-- hello.html -->

<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html"><span class="bold">third item</span></a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
     </ul>
 </div>

 

再利用 etree.parse() 方法來讀取文件。

# lxml_parse.py

from lxml import etree

# 讀取外部文件 hello.html
html = etree.parse('./hello.html')
result = etree.tostring(html, pretty_print=True)

print(result)

 

輸出結果與以前相同:

<html><body>
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
 </div>
</body></html>

 

XPath實例測試

1. 獲取全部的 <li> 標籤

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')
print type(html)  # 顯示etree.parse() 返回類型

result = html.xpath('//li')

print result  # 打印<li>標籤的元素集合
print len(result)
print type(result)
print type(result[0])

 

輸出結果:

<type 'lxml.etree._ElementTree'>
[<Element li at 0x1014e0e18>, <Element li at 0x1014e0ef0>, <Element li at 0x1014e0f38>, <Element li at 0x1014e0f80>, <Element li at 0x1014e0fc8>]
5
<type 'list'>
<type 'lxml.etree._Element'>

 

2. 繼續獲取<li> 標籤的全部 class屬性

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')
result = html.xpath('//li/@class')

print result

 

運行結果

['item-0', 'item-1', 'item-inactive', 'item-1', 'item-0']

 

3. 繼續獲取<li>標籤下hre 爲 link1.html 的 <a> 標籤

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')
result = html.xpath('//li/a[@href="link1.html"]')

print result

 

運行結果

[<Element a at 0x10ffaae18>]

 

4. 獲取<li> 標籤下的全部 <span> 標籤

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')

#result = html.xpath('//li/span')
#注意這麼寫是不對的:
#由於 / 是用來獲取子元素的,而 <span> 並非 <li> 的子元素,因此,要用雙斜槓

result = html.xpath('//li//span')

print result

 

運行結果

[<Element span at 0x10d698e18>]

 

5. 獲取 <li> 標籤下的<a>標籤裏的全部 class

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')
result = html.xpath('//li/a//@class')

print result

 

運行結果

['blod']

 

6. 獲取最後一個 <li> 的 <a> 的 href

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')

result = html.xpath('//li[last()]/a/@href')
# 謂語 [last()] 能夠找到最後一個元素

print result

 

運行結果

['link5.html']

 

7. 獲取倒數第二個元素的內容

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')
result = html.xpath('//li[last()-1]/a')

# text 方法能夠獲取元素內容
print result[0].text

 

運行結果

fourth item

 

8. 獲取 class 值爲 bold 的標籤名

# xpath_li.py

from lxml import etree

html = etree.parse('hello.html')

result = html.xpath('//*[@class="bold"]')

# tag方法能夠獲取標籤名
print result[0].tag

 

運行結果

span

 

案例:使用XPath的爬蟲

如今咱們用XPath來作一個簡單的爬蟲,咱們嘗試爬取某個貼吧裏的全部帖子,而且將該這個帖子裏每一個樓層發佈的圖片下載到本地。

# tieba_xpath.py


#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import urllib
import urllib2
from lxml import etree

class Spider:
    def __init__(self):
        self.tiebaName = raw_input("請須要訪問的貼吧:")
        self.beginPage = int(raw_input("請輸入起始頁:"))
        self.endPage = int(raw_input("請輸入終止頁:"))

        self.url = 'http://tieba.baidu.com/f'
        self.ua_header = {"User-Agent" : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1 Trident/5.0;"}

        # 圖片編號
        self.userName = 1

    def tiebaSpider(self):
        for page in range(self.beginPage, self.endPage + 1):
            pn = (page - 1) * 50 # page number
            word = {'pn' : pn, 'kw': self.tiebaName}

            word = urllib.urlencode(word) #轉換成url編碼格式(字符串)
            myUrl = self.url + "?" + word

            # 示例:http://tieba.baidu.com/f? kw=%E7%BE%8E%E5%A5%B3 & pn=50
            # 調用 頁面處理函數 load_Page
            # 而且獲取頁面全部帖子連接,
            links = self.loadPage(myUrl)  # urllib2_test3.py

    # 讀取頁面內容
    def loadPage(self, url):
        req = urllib2.Request(url, headers = self.ua_header)
        html = urllib2.urlopen(req).read()

        # 解析html 爲 HTML 文檔
        selector=etree.HTML(html)

        #抓取當前頁面的全部帖子的url的後半部分,也就是帖子編號
        # http://tieba.baidu.com/p/4884069807裏的 「p/4884069807」
        links = selector.xpath('//div[@class="threadlist_lz clearfix"]/div/a/@href')

        # links 類型爲 etreeElementString 列表
        # 遍歷列表,而且合併成一個帖子地址,調用 圖片處理函數 loadImage
        for link in links:
            link = "http://tieba.baidu.com" + link
            self.loadImages(link)

    # 獲取圖片
    def loadImages(self, link):
        req = urllib2.Request(link, headers = self.ua_header)
        html = urllib2.urlopen(req).read()

        selector = etree.HTML(html)

        # 獲取這個帖子裏全部圖片的src路徑
        imagesLinks = selector.xpath('//img[@class="BDE_Image"]/@src')

        # 依次取出圖片路徑,下載保存
        for imagesLink in imagesLinks:
            self.writeImages(imagesLink)

    # 保存頁面內容
    def writeImages(self, imagesLink):
        '''
            將 images 裏的二進制內容存入到 userNname 文件中
        '''

        print imagesLink
        print "正在存儲文件 %d ..." % self.userName
        # 1. 打開文件,返回一個文件對象
        file = open('./images/' + str(self.userName)  + '.png', 'wb')

        # 2. 獲取圖片裏的內容
        images = urllib2.urlopen(imagesLink).read()

        # 3. 調用文件對象write() 方法,將page_html的內容寫入到文件裏
        file.write(images)

        # 4. 最後關閉文件
        file.close()

        # 計數器自增1
        self.userName += 1

# 模擬 main 函數
if __name__ == "__main__":

    # 首先建立爬蟲對象
    mySpider = Spider()
    # 調用爬蟲對象的方法,開始工做
    mySpider.tiebaSpider()

 

CSS 選擇器:BeautifulSoup4

和 lxml 同樣,Beautiful Soup 也是一個HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 數據

lxml 只會局部遍歷,而Beautiful Soup 是基於HTML DOM的,會載入整個文檔,解析整個DOM樹,所以時間和內存開銷都會大不少,因此性能要低於lxml。

BeautifulSoup 用來解析 HTML 比較簡單,API很是人性化,支持CSS選擇器、Python標準庫中的HTML解析器,也支持 lxml 的 XML解析器。

Beautiful Soup 3 目前已經中止開發,推薦如今的項目使用Beautiful Soup 4。使用 pip 安裝便可:pip install beautifulsoup4

官方文檔:http://beautifulsoup.readthedocs.io/zh_CN/v4.4.0

抓取工具 速度 使用難度 安裝難度
正則 最快 困難 無(內置)
BeautifulSoup 最簡單 簡單
lxml 簡單 通常

示例:

首先必需要導入 bs4 庫

# beautifulsoup4_test.py

from bs4 import 

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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>
"""

#建立 Beautiful Soup 對象
soup = BeautifulSoup(html)

#打開本地 HTML 文件的方式來建立對象
#soup = BeautifulSoup(open('index.html'))

#格式化輸出 soup 對象的內容
print soup.prettify()

運行結果:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p class="title" name="dromouse">
   <b>
    The Dormouse's story
   </b>
  </p>
  <p class="story">
   Once upon a time there were three little sisters; and their names were
   <a class="sister" href="http://example.com/elsie" id="link1">
    <!-- Elsie -->
   </a>
   ,
   <a class="sister" href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a class="sister" href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p class="story">
   ...
  </p>
 </body>
</html>
  • 若是咱們在 IPython2 下執行,會看到這樣一段警告: 

  • 意思是,若是咱們沒有顯式地指定解析器,因此默認使用這個系統的最佳可用HTML解析器(「lxml」)。若是你在另外一個系統中運行這段代碼,或者在不一樣的虛擬環境中,使用不一樣的解析器形成行爲不一樣。

  • 可是咱們能夠經過soup = BeautifulSoup(html,「lxml」)方式指定lxml解析器。

四大對象種類

Beautiful Soup將複雜HTML文檔轉換成一個複雜的樹形結構,每一個節點都是Python對象,全部對象能夠概括爲4種:

  • Tag
  • NavigableString
  • BeautifulSoup
  • Comment

1. Tag

Tag 通俗點講就是 HTML 中的一個個標籤,例如:

<head><title>The Dormouse's story</title></head>
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>

上面的 title head a p等等 HTML 標籤加上裏面包括的內容就是 Tag,那麼試着使用 Beautiful Soup 來獲取 Tags:

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><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>
"""

#建立 Beautiful Soup 對象
soup = BeautifulSoup(html)


print soup.title
# <title>The Dormouse's story</title>

print soup.head
# <head><title>The Dormouse's story</title></head>

print soup.a
# <a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>

print soup.p
# <p class="title" name="dromouse"><b>The Dormouse's story</b></p>

print type(soup.p)
# <class 'bs4.element.Tag'>

 

咱們能夠利用 soup 加標籤名輕鬆地獲取這些標籤的內容,這些對象的類型是bs4.element.Tag。可是注意,它查找的是在全部內容中的第一個符合要求的標籤。若是要查詢全部的標籤,後面會進行介紹。

對於 Tag,它有兩個重要的屬性,是 name 和 attrs
print soup.name
# [document] #soup 對象自己比較特殊,它的 name 即爲 [document]

print soup.head.name
# head #對於其餘內部標籤,輸出的值便爲標籤自己的名稱

print soup.p.attrs
# {'class': ['title'], 'name': 'dromouse'}
# 在這裏,咱們把 p 標籤的全部屬性打印輸出了出來,獲得的類型是一個字典。

print soup.p['class'] # soup.p.get('class')
# ['title'] #還能夠利用get方法,傳入屬性的名稱,兩者是等價的

soup.p['class'] = "newClass"
print soup.p # 能夠對這些屬性和內容等等進行修改
# <p class="newClass" name="dromouse"><b>The Dormouse's story</b></p>

del soup.p['class'] # 還能夠對這個屬性進行刪除
print soup.p
# <p name="dromouse"><b>The Dormouse's story</b></p>

2. NavigableString

既然咱們已經獲得了標籤的內容,那麼問題來了,咱們要想獲取標籤內部的文字怎麼辦呢?很簡單,用 .string 便可,例如

print soup.p.string
# The Dormouse's story

print type(soup.p.string)
# In [13]: <class 'bs4.element.NavigableString'>

3. BeautifulSoup

BeautifulSoup 對象表示的是一個文檔的內容。大部分時候,能夠把它看成 Tag 對象,是一個特殊的 Tag,咱們能夠分別獲取它的類型,名稱,以及屬性來感覺一下

print type(soup.name)
# <type 'unicode'>

print soup.name 
# [document]

print soup.attrs # 文檔自己的屬性爲空
# {}

4. Comment

Comment 對象是一個特殊類型的 NavigableString 對象,其輸出的內容不包括註釋符號。

print soup.a
# <a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>

print soup.a.string
# Elsie 

print type(soup.a.string)
# <class 'bs4.element.Comment'>

a 標籤裏的內容其實是註釋,可是若是咱們利用 .string 來輸出它的內容時,註釋符號已經去掉了。

遍歷文檔樹

1. 直接子節點 :.contents .children 屬性

.content

tag 的 .content 屬性能夠將tag的子節點以列表的方式輸出

print soup.head.contents 
#[<title>The Dormouse's story</title>]

輸出方式爲列表,咱們能夠用列表索引來獲取它的某一個元素

print soup.head.contents[0]
#<title>The Dormouse's story</title>

.children

它返回的不是一個 list,不過咱們能夠經過遍歷獲取全部子節點。

咱們打印輸出 .children 看一下,能夠發現它是一個 list 生成器對象

print soup.head.children
#<listiterator object at 0x7f71457f5710>

for child in  soup.body.children:
    print child

結果:

<p class="title" name="dromouse"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>

2. 全部子孫節點: .descendants 屬性

.contents 和 .children 屬性僅包含tag的直接子節點,.descendants 屬性能夠對全部tag的子孫節點進行遞歸循環,和 children相似,咱們也須要遍歷獲取其中的內容。

for child in soup.descendants:
    print child

運行結果:

<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body></html>
<head><title>The Dormouse's story</title></head>
<title>The Dormouse's story</title>
The Dormouse's story


<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
</body>


<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<b>The Dormouse's story</b>
The Dormouse's story


<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
Once upon a time there were three little sisters; and their names were

<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>
 Elsie 
,

<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Lacie
 and

<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
Tillie
;
and they lived at the bottom of a well.


<p class="story">...</p>
...

3. 節點內容: .string 屬性

若是tag只有一個 NavigableString 類型子節點,那麼這個tag可使用 .string 獲得子節點。若是一個tag僅有一個子節點,那麼這個tag也可使用 .string 方法,輸出結果與當前惟一子節點的 .string 結果相同。

通俗點說就是:若是一個標籤裏面沒有標籤了,那麼 .string 就會返回標籤裏面的內容。若是標籤裏面只有惟一的一個標籤了,那麼 .string 也會返回最裏面的內容。例如:

print soup.head.string
#The Dormouse's story
print soup.title.string
#The Dormouse's story

 

搜索文檔樹

1.find_all(name, attrs, recursive, text, **kwargs)

1)name 參數

name 參數能夠查找全部名字爲 name 的tag,字符串對象會被自動忽略掉

A.傳字符串

最簡單的過濾器是字符串.在搜索方法中傳入一個字符串參數,Beautiful Soup會查找與字符串完整匹配的內容,下面的例子用於查找文檔中全部的<b>標籤:

soup.find_all('b')
# [<b>The Dormouse's story</b>]

print soup.find_all('a')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

 

B.傳正則表達式

若是傳入正則表達式做爲參數,Beautiful Soup會經過正則表達式的 match() 來匹配內容.下面例子中找出全部以b開頭的標籤,這表示<body><b>標籤都應該被找到

import re
for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
# body
# b

 

C.傳列表

若是傳入列表參數,Beautiful Soup會將與列表中任一元素匹配的內容返回.下面代碼找到文檔中全部<a>標籤和<b>標籤:

soup.find_all(["a", "b"])
# [<b>The Dormouse's story</b>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

 

2)keyword 參數

soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

 

3)text 參數

經過 text 參數能夠搜搜文檔中的字符串內容,與 name 參數的可選值同樣, text 參數接受 字符串 , 正則表達式 , 列表

soup.find_all(text="Elsie")
# [u'Elsie']

soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]

 

CSS選擇器

這就是另外一種與 find_all 方法有殊途同歸之妙的查找方法.

  • 寫 CSS 時,標籤名不加任何修飾,類名前加.,id名前加#

  • 在這裏咱們也能夠利用相似的方法來篩選元素,用到的方法是 soup.select(),返回類型是 list

(1)經過標籤名查找

print soup.select('title') 
#[<title>The Dormouse's story</title>]

print soup.select('a')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

print soup.select('b')
#[<b>The Dormouse's story</b>]

 

(2)經過類名查找

print soup.select('.sister')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

 

(3)經過 id 名查找

print soup.select('#link1')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

 

(4)組合查找

組合查找即和寫 class 文件時,標籤名與類名、id名進行的組合原理是同樣的,例如查找 p 標籤中,id 等於 link1的內容,兩者須要用空格分開

print soup.select('p #link1')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

 

直接子標籤查找,則使用 > 分隔

print soup.select("head > title")
#[<title>The Dormouse's story</title>]

 

(5)屬性查找

查找時還能夠加入屬性元素,屬性須要用中括號括起來,注意屬性和標籤屬於同一節點,因此中間不能加空格,不然會沒法匹配到。

print soup.select('a[class="sister"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

print soup.select('a[href="http://example.com/elsie"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

 

一樣,屬性仍然能夠與上述查找方式組合,不在同一節點的空格隔開,同一節點的不加空格

print soup.select('p a[href="http://example.com/elsie"]')
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]

 

(6) 獲取內容

以上的 select 方法返回的結果都是列表形式,能夠遍歷形式輸出,而後用 get_text() 方法來獲取它的內容。

soup = BeautifulSoup(html, 'lxml')
print type(soup.select('title'))
print soup.select('title')[0].get_text()

for title in soup.select('title'):

案例:使用BeautifuSoup4的爬蟲

咱們以騰訊社招頁面來作演示:http://hr.tencent.com/position.php?&start=10#a

# bs4_tencent.py


from bs4 import BeautifulSoup
import urllib2
import urllib
import json    # 使用了json格式存儲

def tencent():
    url = 'http://hr.tencent.com/'
    request = urllib2.Request(url + 'position.php?&start=10#a')
    response =urllib2.urlopen(request)
    resHtml = response.read()

    output =open('tencent.json','w')

    html = BeautifulSoup(resHtml,'lxml')

# 建立CSS選擇器
    result = html.select('tr[class="even"]')
    result2 = html.select('tr[class="odd"]')
    result += result2

    items = []
    for site in result:
        item = {}

        name = site.select('td a')[0].get_text()
        detailLink = site.select('td a')[0].attrs['href']
        catalog = site.select('td')[1].get_text()
        recruitNumber = site.select('td')[2].get_text()
        workLocation = site.select('td')[3].get_text()
        publishTime = site.select('td')[4].get_text()

        item['name'] = name
        item['detailLink'] = url + detailLink
        item['catalog'] = catalog
        item['recruitNumber'] = recruitNumber
        item['publishTime'] = publishTime

        items.append(item)

    # 禁用ascii編碼,按utf-8編碼
    line = json.dumps(items,ensure_ascii=False)

    output.write(line.encode('utf-8'))
    output.close()

if __name__ == "__main__":
   tencent()

 

數據提取之JSON與JsonPATH

JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式,它使得人們很容易的進行閱讀和編寫。同時也方便了機器進行解析和生成。適用於進行數據交互的場景,好比網站前臺與後臺之間的數據交互。

JSON和XML的比較可謂不相上下。

Python 2.7中自帶了JSON模塊,直接import json就可使用了。

官方文檔:http://docs.python.org/library/json.html

Json在線解析網站:http://www.json.cn/#

JSON

json簡單說就是javascript中的對象和數組,因此這兩種結構就是對象和數組兩種結構,經過這兩種結構能夠表示各類複雜的結構

  1. 對象:對象在js中表示爲{ }括起來的內容,數據結構爲 { key:value, key:value, ... }的鍵值對的結構,在面向對象的語言中,key爲對象的屬性,value爲對應的屬性值,因此很容易理解,取值方法爲 對象.key 獲取屬性值,這個屬性值的類型能夠是數字、字符串、數組、對象這幾種。

  2. 數組:數組在js中是中括號[ ]括起來的內容,數據結構爲 ["Python", "javascript", "C++", ...],取值方式和全部語言中同樣,使用索引獲取,字段值的類型能夠是 數字、字符串、數組、對象幾種。

import json

json模塊提供了四個功能:dumpsdumploadsload,用於字符串 和 python數據類型間進行轉換。

1. json.loads()

把Json格式字符串解碼轉換成Python對象 從json到python的類型轉化對照以下:

# json_loads.py

import json

strList = '[1, 2, 3, 4]'

strDict = '{"city": "北京", "name": "大貓"}'

json.loads(strList) 
# [1, 2, 3, 4]

json.loads(strDict) # json數據自動按Unicode存儲
# {u'city': u'\u5317\u4eac', u'name': u'\u5927\u732b'}

 

2. json.dumps()

實現python類型轉化爲json字符串,返回一個str對象 把一個Python對象編碼轉換成Json字符串

從python原始類型向json類型的轉化對照以下:

 
# json_dumps.py

import json
import chardet

listStr = [1, 2, 3, 4]
tupleStr = (1, 2, 3, 4)
dictStr = {"city": "北京", "name": "大貓"}

json.dumps(listStr)
# '[1, 2, 3, 4]'
json.dumps(tupleStr)
# '[1, 2, 3, 4]'

# 注意:json.dumps() 序列化時默認使用的ascii編碼
# 添加參數 ensure_ascii=False 禁用ascii編碼,按utf-8編碼
# chardet.detect()返回字典, 其中confidence是檢測精確度

json.dumps(dictStr) 
# '{"city": "\\u5317\\u4eac", "name": "\\u5927\\u5218"}'

chardet.detect(json.dumps(dictStr))
# {'confidence': 1.0, 'encoding': 'ascii'}

print json.dumps(dictStr, ensure_ascii=False) 
# {"city": "北京", "name": "大劉"}

chardet.detect(json.dumps(dictStr, ensure_ascii=False))
# {'confidence': 0.99, 'encoding': 'utf-8'}
 

 

 

chardet是一個很是優秀的編碼識別模塊,可經過pip安裝

3. json.dump()

將Python內置類型序列化爲json對象後寫入文件

 
# json_dump.py

import json

listStr = [{"city": "北京"}, {"name": "大劉"}]
json.dump(listStr, open("listStr.json","w"), ensure_ascii=False)

dictStr = {"city": "北京", "name": "大劉"}
json.dump(dictStr, open("dictStr.json","w"), ensure_ascii=False)
 

 

 

4. json.load()

讀取文件中json形式的字符串元素 轉化成python類型

 
# json_load.py

import json

strList = json.load(open("listStr.json"))
print strList

# [{u'city': u'\u5317\u4eac'}, {u'name': u'\u5927\u5218'}]

strDict = json.load(open("dictStr.json"))
print strDict
# {u'city': u'\u5317\u4eac', u'name': u'\u5927\u5218'}
 

 

 

JsonPath

JsonPath 是一種信息抽取類庫,是從JSON文檔中抽取指定信息的工具,提供多種語言實現版本,包括:Javascript, Python, PHP 和 Java。

JsonPath 對於 JSON 來講,至關於 XPATH 對於 XML。

下載地址:https://pypi.python.org/pypi/jsonpath

安裝方法:點擊Download URL連接下載jsonpath,解壓以後執行python setup.py install

官方文檔:http://goessner.net/articles/JsonPath

JsonPath與XPath語法對比:

Json結構清晰,可讀性高,複雜度低,很是容易匹配,下表中對應了XPath的用法。

XPath JSONPath 描述
/ $ 根節點
. @ 現行節點
/ .or[] 取子節點
.. n/a 取父節點,Jsonpath未支持
// .. 就是無論位置,選擇全部符合條件的條件
* * 匹配全部元素節點
@ n/a 根據屬性訪問,Json不支持,由於Json是個Key-value遞歸結構,不須要。
[] [] 迭代器標示(能夠在裏邊作簡單的迭代操做,如數組下標,根據內容選值等)
| [,] 支持迭代器中作多選。
[] ?() 支持過濾操做.
n/a () 支持表達式計算
() n/a 分組,JsonPath不支持

示例:

咱們以拉勾網城市JSON文件 http://www.lagou.com/lbs/getAllCitySearchLabels.json 爲例,獲取全部城市。

# jsonpath_lagou.py

import urllib2
import jsonpath
import json
import chardet

url = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
request =urllib2.Request(url)
response = urllib2.urlopen(request)
html = response.read()

# 把json格式字符串轉換成python對象
jsonobj = json.loads(html)

# 從根節點開始,匹配name節點
citylist = jsonpath.jsonpath(jsonobj,'$..name')

print citylist
print type(citylist)
fp = open('city.json','w')

content = json.dumps(citylist, ensure_ascii=False)
print content

fp.write(content.encode('utf-8'))
fp.close()

 

注意事項:

json.loads() 是把 Json格式字符串解碼轉換成Python對象,若是在json.loads的時候出錯,要注意被解碼的Json字符的編碼。

若是傳入的字符串的編碼不是UTF-8的話,須要指定字符編碼的參數 encoding

dataDict = json.loads(jsonStrGBK);

 

  • dataJsonStr是JSON字符串,假設其編碼自己是非UTF-8的話而是GBK 的,那麼上述代碼會致使出錯,改成對應的:

      dataDict = json.loads(jsonStrGBK, encoding="GBK");

     

  • 若是 dataJsonStr經過encoding指定了合適的編碼,可是其中又包含了其餘編碼的字符,則須要先去將dataJsonStr轉換爲Unicode,而後再指定編碼格式調用json.loads()

``` python
dataJsonStrUni = dataJsonStr.decode("GB2312"); dataDict = json.loads(dataJsonStrUni, encoding="GB2312");
##字符串編碼轉換

這是中國程序員最苦逼的地方,什麼亂碼之類的幾乎都是由漢字引發的。
其實編碼問題很好搞定,只要記住一點:

####任何平臺的任何編碼 都能和 Unicode 互相轉換

UTF-8 與 GBK 互相轉換,那就先把UTF-8轉換成Unicode,再從Unicode轉換成GBK,反之同理。



``` python 
# 這是一個 UTF-8 編碼的字符串
utf8Str = "你好地球"

# 1. 將 UTF-8 編碼的字符串 轉換成 Unicode 編碼
unicodeStr = utf8Str.decode("UTF-8")

# 2. 再將 Unicode 編碼格式字符串 轉換成 GBK 編碼
gbkData = unicodeStr.encode("GBK")

# 1. 再將 GBK 編碼格式字符串 轉化成 Unicode
unicodeStr = gbkData.decode("gbk")

# 2. 再將 Unicode 編碼格式字符串轉換成 UTF-8
utf8Str = unicodeStr.encode("UTF-8")
decode的做用是將其餘編碼的字符串轉換成 Unicode 編碼

encode的做用是將 Unicode 編碼轉換成其餘編碼的字符串

 

一句話:UTF-8是對Unicode字符集進行編碼的一種編碼方式

糗事百科實例:

爬取糗事百科段子,假設頁面的URL是 http://www.qiushibaike.com/8hr/page/1

要求:

  1. 使用requests獲取頁面信息,用XPath / re 作數據提取

  2. 獲取每一個帖子裏的用戶頭像連接用戶姓名段子內容點贊次數評論次數

  3. 保存到 json 文件內

參考代碼

#qiushibaike.py

#import urllib
#import re
#import chardet

import requests
from lxml import etree

page = 1
url = 'http://www.qiushibaike.com/8hr/page/' + str(page) 
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
    'Accept-Language': 'zh-CN,zh;q=0.8'}

try:
    response = requests.get(url, headers=headers)
    resHtml = response.text

    html = etree.HTML(resHtml)
    result = html.xpath('//div[contains(@id,"qiushi_tag")]')

    for site in result:
        item = {}

        imgUrl = site.xpath('./div/a/img/@src')[0].encode('utf-8')
        username = site.xpath('./div/a/@title')[0].encode('utf-8')
        #username = site.xpath('.//h2')[0].text
        content = site.xpath('.//div[@class="content"]/span')[0].text.strip().encode('utf-8')
        # 投票次數
        vote = site.xpath('.//i')[0].text
        #print site.xpath('.//*[@class="number"]')[0].text
        # 評論信息
        comments = site.xpath('.//i')[1].text

        print imgUrl, username, content, vote, comments

except Exception, e:
    print e

演示效果

多線程糗事百科案例

案例要求參考上一個糗事百科單進程案例

Queue(隊列對象)

Queue是python中的標準庫,能夠直接import Queue引用;隊列是線程間最經常使用的交換數據的形式

python下多線程的思考

對於資源,加鎖是個重要的環節。由於python原生的list,dict等,都是not thread safe的。而Queue,是線程安全的,所以在知足使用條件下,建議使用隊列

  1. 初始化: class Queue.Queue(maxsize) FIFO 先進先出

  2. 包中的經常使用方法:

    • Queue.qsize() 返回隊列的大小

    • Queue.empty() 若是隊列爲空,返回True,反之False

    • Queue.full() 若是隊列滿了,返回True,反之False

    • Queue.full 與 maxsize 大小對應

    • Queue.get([block[, timeout]])獲取隊列,timeout等待時間

  3. 建立一個「隊列」對象

    • import Queue
    • myqueue = Queue.Queue(maxsize = 10)
  4. 將一個值放入隊列中

    • myqueue.put(10)
  5. 將一個值從隊列中取出

    • myqueue.get()

多線程示意圖

# -*- coding:utf-8 -*-
import requests
from lxml import etree
from Queue import Queue
import threading
import time
import json


class thread_crawl(threading.Thread):
    '''
    抓取線程類
    '''

    def __init__(self, threadID, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.q = q

    def run(self):
        print "Starting " + self.threadID
        self.qiushi_spider()
        print "Exiting ", self.threadID

    def qiushi_spider(self):
        # page = 1
        while True:
            if self.q.empty():
                break
            else:
                page = self.q.get()
                print 'qiushi_spider=', self.threadID, ',page=', str(page)
                url = 'http://www.qiushibaike.com/8hr/page/' + str(page) + '/'
                headers = {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36',
                    'Accept-Language': 'zh-CN,zh;q=0.8'}
                # 屢次嘗試失敗結束、防止死循環
                timeout = 4
                while timeout > 0:
                    timeout -= 1
                    try:
                        content = requests.get(url, headers=headers)
                        data_queue.put(content.text)
                        break
                    except Exception, e:
                        print 'qiushi_spider', e
                if timeout < 0:
                    print 'timeout', url


class Thread_Parser(threading.Thread):
    '''
    頁面解析類;
    '''

    def __init__(self, threadID, queue, lock, f):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.queue = queue
        self.lock = lock
        self.f = f

    def run(self):
        print 'starting ', self.threadID
        global total, exitFlag_Parser
        while not exitFlag_Parser:
            try:
                '''
                調用隊列對象的get()方法從隊頭刪除並返回一個項目。可選參數爲block,默認爲True。
                若是隊列爲空且block爲True,get()就使調用線程暫停,直至有項目可用。
                若是隊列爲空且block爲False,隊列將引起Empty異常。
                '''
                item = self.queue.get(False)
                if not item:
                    pass
                self.parse_data(item)
                self.queue.task_done()
                print 'Thread_Parser=', self.threadID, ',total=', total
            except:
                pass
        print 'Exiting ', self.threadID

    def parse_data(self, item):
        '''
        解析網頁函數
        :param item: 網頁內容
        :return:
        '''
        global total
        try:
            html = etree.HTML(item)
            result = html.xpath('//div[contains(@id,"qiushi_tag")]')
            for site in result:
                try:
                    imgUrl = site.xpath('.//img/@src')[0]
                    title = site.xpath('.//h2')[0].text
                    content = site.xpath('.//div[@class="content"]/span')[0].text.strip()
                    vote = None
                    comments = None
                    try:
                        vote = site.xpath('.//i')[0].text
                        comments = site.xpath('.//i')[1].text
                    except:
                        pass
                    result = {
                        'imgUrl': imgUrl,
                        'title': title,
                        'content': content,
                        'vote': vote,
                        'comments': comments,
                    }

                    with self.lock:
                        # print 'write %s' % json.dumps(result)
                        self.f.write(json.dumps(result, ensure_ascii=False).encode('utf-8') + "\n")

                except Exception, e:
                    print 'site in result', e
        except Exception, e:
            print 'parse_data', e
        with self.lock:
            total += 1

data_queue = Queue()
exitFlag_Parser = False
lock = threading.Lock()
total = 0

def main():
    output = open('qiushibaike.json', 'a')

    #初始化網頁頁碼page從1-10個頁面
    pageQueue = Queue(50)
    for page in range(1, 11):
        pageQueue.put(page)

    #初始化採集線程
    crawlthreads = []
    crawlList = ["crawl-1", "crawl-2", "crawl-3"]

    for threadID in crawlList:
        thread = thread_crawl(threadID, pageQueue)
        thread.start()
        crawlthreads.append(thread)

    #初始化解析線程parserList
    parserthreads = []
    parserList = ["parser-1", "parser-2", "parser-3"]
    #分別啓動parserList
    for threadID in parserList:
        thread = Thread_Parser(threadID, data_queue, lock, output)
        thread.start()
        parserthreads.append(thread)

    # 等待隊列清空
    while not pageQueue.empty():
        pass

    # 等待全部線程完成
    for t in crawlthreads:
        t.join()

    while not data_queue.empty():
        pass
    # 通知線程是時候退出
    global exitFlag_Parser
    exitFlag_Parser = True

    for t in parserthreads:
        t.join()
    print "Exiting Main Thread"
    with lock:
        output.close()


if __name__ == '__main__':
    main()

因此,咱們只須要匹配到網頁中全部<div class="f18 mb20"> 到 </div> 的數據就能夠了。

根據正則表達式,咱們能夠推算出一個公式是:
<div.*?class="f18 mb20">(.*?)</div>

 

  • 這個表達式實際上就是匹配到全部divclass="f18 mb20 裏面的內容(具體能夠看前面正則介紹)

  • 而後將這個正則應用到代碼中,咱們會獲得如下代碼:

 
def loadPage(self, page):
    """
        @brief 定義一個url請求網頁的方法
        @param page 須要請求的第幾頁
        @returns 返回的頁面html
    """

    url = "http://www.neihan8.com/article/list_5_" + str(page)
+ ".html"
    #User-Agent頭
    user_agent = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT
6.1; Trident/5.0'
    headers = {'User-Agent': user_agent}
    req = urllib2.Request(url, headers = headers)
    response = urllib2.urlopen(req)
    html = response.read()
    gbk_html = html.decode('gbk').encode('utf-8')

    #找到全部的段子內容<div class = "f18 mb20"></div>
    #re.S 若是沒有re.S 則是隻匹配一行有沒有符合規則的字符串,若是沒有則下一行從新匹配
    # 若是加上re.S 則是將全部的字符串將一個總體進行匹配
    pattern = re.compile(r'<div.*?class="f18 mb20">(.*?)</di
v>', re.S)
    item_list = pattern.findall(gbk_html)

    return item_list


def printOnePage(self, item_list, page):
    """
        @brief 處理獲得的段子列表
        @param item_list 獲得的段子列表
        @param page 處理第幾頁
    """

    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        print "================"
        print ite
 

 

 
  • 這裏須要注意一個是re.S是正則表達式中匹配的一個參數。

  • 若是 沒有re.S 則是 只匹配一行 有沒有符合規則的字符串,若是沒有則下一行從新匹配。

  • 若是 加上re.S 則是將 全部的字符串 將一個總體進行匹配,findall 將全部匹配到的結果封裝到一個list中。
  • 而後咱們寫了一個遍歷item_list的一個方法 printOnePage() 。 ok程序寫到這,咱們再一次執行一下。
Power@PowerMac ~$ python duanzi_spider.py
咱們第一頁的所有段子,不包含其餘信息所有的打印了出來。
  • 你會發現段子中有不少 <p> , </p> 非常不舒服,實際上這個是html的一種段落的標籤。
  • 在瀏覽器上看不出來,可是若是按照文本打印會有<p>出現,那麼咱們只須要把咱們不但願的內容去掉便可了。

  • 咱們能夠以下簡單修改一下 printOnePage().

def printOnePage(self, item_list, page):
    """
        @brief 處理獲得的段子列表
        @param item_list 獲得的段子列表
        @param page 處理第幾頁
    """

    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        print "================"
        item = item.replace("<p>", "").replace("</p>", "").repl
ace("<br />", "")
        print item

第三步:保存數據

  • 咱們能夠將全部的段子存放在文件中。好比,咱們能夠將獲得的每一個item不是打印出來,而是存放在一個叫 duanzi.txt 的文件中也能夠。
 
def writeToFile(self, text):
'''
    @brief 將數據追加寫進文件中
    @param text 文件內容
'''
    myFile = open("./duanzi.txt", 'a') #追加形式打開文件
    myFile.write(text)
    myFile.write("---------------------------------------------
--------")
    myFile.close()
 
  • 而後咱們將print的語句 改爲writeToFile() ,當前頁面的全部段子就存在了本地的MyStory.txt文件中。
 
def printOnePage(self, item_list, page):
'''
    @brief 處理獲得的段子列表
    @param item_list 獲得的段子列表
    @param page 處理第幾頁
'''
    print "******* 第 %d 頁 爬取完畢...*******" %page
    for item in item_list:
        # print "================"
        item = item.replace("<p>", "").replace("</p>", "").repl
ace("<br />", "")
        # print item
        self.writeToFile(item)

第四步:顯示數據

  • 接下來咱們就經過參數的傳遞對page進行疊加來遍歷 內涵段子吧的所有段子內容。

  • 只須要在外層加一些邏輯處理便可。

 
def doWork(self):
'''
    讓爬蟲開始工做
'''
    while self.enable:
        try:
            item_list = self.loadPage(self.page)
        except urllib2.URLError, e:
            print e.reason
            continue

        #對獲得的段子item_list處理
        self.printOnePage(item_list, self.page)
        self.page += 1 #此頁處理完畢,處理下一頁
        print "按回車繼續..."
        print "輸入 quit 退出"
        command = raw_input()
        if (command == "quit"):
            self.enable = False
            break
 
  • 最後,咱們執行咱們的代碼,完成後查看當前路徑下的duanzi.txt文件,裏面已經有了咱們要的內涵段子。

以上即是一個很是精簡使用的小爬蟲程序,使用起來非常方便,若是想要爬取其餘網站的信息,只須要修改其中某些參數和一些細節就好了。

相關文章
相關標籤/搜索