python爬虫系列之解析手段
1.正则表达式
正则表达式是贯穿python始终的一个方法,在各个领域都用的非常多,所以必须掌握
1.1常用的匹配规则
普通字符 正常匹配其中的字符。
\n 匹配换行符。
\t 匹配制表符。
\w 匹配字母、数字、下划线。
\W 匹配除了字母、数字、下划线的字符串。字母大写相当于非(个人总结)
\d 匹配十进制数字
\D 匹配除了十进制数字的字符串
\s 匹配空白字符
\S 匹配非空白字符
[asd213] 匹配中括号中的任意一个字符
[^asd213] 匹配中除了括号中的任意一个字符
1.2match()方法
1.2.1示例:
import re
content = 'hello 123 4567 world_this is a regex demo'
print(len(content))
result = re.match('^hello\s\d{3}\s\d{4}\s\w{10}' , content)
print(result)
print(result.group())
print(result.span())
返回如下:
41
<_sre.SRE_Match object; span=(0, 25), match='hello 123 4567 world_this'>
hello 123 4567 world_this
(0, 25)
这里^hello就是匹配字符串开头,是以hello开头,然后\s匹配空格,\d{3}指匹配3个任意【0-9】的数字,后面依次类推
最重要的一点就是,使用正则表达式之前,需要保证你的文本是字符串格式。
1.2.2匹配目标
import re
content = 'hello 123 4567 world_this is a regex demo'
print(len(content))
result = re.match('^hello\s(\d{3})\s(\d{4})\s\w{10}' , content)
print(result)
print(result.group())
print(result.group(1))
print(result.group(2))
print(result.span())
运行结果如下:
41
<_sre.SRE_Match object; span=(0, 25), match='hello 123 4567 world_this'>
hello 123 4567 world_this
123
4567
(0, 25)
这里给其中两个\d加上了(),这样可以在结果中按照索引获取不同的结果。
1.2.3通用匹配
示例:
import re
content = 'hello 123 4567 world_this is a regex demo'
print(len(content))
result = re.match('^hello.*demo$' , content)
print(result)
print(result.group())
print(result.span())
这里没有使用繁杂的\d,\s等,而是直接用.* 。当然是在规定开始和结尾的情况下匹配包含的所有内容
结果如下:
41
<_sre.SRE_Match object; span=(0, 41), match='hello 123 4567 world_this is a regex demo'>
hello 123 4567 world_this is a regex demo
(0, 41)
这样也可以匹配正确的信息,当然这适用于简单的匹配。
1.2.4贪婪与非贪婪
实例:
import re
content = 'hello 1234567 world_this is a regex demo'
print(len(content))
result = re.match('^hello.*(\d+).*demo$' , content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
结果如下:
40
<_sre.SRE_Match object; span=(0, 40), match='hello 1234567 world_this is a regex demo'>
hello 1234567 world_this is a regex demo
7
(0, 40)
这里我们在(\d+)前后都加上了.*,然后输出所有的数字,但是最后只输出了一个7,这是为什么呢?
因为.*是贪婪匹配,会尽可能的匹配多的字符串,就把123456全匹配了,只给\d+留下了一个7.那么如何改变这种情况呢?
如下:
import re
content = 'hello 1234567 world_this is a regex demo'
print(len(content))
result = re.match('^hello.*?(\d+).*demo$' , content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
我们只在前面的.*后面加上了一个?,就将贪婪匹配变成了非贪婪匹配
返回结果如下:
40
<_sre.SRE_Match object; span=(0, 40), match='hello 1234567 world_this is a regex demo'>
hello 1234567 world_this is a regex demo
1234567
(0, 40)
这样就能返回所有数字
1.2.5修饰符
如果我们需要解析的字符串有换行,有大小写夹杂,会对我们的解析产生很多麻烦,那么我们就需要修饰符的帮助了
如下:
import re
content = '''hello 1234567 world_this is a regex
demo'''
print(len(content))
result = re.match('^hello.*?(\d+).*demo$' , content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
运行结果:
41
None
Traceback (most recent call last):
File "C:/Users/86130/Desktop/requests_spider/test.py", line 8, in <module>
print(result.group())
AttributeError: 'NoneType' object has no attribute 'group'
这里换行之后,就会报错,是因为换行之后,无法匹配到结果,那么我们就需要这样:
import re
content = '''hello 1234567 world_this is a regex
demo'''
print(len(content))
result = re.match('^hello.*?(\d+).*demo$' , content,re.S)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
运行结果如下:
41
<_sre.SRE_Match object; span=(0, 41), match='hello 1234567 world_this is a regex \ndemo'>
hello 1234567 world_this is a regex
demo
1234567
(0, 41)
在match函数后面加上一个参数 re.S,就能让匹配包括换行符在内的所有字符
修饰符 | 描述 |
---|---|
re.I | 是匹配对大小写不敏感 |
re.L | 做本地化识别匹配 |
re.M | 多行匹配,会影响^和$ |
re.S | 能匹配包括换行符在内的所有字符 |
re.U | 根据Unicode字符集解析字符。这个标志会影响\w,\W,\b,\B |
re.X | 该标志通过给与你更灵活的格式以便你将正则表达式写的更加易于理解 |
在网页匹配重,较为常用的有re.S和 re.I
1.2.6转义匹配
正则匹配本身定义了许多匹配模式,但是如果目标字符串里面就包含类似.的字符串,该怎么办呢?这时候就需要一个特殊的方法来避免歧义:
import re
content = '''(百度)www.baidu.com'''
print(len(content))
result = re.match('\(百度\)www\.baidu\.com' , content)
print(result)
print(result.group())
print(result.span())
这里在可能会产生歧义的字符前面加上了\号,就会成功匹配字段。
17
<_sre.SRE_Match object; span=(0, 17), match='(百度)www.baidu.com'>
(百度)www.baidu.com
(0, 17)
1.3search()
match()方法是从字符串开头开始匹配,一旦开头不匹配,那么整个匹配就是失败了。看下面的例子:
import re
content = '''extra hello 1234567 world_this is a regex demo'''
print(len(content))
result = re.match('hello.*?(\d+).*demo$' , content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())
运行结果如下:
46
None
Traceback (most recent call last):
File "C:/Users/86130/Desktop/requests_spider/test.py", line 7, in <module>
print(result.group())
AttributeError: 'NoneType' object has no attribute 'group'
匹配失败,因为并不是以字符串hello开头。所以这里我们引入search()方法,我们来替换一下:
import re
content = '''extra hello 1234567 world_this is a regex demo'''
print(len(content))
result = re.search('hello.*?(\d+).*?demo$', content)
print(result)
结果如图:
46
<_sre.SRE_Match object; span=(6, 46), match='hello 1234567 world_this is a regex demo'>
这样就可以实现正确匹配,
这里我们需要知道的一点是,无论match()方法还是search()方法,如果没有匹配到目标,都会返回一个None,那么我们可以做一个判断:
if result: 这可以避免报错,在大型的爬虫中会有用。
1.4findall()
如果要获得所有节点的内容,并进行逐个匹配,这个应用很广泛,需要重点掌握。
类似:
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>',html,re.S)
print(results)
for result in results:
print(result)
print(result[0],result[1],result[2])
这里就会匹配所有的节点,然后会生成一个列表,即results是一个列表类型,然后列表里面的每个元素都是元组。当需要提取多个内容时,就用findall方法
1.5sub()
这是一个批量修改文本的方法,而不是一个解析方法,如:
import re
content = '''extra hello 1234567 world_this is a regex demo'''
print(content)
result = re.sub('\d+','', content)
print(result)
运行结果如下:
extra hello 1234567 world_this is a regex demo
extra hello world_this is a regex demo
这里用\d+来代表数字,后面的参数为’’,就是空,即代替数字。还有时候文本不太好解析,那么可以用这个方法将引起麻烦的部分去掉(灵活使用)。
1.6compile()
compile()方法可以将正则字符串编译成正则表达式对象,以便于后面的匹配重复用,即compile()方法生成的对象可以被以上的所有方法继续解析。
举个例子:
import re
content1 = '2020-1-1 12:00'
content2 = '2020-1-2 12:00'
content3 = '2020-1-3 12:00'
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern,'',content1)
result2 = re.sub(pattern,'',content2)
result3 = re.sub(pattern,'',content3)
print(result1,result2,result3)
运行结果为:
2020-1-1 2020-1-2 2020-1-3
这里我们想去掉时间后面的具体时间,只保留年月日,那么我们就使用compile方法,产生的对象是正则表达式对象,可以后面使用。而且compile方法可以加入修饰符,加上之后,后面的处理就不用加了。compile方法相当于给正则表达式加了一层封装,以便更好地复用。
2.使用XPath
XPath作为一款将近20年,被人广泛使用的库,功能十分强大,提供了非常简洁明了的路径选择表达式。话不多说,直接开始。
2.1常用规则
表达式 | 描述 |
---|---|
/ | 从当前节点选取直接子节点 |
// | 从前节点选取子孙节点 |
. | 选取当前节点 |
… | 选取当前节点的父节点 |
@ | 选取属性 |
示例:
//title[@lang=‘eng’]
这个代表选择所有名称为title,同时属性为lang的值为eng的节点。
后面会通过Python的lxml库,利用XPath进行HTML解析
2.2实例引入
2.2.1etree.HTML()
如下:
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>
'''
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))
输出为:
<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>
这里的etree.HTML()方法是转换对象类型,这个方法可以直接解析服务器返回的数据,然后torstring()方法可以输出修正后的HTML代码,但是是bytes类型,然后用decode()方法转换成str类型。不过你如果只想将返回来的数据使用XPath方法来解析的话,用etree.HTML()方法就够了,用完就可以直接用XPath方法了。
2.2.2etree.parse()方法
如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
运行如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<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>
这里的test.txt里面就是上面例子的html代码。
etree.parse()方法可以直接接受一个本地文件来进行解析。当然这个文件也可以是html文件格式,多了一个DOCTYPE的声明。不过对解析无影响。
2.3所有节点
如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//*')
print(result)
运行结果为:
[<Element html at 0x3939b48>, <Element body at 0x3939b20>, <Element div at 0x3939af8>, <Element ul at 0x3939ad0>, <Element li at 0x3939828>, <Element a at 0x3939698>, <Element li at 0x39396c0>, <Element a at 0x39396e8>, <Element li at 0x39394b8>, <Element a at 0x3939490>, <Element li at 0x3939328>, <Element a at 0x3939300>, <Element li at 0x39390d0>, <Element a at 0x39390a8>]
这里*代表匹配所有节点,也就是说整个HTML文本中的所有节点都会被获取。可以看到,返回的是一个列表,每一个元素都是Element类型,其后跟了节点的名称,如html,body,div,ul等等。
当然这里*可以用ul,li,div来代替,那么会获取对应的节点。由于输出的结果是列表,要取出其中一个,可以用中括号加索引,如[0].
2.4子节点
代码如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//li/a')
print(result)
运行结果如下:
[<Element a at 0x3059b20>, <Element a at 0x3059af8>, <Element a at 0x3059ad0>, <Element a at 0x3059828>, <Element a at 0x3059698>]
这样就获取了所有li节点下的a节点 。这里 // 和 / 的区别要注意。
2.5父节点
如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/../@class')
print(result)
运行结果:
['item-1']
这里的/…/可以换成parent::,效果一样:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//a[@href="link4.html"]/parent::*/@class')
print(result)
结果与上面一样,这里加上*是因为parent::的作用返回上一级,但是并不锁定到任意节点,所以要加上,不然就会报错。
2.6属性匹配
如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]')
print(result)
运行结果为:
[<Element li at 0x2b99af8>, <Element li at 0x2b99ad0>]
这里限制了2个地方,一是li节点,二是class属性要为item-0。那么属性匹配的方法就是如此。
2.7文本获取
上面方法虽然可以获得元素,但是并不是我们想要的文本,文本获取方法如下:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/a/text()')
print(result)
运行结果如下:
['first item', 'fifth item']
这里在最后加上text()就能获取节点里的文本
2.8属性获取
获取节点的属性方法:
from lxml import etree
html = etree.parse('./test.txt',etree.HTMLParser())
result = html.xpath('//li/a/@href')
print(result)
运行结果如下:
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
这里就可以获取了所有li节点下a节点的href属性,并以列表形式返回。
2.9属性多值匹配
举例如下:
from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[@class="li"]/a/text()')
print(result)
运行结果如下:
[]
因为这里的li节点的class属性有两个值li和li-first,如果还想用之前的属性匹配获取,就无法匹配了,需要使用contains()函数,代码如下:
from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li")]/a/text()')
print(result)
运行结果如下:
['first item']
这个方法在某个属性有多个值时使用。
2.10多属性匹配
我们还可能遇到根据多个属性确定一个节点的情况
如下:
from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li") and @name= "item"]/a/text()')
print(result)
运行结果如下:
['first item']
这里的li节点有多个属性,要定位就需要用and 来连接两个属性值。这里的and是XPath里的运算符,还有很多运算符。
运算符 | 描述 |
---|---|
or | 或 |
and | 与 |
mod | 计算除法的余数 |
div | 除法 |
还有类似<,>,=等运算都能使用,除此之外,还有一个特殊的就是
//book | //cd
这里的| 作用为返回所有拥有book和cd元素的节点集
本人基本没用过,0.0
2.11按序选择
我们还可以利用中括号传入索引的方法获得特定次序的节点:
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>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/a/text()')
print(result)
result = html.xpath('//li[last()]/a/text()')
print(result)
result = html.xpath('//li[position()<3]/a/text()')
print(result)
result = html.xpath('//li[last()-2]/a/text()')
print(result)
运行结果如下:
['first item']
['fifth item']
['first item', 'second item']
['third item']
这里用了last(),position(),还有加减法的应用,看你自己需求来用。
2.12节点轴选择
提供节点轴的选择方法,包括父元素,兄弟元素,祖先元素等等
from lxml import etree
text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html"><span>first item<span></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>
'''
html = etree.HTML(text)
result = html.xpath('//li[1]/ancestor::*')
print(result)
result = html.xpath('//li[1]/ancestor::div')
print(result)
result = html.xpath('//li[1]/attribute::*')
print(result)
result = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result)
result = html.xpath('//li[1]/descendant::span')
print(result)
result = html.xpath('//li[1]/following::*[2]')
print(result)
result = html.xpath('//li[1]/following-sibling::*')
print(result)
运行结果如下:
[<Element html at 0x2ece418>, <Element body at 0x2f09b20>, <Element div at 0x2f09af8>, <Element ul at 0x2f09ad0>]
[<Element div at 0x2f09af8>]
['item-0']
[<Element a at 0x2f09b20>]
[<Element span at 0x2f09ad0>, <Element span at 0x2f09af8>]
[<Element a at 0x2f09b48>]
[<Element li at 0x2f09ad0>, <Element li at 0x2f09af8>, <Element li at 0x2f09b20>, <Element li at 0x2f09aa8>]
第一次选择,调用ancestor轴,获得所有祖先元素,这里的body,html在文本中没有,是在etree.HTML方法后被补全的。
第二次选择,加入限定条件,在冒号后面加了div,这样得到的结果就只有div这个祖先节点。
第三次选择,调用attribute轴,又加上了*,就可以获取节点的所有属性值。
第四次选择,调用child轴,可以获取所有直接的子节点,这里加上了限定条件,获取href属性为link1.html的a节点
第五次选择,调用descendant轴,可以获取所有子孙节点,然后加了限定条件获取span节点,所以返回的结果只包含span节点而不包含a节点
第六次选择,调用following轴获取当前节点之后的所有节点,注意:这里的之后的所有节点,是不包括当前节点中包含的节点,是从下一个同级节点开始算第一个。加了限定条件[2],获取第二个,还需注意:当你加入索引之后,*也是不能省的。
第七次选择,调用following-sibling轴,可以获取当前节点之后的所有同级节点,这里使用了全部匹配。
3.使用Beautiful Soup
是一款强大的解析工具,只需要几条语句,就可以完成网页中的元素解析。
3.1基本用法
代码如下:
from bs4 import BeautifulSoup
html = '''
<title>爬虫第二次笔记</title>
<div>
<ul>
<li class="item-0"><a href="link1.html"><span>first item<span></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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.prettify())
print(soup.title.string)
运行结果如下:
<html>
<head>
<title>
爬虫第二次笔记
</title>
</head>
<body>
<div>
<ul>
<li class="item-0">
<a href="link1.html">
<span>
first item
<span>
</span>
</span>
</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>
爬虫第二次笔记
这里声明了变量html,是一个字符串。但并不是完整的HTML字符串,因为没有html,body部分,接着把它当成第一个参数传给Beautifulsoup对象,第二个参数为解析器的类型(这里用的lxml)
注意:解析器还有xml,html.parser,html5lib,每一个都有自己的特点,比如xml速度快,html.parser速度适中,文档容错率强,而html5lib拥有最好的容错性,一浏览器的方式解析文档,生成HTML5格式的文档。
承接上文:此时就完成了BeautifulSoup对象的初始化,将对象赋值给soup变量。
接下来就可以调用soup的方法来解析这串HTML代码了。这里调用了prettify方法,该方法可以把要解析的字符串以标准的缩进格式输出,需要注意的是,输出的字符串是已经自动更正格式的字符串,这一步不是prettify()方法完成的,是在初始化对象的时候就完成了。
然后调用了soup.title.string,这实际上是输出了title节点,再调用string属性就是title节点的文本信息。
3.2节点选择器
3.2.1选择元素
示例如下:
from bs4 import BeautifulSoup
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<p class="item-0" name='pachong'>first item</p>
<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>'''
soup = BeautifulSoup(html,'lxml')
print(type(soup))
print(soup.title)
print(type(soup.title))
print(soup.title.string)
print(soup.head)
运行结果如下:
<title>爬虫第二次笔记</title>
<class 'bs4.element.Tag'>
爬虫第二次笔记
<head><title>爬虫第二次笔记</title></head>
<p class="item-0"><a href="link1.html"><span>first item<span></span></span></a></p>
这里首先打印输出title节点的选择结果,输出title节点的文字内容
接下来,输出它的类型,是bs4.element.Tag类型,经过选择器选择后,选择的结果都是这种tag类型。tag具有一些属性,比如string属性,调用该属性,可以获得节点的文本内容.
然后我们选择p节点的时候,我们只获得了第一个p节点的内容,后面几个p节点并没有选到。也就是说,有多个节点的时候,这种选择方式只会选择第一个匹配的节点。
3.2.2提取信息
上面演示了调用string属性来获取文本的值,那么如何获取节点属性的值呢?
第一种方法:获取名称
选取title节点,然后调用name属性就可以得到节点名称。
print(soup.title.name)
运行后:
title
第二种方法:获取属性
print(soup.p.attrs)
print(soup.p.attrs['name'])
运行结果为:
{'class': ['item-0'], 'name': 'pachong'}
pachong
调用attrs获取所有属性,attrs的返回结果是字典形式,把选择的节点的所有属性和属性值组合成一个字典。那么字典的获取方式,当然是调用键来获取值啦。
当然也可以不用写attrs,直接再节点元素后面加中括号,传入属性名就可以获取属性值了。
print(soup.p['name'])
print(soup.p['class'])
运行结果为:
pachong
['item-0']
第三种方法:获取内容
可以获取string属性获取节点元素包含的文本内容。
print(soup.p.string)
运行结果为:
first item
这里选择的p节点是第一个p节点,获取的文本也是第一个p节点里面的文本。
3.2.4嵌套选择
from bs4 import BeautifulSoup
html = '''
<html><head><title>爬虫第二次笔记</title></head><body>
'''
soup = BeautifulSoup(html,'lxml')
print(soup.head.title)
print(type(soup.head.title))
print(soup.head.title.string)
运行结果为:
<title>爬虫第二次笔记</title>
<class 'bs4.element.Tag'>
爬虫第二次笔记
这里调用head之后再次调用title而选择的title节点元素。然后打印他的类型,仍然是bs4.element.Tag类型。也就是说,我们在tag类型的基础上再次选择得到的依然是tag类型,每次返回的结果都相同,所以这样就可以做嵌套选择。
3.2.5关联选择
有时候不能做到一步就选到想要的节点元素,需要先选中某一个节点元素,然后以它为基础再选择它的子节点,父节点,兄弟节点等,这里就来介绍这些节点元素。
子节点和子孙节点:
from bs4 import BeautifulSoup
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.div.contents)
运行结果如下:
['\n', <ul>
<span>first item<span>
<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>
</span></span></ul>, '\n']
返回结果是列表形式。p节点既包含文本,又包含节点,最后会以列表形式统一返回。
还有一种方式:
from bs4 import BeautifulSoup
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.div.children)
for i,child in enumerate(soup.div.children):
print(i, child)
运行结果为:
<list_iterator object at 0x03854650>
0
1 <ul>
<span>first item<span>
<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>
</span></span></ul>
2
这里是调用了children属性获取所有直接子节点,返回结果是生成器类型,然后用for循环输出相应的内容。
注意:这里的enumerate()函数使用的很多,需要掌握。生成序列号和值。
下面的就不再列出具体代码。
如果要得到子孙节点的话,可以调用descendants属性。
父节点和祖先节点:
父节点:parent属性 (返回列表形式)
祖先节点:parents属性 (返回列表形式的生成器类型)
兄弟节点:
next_sibling:获取下一个兄弟元素
previous_sibling:获取上一个兄弟元素
next_siblings:获取后面的所有兄弟元素
previous_siblings:获取前面的所有兄弟元素
提取信息:
from bs4 import BeautifulSoup
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.a.parents)
print(list(soup.a.parents)[0].attrs['class'])
运行结果如下:
<generator object parents at 0x031DB930>
['item-1']
这里如之前的提取信息的方法相似,再定位到具体的元素后,再调用attrs属性,即可获取相应属性,当然还又string等属性。
3.3方法选择器
前面所讲的是通过属性来选择,如果现在我们需要进行比较复杂的选择,就会不够灵活。这里我们还有查询方法,比如find_all()和find()方法。
fing_all()方法:
from bs4 import BeautifulSoup
import re
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.find_all(name='li'))
print(soup.find_all(attrs={'class':'item-0'}))
print(soup.find_all(text=re.compile('item')))
运行结果如下:
[<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>]
[<li class="item-0"><a href="link5.html">fifth item</a></li>]
['first item', 'second item', 'third item', 'fourth item', 'fifth item']
这里使用了三种方法,第一种方法直接获取节点名称为li的节点;第二种方法获取class属性值为item-0的节点;第三种方法使用re.compile方法获取文本中包含item的文本。
find()方法:
find_all()与find()方法的区别在于,后者不再返回列表,是返回第一个匹配的元素。还有一些方法与find_all()和find()方法完全相同。
find_parents()和find_parent():前者返回所有祖先元素,后者返回直接父节点。
find_next_siblings()和find_next_sibling():前者返回后面所有的兄弟节点,后者返回后面第一个兄弟节点。
find_previous_siblings()和find_previous_sibling():前者返回前面所有的兄弟节点,后者返回前面第一个兄弟节点。
find_all_next()和find_next():前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点。
find_all_previous()和find_previous():前者返回节点后所有符合条件的节点,后者返回第一次符合条件的节点。
3.4CSS选择器
这个选择器如果对Web开发熟悉的话,那么对CSS选择器肯定也不陌生。这个方法也很简单,直接调用select()方法,传入相应的CSS选择器即可。
倘若对CSS选择器不了解,别急,下一章会有比较完整的教学。
from bs4 import BeautifulSoup
import re
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
soup = BeautifulSoup(html,'lxml')
print(soup.select('.item-1'))
print(soup.select('div ul'))
print(soup.select('#item-10'))
运行结果如下:
[<li class="item-1"><a href="link2.html">second item</a></li>, <li class="item-1"><a href="link4.html">fourth item</a></li>]
[<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span></span></ul>]
[<li class="item-inactive" id="item-10"><a href="link3.html">third item</a></li>]
这里使用 . ,# ,和直接定位节点三种方法。
嵌套选择:
比如我们获取到ul节点,然后遍历里面每一个li节点。
from bs4 import BeautifulSoup
import re
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
soup = BeautifulSoup(html,'lxml')
for ul in soup.select('ul'):
print(ul.select('li'))
运行结果为:
[<li class="item-1"><a href="link2.html">second item</a></li>, <li class="item-inactive" id="item-10"><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节点下的所有li节点组成的列表。
获取属性:
from bs4 import BeautifulSoup
import re
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
soup = BeautifulSoup(html,'lxml')
for li in soup.select('li'):
print(li['class'])
运行结果为:
['item-1']
['item-inactive']
['item-1']
['item-0']
这里也可以用print(li.attrs[‘class’]), 效果一样。
获取文本:
from bs4 import BeautifulSoup
import re
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
soup = BeautifulSoup(html,'lxml')
for li in soup.select('li'):
print(li.string)
运行结果为:
second item
third item
fourth item
fifth item
这里的li.string也可以换成li.get_text().
4.使用pyquery(写前端的小伙伴会比较喜欢)
4.1三种初始化方法
4.1.1字符串初始化
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul>
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
doc = pq(html)
print(doc('li'))
运行结果如下:
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
只用一个pq函数,就完成了初始化,将初始化的对象传入CSS选择器。在这个实例中,我们传入li节点,这样就选择了所有的li节点。
4.1.2URL初始化
from pyquery import PyQuery as pq
doc = pq(url = 'https://cuiqingcai.com')
print(doc('title'))
运行结果为:
<title>静觅丨崔庆才的个人站点</title>
这里PyQuery对象会首先请求这个URL,然后用得到的HTML内容完成初始化,就相当于用网页的源代码以字符串形式传递给PyQuery类来初始化。但是实际使用肯定不会这么简单,因为网站基本都有反爬虫的。
4.1.3文件初始化
from pyquery import PyQuery as pq
doc = pq(filename='demo.html')
print(doc('li'))
这里需要本地有一个HTML文件demo.html.
这里给了三种初始化方法,最常用肯定是第一种,可以直接对接基本库传来的数据。
4.2基本CSS选择器
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
doc = pq(html)
print(doc('#container li'))
print(type(doc('#container li')))
运行结果为:
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
<class 'pyquery.pyquery.PyQuery'>
我们初始化一个pq对象后,掺入了一个CSS选择器 #container li,他的意思是选取id为container的节点,然后在选取该节点内部所有的li节点,是一个层层递进的关系。
输出的类型是PyQuery类型。
4.3查找节点
4.3.1子节点
寻找子节点时,需要用到find()方法 ,代码如下:
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>'''
doc = pq(html)
items = doc('#container')
print(type(items))
print(items)
lis = items.find('li')
print(type(lis))
print(lis)
运行结果为:
<class 'pyquery.pyquery.PyQuery'>
<ul id="container">
<span>first item<span>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span></span></ul>
<class 'pyquery.pyquery.PyQuery'>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
我们选取id为container的节点,然后调用find()方法,传入CSS选择器,选取内部的li节点,最后打印输出。可以发现,find()方法会把所有符合条件的节点选择出来与上面beautifulsoup不同噢。
其实find()的查找范围时节点的所有子孙节点,而如果我们只想找子孙节点,那么可以用children()方法 。
find()和children()方法用法相同,唯一不同的是所选范围。
4.3.2父节点
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
items = doc('#item-10')
lis = items.parent()
print(lis)
运行结果如下:
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span>
parent()方法获取直接父节点,还有parents()方法获取祖先节点,可以直接在parents()方法中加入CSS选择器来获取某一个祖先节点。
4.3.3兄弟节点
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
items = doc('.item-inactive')
lis = items.siblings()
print(lis)
运行结果为:
<li class="item-1"><a href="link2.html">second 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>
调用siblings()属性,即可获取兄弟节点,然后如果想选取某一个兄弟节点,就在siblings()方法中加入CSS选择器。
4.4遍历
我们发现pyquery的返回结果可能是多个节点,也可能是一个节点,但并没有返回像beautifulsoup那样的列表。对于多个节点的结果,我们就需要遍历来获取,需要使用到items方法:
<class 'generator'>
<li class="item-1"><a href="link2.html">second item</a></li>
<class 'pyquery.pyquery.PyQuery'>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<class 'pyquery.pyquery.PyQuery'>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<class 'pyquery.pyquery.PyQuery'>
运行结果为:
<class 'generator'>
<li class="item-1"><a href="link2.html">second item</a></li>
<class 'pyquery.pyquery.PyQuery'>
<li class="item-1"><a href="link4.html">fourth item</a></li>
<class 'pyquery.pyquery.PyQuery'>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<class 'pyquery.pyquery.PyQuery'>
调用items()方法后,会得到一个生成器,遍历一哈,就可以获取每个节点了!
4.5获取信息
4.5.1获取属性
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
items = doc('.item-inactive')
lis = items.siblings().items()
for li in lis:
print(li.attr('class'))
运行结果为;
item-1
item-1
item-0
这里又出现了attr,注意这里的和beautifulsoup中的attrs不同噢,这里的li.attr(‘class’)也可以换成li.attr.class,效果一样。
4.5.2获取文本
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
items = doc('.item-inactive')
lis = items.siblings().items()
for li in lis:
print(li.text())
print(li.html())
运行结果为:
second item
<a href="link2.html">second item</a>
fourth item
<a href="link4.html">fourth item</a>
fifth item
<a href="link5.html">fifth item</a>
这里有两个方法,text()方法是获取节点内部的文本信息,html()是获取节点内的所有HTML文本。
他们的区别在于,如果我们的对象是多个节点,text()方法会返回所有节点文本,而HTML()方法只会返回第一个节点的内部HTML文本。
4.6节点操作
这个节点操作是pyquery不同于其他方法的最具特点的位置,在pyquery中,我们可以对节点进行动态修改,比如为某个几点增加,或者删除一个节点。
4.6.1add_class()和remove_class()
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
li = doc('.item.inactive')
print(li)
li.remove_class('inactive')
print(li)
li.add_class('inactive')
print(li)
运行结果为:
<li class="item inactive" id="item-10"><a href="link3.html">third item</a></li>
<li class="item" id="item-10"><a href="link3.html">third item</a></li>
<li class="item inactive" id="item-10"><a href="link3.html">third item</a></li>
这里先选中第二个li节点,然后remove_class方法,再add_class方法。看到结果,这个方法就可以改变节点的class属性。
4.6.2attr,text和html
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item inactive" id="item-10"><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>
</span>
</ul>
</div>'''
doc = pq(html)
li = doc('.item.inactive')
print(li)
li.attr('name','link')
print(li)
li.text('changed')
print(li)
li.html('<span>changed</span>')
print(li)
运行结果为:
<li class="item inactive" id="item-10"><a href="link3.html">third item</a></li>
<li class="item inactive" id="item-10" name="link"><a href="link3.html">third item</a></li>
<li class="item inactive" id="item-10" name="link">changed</li>
<li class="item inactive" id="item-10" name="link"><span>changed</span></li>
这里首先用attr()方法为li节点添加了一个name属性,第一个参数为属性名,第二个参数为属性值。然后调用text()和html()方法改变节点内部的内容。
注意:attr()方法如果只传入一个参数,就是获取这个属性值,传入两个参数,就是修改属性值。text()方法和html()方法如果不传参数,则是获取内部文本,传入参数,就是进行赋值。
4.6.3remove()
remove()方法可以删除某些节点:
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item inactive" id="item-10"><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 <p>第二次</p>item</a></li>
</span>
</ul>
</div>'''
doc = pq(html)
li = doc('.item-0')
print(1,li.text())
li.find('p').remove()
print(2,li.text())
运行结果为:
1 fifth
第二次
item
2 fifth item
这里删除了li节点中的p节点,即将p节点和内部所有东西全部删除。
4.7伪类选择器
CSS选择器强大的原因,一个重要的原因就是它支持多种多样的伪类选择器,例如选择第一个节点,最后一个节点,奇偶数节点等等。
from pyquery import PyQuery as pq
html = '''
<head><title>爬虫第二次笔记</title></head>
<div>
<ul id="container">
<span>first item
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item inactive" id="item-10"><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 <p>第二次</p>item</a></li>
</span>
</ul>
</div>'''
doc = pq(html)
li = doc('li:first-child')
print(li)
li = doc('li:last-child')
print(li)
li = doc('li:nth-child(2)')
print(li)
li = doc('li:gt(2)')
print(li)
li = doc('li:nth-child(2n)')
print(li)
li = doc('li:contains(second)')
print(li)
运行结果为:
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0"><a href="link5.html">fifth <p>第二次</p>item</a></li>
<li class="item inactive" id="item-10"><a href="link3.html">third item</a></li>
<li class="item-0"><a href="link5.html">fifth <p>第二次</p>item</a></li>
<li class="item inactive" id="item-10"><a href="link3.html">third item</a></li>
<li class="item-0"><a href="link5.html">fifth <p>第二次</p>item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
这里依次选择了第一个li节点,最后一个li节点,第三个li节点之后的li节点,偶数位置的li节点,包含second文本的li节点。
5结语
所有解析库讲解完毕,灵活使用多种方法来解析数据,是爬虫最重要的一环!