选择器

当你抓取网页时,最常见的任务是从 HTML 源代码中提取数据。有几种库可以实现这一点,例如:

  • BeautifulSoup 是 Python 程序员中非常流行的网页抓取库,它根据 HTML 代码的结构构建 Python 对象,并且能够很好地处理糟糕的标记,但它有一个缺点:速度慢。

  • lxml 是一个 XML 解析库(也解析 HTML),它基于 ElementTree 提供了符合 Python 习惯的 API。(lxml 不是 Python 标准库的一部分。)

Scrapy 有自己的一套数据提取机制。它们被称为选择器(selectors),因为它们可以通过 XPathCSS 表达式来“选择” HTML 文档的特定部分。

XPath 是一种用于选择 XML 文档节点的语言,也可用于 HTML。CSS 是一种用于给 HTML 文档应用样式的语言。它定义了选择器来将这些样式与特定的 HTML 元素关联起来。

注意

Scrapy 选择器是 parsel 库的一个轻量级封装;这个封装的目的是为了更好地与 Scrapy Response 对象集成。

parsel 是一个独立的网页抓取库,可以在不使用 Scrapy 的情况下使用。它底层使用了 lxml 库,并在 lxml API 的基础上实现了一个简易的 API。这意味着 Scrapy 选择器在速度和解析精度上与 lxml 非常相似。

使用选择器

构建选择器

Response 对象通过 .selector 属性暴露了一个 Selector 实例

>>> response.selector.xpath("//span/text()").get()
'good'

使用 XPath 和 CSS 查询 Response 对象非常常见,因此 Response 对象包含了两个更方便的快捷方式:response.xpath()response.css()先后获得 Response 的便捷属性。

>>> response.xpath("//span/text()").get()
'good'
>>> response.css("span::text").get()
'good'

Scrapy 选择器是 Selector 类实例,通过传递 TextResponse 对象或将标记作为字符串(在 text 参数中)构建。

通常无需手动构建 Scrapy 选择器:response 对象在 Spider 回调中可用,因此在大多数情况下,使用 response.css()response.xpath() 快捷方式更方便。通过使用 response.selector 或其中一个快捷方式,还可以确保响应体只被解析一次。

但如果需要,可以直接使用 Selector。从文本构建

>>> from scrapy.selector import Selector
>>> body = "<html><body><span>good</span></body></html>"
>>> Selector(text=body).xpath("//span/text()").get()
'good'

从 Response 构建 - HtmlResponseTextResponse 的子类之一

>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse
>>> response = HtmlResponse(url="http://example.com", body=body, encoding="utf-8")
>>> Selector(response=response).xpath("//span/text()").get()
'good'

Selector 会根据输入类型自动选择最佳解析规则(XML 或 HTML)。

使用选择器

为了解释如何使用选择器,我们将使用 Scrapy shell(提供交互式测试)以及 Scrapy 文档服务器上的一个示例页面

为了完整起见,这里是它的完整 HTML 代码

<!DOCTYPE html>

<html>
  <head>
    <base href='http://example.com/' />
    <title>Example website</title>
  </head>
  <body>
    <div id='images'>
      <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' alt='image1'/></a>
      <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' alt='image2'/></a>
      <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' alt='image3'/></a>
      <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' alt='image4'/></a>
      <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' alt='image5'/></a>
    </div>
  </body>
</html>

首先,让我们打开 shell

scrapy shell https://docs.scrapy.net.cn/en/latest/_static/selectors-sample1.html

然后,shell 加载后,您将获得作为 response shell 变量的响应,以及其附加的选择器 response.selector 属性。

由于我们处理的是 HTML,选择器将自动使用 HTML 解析器。

因此,通过查看该页面的 HTML 代码,让我们构建一个 XPath 来选择 title 标签内的文本

>>> response.xpath("//title/text()")
[<Selector query='//title/text()' data='Example website'>]

要实际提取文本数据,必须调用选择器的 .get().getall() 方法,如下所示:

>>> response.xpath("//title/text()").getall()
['Example website']
>>> response.xpath("//title/text()").get()
'Example website'

.get() 始终返回单个结果;如果匹配项有多个,则返回第一个匹配项的内容;如果没有匹配项,则返回 None。.getall() 返回一个包含所有结果的列表。

