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


頁面解析和數據提取

一般來講對我們而言,需要抓取的是某個網站或者某個應用的內容,提取有用的價值。內容一般分為兩部分,非結構化的數據 結構化的數據

  • 非結構化數據:先有數據,再有結構,
  • 結構化數據:先有結構、再有數據
  • 不同類型的數據,我們需要采用不同的方式來處理。

非結構化的數據處理

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

結構化的數據處理

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

為什么要學正則表達式

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

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

我們在昨天的案例里實際上省略了第3步,也就是"取"的步驟。因為我們down下了的數據是全部的網頁,這些數據很龐大並且很混亂,大部分的東西使我們不關心的,因此我們需要將之按我們的需要過濾和匹配出來。

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

什么是正則表達式

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

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

給定一個正則表達式和另一個字符串,我們可以達到如下的目的:

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

正則表達式匹配規則

Python 的 re 模塊

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

有一點需要特別注意的是,正則表達式使用 對特殊字符進行轉義,所以如果我們要使用原始字符串,只需加一個 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文件,里面已經有了我們要的內涵段子。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM