頁面解析和數據提取javascript
通常來說對咱們而言,須要抓取的是某個網站或者某個應用的內容,提取有用的價值。內容通常分爲兩部分,非結構化的數據 和 結構化的數據。php
實際上爬蟲一共就四個主要步驟:css
咱們在昨天的案例裏實際上省略了第3步,也就是"取"的步驟。由於咱們down下了的數據是所有的網頁,這些數據很龐大而且很混亂,大部分的東西使咱們不關心的,所以咱們須要將之按咱們的須要過濾和匹配出來。html
那麼對於文本的過濾或者規則的匹配,最強大的就是正則表達式,是Python爬蟲世界裏必不可少的神兵利器。java
正則表達式,又稱規則表達式,一般被用來檢索、替換那些符合某個模式(規則)的文本。node
正則表達式是對字符串操做的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個「規則字符串」,這個「規則字符串」用來表達對字符串的一種過濾邏輯。python
給定一個正則表達式和另外一個字符串,咱們能夠達到以下的目的:程序員
- 給定的字符串是否符合正則表達式的過濾邏輯(「匹配」);
- 經過正則表達式,從文本字符串中獲取咱們想要的特定部分(「過濾」)。
在 Python 中,咱們可使用內置的 re 模塊來使用正則表達式。web
有一點須要特別注意的是,正則表達式使用 對特殊字符進行轉義,因此若是咱們要使用原始字符串,只需加一個 r 前綴,示例:正則表達式
r'chuanzhiboke\t\.\tpython'
使用 compile()
函數將正則表達式的字符串形式編譯爲一個 Pattern
對象
經過 Pattern
對象提供的一系列方法對文本進行匹配查找,得到匹配結果,一個 Match 對象。
Match
對象提供的屬性和方法得到信息,根據須要進行其餘的操做compile 函數用於編譯正則表達式,生成一個 Pattern 對象,它的通常使用形式以下:
import re
# 將正則表達式編譯成 Pattern 對象
pattern = re.compile(r'\d+')
在上面,咱們已將一個正則表達式編譯成 Pattern 對象,接下來,咱們就能夠利用 pattern 的一系列方法對文本進行匹配查找了。
Pattern 對象的一些經常使用方法主要有:
- match 方法:從起始位置開始查找,一次匹配
- search 方法:從任何位置開始查找,一次匹配
- findall 方法:所有匹配,返回列表
- finditer 方法:所有匹配,返回迭代器
- split 方法:分割字符串,返回列表
- sub 方法:替換
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;
再看看一個例子:
>>> 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(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)
上面的 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 方法的行爲跟 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(string[, maxsplit])
其中,maxsplit 用於指定最大分割次數,不指定將所有分割。
看看例子:
import re
p = re.compile(r'[\s\,\;]+')
print p.split('a,b;; c d')
執行結果:
['a', 'b', 'c', 'd']
sub 方法用於替換。它的使用形式以下:
sub(repl, string[, count])
其中,repl 能夠是字符串也能夠是一個函數:
若是 repl 是字符串,則會使用 repl 去替換字符串每個匹配的子串,並返回替換後的字符串,另外,repl 還可使用 id 的形式來引用分組,但不能使用編號 0;
若是 repl 是函數,這個方法應當只接受一個參數(Match 對象),並返回一個字符串用於替換(返回的字符串中不能再引用分組)。
看看例子:
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']
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規律找到了,要想爬取全部的段子,只須要修改一個參數便可。 下面咱們就開始一步一步將全部的段子爬取下來吧。
這裏咱們統必定義一個類,將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方法
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>
這個表達式實際上就是匹配到全部div
中class="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
def writeToFile(self, text):
'''
@brief 將數據追加寫進文件中
@param text 文件內容
'''
myFile = open("./duanzi.txt", 'a') #追加形式打開文件
myFile.write(text)
myFile.write("---------------------------------------------
--------")
myFile.close()
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 節點或元素。
W3School官方文檔:http://www.w3school.com.cn/xml/index.asp
數據格式 | 描述 | 設計目標 |
---|---|---|
XML | Extensible Markup Language (可擴展標記語言) |
被設計爲傳輸和存儲數據,其焦點是數據的內容。 |
HTML | HyperText Markup Language (超文本標記語言) |
顯示數據以及如何更好顯示數據。 |
HTML DOM | Document Object Model for HTML (文檔對象模型) |
經過 HTML DOM,能夠訪問全部的 HTML 元素,連同它們所包含的文本和屬性。能夠對其中的內容進行修改和刪除,同時也能夠建立新的元素。 |
<?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 文檔的標準方法,以樹結構方式表達 HTML 文檔。
每一個元素以及屬性都有一個父。
下面是一個簡單的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>
元素節點可有零個、一個或多個子。
在下面的例子中,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>
擁有相同的父的節點
在下面的例子中,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>
某節點的父、父的父,等等。
在下面的例子中,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>
某個節點的子,子的子,等等。
在下面的例子中,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 (XML Path Language) 是一門在 XML 文檔中查找信息的語言,可用來在 XML 文檔中對元素和屬性進行遍歷。
W3School官方文檔:http://www.w3school.com.cn/xpath/index.asp
XPath 使用路徑表達式來選取 XML 文檔中的節點或者節點集。這些路徑表達式和咱們在常規的電腦文件系統中看到的表達式很是類似。
下面列出了最經常使用的路徑表達式:
表達式 | 描述 |
---|---|
nodename | 選取此節點的全部子節點。 |
/ | 從根節點選取。 |
// | 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。 |
. | 選取當前節點。 |
.. | 選取當前節點的父節點。 |
@ | 選取屬性。 |
在下面的表格中,咱們已列出了一些路徑表達式以及表達式的結果:
路徑表達式 | 結果 | |
---|---|---|
bookstore | 選取 bookstore 元素的全部子節點。 | |
/bookstore | 選取根元素 bookstore。註釋:假如路徑起始於正斜槓( / ),則此路徑始終表明到某元素的絕對路徑! | |
bookstore/book | 選取屬於 bookstore 的子元素的全部 book 元素。 | |
//book | 選取全部 book 子元素,而無論它們在文檔中的位置。 | |
bookstore//book | 選擇屬於 bookstore 元素的後代的全部 book 元素,而無論它們位於 bookstore 之下的什麼位置。 | |
//@lang | 選取名爲 lang 的全部屬性。 |
謂語用來查找某個特定的節點或者包含某個指定的值的節點,被嵌在方括號中。
在下面的表格中,咱們列出了帶有謂語的一些路徑表達式,以及表達式的結果:
路徑表達式 | 結果 |
---|---|
/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 表達式中的運算符:
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>
<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'>
<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']
<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>]
<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>]
<li>
標籤下的<a>
標籤裏的全部 class# xpath_li.py
from lxml import etree
html = etree.parse('hello.html')
result = html.xpath('//li/a//@class')
print result
運行結果
['blod']
<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']
# 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
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來作一個簡單的爬蟲,咱們嘗試爬取某個貼吧裏的全部帖子,而且將該這個帖子裏每一個樓層發佈的圖片下載到本地。
# 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()
和 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
抓取工具 | 速度 | 使用難度 | 安裝難度 |
---|---|---|---|
正則 | 最快 | 困難 | 無(內置) |
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 通俗點講就是 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
。可是注意,它查找的是在全部內容中的第一個符合要求的標籤。若是要查詢全部的標籤,後面會進行介紹。
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>
既然咱們已經獲得了標籤的內容,那麼問題來了,咱們要想獲取標籤內部的文字怎麼辦呢?很簡單,用 .string 便可,例如
print soup.p.string # The Dormouse's story print type(soup.p.string) # In [13]: <class 'bs4.element.NavigableString'>
BeautifulSoup 對象表示的是一個文檔的內容。大部分時候,能夠把它看成 Tag 對象,是一個特殊的 Tag,咱們能夠分別獲取它的類型,名稱,以及屬性來感覺一下
print type(soup.name) # <type 'unicode'> print soup.name # [document] print soup.attrs # 文檔自己的屬性爲空 # {}
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 來輸出它的內容時,註釋符號已經去掉了。
.contents
.children
屬性tag 的 .content 屬性能夠將tag的子節點以列表的方式輸出
print soup.head.contents #[<title>The Dormouse's story</title>]
輸出方式爲列表,咱們能夠用列表索引來獲取它的某一個元素
print soup.head.contents[0] #<title>The Dormouse's story</title>
它返回的不是一個 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>
.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> ...
.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
find_all(name, attrs, recursive, text, **kwargs)
name 參數能夠查找全部名字爲 name 的tag,字符串對象會被自動忽略掉
最簡單的過濾器是字符串.在搜索方法中傳入一個字符串參數,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>]
若是傳入正則表達式做爲參數,Beautiful Soup會經過正則表達式的 match() 來匹配內容.下面例子中找出全部以b開頭的標籤,這表示<body>
和<b>
標籤都應該被找到
import re
for tag in soup.find_all(re.compile("^b")):
print(tag.name)
# body
# b
若是傳入列表參數,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>]
soup.find_all(id='link2') # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
經過 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"]
這就是另外一種與 find_all 方法有殊途同歸之妙的查找方法.
寫 CSS 時,標籤名不加任何修飾,類名前加.
,id名前加#
在這裏咱們也能夠利用相似的方法來篩選元素,用到的方法是 soup.select()
,返回類型是 list
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>]
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>]
print soup.select('#link1') #[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
組合查找即和寫 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>]
查找時還能夠加入屬性元素,屬性須要用中括號括起來,注意屬性和標籤屬於同一節點,因此中間不能加空格,不然會沒法匹配到。
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>]
以上的 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'):
咱們以騰訊社招頁面來作演示: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(JavaScript Object Notation) 是一種輕量級的數據交換格式,它使得人們很容易的進行閱讀和編寫。同時也方便了機器進行解析和生成。適用於進行數據交互的場景,好比網站前臺與後臺之間的數據交互。
JSON和XML的比較可謂不相上下。
Python 2.7中自帶了JSON模塊,直接import json
就可使用了。
官方文檔:http://docs.python.org/library/json.html
Json在線解析網站:http://www.json.cn/#
json簡單說就是javascript中的對象和數組,因此這兩種結構就是對象和數組兩種結構,經過這兩種結構能夠表示各類複雜的結構
對象:對象在js中表示爲
{ }
括起來的內容,數據結構爲{ key:value, key:value, ... }
的鍵值對的結構,在面向對象的語言中,key爲對象的屬性,value爲對應的屬性值,因此很容易理解,取值方法爲 對象.key 獲取屬性值,這個屬性值的類型能夠是數字、字符串、數組、對象這幾種。數組:數組在js中是中括號
[ ]
括起來的內容,數據結構爲["Python", "javascript", "C++", ...]
,取值方式和全部語言中同樣,使用索引獲取,字段值的類型能夠是 數字、字符串、數組、對象幾種。
json模塊提供了四個功能:dumps
、dump
、loads
、load
,用於字符串 和 python數據類型間進行轉換。
把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'}
實現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安裝
將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)
讀取文件中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 是一種信息抽取類庫,是從JSON文檔中抽取指定信息的工具,提供多種語言實現版本,包括:Javascript, Python, PHP 和 Java。
JsonPath 對於 JSON 來講,至關於 XPATH 對於 XML。
下載地址:https://pypi.python.org/pypi/jsonpath
安裝方法:點擊
Download URL
連接下載jsonpath,解壓以後執行python setup.py install
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
使用requests獲取頁面信息,用XPath / re 作數據提取
獲取每一個帖子裏的用戶頭像連接
、用戶姓名
、段子內容
、點贊次數
和評論次數
保存到 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是python中的標準庫,能夠直接import Queue引用;隊列是線程間最經常使用的交換數據的形式
python下多線程的思考
對於資源,加鎖是個重要的環節。由於python原生的list,dict等,都是not thread safe的。而Queue,是線程安全的,所以在知足使用條件下,建議使用隊列
初始化: class Queue.Queue(maxsize) FIFO 先進先出
包中的經常使用方法:
Queue.qsize() 返回隊列的大小
Queue.empty() 若是隊列爲空,返回True,反之False
Queue.full() 若是隊列滿了,返回True,反之False
Queue.full 與 maxsize 大小對應
Queue.get([block[, timeout]])獲取隊列,timeout等待時間
建立一個「隊列」對象
將一個值放入隊列中
將一個值從隊列中取出
# -*- 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>
這個表達式實際上就是匹配到全部div
中class="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
def writeToFile(self, text): ''' @brief 將數據追加寫進文件中 @param text 文件內容 ''' myFile = open("./duanzi.txt", 'a') #追加形式打開文件 myFile.write(text) myFile.write("--------------------------------------------- --------") myFile.close()
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文件,裏面已經有了咱們要的內涵段子。
以上即是一個很是精簡使用的小爬蟲程序,使用起來非常方便,若是想要爬取其餘網站的信息,只須要修改其中某些參數和一些細節就好了。