Lzh on GitHub

在抓取网页时,最常见的任务是从 HTML 源代码中提取数据。有几种可用的库可以实现此目的,例如:

  • BeautifulSoup 是 Python 程序员中非常流行的网页抓取库,它根据 HTML 代码的结构构建一个 Python 对象,并且能够很好地处理不良标记,但它有一个缺点:速度慢
  • lxml 是一个 XML 解析库(也解析 HTML),具有基于 ElementTree 的 Pythonic API。(lxml 不是 Python 标准库的一部分。)

Scrapy 附带了自己的数据提取机制。它们被称为选择器,因为它们通过 XPathCSS 表达式“选择”HTML 文档的特定部分。

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

注意

Scrapy 选择器是 parsel 库的一个薄包装器;此包装器的目的是提供与 Scrapy 响应对象更好的集成。

parsel 是一个独立的网页抓取库,可以在没有 Scrapy 的情况下使用。它在底层使用 lxml 库,并在 lxml API 之上实现了一个简单的 API。这意味着 Scrapy 选择器在速度和解析准确性方面与 lxml 非常相似。

使用选择器

构造选择器

Response 对象在 .selector 属性上公开一个 Selector 实例:

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

使用 XPath 和 CSS 查询响应非常常见,因此响应包含另外两个快捷方式:response.xpath()response.css()

>>> 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'

从响应构造 - 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 vs HTML)。

使用选择器

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

https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

为了完整起见,这是它的完整 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.org/en/latest/_static/selectors-sample1.html

然后,在 shell 加载后,您将可以在 response shell 变量中获取响应,并在 response.selector 属性中获取其附加的选择器。

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

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

>>> 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()
[]

这意味着 response.css('foo::text').get() 即使元素存在也可能返回 None。如果您总是想要一个字符串,请使用 default=''

>>> response.css("img::text").get()
None
>>> 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 功能,并且 @attributes 可以用于 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() 方法编写。我们认为这些新方法会使代码更简洁易读。

以下示例显示了这些方法如何相互映射。

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

>>> response.css("a::attr(href)").get()
'image1.html'
>>> response.css("a::attr(href)").extract_first()
'image1.html'

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']

Selector.get()Selector.extract() 相同:

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

为了保持一致性,还有 Selector.getall(),它返回一个列表:

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

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

使用 XPath

以下是一些可以帮助您有效使用 Scrapy 选择器中的 XPath 的技巧。如果您对 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 规范中的位置路径部分。

按类查询时,考虑使用 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() 会生成一个文本元素集合——一个节点集。当节点集转换为字符串时(当它作为 contains()starts-with() 等字符串函数的参数传递时发生),它只会导致第一个元素的文本。

示例:

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

节点集转换为字符串:

>>> 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 ']

然而,将节点转换为字符串,会将其自身以及所有后代的文本组合在一起:

>>> 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: exception)。这是通过传递尽可能多的命名参数来完成的。

parsel 是 Scrapy 选择器背后的库,它有更多关于 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 扩展

Scrapy 选择器基于 lxml 构建,支持一些 EXSLT 扩展,并预注册了以下命名空间以用于 XPath 表达式:

前缀命名空间用法
rehttp://exslt.org/regular-expressions正则表达式
sethttp://exslt.org/sets集合操作

正则表达式

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

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

>>> 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),其中包含 itemscope 组和相应的 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 是一个 HtmlResponseXmlResponse 对象,将用于选择和提取数据。
  • text 是一个 Unicode 字符串或 UTF-8 编码的文本,用于 response 不可用的情况。同时使用 textresponse 是未定义行为。
  • type 定义选择器类型,可以是 "html""xml""json"None(默认)。
    • 如果 typeNone,选择器会根据 response 类型自动选择最佳类型(见下文),或者在与 text 一起使用时默认为 "html"
    • 如果 typeNone 并且传递了 response,则选择器类型根据响应类型推断如下:
      • "html" 用于 HtmlResponse 类型
      • "xml" 用于 XmlResponse 类型
      • "json" 用于 TextResponse 类型
      • "html" 用于其他任何类型
    • 否则,如果设置了 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 映射(字典),用于除了使用 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][source]

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

regex 可以是编译后的正则表达式,也可以是使用 re.compile(regex) 编译成正则表达式的字符串。 默认情况下,字符实体引用会被其对应的字符替换(&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

应用给定的正则表达式并返回第一个匹配的字符串。如果没有匹配项,则返回默认值(如果未提供参数,则为 None)。 默认情况下,字符实体引用会被其对应的字符替换(&amp;&lt; 除外)。将 replace_entities 设置为 False 会关闭这些替换。

register_namespace(prefix: str, uri: str) -> None[source]

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

remove_namespaces() -> None[source]

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

__bool__() -> bool[source]

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

getall() -> List[str][source]

序列化并以 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][source]

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

  • xpathSelector.xpath() 中的参数相同
  • namespaces 是一个可选的 prefix: namespace-uri 映射(字典),用于除了使用 register_namespace(prefix, uri) 注册的前缀之外的其他前缀。与 register_namespace() 不同,这些前缀不会保存以供将来调用。

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

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

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

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

  • querySelector.css() 中的参数相同

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

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

  • querySelector.jmespath() 中的参数相同。

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

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

getall() -> List[str][source]

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

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

get(default: None = None) -> str | None[source]

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 示例,用于说明几个概念。在所有情况下,我们都假设已经使用 HtmlResponse 对象实例化了一个 Selector,如下所示:

sel = Selector(html_response)

从 HTML 响应正文中选择所有 <h1> 元素,返回一个 Selector 对象列表(即 SelectorList 对象):

sel.xpath("//h1")

从 HTML 响应正文中提取所有 <h1> 元素的文本,返回一个字符串列表:

sel.xpath("//h1").getall()  # this includes the h1 tag
sel.xpath("//h1/text()").getall()  # this excludes the h1 tag

迭代所有 <p> 标签并打印它们的 class 属性:

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

XML 响应上的选择器示例

以下是一些示例,用于说明使用 XmlResponse 对象实例化的 Selector 对象的概念:

sel = Selector(xml_response)

从 XML 响应正文中选择所有 <product> 元素,返回一个 Selector 对象列表(即 SelectorList 对象):

sel.xpath("//product")

Google Base XML feed 中提取所有价格,这需要注册命名空间:

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