英文(中文)版文档:https://www.crummy.com/software/BeautifulSoup/bs4/doc/
1、Beautiful Soup 简介
bs4 简介
Beautiful Soup 是 python的一个库,主要用来解析 html 或者 xml,然后生成 dom 结构树。并且可以自动将输入文档转换为 Unicode 编码,输出文档转换为 utf-8 编码。
Beautiful Soup 3 已经停止开发,推荐使用 Beautiful Soup 4
- bs3 以及更早的都是 驼峰方式 的 方法。在 bs4 中还可以使用,但是不推荐。
- bs4 都是使用 下划线方式 的 方法,为了保持好的编程风格,推荐使用下划线命名的方法。
bs4 安装
pypi :https://pypi.org/project/beautifulsoup4/
pip install beautifulsoup4
html5lib 是纯Python实现的解析器,解析方式与浏览器相同:pip install html5lib
Beautiful Soup 支持 Python 标准库中的 HTML 解析器,还支持一些第三方的解析器,默认会使用 Python默认的解析器,lxml 解析器更加强大,速度更快,推荐安装。
解析器 | 使用方法 | 优势 | 劣势 |
Python标准库 | BeautifulSoup(markup, “html.parser”) | Python的内置标准库 执行速度适中 文档容错能力强 | Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差 |
lxml HTML 解析器 | BeautifulSoup(markup, “lxml”) | 速度快 文档容错能力强 | 需要安装C语言库 |
lxml XML 解析器 | BeautifulSoup(markup, [“lxml”, “xml”]) BeautifulSoup(markup, “xml”) | 速度快 唯一支持XML的解析器 | 需要安装C语言库 |
html5lib | BeautifulSoup(markup, “html5lib”) | 最好的容错性 以浏览器的方式解析文档 生成HTML5格式的文档 | 速度慢 |
2、创建 Beautiful Soup 对象
BeautifulSoup 用来解析 html 或者 xml,然后生成 dom 结构树。通过源码可以看到, BeautifulSoup 类 继承自 Tag类,Tag类 又是继承自 PageElement类。
PageElement 类
class PageElement(object):
"""Contains the navigational information for some part of the page:
that is, its current location in the parse tree.
NavigableString, Tag, etc. are all subclasses of PageElement.
"""
Tag 类
class Tag(PageElement):
"""Represents an HTML or XML tag that is part of a parse tree, along
with its attributes and contents.
When Beautiful Soup parses the markup <b>penguin</b>, it will
create a Tag object representing the <b> tag.
"""
def __init__(self, parser=None, builder=None, name=None, namespace=None,
prefix=None, attrs=None, parent=None, previous=None,
is_xml=None, sourceline=None, sourcepos=None,
can_be_empty_element=None, cdata_list_attributes=None,
preserve_whitespace_tags=None,
interesting_string_types=None,
namespaces=None
):
BeautifulSoup 类
class BeautifulSoup(Tag):
"""
BeautifulSoup 就是解析 HTML 或者 XML 后的 dom 结构.
在BeautifulSoup对象上调用的大多数方法都继承自PageElement或Tag
"""
def __init__(self, markup="", features=None, builder=None,
parse_only=None, from_encoding=None, exclude_encodings=None,
element_classes=None, **kwargs):
所以 BeautifulSoup 对象上调用的大多数方法都继承自 PageElement 或 Tag。
示例:bs4 指定编码
import requests
from bs4 import BeautifulSoup
resp = requests.get('https://www.gushiwen.cn/')
html_string = resp.text
soup_1 = BeautifulSoup(html_string, from_encoding='utf-8')
# soup_2 = BeautifulSoup(open('index.html'))
print(soup_1.prettify())
示例:bs4 的使用
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title>网页的title</title></head>
<body>
<p class="p_class"><b>第一个p标签</b></p>
<p class="story">第二个p标签
<a href="https://www.google.com" class="search_1" id="link3">谷歌搜索</a>;
<a href="https://www.bing.com" class="search_2" id="link2">微软搜索</a>
and
<a href="https://www.baidu.com" class="search_3" id="link1">百度搜索</a>,
and they lived at the bottom of a well.
</p>
<p class="story">佛祖保佑,永无bug</p>
<ul class="country">
<li>USA</li>
<li>Russia</li>
<li>China</li>
</ul>
"""
# 使用 html字符串 创建 BeautifulSoup对象
# soup = BeautifulSoup(html_string, 'html.parser', from_encoding='utf-8')
soup = BeautifulSoup(html_string, 'html.parser')
print(soup.title) # 输出第一个 title 标签
print(soup.title.name) # 输出第一个 title 标签 的 标签名称
print(soup.title.string) # 输出第一个 title 标签 的 包含内容
print(soup.title.parent.name) # 输出第一个 title 标签的父标签的标签名称
print(soup.p) # 输出第一个 p 标签
print(soup.p['class']) # 输出第一个 p 标签的 class 属性内容
print(soup.a['href']) # 输出第一个 a 标签的 href 属性内容
'''
soup 的属性可以被添加、删除、修改
soup 的属性操作方法与字典一样
'''
# 修改第一个 a 标签的 href 属性为 https://so.gushiwen.cn/shiwen/
soup.a['href'] = 'https://so.gushiwen.cn/shiwen/'
soup.a['name'] = u'百度' # 给第一个 a 标签添加 name 属性
del soup.a['class'] # 删除第一个 a 标签的 class 属性为
print(soup.p.contents) # 输出第一个 p 标签的所有子节点
print(soup.a) # 输出第一个 a 标签
print(soup.find_all('a')) # 输出所有的 a 标签,以列表形式显示
print(soup.find(id="link3")) # 输出第一个 id 属性等于 link3 的 a 标签
ul = soup.find('ul', attrs={'class': 'country'})
print(ul)
print(ul.find('li')) # 返回第一个匹配的ul标签
print(soup.get_text()) # 获取所有文字内容
print(soup.a.attrs) # 输出第一个 a 标签的所有属性信息
for link in soup.find_all('a'):
print(link.get('href')) # 获取 link 的 href 属性内容
# 对 soup.p 的子节点进行循环输出
for child in soup.p.children:
print(child)
# 正则匹配,名字中带有b的标签
for tag in soup.find_all(re.compile("b")):
print(tag.name)
示例 2:
from bs4 import BeautifulSoup
# 下面代码示例都是用此文档测试
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="https://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="https://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="https://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
soup = BeautifulSoup(html_doc, 'lxml')
print("1;获取head标签")
print(soup.head)
print("2;#获取p节点下的b节点")
print(soup.p.b)
# name属性获取节点名称:
print("4;name属性获取节点名称")
print(soup.body.name)
# attrs属性获取节点属性,也可以字典的形式直接获取,返回的结果可能是列表或字符串类型,取决于节点类型
print("5;获取p节点所有属性")
print(soup.p.attrs)
print("6;获取p节点class属性")
print(soup.p.attrs['class'])
print("7;直接获取p节点class属性")
print(soup.p['class'])
# string属性获取节点元素包含的文本内容:
print("8;获取a标签下的文本,只获取第一个")
print(soup.p.string)
# contents属性获取节点的直接子节点,以列表的形式返回内容
print("9;contents属性获取节点的直接子节点,以列表的形式返回内容")
print(soup.body.contents)
# children属性获取的也是节点的直接子节点,只是以生成器的类型返回
print("10;children属性获取的也是节点的直接子节点,只是以生成器的类型返回")
print(soup.body.children)
# descendants属性获取子孙节点,返回生成器
print("11;descendants属性获取子孙节点,返回生成器")
print(soup.body.descendants)
# parent属性获取父节点,parents获取祖先节点,返回生成器
print("12;parent属性获取父节点,parents获取祖先节点,返回生成器")
print(soup.b.parent)
print(soup.b.parents)
# next_sibling属性返回下一个兄弟节点
print("13;next_sibling属性返回下一个兄弟节点")
print(soup.a.next_sibling)
# previous_sibling返回上一个兄弟节点,注意换行符也是一个节点
print("14;previous_sibling返回上一个兄弟节点,注意换行符也是一个节点")
print(soup.a.previous_sibling)
# next_siblings属性返回下所有兄弟节点
print("15;next_sibling属性返回下一个兄弟节点")
print(soup.a.next_siblings)
# previous_siblings返回上所有兄弟节点,注意换行符也是一个节点
print("16;previous_sibling返回上一个兄弟节点,注意换行符也是一个节点")
print(soup.a.previous_siblings)
# next_element和previous_element属性获取下一个被解析的对象,或者上一个
print("17;next_element和previous_element属性获取下一个被解析的对象,或者上一个")
print(soup.a.next_element)
print(soup.a.previous_element)
# next_elements和previous_elements迭代器向前或者后访问文档解析内容
print("18;next_elements和previous_elements迭代器向前或者后访问文档解析内容")
print(soup.a.next_elements)
print(soup.a.previous_elements)
3、四大对象种类
Beautiful Soup 将复杂 HTML 文档转换成一个复杂的树形结构,每个节点都是 Python 对象,所有对象可以归纳为4种:
1. Tag
2. NavigableString
3. BeautifulSoup
4. Comment
Tag
Tag 是什么?通俗点讲就是 HTML 中的一个个标签,例如
<title>The Dormouse's story</title>
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
上面的 title a 等等 HTML 标签加上里面包括的内容就是 Tag,
用 Beautiful Soup 可以方便地获取 Tags
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title>网页的title</title></head>
<body>
<p class="p_class"><b>第一个p标签</b></p>
<p class="story">第二个p标签
<a href="https://www.google.com" class="search_1" id="link3">谷歌搜索</a>;
<a href="https://www.bing.com" class="search_2" id="link2">微软搜索</a>
and
<a href="https://www.baidu.com" class="search_3" id="link1">百度搜索</a>,
and they lived at the bottom of a well.
</p>
<p class="story">佛祖保佑,永无bug</p>
<ul class="country">
<li>USA</li>
<li>Russia</li>
<li>China</li>
</ul>
"""
soup = BeautifulSoup(html_string, 'html.parser')
print(soup.title)
print(soup.head)
print(soup.a)
print(soup.p)
利用 soup 加标签名轻松地获取这些标签的内容,是不是感觉比正则表达式方便多了?不过有一点是,它查找的是在所有内容中的第一个符合要求的标签,soup.title 得到的是title标签,soup.p 得到的是文档中的第一个p标签,要想得到所有标签,得用 find_all 函数。find_all 函数返回的是一个序列,可以对它进行循环,依次得到想到的东西.。
我们可以验证一下这些对象的类型
print(type(soup.a))
#<class 'bs4.element.Tag'>
对于 Tag,它有两个重要的属性,是 name 和 attrs
name
print(soup.name) #[document]
print(soup.head.name) #head
soup 对象本身比较特殊,它的 name 即为 [document],对于其他内部标签,输出的值便为标签本身的名称。
attrs
print(soup.p.attrs)
#{'class': ['title'], 'name': 'dromouse'}
在这里,我们把 p 标签的所有属性打印输出了出来,得到的类型是一个字典。如果我们想要单独获取某个属性,可以这样,例如我们获取它的 class 叫什么
prin(soup.p['class'])
#['title']
还可以这样,利用get方法,传入属性的名称,二者是等价的
prin(soup.p.get('class'))
#['title']
我们可以对这些属性和内容等等进行修改,例如
soup.p['class']="newClass"
print(soup.p)
#<p class="newClass" name="dromouse"><b>The Dormouse's story</b></p>
还可以对这个属性进行删除,例如
del soup.p['class']
print(soup.p)
#<p name="dromouse"><b>The Dormouse's story</b></p>
不过,对于修改删除的操作,不是我们的主要用途,在此不做详细介绍了,如果有需要,请查看前面提供的官方文档
head = soup.find('head')
# head = soup.head
# head = soup.contents[0].contents[0]
print(head)html = soup.contents[0] # <html> ... </html>
可以通过 Tag.attrs 访问,返回字典结构的属性。
或者 Tag.name 这样访问特定属性值,如果是多值属性则以列表形式返回。
NavigableString
既然我们已经得到了标签的内容,那么问题来了,我们要想获取标签内部的文字怎么办呢?很简单,用 .string 即可,例如
prin(soup.p.string)
#The Dormouse's story
这样我们就轻松获取到了标签里面的内容,想想如果用正则表达式要多麻烦。它的类型是一个 NavigableString,翻译过来叫 可以遍历的字符串,不过我们最好还是称它英文名字吧。来检查一下它的类型
prin(type(soup.p.string))
#<class 'bs4.element.NavigableString'>
BeautifulSoup
BeautifulSoup 对象表示的是一个文档的全部内容。大部分时候,可以把它当作 Tag 对象,是一个特殊的 Tag,我们可以分别获取它的类型,名称,以及属性来感受一下
prin(type(soup.name))
#<type 'unicode'>
print(soup.name)
# [document]
print(soup.attrs)
#{} 空字典
Comment
Comment 对象是一个特殊类型的 NavigableString 对象,其实输出的内容仍然不包括注释符号,但是如果不好好处理它,可能会对我们的文本处理造成意想不到的麻烦。
我们找一个带注释的标签
import re
import bs4
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title>网页的title</title></head>
<body>
<a class="sister" href="https://www.bing.com" id="link1"><!-- 微软bing搜索 --></a>
"""
soup = BeautifulSoup(html_string, 'html.parser')
print(soup.a)
print(soup.a.string)
print(type(soup.a.string))
if type(soup.a.string) == bs4.element.Comment:
print(soup.a.string)
运行结果如下
<a class="sister" href="https://www.bing.com" id="link1"><!-- 微软bing搜索 --></a>
微软bing搜索
<class 'bs4.element.Comment'>
微软bing搜索
a 标签里的内容实际上是注释,但是如果我们利用 .string 来输出它的内容,我们发现它已经把注释符号去掉了,所以这可能会给我们带来不必要的麻烦。
另外我们打印输出下它的类型,发现它是一个 Comment 类型,所以,我们在使用前最好做一下判断是否为 Comment 类型,然后再进行其他操作,如打印输出。
4、遍历文档树
PageElement 类中的方法
class PageElement(object):
"""NavigableString, Tag等都是PageElement的子类"""
parent: Tag | None
previous_element: PageElement | None
next_element: PageElement | None
next_sibling: PageElement | None
previous_sibling: PageElement | None
known_xml = None
def setup(self, parent=None, previous_element=None, next_element=None,previous_sibling=None, next_sibling=None):
pass
def format_string(self, s, formatter):
"""使用formatter格式化字符串."""
pass
def formatter_for_name(self, formatter):
"""给指定的 identifier 创建 Formatter"""
pass
nextSibling = _alias("next_sibling") # BS3
previousSibling = _alias("previous_sibling") # BS3
@property
def stripped_strings(self) -> Iterator[str]: ...
"""返回在 PageElement 的所有字符串"""
pass
def get_text(self, separator="", strip=False, types=default):
"""获取这个PageElement的所有子字符串,使用给定的分隔符进行连接.
pass
getText = get_text
text = property(get_text)
def replace_with(self, *args):
"""用一个或多个PageElement替换这个PageElement,保持树的其余部分不变 """
pass
replaceWith = replace_with # BS3
def unwrap(self):
"""用它的内容替换这个PageElement"""
pass
replace_with_children = unwrap
replaceWithChildren = unwrap # BS3
def wrap(self, wrap_inside):
"""将此PageElement包装到另一个PageElement中"""
pass
def extract(self, _self_index=None):
"""破坏性地将此元素从树中剥离"""
pass
def insert(self, position, new_child):
"""在这个PageElement的子元素列表中插入一个新的PageElement. 原理同 list.insert"""
pass
def append(self, tag):
"""将给定的PageElement附加到这个页面的内容"""
pass
def extend(self, tags):
"""将给定的PageElements附加到这个页面中"""
pass
def insert_before(self, *args):
"""使给定的元素成为这个元素的直接前身"""
pass
def insert_after(self, *args):
"""使给定元素成为此元素的直接后继元素"""
pass
def find_next(self, name=None, attrs={}, string=None, **kwargs):
"""查找匹配的下一个PageElement。所有的 find_* 都有相同的参数"""
pass
findNext = find_next # BS3
def find_all_next(self, name=None, attrs={}, string=None, limit=None, **kwargs):
"""查找匹配后,后面所有的PageElement。所有的 find_* 都有相同的参数"""
pass
findAllNext = find_all_next # BS3
def find_next_sibling(self, name=None, attrs={}, string=None, **kwargs):
"""查找与此PageElement最接近的、匹配给定条件并在文档后面出现的同级元素"""
pass
findNextSibling = find_next_sibling # BS3
def find_next_siblings(self, name=None, attrs={}, string=None, limit=None,**kwargs):
"""查找与此PageElement最接近的、匹配给定条件并在文档后面出现的所有同级元素"""
pass
findNextSiblings = find_next_siblings # BS3
fetchNextSiblings = find_next_siblings # BS2
def find_previous(self, name=None, attrs={}, string=None, **kwargs):
"""从这个PageElement往回查找,找到第一个符合给定条件的PageElement"""
pass
findPrevious = find_previous # BS3
def find_all_previous(self, name=None, attrs={}, string=None, limit=None,**kwargs):
"""从这个PageElement往回查找,找到所有符合给定条件的PageElement"""
pass
findAllPrevious = find_all_previous # BS3
fetchPrevious = find_all_previous # BS2
def find_previous_sibling(self, name=None, attrs={}, string=None, **kwargs):
"""返回与此PageElement最接近的兄弟元素,该元素匹配给定的条件并出现在文档的较早位置"""
pass
findPreviousSibling = find_previous_sibling # BS3
def find_previous_siblings(self, name=None, attrs={}, string=None,limit=None, **kwargs):
"""返回与此PageElement最接近的所有兄弟元素."""
pass
findPreviousSiblings = find_previous_siblings # BS3
fetchPreviousSiblings = find_previous_siblings # BS2
def find_parent(self, name=None, attrs={}, **kwargs):
"""查找与给定条件匹配的最接近的PageElement父元素"""
pass
findParent = find_parent # BS3
def find_parents(self, name=None, attrs={}, limit=None, **kwargs):
"""查找与给定条件匹配的所有的PageElement父元素"""
pass
findParents = find_parents # BS3
fetchParents = find_parents # BS2
@property
def next(self):
"""一个PageElement,如果后面有则返回"""
pass
@property
def previous(self):
"""一个PageElement,如果前面有则返回"""
pass
@property
def next_elements(self):
"""在此之后的所有 PageElements"""
yield xxx
@property
def next_siblings(self):
"""在此之后的所有兄弟 PageElements"""
yield xxx
@property
def previous_elements(self):
"""在此之前的所有 PageElements"""
yield xxx
@property
def previous_siblings(self):
"""在此之前的所有兄弟 PageElements"""
yield xxx
@property
def parents(self):
"""当前 PageElement 的所有 父 PageElements"""
yield xxx
@property
def decomposed(self):
"""检查PageElement是否已被分解"""
yield xxx
通过上面可以发现:
- getText()、text() 和 get_text() 这三个方法都用于提取 BeautifulSoup对象中节点以及所有子节点的文本,都是指向 get_text() 方法。
- Tag 类 中
string;获取节点的 字符串
strings:获取节点的所有 字符串
Tag 类中的方法
class Tag(PageElement):
"""
Tag 对象,表示HTML或XML解析后DOM结构中的属性和内容
示例:当Beautiful Soup解析标记<b>企鹅</b>时,它将创建一个表示<b>标记的Tag对象
"""
parser_class: type[BeautifulSoup] | None
parserClass = _alias("parser_class") # BS3
name: str
namespace: str | None
prefix: str | None
sourceline: int | None
sourcepos: int | None
known_xml: bool | None
attrs: dict[str, str | Any]
contents: list[PageElement] # 所有子节点列表
hidden: bool
can_be_empty_element: bool | None
cdata_list_attributes: list[str] | None
preserve_whitespace_tags: list[str] | None
@property
def is_empty_element(self):
"""判断标签是否为空标签,标签有contents就不是空
pass
isSelfClosing = is_empty_element # BS3
@property
def string(self):
"""
属性,获取此 PageElement中 字符串
如果此元素有一个字符串子元素,则返回值为该字符串。
如果此元素有子标签,则递归地所有子标签,返回值是子标签的'string'属性
如果此元素本身是字符串,没有子元素,或者有多个子元素,则返回值为None
"""
pass
@string.setter
def string(self, string):
"""将此PageElement的内容替换为 string。"""
pass
# 返回所有的 string, 可以使用 list 获取所有字符串的列表
@property
def strings(self) -> Iterable[str]: ...
def decompose(self):
"""递归地销毁此PageElement及其子元素"""
pass
def clear(self, decompose=False):
"""通过调用extract()来清除这个PageElement的所有子元素."""
pass
def smooth(self):
"""通过合并连续字符串来平滑该元素的子元素."""
pass
def index(self, element):
"""Find the index of a child by identity, not value.
pass
def get(self, key, default=None):
"""返回标签的属性值"""
return self.attrs.get(key, default)
def get_attribute_list(self, key, default=None):
"""同 get(), 但是返回的是 list"""
pass
def has_attr(self, key):
return key in self.attrs
def encode(self, encoding=DEFAULT_OUTPUT_ENCODING,indent_level=None, formatter="minimal",errors="xmlcharrefreplace"):
"""返回 此PageElement及其内容的字节字符串
:return: A bytestring.
"""
pass
def decode(self, indent_level=None,eventual_encoding=DEFAULT_OUTPUT_ENCODING,formatter="minimal",iterator=None):
pass
def prettify(self, encoding=None, formatter="minimal"):
"""使用 Pretty-print 把当前 PageElement 打印为字符串"""
pass
def decode_contents(self, indent_level=None,eventual_encoding=DEFAULT_OUTPUT_ENCODING,formatter="minimal"):
"""将此标记的内容呈现为Unicode字符串"""
pass
def encode_contents(self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,formatter="minimal"):
"""将此标记的内容呈现为 byte 字符串"""
pass
def find(self, name=None, attrs={}, recursive=True, string=None,**kwargs):
"""查看此PageElement的子元素,并找到与给定条件匹配的第一个PageElement."""
pass
findChild = find #BS2
def find_all(self, name=None, attrs={}, recursive=True, string=None,limit=None, **kwargs):
"""查看此PageElement与给定条件匹配的所有子PageElement."""
pass
findAll = find_all # BS3
findChildren = find_all # BS2
#Generator methods
@property
def children(self):
"""迭代此PageElement的所有直接子元素.
:yield: A sequence of PageElements.
"""
pass
@property
def self_and_descendants(self):
"""以宽度优先的顺序遍历这个PageElement及其子元素
:yield: A sequence of PageElements.
"""
pass
@property
def descendants(self):
"""以宽度优先顺序遍历此PageElement的所有子元素
:yield: A sequence of PageElements.
"""
pass
# CSS selector code
def select_one(self, selector, namespaces=None, **kwargs):
"""对当前元素执行CSS选择操作"""
return self.css.select_one(selector, namespaces, **kwargs)
def select(self, selector, namespaces=None, limit=None, **kwargs):
"""对当前元素执行CSS选择操作"""
return self.css.select(selector, namespaces, limit, **kwargs)
@property
def css(self):
"""返回 CSS selector API 接口"""
return CSS(self)
(1)直接 子节点
知识点:.contents 和 .children 属性
.contents
tag 的 .content 属性可以将 tag 的子节点以列表的方式输出。然后使用 [num] 的形式访问。
使用 contents 向后遍历树,使用 parent 向前遍历树
- Tag.contents:以列表形式返回所有子节点。
- Tag.children:生成器,可用于循环访问:for child in Tag.children
示例:soup.find('div').span
示例:soup.find('div').contents[1]
(2)所有 子孙 节点
知识点:.descendants 属性
.descendants
.contents 和 .children 属性仅包含 tag 的直接子节点,.descendants 属性可以对所有 tag 的子孙节点进行递归循环,和 children类似,我们也需要遍历获取其中的内容。
Tag.descendants:生成器,可用于循环访问:for des inTag.descendants
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title>网页的title</title></head>
<body>
<p class="p_class"><b>第一个p标签</b></p>
<p class="story">第二个p标签
<a href="https://www.google.com" class="search_1" id="link3">谷歌搜索</a>;
<a href="https://www.bing.com" class="search_2" id="link2">微软搜索</a>
and
<a href="https://www.baidu.com" class="search_3" id="link1">百度搜索</a>,
and they lived at the bottom of a well.
</p>
<p class="story">佛祖保佑,永无bug</p>
<ul class="country">
<li>USA</li>
<li>Russia</li>
<li>China</li>
</ul>
"""
soup = BeautifulSoup(html_string, 'html.parser')
all_tag_list = list(soup.descendants)
for item_tag in all_tag_list:
print(item_tag)
print(f'tag 数量 ---> {len(all_tag_list)}')
运行结果如下,可以发现,所有的节点都被打印出来了,先生成最外层的 HTML标签,其次从 head 标签一个个剥离,以此类推。
(3)节点 内容
知识点:.string、.Strings、.stripped_strings
- Tag.String:Tag 只有一个 String 子节点时可以这么访问,否则返回 None
- Tag.Strings:生成器,可用于循环访问:for str in Tag.Strings
- Tag.stripped_strings 输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容
示例代码:
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<body>
<h1><p>h1中p的内容</p></h1>
<h2>h2<p>h2中p的内容</p></h2>
"""
soup = BeautifulSoup(html_string, 'html.parser')
print(soup.h1.string)
print(soup.h2.string)
for item_str in soup.strings:
print(str(item_str))
for item_str in soup.stripped_strings:
print(str(item_str))
如果 tag 只有一个子节点,那么这个 tag 可以使用 .string 得到子节点。输出结果与当前唯一子节点的 .string 结果相同。通俗点说就是:
- 如果一个标签里面没有标签了,那么 .string 就会返回标签里面的内容。
- 如果标签里面只有唯一的一个标签了,那么 .string 也会返回最里面的内容。
- 如果超过一个标签的话,那么就会返回 None
(4)父 节点
知识点: .parent 属性
使用 parent 获取父节点。
- Tag.parent:父节点
- Tag.parents:父到根的所有节点
body = soup.body
html = body.parent # html是body的父亲
p = soup.p
print(p.parent.name)
content = soup.head.title.string
print(content.parent.name)
(5)全部 父节点
知识点:.parents 属性
通过元素的 .parents 属性可以递归得到元素的所有父辈节点,例如
content = soup.head.title.string
for parent in content.parents:
print(parent.name)
(6)兄弟 节点
知识点:.next_sibling 和 .previous_sibling 属性
使用 nextSibling,previousSibling获取前后兄弟
Tag.next_sibling
Tag.next_siblingsTag.previous_sibling
Tag.previous_siblings
兄弟节点可以理解为和本节点处在统一级的节点,.next_sibling 属性获取了该节点的下一个兄弟节点,.previous_sibling 则与之相反,如果节点不存在,则返回 None。
注意:实际文档中的tag的 .next_sibling 和 .previous_sibling 属性通常是字符串或空白,因为空白或者换行也可以被视作一个节点,所以得到的结果可能是空白或者换行
print(soup.p.next_sibling)
print(soup.p.prev_sibling)
print(soup.p.next_sibling.next_sibling)
.next 方法:只能针对单一元素进行.next,或者说是对contents列表元素的挨个清点。
比如
soup.contents[1] = 'HTML'
soup.contents[2] = '\n'
则 soup.contents[1].next 等价于 soup.contents[2]
head = body.previousSibling # head 和 body 在同一层,是body的前一个兄弟
p1 = body.contents[0] # p1, p2都是body的儿子,我们用contents[0]取得p1
p2 = p1.nextSibling # p2与p1在同一层,是p1的后一个兄弟, 当然body.content[1]也可得到
contents[] 的灵活运用也可以寻找关系节点
findParent(s)、findNextSibling(s)、findPreviousSibling(s) 可以寻找 祖先、子孙
(7)全部 兄弟 节点
知识点:.next_siblings 和 .previous_siblings 属性
通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出
for sibling in soup.a.next_siblings:
print(repr(sibling))
(8)前 后 节点
知识点:.next_element 和 .previous_element 属性
与 .next_sibling .previous_sibling 不同,它并不是针对于兄弟节点,而是在所有节点,不分层次。比如 head 节点为
<head><title>The Dormouse's story</title></head>
那么它的下一个节点便是 title,它是不分层次关系的
print soup.head.next_element
#<title>The Dormouse's story</title>
(9)所有 前 后 节点
知识点:.next_elements 和 .previous_elements 属性
通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样
for element in last_a_tag.next_elements:
print(repr(element))
以上是遍历文档树的基本用法。
5、搜索 文档树
最常用的是 find_all() 函数
(1)find_all( name , attrs , recursive , text , **kwargs )
find_all() 方法搜索当前 tag 的所有 tag 子节点,并判断是否符合过滤器的条件
1)name 参数
name 参数可以查找所有名字为 name 的tag,字符串对象会被自动忽略掉
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title class="title_class">网页的title</title></head>
<body>
"""
soup = BeautifulSoup(html_string, 'html.parser')
# 第一个参数为Tag的名称
print(soup.find_all('title'))
print(soup.find_all('title', attrs={'class': 'title_class'}))
print(soup.find_all('title', 'title_class'))
A.传字符串
最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的<b>标签
soup.find_all('b')
print(soup.find_all('a'))
B.传正则表达式
如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 match() 来匹配内容.下面例子中找出所有以b开头的标签,这表示<body>和<b>标签都应该被找到
import re
for tag in soup.find_all(re.compile("^b")):
print(tag.name)
C.传列表
如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有<a>标签和<b>标签
soup.find_all(["a", "b"])
D.传 True
True 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点
for tag in soup.find_all(True):
print(tag.name)
E.传方法
如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数,如果这个方法返回 True 表示当前元素匹配并且被找到。如果不是则反回 False。
下面方法校验了当前元素,如果包含 class 属性却不包含 id 属性,那么将返回 True
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title class="title_class">网页的title</title></head>
<body>
"""
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
soup = BeautifulSoup(html_string, 'html.parser')
print(soup.find_all(has_class_but_no_id))
2)keyword 参数
注意:如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字 tag 的属性来搜索,如果包含一个名字为 id 的参数,Beautiful Soup 会搜索每个 tag 的 "id" 属性
soup.find_all(id='link2')
如果传入 href 参数,Beautiful Soup 会搜索每个 tag 的 "href" 属性
soup.find_all(href=re.compile("elsie"))
使用多个指定名字的参数可以同时过滤 tag 的多个属性
soup.find_all(href=re.compile("elsie"), id='link1')
想用 class 过滤,不过 class 是 python 的关键词,这怎么办?加个下划线就可以
soup.find_all("a", class_="sister")
有些 tag 属性在搜索不能使用,比如 HTML5 中的 data-* 属性
data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
data_soup.find_all(data-foo="value")
# SyntaxError: keyword can't be an expression
但是可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的 tag
data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]
3)text 参数
通过标签文本内容进行搜索。text 参数接受 "字符串 , 正则表达式、列表、True"
soup.find_all(text="Elsie")
# [u'Elsie']
soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']
soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]
4)limit 参数
find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢。如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量,效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果。
示例:只返回2个搜索结果
soup.find_all("a", limit=2)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
5)recursive 参数
调用 tag 的 find_all() 方法时,Beautiful Soup 会检索当前 tag 的所有子孙节点,如果只想搜索 tag的直接子节点,可以使用参数 recursive=False 。一段简单的文档:
<html>
<head>
<title>
The Dormouse's story
</title>
</head>
...
是否使用 recursive 参数的搜索结果:
soup.html.find_all("title")
# [<title>The Dormouse's story</title>]
soup.html.find_all("title", recursive=False)
# []
(2)find(name=None, attrs={}, recursive=True, text=None, **kwargs)
find_all() 方法的返回一个元素的列表,而 find() 方法直接返回找到的第一个元素
tag 搜索
find(tagname) # 直接搜索名为 tagname 的tag 如:find('head')
find(list) # 搜索在list中的tag,如: find(['head', 'body'])
find(dict) # 搜索在dict中的tag,如:find({'head':True, 'body':True})
find(re.compile('')) # 搜索符合正则的tag, 如:find(re.compile('^p')) 搜索以p开头的tag
find(lambda) # 搜索函数返回结果为true的tag, 如:find(lambda name: if len(name) == 1) 搜索长度为1的tag
find(True) # 搜索所有tagattrs 搜索
find(id='xxx') # 寻找id属性为xxx的
find(attrs={id=re.compile('xxx'), algin='xxx'}) # 寻找id属性符合正则且algin属性为xxx的
find(attrs={id=True, algin=None}) # 寻找有id属性但是没有algin属性的resp1 = soup.findAll('a', attrs = {'href': match1})
resp2 = soup.findAll('h1', attrs = {'class': match2})
resp3 = soup.findAll('img', attrs = {'id': match3})text 搜索
文字的搜索会导致其他搜索给的值,如:tag, attrs 都失效。
使用方法与搜索tag一致
print(p1.text)
print(p2.text)# 注意:
1,每个tag的text包括了它以及它子孙的text。
2,所有text已经被自动转为unicode,如果需要,可以自行转码encode(xxx)recursive和limit属性
recursive=False表示只搜索直接儿子,否则搜索整个子树,默认为True。
当使用findAll或者类似返回list的方法时,limit属性用于限制返回的数量,
如:findAll('p', limit=2): 返回首先找到的两个tag
(3)find_parents() find_parent()
find_all() 和 find() 只搜索当前节点的所有子节点,孙子节点等. find_parents() 和 find_parent() 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容
(4)find_next_siblings() find_next_sibling()
这2个方法通过 .next_siblings 属性对当 tag 的所有后面解析的兄弟 tag 节点进行迭代, find_next_siblings() 方法返回所有符合条件的后面的兄弟节点,find_next_sibling() 只返回符合条件的后面的第一个tag节点
(5)find_previous_siblings() find_previous_sibling()
这2个方法通过 .previous_siblings 属性对当前 tag 的前面解析的兄弟 tag 节点进行迭代, find_previous_siblings()方法返回所有符合条件的前面的兄弟节点, find_previous_sibling() 方法返回第一个符合条件的前面的兄弟节点
(6)find_all_next() find_next()
这2个方法通过 .next_elements 属性对当前 tag 的之后的 tag 和字符串进行迭代, find_all_next() 方法返回所有符合条件的节点, find_next() 方法返回第一个符合条件的节点
(7)find_all_previous() 和 find_previous()
这2个方法通过 .previous_elements 属性对当前节点前面的 tag 和字符串进行迭代, find_all_previous() 方法返回所有符合条件的节点, find_previous()方法返回第一个符合条件的节点
注:以上(2)(3)(4)(5)(6)(7)方法参数用法与 find_all() 完全相同,原理均类似,在此不再赘述。
6、CSS 选择器
在写 CSS 时,标签名不加任何修饰,类名前加点,id名前加 #
在这里我们也可以利用类似的方法来筛选元素,用到的方法是 soup.select(),返回类型是 list
(1)通过标签名查找
print(soup.select('title') )
#[<title>The Dormouse's story</title>]print(soup.select('a'))
print(soup.select('b'))
(2)通过类名查找
print(soup.select('.sister'))
(3)通过 id 名查找
prin(soup.select('#link1'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
(4)组合查找
组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如:查找 p 标签中,id 等于 link1的内容,二者需要用空格分开
print(soup.select('p #link1'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
直接子标签查找
print(soup.select("head > title"))
#[<title>The Dormouse's story</title>]
(5)属性查找
查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。
prin(soup.select('a[class="sister"]'))
print(soup.select('a[href="http://example.com/elsie"]'))
同样,属性仍然可以与上述查找方式组合,不在同一节点的空格隔开,同一节点的不加空格
print(soup.select('p a[href="http://example.com/elsie"]'))
#[<a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>]
以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。
import re
from bs4 import BeautifulSoup
# 待分析字符串
html_string = """
<html>
<head><title class="title_class">网页的title</title></head>
<body>
"""
soup = BeautifulSoup(html_string, 'lxml')
print(type(soup.select('title')))
print(soup.select('title')[0].get_text())
for title in soup.select('title'):
print(title.get_text())
这就是另一种与 find_all 方法有异曲同工之妙的查找方法,是不是感觉很方便
print(soup.find_all("a", class_="sister"))
print(soup.select("p.title"))
# 通过属性进行查找
print(soup.find_all("a", attrs={"class": "sister"}))
# 通过文本进行查找
print(soup.find_all(text="Elsie"))
print(soup.find_all(text=["Tillie", "Elsie", "Lacie"]))
# 限制结果个数
print(soup.find_all("a", limit=2))