请注意,CSS 选择器可以使用 CSS3 伪元素选择文本或属性节点

>>> response.css("title::text").get()
'Example website'

如您所见,.xpath().css() 方法返回一个 SelectorList 实例,这是一个新选择器的列表。此 API 可用于快速选择嵌套数据

>>> response.css("img").xpath("@src").getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']

如果要仅提取第一个匹配的元素,可以调用选择器的 .get()(或其别名 .extract_first(),这在以前的 Scrapy 版本中常用)

>>> response.xpath('//div[@id="images"]/a/text()').get()
'Name: My image 1 '

如果没有找到元素,则返回 None

>>> response.xpath('//div[@id="not-exists"]/text()').get() is None
True

可以提供一个默认返回值作为参数,用于替代 None

>>> response.xpath('//div[@id="not-exists"]/text()').get(default="not-found")
'not-found'

除了使用例如 '@src' XPath,还可以使用 Selector.attrib 属性来查询属性

>>> [img.attrib["src"] for img in response.css("img")]
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']

作为快捷方式,.attrib 也直接在 SelectorList 上可用;它返回第一个匹配元素的属性

>>> response.css("img").attrib["src"]
'image1_thumb.jpg'

这在预期只有一个结果时最有用,例如按 ID 选择或选择网页上的唯一元素时

>>> response.css("base").attrib["href"]
'http://example.com/'

现在我们将获取基本 URL 和一些图片链接

>>> response.xpath("//base/@href").get()
'http://example.com/'

>>> response.css("base::attr(href)").get()
'http://example.com/'

>>> response.css("base").attrib["href"]
'http://example.com/'

>>> response.xpath('//a[contains(@href, "image")]/@href').getall()
['image1.html',
'image2.html',
'image3.html',
'image4.html',
'image5.html']

>>> response.css("a[href*=image]::attr(href)").getall()
['image1.html',
'image2.html',
'image3.html',
'image4.html',
'image5.html']

>>> response.xpath('//a[contains(@href, "image")]/img/@src').getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']

>>> response.css("a[href*=image] img::attr(src)").getall()
['image1_thumb.jpg',
'image2_thumb.jpg',
'image3_thumb.jpg',
'image4_thumb.jpg',
'image5_thumb.jpg']

CSS 选择器扩展

根据 W3C 标准,CSS 选择器不支持选择文本节点或属性值。但在网页抓取场景中,选择这些内容至关重要,因此 Scrapy (parsel) 实现了一些非标准伪元素

  • 要选择文本节点,使用 ::text

  • 要选择属性值,使用 ::attr(name),其中 name 是您想要获取值的属性名

警告

这些伪元素是 Scrapy/Parsel 特有的。它们很可能不适用于 lxmlPyQuery 等其他库。

示例

  • title::text 选择后代 <title> 元素的子文本节点

>>> response.css("title::text").get()
'Example website'
  • *::text 选择当前选择器上下文的所有后代文本节点

>>> response.css("#images *::text").getall()
['\n   ',
'Name: My image 1 ',
'\n   ',
'Name: My image 2 ',
'\n   ',
'Name: My image 3 ',
'\n   ',
'Name: My image 4 ',
'\n   ',
'Name: My image 5 ',
'\n  ']
  • 如果 foo 元素存在但没有包含文本(即文本为空),foo::text 不会返回结果

>>> response.css("img::text").getall()
[]

This means ``.css('foo::text').get()`` could return None even if an element
exists. Use ``default=''`` if you always want a string:
>>> response.css("img::text").get()
>>> response.css("img::text").get(default="")
''
  • a::attr(href) 选择后代链接的 href 属性值

>>> response.css("a::attr(href)").getall()
['image1.html',
'image2.html',
'image3.html',
'image4.html',
'image5.html']

注意

另请参阅:选择元素属性

注意

您不能链式使用这些伪元素。但在实践中这也没有多大意义:文本节点没有属性,而属性值本身已经是字符串值,也没有子节点。

嵌套选择器

选择方法(.xpath().css())返回一个相同类型选择器的列表,因此您也可以对这些选择器调用选择方法。例如:

>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.getall()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg" alt="image1"></a>',
'<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg" alt="image2"></a>',
'<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg" alt="image3"></a>',
'<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg" alt="image4"></a>',
'<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg" alt="image5"></a>']

