Beautiful Soup
Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式
对网页进行析取时,若未规定解析器,此时使用的是python内部默认的解析器“html.parser”。
官方文档上多次提到推荐使用"lxml"和"html5lib"解析器,因为默认的"html.parser"自动补全标签的功能很差,经常会出问题。
解析器是什么呢?
BeautifulSoup做的工作就是对html标签进行解释和分类,不同的解析器对相同html标签会做出不同解释。
# Beautiful Soup 支持 python 标准库中 HTML 解析器, 还支持一些第三方的解析器, 其中一个是 lxml
# 安装 lxml
pip install lxml
# 另一个可供选择的解析器是纯 python 实现的 html5lib, html5lib与浏览器相同, 可以选择下列方法来安装
pip install html5lib
BeautifulSoup的使用
html_file_path = os.path.join(os.getcwd(), '../html_dir', 'test_lxml.html')
html_file = ''
with open(html_file_path, 'r') as f:
lines = f.readlines()
for line in lines:
html_file += line
# 初始化时自动更正格式, 输出结果中包含 html 和 body 节点, 不会自动缩进
# "lxml": 指定解析器, 优先使用 lxml 解析器
soup = BeautifulSoup(html_file, 'lxml') # 传入字符串格式的 HTML
soup = BeautifulSoup(open(html_file_path)) # 传入一个文件对象
HTML 文档的内容
<head><title>The Dormouse's story</title></head>
<p class="story">
this is P label
<a href="http://www.baidu.com" class="baidu" id="link1"><span>baidu</span></a><span>this is span</span>
<a href="http://www.cnblogs.com" class="cnblogs" id="link2"><span>cnblogs</span></a>
<ul class="ul1">
<li class="item-0 li" name="item0"><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>
<li class="aaa li-aaa"><a href="link6.html">aaaaa item</a></li>
<li class="li li-first" name="item6"><a href="link6.html"><span>six item</span></a></li>
<li class="li li-first" name="item7"><a href="link7.html"><span>seven item</span></a></li>
<ul class="ul2">
<li class="item-10 li" name="item10"><a href="link10.html">10 item</a></li>
<li class="item-11 li" name="item11"><a href="link11.html">11 item</a></li>
<li class="item-12 li" name="item12"><a href="link12.html">12 item</a></li>
<li class="item-13 li" name="item13"><a href="link13.html">13 item</a></li>
<li class="item-14 li" name="item14"><a href="link14.html">14 item</a></li>
<li class="item-15 li" name="item15"><a href="link15.html">15 item</a></li>
<li class="item-16 li" name="item16"><a href="link16.html">16 item</a></li>
每一个标签都有自己的名字, 通过 tag.name 的方式获取
tag.name = "tag_new_name": 修改标签的名字, 后面在获取该标签信息需要使用新名字, tag_new_name.name
print(f'通过 .name 获取标签名: {soup.p.name}')
soup.p.name = 'p_tag' # 修改 p 标签名
print(f'需要通过修改后的标签名 p_tag.name 获取标签名: {soup.p_tag.name}')
print(f'通过 .name 获取标签名: {soup.ul.name}')
soup.ul.name = 'new_ul' # 修改 ul 标签名
print(f'需要通过修改后的标签名 p_tag.name 获取标签名: {soup.new_ul.name}')
attributes
一个标签可能有很多属性, 比如: class、name、id..., 标签属性的操作方法和字典相同
tag.attrs: 获取标签所有属性, 返回一个字典格式的 {属性: 属性值} 键值对
标签属性的操作方法和字典一样, 增删改查
print(f"需要通过修改后的标签名 p_tag.name 获取 class 属性: {soup.p_tag['class']}")
soup.p_tag['id'] = 'p1' # p 标签增加 id 属性
soup.p_tag['class'] = 'story' # p 标签修改 class 属性
print(f"需要通过修改后的标签名 p_tag.name 获取所有属性: {soup.p_tag.attrs}")
print(f"需要通过修改后的标签名 p_tag.name 获取 id 属性: {soup.p_tag['id']}")
HTML4 定义了一系列可以包含多个值的属性。在 HTML5 中移除了一些,却增加更多.最常见的多值的属性是 class (一个tag可以有多个CSS的class). 还有一些属性 rel , rev , accept-charset , headers , accesskey . 在Beautiful Soup中多值属性的返回类型是list
print(f"获取 li 标签的所有属性, class 是多值属性, value 是列表格式的两个属性值: {soup.li.attrs}")
如果某个属性看起来好像有多个值, 但在任何版本的 HTML 定义中都没有被定义为多值属性, 那么 Beautiful Soup 会将这个属性作为字符串返回
id_soup = BeautifulSoup("<p id='my id1'></p>")
print(f"HTML未定义过的多值属性, 将两个值返回成一个字符串: {id_soup.p['id']}")
如果转换的文档是XML格式,那么tag中不包含多值属性
id_soup = BeautifulSoup("<p id='my id1'></p>", 'xml')
print(f"xml 格式中的多值属性, 将两个值返回成一个字符串: {id_soup.p['id']}")
可遍历的字符串
print(f'可遍历的字符串: {soup.a.string}, type: {type(soup.a.string)}')
soup.a.string.replace_with('No longer bold') # 标签的 字符串不能编辑, 但是可以替换
print(f"可遍历的字符串, 替换后的字符串: {soup.a.string}, type: {type(soup.a.string)}")
Tag 的名字
操作文档树最简单的方法就是告诉它你想获取的 tag 的 name。如果想获取 标签,只要用 soup.head :
可以在文档树的tag中多次调用这个方法
print(f'Tag 的名字, 将会打印包括 head 标签及其内的所有内容: {soup.head}')
print(f'获取 title: {soup.head.title}')
print(f'获取 ul 标签下 li 标签下 a 标签的名字: {soup.ul.li.a}')
find_all()
查找所有符合条件的标签
print(f"查找所有的 a 标签数量: {len(soup.find_all('a'))}, 结果: {soup.find_all('a')}")
contents()
将 tag 的子节点以列表的方式输出
.contents 属性仅包含tag的直接子节点
print(f"查找所有的 ul 标签下的第二个 li 标签下的 a 标签: {soup.ul.contents[3].a}")
print(f'contents 将子节点以列表的形式输出: 数量: {len(soup.ul.contents)}, 结果: {soup.ul.contents}')
children
返回对象是一个生成器
children 属性仅包含tag的直接子节点
li_list = soup.ul
for item in li_list.children:
if item != '\n': # 去掉换行符
print(f'ul 下的 li 标签下的 a 标签的文本: {item.a.string}')
descendants
返回对象是一个生成器
descendants 属性可以对所有 tag 的子孙节点进行递归循环
li_list = soup.ul
print(f'descendants 对象是一个生成器: {len(list(li_list.descendants))}, 结果: {li_list.descendants}')
for item in li_list.descendants:
if item != '\n': # 去掉换行符
print(f'descendants 递归循环 ul 下的所有子孙节点: {item}')
string
如果tag只有一个 NavigableString 类型子节点,那么这个tag可以使用 .string 得到子节点
如果一个tag仅有一个子节点,那么这个tag也可以使用 .string 方法,输出结果与当前唯一子节点的 .string 结果相同
如果tag包含了多个子节点,tag就无法确定 .string 方法应该调用哪个子节点的内容, .string 的输出结果是 None
print(f"head 只有一个 title 子节点: {soup.head.string}")
print(f"title 只有一个文本子节点: {soup.head.title.string}")
print(f"ul 有多个子节点: {soup.ul.string}")
strings 和 stripped_strings
返回的是一个生成器
如果 tag 中包含多个字符串, 可以使用 .strings 来循环获取, 但是会包含空白内容或换行符等
使用 .stripped_strings 可以去除多余空白内容, 全部是空格的行会被忽略掉,段首和段末的空白会被删除
print('使用 strings 获取 ul 下的多个子节点')
for item in soup.ul.strings:
if item != '\n':
print(item)
print('使用 stripped_strings 获取 ul 下的多个子节点')
for item in soup.ul.stripped_strings:
if item != '\n':
print(item)
parent
通过 parent 属性来获取某个元素的父节点
print(f'获取 title 的父节点 head: {soup.title.parent}')
print(f'获取 title 文本的父节点 title: {soup.title.string.parent}')
print(f'获取 html 顶层节点的父节点是整个 HTML, 返回 bs4.BeautifulSoup 对象: {type(soup.html.parent)}')
print(f'soup 的 parent 是 None: {soup.parent}')
parents
返回对象是一个生成器
通过元素的 .parents 属性可以递归得到元素的所有祖先节点
print(f'获取 title 的所有的祖先节点, 返回对象是一个生成器: {soup.title.parents}')
for item in soup.title.parents:
if item != '\n':
print(item, end='\n')
next_sibling【下一个兄弟节点】 和 previous_sibling【上一个兄弟节点】
实际文档中的tag的 .next_sibling 和 .previous_sibling 属性通常是字符串或空白
如果以为第一个 标签的 .next_sibling 结果是第二个 标签,那就错了,真实结果是第一个 标签和第二个 标签之间的换行符
print(f'ul 节点下的 li 节点: {list(soup.ul.children)}')
# 注意: 我下面选择的元素都是换行符, 所以打印的结果是标签
print(f'ul 节点下的 li 节点的下一个兄弟节点: {list(soup.ul.children)[0].next_sibling}')
print(f'ul 节点下的 li 节点的下一个兄弟节点: {list(soup.ul.children)[2].next_sibling}')
print(f'ul 节点下的 li 节点的上一个兄弟节点: {list(soup.ul.children)[4].previous_sibling}')
print(f'ul 节点下的 li 节点的上一个兄弟节点: {list(soup.ul.children)[2].previous_sibling}')
通过 .next_siblings 和 .previous_siblings 属性可以对当前节点的兄弟节点迭代输出
.next_siblings 和 .previous_siblings: 返回结果是生成器
print(f'ul 节点下的 li 节点: {list(soup.ul.children)}')
print(f'next_siblings : {list(soup.ul.children)[0].next_siblings}')
print(f'previous_siblings : {list(soup.ul.children)[4].previous_siblings}')
print('迭代 next_siblings 的结果: ')
# 这次循环打印会有换行
for item in list(soup.ul.children)[0].next_siblings:
print(item)
print('迭代 previous_siblings 的结果: ')
# 这次循环打印会有换行
for item in list(soup.ul.children)[4].previous_siblings:
print(item)
回退和前进
.next_element 和 .previous_element
next_element:
指向解析过程中下一个被解析的对象(字符串或tag),结果可能与 .next_sibling 相同,但通常是不一样的
next_element 解析的内容当前标签内的内容, 而不是当前标签结束后的下一个标签
例如: <li class="item-10 li" name="item10"><a href="link10.html">10 item</a></li>
解析器先进入 <li> 标签, 然后是 <a> 标签, 然后是字符串 10 item, 然后关闭 </a> 标签, 关闭 </li> 标签
next_element 解析的就是 <li> 标签后面一个对象 <a> 标签
print(f'ul 节点下的 li 节点: {list(soup.ul.children)}')
print(f'ul 节点下的 li 节点下的所有子节点 第三个 li: {list(soup.ul.children)[3]}')
print(f'ul 节点下的 li 节点下的的所有子节点 第三个 li 的中的标签 a: {list(soup.ul.children)[3].next_element}')
print(f'ul 节点下的 li 节点下的的所有子节点 第三个 li 的中的标签 a 的上一个解析标签: {list(soup.ul.children)[3].next_element.previous_element}')
.next_elements 和 .previous_elements
返回的是生成器
通过 .next_elements 和 .previous_elements 的迭代器就可以向前或向后访问文档的解析内容,就好像文档正在被解析一样
print(f'ul 节点下的 li 节点: {list(soup.ul.children)}')
print(f'ul 节点下的 li 节点下的所有子节点 第三个 li: {list(soup.ul.children)[3]}')
print(f'ul 节点下的 li 节点下的的所有子节点 第三个 li 的中的标签 a: next_elements')
for item in list(soup.ul.children)[3].next_elements:
print(item, end='\n==========\n')
print(f'ul 节点下的 li 节点下的的所有子节点 第三个 li 的中的标签 a 的上一个解析标签: previous_elements')
for item in list(soup.ul.children)[3].next_element.previous_elements:
print(item, end='\n==========\n')
搜索文档树
find()
获取匹配的第一个标签
find(name, attrs, recursive, text, **kwargs)
唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表, 而 find() 方法直接返回结果
find_all() 方法没有找到目标是返回空列表, find() 方法找不到目标时, 返回 None
soup.head.title 是标签的名字方法的简写, 这个简写的原理就是多次调用当前标签的 find() 方法
soup.head.title 和 soup.find('head').find('title') 实际一样
find_all(): 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件
find_all(name, attrs, recursive, text, **kwargs )
name:
name 参数可以查找所有名字为 name 的标签, 字符串对象会被自动忽略掉
name 参数可以是任意类型过滤器, 字符串, 正则表达式, 列表, True
recursive:
recursive=False: 只搜索标签的直接子节点
keyword:
如果指定名字的参数不是搜索内置参数名, 搜索时会把该参数当做指定名字标签的属性来搜索, 如果包含一个名字为 id 的参数, Beautiful Soup 会搜索每个标签的 id 属性
如果传入 href 参数, Beautiful Soup 会搜索每个标签的 href 属性
搜索指定名字的属性时可以使用的参数包括: 字符串, 正则表达式, 列表, True
有些标签属性搜索不能使用, 比如: HTML5 中的 data-* 属性, 可以通过 find_all() 方法的 attr 参数定义一个字典参数来搜索包括含特殊属性的标签
print(f"两个方法等价: {soup.title.find_all(text=True)}, {soup.title(text=True)}")
print(f"定义一个字典参数来搜索包含特殊属性的标签: {soup.find_all(attrs={'data-foo': 'value'})}")
字符串: 在搜索方法中传入一个字符串参数, Beautiful Soup 会查找与字符串完整匹配的内容
如果传入字节码参数, Beautiful Soup 会当做 UTF-8 编码, 可以传入一段 Unicode 编码来避免 Beautiful Soup 解析编码出错
print(f"查找所有的 a 标签: {soup.find_all('a')}")
print(f"查找所有的 title 标签: {soup.find_all('title')}")
正则表达式: 如果传入正则表达式作为参数, Beautiful Soup 会通过正则表达式的 match() 来匹配内容
print(f"查找所有的 p 开头的标签: {soup.find_all(re.compile('^p'))}")
print(f"查找所有的 ul 开头的标签: {soup.find_all(re.compile('^ul'))}")
print(f"查找所有包含的 l 标签: 数量: {len(soup.find_all(re.compile('l')))}, 结果: {soup.find_all(re.compile('l'))}")
列表: 如果传入列表参数, Beautiful Soup 会将与列表中任意一元素匹配的内容返回
print(f"查找所有的 a、title、form 标签: {soup.find_all(['a', 'title', 'form'])}")
True: 可以匹配任何值, 查找所有的标签, 但是不会返回字符串节点
print(f"查找所有的标签: {soup.find_all(True)}")
如果没有合适的过滤器, 那么还可以自定义一个方法, 方法只接受一个参数, 如果这个方法返回 True 表示当前元素匹配并且被找到, 如果不是则返回 None
print(f"查找所有包含 class 和 id 属性: {soup.find_all(lambda tag: tag.has_attr('class') and tag.has_attr('id'))}")
按 CSS 搜索
标识CSS类名的关键字 class 在 Python中是保留字, 使用 class 做参数会导致语法错误, 从 Beautiful Soup 的 4.1.1 版本开始, 可以通过 class_ 参数搜索有指定 CSS 类名的标签
class_ 参数同样接受不同类型的过滤器, 字符串, 正则表达式, 方法, True
标签的 class 属性是多值属性, 按照 CSS 类名搜索标签时, 可以分别搜索标签中的每个 CSS 类名
搜索 class 属性时也可以通过 CSS 值完全匹配
完全匹配时, 如果 CSS 的类名的顺序与实际不符, 将搜索不到结果
text 参数
通过 text 参数可以搜索文档中的字符内容, 与 name 参数的可选值一样, text 参数接受 字符串, 正则表达式, 列表, True
limit 参数
find_all() 方法返回所有的搜索结果, 如果文档树很大搜索结果会很慢, 如果我们不需要全部结果, 可以使用 limit 参数限制返回结果的数量, 效果与 SQL 中的 limit 关键字类似, 当搜索到的结果达到 limit 限制时, 就会停止搜索返回结果