💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 搜索文档树 Beautiful Soup定义了很多搜索方法,这里着重介绍2个: `find()` 和 `find_all()` .其它方法的参数和用法类似,请读者举一反三. 再以“爱丽丝”文档作为例子: ``` html_doc = """ <html><head><title>The Dormouse's story</title></head> <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>; and they lived at the bottom of a well.</p> <p class="story">...</p> """ from bs4 import BeautifulSoup soup = BeautifulSoup(html_doc) ``` 使用 `find_all()` 类似的方法可以查找到想要查找的文档内容 ## 过滤器 介绍 `find_all()` 方法前,先介绍一下过滤器的类型 \[3\] ,这些过滤器贯穿整个搜索的API.过滤器可以被用在tag的name中,节点的属性中,字符串中或他们的混合中. ### 字符串 最简单的过滤器是字符串.在搜索方法中传入一个字符串参数,Beautiful Soup会查找与字符串完整匹配的内容,下面的例子用于查找文档中所有的`<b>`标签: ``` soup.find_all('b') # [<b>The Dormouse's story</b>] ``` 如果传入字节码参数,Beautiful Soup会当作UTF-8编码,可以传入一段Unicode 编码来避免Beautiful Soup解析编码出错 ### 正则表达式 如果传入正则表达式作为参数,Beautiful Soup会通过正则表达式的 `match()` 来匹配内容.下面例子中找出所有以b开头的标签,这表示`<body>`和`<b>`标签都应该被找到: ``` import re for tag in soup.find_all(re.compile("^b")): print(tag.name) # body # b ``` 下面代码找出所有名字中包含”t”的标签: ``` for tag in soup.find_all(re.compile("t")): print(tag.name) # html # title ``` ### 列表 如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有`<a>`标签和`<b>`标签: ``` soup.find_all(["a", "b"]) # [<b>The Dormouse's story</b>, # <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>] ``` ### True `True` 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点 ``` for tag in soup.find_all(True): print(tag.name) # html # head # title # body # p # b # p # a # a # a # p ``` ### 方法 如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 \[4\] ,如果这个方法返回 `True` 表示当前元素匹配并且被找到,如果不是则反回 `False` 下面方法校验了当前元素,如果包含 `class` 属性却不包含 `id` 属性,那么将返回 `True`: ``` def has_class_but_no_id(tag): return tag.has_attr('class') and not tag.has_attr('id') ``` 将这个方法作为参数传入 `find_all()` 方法,将得到所有`<p>`标签: ``` 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...</p>, # <p class="story">...</p>] ``` 返回结果中只有`<p>`标签没有`<a>`标签,因为`<a>`标签还定义了”id”,没有返回`<html>`和`<head>`,因为`<html>`和`<head>`中没有定义”class”属性. 下面代码找到所有被文字包含的节点内容: ``` from bs4 import NavigableString def surrounded_by_strings(tag): return (isinstance(tag.next_element, NavigableString) and isinstance(tag.previous_element, NavigableString)) for tag in soup.find_all(surrounded_by_strings): print tag.name # p # a # a # a # p ``` 现在来了解一下搜索方法的细节 ## find_all() `find_all( name , attrs , recursive , text , **kwargs )` `find_all()` 方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件.这里有几个例子: ``` soup.find_all("title") # [<title>The Dormouse's story</title>] soup.find_all("p", "title") # [<p class="title"><b>The Dormouse's story</b></p>] soup.find_all("a") # [<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>] soup.find_all(id="link2") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] import re soup.find(text=re.compile("sisters")) # u'Once upon a time there were three little sisters; and their names were\n' ``` 有几个方法很相似,还有几个方法是新的,参数中的 `text` 和 `id` 是什么含义? 为什么 `find_all("p", "title")` 返回的是CSS Class为”title”的`<p>`标签? 我们来仔细看一下 `find_all()` 的参数 ### name 参数 `name` 参数可以查找所有名字为 `name` 的tag,字符串对象会被自动忽略掉. 简单的用法如下: ``` soup.find_all("title") # [<title>The Dormouse's story</title>] ``` 重申: 搜索 `name` 参数的值可以使任一类型的 [过滤器](#id25) ,字符窜,正则表达式,列表,方法或是 `True` . ### keyword 参数 如果一个指定名字的参数不是搜索内置的参数名,搜索时会把该参数当作指定名字tag的属性来搜索,如果包含一个名字为 `id` 的参数,Beautiful Soup会搜索每个tag的”id”属性. ``` soup.find_all(id='link2') # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] ``` 如果传入 `href` 参数,Beautiful Soup会搜索每个tag的”href”属性: ``` soup.find_all(href=re.compile("elsie")) # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] ``` 搜索指定名字的属性时可以使用的参数值包括 [字符串](#id27) , [正则表达式](#id28) , [列表](#id29), [True](#true) . 下面的例子在文档树中查找所有包含 `id` 属性的tag,无论 `id` 的值是什么: ``` soup.find_all(id=True) # [<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>] ``` 使用多个指定名字的参数可以同时过滤tag的多个属性: ``` soup.find_all(href=re.compile("elsie"), id='link1') # [<a class="sister" href="http://example.com/elsie" id="link1">three</a>] ``` 有些tag属性在搜索不能使用,比如HTML5中的 `data-*` 属性: ``` data_soup = BeautifulSoup('foo!') 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"}) # [foo!] ``` ### 按CSS搜索 按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字 `class` 在Python中是保留字,使用 `class` 做参数会导致语法错误.从Beautiful Soup的4.1.1版本开始,可以通过 `class_` 参数搜索有指定CSS类名的tag: ``` soup.find_all("a", class_="sister") # [<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>] ``` `class_` 参数同样接受不同类型的 `过滤器` ,字符串,正则表达式,方法或 `True` : ``` soup.find_all(class_=re.compile("itl")) # [<p class="title"><b>The Dormouse's story</b></p>] def has_six_characters(css_class): return css_class is not None and len(css_class) == 6 soup.find_all(class_=has_six_characters) # [<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>] ``` tag的 `class` 属性是 [多值属性](#id12) .按照CSS类名搜索tag时,可以分别搜索tag中的每个CSS类名: ``` css_soup = BeautifulSoup('<p class="body strikeout"></p>') css_soup.find_all("p", class_="strikeout") # [<p class="body strikeout"></p>] css_soup.find_all("p", class_="body") # [<p class="body strikeout"></p>] ``` 搜索 `class` 属性时也可以通过CSS值完全匹配: ``` css_soup.find_all("p", class_="body strikeout") # [<p class="body strikeout"></p>] ``` 完全匹配 `class` 的值时,如果CSS类名的顺序与实际不符,将搜索不到结果: ``` soup.find_all("a", attrs={"class": "sister"}) # [<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>] ``` ### `text` 参数 通过 `text` 参数可以搜搜文档中的字符串内容.与 `name` 参数的可选值一样, `text` 参数接受 [字符串](#id27) , [正则表达式](#id28) , [列表](#id29), [True](#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"] def is_the_only_string_within_a_tag(s): ""Return True if this string is the only child of its parent tag."" return (s == s.parent.string) soup.find_all(text=is_the_only_string_within_a_tag) # [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...'] ``` 虽然 `text` 参数用于搜索字符串,还可以与其它参数混合使用来过滤tag.Beautiful Soup会找到 `.string` 方法与 `text` 参数值相符的tag.下面代码用来搜索内容里面包含“Elsie”的`<a>`标签: ``` soup.find_all("a", text="Elsie") # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>] ``` ### `limit` 参数 `find_all()` 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢.如果我们不需要全部结果,可以使用 `limit` 参数限制返回结果的数量.效果与SQL中的limit关键字类似,当搜索到的结果数量达到 `limit` 的限制时,就停止搜索返回结果. 文档树中有3个tag符合搜索条件,但结果只返回了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>] ``` ### `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) # [] ``` ## 像调用 `find_all()` 一样调用tag `find_all()` 几乎是Beautiful Soup中最常用的搜索方法,所以我们定义了它的简写方法. `BeautifulSoup` 对象和 `tag` 对象可以被当作一个方法来使用,这个方法的执行结果与调用这个对象的 `find_all()` 方法相同,下面两行代码是等价的: ``` soup.find_all("a") soup("a") ``` 这两行代码也是等价的: ``` soup.title.find_all(text=True) soup.title(text=True) ``` ## find() `find( name , attrs , recursive , text , **kwargs )` `find_all()` 方法将返回文档中符合条件的所有tag,尽管有时候我们只想得到一个结果.比如文档中只有一个`<body>`标签,那么使用 `find_all()` 方法来查找`<body>`标签就不太合适, 使用 `find_all` 方法并设置 `limit=1` 参数不如直接使用 `find()` 方法.下面两行代码是等价的: ``` soup.find_all('title', limit=1) # [<title>The Dormouse's story</title>] soup.find('title') # <title>The Dormouse's story</title> ``` 唯一的区别是 `find_all()` 方法的返回结果是值包含一个元素的列表,而 `find()` 方法直接返回结果. `find_all()` 方法没有找到目标是返回空列表, `find()` 方法找不到目标时,返回 `None` . ``` print(soup.find("nosuchtag")) # None ``` `soup.head.title` 是 [tag的名字](#id17) 方法的简写.这个简写的原理就是多次调用当前tag的 `find()` 方法: ``` soup.head.title # <title>The Dormouse's story</title> soup.find("head").find("title") # <title>The Dormouse's story</title> ``` ## find_parents() 和 find_parent() `find_parents( name , attrs , recursive , text , **kwargs )` `find_parent( name , attrs , recursive , text , **kwargs )` 我们已经用了很大篇幅来介绍 `find_all()` 和 `find()` 方法,Beautiful Soup中还有10个用于搜索的API.它们中的五个用的是与 `find_all()` 相同的搜索参数,另外5个与 `find()` 方法的搜索参数类似.区别仅是它们搜索文档的不同部分. 记住: `find_all()` 和 `find()` 只搜索当前节点的所有子节点,孙子节点等. `find_parents()` 和 `find_parent()` 用来搜索当前节点的父辈节点,搜索方法与普通tag的搜索方法相同,搜索文档搜索文档包含的内容. 我们从一个文档中的一个叶子节点开始: ``` a_string = soup.find(text="Lacie") a_string # u'Lacie' a_string.find_parents("a") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] a_string.find_parent("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>; # and they lived at the bottom of a well.</p> a_string.find_parents("p", class="title") # [] ``` 文档中的一个`<a>`标签是是当前叶子节点的直接父节点,所以可以被找到.还有一个`<p>`标签,是目标叶子节点的间接父辈节点,所以也可以被找到.包含class值为”title”的`<p>`标签不是不是目标叶子节点的父辈节点,所以通过 `find_parents()` 方法搜索不到. `find_parent()` 和 `find_parents()` 方法会让人联想到 [.parent](#parent) 和 [.parents](#parents) 属性.它们之间的联系非常紧密.搜索父辈节点的方法实际上就是对 `.parents` 属性的迭代搜索. ## find_next_siblings() 合 find_next_sibling() `find_next_siblings( name , attrs , recursive , text , **kwargs )` `find_next_sibling( name , attrs , recursive , text , **kwargs )` 这2个方法通过 [.next_siblings](#next-siblings-previous-siblings) 属性对当tag的所有后面解析 \[5\] 的兄弟tag节点进行迭代, `find_next_siblings()` 方法返回所有符合条件的后面的兄弟节点, `find_next_sibling()` 只返回符合条件的后面的第一个tag节点. ``` first_link = soup.a first_link # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> first_link.find_next_siblings("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>] first_story_paragraph = soup.find("p", "story") first_story_paragraph.find_next_sibling("p") # <p class="story">...</p> ``` ## find_previous_siblings() 和 find_previous_sibling() `find_previous_siblings( name , attrs , recursive , text , **kwargs )` `find_previous_sibling( name , attrs , recursive , text , **kwargs )` 这2个方法通过 [.previous_siblings](#next-siblings-previous-siblings) 属性对当前tag的前面解析 \[5\] 的兄弟tag节点进行迭代, `find_previous_siblings()` 方法返回所有符合条件的前面的兄弟节点, `find_previous_sibling()` 方法返回第一个符合条件的前面的兄弟节点: ``` last_link = soup.find("a", id="link3") last_link # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a> last_link.find_previous_siblings("a") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] first_story_paragraph = soup.find("p", "story") first_story_paragraph.find_previous_sibling("p") # <p class="title"><b>The Dormouse's story</b></p> ``` ## find_all_next() 和 find_next() `find_all_next( name , attrs , recursive , text , **kwargs )` `find_next( name , attrs , recursive , text , **kwargs )` 这2个方法通过 [.next_elements](#next-elements-previous-elements) 属性对当前tag的之后的 \[5\] tag和字符串进行迭代, `find_all_next()` 方法返回所有符合条件的节点, `find_next()` 方法返回第一个符合条件的节点: ``` first_link = soup.a first_link # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> first_link.find_all_next(text=True) # [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie', # u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n'] first_link.find_next("p") # <p class="story">...</p> ``` 第一个例子中,字符串 “Elsie”也被显示出来,尽管它被包含在我们开始查找的`<a>`标签的里面.第二个例子中,最后一个`<p>`标签也被显示出来,尽管它与我们开始查找位置的`<a>`标签不属于同一部分.例子中,搜索的重点是要匹配过滤器的条件,并且在文档中出现的顺序而不是开始查找的元素的位置. ## find_all_previous() 和 find_previous() `find_all_previous( name , attrs , recursive , text , **kwargs )` `find_previous( name , attrs , recursive , text , **kwargs )` 这2个方法通过 [.previous_elements](#next-elements-previous-elements) 属性对当前节点前面 \[5\] 的tag和字符串进行迭代, `find_all_previous()` 方法返回所有符合条件的节点, `find_previous()` 方法返回第一个符合条件的节点. ``` first_link = soup.a first_link # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> first_link.find_all_previous("p") # [<p class="story">Once upon a time there were three little sisters; ...</p>, # <p class="title"><b>The Dormouse's story</b></p>] first_link.find_previous("title") # <title>The Dormouse's story</title> ``` `find_all_previous("p")` 返回了文档中的第一段(class=”title”的那段),但还返回了第二段,`<p>`标签包含了我们开始查找的`<a>`标签.不要惊讶,这段代码的功能是查找所有出现在指定`<a>`标签之前的`<p>`标签,因为这个`<p>`标签包含了开始的`<a>`标签,所以`<p>`标签一定是在`<a>`之前出现的. ## CSS选择器 Beautiful Soup支持大部分的CSS选择器 \[6\] ,在 `Tag` 或 `BeautifulSoup` 对象的 `.select()` 方法中传入字符串参数,即可使用CSS选择器的语法找到tag: ``` soup.select("title") # [<title>The Dormouse's story</title>] soup.select("p nth-of-type(3)") # [<p class="story">...</p>] ``` 通过tag标签逐层查找: ``` soup.select("body a") # [<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>] soup.select("html head title") # [<title>The Dormouse's story</title>] ``` 找到某个tag标签下的直接子标签 \[6\] : ``` soup.select("head > title") # [<title>The Dormouse's story</title>] soup.select("p > a") # [<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>] soup.select("p > a:nth-of-type(2)") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] soup.select("p > #link1") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] soup.select("body > a") # [] ``` 找到兄弟节点标签: ``` soup.select("#link1 ~ .sister") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] soup.select("#link1 + .sister") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] ``` 通过CSS的类名查找: ``` soup.select(".sister") # [<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>] soup.select("[class~=sister]") # [<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>] ``` 通过tag的id查找: ``` soup.select("#link1") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] soup.select("a#link2") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] ``` 通过是否存在某个属性来查找: ``` soup.select('a[href]') # [<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>] ``` 通过属性的值来查找: ``` soup.select('a[href="http://example.com/elsie"]') # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] soup.select('a[href^="http://example.com/"]') # [<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>] soup.select('a[href$="tillie"]') # [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] soup.select('a[href*=".com/el"]') # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] ``` 通过语言设置来查找: ``` multilingual_markup = """ <p lang="en">Hello</p> <p lang="en-us">Howdy, y'all</p> <p lang="en-gb">Pip-pip, old fruit</p> <p lang="fr">Bonjour mes amis</p> """ multilingual_soup = BeautifulSoup(multilingual_markup) multilingual_soup.select('p[lang|=en]') # [<p lang="en">Hello</p>, # <p lang="en-us">Howdy, y'all</p>, # <p lang="en-gb">Pip-pip, old fruit</p>] ``` 对于熟悉CSS选择器语法的人来说这是个非常方便的方法.Beautiful Soup也支持CSS选择器API,如果你仅仅需要CSS选择器的功能,那么直接使用 `lxml` 也可以,而且速度更快,支持更多的CSS选择器语法,但Beautiful Soup整合了CSS选择器的语法和自身方便使用API.