>>> for index, link in enumerate(links):
...     href_xpath = link.xpath("@href").get()
...     img_xpath = link.xpath("img/@src").get()
...     print(f"Link number {index} points to url {href_xpath!r} and image {img_xpath!r}")
...
Link number 0 points to url 'image1.html' and image 'image1_thumb.jpg'
Link number 1 points to url 'image2.html' and image 'image2_thumb.jpg'
Link number 2 points to url 'image3.html' and image 'image3_thumb.jpg'
Link number 3 points to url 'image4.html' and image 'image4_thumb.jpg'
Link number 4 points to url 'image5.html' and image 'image5_thumb.jpg'

选择元素属性

有几种方法可以获取属性的值。首先,可以使用 XPath 语法

>>> response.xpath("//a/@href").getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

XPath 语法有一些优势:它是标准的 XPath 特性,并且 @属性名 可以用于 XPath 表达式的其他部分 - 例如,可以按属性值进行过滤。

Scrapy 还提供了 CSS 选择器的扩展(::attr(...)),允许获取属性值

>>> response.css("a::attr(href)").getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

除此之外,Selector 还有一个 .attrib 属性。如果您倾向于在 Python 代码中查找属性,而不使用 XPath 或 CSS 扩展,可以使用它

>>> [a.attrib["href"] for a in response.css("a")]
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

此属性在 SelectorList 上也可用;它返回一个包含第一个匹配元素属性的字典。当选择器预期返回单个结果时(例如按元素 ID 选择,或选择页面上的唯一元素时),使用它很方便

>>> response.css("base").attrib
{'href': 'http://example.com/'}
>>> response.css("base").attrib["href"]
'http://example.com/'

空的 SelectorList 的 .attrib 属性是空的

>>> response.css("foo").attrib
{}

将选择器与正则表达式结合使用

Selector 还有一个 .re() 方法,用于使用正则表达式提取数据。然而,与使用 .xpath().css() 方法不同,.re() 返回一个字符串列表。因此您不能构建嵌套的 .re() 调用。

这里有一个示例,用于从上面的 HTML 代码 中提取图片名称

>>> response.xpath('//a[contains(@href, "image")]/text()').re(r"Name:\s*(.*)")
['My image 1 ',
'My image 2 ',
'My image 3 ',
'My image 4 ',
'My image 5 ']

还有一个额外的助手方法,与 .get()(及其别名 .extract_first())对应 .re(),名为 .re_first()。使用它来仅提取第一个匹配的字符串

>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r"Name:\s*(.*)")
'My image 1 '

extract() 和 extract_first()

如果您是 Scrapy 的老用户,您可能对选择器方法 .extract().extract_first() 很熟悉。许多博客文章和教程也使用它们。这些方法仍然受到 Scrapy 支持,没有计划弃用它们。

然而,Scrapy 的使用文档现在使用 .get().getall() 方法编写。我们认为这些新方法使代码更加简洁和易读。

以下示例展示了这些方法如何相互对应。

  1. SelectorList.get()SelectorList.extract_first() 相同

>>> response.css("a::attr(href)").get()
'image1.html'
>>> response.css("a::attr(href)").extract_first()
'image1.html'
  1. SelectorList.getall()SelectorList.extract() 相同

>>> response.css("a::attr(href)").getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
>>> response.css("a::attr(href)").extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
  1. Selector.get()Selector.extract() 相同

>>> response.css("a::attr(href)")[0].get()
'image1.html'
>>> response.css("a::attr(href)")[0].extract()
'image1.html'
  1. 为了一致性,也有 Selector.getall(),它返回一个列表

>>> response.css("a::attr(href)")[0].getall()
['image1.html']

因此,主要区别在于 .get().getall() 方法的输出更可预测:.get() 总是返回单个结果,.getall() 总是返回所有提取结果的列表。而使用 .extract() 方法时,结果是否为列表并不总是很明显;要获取单个结果,必须调用 .extract().extract_first()

使用 XPath

这里有一些可以帮助您有效使用 XPath 和 Scrapy 选择器的技巧。如果您还不熟悉 XPath,建议先看看这个 XPath 教程

注意

其中一些技巧基于 Zyte 博客的这篇文章

使用相对 XPath

