解析器的主要类¶
了解各种元素选择方式后,我们再整体看看 Selector 和其他对象,以便更好地理解解析引擎。
Selector 是 Scrapling 解析引擎的核心,负责解析 HTML 和选择元素。可以通过以下任意方式导入:
然后像概览中介绍的那样直接使用:page = Selector(
'<html>...</html>',
url='https://example.com'
)
# Then select elements as you like
elements = page.css('.product')
也就是说,整个页面是一个 Selector 对象,里面的各个元素也是 Selector 对象。元素文本和属性值等文本内容是 TextHandler 对象。每个元素的属性集合保存在 AttributesHandler 中。这两个类稍后再讲,先来看 Selector。
Selector¶
参数说明¶
最重要的参数是 content,用于传入要解析的 HTML,支持 str 和 bytes。
url 设置页面 URL,用于解析相对链接,并按域名区分已保存的自适应数据。adaptive、storage 和 storage_args 与自适应功能有关。不使用该功能时,这些参数不会产生影响。
还有一些参数用于调整解析过程,或控制解析时如何处理 HTML:
- encoding:解析 HTML 使用的编码,默认为
UTF-8。 - keep_comments:解析时是否保留 HTML 注释。默认关闭,因为注释可能以多种方式影响抓取。
这里先不介绍 huge_tree 和 root,避免让这页变得过于复杂。
类似的高级功能会暂时略过,因为正常使用库时并不需要了解它们。如果想深入研究,可以阅读开发部分。
页面和元素的大多数属性都采用延迟加载,只有使用时才会初始化,例如页面或元素的文本内容。这也是 Scrapling 速度快的原因之一 :)
属性¶
概览已经展示过不少内容。没看过也不用担心,这里会结合更多方法和用法详细介绍。为了便于阅读,用于遍历的属性放在后面的遍历部分。
为方便演示,假设要解析以下 HTML 页面:
<html>
<head>
<title>Some page</title>
</head>
<body>
<div class="product-list">
<article class="product" data-id="1">
<h3>Product 1</h3>
<p class="description">This is product 1</p>
<span class="price">$10.99</span>
<div class="hidden stock">In stock: 5</div>
</article>
<article class="product" data-id="2">
<h3>Product 2</h3>
<p class="description">This is product 2</p>
<span class="price">$20.99</span>
<div class="hidden stock">In stock: 3</div>
</article>
<article class="product" data-id="3">
<h3>Product 3</h3>
<p class="description">This is product 3</p>
<span class="price">$15.99</span>
<div class="hidden stock">Out of stock</div>
</article>
</div>
<script id="page-data" type="application/json">
{
"lastUpdated": "2024-09-22T10:30:00Z",
"totalProducts": 3
}
</script>
</body>
</html>
>>> page.get_all_text()
'Some page\n\n \n\n \nProduct 1\nThis is product 1\n$10.99\nIn stock: 5\nProduct 2\nThis is product 2\n$20.99\nIn stock: 3\nProduct 3\nThis is product 3\n$15.99\nOut of stock'
get_all_text 有以下可选参数:
- separator:使用这个分隔符连接所有收集到的字符串,默认为 '\n'。
- strip:连接前移除字符串首尾的空白,默认关闭。
- ignore_tags:需要忽略的标签名元组,标签内的所有嵌套元素也会被忽略。默认为
('script', 'style',)。 - valid_values:只收集有实际文本的元素,忽略空文本或只有空白的元素。默认开启。
这里返回的文本不是普通字符串,而是 TextHandler,后面会详细介绍。如果文本可以解析为 JSON,可以直接调用 .json():
>>> script = page.find('script')
>>> script.json()
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
html:
现在应该已经理解 page 和 element 的关系,后面就不再重复了。
获取元素的属性集合:
通过以下任意方式访问某个属性: 通过以下方式检查是否包含指定属性: 获取元素的 HTML 内容:>>> article.html_content
'<article class="product" data-id="1"><h3>Product 1</h3>\n <p class="description">This is product 1</p>\n <span class="price">$10.99</span>\n <div class="hidden stock">In stock: 5</div>\n </article>'
<article class="product" data-id="1"><h3>Product 1</h3>
<p class="description">This is product 1</p>
<span class="price">$10.99</span>
<div class="hidden stock">In stock: 5</div>
</article>
.body 获取页面的原始内容。从 v0.4 开始,对 Fetcher 返回的 Response 对象调用 .body,结果始终为 bytes。
获取元素在 DOM 树中的所有祖先:
>>> article.path
[<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>,
<data='<body> <div class="product-list"> <artic...' parent='<html><head><title>Some page</title></he...'>,
<data='<html><head><title>Some page</title></he...'>]
>>> article.generate_css_selector
'body > div > article'
>>> article.generate_full_css_selector
'body > div > article'
>>> article.generate_xpath_selector
"//body/div/article"
>>> article.generate_full_xpath_selector
"//body/div/article"
遍历¶
使用上面找到的元素,详细看看如何在页面中移动。
如果还不了解 DOM 树或树形数据结构,下面的内容可能不太容易理解。建议先查阅这些概念。
如果不想另外查找,这里有一个简单说明。
html 是页面树的根元素,因为每个页面都从 html 元素开始。
head 和 body 等元素直接位于它下面,因此是 html 的子元素,而 html 是它们的父元素。body 和 head 有同一个父元素,所以互为同级元素。
访问元素的父元素:
>>> article.parent
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
>>> article.parent.tag
'div'
>>> article.children
[<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>]
children:
>>> article.below_elements
[<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>]
children 相同。
使用类名为 product-list 的元素,可以更清楚地看到 children 和 below_elements 的区别:
>>> products_list = page.css('.product-list')[0]
>>> products_list.children
[<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]
>>> products_list.below_elements
[<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>,
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
...]
>>> article.siblings
[<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]
>>> article.next
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>
previous 用于获取上一个同级元素:
>>> article.previous # It's the first child, so it doesn't have a previous element
>>> second_article = page.css('.product[data-id="2"]')[0]
>>> second_article.previous
<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>
True,否则返回 False:
>>> article.find_ancestor(lambda ancestor: ancestor.has_class('product-list'))
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
>>> article.find_ancestor(lambda ancestor: ancestor.css('.product-list')) # Same result, different approach
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
Selectors¶
Selectors 是 Selector 的列表版本。它继承 Python 的标准 List 类型,保留所有 List 属性和方法,并添加一些方法,让操作其中的 Selector 实例更方便。
Selector 中所有返回一组元素的方法和属性,都会返回 Selectors 实例。
从 v0.4 开始,所有选择方法都统一返回 Selector/Selectors,包括文本节点和属性值。通过 ::text、/text()、::attr()、/@attr 选出的文本节点也会包装成 Selector。这些对象的 tag 是 "#text",text 属性返回文本值。仍然可以直接读取文本值,其他属性会返回空值或默认值,不会因此报错。
page.css('a::text') # -> Selectors (of text node Selectors)
page.xpath('//a/text()') # -> Selectors
page.css('a::text').get() # -> TextHandler (the first text value)
page.css('a::text').getall() # -> TextHandlers (all text values)
page.css('a::attr(href)') # -> Selectors
page.xpath('//a/@href') # -> Selectors
page.css('.price_color') # -> Selectors
数据提取方法¶
从 v0.4 开始,Selector 和 Selectors 都提供 get()、getall() 及其别名 extract_first 和 extract,遵循 Scrapy 的用法。旧的 get_all() 已移除。
对于 Selector 对象:
get()返回TextHandler。对于文本节点,返回文本值;对于 HTML 元素,返回包含元素自身标签的序列化 HTML。getall()返回一个TextHandlers列表,其中只包含这一个序列化字符串。extract_first是get()的别名,extract是getall()的别名。
>>> page.css('h3')[0].get() # Outer HTML of the element
'<h3>Product 1</h3>'
>>> page.css('h3::text')[0].get() # Text value of the text node
'Product 1'
对于 Selectors 对象:
get(default=None)返回第一个元素的序列化字符串。列表为空时返回default。getall()序列化所有元素,并返回TextHandlers列表。extract_first是get()的别名,extract是getall()的别名。
>>> page.css('.price::text').get() # First price text
'$10.99'
>>> page.css('.price::text').getall() # All price texts
['$10.99', '$20.99', '$15.99']
>>> page.css('.price::text').get('') # With default value
'$10.99'
这些方法适用于 CSS、XPath、find 等所有选择方式,也是以兼容 Scrapy 的方式提取文本和属性值的推荐方法。
接着看看 Selectors 还提供了哪些功能。
属性¶
除了遍历、切片等标准 Python 列表操作,还可以使用以下功能:
直接对包含的 Selector 实例执行 CSS 和 XPath 选择。返回类型与 Selector 的 css、xpath 相同。参数也类似,但这里不能使用 adaptive。这让链式调用很方便。
>>> page.css('.product h3::text').getall()
['Product 1', 'Product 2', 'Product 3']
>>> page.css('.product').css('h3::text').getall()
['Product 1', 'Product 2', 'Product 3']
re 和 re_first。它们的参数与 Selector 中相同,后面的 TextHandler 部分会详细解释。
不过,这里的 re_first 会对每个 Selector 调用 re,并返回第一个有结果的匹配。re 则照常返回 TextHandlers,把所有 TextHandler 实例合并到一个 TextHandlers 实例中。
>>> page.css('.price::text').re(r'[\d.]+')
['10.99', '20.99', '15.99']
>>> page.css('.product h3::text').re(r'Product (\d+)')
['1', '2', '3']
search 可以快速查找包含的 Selector 实例。传入的函数必须接收一个 Selector 作为第一个参数,并返回 True 或 False。方法会返回第一个满足条件的 Selector,没有匹配时返回 None。
>>> product = page.css('.product').search(lambda p: p.css('.price::text').get() == '$20.99')
>>> product.css('h3::text').get()
'Product 2'
filter 也接收与 search 类似的函数,但会返回一个 Selectors,包含所有满足条件的 Selector:
>>> products = page.css('.product').filter(lambda p: float(p.css('.price::text').re_first(r'[\d.]+')) > 15)
>>> products.css('h3::text').getall()
['Product 2', 'Product 3']
>>> page.css('.product').first # First Selector or None
<data='<article class="product" data-id="1"><h3...'>
>>> page.css('.product').last # Last Selector or None
<data='<article class="product" data-id="3"><h3...'>
>>> page.css('.nonexistent').first # Returns None instead of raising IndexError
如果想用简短的方式获取 Selectors 中 Selector 实例的数量,可以这样写:
这等同于: 没错,就像 JavaScript :)TextHandler¶
这个类很重要。所有应返回字符串的方法和属性都会返回 TextHandler,应返回字符串列表的则会返回 TextHandlers。
TextHandler 继承标准 Python 字符串,因此普通字符串能做的操作,它也能做。那为什么还需要单独的名称呢?
因为 TextHandler 提供了额外的方法和属性。库中的各个类都使用 TextHandler 返回字符串,所以可以灵活组合这些功能,让代码更短、更清晰。也可以直接导入它,对任意字符串使用,具体会在后面介绍。
用法¶
切片、索引访问,以及 split、replace、strip 等操作和方法,都会再次返回 TextHandler,因此可以继续链式调用。如果发现某个方法或属性返回了普通字符串,而不是 TextHandler,请提交 Issue,我们也会调整它。
先介绍 re 和 re_first。这些方法也存在于 Selector、Selectors 和 TextHandlers 中,参数相同。
-
re的第一个参数可以是字符串或编译后的正则表达式。它查找所有匹配的字符串,并返回 TextHandlers。re_first接收相同参数,行为也类似,但只返回第一个结果,类型为TextHandler。还有以下实用参数:
- replace_entities:默认开启,将字符实体引用替换为对应字符。
- clean_match:默认关闭,匹配时忽略所有空白,包括连续空格。
- case_sensitive:默认开启。关闭后,编译正则表达式时会忽略大小写。
这些例子前面已经见过。因为调用的是
re,所以结果为 TextHandlers。为说明其他参数,下面的每个例子使用一个自定义字符串:>>> page.css('.price::text').re(r'[\d.]+') ['10.99', '20.99', '15.99'] >>> page.css('.product h3::text').re(r'Product (\d+)') ['1', '2', '3']到处使用>>> from scrapling import TextHandler >>> test_string = TextHandler('hi there') # Hence the two spaces >>> test_string.re('hi there') >>> test_string.re('hi there', clean_match=True) # Using `clean_match` will clean the string before matching the regex ['hi there'] >>> test_string2 = TextHandler('Oh, Hi Mark') >>> test_string2.re_first('oh, hi Mark') >>> test_string2.re_first('oh, hi Mark', case_sensitive=False) # Hence disabling `case_sensitive` 'Oh, Hi Mark' # Mixing arguments >>> test_string.re('hi there', clean_match=True, case_sensitive=False) ['hi there']TextHandler的另一个好处是,html_content也会返回TextHandler。因此可以直接对 HTML 内容使用正则表达式: -
.json()会尝试快速把内容转换为 JSON 对象,无法转换时抛出错误:如果选择元素时没有指定文本节点,例如元素文本或属性文本,就会自动使用元素的文本内容: Selector 还做了额外处理。假设页面如下:>>> page.css('#page-data::text').get() '\n {\n "lastUpdated": "2024-09-22T10:30:00Z",\n "totalProducts": 3\n }\n ' >>> page.css('#page-data::text').get().json() {'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}前面介绍的 Selector 方法<html> <body> <div> <script id="page-data" type="application/json"> { "lastUpdated": "2024-09-22T10:30:00Z", "totalProducts": 3 } </script> </div> </body> </html>get_all_text也会返回TextHandler。
如果这样调用: 就会报错,因为div标签没有可以解析为 JSON 的直接文本,实际上它完全没有直接文本。
这时可以使用get_all_text:这里使用>>> page.css('div')[0].get_all_text(ignore_tags=[]).json() {'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}ignore_tags,是因为它默认忽略('script', 'style',)。
使用 Fetcher 时还有一个相关行为。假设获取到这样的 JSON 响应: Selector 主要处理 HTML 页面,因此会把它当作不完整的 HTML 并修复。读取html_content时会得到: 不过,直接调用json仍然有效:html标签没有直接文本,为什么还能成功呢?
为处理 JSON 响应等情况,Selector 会保留收到内容的原始副本。调用.json()时,会先尝试把这个副本解析为 JSON。如果没有副本,例如操作的是单个元素,则检查元素的直接文本;否则会直接使用get_all_text。
-
还可以传入.clean()会移除空白字符和连续空格,并返回新的TextHandler实例:remove_entities,让clean把 HTML 实体替换为对应字符。 -
也可以反向排序:.sort()可以像处理列表一样对字符串排序:
以后会继续添加方法和属性。请记住,库中几乎所有原本返回字符串的地方都会返回这个类。
TextHandlers¶
这个类的思路与 Selectors 和 Selector 类似。它继承标准列表的行为,并增加 re 和 re_first。
区别在于,这里的 re_first 会对每个 TextHandler 调用 re,返回第一个结果或 None。目前没有其他需要说明的内容,以后会继续添加方法。
AttributesHandler¶
这是标准 Python 字典 dict 的只读版本,专门保存元素或 Selector 实例的属性。
>>> print(page.find('script').attrib)
{'id': 'page-data', 'type': 'application/json'}
>>> type(page.find('script').attrib).__name__
'AttributesHandler'
目前还提供两个简单功能:
-
search_values方法普通字典可以用
dict.get("key_name")检查键是否存在。如果要按值搜索,通常需要额外代码。这个方法可以直接搜索属性值,并为每个匹配项返回一个字典。简单示例:
还可以通过partial搜索值的一部分:上面的例子比较简单。更实际的用法是与>>> for i in page.find('script').attrib.search_values('page', partial=True): print(i) {'id': 'page-data'}find_all配合,查找具有指定属性值的所有元素:这些元素的>>> page.find_all(lambda element: list(element.attrib.search_values('product'))) [<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>, <data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>, <data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]class属性值都是 'product'。这里使用
list,是因为search_values返回生成器。如果不转换,对所有元素都会被判断为True。 -
json_string属性如果当前属性可以序列化为 JSON,这个属性会将它们转换为 JSON 字符串,否则抛出错误。