爬虫与网络编程基础-Task03:BS4 基础使用
0x00 Abstract
Beautiful Soup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库。它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。Beautiful Soup 会帮你节省数小时甚至数天的工作时间。
- 学习资料: Beautiful Soup 4.2.0 documentation
- 步骤 1:使用 requests 和 bs4 爬取 sklearn api 页面
- 步骤 2:在 api 页面中有多少个模块?有多少个 API?如 sklearn.base.DensityMixin,其中 base 为模块,DensityMixin 为 API。
- 步骤 3:将模块名作为 key,api 作为 value 存储为字典。
0x01 爬取 sklearn api 页面
Requests 获得网页 HTML 源代码
import requests
r = requests.get("https://scikit-learn.org/stable/modules/classes.html")
print(r.text)
demo = r.text
BS4 制作 Soup
from bs4 import BeautifulSoup
soup = BeautifulSoup(demo, "html.parser")
print(soup.prettify())
成功解析网页:
PS: .prettify()
方法可以帮助我们对 HTML 格式化与编码,为 HTML 源码添加一些换行符 \n
,来增强 HTML 的可读性。
0x02 BS4 解析库用法详解
Beautiful Soup 库的理解 Beautiful Soup 库是解析、遍历、维护“标签树”的功能库。
Beautiful Soup 类的理解
Beautiful Soup 库解析器
官方推荐使用 lxml 作为解析器,因为效率更高。
BS4 常用语法
Beautiful Soup 将 HTML 文档转换成一个树形结构,该结构有利于快速地遍历和搜索 HTML 文档。下面使用树状结构来描述一段 HTML 文档:
<html><head><title>c语言中文网</title></head><h1>c.biancheng.net</h1><p><b>一个学习编程的网站</b></p></body></html>
树状图如下:
文档树中的每个节点都是 Python 对象,这些对象大致分为四类:Tag , NavigableString , BeautifulSoup , Comment 。其中使用最多的是 Tag 和 NavigableString。
- Tag:标签类,HTML 文档中所有的标签都可以看做 Tag 对象。
- NavigableString:字符串类,指的是标签中的文本内容,使用 text、string、strings 来获取文本内容。
- BeautifulSoup:表示一个 HTML 文档的全部内容,您可以把它当作一个人特殊的 Tag 对象。
- Comment:表示 HTML 文档中的注释内容以及特殊字符串,它是一个特殊的 NavigableString。(不常用)
1. Tag 节点
标签(Tag)是组成 HTML 文档的基本元素。在 BS4 中,通过标签名和标签属性可以提取出想要的内容。看一组简单的示例:
from bs4 import BeautifulSoup
soup = BeautifulSoup('<p class="Web site url"><b>c.biancheng.net</b></p>', 'html.parser')
#获取整个p标签的html代码
print(soup.p)
#获取b标签
print(soup.p.b)
#获取p标签内容,使用NavigableString类中的string、text、get_text()
print(soup.p.text)
#返回一个字典,里面是属性和值
print(soup.p.attrs)
#查看返回的数据类型
print(type(soup.p))
#根据属性,获取标签的属性值,返回值为列表
print(soup.p['class'])
#给class属性赋值,此时属性值由列表转换为字符串
soup.p['class']=['Web','Site']
print(soup.p)
soup.p输出结果:
<p class="Web site url"><b>c.biancheng.net</b></p>
soup.p.b输出结果:
<b>c.biancheng.net</b>
soup.p.text输出结果:
c.biancheng.net
soup.p.attrs输出结果:
{'class': ['Web', 'site', 'url']}
type(soup.p)输出结果:
<class 'bs4.element.Tag'>
soup.p['class']输出结果:
['Web', 'site', 'url']
class属性重新赋值:
<p class="Web Site"><b>c.biancheng.net</b></p>
小结:Beautiful Soup 类的五种基本元素
2. 遍历节点
由于 HTML 本身是一种树形结构,所以我们有以下几种遍历方式。
Tag 对象提供了许多遍历 tag 节点的属性,比如 contents、children 用来遍历子节点;parent 与 parents 用来遍历父节点;而 next_sibling 与 previous_sibling 则用来遍历兄弟节点。
2.1. 下行遍历(子节点)
Tag 的 .contents
属性可以将 tag 的子节点以列表的方式输出:
from bs4 import BeautifulSoup
html_doc = """
<html><head><title>"c语言中文网"</title></head>
<body>
<p class="title"><b>c.biancheng.net</b></p>
<p class="website">一个学习编程的网站</p>
<a href="http://c.biancheng.net/python/" id="link1">python教程</a>,
<a href="http://c.biancheng.net/c/" id="link2">c语言教程</a> and
"""
soup = BeautifulSoup(html_doc, 'html.parser')
body_tag=soup.body
#打印原<body>标签内容
print(body_tag)
#以列表的形式输出,所有子节点
print(body_tag.contents)
输出结果:
<body>
<p class="title"><b>c.biancheng.net</b></p>
<p class="website">一个学习编程的网站</p>
<a href="http://c.biancheng.net/python/" id="link1">python教程</a>,
<a href="http://c.biancheng.net/c/" id="link2">c语言教程</a> and
</body>
#以列表的形式输出 可以看见换行符也在内
['\n', <p class="title"><b>c.biancheng.net</b></p>, '\n', <p class="website">一个学习编程的网站</p>, '\n', <a href=" http://c.biancheng.net/python/" id="link1">python教程</a>, '\n', <a href=" http://c.biancheng.net/c/" id="link2">c语言教程</a>, '\n']
因为 .contents
返回的是一个列表,所以可以有以下操作:
#打印列表的长度
print(len(body_tag.contents))
#打印列表中第二个元素(第一个元素是换行符)
print(body_tag.contents[1])
输出结果:
#打印列表的长度
9
#打印列表中第二个元素
<p class="title"><b>c.biancheng.net</b></p>
除了 .contents
之外,Tag 的 .children
属性会生成一个可迭代对象,可以用来遍历子节点,示例如下:
for child in body_tag.children:
print(child)
输出结果:
#注意此处已将换行符"\n"省略
<p class="title"><b>c.biancheng.net</b></p>
<p class="website">一个学习编程的网站</p>
<a href=" http://c.biancheng.net/python/" id="link1">python教程</a>
<a href=" http://c.biancheng.net/c/" id="link2">c语言教程</a>
.contents
和 .children
属性仅包含 tag 的直接子节点。例如,当前的 <head> 标签只有一个直接子节点 <title>:
head_tag.contents
'''输出
[<title>The Dormouse's story</title>]
'''
但是 <title> 标签也包含一个子节点:字符串 “The Dormouse’s story”,这种情况下字符串“The Dormouse’s story”也属于 <head> 标签的子孙节点。.descendants
属性可以对所有 tag 的子孙节点进行递归循环:
for child in head_tag.descendants:
print(child)
'''输出
<title>The Dormouse's story</title>
The Dormouse's story
'''
2.2. 上行遍历(父节点)
打印 soup.a
的所有父节点:
由于 soup.a
的父节点遍历会包含 soup
本身,但是当遍历到 soup.parent
时,由于 soup
不存在先辈节点,所以为空。因此这里要加一个判断 parent
是否为空。即 BeautifulSoup
对象的 .parent
是 None。
print(soup.parent)
# None
2.2. 平行遍历(兄弟节点)
小结:
3. 基于 BS4 库的 HTML 内容查找
了解 BS4 的基本元素之后,我们就需要利用 BS4 来对 HTML 做信息提取,从中找到我们需要的信息。信息提取的一般方法如下。
- 方法一:完整解析信息的标记形式,再提取关键信息。
- 信息标记的三种形式:XML JSON YAML
- 需要标记解析器,如:bs4 库的标签树遍历
- 优点:信息解析准确
- 缺点:提取过程繁琐,速度慢
- 方法二:无视标记形式,直接搜索关键信息
- 搜索:对信息的文本使用查找函数即可
- 优点:提取过程简洁,速度较快
- 缺点:提取结果的准确性与信息内容直接相关(也就是如果信息质量不好,提取的准确性差)
3.1. find_all() 与 find() find_all()
与 find()
是解析 HTML 文档的常用方法,它们可以在 HTML 文档中按照一定的条件(相当于过滤器)查找所需内容。find()
与 find_all()
的语法格式相似,希望大家在学习的时候,可以举一反三。
BS4 库中定义了许多用于搜索的方法,find() 与 find_all() 是最为关键的两个方法,其余方法的参数和使用与其类似。
3.1.1 find_all() find_all()
方法用来搜索当前 tag 的所有子节点,并判断这些节点是否符合过滤条件,最后以列表形式将符合条件的内容返回,语法格式如下:
find_all(name, attrs, recursive, text, limit)
参数说明:
- name:查找所有名字为 name 的 tag 标签,字符串对象会被自动忽略。
- attrs:按照属性名和属性值搜索 tag 标签,注意由于 class 是 Python 的关键字码,所以要使用 "class_"。
- recursive:find_all() 会搜索 tag 的所有子孙节点,设置 recursive=False 可以只搜索 tag 的直接子节点。
- text:用来搜文档中的字符串内容,该参数可以接受字符串 、正则表达式 、列表、True。
- limit:由于 find_all() 会返回所有的搜索结果,这样会影响执行效率,通过 limit 参数可以限制返回结果的数量。
3.1.2 find_all() find()
方法与 find_all()
类似,不同之处在于 find_all()
会将文档中所有符合条件的结果返回,而 find()
仅返回一个符合条件的结果,所以 find() 方法没有 limit
参数。
两种方法可以结合 re
使用,官方文档中说明的很详细,给的应用样例也很好,这里就不赘述了。 Beautiful Soup 4.4.0 文档 — Beautiful Soup 4.2.0 documentation
BS4 入门方法总结
与信息提取的方法 find_all
和 find()
。
0x03 提取 sklearn api 页面 的信息
具体任务:在 api 页面中有多少个模块?有多少个 API?如 sklearn.base.DensityMixin,其中 base 为模块,DensityMixin 为 API。
1. 查找 Module
首先查看网页的源代码。观察一下网页与源代码。 由于我们已知 sklearn.base 为模块,那我们先来在源代码中搜索一下。
可以很轻松地发现有这么一块,API Reference,结合网页发现是左边的一个索引栏(或者说目录)。检查后确认所有 Module 在这个索引栏的 <li>
列表里有依次列出。因此我们只需要从这一块代码来找 Module 就好了。
以第一个 Module 为例:
<li>
<a class="reference internal" href="[#module-sklearn.base](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.base)">
<code class="xref py py-mod docutils literal notranslate">
<span class="pre">sklearn.base</span>
</code>: Base classes and utility functions
</a>
可以看到内容写在 <span>
这个 Tag 里,然后外面还嵌套了好几层其他的标签。 上面的是我手工美化的,源码如下:
<li><a class="reference internal" href="[#module-sklearn.base](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.base)"><code class="xref py py-mod docutils literal notranslate"><span class="pre">sklearn.base</span></code>: Base classes and utility functions</a><ul>
<li><a class="reference internal" href="[#base-classes](https://scikit-learn.org/stable/modules/classes.html#base-classes)">Base classes</a></li>
<li><a class="reference internal" href="[#functions](https://scikit-learn.org/stable/modules/classes.html#functions)">Functions</a></li>
</ul>
</li>
如果你观察了网页源码会发现,这样一个 <li>
标签包裹的列表是第一个 Module sklearn.base
的全部内容(在网页左侧 API Reference 这一栏中)。然后所有的 Module 又一起嵌套在一个更大的 <li>
中。 为什么这里要重点提一下,因为我直到发现这些信息,才明白我直接做搜索的时候,那些一开始想不通的事情。
那么很简单,直接来把 API Reference 这一栏的所有 Module 检索出来就好了。既然都嵌套在 <li>
中,在源码中再搜索一下这个标签,发现有一大部分集中在一起(API Reference 的位置),但还有零散的一些分布在其他位置。但没关系直接从这些所有的 <li>
标签中找符合我们条件的就好。
思路如下:先找 <li>
,然后找其中嵌套着 class="pre"
的 <span>
标签。<span>
标签的 string
正是我们要搜索的信息,用一个正则表达式搜索 sklearn.
就好咯。
# 嵌套查询
# 不能直接 soup.find_all('li').find_all(),会报错
li = soup.find_all('li')
module = [] # 设置一个列表来存储读到的数据
# 我第一次搜素的时候发现有很多空列表值,所以加一个 if条件
# 另外一个重点是,对 class 属性设置条件时,应该用 class_ []
for i in li:
if i.find_all('span', class_ = 'pre', string = re.compile('sklearn.')) != []:
module.append(i.find_all('span', class_ = 'pre', string = re.compile('sklearn.')))
print(module[0])
来看一下获得的 module
列表:
module[0]
[<span class="pre">sklearn.base</span>,
<span class="pre">sklearn.calibration</span>,
<span class="pre">sklearn.cluster</span>,
<span class="pre">sklearn.compose</span>,
...
<span class="pre">sklearn.svm</span>,
<span class="pre">sklearn.tree</span>,
<span class="pre">sklearn.utils</span>]
module[1]:
[<span class="pre">sklearn.base</span>]
可以看到 module[0]
存储了所有的 Module,module[1]
存储了第一个 Module。后面的也都是存储单个的 Module。
我感觉是因为 api[0]
存的是,从最大的 <li>
找的里面的所有 <span>
标签。后面的是从最大的 <li>
里面嵌套的 <li>
中的 <span>
找到的信息,因为最小级别的 <li>
里面只有一个 <span>
。
再看一下 sklearn.base
Module 的源码,整体关系是这样的:
<li>
<li>
<span class="pre">sklearn.base</span>
<li></li>
<li></li>
</li>
...
</li>
理清了关系,来看一下有多少个 Module:
print(len(api[0]))
print(len(api))
38 39
因此,有 38 个 Module。后面多的那一个,不用问是什么了吧?
将 Module 存入 module
列表:
li = soup.find_all('li')
module = []
# 首先在所有<li>中遍历
# 然后找到我们需要的<li>
# 接着只用第一个最大的<li>,所以是 find(),只找第一个
# 最后找到这个最大<li>中所有的<span>,并返回他们的 string
# 存储后跳出循环,否则会继续遍历最大<li>中的其余子<li>标签
for i in li:
if i.find('a', class_ = 'reference internal') != None:
if i.find('span', class_ = 'pre', string = re.compile('sklearn.')) != None:
span = i.find_all('span', class_ = 'pre', string = re.compile('sklearn.'))
module = [x.string for x in span]
break
这里有一个细节需要注意。find()
如果找不到,返回 None
; find_all()
如果找不到,返回的是空列表 []
。也就是说前者返回一串字符,后者返回一个包含所有结果的列表。
2. 查找 API
一样的步骤,以 base
的第一个 API 为例,先找一下 base.BaseEstimator
的位置。可以发现一个 Module 的 API 都在 <tbody>
中(一个 Module 的所有 API 在两个 <tbody>
里面。一个 <section id="base-classes">
,一个 <section id="functions">
)。然后每一个 API 在其中的一个个 <tr>
里面。
<tr class="row-odd">
<td><p><a class="reference internal" href="[generated/sklearn.base.BaseEstimator.html#sklearn.base.BaseEstimator](https://scikit-learn.org****mator)" title="sklearn.base.BaseEstimator">
<code class="xref py py-obj docutils literal notranslate"><span class="pre">base.BaseEstimator</span></code></a></p></td>
<td><p>Base class for all estimators in scikit-learn.</p></td>
</tr>
我们要找的 API 是在 <tr>
中的第一个 <td>
的 .string
。
思路:首先找到 <tbody>
获得所有 API 的信息,然后在 <tbody>
中解析 <tr>
获得每一个 API 的信息,再把 <tr>
标签中的 <td>
标签找到,把 API 的名字写入列表。通过遍历节点已经查找方法获得。
2.1 首先试试对第一个 <tbody>
爬取:
import bs4
# 因为每一个标签的子标签可能存在字符串类型
# 而所有的 API 信息封装在 <tr> 标签中,<tr> 标签是标签类型
# 所以要过滤掉非标签类型的其他信息
for tr in soup.find('tbody').children:
if isinstance(tr, bs4.element.Tag): # 检测 tr 是否为标签类型
tds = tr('td')
print(tds[0].string) # 打印第一个 <td>
# 输出了第一个 <tbody> 里面的 API
base.BaseEstimator
base.BiclusterMixin
base.ClassifierMixin
base.ClusterMixin
base.DensityMixin
base.RegressorMixin
base.TransformerMixin
feature_selection.SelectorMixin
2.2 爬取所有 <tbody>
正解
api = []
for tbody in soup.find_all('tbody'): # 找到所有 <tbody>
for tr in tbody.find_all('tr'): # 对每一个 <tbody> 的子孙节点遍历
if isinstance(tr, bs4.element.Tag): # 检测 tr 是否为标签类型
tds = tr('td')
api.append(tds[0]('span')[0].string)
代码中的最后一步是,取 <tr>
中的第一个 <td>
,然后找 <td>
中的第一个 <span>
并且读取 .string
。
思考 正解是经过尝试,发现如果直接用之前的 tds[0].string
,会导致一些 API 读不到,所以取到更精确的那一级 <span>
标签。还没想明白为什么。情况如下(不正确的读取):
api = []
for tbody in soup.find_all('tbody'): # 找到所有 <tbody>
for tr in tbody.find_all('tr'): # 对每一个 <tbody> 的子孙节点遍历
if isinstance(tr, bs4.element.Tag): # 检测 tr 是否为标签类型
tds = tr('td') # 查找所有 <td>
api.append(tds[0]) # 只保存第一个 <td>
print(tds[0].string)
# 输出如下(省略了一些)
...
base.RegressorMixin
base.TransformerMixin
feature_selection.SelectorMixin
None
None
None
None
...
experimental.enable_iterative_imputer
experimental.enable_halving_search_cv
None
None
...
长度跟正解是一样的,只是有的 .string
读不出来。
再对存为列表的所有 <td>
研究一下:
这里是从中选了两个 <td>
标签的内容。
第一个能读出来,第二个读不出来:
进一步精细搜索:
原因未知。我猜可能是因为那些读不出来的是解码器识别不到,看看源码:
# api[50]
<td><p><a class="reference internal" href="generated/sklearn.covariance.OAS.html#sklearn.covariance.OAS" title="sklearn.covariance.OAS"><code class="xref py py-obj docutils literal notranslate"><span class="pre">covariance.OAS</span></code></a>(*[, store_precision, ...])</p></td>
<td>
中确实没有 string
(不知道 </p>
前面那个算不算),包含 string
的是其中的 <span>
标签,可能是这个原因吧。
后来经过群友的努力,大概是这个原因:
Type: property
String form: <property object at 0x000002036EFD32C8>
Source:
# text.string.fget
@property
def string(self):
"""Convenience property to get the single string within this
PageElement.
TODO It might make sense to have NavigableString.string return
itself.
:return: If this element has a single string child, return
value is that string. If this element has one child tag,
return value is the 'string' attribute of the child tag,
recursively. If this element is itself a string, has no
children, or has more than one child, return value is None.
"""
if len(self.contents) != 1:
return None
child = self.contents[0]
if isinstance(child, NavigableString):
return child
return child.string
# text.string.fset
@string.setter
def string(self, string):
"""Replace this PageElement's contents with `string`."""
self.clear()
self.append(string.__class__(string))
return
那里解释说到:If this element is itself a string, has no children, or has more than one child, return value is None.
所以大概是还有一个 (*[, store_precision, ...])
算是 have more than one child
吧。
api[50].text
的输出为 'covariance.OAS(*[,\xa0store_precision,\xa0...])'
。
0x04 存储爬取的信息
将爬取到的模块名作为 Key,API 作为 Value 存储为字典
其实跟上面的两个任务大同小异,关键是怎么搜索到每个 Moudule 对应的 API。
因为每个 Module 里面有多少 <tbody>
是不固定的,所以要找到每个 Module 外面包围的那个标签,不能像上一节一样只对 <tbody>
做处理。
看了源码可以发现每个 Module 对应了一个 <section>
标签,并且里面的 id
描述了这一个 <section>
。
<section id="module-sklearn.base">
先对第一个 Module 试试:
sec = soup.find('section', id = 'module-sklearn.base')
api = []
for tbody in sec.find_all('tbody'): # 找到所有 <tbody>
for tr in tbody.find_all('tr'): # 对每一个 <tbody> 的子孙节点遍历
if isinstance(tr, bs4.element.Tag): # 检测 tr 是否为标签类型
tds = tr('td')
api.append(tds[0]('span')[0].string)
# api
['base.BaseEstimator',
'base.BiclusterMixin',
'base.ClassifierMixin',
'base.ClusterMixin',
'base.DensityMixin',
'base.RegressorMixin',
'base.TransformerMixin',
'feature_selection.SelectorMixin',
'base.clone',
'base.is_classifier',
'base.is_regressor',
'config_context',
'get_config',
'set_config',
'show_versions']
使用我们之前存储的 module
列表,推广到所有的 <section>
:
def api_find(sec):
for tbody in sec.find_all('tbody'): # 找到所有 <tbody>
for tr in tbody.find_all('tr'): # 对每一个 <tbody> 的子孙节点遍历
if isinstance(tr, bs4.element.Tag): # 检测 tr 是否为标签类型
tds = tr('td')
print(tds[0]('span')[0].string)
print('------------------')
for mo in module:
print(mo)
print('------------------')
sec = soup.find('section', id = 'module-'+mo)
api_find(sec)
# 输出
sklearn.base
------------------
base.BaseEstimator
...
show_versions
------------------
sklearn.calibration
------------------
calibration.CalibratedClassifierCV
calibration.calibration_curve
------------------
sklearn.cluster
------------------
cluster.AffinityPropagation
cluster.AgglomerativeClustering
...
存为字典
def api_find(sec, dic, mo:str):
dic[mo] = [] # 创建字典当前 key 的 value list
if sec: # 添加这个是因为有的 sec 为 None,不加会报错,不知原因
for tbody in sec.find_all('tbody'):
for tr in tbody.find_all('tr'):
if isinstance(tr, bs4.element.Tag):
tds = tr('td')
dic[mo].append(tds[0]('span')[0].string)
sklearn_dic = {}
for mo in module:
sec = soup.find('section', id = 'module-' + mo)
api_find(sec, sklearn_dic, mo)
# sklearn_dic
{'sklearn.base': ['base.BaseEstimator',
'base.BiclusterMixin',
...
'show_versions'],
'sklearn.calibration': ['calibration.CalibratedClassifierCV',
'calibration.calibration_curve'],
'sklearn.cluster': ['cluster.AffinityPropagation',
'cluster.AgglomerativeClustering',
'cluster.Birch',
...
'cluster.spectral_clustering',
'cluster.ward_tree'],
'sklearn.compose': ['compose.ColumnTransformer',
'compose.TransformedTargetRegressor',
'compose.make_column_transformer',
'compose.make_column_selector'],
'sklearn.covariance': ['covariance.EmpiricalCovariance',
'covariance.EllipticEnvelope',
'covariance.GraphicalLasso',
...}
也可以用 from collections import defaultdict
,免得创建空列表了。
0x05 Conclusion
终于写完这一章了,感觉写的快要了老命了。后面的内容,如果有总结的很好的基础知识,考虑是不是不用重复造轮子。但写一遍好像又会帮我掌握的更好一些。基础知识这块尽量还是写的更加精简,把自己的思考写出来就好。
这章写完体会就是,重点还是在动手,实战过程中会遇到各种问题,可以引发更多的思考,或者对知识查缺补漏。
通过学习,掌握了 BS4 的基本用法。
References
Python BS4 解析库用法详解 Python 爬虫视频教程全集(62P)| bilibili BeautifulSoup 利用 find_all() 多级标签索引和获取标签中的属性内容 Python 中的 x for y in z for x in y语法详解 python在字典中创建一键多值的几种方法 Python 中3种创建字典数据的方法 Python字典及基本操作(超级详细)