请记住,如果您正在嵌套选择器并使用以 / 开头的 XPath,该 XPath 将是相对于文档的绝对路径,而不是相对于您调用它的 Selector 的相对路径。

例如,假设您想提取所有 <div> 元素内的所有 <p> 元素。首先,您将获取所有 <div> 元素

>>> divs = response.xpath("//div")

起初,您可能会试图使用以下方法,这是错误的,因为它实际上提取了文档中的所有 <p> 元素,而不仅仅是 <div> 元素内部的。

>>> for p in divs.xpath("//p"):  # this is wrong - gets all <p> from the whole document
...     print(p.get())
...

这才是正确的做法(注意 .//p XPath 前缀的圆点)

>>> for p in divs.xpath(".//p"):  # extracts all <p> inside
...     print(p.get())
...

另一种常见情况是提取所有直接的 <p> 子元素

>>> for p in divs.xpath("p"):
...     print(p.get())
...

有关相对 XPath 的更多详细信息,请参阅 XPath 规范中的 位置路径 (Location Paths) 部分。

按类查询时,考虑使用 CSS

因为一个元素可以包含多个 CSS 类,所以使用 XPath 按类选择元素的方式相当冗长

*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]

如果您使用 @class='someclass',您可能会遗漏包含其他类的元素;如果您仅使用 contains(@class, 'someclass') 来弥补这一点,如果元素具有包含字符串 someclass 的不同类名,您可能会得到超出预期的更多元素。

实际上,Scrapy 选择器允许您链式使用选择器,因此在大多数情况下,您可以直接使用 CSS 按类选择,然后在需要时切换到 XPath

>>> from scrapy import Selector
>>> sel = Selector(
...     text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>'
... )
>>> sel.css(".shout").xpath("./time/@datetime").getall()
['2014-07-23 19:00']

这比上面展示的冗长 XPath 技巧更简洁。只需记住在后续的 XPath 表达式中使用 .

注意 //node[1] 和 (//node)[1] 的区别

//node[1] 选择在其各自父节点下的第一个出现的节点。

