赞
踩
目录
页面解析和数据提取
一般来讲对我们而言,需要抓取的是某个网站或者某个应用的内容,提取有用的价值。内容一般分为两部分,非结构化的数据 和 结构化的数据。
文本、电话号码、邮箱地址
HTML 文件
JSON 文件
XML 文件
实际上爬虫一共就四个主要步骤:
我们在昨天的案例里实际上省略了第3步,也就是"取"的步骤。因为我们down下了的数据是全部的网页,这些数据很庞大并且很混乱,大部分的东西使我们不关心的,因此我们需要将之按我们的需要过滤和匹配出来。
那么对于文本的过滤或者规则的匹配,最强大的就是正则表达式,是Python爬虫世界里必不可少的神兵利器。
正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑。
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
- 给定的字符串是否符合正则表达式的过滤逻辑("匹配");
- 通过正则表达式,从文本字符串中获取我们想要的特定部分("过滤")。
在 Python 中,我们可以使用内置的 re 模块来使用正则表达式。
有一点需要特别注意的是,正则表达式使用 对特殊字符进行转义,所以如果我们要使用原始字符串,只需加一个 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;
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(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 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。
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):
- print(m)
- return 'hi' + ' ' + m.group(2) #group(0) 表示本身,group(1)表示hello,group(2) 表示后面的数字
-
- print (p.sub(func, s)) #多次sub,每次sub的结果传递给func
- 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 = '你好,hello,世界'
- pattern = re.compile(r'[\u4e00-\u9fa5]+')
- result = pattern.findall(title)
-
- print (result)
注意到,我们在正则表达式前面加上了两个前缀 ur,其中 r 表示使用原始字符串,u 表示是 unicode 字符串。
执行结果:
['你好', '世界']
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类,并且添加一个加载页面的成员方法
- class Duanzi_spider():
- def __init__(self):
- self.url = "http://www.neihan8.com/article/list_5_%s.html"
- self.headers = {
- "User_Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleW\
- ebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
- "Accept-Encoding":None,
- "Accept-Language": "zh-CN,zh;q=0.8"
-
- }
-
- def load_page(self,url):
- '''可以复用的页面请求方法
- '''
- response = requests.get(url,timeout=10,headers=self.headers)
- if response.status_code==200:
- print(response.request.headers)
- return response.content.decode("gbk")
- else:
- raise ValueError("status_code is:",response.status_code)
注意 :对于每个网站对中文的编码各自不同,所以html.decode('gbk')的写法并不是通用写法,根据网站的编码而异
接下来我们已经得到了整个页面的数据。 但是,很多内容我们并不关心,所以下一步我们需要进行筛选。 如何筛选,就用到了上一节讲述的正则表达式。
import re
我们可以打开内涵段子的网页,鼠标点击右键 " 查看源代码 " 你会发现每条段子的内容大致如下
- <a href="/article/44959.html"><b>回家奔丧</b></a></h4>
- <div class="f18 mb20">
- 一老太太跋山涉水来到部队,看望她的孙子,<br />
- 警卫问:“她找谁?”老太说:“找xx,”警卫打完电话说:<br />
- “xx三天前说她他奶奶过世,回家奔丧去了,奔丧去了,去了。。”
-
- </div>
- def get_content(self,html):
- ''' 根据网页内容,同时匹配标题和段子内容
- '''
- pattern = re.compile(r'<a\shref="/article/\d+\.html">(.*?)</a>.*?<div\sclass="f18 mb20">(.*?)</div>', re.S)
- t = pattern.findall(html)
- result = []
- for i in t:
- temp = []
- for j in i:
- j = re.sub(r"[<b>|</b>|<br />|<br>|<p>|</p>|\\u3000|\\r\\n|\s]","",j)
- j = j.replace("&ldqo;",'"').replace("&helli;","...").replace("&dqo;",'"').strip()
- # j = re.sub(r"[&ldqo;|&dqo;]","\"",j)?
- # j = re.sub(r"…","...",j)
- temp.append(j)
- print(temp)
- result.append(temp)
- return result
这里需要注意一个是
re.S
是正则表达式中匹配的一个参数。如果 没有re.S 则是 只匹配一行 有没有符合规则的字符串,如果没有则下一行重新匹配。
如果 加上re.S 则是将 所有的字符串 将一个整体进行匹配,findall 将所有匹配到的结果封装到一个list中。
Power@PowerMac ~$ python duanzi_spider.py
<p>
, </p>
很是不舒服,实际上这个是html的一种段落的标签。在浏览器上看不出来,但是如果按照文本打印会有<p>
出现,那么我们只需要把我们不希望的内容去掉即可了。
我们可以如下简单修改一下 get_content().
- j = re.sub(r"[<b>|</b>|<br />|<br>|<p>|</p>|\\u3000|\\r\\n|\s]","",j)
- j = j.replace("&ldqo;",'"').replace("&helli;","...").replace("&dqo;",'"').strip()
- def save_content(self,content):
- myFile = open("./duanzi.txt", 'a')
- for temp in content:
- myFile.write("\n"+temp[0]+"\n"+temp[1]+"\n")
- myFile.write("-----------------------------------------------------")
- myFile.close()
接下来我们就通过参数的传递对page进行叠加来遍历 内涵段子吧的全部段子内容。
同时也通过这个run方法实现整个程序的主要逻辑
- def run(self):
- i = 1
- while True:
- html = self.load_page(self.url%i)
- result = self.get_content(html)
- print ("按回车继续...")
- print ("输入 quit 退出")
- command = input()
- if (command == "quit"):
- break
- i+=1
最后,我们执行我们的代码,完成后查看当前路径下的duanzi.txt文件,里面已经有了我们要的内涵段子。
以上便是一个非常精简使用的小爬虫程序,使用起来很是方便,如果想要爬取其他网站的信息,只需要修改其中某些参数和一些细节就行了。
有同学说,我正则用的不好,处理HTML文档很累,有没有其他的方法?
有!那就是XPath,我们可以先将 HTML文件 转换成 XML文档,然后用 XPath 查找 HTML 节点或元素。
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 文档。
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 (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 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
html/node()/meta/@* | 选择html下面任意节点下的meta节点的所有属性 |
//title[@*] | 选取所有带有属性的 title 元素。 |
通过在路径表达式中使用“|”运算符,您可以选取若干个路径。
实例
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
下面列出了可用在 XPath 表达式中的运算符:
这些就是XPath的语法内容,在运用到Python抓取时要先转换为xml。
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>
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来做一个简单的爬虫,我们尝试爬取某个贴吧里的所有帖子,并且将该这个帖子里每个楼层发布的图片下载到本地。
- #coding=utf-8
- import requests
- from lxml import etree
- import json
-
- class Tieba:
-
- def __init__(self,tieba_name):
- self.tieba_name = tieba_name #接收贴吧名
- #设置为手机端的UA
- self.headers = {"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1"}
-
- def get_total_url_list(self):
- '''获取所有的urllist'''
- url = "https://tieba.baidu.com/f?kw="+self.tieba_name+"&ie=utf-8&pn={}&"
- url_list = []
- for i in range(100): #通过循环拼接100个url
- url_list.append(url.format(i*50))
- return url_list #返回100个url的urllist
-
- def parse_url(self,url):
- '''一个发送请求,获取响应,同时etree处理html'''
- print("parsing url:",url)
- response = requests.get(url,headers=self.headers,timeout=10) #发送请求
- html = response.content.decode() #获取html字符串
- html = etree.HTML(html) #获取element 类型的html
- return html
-
- def get_title_href(self,url):
- '''获取一个页面的title和href'''
- html = self.parse_url(url)
- li_temp_list = html.xpath("//li[@class='tl_shadow']") #分组,按照li标签分组
- total_items = []
- for i in li_temp_list: #遍历分组
- href = "https:"+i.xpath("./a/@href")[0] if len(i.xpath("./a/@href"))>0 else None
- text = i.xpath("./a/div[1]/span[1]/text()")
- text = text[0] if len(text)>0 else None
- item = dict( #放入字典
- href = href,
- text = text
- )
- total_items.append(item)
- return total_items #返回一个页面所有的item
-
- def get_img(self,url):
- '''获取一个帖子里面的所有图片'''
- html = self.parse_url(url) #返回elemet累心的html,具有xpath方法
- img_list = html.xpath('//div[@data-class="BDE_Image"]/@data-url')
- img_list = [i.split("src=")[-1] for i in img_list] #提取图片的url
- img_list = [requests.utils.unquote(i) for i in img_list]
- return img_list
-
- def save_item(self,item):
- '''保存一个item'''
- with open("teibatupian.txt","a") as f:
- f.write(json.dumps(item,ensure_ascii=False,indent=2))
- f.write("\n")
-
- def run(self):
- #1、找到了url规律,url list
- url_list = self.get_total_url_list()
- for url in url_list:
- #2、遍历urllist 发送请求,获得响应,etree处理html
- # 3、提取title,href
- total_item = self.get_title_href(url)
- for item in total_item:
- href = item["href"]
- img_list = self.get_img(href) #获取到了帖子的图片列表
- item["img"] = img_list
- # 4、保存到本地
- print(item)
- self.save_item(item)
-
- if __name__ == "__main__":
- tieba = Tieba("猫")
- tieba.run()
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 requests
- import jsonpath
- import json
- import chardet
-
- url = 'http://www.lagou.com/lbs/getAllCitySearchLabels.json'
- response = equests.get(url)
- html = response.text
-
- # 把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()
爬取糗事百科段子,假设页面的URL是 http://www.qiushibaike.com/8hr/page/1
使用requests获取页面信息,用XPath / re 做数据提取
获取每个帖子里的用户头像链接
、用户姓名
、段子内容
、点赞次数
和评论次数
保存到 json 文件内
- #coding=utf-8
- import requests
- from retrying import retry
- from lxml import etree
-
- class Qiubai_spider():
- def __init__(self):
- self.url = "http://www.qiushibaike.com/8hr/page/{}/"
- self.headers = {
- "User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1 Trident/5.0;"
- }
-
- @retry(stop_max_attempt_number=5) #调用retry,当assert出错时候,重复请求5次
- def parse_url(self,url):
- response = requests.get(url,timeout=10,headers=self.headers) #请求url
- assert response.status_code==200 #当响应码不是200时候,做断言报错处理
- print(url)
- return etree.HTML(response.text) #返回etree之后的html
-
- def parse_content(self,html):
- item_temp = html.xpath("//div[@class='article block untagged mb15']")
- print(len(item_temp))
- for item in item_temp:
- #获取用户头像地址
- avatar = item.xpath("./div[1]/a[1]/img/@src")[0] if len(item.xpath("./div[1]/a[1]/img/@src"))>0 else None
- #为头像地址添加前缀
- if avatar is not None and not avatar.startswith("http:"):
- avatar = "http:"+avatar
- print(avatar)
- name = item.xpath("./div[1]/a[2]/h2/text()")[0] #获取用户名
- print(name)
- content = item.xpath("./a[@class='contentHerf']/div/span/text()")[0] #获取内容
- print(content)
- star_number = item.xpath("./div[@class='stats']/span[1]/i/text()")[0] #获取点赞数
- print(star_number)
- comment_number = item.xpath("./div[@class='stats']/span[2]/a/i/text()")[0] #获取评论数
- print(comment_number)
- print("*"*100)
-
- def run(self):
- '''函数的主要逻辑实现
- '''
- url = self.url.format(1) #获取到url
- html = self.parse_url(url) #请求url
- self.parse_content(html) #解析页面内容并把内容存入内容队列
-
- if __name__ == "__main__":
- qiubai = Qiubai_spider()
- qiubai.run()
案例要求参考上一个糗事百科单进程案例
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
- import json
- from queue import Queue
- import threading
-
- class Qiubai:
- def __init__(self):
- self.headers = {
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWeb\
- Kit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"}
- self.url_queue = Queue() #实例化三个队列,用来存放内容
- self.html_queue =Queue()
- self.content_queue = Queue()
-
-
- def get_total_url(self):
- '''
- 获取了所有的页面url,并且返回urllist
- return :list
- '''
- url_temp = 'https://www.qiushibaike.com/8hr/page/{}/'
- url_list = []
- for i in range(1,36):
- # url_list.append(url_temp.format(i))
- self.url_queue.put(url_temp.format(i))
-
- def parse_url(self):
- '''
- 一个发送请求,获取响应,同时etree处理html
- '''
- while self.url_queue.not_empty:
- url = self.url_queue.get()
- print("parsing url:",url)
- response = requests.get(url,headers=self.headers,timeout=10) #发送请求
- html = response.content.decode() #获取html字符串
- html = etree.HTML(html) #获取element 类型的html
- self.html_queue.put(html)
- self.url_queue.task_done()
-
- def get_content(self):
- '''
- :param url:
- :return: 一个list,包含一个url对应页面的所有段子的所有内容的列表
- '''
- while self.html_queue.not_empty:
- html = self.html_queue.get()
- total_div = html.xpath('//div[@class="article block untagged mb15"]') #返回divelememtn的一个列表
- items = []
- for i in total_div: #遍历div标枪,获取糗事百科每条的内容的全部信息
- author_img = i.xpath('./div[@class="author clearfix"]/a[1]/img/@src')
- author_img = "https:" + author_img[0] if len(author_img) > 0 else None
- author_name = i.xpath('./div[@class="author clearfix"]/a[2]/h2/text()')
- author_name = author_name[0] if len(author_name) > 0 else None
- author_href = i.xpath('./div[@class="author clearfix"]/a[1]/@href')
- author_href = "https://www.qiushibaike.com" + author_href[0] if len(author_href) > 0 else None
- author_gender = i.xpath('./div[@class="author clearfix"]//div/@class')
- author_gender = author_gender[0].split(" ")[-1].replace("Icon", "") if len(author_gender) > 0 else None
- author_age = i.xpath('./div[@class="author clearfix"]//div/text()')
- author_age = author_age[0] if len(author_age) > 0 else None
- content = i.xpath('./a[@class="contentHerf"]/div/span/text()')
- content_vote = i.xpath('./div[@class="stats"]/span[1]/i/text()')
- content_vote = content_vote[0] if len(content_vote) > 0 else None
- content_comment_numbers = i.xpath('./div[@class="stats"]/span[2]/a/i/text()')
- content_comment_numbers = content_comment_numbers[0] if len(content_comment_numbers) > 0 else None
- hot_comment_author = i.xpath('./a[@class="indexGodCmt"]/div/span[last()]/text()')
- hot_comment_author = hot_comment_author[0] if len(hot_comment_author) > 0 else None
- hot_comment = i.xpath('./a[@class="indexGodCmt"]/div/div/text()')
- hot_comment = hot_comment[0].replace("\n:", "").replace("\n", "") if len(hot_comment) > 0 else None
- hot_comment_like_num = i.xpath('./a[@class="indexGodCmt"]/div/div/div/text()')
- hot_comment_like_num = hot_comment_like_num[-1].replace("\n", "") if len(hot_comment_like_num) > 0 else None
- item = dict(
- author_name=author_name,
- author_img=author_img,
- author_href=author_href,
- author_gender=author_gender,
- author_age=author_age,
- content=content,
- content_vote=content_vote,
- content_comment_numbers=content_comment_numbers,
- hot_comment=hot_comment,
- hot_comment_author=hot_comment_author,
- hot_comment_like_num=hot_comment_like_num
- )
- items.append(item)
- self.content_queue.put(items)
- self.html_queue.task_done() #task_done的时候,队列计数减一
-
- def save_items(self):
- '''
- 保存items
- :param items:列表
- '''
- while self.content_queue.not_empty:
- items = self.content_queue.get()
- f = open("qiubai.txt","a")
- for i in items:
- json.dump(i,f,ensure_ascii=False,indent=2)
- # f.write(json.dumps(i))
- f.close()
- self.content_queue.task_done()
-
- def run(self):
- # 1.获取url list
- # url_list = self.get_total_url()
- thread_list = []
- thread_url = threading.Thread(target=self.get_total_url)
- thread_list.append(thread_url)
- #发送网络请求
- for i in range(10):
- thread_parse = threading.Thread(target=self.parse_url)
- thread_list.append(thread_parse)
- #提取数据
- thread_get_content = threading.Thread(target=self.get_content)
- thread_list.append(thread_get_content)
- #保存
- thread_save = threading.Thread(target=self.save_items)
- thread_list.append(thread_save)
- for t in thread_list:
- t.setDaemon(True) #为每个进程设置为后台进程,效果是主进程退出子进程也会退出
- t.start() #为了解决程序结束无法退出的问题
- #
- # for t in thread_list:
- # t.join()
-
- self.url_queue.join() #让主线程等待,所有的队列为空的时候才能退出
- self.html_queue.join()
- self.content_queue.join()
-
- if __name__ == "__main__":
- qiubai = Qiubai()
- qiubai.run()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。