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


頁面解析和數據提取

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

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

非結構化的數據處理

文本、電話號碼、郵箱地址

  • 正則表達式

HTML 文件

  • 正則表達式
  • XPath
  • CSS選擇器

結構化的數據處理

JSON 文件

  • JSON Path
  • 轉化成Python類型進行操作(json類)

XML 文件

  • 轉化成Python類型(xmltodict)
  • XPath
  • CSS選擇器
  • 正則表達式

正則表達式

"HTTP請求報文格式"

正則表達式匹配規則

"HTTP請求報文格式"

Python的re模塊

在Python中,可以使用內置的re模塊來使用正則表達式。
有一點需要特別注意的是,正則表達式使用對特殊字符進行轉義,所以如果我們要使用原始字符串,只需加一個 r 前綴,例如:

r’chuanzhiboket.tpython’

re模塊的一般使用步驟
  1. 使用compile()函數將正則表達式的字符串形式編譯為一個Pattern對象;
  2. 通過Pattern對象提供的一系列方法對文本進行匹配查找,獲得匹配結果,一個Match對象;
  3. 最后使用Match對象提供的屬性和方法獲得信息,根據需要進行其他的操作。
compile 函數

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

1
2
3
4
import re


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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> 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))。

再看看一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
>>> 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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)
findall方法

上面的match和search方法都是一次匹配,只要找到了一個匹配的結果就返回。然而,在大多數時候,我們需要搜索整個字符串,獲得所有匹配的結果。
findall方法的使用形式如下:

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

其中,string是待匹配的字符串,pos和endpos是可選參數,指定字符串的起始和終點位置,默認值分別是0和len(字符串長度)。
findall以列表形式返回全部能匹配的子串,如果沒有匹配,則返回一個空列表。

1
2
3
4
5
6
7
8
import re
pattern = re.compile(r'd+') # 查找數字

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

print(result1)
print(result2)
finditer方法

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

split方法

split方法按照能夠匹配的子串將字符串分割后返回列表,它的使用形式如下:

split(string[, maxsplit])

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

1
2
3
import re
p = re.compile(r'[s,;]+')
print(p.split('a,b;; c d'))
sub方法

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

sub(repl, string[, count])

其中,repl可以是字符串也可以是一個函數:

  • 如果repl是字符串,則會使用repl去替換字符串每一個匹配的子串,並返回替換后的字符串,另外,repl還可以使用id的形式來引用分組,但不能使用編號0;
  • 如果repl是函數,這個方法應當只接受一個參數(Match 對象),並返回一個字符串用於替換(返回的字符串中不能再引用分組)。
  • count用於指定最多替換次數,不指定時全部替換。
1
2
3
4
5
6
7
8
9
10
11
12
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 (m):
return 'hi' + ' ' + m.group(2)

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

在某些情況下,需要匹配文本中的漢字,有一點需要注意的是,中文的unicode編碼范圍 主要在[u4e00-u9fa5],這里說主要是因為這個范圍並不完整,比如沒有包括全角(中文)標點,不過,在大部分情況下,應該是夠用的。

假設現在想把字符串title = u’你好,hello,世界’中的中文提取出來,可以:

1
2
3
4
5
6
7
import re

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

print(result)
貪婪模式與非貪婪模式
  • 貪婪模式:在整個表達式匹配成功的前提下,盡可能多的匹配( * );
  • 非貪婪模式:在整個表達式匹配成功的前提下,盡可能少的匹配( ? );
  • Python里數量詞默認是貪婪的。

使用正則表達式的爬蟲

使用正則表達式爬去騰訊社招的信息:騰訊社招

XPath與lxml庫

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

XPath開發工具

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

選取節點

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

最常用的路徑表達式如下:

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

謂語(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’] 大專欄  非結構化和結構化數據提取left">選取所有 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() 匹配任何類型的節點。

選取若干路徑

通過在路徑表達式中使用|運算符,可以選取若干個路徑。
在下面的表格中,列出了一些路徑表達式,以及這些表達式的結果:

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

lxml庫

lxml是一個HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 數據。
lxml和正則一樣,也是用 C 實現的,是一款高性能的 Python HTML/XML 解析器,可以利用XPath語法,來快速的定位特定元素以及節點信息。

使用xpath獲取騰訊社招每個職位的詳細信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from urllib import request
from lxml import etree
import json
import codecs


class TencentSpider(object):
def __init__(self, file, beginPage, endPage):
self.url = "https://hr.tencent.com/position.php?&start="
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
}
self.beginPage = beginPage
self.endPage = endPage
self.file = file
self.positionJson = []

def start_spider(self):
for page in range(self.beginPage, self.endPage + 1):
offsetNumber = (page - 1) * 10
fullUrl = self.url + str(offsetNumber)
self.load_page(fullUrl)

