是什么导致 BeautifulSoup 函数出现“无”结果?如何避免“AttributeError: 'NoneType' object has no attribute...”用 BeautifulSoup?

What causes `None` results from BeautifulSoup functions? How can I avoid "AttributeError: 'NoneType' object has no attribute..." with BeautifulSoup?

提问人:Karl Knechtel 提问时间:3/26/2023 最后编辑:Karl Knechtel 更新时间:3/30/2023 访问量:724

问:

通常,当我尝试使用 BeautifulSoup 解析网页时,我会从 BeautifulSoup 函数中得到结果,否则会引发一个。NoneAttributeError

以下是一些独立的(即,由于数据是硬编码的,因此不需要 Internet 访问)示例,这些示例基于文档中的示例,不需要 Internet 访问:

>>> 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="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, 'html.parser')
>>> print(soup.sister)
None
>>> print(soup.find('a', class_='brother'))
None
>>> print(soup.select_one('a.brother'))
None
>>> soup.select_one('a.brother').text
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'text'

我知道这是 Python 中的一个特殊值这就是它的类型;但。。。现在怎么办?为什么我会得到这些结果,我该如何正确处理它们?NoneNoneType


这个问题专门针对查找单个结果(如 .find)的 BeautifulSoup 方法。如果使用通常返回列表的 .find_all 方法获得此结果,则可能是由于 HTML 解析器存在问题。有关详细信息,请参阅 Python Beautiful Soup 'NoneType' 对象错误

python beautifulsoup 属性错误 nonetype

评论


答:

2赞 Karl Knechtel 3/26/2023 #1

概述

通常,BeautifulSoup 提供两种查询:一种是查找单个特定元素(标签、属性、文本等),另一种是查找满足要求的每个元素。

对于后一组 - 这样的组可以给出多个结果 - 返回值将是一个列表。如果没有任何结果,则列表为空。漂亮而简单。.find_all

