选择器
当你抓取网页时,最常见的任务是从 HTML 源代码中提取数据。有几种库可以实现这一点,例如:
BeautifulSoup 是 Python 程序员中非常流行的网页抓取库,它根据 HTML 代码的结构构建 Python 对象,并且能够很好地处理糟糕的标记,但它有一个缺点:速度慢。
lxml 是一个 XML 解析库(也解析 HTML),它基于
ElementTree
提供了符合 Python 习惯的 API。(lxml 不是 Python 标准库的一部分。)
Scrapy 有自己的一套数据提取机制。它们被称为选择器(selectors),因为它们可以通过 XPath 或 CSS 表达式来“选择” 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 构建 - HtmlResponse
是 TextResponse
的子类之一
>>> 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 是您想要获取值的属性名
示例
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()
方法编写。我们认为这些新方法使代码更加简洁和易读。
以下示例展示了这些方法如何相互对应。
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
这里有一些可以帮助您有效使用 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:
异常)。这通过传递所需数量的命名参数来完成。
移除命名空间
在处理抓取项目时,完全摆脱命名空间并直接使用元素名称来编写更简单/方便的 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+'>,
...
如果您想知道为什么默认情况下不总是调用命名空间移除过程,而需要手动调用,这是出于两个原因,按重要性顺序排列如下:
移除命名空间需要迭代和修改文档中的所有节点,这对 Scrapy 抓取的所有文档来说是一个相当耗时的默认操作
在某些情况下,可能确实需要使用命名空间,以防不同命名空间中的一些元素名称发生冲突。不过,这种情况非常罕见。
使用 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
不可用的情况。同时使用text
和response
是未定义行为。type
定义选择器类型,可以是"html"
、"xml"
、"json"
或None
(默认)。如果
type
是None
,选择器会根据response
类型自动选择最佳类型(见下文),或者在与text
一起使用时默认为"html"
。如果
type
是None
并且传递了response
,选择器类型会根据响应类型推断,如下所示:对于
HtmlResponse
类型为"html"
对于
XmlResponse
类型为"xml"
对于
TextResponse
类型为"json"
对于其他任何类型为
"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
映射(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()
- re(regex: str | Pattern[str], replace_entities: bool = True) List[str]
应用给定的正则表达式并返回包含匹配项的字符串列表。
regex
可以是已编译的正则表达式,也可以是将被re.compile(regex)
编译成正则表达式的字符串。默认情况下,字符实体引用会被其对应的字符替换(
&
和<
除外)。将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
)。默认情况下,字符实体引用会被其对应的字符替换(
&
和<
除外)。将replace_entities
设置为False
会关闭这些替换。
- register_namespace(prefix: str, uri: str) None
注册给定的命名空间以便在此
Selector
中使用。如果不注册命名空间,你将无法从非标准命名空间中选择或提取数据。参阅 XML 响应的选择器示例。
- 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))
- get(default: None = None) str | None
- get(default: str) str
返回此列表中第一个元素调用
.get()
的结果。如果列表为空,则返回默认值。
- re(regex: str | Pattern[str], replace_entities: bool = True) List[str] [source]
对列表中的每个元素调用
.re()
方法,并将结果展平,作为字符串列表返回。默认情况下,字符实体引用会替换为其对应的字符(除了
&
和<
)。将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
)。默认情况下,字符实体引用会替换为其对应的字符(除了
&
和<
)。将replace_entities
设置为False
可以关闭这些替换。
示例
HTML 响应上的 Selector 示例
这里有一些 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 响应上的 Selector 示例
这里有一些示例来说明使用 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()