Product 1
+Description 1
+Description 1
+Description 2
+Description 1
+Description 2
+This is product 1
+ $10.99 +This is product 2
+ $20.99 +This is product 3
+ $15.99 +This is product 1
\n $10.99\nThis is product 1
+ $10.99 +{"some_key": "some_value"}
' + ``` + 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'))) + [£51.77
' parent='£51.77
' parent='£53.74
' parent='£50.10
' parent='£47.82
' parent=',
+ ,
+ ,
+...]
+```
+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')
+[