但是,对于像 .find.select_one 这样只能给出单个结果的方法,如果在 HTML 中找不到任何内容,则结果将为 None。BeautifulSoup 不会直接提出异常来解释问题。相反,在下面的代码中通常会出现 将,它试图不恰当地使用 (因为它希望接收其他内容 - 通常是 BeautifulSoup 定义的类的实例)。发生这种情况是因为根本不支持该操作;之所以称为 an,是因为语法意味着访问左侧任何内容的属性。 [TODO:一旦存在适当的规范,请链接到对属性是什么和什么是属性的解释。AttributeErrorNoneTagNoneAttributeError.AttributeError

例子

让我们一一考虑问题中不起作用的代码示例:

>>> print(soup.sister)
None

这将尝试在 HTML 中查找标记(而不是具有 等于 的其他此类属性的其他标记)。没有一个,所以结果是“无”。<sister>classidsister

>>> print(soup.find('a', class_='brother'))
None

这将尝试查找属性等于 的标签,例如 。该文档不包含任何类似内容;没有一个标签具有该类(它们都具有该类)。<a>classbrother<a href="https://example.com/bobby" class="brother">Bobby</a>asister

>>> print(soup.select_one('a.brother'))
None

这是使用不同方法执行与上一个示例相同的操作的另一种方法。(我们没有传递标记名称和一些属性值,而是传递 CSS 查询选择器。结果是一样的。

>>> soup.select_one('a.brother').text
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'text'

既然返回了,这和尝试做是一样的。该错误的含义正是它所说的:None 没有要访问的文本。事实上,它没有任何“普通”属性;该类仅定义特殊方法,例如(转换为字符串,以便它在打印时看起来像实际文本)。soup.select_one('a.brother')NoneNone.textNoneType__str__None'None'None

2赞 Karl Knechtel 3/30/2023 #2

真实世界数据的常见问题

当然,使用硬编码文本的一个小示例可以清楚地说明为什么对 etc. 方法的某些调用会失败 - 内容根本不存在,只需读取几行数据即可立即显现出来。任何调试代码的尝试都应首先仔细检查拼写错误find

>>> 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="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, 'html.parser')
>>> print(soup.find('a', class_='sistre')) # note the typo
None
>>> print(soup.find('a', class_='sister')) # corrected
<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

然而,在现实世界中,网页很容易跨越数千字节甚至兆字节的文本,因此这种视觉检查是不切实际的。一般来说,对于更复杂的任务,值得先花时间检查给定的网页是否提供了访问数据的 API,而不是将其从页面内容中抓取出来。许多网站都乐于以更易于使用的格式直接提供数据(因为它专门设计为作为数据使用,而不是填补“模板”网页的空白)。

粗略地概述一下:API 由端点组成 - 可以像网页 URL 一样直接访问的 URI,但响应不是网页。到目前为止,最常见的格式是 JSON,尽管根据确切的用例,可以使用任何数据格式 - 例如,数据表可能以 CSV 格式返回。若要使用标准 JSON 终结点,请编写代码来找出要使用的确切 URI,正常加载它,读取和分析 JSON 响应,然后继续处理该数据。(在某些情况下,“API密钥”是必要的;一些公司使用这些密钥来计费高级数据访问,但通常只是为了将信息请求与特定用户相关联。

通常,这比使用 BeautifulSoup 可以完成的任何事情都容易得多,并且还可以节省带宽。为其网页提供公开文档的 API 的公司希望您使用它们;一般来说,这对所有相关人员都更好。

综上所述,以下是 BeautifulSoup 解析的 Web 响应不包含预期内容处理起来不简单的一些常见原因。

动态(客户端)生成的内容

请记住,BeautifulSoup 处理的是静态 HTML,而不是 JavaScript。它只能使用在禁用 JavaScript 的情况下访问网页时看到的数据。

现代网页通常通过在客户端的 Web 浏览器中运行 JavaScript 来生成大量页面数据。在典型情况下,此 JavaScript 代码将发出更多的 HTTP 请求来获取数据、格式化数据并有效地即时编辑页面(更改 DOM)。BeautifulSoup 无法处理任何此类问题。它将网页中的 JavaScript 代码视为更多文本

抓取动态网站请考虑使用 Selenium 来模拟与网页的交互。

或者,调查正常使用网站时发生的情况。通常,页面上的 JavaScript 代码将调用 API 端点,这些端点可以在 Web 浏览器开发人员控制台的“网络”(或类似名称)选项卡上看到。这可能是理解网站API的一个很好的提示,即使要找到好的文档并不容易。

用户代理检查

每个 HTTP 请求都包含标头,这些标头向服务器提供信息以帮助服务器处理请求。这些信息包括有关缓存的信息(因此服务器可以决定是否可以使用缓存版本的数据)、可接受的数据格式(因此服务器可以对响应应用压缩以节省带宽)以及有关客户端的信息(因此服务器可以调整输出以在每个 Web 浏览器中看起来正确)。

最后一部分是使用标头的“user-agent”部分完成的。但是,默认情况下,HTML 库(like 和 )通常根本不会声明任何 Web 浏览器 - 在服务器端,这是“该用户正在运行一个程序来抓取网页,而不是实际使用 Web 浏览器”的一大危险信号。urllibrequests

大多数公司都不太喜欢这样。他们宁愿让您看到实际的网页(包括广告)。因此,服务器可能只是生成某种虚拟页面(或HTTP错误)。(注意:这可能包括“请求过多”错误,否则将指向下一节所述的速率限制。

若要解决此问题,请以适当的方式为 HTTP 库设置标头:

速率限制

“机器人”的另一个明显迹象是,同一用户在互联网连接允许的范围内请求多个网页,或者甚至没有等待一个页面完成加载,然后再请求另一个页面。即使不需要登录,服务器也会通过 IP(可能还有其他“指纹”信息)跟踪谁在发出请求,并且可能只是拒绝页面内容给请求页面太快的人。

这样的限制通常同样适用于 API(如果可用)——服务器正在保护自己免受拒绝服务攻击。因此,通常唯一的解决方法是修复代码以降低发出请求的频率,例如在请求之间暂停程序。

例如,请参阅如何避免 HTTP 错误 429(请求过多)python

需要登录

这非常简单:如果内容通常仅供登录用户使用,则抓取脚本将不得不模拟站点使用的任何登录程序。

服务器端动态/随机名称

请记住,服务器决定为每个请求发送什么。它不必每次都是一样的,也不必与服务器永久存储中的任何实际文件相对应。

例如,它可能包括动态生成的随机类名或 ID,每次访问页面时都可能不同。更棘手的是:由于缓存,名称可能看起来是一致的......直到缓存过期。

如果 HTML 源代码中的类名或 ID 中似乎有一堆无意义的垃圾字符,请考虑不要依赖该名称保持一致 - 想想另一种方法来识别必要的数据。或者,可以通过查看 HTML 中的其他标记如何引用标记 ID 来动态地找出标记 ID。

不规则结构化数据

例如,假设公司网站的“关于”页面显示几个关键员工的联系信息,并用一个标签包装每个人的信息。其中一些列出了电子邮件地址,而另一些则没有;当地址未列出时,相应的标签将完全不存在,而不仅仅是没有任何文本:<div class="staff">

soup = BeautifulSoup("""<html>
<head><title>Company staff</title></head><body>
<div class="staff">Name: <span class="name">Alice A.</span> Email: <span class="email">[email protected]</span></div>
<div class="staff">Name: <span class="name">Bob B.</span> Email: <span class="email">[email protected]</span></div>
<div class="staff">Name: <span class="name">Cameron C.</span></div>
</body>
</html>""", 'html.parser')

尝试迭代和打印每个名称和电子邮件将失败,因为缺少电子邮件:

>>> for staff in soup.select('div.staff'):
...     print('Name:', staff.find('span', class_='name').text)
...     print('Email:', staff.find('span', class_='email').text)
... 
Name: Alice A.
Email: [email protected]
Name: Bob B.
Email: [email protected]
Name: Cameron C.
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
AttributeError: 'NoneType' object has no attribute 'text'

这只是一种必须预料和处理的违规行为。

但是,根据确切的要求,可能会有更优雅的方法。例如,如果目标只是收集所有电子邮件地址(而不用担心名称),我们可以首先尝试使用列表推导式处理子标记的代码:

>>> [staff.find('span', class_='email').text for staff in soup.select('div.staff')]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
AttributeError: 'NoneType' object has no attribute 'text'

我们可以通过获取每个名称的电子邮件列表(其中将有 0 或 1 个元素)并使用专为平坦结果而设计的嵌套列表推导来解决这个问题:

>>> [email.text for staff in soup.select('div.staff') for email in staff.find_all('span', class_='email')]
['[email protected]', '[email protected]']

或者我们可以简单地使用一个更好的查询:

>>> # maybe we don't need to check for the div tags at all?
>>> [email.text for email in soup.select('span.email')]
['[email protected]', '[email protected]']
>>> # Or if we do, use a fancy CSS selector:
>>> # look for the span anywhere inside the div
>>> [email.text for email in soup.select('div.staff span.email')]
['[email protected]', '[email protected]']
>>> # require the div as an immediate parent of the span
>>> [email.text for email in soup.select('div.staff > span.email')]
['[email protected]', '[email protected]']

浏览器“更正”了无效的 HTML

HTML 很复杂,现实世界的 HTML 经常充斥着浏览器掩盖的错别字和小错误。没有人会使用一个迂腐的浏览器,如果页面源代码不是 100% 完全符合标准(无论是开始还是之后),都会弹出一条错误消息——因为如此庞大的 Web 部分会从视野中消失。

BeautifulSoup 通过让 HTML 解析器处理它来实现这一点,并让用户选择一个 HTML 解析器(如果除了标准库之外还安装了其他解析器)。另一方面,Web 浏览器内置了自己的 HTML 解析器,这可能要宽松得多,并且还采用更重量级的方法来“纠正”错误。

在此示例中,OP 的浏览器在其“Inspect Element”视图中显示了一个标记,即使该标记在实际页面源中不存在。另一方面,BeautifulSoup 使用的 HTML 解析器没有;它只是接受将标签直接嵌套在 .因此,由 BeautifulSoup 创建的用于表示表的相应元素,报告其属性。<tbody><table><tr><table>TagNonetbody

通常,像这样的问题可以通过在汤的子部分内搜索来解决(例如,通过使用CSS选择器),而不是尝试“单步执行”每个嵌套标签。这类似于结构不规则的数据问题。

根本不是HTML

因为它有时会出现,并且也与顶部的警告有关:并非每个 Web 请求都会生成一个网页。例如,图像不能使用 BeautifulSoup 进行处理;它甚至不表示文本,更不用说 HTML。不太明显的是,中间有类似内容的 URL 很可能是作为 API 端点,而不是网页;响应很可能是 JSON 格式的数据,而不是 HTML。BeautifulSoup 不是解析此数据的合适工具。/api/v1/

现代 Web 浏览器通常会为此类数据生成一个“包装器”HTML 文档。例如,如果我在 Imgur 上查看图像,使用直接图像 URL(不是 Imgur 自己的“图库”页面之一),并打开浏览器的 Web 检查器视图,我会看到类似的东西(替换了一些占位符):

<html>
    <head>
        <meta name="viewport" content="width=device-width; height=device-height;">
        <link rel="stylesheet" href="resource://content-accessible/ImageDocument.css">
        <link rel="stylesheet" href="resource://content-accessible/TopLevelImageDocument.css">
        <title>[image name] ([format] Image, [width]×[height] pixels) — Scaled ([scale factor])</title>
    </head>
    <body>
        <img src="[url]" alt="[url]" class="transparent shrinkToFit" width="[width]" height="[height]">
    </body>
</html>

对于 JSON,会生成一个更复杂的包装器 - 这实际上是浏览器的 JSON 查看器实现方式的一部分。

这里需要注意的重要一点是,当 Python 代码发出 Web 请求时,BeautifulSoup 不会看到任何此类 HTML - 该请求从未通过 Web 浏览器过滤,创建此 HTML 的是本地浏览器,而不是远程服务器。