First version of Scrapling full documentation
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
## Introduction
|
||||
Auto-matching is one of Scrapling's most powerful features. It allows your scraper to survive website changes by intelligently tracking and relocating elements.
|
||||
|
||||
Let's say you are scraping 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>
|
||||
```
|
||||
And you want to scrape the first product, the one with the `p1` ID. You will probably write a selector like this
|
||||
```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 auto-matching feature comes into play.
|
||||
|
||||
With Scrapling, you can enable the `automatch` 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 and without AI :)
|
||||
|
||||
```python
|
||||
from scrapling import Adaptor, Fetcher
|
||||
# Before the change
|
||||
page = Adaptor(page_source, auto_match=True, url='example.com')
|
||||
# or
|
||||
Fetcher.auto_match = 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', auto_match=True) # Scrapling still finds it!
|
||||
# the rest of your code...
|
||||
```
|
||||
Below, I will show you one usage example for this feature. Then, we will dive deep into how to use it and provide details about this feature.
|
||||
|
||||
## Real-World Scenario
|
||||
Let's use a real website as an example and use one of the fetchers to fetch its source. To do this, we need to find a website that will soon change its design/structure, take a copy of its source, and then wait for the website to make the change. Of course, that's nearly impossible to know unless I know the website's owner, but that will make it a staged test, haha.
|
||||
|
||||
To solve this issue, I will use [The Web Archive](https://archive.org/)'s [Wayback Machine](https://web.archive.org/). Here is a copy of [StackOverFlow's website in 2010](https://web.archive.org/web/20100102003420/http://stackoverflow.com/); pretty old, eh?</br>Let's test if the automatch feature can extract the same button in the old design from 2010 and the current design using the same selector :)
|
||||
|
||||
If I want to extract the Questions button from the old design, I can use a selector like this: `#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a` This selector is too specific because it was generated by Google Chrome.
|
||||
|
||||
|
||||
Now, let's test 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(auto_match = True, automatch_domain='stackoverflow.com')
|
||||
>>
|
||||
>> page = Fetcher.get(old_url, timeout=30)
|
||||
>> element1 = page.css_first(selector, auto_save=True)
|
||||
>>
|
||||
>> # Same selector but used in the updated website
|
||||
>> page = Fetcher.get(new_url)
|
||||
>> element2 = page.css_first(selector, auto_match=True)
|
||||
>>
|
||||
>> 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!'
|
||||
```
|
||||
Note that I used a new argument called `automatch_domain`; this is because, for Scrapling, these are two different domains(`archive.org` and `stackoverflow.com`), so scrapling will isolate their `auto_match` data. To tell Scrapling they are the same website, we need to pass the custom domain we want to use while saving auto-match data for them both so Scrapling doesn't isolate them.
|
||||
|
||||
The code will be the same in a real-world scenario, except it will use the same URL for both requests, so you won't need to use the `automatch_domain` argument. This is the closest example I can give to real-world cases, so I hope it didn't confuse you :)
|
||||
|
||||
Hence, in the two examples above, I used both the `Adaptor` class and the `Fetcher` class to show you that the logic for automatch is the same.
|
||||
|
||||
## How the automatch feature works
|
||||
Auto-matching works in two phases:
|
||||
|
||||
1. **Save Phase**: Store unique properties of elements
|
||||
2. **Match Phase**: Find elements with similar properties later
|
||||
|
||||
Let's say you have an element you got through selection or any method and want the library to find it the next time you scrape this website, even if it had structural/design changes.
|
||||
|
||||
As little technical details as possible, the general logic goes as the following:
|
||||
|
||||
1. You tell Scrapling to save that element's unique properties in one of the ways we will show below.
|
||||
2. Scrapling uses its configured database (SQLite by default) and saves each element's unique properties.
|
||||
3. Now, because everything about the element can be changed or removed from the website's owner(s), nothing from the element can be used as a unique identifier for the database. To solve this issue, I made the storage system rely on two things:
|
||||
1. The domain of the current website. If you are using the `Adaptor` class, you should pass it while initializing the class, or if you are using one of the fetchers, the domain will be taken from the URL automatically.
|
||||
2. An `identifier` to query that element's properties from the database. You don't always have to set the identifier yourself, as you will see later when we discuss this.
|
||||
|
||||
Together, they will be used to retrieve the element's unique properties from the database later.
|
||||
|
||||
4. Later, when the website structural changes, you tell Scrapling to automatch the element. Scrapling retrieves the element's unique properties and matches all elements on the page against the unique properties we already have for this element. A score is calculated for their similarity to the element we want. In that comparison, everything is taken into consideration, as you will see later
|
||||
5. The element(s) with the highest similarity score to the wanted element are returned.
|
||||
|
||||
### The unique properties
|
||||
You might wonder, if all aspects of an element can be removed or changed, what unique properties we are talking about.
|
||||
|
||||
For Scrapling, the unique elements we are relying 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.
|
||||
|
||||
But you need to understand that the comparison between elements is not exact; it's more about finding how similar these values are. So everything is considered, even the values' order, like the order in which the element class names were written before and the order in which the same element class names are written now.
|
||||
|
||||
## How to use automatch feature
|
||||
The automatch feature can be used on any element you have, and it's added as arguments to CSS/XPath Selection methods, as you saw above, but we will get back to that later.
|
||||
|
||||
First, you must enable the automatch feature by passing `auto_match=True` to the [Adaptor](main_classes.md#adaptor) class when you initialize it or enable it in the fetcher you are using of the available fetchers, as we will show.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
>>> from scrapling import Adaptor, Fetcher
|
||||
>>> page = Adaptor(html_doc, auto_match=True)
|
||||
# OR
|
||||
>>> Fetcher.auto_match = True
|
||||
>>> page = Fetcher.fetch('https://example.com')
|
||||
```
|
||||
If you are using the [Adaptor](main_classes.md#adaptor) class, you need to pass the url of the website you are using with the argument `url` so Scrapling can separate the properties saved for each element by domain.
|
||||
|
||||
If you didn't pass a URL, the word `default` will be used in place of the URL field while saving the element's unique properties. So, this will only be an issue if you used the same identifier later for a different website and didn't pass the URL parameter while initializing it. The save process will overwrite the previous data, and auto-matching only uses the latest saved properties.
|
||||
|
||||
Besides those arguments, we have `storage` and `storage_args`. Both are for the class to be used to connect to the database; by default, it's set to the SQLite class that the library is using. Those arguments shouldn't matter unless you want to write your own storage system, which we will cover on a [separate page in the development section](../development/automatch_storage_system.md).
|
||||
|
||||
Now, after enabling the automatch feature globally, you have two main ways to use it.
|
||||
|
||||
### The CSS/XPath Selection way
|
||||
As you have seen in the example above, first, you have to use the `auto_save` argument while selecting an element that exists on the page like below
|
||||
```python
|
||||
element = page.css('#p1' auto_save=True)
|
||||
```
|
||||
and when the element doesn't exist, you can use the same selector and the `auto_match` argument, and the library will find it for you
|
||||
```python
|
||||
element = page.css('#p1', auto_match=True)
|
||||
```
|
||||
Pretty simple, eh?
|
||||
|
||||
Well, a lot happened under the hood here. Remember the identifier part we mentioned before that you need to set so you can retrieve the element you want? Here, with the `css`/`css_first`/`xpath`/`xpath_first` methods, the identifier is set automatically as the selector you passed here to make things easier :)
|
||||
|
||||
Also, that's why here, for all these methods, you can pass the `identifier` argument to set it yourself, and there are cases for this, or you can use it to save the properties with the `auto_save` argument.
|
||||
|
||||
### The manual way
|
||||
You manually save and retrieve an element, then relocate it, which all happens within the automatch feature, as shown below. This allows you to automatch any element you have by any way or any selection method!
|
||||
|
||||
First, let's say you got an element like this by text:
|
||||
```python
|
||||
>>> element = page.find_by_text('Tipping the Velvet', first_match=True)
|
||||
```
|
||||
You can save its unique properties with the `save` method like below, but you must set the identifier yourself. For this example, I chose `my_special_element` as an identifier, but it's best to use a meaningful identifier in your code for the same reason you use meaningful variable names :)
|
||||
```python
|
||||
>>> page.save(element, 'my_special_element')
|
||||
```
|
||||
Now, later, when you want to retrieve it and relocate it inside the page with auto-matching, it would be like this
|
||||
```python
|
||||
>>> element_dict = page.retrieve('my_special_element')
|
||||
>>> page.relocate(element_dict, adaptor_type=True)
|
||||
[<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>]
|
||||
>>> page.relocate(element_dict, adaptor_type=True).css('::text')
|
||||
['Tipping the Velvet']
|
||||
```
|
||||
Hence, the `retrieve` and relocate` methods are used.
|
||||
|
||||
if you want to keep it as `lxml.etree` object, leave the `adaptor_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', auto_match=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 auto-matching save process, the unique properties of the first element from the selection results are the only ones that get saved. So if the selector you are using selects different elements on the page in different locations, auto-matching 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 get separated, and each selector gets executed alone.
|
||||
|
||||
## Final thoughts
|
||||
Explaining this feature in detail without complications turned out to be challenging, but still, if there's something left unclear, you can head out to the [discussions section](https://github.com/D4Vinci/Scrapling/discussions), and I will reply to you ASAP or reach out to me privately and have a chat :)
|
||||
@@ -0,0 +1,539 @@
|
||||
## Introduction
|
||||
After exploring the various ways to select elements with Scrapling and related features, Let's take a step back and examine the [Adaptor](#adaptor) class generally and other objects to better understand the parsing engine.
|
||||
|
||||
The [Adaptor](#adaptor) class is the core parsing engine in Scrapling that provides HTML parsing and element selection capabilities. You can always import it with any of the following imports
|
||||
```python
|
||||
from scrapling import Adaptor
|
||||
from scrapling.parser import Adaptor
|
||||
```
|
||||
then use it directly as you already learned in the [overview](../overview.md) page
|
||||
```python
|
||||
adaptor = Adaptor(
|
||||
text='<html>...</html>',
|
||||
url='https://example.com'
|
||||
)
|
||||
|
||||
# Then select elements as you like
|
||||
elements = adaptor.css('.product')
|
||||
```
|
||||
In Scrapling, the main object you deal with after passing an HTML source or fetching a website is, of course, an [Adaptor](#adaptor) object. Any operation you do, like selection, navigation, etc., will return either an [Adaptor](#adaptor) object or an [Adaptors](#adaptors) object, given that the result is element/elements from the page, not text or similar.
|
||||
|
||||
In other words, the main page is a [Adaptor](#adaptor) object, and the elements within are [Adaptor](#adaptor) objects, and so on. Any text, such as the text content inside elements or the text inside element attributes, is a [TextHandler](#texthandler) object, and the attributes of each element are stored as [AttributesHandler](#attributeshandler). We will return to both objects later, so let's focus on the [Adaptor](#adaptor) object.
|
||||
|
||||
## Adaptor
|
||||
### Arguments explained
|
||||
The most important ones are `text` and `body`. Both are used to pass the HTML code you want to parse, but the first one accepts `str`, and the latter accepts `bytes` like how you used to do with `parsel` :)
|
||||
|
||||
Otherwise, you have the arguments `url`, `auto_match`, `storage`, and `storage_args`. All these arguments are settings used with the `auto_match` feature, and they don't make a difference if you are not going to use that feature, so just ignore them for now, and we will explain them in the [automatch](automatch.md) feature page.
|
||||
|
||||
Then you have the arguments for adjustments for parsing or adjusting/manipulating the HTML while the library parsing it:
|
||||
|
||||
- **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, as it can mess up your scraping in many 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. This also means when you check for the raw html content, you will find it doesn't have the cdata.
|
||||
|
||||
I have intended to ignore the arguments `huge_tree` and `root` to avoid making this page more complicated than needed.
|
||||
You may notice that I'm doing that a lot, and that's because it's something you don't need to know to use the library. The development section will cover these missing parts if you are that interested.
|
||||
|
||||
After that, for the main page and elements within, most properties don't get initialized until you use it like the text content of a page/element, and this is one of the reasons for Scrapling speed :)
|
||||
|
||||
### Properties
|
||||
You have already seen much of this on the [overview](../overview.md) page, but don't worry if you didn't. We will review it more thoroughly using more advanced methods/usages. For clarity, the properties for traversal are separated below in the [traversal](#traversal) section.
|
||||
|
||||
Let's say we are parsing this HTML page for simplicity:
|
||||
```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 Adaptor
|
||||
page = Adaptor(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 as explained before; we will use it as an example
|
||||
```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; notice the logic difference
|
||||
```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. 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
|
||||
|
||||
By the way, the text returned here is not a standard string but a [TextHandler](#texthandler); we will get to this in detail later, so if the text content can be serialized to JSON, then 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'
|
||||
```
|
||||
If you used it on the page directly, you will find you are operating on the root `html` element
|
||||
```python
|
||||
>>> page.tag
|
||||
'html'
|
||||
```
|
||||
Now, I think I hammered the (`page`/`element`) idea, so I won't return to it again.
|
||||
|
||||
Getting the attributes of the element
|
||||
```python
|
||||
>>> print(article.attrib)
|
||||
{'class': 'product', 'data-id': '1'}
|
||||
```
|
||||
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>'
|
||||
```
|
||||
It's the same if you used the `.body` property
|
||||
```python
|
||||
>>> article.body
|
||||
'<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 HTML content of the element
|
||||
```python
|
||||
>>> print(article.prettify())
|
||||
<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>
|
||||
```
|
||||
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
|
||||
Using the elements we found above, we will go over the properties/methods for moving in the page in detail.
|
||||
|
||||
If you are unfamiliar with the DOM tree or the tree data structure in general, the following traversal part can be confusing. I recommend you look up these concepts online for a better understanding.
|
||||
|
||||
If you are too lazy to search about it, here's a quick explanation to give you a good idea.<br/>
|
||||
Simply put, the `html` element is the root of the website's tree, as every page starts with an `html` element.<br/>
|
||||
This element will be directly above elements like `head` and `body`. These are considered "children" of the `html` element, and the `html` element is considered their "parent." The element `body` is a "sibling" of the element `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'
|
||||
```
|
||||
You can chain it as you want, which applies to all similar properties/methods we will review.
|
||||
```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_first('.product-list')
|
||||
>>> 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 # gets the next element, the same logic applies to `quote.previous`
|
||||
<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_first('.product[data-id="2"]')
|
||||
>>> second_article.previous
|
||||
<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>
|
||||
```
|
||||
You can check easily and pretty fast if an element has a specific class name or not
|
||||
```python
|
||||
>>> article.has_class('product')
|
||||
True
|
||||
```
|
||||
If your case needs more than the element's parent, you can iterate over the whole ancestors' tree of any element, like the example below
|
||||
```python
|
||||
for ancestor in article.iterancestors():
|
||||
# do something with it...
|
||||
```
|
||||
You can search for a specific ancestor of an element that satisfies a function; all you need to do is to pass a function that takes an [Adaptor](#adaptor) object as an argument and return `True` if the condition satisfies or `False` otherwise like below:
|
||||
```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...'>
|
||||
```
|
||||
## Adaptors
|
||||
The class `Adaptors` is the "List" version of the [Adaptor](#adaptor) 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 [Adaptor](#adaptor) instances within more straightforward.
|
||||
|
||||
In the [Adaptor](#adaptor) class, all methods/properties that should return a group of elements return them as an [Adaptors](#adaptors) class instance. The only exceptions are when you use the CSS/XPath methods as follows:
|
||||
|
||||
- If you selected a text node with the selector, then the return type will be [TextHandler](#texthandler)/[TextHandlers](#texthandlers). <br/>Examples:
|
||||
```python
|
||||
>>> page.css('a::text') # -> TextHandlers
|
||||
>>> page.xpath('//a/text()') # -> TextHandlers
|
||||
>>> page.css_first('a::text') # -> TextHandler
|
||||
>>> page.xpath_first('//a/text()') # -> TextHandler
|
||||
>>> page.css('a::attr(href)') # -> TextHandlers
|
||||
>>> page.xpath('//a/@href') # -> TextHandlers
|
||||
>>> page.css_first('a::attr(href)') # -> TextHandler
|
||||
>>> page.xpath_first('//a/@href') # -> TextHandler
|
||||
```
|
||||
- If you used a combined selector that returns mixed types, the result will be a Python standard `List`. <br/>Examples:
|
||||
```python
|
||||
>>> page.css('.price_color') # -> Adaptors
|
||||
>>> page.css('.product_pod a::attr(href)') # -> TextHandlers
|
||||
>>> page.css('.price_color, .product_pod a::attr(href)') # -> List
|
||||
```
|
||||
|
||||
Let's see what [Adaptors](#adaptors) class adds to the table with that out of the way.
|
||||
### Properties
|
||||
Apart from the normal operations on Python lists like iteration, slicing, etc...
|
||||
|
||||
You can do the following:
|
||||
|
||||
Execute CSS and XPath selectors directly on the [Adaptor](#adaptor) instances it has while the arguments and the return types are the same as [Adaptor](#adaptor)'s `css` and `xpath` methods. This, of course, makes chaining methods very 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...'>,
|
||||
...]
|
||||
```
|
||||
Run the `re` and `re_first` methods directly. They take the same arguments passed as the [Adaptor](#adaptor) class. I'm still leaving these methods to be explained in the [TextHandler](#texthandler) section below.
|
||||
|
||||
However, in this class, the `re_first` behaves differently as it runs `re` on each [Adaptor](#adaptor) within and returns the first one with a result. The `re` method will return a [TextHandlers](#texthandlers) object as normal that has all the results combined in one [TextHandlers](#texthandlers) instance.
|
||||
```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',
|
||||
...]
|
||||
```
|
||||
With the `search` method, you can search quickly in the available [Adaptor](#adaptor) classes. The function you pass must accept an [Adaptor](#adaptor) instance as the first argument and return True/False. The method will return the first [Adaptor](#adaptor) instance that satisfies the function; otherwise, it will return `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...'>
|
||||
```
|
||||
You can use the `filter` method, too, which takes a function like the `search` method but returns an `Adaptors` instance of all the [Adaptor](#adaptor) classes that satisfy the function
|
||||
```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...'>,
|
||||
...]
|
||||
```
|
||||
|
||||
## TextHandler
|
||||
This class is mandatory to understand, as all methods/properties that should return a string for you will return `TextHandler`, and the ones that should return a list of strings will return [TextHandlers](#texthandlers) instead.
|
||||
|
||||
TextHandler is a subclass of the standard Python string, so you can do anything with it. So, what is the difference that requires a different naming?
|
||||
|
||||
Of course, TextHandler provides extra methods and properties that the standard Python strings can't do. We will review them now, but remember that all methods and properties in all classes that return string(s) are returning TextHandler, which opens the door for creativity and makes the code shorter and cleaner, as you will see. Also, you can import it directly and use it on any string, which we will explain later.
|
||||
### Usage
|
||||
First, before discussing the added methods, you need to know that all operations on it, like slicing, accessing by index, etc., and methods like `split`, `replace`, `strip`, etc., all return a TextHandler again, so you can chain them as you want. If you find a method or property that returns a standard string instead of TextHandler, please open an issue, and we will override it as well.
|
||||
|
||||
First, we start with the `re` and `re_first` methods. These are the same methods that exist in the rest of the classes ([Adaptor](#adaptor), [Adaptors](#adaptors), and [TextHandlers](#texthandlers)), so they will take the same arguments as well.
|
||||
|
||||
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 and behaves similarly, but as you probably figured out from the naming, it returns the first result only 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 makes the method ignore all whitespaces and consecutive spaces while matching.
|
||||
- **case_sensitive**: It's enabled by default. As the name implies, disabling it will make the regex ignore letters case while compiling it.
|
||||
|
||||
You have seen these examples before; the return result is [TextHandlers](#texthandlers) because we used the `re` method.
|
||||
```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',
|
||||
...]
|
||||
```
|
||||
To explain the other arguments better, we will use a custom string for each example below
|
||||
```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']
|
||||
```
|
||||
Another use of the idea of replacing strings with `TextHandler` everywhere is a property like `html_content` returns `TextHandler` so you can do regex on the HTML content if you want:
|
||||
```python
|
||||
>>> page.html_content.re('div class=".*">(.*)</div')
|
||||
['In stock: 5', 'In stock: 3', 'Out of stock']
|
||||
```
|
||||
|
||||
- You also have the `.json()` method, which tries to convert the content to a json object quickly if possible; otherwise, it throws an error
|
||||
```python
|
||||
>>> page.css_first('#page-data::text')
|
||||
'\n {\n "lastUpdated": "2024-09-22T10:30:00Z",\n "totalProducts": 3\n }\n '
|
||||
>>> page.css_first('#page-data::text').json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
Hence, if you didn't specify a text node while selecting an element (like the text content or an attribute text content), the text content will be selected automatically like this
|
||||
```python
|
||||
>>> page.css_first('#page-data').json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
The [Adaptor](#adaptor) class adds one thing here, too; let's say this is the page we are working with:
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<script id="page-data" type="application/json">
|
||||
{
|
||||
"lastUpdated": "2024-09-22T10:30:00Z",
|
||||
"totalProducts": 3
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
The [Adaptor](#adaptor) class has the `get_all_text` method, which you should be aware of by now. This method returns a `TextHandler`, of course.<br/><br/>
|
||||
So, as you know here, if you did something like this
|
||||
```python
|
||||
>>> page.css_first('div::text').json()
|
||||
```
|
||||
You will get an error because the `div` tag doesn't have direct text content that can be serialized to JSON; it actually doesn't have text content at all.<br/><br/>
|
||||
In this case, the `get_all_text` method comes to the rescue, so you can do something like that
|
||||
```python
|
||||
>>> page.css_first('div').get_all_text(ignore_tags=[]).json()
|
||||
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
|
||||
```
|
||||
I used the `ignore_tags` argument here because the default value of it is `('script', 'style',)`, as you are aware.<br/><br/>
|
||||
Another related behavior you should be aware of is the case while using any of the fetchers, which we will explain later. If you have a JSON response like this example:
|
||||
```python
|
||||
>>> page = Adaptor("""{"some_key": "some_value"}""")
|
||||
```
|
||||
Because the [Adaptor](#adaptor) class is optimized to deal with HTML pages, it will deal with it as a broken HTML response and fix it, so if you used the `html_content` property, you get this
|
||||
```python
|
||||
>>> page.html_content
|
||||
'<html><body><p>{"some_key": "some_value"}</p></body></html>'
|
||||
```
|
||||
Here, you can use `json` method directly, and it will work
|
||||
```python
|
||||
>>> page.json()
|
||||
{'some_key': 'some_value'}
|
||||
```
|
||||
You might wonder how this happened while the `html` tag lacks direct text?<br/>
|
||||
Well, for these cases like JSON responses, I made the `.json()` method inside the [Adaptor](#adaptor) class to check if the current element doesn't have text content; it will use the `get_all_text` method directly.<br/><br/>It might sound hacky a bit but remember, Scrapling is currently optimized to work with HTML pages only so that's the best way till now to handle JSON responses currently without sacrificing speed. This will be changed in the upcoming versions.
|
||||
|
||||
- Another handy method is `.clean()`, this will remove all white spaces and consecutive spaces for you and return a new `TextHandler`, wonderful
|
||||
```python
|
||||
>>> TextHandler('\n wonderful idea, \reh?').clean()
|
||||
'wonderful idea, eh?'
|
||||
```
|
||||
|
||||
- Another method that might be helpful in some cases is the `.sort()` method to sort the string for you as you do with lists
|
||||
```python
|
||||
>>> TextHandler('acb').sort()
|
||||
'abc'
|
||||
```
|
||||
Or do it in reverse:
|
||||
```python
|
||||
>>> TextHandler('acb').sort(reverse=True)
|
||||
'cba'
|
||||
```
|
||||
|
||||
Other methods and properties will be added over time, but remember that this class is returned in place of strings nearly everywhere in the library.
|
||||
|
||||
## TextHandlers
|
||||
You probably guessed it: This class is similar to [Adaptors](#adaptors) and [Adaptor](#adaptor), but here it inherits the same logic and method as standard lists, with only `re` and `re_first` as new methods.
|
||||
|
||||
The only difference is that the `re_first` method logic here does `re` on each [TextHandler](#texthandler) within and returns the first result it has or `None`. Nothing is new to explain here, but new methods will be added here with time.
|
||||
|
||||
## AttributesHandler
|
||||
This is a read-only version of Python's standard dictionary or `dict` that's only used to store the attributes of each element or each [Adaptor](#adaptor) instance, in other words.
|
||||
```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/properties other than those allowing you to modify/override the data.
|
||||
|
||||
It currently adds two extra simple methods:
|
||||
|
||||
- The `search_values` method
|
||||
|
||||
In standard dictionaries, you can do `dict.get("key_name")` to check if a key exists. However, if you want to search by values instead of keys, it will take you some code lines. This method does that for you. It allows you to search the current attributes by values 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'}
|
||||
```
|
||||
These examples won't happen in the real world; most likely, a more real-world example would be using it with the `find_all` method to find all elements that have a specific value in their arguments:
|
||||
```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 a value for the attribute `class`.
|
||||
|
||||
Hence, I used the `list` function 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 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,512 @@
|
||||
## Introduction
|
||||
Scrapling currently supports parsing HTML pages exclusively, so it doesn't support XML feeds. This decision was made because the automatch feature won't work with XML, but that might change soon, so stay tuned :)
|
||||
|
||||
In Scrapling, there are 5 main ways to find elements:
|
||||
|
||||
1. CSS3 Selectors
|
||||
2. XPath Selectors
|
||||
3. Finding elements based on filters/conditions.
|
||||
4. Finding elements whose content contains specific text
|
||||
5. Finding elements whose content matches specific regex
|
||||
|
||||
Of course, there are other indirect ways to find elements with Scrapling, but here we will discuss the main ways in detail. We will also bring up one of the most remarkable features of Scrapling: the ability to find elements that are similar to the element you have; you can jump to that section directly from [here](#finding-similar-elements).
|
||||
|
||||
If you are new to Web Scraping, have little to no experience writing selectors, and want to start quickly, I recommend you jump directly to learning the `find`/`find_all` methods from [here](#filters-based-searching).
|
||||
|
||||
## 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
|
||||
|
||||
In short, if you come from Scrapy/Parsel, you will find the same logic for selectors here to make it easier. No need to implement a stranger logic to the one that most of us are used to :)
|
||||
|
||||
To select elements with CSS selectors, you have the `css` and `css_first` methods. The latter is useful when you are interested in the first element it finds only, or if it's one element, etc., and the first when it's more than one, as it returns `Adaptors`.
|
||||
|
||||
### 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.
|
||||
|
||||
In short, it is the same situation as CSS Selectors; if you come from Scrapy/Parsel, you will find the same logic for selectors here. BUT Scrapling doesn't implement the XPath extension function `has-class` as Scrapy/Parsel—instead, there's the `has_class` method that you can use on elements returned for the same purpose.
|
||||
|
||||
To select elements with XPath selectors, you have the `xpath` and `xpath_first` methods. Again, these methods follow the same logic as the CSS selectors methods above.
|
||||
|
||||
> Note that each method of `css`, `css_first`, `xpath`, and `xpath_first` have additional arguments, but we didn't explain them here as they are all about the automatch feature. The automatch feature will have its 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 one won't be accurate if there's another class; better rely on CSS for selecting by class
|
||||
|
||||
Select the first element with the class `product`
|
||||
```python
|
||||
product = page.css_first('.product')
|
||||
product = page.xpath_first('//*[@class="product"]')
|
||||
```
|
||||
Which would be the same as doing
|
||||
```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_first('h1::text')
|
||||
title = page.xpath_first('//h1//text()')
|
||||
```
|
||||
Which is again the same as doing
|
||||
```python
|
||||
title = page.css_first('h1').text
|
||||
title = page.xpath_first('//h1').text
|
||||
```
|
||||
Get the `href` attribute of the first element with `a` tag name
|
||||
```python
|
||||
link = page.css_first('a::attr(href)')
|
||||
link = page.xpath_first('//a/@href')
|
||||
```
|
||||
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_first('.product h1:contains("Phone")::text')
|
||||
title = page.page.xpath_first('//*[@class="product"]//h1[contains(text(),"Phone")]/text()')
|
||||
```
|
||||
You can nest and chain selectors as you want, given that it returns results
|
||||
```python
|
||||
page.css_first('.product').css_first('h1:contains("Phone")::text')
|
||||
page.xpath_first('//*[@class="product"]').xpath_first('//h1[contains(text(),"Phone")]/text()')
|
||||
page.xpath_first('//*[@class="product"]').css_first('h1:contains("Phone")::text')
|
||||
```
|
||||
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)')
|
||||
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 the ability to select elements based on their direct text content, and you have two ways to do this:
|
||||
|
||||
1. Elements whose direct text content contains 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.
|
||||
|
||||
What you can do with `find_by_text` can be done with `find_by_regex` if you are good enough with regular expressions (regex), but we are providing more options to make them easier for all users to access.
|
||||
|
||||
With `find_by_text`, you will pass the text as the first argument; with the `find_by_regex` method, the regex pattern is the first. 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 ignored while matching.
|
||||
|
||||
By default, Scrapling search for exact matching for the text you pass to `find_by_text`, so the text content of the wanted element have to be ONLY the text you inputted, 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, as you will see in the upcoming examples.
|
||||
|
||||
### Finding Similar Elements
|
||||
One of the most remarkable new features that Scrapling puts on the table is the feature that allows the user to tell Scrapling to find elements similar to the element at hand. This feature inspiration came from the AutoScraper library, but here, it can be used on elements found by any method. Most likely, most of its usage would be after finding elements through text content like how AutoScraper works, so it would also be convenient to explain it here.
|
||||
|
||||
So, how does it work?
|
||||
|
||||
Imagine a scenario where you found a product by its title, for example, and you want to extract other products listed in the same table/container. With the element you have, you can simply call the method `.find_similar()` on it, and Scrapling will:
|
||||
|
||||
1. Find all page elements with the same 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. Now we are sure (like 99% sure) that these elements are the ones we want, but as a last check, Scrapling will use fuzzy matching to drop the elements whose attributes don't look like the attributes of our element. There's a percentage to control this step, and I recommend you not play with it unless the default settings don't get the elements you want.
|
||||
|
||||
That's a lot of talking, I know, but I had to go deep, I will give examples of using this method in the next section, but first, these are the arguments that can be passed to this method:
|
||||
|
||||
* **similarity_threshold**: This is the percentage we discussed in step 3 for comparing elements' attributes. The default value is 0.2. In Simpler words, the attributes' values of both elements should be at least 20% similar. If you want to turn off this check (Step 3, basically), you can set this attribute to 0, but I recommend you read what other arguments do first.
|
||||
* **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 a lot between elements, making them unreliable.
|
||||
* **match_text**: If `True`, the element's text content will be considered when matching. Using this in normal cases is not recommended, but it depends.
|
||||
|
||||
Now, let's check out the examples below.
|
||||
|
||||
### Examples
|
||||
Let's see some shared examples of finding elements with raw text and regex.
|
||||
|
||||
I will use the `Fetcher` to clarify these examples, but it will be explained in detail later.
|
||||
```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 (hence, it returned 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, so those results have `The`, not only the lowercase one `the`; let's limit the search to the elements with `the` only.
|
||||
```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 that its 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...'>,
|
||||
...]
|
||||
```
|
||||
Notice that 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',
|
||||
...]
|
||||
```
|
||||
To increase the complexity a little bit, let's say we want to get all books' data using that element as a starting point for some reason
|
||||
```python
|
||||
>>> for product in element.parent.parent.find_similar():
|
||||
print({
|
||||
"name": product.css_first('h3 a::text'),
|
||||
"price": product.css_first('.price_color').re_first(r'[\d\.]+'),
|
||||
"stock": product.css('.availability::text')[-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
|
||||
See more advanced or real-world 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_first('h3::text'),
|
||||
'price': p.css_first('.price::text').re_first(r'\d+\.\d{2}'),
|
||||
'stock': 'In stock' in p.text,
|
||||
'rating': p.css_first('.rating').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_first('table tbody tr')
|
||||
|
||||
# Find similar rows
|
||||
rows = first_row.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'column1': row.css_first('td:nth-child(1)::text'),
|
||||
'column2': row.css_first('td:nth-child(2)::text'),
|
||||
'column3': row.css_first('td:nth-child(3)::text')
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
```
|
||||
Form Field Extraction
|
||||
```python
|
||||
def extract_form_fields(page):
|
||||
# Find first form field container
|
||||
first_field = page.css_first('input').find_ancestor(
|
||||
lambda e: e.has_class('form-field')
|
||||
)
|
||||
|
||||
# Find similar field containers
|
||||
fields = first_field.find_similar()
|
||||
|
||||
return [
|
||||
{
|
||||
'label': f.css_first('label::text'),
|
||||
'type': f.css_first('input').attrib.get('type'),
|
||||
'required': 'required' in f.css_first('input').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_first('.review-text::text'),
|
||||
'rating': r.attrib.get('data-rating'),
|
||||
'author': r.css_first('.reviewer::text')
|
||||
}
|
||||
for r in all_reviews
|
||||
]
|
||||
```
|
||||
## Filters-based searching
|
||||
This search method might be arguably the best way to find elements in Scrapling because it is powerful and easier to learn for newcomers to Web Scraping than learning to write selectors.
|
||||
|
||||
Inspired by BeautifulSoup's `find_all` function, you can find elements using the `find_all` and `find` methods. Both methods can take multiple types of filters and return all elements in the pages that all these filters apply to.
|
||||
|
||||
To be more specific:
|
||||
|
||||
* Any string passed is considered a tag name.
|
||||
* Any iterable passed like List/Tuple/Set is considered 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. As you probably understood, the filtering process always starts from the first filter it finds in the filtering order above. So, if no tag name(s) are passed but attributes are passed, the process starts from that layer, and so on.
|
||||
2. The order in which you pass the arguments doesn't matter. The only order that's taken into consideration is the order explained above.
|
||||
|
||||
Check examples to clear any confusion :)
|
||||
|
||||
### 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_first('.text::text'))
|
||||
[<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>]
|
||||
```
|
||||
Find all elements that don't 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 its 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')
|
||||
['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 that its `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
|
||||
You can always generate CSS/XPath selectors for any element that can be reused here or anywhere else, and the most remarkable thing is that it doesn't matter what method you used to find that element!
|
||||
|
||||
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: <br>
|
||||
> When you tell Scrapling to create a short selector, it tries to find a unique element to use in generation as a stop point, like an element with an `id` attribute, but in our case, there wasn't any so that's why the short and the full selector will be the same.
|
||||
|
||||
## Using selectors with regular expressions
|
||||
Like in `parsel`/`scrapy`, you have the methods `re` and `re_first` for extracting data using regular expressions. However, unlike the former, these methods are in nearly all classes like `Adaptor`/`Adaptors`/`TextHandler` and `TextHandlers`, which means you can use them directly on the element even if you didn't select a text node.
|
||||
|
||||
We will have a deep look at it while explaining the [TextHandler](main_classes.md#texthandler) class, but in general, it works like the below examples:
|
||||
```python
|
||||
>>> page.css_first('.price_color').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']
|
||||
```
|
||||
And so on. You get the idea. We will explain this in more detail on the next page while explaining the [TextHandler](main_classes.md#texthandler) class.
|
||||
Reference in New Issue
Block a user