(//node)[1] 选择文档中的所有节点,然后只获取其中的第一个。

示例

>>> from scrapy import Selector
>>> sel = Selector(
...     text="""
...     <ul class="list">
...         <li>1</li>
...         <li>2</li>
...         <li>3</li>
...     </ul>
...     <ul class="list">
...         <li>4</li>
...         <li>5</li>
...         <li>6</li>
...     </ul>"""
... )
>>> xp = lambda x: sel.xpath(x).getall()

这将获取在其各自父节点下的所有第一个 <li> 元素

>>> xp("//li[1]")
['<li>1</li>', '<li>4</li>']

而这将获取整个文档中的第一个 <li> 元素

>>> xp("(//li)[1]")
['<li>1</li>']

这将获取 <ul> 父节点下的所有第一个 <li> 元素

>>> xp("//ul/li[1]")
['<li>1</li>', '<li>4</li>']

而这将获取整个文档中 <ul> 父节点下的第一个 <li> 元素

>>> xp("(//ul/li)[1]")
['<li>1</li>']

在条件中使用文本节点

当您需要将文本内容用作 XPath 字符串函数的参数时,避免使用 .//text(),而应使用 .

这是因为表达式 .//text() 产生一个文本元素集合——一个 节点集 (node-set)。当一个节点集转换为字符串时(当它作为参数传递给 contains()starts-with() 等字符串函数时发生),结果只会是第一个元素的文本。

示例

>>> from scrapy import Selector
>>> sel = Selector(
...     text='<a href="#">Click here to go to the <strong>Next Page</strong></a>'
... )

节点集 (node-set) 转换为字符串

>>> sel.xpath("//a//text()").getall()  # take a peek at the node-set
['Click here to go to the ', 'Next Page']
>>> sel.xpath("string(//a[1]//text())").getall()  # convert it to string
['Click here to go to the ']

然而,将一个 节点 (node) 转换为字符串会将其自身的文本与其所有后代的文本组合在一起

>>> sel.xpath("//a[1]").getall()  # select the first node
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath("string(//a[1])").getall()  # convert it to string
['Click here to go to the Next Page']

因此,在这种情况下使用 .//text() 节点集将不会选择任何内容

>>> sel.xpath("//a[contains(.//text(), 'Next Page')]").getall()
[]

但使用 . 表示节点,就可以正常工作

>>> sel.xpath("//a[contains(., 'Next Page')]").getall()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']

XPath 表达式中的变量

XPath 允许您在 XPath 表达式中引用变量,使用 $somevariable 语法。这与 SQL 世界中的参数化查询或预处理语句有些类似,您可以用 ? 等占位符替换查询中的某些参数,然后用查询传递的值替换它们。

这里有一个示例,用于根据元素的“id”属性值进行匹配,而无需硬编码(这在前面已经展示过)

>>> # `$val` used in the expression, a `val` argument needs to be passed
>>> response.xpath("//div[@id=$val]/a/text()", val="images").get()
'Name: My image 1 '

这是另一个示例,查找包含五个 <a> 子元素的 <div> 标签的“id”属性(这里我们将值 5 作为整数传递)

>>> response.xpath("//div[count(a)=$cnt]/@id", cnt=5).get()
'images'

调用 .xpath() 时,所有变量引用都必须有绑定值(否则您将收到 ValueError: XPath error: 异常)。这通过传递所需数量的命名参数来完成。

为 Scrapy 选择器提供支持的库 parsel,在其文档中有关于 XPath 变量的更多细节和示例。

移除命名空间

在处理抓取项目时,完全摆脱命名空间并直接使用元素名称来编写更简单/方便的 XPath 通常非常方便。您可以使用 Selector.remove_namespaces() 方法来实现这一点。

让我们用 Python Insider 博客的 atom feed 来举例说明这一点。

首先,我们使用要抓取的 URL 打开 shell

$ scrapy shell https://feeds.feedburner.com/PythonInsider

文件开头是这样的

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet ...
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/"
      xmlns:blogger="http://schemas.google.com/blogger/2008"
      xmlns:georss="http://www.georss.org/georss"
      xmlns:gd="http://schemas.google.com/g/2005"
      xmlns:thr="http://purl.org/syndication/thread/1.0"
      xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0">
  ...

您可以看到几个命名空间声明,包括默认的 "http://www.w3.org/2005/Atom" 以及使用 gd: 前缀的 "http://schemas.google.com/g/2005"

进入 shell 后,我们可以尝试选择所有 <link> 对象,会发现它不起作用(因为 Atom XML 命名空间正在混淆这些节点)

>>> response.xpath("//link")
[]

但是一旦我们调用 Selector.remove_namespaces() 方法,所有节点都可以通过它们的名称直接访问

>>> response.selector.remove_namespaces()
>>> response.xpath("//link")
[<Selector query='//link' data='<link rel="alternate" type="text/html" h'>,
    <Selector query='//link' data='<link rel="next" type="application/atom+'>,
    ...

如果您想知道为什么默认情况下不总是调用命名空间移除过程,而需要手动调用,这是出于两个原因,按重要性顺序排列如下:

  1. 移除命名空间需要迭代和修改文档中的所有节点,这对 Scrapy 抓取的所有文档来说是一个相当耗时的默认操作

  2. 在某些情况下,可能确实需要使用命名空间,以防不同命名空间中的一些元素名称发生冲突。不过,这种情况非常罕见。

使用 EXSLT 扩展

由于构建在 lxml 之上,Scrapy 选择器支持一些 EXSLT 扩展,并带有以下预注册的命名空间,可在 XPath 表达式中使用

前缀

命名空间

用法

re

http://exslt.org/regular-expressions

正则表达式

set

http://exslt.org/sets

集合操作

正则表达式

例如,当 XPath 的 starts-with()contains() 不够用时,test() 函数会非常有用。

示例:选择“class”属性以数字结尾的列表项中的链接

>>> from scrapy import Selector
>>> doc = """
... <div>
...     <ul>
...         <li class="item-0"><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>
...     </ul>
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> sel.xpath("//li//@href").getall()
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').getall()
['link1.html', 'link2.html', 'link4.html', 'link5.html']

警告

C 库 libxslt 本身不支持 EXSLT 正则表达式,因此 lxml 的实现使用了 Python 的 re 模块的钩子。因此,在 XPath 表达式中使用正则表达式函数可能会带来轻微的性能损失。

集合操作

例如,在提取文本元素之前,这些操作对于排除文档树的部分内容非常有用。

示例:提取微数据(示例内容取自 https://schema.org/Product),包含 itemscopes 组和相应的 itemprops

>>> doc = """
... <div itemscope itemtype="http://schema.org/Product">
...   <span itemprop="name">Kenmore White 17" Microwave</span>
...   <img src="kenmore-microwave-17in.jpg" alt='Kenmore 17" Microwave' />
...   <div itemprop="aggregateRating"
...     itemscope itemtype="http://schema.org/AggregateRating">
...    Rated <span itemprop="ratingValue">3.5</span>/5
...    based on <span itemprop="reviewCount">11</span> customer reviews
...   </div>
...   <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
...     <span itemprop="price">$55.00</span>
...     <link itemprop="availability" href="http://schema.org/InStock" />In stock
...   </div>
...   Product description:
...   <span itemprop="description">0.7 cubic feet countertop microwave.
...   Has six preset cooking categories and convenience features like
...   Add-A-Minute and Child Lock.</span>
...   Customer reviews:
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Not a happy camper</span> -
...     by <span itemprop="author">Ellie</span>,
...     <meta itemprop="datePublished" content="2011-04-01">April 1, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1">
...       <span itemprop="ratingValue">1</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">The lamp burned out and now I have to replace
...     it. </span>
...   </div>
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Value purchase</span> -
...     by <span itemprop="author">Lucas</span>,
...     <meta itemprop="datePublished" content="2011-03-25">March 25, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1"/>
...       <span itemprop="ratingValue">4</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">Great microwave for the price. It is small and
...     fits in my apartment.</span>
...   </div>
...   ...
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> for scope in sel.xpath("//div[@itemscope]"):
...     print("current scope:", scope.xpath("@itemtype").getall())
...     props = scope.xpath(
...         """
...                 set:difference(./descendant::*/@itemprop,
...                                .//*[@itemscope]/*/@itemprop)"""
...     )
...     print(f"    properties: {props.getall()}")
...     print("")
...

current scope: ['http://schema.org/Product']
    properties: ['name', 'aggregateRating', 'offers', 'description', 'review', 'review']

current scope: ['http://schema.org/AggregateRating']
    properties: ['ratingValue', 'reviewCount']

current scope: ['http://schema.org/Offer']
    properties: ['price', 'availability']

current scope: ['http://schema.org/Review']
    properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']

current scope: ['http://schema.org/Rating']
    properties: ['worstRating', 'ratingValue', 'bestRating']

current scope: ['http://schema.org/Review']
    properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']

current scope: ['http://schema.org/Rating']
    properties: ['worstRating', 'ratingValue', 'bestRating']

这里我们首先迭代 itemscope 元素,对于每一个,我们查找所有 itemprops 元素,并排除那些本身位于另一个 itemscope 内部的元素。

其他 XPath 扩展

Scrapy 选择器还提供了一个急需的 XPath 扩展函数 has-class,对于包含所有指定 HTML 类的节点,该函数返回 True

对于以下 HTML

>>> from scrapy.http import HtmlResponse
>>> response = HtmlResponse(
...     url="http://example.com",
...     body="""
... <html>
...     <body>
...         <p class="foo bar-baz">First</p>
...         <p class="foo">Second</p>
...         <p class="bar">Third</p>
...         <p>Fourth</p>
...     </body>
... </html>
... """,
...     encoding="utf-8",
... )

你可以这样使用它

>>> response.xpath('//p[has-class("foo")]')
[<Selector query='//p[has-class("foo")]' data='<p class="foo bar-baz">First</p>'>,
<Selector query='//p[has-class("foo")]' data='<p class="foo">Second</p>'>]
>>> response.xpath('//p[has-class("foo", "bar-baz")]')
[<Selector query='//p[has-class("foo", "bar-baz")]' data='<p class="foo bar-baz">First</p>'>]
>>> response.xpath('//p[has-class("foo", "bar")]')
[]

因此,XPath //p[has-class("foo", "bar-baz")] 大致相当于 CSS p.foo.bar-baz。请注意,在大多数情况下,它的速度较慢,因为它是一个纯 Python 函数,会为每个相关节点调用,而 CSS 查询则被翻译成 XPath,因此运行效率更高。所以在性能方面,它的用途仅限于那些不易用 CSS 选择器描述的情况。

Parsel 还通过 set_xpathfunc() 简化了添加你自己的 XPath 扩展。

内置选择器参考

Selector 对象

class scrapy.Selector(*args: Any, **kwargs: Any)[source]

Selector 的实例是响应(response)的一个包装器,用于选择其内容的特定部分。

response 是一个 HtmlResponse 或一个 XmlResponse 对象,将用于选择和提取数据。

text 是一个 unicode 字符串或 utf-8 编码的文本,用于 response 不可用的情况。同时使用 textresponse 是未定义行为。

type 定义选择器类型,可以是 "html""xml""json"None(默认)。

如果 typeNone,选择器会根据 response 类型自动选择最佳类型(见下文),或者在与 text 一起使用时默认为 "html"

如果 typeNone 并且传递了 response,选择器类型会根据响应类型推断,如下所示:

否则,如果设置了 type,则选择器类型将被强制使用,不会进行自动检测。

xpath(query: str, namespaces: Mapping[str, str] | None = None, **kwargs: Any) SelectorList[_SelectorType][source]

查找与 xpath query 匹配的节点,并将结果作为所有元素展平后的 SelectorList 实例返回。列表元素也实现了 Selector 接口。

query 是一个包含要应用的 XPATH 查询的字符串。

namespaces 是一个可选的 prefix: namespace-uri 映射(dict),用于为通过 register_namespace(prefix, uri) 注册的额外前缀。与 register_namespace() 相反,这些前缀不会为将来的调用保存。

任何额外的命名参数都可以用来传递 XPath 表达式中 XPath 变量的值,例如:

selector.xpath('//a[href=$url]', url="http://www.example.com")

注意

为了方便,此方法可以调用为 response.xpath()

css(query: str) SelectorList[_SelectorType][source]

应用给定的 CSS 选择器并返回一个 SelectorList 实例。

query 是一个包含要应用的 CSS 选择器的字符串。

在后台,CSS 查询使用 cssselect 库翻译成 XPath 查询,并运行 .xpath() 方法。

注意

为了方便,此方法可以调用为 response.css()

jmespath(query: str, **kwargs: Any) SelectorList[_SelectorType][source]

查找与 JMESPath query 匹配的对象,并将结果作为所有元素展平后的 SelectorList 实例返回。列表元素也实现了 Selector 接口。

query 是一个包含要应用的 JMESPath 查询的字符串。

任何额外的命名参数都会传递给底层的 jmespath.search 调用,例如:

selector.jmespath('author.name', options=jmespath.Options(dict_cls=collections.OrderedDict))

注意

为了方便,此方法可以调用为 response.jmespath()

get() Any[source]

序列化并返回匹配的节点。

对于 HTML 和 XML,结果总是一个字符串,且百分比编码的内容会被解码。

另请参阅:extract() 和 extract_first()

attrib

返回底层元素的属性字典。

另请参阅:选择元素属性

re(regex: str | Pattern[str], replace_entities: bool = True) List[str]

应用给定的正则表达式并返回包含匹配项的字符串列表。

regex 可以是已编译的正则表达式,也可以是将被 re.compile(regex) 编译成正则表达式的字符串。

默认情况下,字符实体引用会被其对应的字符替换(&amp;&lt; 除外)。将 replace_entities 设置为 False 会关闭这些替换。

re_first(regex: str | Pattern[str], default: None = None, replace_entities: bool = True) str | None
re_first(regex: str | Pattern[str], default: str, replace_entities: bool = True) str

应用给定的正则表达式并返回第一个匹配的字符串。如果没有匹配项,则返回默认值(如果未提供该参数,则为 None)。

默认情况下,字符实体引用会被其对应的字符替换(&amp;&lt; 除外)。将 replace_entities 设置为 False 会关闭这些替换。

register_namespace(prefix: str, uri: str) None

注册给定的命名空间以便在此 Selector 中使用。如果不注册命名空间,你将无法从非标准命名空间中选择或提取数据。参阅 XML 响应的选择器示例

remove_namespaces() None

移除所有命名空间,允许使用无命名空间的 xpath 遍历文档。参阅 移除命名空间

__bool__() bool

如果选择了任何实际内容,则返回 True,否则返回 False。换句话说,Selector 的布尔值由其选择的内容决定。

getall() List[str]

序列化并返回包含匹配节点的 1 元素字符串列表。

此方法添加到 Selector 是为了保持一致性;它在使用 SelectorList 时更有用。另请参阅:extract() 和 extract_first()

SelectorList 对象

class scrapy.selector.SelectorList(iterable=(), /)[source]

SelectorList 类是内置 list 类的子类,提供了一些额外的方法。

xpath(xpath: str, namespaces: Mapping[str, str] | None = None, **kwargs: Any) SelectorList[_SelectorType]

对此列表中的每个元素调用 .xpath() 方法,并将其结果展平后作为另一个 SelectorList 返回。

xpath 的参数与 Selector.xpath() 中的参数相同。

namespaces 是一个可选的 prefix: namespace-uri 映射(dict),用于为通过 register_namespace(prefix, uri) 注册的额外前缀。与 register_namespace() 相反,这些前缀不会为将来的调用保存。

任何额外的命名参数都可以用来传递 XPath 表达式中 XPath 变量的值,例如:

selector.xpath('//a[href=$url]', url="http://www.example.com")
css(query: str) SelectorList[_SelectorType]

对此列表中的每个元素调用 .css() 方法,并将其结果展平后作为另一个 SelectorList 返回。

query 的参数与 Selector.css() 中的参数相同。

jmespath(query: str, **kwargs: Any) SelectorList[_SelectorType]

对此列表中的每个元素调用 .jmespath() 方法,并将其结果展平后作为另一个 SelectorList 返回。

query 的参数与 Selector.jmespath() 中的参数相同。

任何额外的命名参数都会传递给底层的 jmespath.search 调用,例如:

selector.jmespath('author.name', options=jmespath.Options(dict_cls=collections.OrderedDict))
getall() List[str]

对此列表中的每个元素调用 .get() 方法,并将其结果展平后作为字符串列表返回。

另请参阅:extract() 和 extract_first()

get(default: None = None) str | None
get(default: str) str

返回此列表中第一个元素调用 .get() 的结果。如果列表为空,则返回默认值。

另请参阅:extract() 和 extract_first()

re(regex: str | Pattern[str], replace_entities: bool = True) List[str][source]

对列表中的每个元素调用 .re() 方法,并将结果展平,作为字符串列表返回。

默认情况下,字符实体引用会替换为其对应的字符(除了 &amp;&lt;)。将 replace_entities 设置为 False 可以关闭这些替换。

re_first(regex: str | Pattern[str], default: None = None, replace_entities: bool = True) str | None[source]
re_first(regex: str | Pattern[str], default: str, replace_entities: bool = True) str

对列表中第一个元素调用 .re() 方法,并以字符串形式返回结果。如果列表为空或正则表达式不匹配任何内容,则返回默认值(如果未提供参数,则为 None)。

默认情况下,字符实体引用会替换为其对应的字符(除了 &amp;&lt;)。将 replace_entities 设置为 False 可以关闭这些替换。

attrib

返回第一个元素的属性字典。如果列表为空,则返回空字典。

另请参阅:选择元素属性

示例

HTML 响应上的 Selector 示例

这里有一些 Selector 示例来说明几个概念。在所有情况下,我们都假设已经使用 HtmlResponse 对象实例化了一个 Selector,如下所示

sel = Selector(html_response)
  1. 从 HTML 响应体中选择所有 <h1> 元素,返回 Selector 对象的列表(即一个 SelectorList 对象)

    sel.xpath("//h1")
    
  2. 从 HTML 响应体中提取所有 <h1> 元素的文本,返回字符串列表

    sel.xpath("//h1").getall()  # this includes the h1 tag
    sel.xpath("//h1/text()").getall()  # this excludes the h1 tag
    
  3. 遍历所有 <p> 标签并打印其 class 属性

    for node in sel.xpath("//p"):
        print(node.attrib["class"])
    

XML 响应上的 Selector 示例

这里有一些示例来说明使用 XmlResponse 对象实例化的 Selector 对象的概念

sel = Selector(xml_response)
  1. 从 XML 响应体中选择所有 <product> 元素,返回 Selector 对象的列表(即一个 SelectorList 对象)

    sel.xpath("//product")
    
  2. 从需要注册命名空间的 Google Base XML feed 中提取所有价格

    sel.register_namespace("g", "http://base.google.com/ns/1.0")
    sel.xpath("//g:price").getall()