BeautifulSoup4
Beautiful Soup是python的一个HTML或XML的解析库,我们可以用它来方便的从网页中提取数据,它拥有强大的API和多样的解析方式。
Beautiful Soup的三个特点:
- Beautiful Soup提供一些简单的方法和python式函数,用于浏览,搜索和修改解析树,它是一个工具箱,通过解析文档为用户提供需要抓取的数据
- Beautiful Soup自动将转入稳定转换为Unicode编码,输出文档转换为UTF-8编码,不需要考虑编码,除非文档没有指定编码方式,这时只需要指定原始编码即可
- Beautiful Soup位于流行的Python解析器(如lxml和html5lib)之上,允许您尝试不同的解析策略或交易速度以获得灵活性
Beautiful Soup在解析时实际上是依赖解析器的,它除了支持python标准库中的HTML解析器外还支持第三方解析器如lxml
Beautiful Soup支持的解析器,以及它们的优缺点:
BeautifulSoup的基本用法
初始化
from bs4 import BeautifulSoup
soup = BeautifulSoup("<html>A Html Text</html>", "html.parser")
两个参数:第一个参数是要解析的html文本,第二个参数是使用那种解析器,对于HTML来讲就是html.parser,这个是bs4自带的解析器。
格式化输出
soup.prettify() # prettify 有括号和没括号都可以
对象
通过传入一段字符或一个文件句柄,BeautifulSoup的构造方法就能得到一个文档的对象,选择合适的解析器来解析文档,如手动指定将选择指定的解析器来解析文档,Beautiful Soup将复杂的HTML文档转换成一个复杂的树形结构,每个节点都是python对象,所有对象可以归纳为4种:Tag、NavigableString、BeautifulSoup、Comment
tag
Tag对象与 xml 或 html 原生文档中的 tag 相同。
from bs4 import BeautifulSoup
s = """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>这是前端day01学习</title>
</head>
<body>
<a href="http://www.baidu.com" target="_self">百度</a>
<span>这是一个文字</span>
<p>这是一个段落</p>
<label for="">这是一个label</label>
<input type="text">
<!--<input type="button" class="button" value="按钮">-->
<a href="#">回到顶部</a>
</body>
<script>
</script>
</html>
"""
soup = BeautifulSoup(s,"lxml")
tag = soup.a
print(tag)
"""
<a href="http://www.baidu.com" target="_self">百度</a>
"""
Name
每个 tag 都有自己的名字
soup = BeautifulSoup(s,"lxml")
tag = soup.name
print(tag)
"""
[document]
"""
Attributes
tag 的属性是一个字典
soup = BeautifulSoup(s,"lxml")
tag = soup.label
print(tag["name"])
print(tag.attrs)
"""
你好
{'name': '你好'}
"""
多值属性
最常见的多值属性是class,多值属性的返回 list
soup = BeautifulSoup('<p class="body strikeout"></p>')
print(soup.p['class']) # ['body', 'strikeout']
print(soup.p.attrs) # {'class': ['body', 'strikeout']}
如果某个属性看起来好像有多个值,但在任何版本的HTML定义中都没有被定义为多值属性,那么Beautiful Soup会将这个属性作为字符串返回。
soup = BeautifulSoup('<p id="my id"></p>', 'html.parser')
print(soup.p['id']) # 'my id'
Text
text 属性返回 tag 的所有字符串连成的字符串。
其他方法
tag.has_attr('id') # 返回 tag 是否包含 id 属性
当然,以上代码还可以写成 'id' in tag.attrs,之前说过,tag 的属性是一个字典。顺便提一下,has_key是老旧遗留的api,为了支持2.2之前的代码留下的。Python3已经删除了该函数。
NavigableString
字符串常被包含在 tag 内,Beautiful Soup 用 NavigableString 类来包装 tag 中的字符串。但是字符串中不能包含其他 tag。
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
s = soup.b.string
print(s) # Extremely bold
print(type(s)) # <class 'bs4.element.NavigableString'>
BeautifulSoup
BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象。但是 BeautifulSoup 对象并不是真正的 HTM L或 XML 的 tag,它没有attribute属性,name 属性是一个值为“[document]”的特殊属性。
Comment
Comment 一般表示文档的注释部分。
遍历
子节点
contents 属性
contents 属性返回所有子节点的列表,包括 NavigableString 类型节点。如果节点当中有换行符,会被当做是 NavigableString 类型节点而作为一个子节点。
NavigableString 类型节点没有 contents 属性,因为没有子节点。
soup = BeautifulSoup("""<div>
<span>test</span>
</div>
""")
element = soup.div.contents
print(element) # ['\n', <span>test</span>, '\n']
children 属性
children 属性跟 contents 属性基本一样,只不过返回的不是子节点列表,而是子节点的可迭代对象。
descendants 属性
descendants 属性返回 tag 的所有子孙节点。
string 属性
如果 tag 只有一个 NavigableString 类型子节点,那么这个 tag 可以使用 .string 得到子节点。
如果一个 tag 仅有一个子节点,那么这个 tag 也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同。
如果 tag 包含了多个子节点,tag 就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None。
soup = BeautifulSoup("""<div>
<p><span><b>test</b></span></p>
</div>
""")
element = soup.p.string
print(element) # test
print(type(element)) # <class 'bs4.element.NavigableString'>
特别注意,为了清楚显示,一般我们会将 html 节点换行缩进显示,而在BeautifulSoup 中会被认为是一个 NavigableString 类型子节点,导致出错。上例中,如果改成 element = soup.div.string 就会出错。
strings 和 stripped_strings 属性
如果 tag 中包含多个字符串,可以用 strings 属性来获取。如果返回结果中要去除空行,则可以用 stripped_strings 属性。
soup = BeautifulSoup("""<div>
<p> </p>
<p>test 1</p>
<p>test 2</p>
</div>
""", 'html.parser')
element = soup.div.stripped_strings
print(list(element)) # ['test 1', 'test 2']
父节点
parent 属性
parent 属性返回某个元素(tag、NavigableString)的父节点,文档的顶层节点的父节点是 BeautifulSoup 对象,BeautifulSoup 对象的父节点是 None。
parents 属性
parent 属性递归得到元素的所有父辈节点,包括 BeautifulSoup 对象。
兄弟节点
next_sibling 和 previous_sibling
next_sibling 返回后一个兄弟节点,previous_sibling 返回前一个兄弟节点。直接看个例子,注意别被换行缩进搅了局。
soup = BeautifulSoup("""<div>
<p>test 1</p><b>test 2</b><h>test 3</h></div>
""", 'html.parser')
print(soup.b.next_sibling) # <h>test 3</h>
print(soup.b.previous_sibling) # <p>test 1</p>
print(soup.h.next_sibling) # None
next_siblings 和 previous_siblings
next_siblings 返回后面的兄弟节点
previous_siblings 返回前面的兄弟节点
回退和前进
把html解析看成依次解析标签的一连串事件,BeautifulSoup 提供了重现解析器初始化过程的方法。
next_element 属性指向解析过程中下一个被解析的对象(tag 或 NavigableString)。
previous_element 属性指向解析过程中前一个被解析的对象。
另外还有next_elements 和 previous_elements 属性,不赘述了。
搜索
1、过滤器
介绍 find_all() 方法前,先介绍一下过滤器的类型,这些过滤器贯穿整个搜索的API。过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中。
示例使用的 html 文档如下:
html = """
<div>
<p class="title"><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></p>
</div>
"""
soup = BeautifulSoup(html, 'html.parser')
字符串
查找所有的标签
soup.find_all('b') # [<b>The Dormouse's story</b>]
正则表达式
传入正则表达式作为参数,返回满足正则表达式的标签。下面例子中找出所有以b开头的标签。
soup.find_all(re.compile("^b")) # [<b>The Dormouse's story</b>]
列表
传入列表参数,将返回与列表中任一元素匹配的内容。下面例子中找出所有标签和标签。
soup.find_all(["a", "b"])
True
True可以匹配任何值,下面的代码查找到所有的tag,但是不会返回字符串节点。
soup.find_all(True)
方法
如果没有合适过滤器,那么还可以自定义一个方法,方法只接受一个元素参数,如果这个方法返回True表示当前元素匹配被找到。下面示例返回所有包含 class 属性但不包含 id 属性的标签。
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
print(soup.find_all(has_class_but_no_id))
返回结果:
[<p class="title"><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></p>]
这个结果乍一看不对,标签含有 id 属性,其实返回的 list 中只有2个元素,都是
2、find 和 find_all
搜索当前 tag 的所有 tag 子节点,并判断是否符合过滤器的条件
语法:
find(name=None, attrs={}, recursive=True, text=None, **kwargs)
find_all(name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)
参数:
name:查找所有名字为 name 的 tag,字符串对象会被自动忽略掉。上面过滤器示例中的参数都是 name 参数。当然,其他参数中也可以使用过滤器。
attrs:按属性名和值查找。传入字典,key 为属性名,value 为属性值。
recursive:是否递归遍历所有子孙节点,默认 True。
text:用于搜索字符串,会找到 .string 方法与 text 参数值相符的tag,通常配合正则表达式使用。也就是说,虽然参数名是 text,但实际上搜索的是 string 属性。
limit:限定返回列表的最大个数。
kwargs:如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作 tag 的属性来搜索。这里注意,如果要按 class 属性搜索,因为 class 是 python 的保留字,需要写作 class_。
[
find( name , attrs , recursive , text , **kwargs ):它返回的是单个元素,也就是第一个匹配的元素,类型依然是tag类型
参数同find_all()一样
另外还有许多查询方法,其用法和前面介绍的find_all()方法完全相同,只不过查询范围不同,参数也一样
find_parents(name , attrs , recursive , text , **kwargs )和find_parent(name , attrs , recursive , text , **kwargs ):前者返回所有祖先节点,后者返回直接父节点
find_next_siblings(name , attrs , recursive , text , **kwargs )和find_next_sibling(name , attrs , recursive , text , **kwargs ):对当前tag后面的节点进行迭代,前者返回后面的所有兄弟节点,后者返回后面第一个兄弟节点
find_previous_siblings(name , attrs , recursive , text , **kwargs )和find_previous_sibling(name , attrs , recursive , text , **kwargs ):对当前tag前面的节点进行迭代,前者返回前面的所有兄弟节点,后者返回前面的第一个兄弟节点
find_all_next(name , attrs , recursive , text , **kwargs )和find_next(name , attrs , recursive , text , **kwargs ):对当前tag之后的tag和字符串进行迭代,前者返回所有符合条件的节点,后者返回第一个符合条件的节点
find_all_previous()和find_previous():对当前tag之前的tag和字符串进行迭代,前者返回节点后所有符合条件的节点,后者返回第一个符合条件的节点
]
Tag 的有些属性在搜索中不能作为 kwargs 参数使用,比如 html5 中的 data-* 属性。
data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
print(data_soup.find_all(data-foo="value"))
# SyntaxError: keyword can't be an expression
但是可以通过 attrs 参数传递:
data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
print(data_soup.find_all(attrs={"data-foo": "value"}))
# [<div data-foo="value">foo!</div>]
而按 class_ 查找时,只要一个CSS类名满足即可,如果写了多个CSS名称,那么顺序必须一致,而且不能跳跃。以下示例中,前三个可以查找到元素,后两个不可以。
css_soup = BeautifulSoup('<p class="body bold strikeout"></p>')
print(css_soup.find_all("p", class_="strikeout"))
print(css_soup.find_all("p", class_="body"))
print(css_soup.find_all("p", class_="body bold strikeout"))
# [<p class="body strikeout"></p>]
print(css_soup.find_all("p", class_="body strikeout"))
print(css_soup.find_all("p", class_="strikeout body"))
# []
3、像调用find_all()一样调用tag
find_all() 几乎是 BeautifulSoup 中最常用的搜索方法,所以我们定义了它的简写方法。BeautifulSoup 对象和 tag 对象可以被当作一个方法来使用,这个方法的执行结果与调用这个对象的 find_all() 方法相同,下面两行代码是等价的:
soup.find_all('b')
soup('b')
4、其他搜索方法
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() 返回节点前所有符合条件的节点
CSS选择器
BeautifulSoup支持大部分的CSS选择器,这里直接用代码来演示。
from bs4 import BeautifulSoup
html = """
<html>
<head><title>标题</title></head>
<body>
<p class="title" name="dromouse"><b>标题</b></p>
<div name="divlink">
<p>
<a href="http://example.com/1" class="sister" id="link1">链接1</a>
<a href="http://example.com/2" class="sister" id="link2">链接2</a>
<a href="http://example.com/3" class="sister" id="link3">链接3</a>
</p>
</div>
<p></p>
<div name='dv2'></div>
</body>
</html>
"""
soup = BeautifulSoup(html, 'lxml')
# 通过tag查找
print(soup.select('title')) # [<title>标题</title>]
# 通过tag逐层查找
print(soup.select("html head title")) # [<title>标题</title>]
# 通过class查找
print(soup.select('.sister'))
# [<a class="sister" href="http://example.com/1" id="link1">链接1</a>,
# <a class="sister" href="http://example.com/2" id="link2">链接2</a>,
# <a class="sister" href="http://example.com/3" id="link3">链接3</a>]
# 通过id查找
print(soup.select('#link1, #link2'))
# [<a class="sister" href="http://example.com/1" id="link1">链接1</a>,
# <a class="sister" href="http://example.com/2" id="link2">链接2</a>]
# 组合查找
print(soup.select('p #link1')) # [<a class="sister" href="http://example.com/1" id="link1">链接1</a>]
# 查找直接子标签
print(soup.select("head > title")) # [<title>标题</title>]
print(soup.select("p > #link1")) # [<a class="sister" href="http://example.com/1" id="link1">链接1</a>]
print(soup.select("p > a:nth-of-type(2)")) # [<a class="sister" href="http://example.com/2" id="link2">链接2</a>]
# nth-of-type 是CSS选择器
# 查找兄弟节点(向后查找)
print(soup.select("#link1 ~ .sister"))
# [<a class="sister" href="http://example.com/2" id="link2">链接2</a>,
# <a class="sister" href="http://example.com/3" id="link3">链接3</a>]
print(soup.select("#link1 + .sister"))
# [<a class="sister" href="http://example.com/2" id="link2">链接2</a>]
# 通过属性查找
print(soup.select('a[href="http://example.com/1"]'))
# ^ 以XX开头
print(soup.select('a[href^="http://example.com/"]'))
# * 包含
print(soup.select('a[href*=".com/"]'))
# 查找包含指定属性的标签
print(soup.select('[name]'))
# 查找第一个元素
print(soup.select_one(".sister"))
tag修改方法
Beautiful Soup的强项是文档的搜索功能,修改功能使用场景不是很多只做简单介绍,要了解更多修改方法请前往Beautiful Soup官方文档查看。
Beautiful Soup可以实现改变tag标志的属性的值,添加或删除属性和内容,下面介绍一些常用的方法
In [26]: markup='<a href="http://www.baidu.com/">baidu</a>'
In [28]: soup=BeautifulSoup(markup,'lxml')
In [29]: soup.a.string='百度'
In [30]: soup.a
Out[30]: <a href="http://www.baidu.com/">百度</a>
#如果a节点下包括子也将被覆盖掉
Tag.append() 方法想tag中添加内容,就好像Python的列表的 .append() 方法:
In [30]: soup.a
Out[30]: <a href="http://www.baidu.com/">百度</a>
In [31]: soup.a.append('一下')
In [32]: soup.a
Out[32]: <a href="http://www.baidu.com/">百度一下</a>
new_tag()方法用于创建一个tag标签
In [33]: soup=BeautifulSoup('<b></b>','lxml')
In [34]: new_tag=soup.new_tag('a',href="http://www.python.org") #创建tag,第一个参数必须为tag的名称
In [35]: soup.b.append(new_tag) #添加到b节点下
In [36]: new_tag.string='python' #为tag设置值
In [37]: soup.b
Out[37]: <b><a href="http://www.python.org">python</a></b>
其他方法:
insert()将元素插入到指定的位置
inert_before()在当前tag或文本节点前插入内容
insert_after()在当前tag或文本节点后插入内容
clear()移除当前tag的内容
extract()将当前tag移除文档数,并作为方法结果返回
prettify()将Beautiful Soup的文档数格式化后以Unicode编码输出,tag节点也可以调用
get_text()输出tag中包含的文本内容,包括子孙tag中的内容
soup.original_encoding 属性记录了自动识别的编码结果
from_encoding:参数在创建BeautifulSoup对象是可以用来指定编码,减少猜测编码的运行速度
#解析部分文档,可以使用SoupStrainer类来创建一个内容过滤器,它接受同搜索方法相同的参数
from bs4 import BeautifulSoup,SoupStrainer
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><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>
ddd
<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>
<span>中文</span>
"""
only_a_tags = SoupStrainer('a') #过滤器
soup=BeautifulSoup(html_doc,'lxml',parse_only=only_a_tags)
print(soup.prettify())
#
<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异常处理:
HTMLParser.HTMLParseError:malformed start tag
HTMLParser.HTMLParseError:bad end tag 这个两个异常都是解析器引起的,解决方法是安装lxml或者html5lib