feat: Upload the library agent skill
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Adaptive scraping
|
||||
|
||||
Adaptive scraping (previously known as automatch) is one of Scrapling's most powerful features. It allows your scraper to survive website changes by intelligently tracking and relocating elements.
|
||||
|
||||
Consider a page with a structure like this:
|
||||
```html
|
||||
<div class="container">
|
||||
<section class="products">
|
||||
<article class="product" id="p1">
|
||||
<h3>Product 1</h3>
|
||||
<p class="description">Description 1</p>
|
||||
</article>
|
||||
<article class="product" id="p2">
|
||||
<h3>Product 2</h3>
|
||||
<p class="description">Description 2</p>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
To scrape the first product (the one with the `p1` ID), a selector like this would be used:
|
||||
```python
|
||||
page.css('#p1')
|
||||
```
|
||||
When website owners implement structural changes like
|
||||
```html
|
||||
<div class="new-container">
|
||||
<div class="product-wrapper">
|
||||
<section class="products">
|
||||
<article class="product new-class" data-id="p1">
|
||||
<div class="product-info">
|
||||
<h3>Product 1</h3>
|
||||
<p class="new-description">Description 1</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="product new-class" data-id="p2">
|
||||
<div class="product-info">
|
||||
<h3>Product 2</h3>
|
||||
<p class="new-description">Description 2</p>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
The selector will no longer function, and your code needs maintenance. That's where Scrapling's `adaptive` feature comes into play.
|
||||
|
||||
With Scrapling, you can enable the `adaptive` feature the first time you select an element, and the next time you select that element and it doesn't exist, Scrapling will remember its properties and search on the website for the element with the highest percentage of similarity to that element.
|
||||
|
||||
```python
|
||||
from scrapling import Selector, Fetcher
|
||||
# Before the change
|
||||
page = Selector(page_source, adaptive=True, url='example.com')
|
||||
# or
|
||||
Fetcher.adaptive = True
|
||||
page = Fetcher.get('https://example.com')
|
||||
# then
|
||||
element = page.css('#p1', auto_save=True)
|
||||
if not element: # One day website changes?
|
||||
element = page.css('#p1', adaptive=True) # Scrapling still finds it!
|
||||
# the rest of your code...
|
||||
```
|
||||
It works with all selection methods, not just CSS/XPath selection.
|
||||
|
||||
## Real-World Scenario
|
||||
This example uses [The Web Archive](https://archive.org/)'s [Wayback Machine](https://web.archive.org/) to demonstrate adaptive scraping across different versions of a website. A copy of [StackOverflow's website in 2010](https://web.archive.org/web/20100102003420/http://stackoverflow.com/) is compared against the current design to show that the adaptive feature can extract the same button using the same selector.
|
||||
|
||||
To extract the Questions button from the old design, a selector like `#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a` can be used (this specific selector was generated by Chrome).
|
||||
|
||||
Testing the same selector in both versions:
|
||||
```python
|
||||
>> from scrapling import Fetcher
|
||||
>> selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a'
|
||||
>> old_url = "https://web.archive.org/web/20100102003420/http://stackoverflow.com/"
|
||||
>> new_url = "https://stackoverflow.com/"
|
||||
>> Fetcher.configure(adaptive = True, adaptive_domain='stackoverflow.com')
|
||||
>>
|
||||
>> page = Fetcher.get(old_url, timeout=30)
|
||||
>> element1 = page.css(selector, auto_save=True)[0]
|
||||
>>
|
||||
>> # Same selector but used in the updated website
|
||||
>> page = Fetcher.get(new_url)
|
||||
>> element2 = page.css(selector, adaptive=True)[0]
|
||||
>>
|
||||
>> if element1.text == element2.text:
|
||||
... print('Scrapling found the same element in the old and new designs!')
|
||||
'Scrapling found the same element in the old and new designs!'
|
||||
```
|
||||
The `adaptive_domain` argument is used here because Scrapling sees `archive.org` and `stackoverflow.com` as two different domains and would isolate their `adaptive` data. Passing `adaptive_domain` tells Scrapling to treat them as the same website for adaptive data storage.
|
||||
|
||||
In a typical scenario with the same URL for both requests, the `adaptive_domain` argument is not needed. The adaptive logic works the same way with both the `Selector` and `Fetcher` classes.
|
||||
|
||||
**Note:** The main reason for creating the `adaptive_domain` argument was to handle if the website changed its URL while changing the design/structure. In that case, it can be used to continue using the previously stored adaptive data for the new URL. Otherwise, Scrapling will consider it a new website and discard the old data.
|
||||
|
||||
## How the adaptive scraping feature works
|
||||
Adaptive scraping works in two phases:
|
||||
|
||||
1. **Save Phase**: Store unique properties of elements
|
||||
2. **Match Phase**: Find elements with similar properties later
|
||||
|
||||
After selecting an element through any method, the library can find it the next time the website is scraped, even if it undergoes structural/design changes.
|
||||
|
||||
The general logic is as follows:
|
||||
|
||||
1. Scrapling saves that element's unique properties (methods shown below).
|
||||
2. Scrapling uses its configured database (SQLite by default) and saves each element's unique properties.
|
||||
3. Because everything about the element can be changed or removed by the website's owner(s), nothing from the element can be used as a unique identifier for the database. The storage system relies on two things:
|
||||
1. The domain of the current website. When using the `Selector` class, pass it when initializing; when using a fetcher, the domain is automatically taken from the URL.
|
||||
2. An `identifier` to query that element's properties from the database. The identifier does not always need to be set manually (see below).
|
||||
|
||||
Together, they will later be used to retrieve the element's unique properties from the database.
|
||||
|
||||
4. Later, when the website's structure changes, enabling `adaptive` causes Scrapling to retrieve the element's unique properties and match all elements on the page against them. A score is calculated based on their similarity to the desired element. Everything is taken into consideration in that comparison.
|
||||
5. The element(s) with the highest similarity score to the wanted element are returned.
|
||||
|
||||
### The unique properties
|
||||
The unique properties Scrapling relies on are:
|
||||
|
||||
- Element tag name, text, attributes (names and values), siblings (tag names only), and path (tag names only).
|
||||
- Element's parent tag name, attributes (names and values), and text.
|
||||
|
||||
The comparison between elements is not exact; it is based on how similar these values are. Everything is considered, including the values' order (e.g., the order in which class names are written).
|
||||
|
||||
## How to use adaptive feature
|
||||
The adaptive feature can be applied to any found element and is added as arguments to CSS/XPath selection methods.
|
||||
|
||||
First, enable the `adaptive` feature by passing `adaptive=True` to the [Selector](main_classes.md#selector) class when initializing it, or enable it on the fetcher being used.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
>>> from scrapling import Selector, Fetcher
|
||||
>>> page = Selector(html_doc, adaptive=True)
|
||||
# OR
|
||||
>>> Fetcher.adaptive = True
|
||||
>>> page = Fetcher.get('https://example.com')
|
||||
```
|
||||
When using the [Selector](main_classes.md#selector) class, pass the URL of the website with the `url` argument so Scrapling can separate the properties saved for each element by domain.
|
||||
|
||||
If no URL is passed, the word `default` will be used in place of the URL field while saving the element's unique properties. This is only an issue when using the same identifier for a different website without passing the URL parameter. The save process overwrites previous data, and the `adaptive` feature uses only the latest saved properties.
|
||||
|
||||
The `storage` and `storage_args` arguments control the database connection; by default, the SQLite class provided by the library is used.
|
||||
|
||||
There are two main ways to use the `adaptive` feature:
|
||||
|
||||
### The CSS/XPath Selection way
|
||||
First, use the `auto_save` argument while selecting an element that exists on the page:
|
||||
```python
|
||||
element = page.css('#p1', auto_save=True)
|
||||
```
|
||||
When the element no longer exists, use the same selector with the `adaptive` argument to have the library find it:
|
||||
```python
|
||||
element = page.css('#p1', adaptive=True)
|
||||
```
|
||||
With the `css`/`xpath` methods, the identifier is set automatically to the selector string passed to the method.
|
||||
|
||||
Additionally, for all these methods, you can pass the `identifier` argument to set it yourself. This is useful in some instances, or you can use it to save properties with the `auto_save` argument.
|
||||
|
||||
### The manual way
|
||||
Elements can be manually saved, retrieved, and relocated within the `adaptive` feature. This allows relocating any element found by any method.
|
||||
|
||||
Example of getting an element by text:
|
||||
```python
|
||||
>>> element = page.find_by_text('Tipping the Velvet', first_match=True)
|
||||
```
|
||||
Save its unique properties using the `save` method. The identifier must be set manually (use a meaningful identifier):
|
||||
```python
|
||||
>>> page.save(element, 'my_special_element')
|
||||
```
|
||||
Later, retrieve and relocate the element inside the page with `adaptive`:
|
||||
```python
|
||||
>>> element_dict = page.retrieve('my_special_element')
|
||||
>>> page.relocate(element_dict, selector_type=True)
|
||||
[<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>]
|
||||
>>> page.relocate(element_dict, selector_type=True).css('::text').getall()
|
||||
['Tipping the Velvet']
|
||||
```
|
||||
The `retrieve` and `relocate` methods are used here.
|
||||
|
||||
To keep it as a `lxml.etree` object, omit the `selector_type` argument:
|
||||
```python
|
||||
>>> page.relocate(element_dict)
|
||||
[<Element a at 0x105a2a7b0>]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Matches Found
|
||||
```python
|
||||
# 1. Check if data was saved
|
||||
element_data = page.retrieve('identifier')
|
||||
if not element_data:
|
||||
print("No data saved for this identifier")
|
||||
|
||||
# 2. Try with different identifier
|
||||
products = page.css('.product', adaptive=True, identifier='old_selector')
|
||||
|
||||
# 3. Save again with new identifier
|
||||
products = page.css('.new-product', auto_save=True, identifier='new_identifier')
|
||||
```
|
||||
|
||||
### Wrong Elements Matched
|
||||
```python
|
||||
# Use more specific selectors
|
||||
products = page.css('.product-list .product', auto_save=True)
|
||||
|
||||
# Or save with more context
|
||||
product = page.find_by_text('Product Name').parent
|
||||
page.save(product, 'specific_product')
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
In the `adaptive` save process, only the unique properties of the first element in the selection results are saved. So if the selector you are using selects different elements on the page in other locations, `adaptive` will return the first element to you only when you relocate it later. This doesn't include combined CSS selectors (Using commas to combine more than one selector, for example), as these selectors are separated and each is executed alone.
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
# Parsing main classes
|
||||
|
||||
The [Selector](#selector) class is the core parsing engine in Scrapling, providing HTML parsing and element selection capabilities. You can always import it with any of the following imports
|
||||
```python
|
||||
from scrapling import Selector
|
||||
from scrapling.parser import Selector
|
||||
```
|
||||
Usage:
|
||||
```python
|
||||
page = Selector(
|
||||
'<html>...</html>',
|
||||
url='https://example.com'
|
||||
)
|
||||
|
||||
# Then select elements as you like
|
||||
elements = page.css('.product')
|
||||
```
|
||||
In Scrapling, the main object you deal with after passing an HTML source or fetching a website is, of course, a [Selector](#selector) object. Any operation you do, like selection, navigation, etc., will return either a [Selector](#selector) object or a [Selectors](#selectors) object, given that the result is element/elements from the page, not text or similar.
|
||||
|
||||
The main page is a [Selector](#selector) object, and the elements within are [Selector](#selector) objects. Any text (text content inside elements or attribute values) is a [TextHandler](#texthandler) object, and element attributes are stored as [AttributesHandler](#attributeshandler).
|
||||
|
||||
## Selector
|
||||
### Arguments explained
|
||||
The most important one is `content`, it's used to pass the HTML code you want to parse, and it accepts the HTML content as `str` or `bytes`.
|
||||
|
||||
The arguments `url`, `adaptive`, `storage`, and `storage_args` are settings used with the `adaptive` feature. They are explained in the [adaptive](adaptive.md) feature page.
|
||||
|
||||
Arguments for parsing adjustments:
|
||||
|
||||
- **encoding**: This is the encoding that will be used while parsing the HTML. The default is `UTF-8`.
|
||||
- **keep_comments**: This tells the library whether to keep HTML comments while parsing the page. It's disabled by default because it can cause issues with your scraping in various ways.
|
||||
- **keep_cdata**: Same logic as the HTML comments. [cdata](https://stackoverflow.com/questions/7092236/what-is-cdata-in-html) is removed by default for cleaner HTML.
|
||||
|
||||
The arguments `huge_tree` and `root` are advanced features not covered here.
|
||||
|
||||
Most properties on the main page and its elements are lazily loaded (not initialized until accessed), which contributes to Scrapling's speed.
|
||||
|
||||
### Properties
|
||||
Properties for traversal are separated in the [traversal](#traversal) section below.
|
||||
|
||||
Parsing this HTML page as an example:
|
||||
```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>
|
||||
```
|
||||
Load the page directly as shown before:
|
||||
```python
|
||||
from scrapling import Selector
|
||||
page = Selector(html_doc)
|
||||
```
|
||||
Get all text content on the page recursively
|
||||
```python
|
||||
>>> 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 the first article (used as an example throughout):
|
||||
```python
|
||||
article = page.find('article')
|
||||
```
|
||||
With the same logic, get all text content on the element recursively
|
||||
```python
|
||||
>>> article.get_all_text()
|
||||
'Product 1\nThis is product 1\n$10.99\nIn stock: 5'
|
||||
```
|
||||
But if you try to get the direct text content, it will be empty because it doesn't have direct text in the HTML code above
|
||||
```python
|
||||
>>> article.text
|
||||
''
|
||||
```
|
||||
The `get_all_text` method has the following optional arguments:
|
||||
|
||||
1. **separator**: All strings collected will be concatenated using this separator. The default is '\n'.
|
||||
2. **strip**: If enabled, strings will be stripped before concatenation. Disabled by default.
|
||||
3. **ignore_tags**: A tuple of all tag names you want to ignore in the final results and ignore any elements nested within them. The default is `('script', 'style',)`.
|
||||
4. **valid_values**: If enabled, the method will only collect elements with real values, so all elements with empty text content or only whitespaces will be ignored. It's enabled by default
|
||||
|
||||
The text returned is a [TextHandler](#texthandler), not a standard string. If the text content can be serialized to JSON, use `.json()` on it:
|
||||
```python
|
||||
>>> script = page.find('script')
|
||||
>>> script.json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
Let's continue to get the element tag
|
||||
```python
|
||||
>>> article.tag
|
||||
'article'
|
||||
```
|
||||
Using it on the page directly operates on the root `html` element:
|
||||
```python
|
||||
>>> page.tag
|
||||
'html'
|
||||
```
|
||||
Getting the attributes of the element
|
||||
```python
|
||||
>>> print(article.attrib)
|
||||
{'class': 'product', 'data-id': '1'}
|
||||
```
|
||||
Access a specific attribute with any of the following
|
||||
```python
|
||||
>>> article.attrib['class']
|
||||
>>> article.attrib.get('class')
|
||||
>>> article['class'] # new in v0.3
|
||||
```
|
||||
Check if the attributes contain a specific attribute with any of the methods below
|
||||
```python
|
||||
>>> 'class' in article.attrib
|
||||
>>> 'class' in article # new in v0.3
|
||||
```
|
||||
Get the HTML content of the element
|
||||
```python
|
||||
>>> 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>'
|
||||
```
|
||||
Get the prettified version of the element's HTML content
|
||||
```python
|
||||
print(article.prettify())
|
||||
```
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
Use the `.body` property to get the raw content of the page. Starting from v0.4, when used on a `Response` object from fetchers, `.body` always returns `bytes`.
|
||||
```python
|
||||
>>> page.body
|
||||
'<html>\n <head>\n <title>Some page</title>\n </head>\n ...'
|
||||
```
|
||||
To get all the ancestors in the DOM tree of this element
|
||||
```python
|
||||
>>> 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...'>]
|
||||
```
|
||||
Generate a CSS shortened selector if possible, or generate the full selector
|
||||
```python
|
||||
>>> article.generate_css_selector
|
||||
'body > div > article'
|
||||
>>> article.generate_full_css_selector
|
||||
'body > div > article'
|
||||
```
|
||||
Same case with XPath
|
||||
```python
|
||||
>>> article.generate_xpath_selector
|
||||
"//body/div/article"
|
||||
>>> article.generate_full_xpath_selector
|
||||
"//body/div/article"
|
||||
```
|
||||
|
||||
### Traversal
|
||||
Properties and methods for navigating elements on the page.
|
||||
|
||||
The `html` element is the root of the website's tree. Elements like `head` and `body` are "children" of `html`, and `html` is their "parent". The element `body` is a "sibling" of `head` and vice versa.
|
||||
|
||||
Accessing the parent of an element
|
||||
```python
|
||||
>>> article.parent
|
||||
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
|
||||
>>> article.parent.tag
|
||||
'div'
|
||||
```
|
||||
Chaining is supported, as with all similar properties/methods:
|
||||
```python
|
||||
>>> article.parent.parent.tag
|
||||
'body'
|
||||
```
|
||||
Get the children of an element
|
||||
```python
|
||||
>>> 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...'>]
|
||||
```
|
||||
Get all elements underneath an element. It acts as a nested version of the `children` property
|
||||
```python
|
||||
>>> 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...'>]
|
||||
```
|
||||
This element returns the same result as the `children` property because its children don't have children.
|
||||
|
||||
Another example of using the element with the `product-list` class will clear the difference between the `children` property and the `below_elements` property
|
||||
```python
|
||||
>>> 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...'>,
|
||||
...]
|
||||
```
|
||||
Get the siblings of an element
|
||||
```python
|
||||
>>> 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...'>]
|
||||
```
|
||||
Get the next element of the current element
|
||||
```python
|
||||
>>> article.next
|
||||
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>
|
||||
```
|
||||
The same logic applies to the `previous` property
|
||||
```python
|
||||
>>> 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...'>
|
||||
```
|
||||
Check if an element has a specific class name:
|
||||
```python
|
||||
>>> article.has_class('product')
|
||||
True
|
||||
```
|
||||
Iterate over the entire ancestors' tree of any element:
|
||||
```python
|
||||
for ancestor in article.iterancestors():
|
||||
# do something with it...
|
||||
```
|
||||
Search for a specific ancestor that satisfies a search function. Pass a function that takes a [Selector](#selector) object as an argument and returns `True`/`False`:
|
||||
```python
|
||||
>>> 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
|
||||
The class `Selectors` is the "List" version of the [Selector](#selector) class. It inherits from the Python standard `List` type, so it shares all `List` properties and methods while adding more methods to make the operations you want to execute on the [Selector](#selector) instances within more straightforward.
|
||||
|
||||
In the [Selector](#selector) class, all methods/properties that should return a group of elements return them as a [Selectors](#selectors) class instance.
|
||||
|
||||
Starting with v0.4, all selection methods consistently return [Selector](#selector)/[Selectors](#selectors) objects, even for text nodes and attribute values. Text nodes (selected via `::text`, `/text()`, `::attr()`, `/@attr`) are wrapped in [Selector](#selector) objects. These text node selectors have `tag` set to `"#text"`, and their `text` property returns the text value. You can still access the text value directly, and all other properties return empty/default values gracefully.
|
||||
|
||||
```python
|
||||
>>> 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
|
||||
```
|
||||
|
||||
### Data extraction methods
|
||||
Starting with v0.4, [Selector](#selector) and [Selectors](#selectors) both provide `get()`, `getall()`, and their aliases `extract_first` and `extract` (following Scrapy conventions). The old `get_all()` method has been removed.
|
||||
|
||||
**On a [Selector](#selector) object:**
|
||||
|
||||
- `get()` returns a `TextHandler` — for text node selectors, it returns the text value; for HTML element selectors, it returns the serialized outer HTML.
|
||||
- `getall()` returns a `TextHandlers` list containing the single serialized string.
|
||||
- `extract_first` is an alias for `get()`, and `extract` is an alias for `getall()`.
|
||||
|
||||
```python
|
||||
>>> 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'
|
||||
```
|
||||
|
||||
**On a [Selectors](#selectors) object:**
|
||||
|
||||
- `get(default=None)` returns the serialized string of the **first** element, or `default` if the list is empty.
|
||||
- `getall()` serializes **all** elements and returns a `TextHandlers` list.
|
||||
- `extract_first` is an alias for `get()`, and `extract` is an alias for `getall()`.
|
||||
|
||||
```python
|
||||
>>> 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'
|
||||
```
|
||||
|
||||
These methods work seamlessly with all selection types (CSS, XPath, `find`, etc.) and are the recommended way to extract text and attribute values in a Scrapy-compatible style.
|
||||
|
||||
### Properties
|
||||
Apart from the standard operations on Python lists (iteration, slicing, etc.), the following operations are available:
|
||||
|
||||
CSS and XPath selectors can be executed directly on the [Selector](#selector) instances, with the same return types as [Selector](#selector)'s `css` and `xpath` methods. The arguments are similar, except the `adaptive` argument is not available. This makes chaining methods straightforward:
|
||||
```python
|
||||
>>> page.css('.product_pod a')
|
||||
[<data='<a href="catalogue/a-light-in-the-attic_...' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/a-light-in-the-attic_...' parent='<h3><a href="catalogue/a-light-in-the-at...'>,
|
||||
<data='<a href="catalogue/tipping-the-velvet_99...' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>,
|
||||
<data='<a href="catalogue/soumission_998/index....' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/soumission_998/index....' parent='<h3><a href="catalogue/soumission_998/in...'>,
|
||||
...]
|
||||
|
||||
>>> page.css('.product_pod').css('a') # Returns the same result
|
||||
[<data='<a href="catalogue/a-light-in-the-attic_...' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/a-light-in-the-attic_...' parent='<h3><a href="catalogue/a-light-in-the-at...'>,
|
||||
<data='<a href="catalogue/tipping-the-velvet_99...' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>,
|
||||
<data='<a href="catalogue/soumission_998/index....' parent='<div class="image_container"> <a href="c...'>,
|
||||
<data='<a href="catalogue/soumission_998/index....' parent='<h3><a href="catalogue/soumission_998/in...'>,
|
||||
...]
|
||||
```
|
||||
The `re` and `re_first` methods can be run directly. They take the same arguments as the [Selector](#selector) class. In this class, `re_first` runs `re` on each [Selector](#selector) within and returns the first one with a result. The `re` method returns a [TextHandlers](#texthandlers) object combining all matches:
|
||||
```python
|
||||
>>> page.css('.price_color').re(r'[\d\.]+')
|
||||
['51.77',
|
||||
'53.74',
|
||||
'50.10',
|
||||
'47.82',
|
||||
'54.23',
|
||||
...]
|
||||
|
||||
>>> page.css('.product_pod h3 a::attr(href)').re(r'catalogue/(.*)/index.html')
|
||||
['a-light-in-the-attic_1000',
|
||||
'tipping-the-velvet_999',
|
||||
'soumission_998',
|
||||
'sharp-objects_997',
|
||||
...]
|
||||
```
|
||||
The `search` method searches the available [Selector](#selector) instances. The function passed must accept a [Selector](#selector) instance as the first argument and return True/False. Returns the first matching [Selector](#selector) instance, or `None`:
|
||||
```python
|
||||
# Find all the products with price '53.23'.
|
||||
>>> search_function = lambda p: float(p.css('.price_color').re_first(r'[\d\.]+')) == 54.23
|
||||
>>> page.css('.product_pod').search(search_function)
|
||||
<data='<article class="product_pod"><div class=...' parent='<li class="col-xs-6 col-sm-4 col-md-3 co...'>
|
||||
```
|
||||
The `filter` method takes a function like `search` but returns a `Selectors` instance of all matching [Selector](#selector) instances:
|
||||
```python
|
||||
# Find all products with prices over $50
|
||||
>>> filtering_function = lambda p: float(p.css('.price_color').re_first(r'[\d\.]+')) > 50
|
||||
>>> page.css('.product_pod').filter(filtering_function)
|
||||
[<data='<article class="product_pod"><div class=...' parent='<li class="col-xs-6 col-sm-4 col-md-3 co...'>,
|
||||
<data='<article class="product_pod"><div class=...' parent='<li class="col-xs-6 col-sm-4 col-md-3 co...'>,
|
||||
<data='<article class="product_pod"><div class=...' parent='<li class="col-xs-6 col-sm-4 col-md-3 co...'>,
|
||||
...]
|
||||
```
|
||||
Safe access to the first or last element without index errors:
|
||||
```python
|
||||
>>> 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
|
||||
```
|
||||
|
||||
Get the number of [Selector](#selector) instances in a [Selectors](#selectors) instance:
|
||||
```python
|
||||
page.css('.product_pod').length
|
||||
```
|
||||
which is equivalent to
|
||||
```python
|
||||
len(page.css('.product_pod'))
|
||||
```
|
||||
|
||||
## TextHandler
|
||||
All methods/properties that return a string return `TextHandler`, and those that return a list of strings return [TextHandlers](#texthandlers) instead.
|
||||
|
||||
TextHandler is a subclass of the standard Python string, so all standard string operations are supported.
|
||||
|
||||
TextHandler provides extra methods and properties beyond standard Python strings. All methods and properties in all classes that return string(s) return TextHandler, enabling chaining and cleaner code. It can also be imported directly and used on any string.
|
||||
### Usage
|
||||
All operations (slicing, indexing, etc.) and methods (`split`, `replace`, `strip`, etc.) return a `TextHandler`, so they can be chained.
|
||||
|
||||
The `re` and `re_first` methods exist in [Selector](#selector), [Selectors](#selectors), and [TextHandlers](#texthandlers) as well, accepting the same arguments.
|
||||
|
||||
- The `re` method takes a string/compiled regex pattern as the first argument. It searches the data for all strings matching the regex and returns them as a [TextHandlers](#texthandlers) instance. The `re_first` method takes the same arguments but returns only the first result as a `TextHandler` instance.
|
||||
|
||||
Also, it takes other helpful arguments, which are:
|
||||
|
||||
- **replace_entities**: This is enabled by default. It replaces character entity references with their corresponding characters.
|
||||
- **clean_match**: It's disabled by default. This causes the method to ignore all whitespace, including consecutive spaces, while matching.
|
||||
- **case_sensitive**: It's enabled by default. As the name implies, disabling it causes the regex to ignore letter case during compilation.
|
||||
|
||||
The return result is [TextHandlers](#texthandlers) because the `re` method is used:
|
||||
```python
|
||||
>>> page.css('.price_color').re(r'[\d\.]+')
|
||||
['51.77',
|
||||
'53.74',
|
||||
'50.10',
|
||||
'47.82',
|
||||
'54.23',
|
||||
...]
|
||||
|
||||
>>> page.css('.product_pod h3 a::attr(href)').re(r'catalogue/(.*)/index.html')
|
||||
['a-light-in-the-attic_1000',
|
||||
'tipping-the-velvet_999',
|
||||
'soumission_998',
|
||||
'sharp-objects_997',
|
||||
...]
|
||||
```
|
||||
Examples with custom strings demonstrating the other arguments:
|
||||
```python
|
||||
>>> 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']
|
||||
```
|
||||
Since `html_content` returns `TextHandler`, regex can be applied directly on HTML content:
|
||||
```python
|
||||
>>> page.html_content.re('div class=".*">(.*)</div')
|
||||
['In stock: 5', 'In stock: 3', 'Out of stock']
|
||||
```
|
||||
|
||||
- The `.json()` method converts the content to a JSON object if possible; otherwise, it throws an error:
|
||||
```python
|
||||
>>> 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}
|
||||
```
|
||||
If no text node is specified while selecting an element, the text content is selected automatically:
|
||||
```python
|
||||
>>> page.css('#page-data')[0].json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
The [Selector](#selector) class adds additional behavior. Given this page:
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<script id="page-data" type="application/json">
|
||||
{
|
||||
"lastUpdated": "2024-09-22T10:30:00Z",
|
||||
"totalProducts": 3
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
The [Selector](#selector) class has the `get_all_text` method, which returns a `TextHandler`. For example:
|
||||
```python
|
||||
>>> page.css('div::text').get().json()
|
||||
```
|
||||
This throws an error because the `div` tag has no direct text content. The `get_all_text` method handles this case:
|
||||
```python
|
||||
>>> page.css('div')[0].get_all_text(ignore_tags=[]).json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
The `ignore_tags` argument is used here because its default value is `('script', 'style',)`.
|
||||
|
||||
When dealing with a JSON response:
|
||||
```python
|
||||
>>> page = Selector("""{"some_key": "some_value"}""")
|
||||
```
|
||||
The [Selector](#selector) class is optimized for HTML, so it treats this as a broken HTML response and wraps it. The `html_content` property shows:
|
||||
```python
|
||||
>>> page.html_content
|
||||
'<html><body><p>{"some_key": "some_value"}</p></body></html>'
|
||||
```
|
||||
The `json` method can be used directly:
|
||||
```python
|
||||
>>> page.json()
|
||||
{'some_key': 'some_value'}
|
||||
```
|
||||
For JSON responses, the [Selector](#selector) class keeps a raw copy of the content it receives. When `.json()` is called, it checks for that raw copy first and converts it to JSON. If the raw copy is unavailable (as with sub-elements), it checks the current element's text content, then falls back to `get_all_text`.
|
||||
|
||||
- The `.clean()` method removes all whitespace and consecutive spaces, returning a new `TextHandler` instance:
|
||||
```python
|
||||
>>> TextHandler('\n wonderful idea, \reh?').clean()
|
||||
'wonderful idea, eh?'
|
||||
```
|
||||
The `remove_entities` argument causes `clean` to replace HTML entities with their corresponding characters.
|
||||
|
||||
- The `.sort()` method sorts the string characters:
|
||||
```python
|
||||
>>> TextHandler('acb').sort()
|
||||
'abc'
|
||||
```
|
||||
Or do it in reverse:
|
||||
```python
|
||||
>>> TextHandler('acb').sort(reverse=True)
|
||||
'cba'
|
||||
```
|
||||
|
||||
This class is returned in place of strings nearly everywhere in the library.
|
||||
|
||||
## TextHandlers
|
||||
This class inherits from standard lists, adding `re` and `re_first` as new methods.
|
||||
|
||||
The `re_first` method runs `re` on each [TextHandler](#texthandler) and returns the first result, or `None`.
|
||||
|
||||
## AttributesHandler
|
||||
This is a read-only version of Python's standard dictionary, or `dict`, used solely to store the attributes of each element/[Selector](#selector) instance.
|
||||
```python
|
||||
>>> print(page.find('script').attrib)
|
||||
{'id': 'page-data', 'type': 'application/json'}
|
||||
>>> type(page.find('script').attrib).__name__
|
||||
'AttributesHandler'
|
||||
```
|
||||
Because it's read-only, it will use fewer resources than the standard dictionary. Still, it has the same dictionary method and properties, except those that allow you to modify/override the data.
|
||||
|
||||
It currently adds two extra simple methods:
|
||||
|
||||
- The `search_values` method
|
||||
|
||||
Searches the current attributes by values (rather than keys) and returns a dictionary of each matching item.
|
||||
|
||||
A simple example would be
|
||||
```python
|
||||
>>> for i in page.find('script').attrib.search_values('page-data'):
|
||||
print(i)
|
||||
{'id': 'page-data'}
|
||||
```
|
||||
But this method provides the `partial` argument as well, which allows you to search by part of the value:
|
||||
```python
|
||||
>>> for i in page.find('script').attrib.search_values('page', partial=True):
|
||||
print(i)
|
||||
{'id': 'page-data'}
|
||||
```
|
||||
A more practical example is using it with `find_all` to find all elements that have a specific value in their attributes:
|
||||
```python
|
||||
>>> 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...'>]
|
||||
```
|
||||
All these elements have 'product' as the value for the `class` attribute.
|
||||
|
||||
The `list` function is used here because `search_values` returns a generator, so it would be `True` for all elements.
|
||||
|
||||
- The `json_string` property
|
||||
|
||||
This property converts current attributes to a JSON string if the attributes are JSON serializable; otherwise, it throws an error.
|
||||
|
||||
```python
|
||||
>>>page.find('script').attrib.json_string
|
||||
b'{"id":"page-data","type":"application/json"}'
|
||||
```
|
||||
@@ -0,0 +1,494 @@
|
||||
# Querying elements
|
||||
Scrapling currently supports parsing HTML pages exclusively (no XML feeds), because the adaptive feature does not work with XML.
|
||||
|
||||
In Scrapling, there are five main ways to find elements:
|
||||
|
||||
1. CSS3 Selectors
|
||||
2. XPath Selectors
|
||||
3. Finding elements based on filters/conditions.
|
||||
4. Finding elements whose content contains a specific text
|
||||
5. Finding elements whose content matches a specific regex
|
||||
|
||||
There are also other indirect ways to find elements. Scrapling can also find elements similar to a given element; see [Finding Similar Elements](#finding-similar-elements).
|
||||
|
||||
## CSS/XPath selectors
|
||||
|
||||
### What are CSS selectors?
|
||||
[CSS](https://en.wikipedia.org/wiki/CSS) is a language for applying styles to HTML documents. It defines selectors to associate those styles with specific HTML elements.
|
||||
|
||||
Scrapling implements CSS3 selectors as described in the [W3C specification](http://www.w3.org/TR/2011/REC-css3-selectors-20110929/). CSS selectors support comes from `cssselect`, so it's better to read about which [selectors are supported from cssselect](https://cssselect.readthedocs.io/en/latest/#supported-selectors) and pseudo-functions/elements.
|
||||
|
||||
Also, Scrapling implements some non-standard pseudo-elements like:
|
||||
|
||||
* To select text nodes, use ``::text``.
|
||||
* To select attribute values, use ``::attr(name)`` where name is the name of the attribute that you want the value of
|
||||
|
||||
The selector logic follows the same conventions as Scrapy/Parsel.
|
||||
|
||||
To select elements with CSS selectors, use the `css` method, which returns `Selectors`. Use `[0]` to get the first element, or `.get()` / `.getall()` to extract text values from text/attribute pseudo-selectors.
|
||||
|
||||
### What are XPath selectors?
|
||||
[XPath](https://en.wikipedia.org/wiki/XPath) is a language for selecting nodes in XML documents, which can also be used with HTML. This [cheatsheet](https://devhints.io/xpath) is a good resource for learning about [XPath](https://en.wikipedia.org/wiki/XPath). Scrapling adds XPath selectors directly through [lxml](https://lxml.de/).
|
||||
|
||||
The logic follows the same conventions as Scrapy/Parsel. However, Scrapling does not implement the XPath extension function `has-class` as Scrapy/Parsel does. Instead, it provides the `has_class` method on returned elements.
|
||||
|
||||
To select elements with XPath selectors, use the `xpath` method, which follows the same logic as the CSS selectors method above.
|
||||
|
||||
> Note that each method of `css` and `xpath` has additional arguments, but we didn't explain them here, as they are all about the adaptive feature. The adaptive feature will have its own page later to be described in detail.
|
||||
|
||||
### Selectors examples
|
||||
Let's see some shared examples of using CSS and XPath Selectors.
|
||||
|
||||
Select all elements with the class `product`.
|
||||
```python
|
||||
products = page.css('.product')
|
||||
products = page.xpath('//*[@class="product"]')
|
||||
```
|
||||
**Note:** The XPath version won't be accurate if there's another class; it's always better to rely on CSS for selecting by class.
|
||||
|
||||
Select the first element with the class `product`.
|
||||
```python
|
||||
product = page.css('.product')[0]
|
||||
product = page.xpath('//*[@class="product"]')[0]
|
||||
```
|
||||
Get the text of the first element with the `h1` tag name
|
||||
```python
|
||||
title = page.css('h1::text').get()
|
||||
title = page.xpath('//h1//text()').get()
|
||||
```
|
||||
Which is the same as doing
|
||||
```python
|
||||
title = page.css('h1')[0].text
|
||||
title = page.xpath('//h1')[0].text
|
||||
```
|
||||
Get the `href` attribute of the first element with the `a` tag name
|
||||
```python
|
||||
link = page.css('a::attr(href)').get()
|
||||
link = page.xpath('//a/@href').get()
|
||||
```
|
||||
Select the text of the first element with the `h1` tag name, which contains `Phone`, and under an element with class `product`.
|
||||
```python
|
||||
title = page.css('.product h1:contains("Phone")::text').get()
|
||||
title = page.xpath('//*[@class="product"]//h1[contains(text(),"Phone")]/text()').get()
|
||||
```
|
||||
You can nest and chain selectors as you want, given that they return results
|
||||
```python
|
||||
page.css('.product')[0].css('h1:contains("Phone")::text').get()
|
||||
page.xpath('//*[@class="product"]')[0].xpath('//h1[contains(text(),"Phone")]/text()').get()
|
||||
page.xpath('//*[@class="product"]')[0].css('h1:contains("Phone")::text').get()
|
||||
```
|
||||
Another example
|
||||
|
||||
All links that have 'image' in their 'href' attribute
|
||||
```python
|
||||
links = page.css('a[href*="image"]')
|
||||
links = page.xpath('//a[contains(@href, "image")]')
|
||||
for index, link in enumerate(links):
|
||||
link_value = link.attrib['href'] # Cleaner than link.css('::attr(href)').get()
|
||||
link_text = link.text
|
||||
print(f'Link number {index} points to this url {link_value} with text content as "{link_text}"')
|
||||
```
|
||||
|
||||
## Text-content selection
|
||||
Scrapling provides two ways to select elements based on their direct text content:
|
||||
|
||||
1. Elements whose direct text content contains the given text with many options through the `find_by_text` method.
|
||||
2. Elements whose direct text content matches the given regex pattern with many options through the `find_by_regex` method.
|
||||
|
||||
Anything achievable with `find_by_text` can also be done with `find_by_regex`, but both are provided for convenience.
|
||||
|
||||
With `find_by_text`, you pass the text as the first argument; with `find_by_regex`, the regex pattern is the first argument. Both methods share the following arguments:
|
||||
|
||||
* **first_match**: If `True` (the default), the method used will return the first result it finds.
|
||||
* **case_sensitive**: If `True`, the case of the letters will be considered.
|
||||
* **clean_match**: If `True`, all whitespaces and consecutive spaces will be replaced with a single space before matching.
|
||||
|
||||
By default, Scrapling searches for the exact matching of the text/pattern you pass to `find_by_text`, so the text content of the wanted element has to be ONLY the text you input, but that's why it also has one extra argument, which is:
|
||||
|
||||
* **partial**: If enabled, `find_by_text` will return elements that contain the input text. So it's not an exact match anymore
|
||||
|
||||
**Note:** The method `find_by_regex` can accept both regular strings and a compiled regex pattern as its first argument.
|
||||
|
||||
### Finding Similar Elements
|
||||
Scrapling can find elements similar to a given element, inspired by the AutoScraper library but usable with elements found by any method.
|
||||
|
||||
Given an element (e.g., a product found by title), calling `.find_similar()` on it causes Scrapling to:
|
||||
|
||||
1. Find all page elements with the same DOM tree depth as this element.
|
||||
2. All found elements will be checked, and those without the same tag name, parent tag name, and grandparent tag name will be dropped.
|
||||
3. As a final check, Scrapling uses fuzzy matching to drop elements whose attributes don't resemble the original element's attributes. A configurable percentage controls this step (see arguments below).
|
||||
|
||||
Arguments for `find_similar()`:
|
||||
|
||||
* **similarity_threshold**: The percentage for comparing elements' attributes (step 3). Default is 0.2 (tag attributes must be at least 20% similar). Set to 0 to disable this check entirely.
|
||||
* **ignore_attributes**: The attribute names passed will be ignored while matching the attributes in the last step. The default value is `('href', 'src',)` because URLs can change significantly across elements, making them unreliable.
|
||||
* **match_text**: If `True`, the element's text content will be considered when matching (Step 3). Using this argument in typical cases is not recommended, but it depends.
|
||||
|
||||
### Examples
|
||||
Examples of finding elements with raw text, regex, and `find_similar`.
|
||||
```python
|
||||
from scrapling.fetchers import Fetcher
|
||||
page = Fetcher.get('https://books.toscrape.com/index.html')
|
||||
```
|
||||
Find the first element whose text fully matches this text
|
||||
```python
|
||||
>>> page.find_by_text('Tipping the Velvet')
|
||||
<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>
|
||||
```
|
||||
Combining it with `page.urljoin` to return the full URL from the relative `href`.
|
||||
```python
|
||||
>>> page.find_by_text('Tipping the Velvet').attrib['href']
|
||||
'catalogue/tipping-the-velvet_999/index.html'
|
||||
>>> page.urljoin(page.find_by_text('Tipping the Velvet').attrib['href'])
|
||||
'https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html'
|
||||
```
|
||||
Get all matches if there are more (notice it returns a list)
|
||||
```python
|
||||
>>> page.find_by_text('Tipping the Velvet', first_match=False)
|
||||
[<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>]
|
||||
```
|
||||
Get all elements that contain the word `the` (Partial matching)
|
||||
```python
|
||||
>>> results = page.find_by_text('the', partial=True, first_match=False)
|
||||
>>> [i.text for i in results]
|
||||
['A Light in the ...',
|
||||
'Tipping the Velvet',
|
||||
'The Requiem Red',
|
||||
'The Dirty Little Secrets ...',
|
||||
'The Coming Woman: A ...',
|
||||
'The Boys in the ...',
|
||||
'The Black Maria',
|
||||
'Mesaerion: The Best Science ...',
|
||||
"It's Only the Himalayas"]
|
||||
```
|
||||
The search is case-insensitive by default, so those results include `The`, not just the lowercase `the`. To limit to exact case:
|
||||
```python
|
||||
>>> results = page.find_by_text('the', partial=True, first_match=False, case_sensitive=True)
|
||||
>>> [i.text for i in results]
|
||||
['A Light in the ...',
|
||||
'Tipping the Velvet',
|
||||
'The Boys in the ...',
|
||||
"It's Only the Himalayas"]
|
||||
```
|
||||
Get the first element whose text content matches my price regex
|
||||
```python
|
||||
>>> page.find_by_regex(r'£[\d\.]+')
|
||||
<data='<p class="price_color">£51.77</p>' parent='<div class="product_price"> <p class="pr...'>
|
||||
>>> page.find_by_regex(r'£[\d\.]+').text
|
||||
'£51.77'
|
||||
```
|
||||
It's the same if you pass the compiled regex as well; Scrapling will detect the input type and act upon that:
|
||||
```python
|
||||
>>> import re
|
||||
>>> regex = re.compile(r'£[\d\.]+')
|
||||
>>> page.find_by_regex(regex)
|
||||
<data='<p class="price_color">£51.77</p>' parent='<div class="product_price"> <p class="pr...'>
|
||||
>>> page.find_by_regex(regex).text
|
||||
'£51.77'
|
||||
```
|
||||
Get all elements that match the regex
|
||||
```python
|
||||
>>> page.find_by_regex(r'£[\d\.]+', first_match=False)
|
||||
[<data='<p class="price_color">£51.77</p>' parent='<div class="product_price"> <p class="pr...'>,
|
||||
<data='<p class="price_color">£53.74</p>' parent='<div class="product_price"> <p class="pr...'>,
|
||||
<data='<p class="price_color">£50.10</p>' parent='<div class="product_price"> <p class="pr...'>,
|
||||
<data='<p class="price_color">£47.82</p>' parent='<div class="product_price"> <p class="pr...'>,
|
||||
...]
|
||||
```
|
||||
And so on...
|
||||
|
||||
Find all elements similar to the current element in location and attributes. For our case, ignore the 'title' attribute while matching
|
||||
```python
|
||||
>>> element = page.find_by_text('Tipping the Velvet')
|
||||
>>> element.find_similar(ignore_attributes=['title'])
|
||||
[<data='<a href="catalogue/a-light-in-the-attic_...' parent='<h3><a href="catalogue/a-light-in-the-at...'>,
|
||||
<data='<a href="catalogue/soumission_998/index....' parent='<h3><a href="catalogue/soumission_998/in...'>,
|
||||
<data='<a href="catalogue/sharp-objects_997/ind...' parent='<h3><a href="catalogue/sharp-objects_997...'>,
|
||||
...]
|
||||
```
|
||||
The number of elements is 19, not 20, because the current element is not included in the results:
|
||||
```python
|
||||
>>> len(element.find_similar(ignore_attributes=['title']))
|
||||
19
|
||||
```
|
||||
Get the `href` attribute from all similar elements
|
||||
```python
|
||||
>>> [
|
||||
element.attrib['href']
|
||||
for element in element.find_similar(ignore_attributes=['title'])
|
||||
]
|
||||
['catalogue/a-light-in-the-attic_1000/index.html',
|
||||
'catalogue/soumission_998/index.html',
|
||||
'catalogue/sharp-objects_997/index.html',
|
||||
...]
|
||||
```
|
||||
Getting all books' data using that element as a starting point:
|
||||
```python
|
||||
>>> for product in element.parent.parent.find_similar():
|
||||
print({
|
||||
"name": product.css('h3 a::text').get(),
|
||||
"price": product.css('.price_color')[0].re_first(r'[\d\.]+'),
|
||||
"stock": product.css('.availability::text').getall()[-1].clean()
|
||||
})
|
||||
{'name': 'A Light in the ...', 'price': '51.77', 'stock': 'In stock'}
|
||||
{'name': 'Soumission', 'price': '50.10', 'stock': 'In stock'}
|
||||
{'name': 'Sharp Objects', 'price': '47.82', 'stock': 'In stock'}
|
||||
...
|
||||
```
|
||||
### Advanced examples
|
||||
Advanced examples using the `find_similar` method:
|
||||
|
||||
E-commerce Product Extraction
|
||||
```python
|
||||
def extract_product_grid(page):
|
||||
# Find the first product card
|
||||
first_product = page.find_by_text('Add to Cart').find_ancestor(
|
||||
lambda e: e.has_class('product-card')
|
||||
)
|
||||
|
||||
# Find similar product cards
|
||||
products = first_product.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'name': p.css('h3::text').get(),
|
||||
'price': p.css('.price::text').re_first(r'\d+\.\d{2}'),
|
||||
'stock': 'In stock' in p.text,
|
||||
'rating': p.css('.rating')[0].attrib.get('data-rating')
|
||||
}
|
||||
for p in products
|
||||
]
|
||||
```
|
||||
Table Row Extraction
|
||||
```python
|
||||
def extract_table_data(page):
|
||||
# Find the first data row
|
||||
first_row = page.css('table tbody tr')[0]
|
||||
|
||||
# Find similar rows
|
||||
rows = first_row.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'column1': row.css('td:nth-child(1)::text').get(),
|
||||
'column2': row.css('td:nth-child(2)::text').get(),
|
||||
'column3': row.css('td:nth-child(3)::text').get()
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
```
|
||||
Form Field Extraction
|
||||
```python
|
||||
def extract_form_fields(page):
|
||||
# Find first form field container
|
||||
first_field = page.css('input')[0].find_ancestor(
|
||||
lambda e: e.has_class('form-field')
|
||||
)
|
||||
|
||||
# Find similar field containers
|
||||
fields = first_field.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'label': f.css('label::text').get(),
|
||||
'type': f.css('input')[0].attrib.get('type'),
|
||||
'required': 'required' in f.css('input')[0].attrib
|
||||
}
|
||||
for f in fields
|
||||
]
|
||||
```
|
||||
Extracting reviews from a website
|
||||
```python
|
||||
def extract_reviews(page):
|
||||
# Find first review
|
||||
first_review = page.find_by_text('Great product!')
|
||||
review_container = first_review.find_ancestor(
|
||||
lambda e: e.has_class('review')
|
||||
)
|
||||
|
||||
# Find similar reviews
|
||||
all_reviews = review_container.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'text': r.css('.review-text::text').get(),
|
||||
'rating': r.attrib.get('data-rating'),
|
||||
'author': r.css('.reviewer::text').get()
|
||||
}
|
||||
for r in all_reviews
|
||||
]
|
||||
```
|
||||
## Filters-based searching
|
||||
Inspired by BeautifulSoup's `find_all` function, elements can be found using the `find_all` and `find` methods. Both methods accept multiple filters and return all elements on the pages where all filters apply.
|
||||
|
||||
To be more specific:
|
||||
|
||||
* Any string passed is considered a tag name.
|
||||
* Any iterable passed, like List/Tuple/Set, will be considered as an iterable of tag names.
|
||||
* Any dictionary is considered a mapping of HTML element(s), attribute names, and attribute values.
|
||||
* Any regex patterns passed are used to filter elements by content, like the `find_by_regex` method
|
||||
* Any functions passed are used to filter elements
|
||||
* Any keyword argument passed is considered as an HTML element attribute with its value.
|
||||
|
||||
It collects all passed arguments and keywords, and each filter passes its results to the following filter in a waterfall-like filtering system.
|
||||
|
||||
It filters all elements in the current page/element in the following order:
|
||||
|
||||
1. All elements with the passed tag name(s) get collected.
|
||||
2. All elements that match all passed attribute(s) are collected; if a previous filter is used, then previously collected elements are filtered.
|
||||
3. All elements that match all passed regex patterns are collected, or if previous filter(s) are used, then previously collected elements are filtered.
|
||||
4. All elements that fulfill all passed function(s) are collected; if a previous filter(s) is used, then previously collected elements are filtered.
|
||||
|
||||
**Notes:**
|
||||
|
||||
1. The filtering process always starts from the first filter it finds in the filtering order above. If no tag name(s) are passed but attributes are passed, the process starts from step 2, and so on.
|
||||
2. The order in which arguments are passed does not matter. The only order considered is the one explained above.
|
||||
|
||||
### Examples
|
||||
```python
|
||||
>>> from scrapling.fetchers import Fetcher
|
||||
>>> page = Fetcher.get('https://quotes.toscrape.com/')
|
||||
```
|
||||
Find all elements with the tag name `div`.
|
||||
```python
|
||||
>>> page.find_all('div')
|
||||
[<data='<div class="container"> <div class="row...' parent='<body> <div class="container"> <div clas...'>,
|
||||
<data='<div class="row header-box"> <div class=...' parent='<div class="container"> <div class="row...'>,
|
||||
...]
|
||||
```
|
||||
Find all div elements with a class that equals `quote`.
|
||||
```python
|
||||
>>> page.find_all('div', class_='quote')
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
...]
|
||||
```
|
||||
Same as above.
|
||||
```python
|
||||
>>> page.find_all('div', {'class': 'quote'})
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
...]
|
||||
```
|
||||
Find all elements with a class that equals `quote`.
|
||||
```python
|
||||
>>> page.find_all({'class': 'quote'})
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
...]
|
||||
```
|
||||
Find all div elements with a class that equals `quote` and contains the element `.text`, which contains the word 'world' in its content.
|
||||
```python
|
||||
>>> page.find_all('div', {'class': 'quote'}, lambda e: "world" in e.css('.text::text').get())
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>]
|
||||
```
|
||||
Find all elements that have children.
|
||||
```python
|
||||
>>> page.find_all(lambda element: len(element.children) > 0)
|
||||
[<data='<html lang="en"><head><meta charset="UTF...'>,
|
||||
<data='<head><meta charset="UTF-8"><title>Quote...' parent='<html lang="en"><head><meta charset="UTF...'>,
|
||||
<data='<body> <div class="container"> <div clas...' parent='<html lang="en"><head><meta charset="UTF...'>,
|
||||
...]
|
||||
```
|
||||
Find all elements that contain the word 'world' in their content.
|
||||
```python
|
||||
>>> page.find_all(lambda element: "world" in element.text)
|
||||
[<data='<span class="text" itemprop="text">“The...' parent='<div class="quote" itemscope itemtype="h...'>,
|
||||
<data='<a class="tag" href="/tag/world/page/1/"...' parent='<div class="tags"> Tags: <meta class="ke...'>]
|
||||
```
|
||||
Find all span elements that match the given regex
|
||||
```python
|
||||
>>> page.find_all('span', re.compile(r'world'))
|
||||
[<data='<span class="text" itemprop="text">“The...' parent='<div class="quote" itemscope itemtype="h...'>]
|
||||
```
|
||||
Find all div and span elements with class 'quote' (No span elements like that, so only div returned)
|
||||
```python
|
||||
>>> page.find_all(['div', 'span'], {'class': 'quote'})
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
|
||||
...]
|
||||
```
|
||||
Mix things up
|
||||
```python
|
||||
>>> page.find_all({'itemtype':"http://schema.org/CreativeWork"}, 'div').css('.author::text').getall()
|
||||
['Albert Einstein',
|
||||
'J.K. Rowling',
|
||||
...]
|
||||
```
|
||||
A bonus pro tip: Find all elements whose `href` attribute's value ends with the word 'Einstein'.
|
||||
```python
|
||||
>>> page.find_all({'href$': 'Einstein'})
|
||||
[<data='<a href="/author/Albert-Einstein">(about...' parent='<span>by <small class="author" itemprop=...'>,
|
||||
<data='<a href="/author/Albert-Einstein">(about...' parent='<span>by <small class="author" itemprop=...'>,
|
||||
<data='<a href="/author/Albert-Einstein">(about...' parent='<span>by <small class="author" itemprop=...'>]
|
||||
```
|
||||
Another pro tip: Find all elements whose `href` attribute's value has '/author/' in it
|
||||
```python
|
||||
>>> page.find_all({'href*': '/author/'})
|
||||
[<data='<a href="/author/Albert-Einstein">(about...' parent='<span>by <small class="author" itemprop=...'>,
|
||||
<data='<a href="/author/J-K-Rowling">(about)</a...' parent='<span>by <small class="author" itemprop=...'>,
|
||||
<data='<a href="/author/Albert-Einstein">(about...' parent='<span>by <small class="author" itemprop=...'>,
|
||||
...]
|
||||
```
|
||||
And so on...
|
||||
|
||||
## Generating selectors
|
||||
CSS/XPath selectors can be generated for any element, regardless of the method used to find it.
|
||||
|
||||
Generate a short CSS selector for the `url_element` element (if possible, create a short one; otherwise, it's a full selector)
|
||||
```python
|
||||
>>> url_element = page.find({'href*': '/author/'})
|
||||
>>> url_element.generate_css_selector
|
||||
'body > div > div:nth-of-type(2) > div > div > span:nth-of-type(2) > a'
|
||||
```
|
||||
Generate a full CSS selector for the `url_element` element from the start of the page
|
||||
```python
|
||||
>>> url_element.generate_full_css_selector
|
||||
'body > div > div:nth-of-type(2) > div > div > span:nth-of-type(2) > a'
|
||||
```
|
||||
Generate a short XPath selector for the `url_element` element (if possible, create a short one; otherwise, it's a full selector)
|
||||
```python
|
||||
>>> url_element.generate_xpath_selector
|
||||
'//body/div/div[2]/div/div/span[2]/a'
|
||||
```
|
||||
Generate a full XPath selector for the `url_element` element from the start of the page
|
||||
```python
|
||||
>>> url_element.generate_full_xpath_selector
|
||||
'//body/div/div[2]/div/div/span[2]/a'
|
||||
```
|
||||
**Note:** When generating a short selector, Scrapling tries to find a unique element (e.g., one with an `id` attribute) as a stop point. If none exists, the short and full selectors will be identical.
|
||||
|
||||
## Using selectors with regular expressions
|
||||
Similar to `parsel`/`scrapy`, `re` and `re_first` methods are available for extracting data using regular expressions. These methods exist in `Selector`, `Selectors`, `TextHandler`, and `TextHandlers`, so they can be used directly on elements even without selecting a text node. See the [TextHandler](main_classes.md#texthandler) class for details.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
>>> page.css('.price_color')[0].re_first(r'[\d\.]+')
|
||||
'51.77'
|
||||
|
||||
>>> page.css('.price_color').re_first(r'[\d\.]+')
|
||||
'51.77'
|
||||
|
||||
>>> page.css('.price_color').re(r'[\d\.]+')
|
||||
['51.77',
|
||||
'53.74',
|
||||
'50.10',
|
||||
'47.82',
|
||||
'54.23',
|
||||
...]
|
||||
|
||||
>>> page.css('.product_pod h3 a::attr(href)').re(r'catalogue/(.*)/index.html')
|
||||
['a-light-in-the-attic_1000',
|
||||
'tipping-the-velvet_999',
|
||||
'soumission_998',
|
||||
'sharp-objects_997',
|
||||
...]
|
||||
|
||||
>>> filtering_function = lambda e: e.parent.tag == 'h3' and e.parent.parent.has_class('product_pod') # As above selector
|
||||
>>> page.find('a', filtering_function).attrib['href'].re(r'catalogue/(.*)/index.html')
|
||||
['a-light-in-the-attic_1000']
|
||||
|
||||
>>> page.find_by_text('Tipping the Velvet').attrib['href'].re(r'catalogue/(.*)/index.html')
|
||||
['tipping-the-velvet_999']
|
||||
```
|
||||
See the [TextHandler](main_classes.md#texthandler) class for more details on regex methods.
|
||||
Reference in New Issue
Block a user