python3 bs4
Beautiful Soup
- Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式
- 官方文档
解析器
- 对网页进行析取时,若未规定解析器,此时使用的是python内部默认的解析器“html.parser”。
- 官方文档上多次提到推荐使用"lxml"和"html5lib"解析器,因为默认的"html.parser"自动补全标签的功能很差,经常会出问题。
- 解析器是什么呢?
- BeautifulSoup做的工作就是对html标签进行解释和分类,不同的解析器对相同html标签会做出不同解释。
解析器 | 使用方法 | 优势 | 劣势 |
---|---|---|---|
python 标准库 | BeautifulSoup(markup, "html.parser") | 1、python 内置的标准库 | python2.7.2 or python3.2.2 前的文档容错性差 |
2、执行速度适中 | |||
3、文档容错能力强 | |||
lxml HTML 解析 | BeautifulSoup(markup, "lxml") | 1、速度快 | 需要安装C语言库 |
2、文档容错能力强 | |||
lxml XML 解析器 | BeautifulSoup(markup, ["lxml","xml"]) | 1、速度快 | 需要安装C语言库 |
BeautifulSoup(markup, "xml") | 2、唯一支持 XML 的解析器 | ||
html5lib | BeautifulSoup(markup, "html5lib") | 1、最好的容错性 | 1、需要安装C语言库 |
2、以浏览器的方式解析文档 | 2、不依赖外部扩展 | ||
3、生成 HTML5 格式的文档 |
安装及基本使用
- Windows下安装
# 安装 BeautifulSoup
pip install beautifulsoup4
# 安装解析器
# 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>
</p>
<div>
<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>
<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>
</ul>
</div>
Tag
- name
- 每一个标签都有自己的名字, 通过 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> 标签
- previous_element: 与 next_element 正好相反, 当前对象的上一个解析对象
- next_element:
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()
- 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 限制时, 就会停止搜索返回结果