self.file.write(json.dumps(self.positionJson, ensure_ascii=False))

def load_page(self, url):
req = request.Request(url, headers=self.headers)
res = request.urlopen(req)
html = etree.HTML(res.read())

links = html.xpath('//div[@class="left wcont_b box"]//tr[@class="even" or "odd"]/td[1]/a/@href')
for link in links:
fullLink = "https://hr.tencent.com/" + link
self.get_position_detail(fullLink)

def get_position_detail(self, url):
req = request.Request(url, headers=self.headers)
res = request.urlopen(req)
html = etree.HTML(res.read())

positionDetail = {}
# 職位名稱
positionDetail['positionName'] = html.xpath('//div[@class="box wcont_a"]/table//tr[1]/td/text()')[0]
# 工作地點
positionDetail['positionAddress'] = html.xpath('//div[@class="box wcont_a"]/table//tr[2]/td[1]/text()')[0]
# 職位類別
if(html.xpath('//div[@class="box wcont_a"]/table//tr[2]/td[2]/text()')):
positionDetail['positionCategory'] = html.xpath('//div[@class="box wcont_a"]/table//tr[2]/td[2]/text()')[0]
else:
positionDetail['positionCategory'] = ""
# 招聘人數
positionDetail['postionNumber'] = html.xpath('//div[@class="box wcont_a"]/table//tr[2]/td[3]/text()')[0]
# 工作職責
positionDutyList = html.xpath('//div[@class="box wcont_a"]/table//tr[3]/td/ul//li')
positionDutyStr = ""

for duty in positionDutyList:
positionDutyStr += duty.text
# print(positionDutyStr)

positionDetail['positionDutyStr'] = positionDutyStr

# 工作要求
positionRequirementList = html.xpath('//div[@class="box wcont_a"]/table//tr[4]/td/ul//li')
positionRequirementStr = ""

for requirement in positionRequirementList:
positionRequirementStr += requirement.text
# print(positionRequirementStr)

positionDetail['positionRequirementStr'] = positionRequirementStr

self.positionJson.append(positionDetail)


if __name__ == "__main__":
beginPage = int(input("請輸入起始頁:"))
endPage = int(input("請輸入終止頁"))
file = codecs.open("tencentposition.json", "w", "utf-8")
tencentspider = TencentSpider(file, beginPage, endPage)
tencentspider.start_spider()
file.close()

數據提取之JSON與JsonPATH

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

JSON

json簡單說就是javascript中的對象和數組,所以這兩種結構就是對象和數組兩種結構,通過這兩種結構可以表示各種復雜的結構。

對象:對象在js中表示為{ }括起來的內容,數據結構為{ key:value, key:value, ... }的鍵值對的結構,在面向對象的語言中,key為對象的屬性,value為對應的屬性值,所以很容易理解,取值方法為 對象.key 獲取屬性值,這個屬性值的類型可以是數字、字符串、數組、對象這幾種。
數組:數組在js中是中括號[ ]括起來的內容,數據結構為["Python", "javascript", "C++", ...],取值方式和所有語言中一樣,使用索引獲取,字段值的類型可以是 數字、字符串、數組、對象幾種。

json模塊

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

json.loads()

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

Python JSON
dict object
list, tuple array
str, unicode string
int, long, float number
True true
False false
None null
json.dumps()

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

json.dump()

將Python內置類型序列化為json對象后寫入文件。

1
2
3
4
5
6
7
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.load()

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

1
2
3
4
import json

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

JsonPath

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

JsonPath與XPath語法對比
XPath JSONPath 描述
/ $ 根節點
. @ 現行節點
/ .or[] 取子節點
.. n/a 取父節點,Jsonpath未支持
// .. 就是不管位置,選擇所有符合條件的條件
* * 匹配所有元素節點
@ n/a 根據屬性訪問,Json不支持,因為Json是個Key-value遞歸結構,不需要。
[] [] 迭代器標示(可以在里邊做簡單的迭代操作,如數組下標,根據內容選值等)
| [,] 支持迭代器中做多選。
[] ?() 支持過濾操作.
n/a () 支持表達式計算
() n/a 分組,JsonPath不支持
注意事項

json.loads()是把Json格式字符串解碼轉換成Python對象,如果在json.loads的時候出錯,要注意被解碼的Json字符的編碼。
如果傳入的字符串的編碼不是UTF-8的話,需要指定字符編碼的參數encoding:

1
2
import json
dataDict = json.loads(jsonStrGBK, encoding="GBK");
  • decode的作用是將其他編碼的字符串轉換成 Unicode 編碼;
  • encode的作用是將 Unicode 編碼轉換成其他編碼的字符串。

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

多線程爬蟲實例

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()

多線程示意圖

"多線程示意圖"

1
2



免責聲明!

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



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