Key Features¶ Fetch websites as you prefer with async support¶ HTTP Requests: Fast and stealthy HTTP requests with the Fetcher class. Dynamic Loading & Automation: Fetch dynamic websites with the PlayWrightFetcher class through your real browser, Scrapling's stealth mode, Playwright's Chromium browser, or NSTbrowser's browserless! Anti-bot Protections Bypass: Easily bypass protections with the StealthyFetcher and PlayWrightFetcher classes. Easy Scraping¶ Smart Element Tracking: Relocate elements after website changes using an intelligent similarity system and integrated storage. Flexible Selection: CSS selectors, XPath selectors, filters-based search, text search, regex search, and more. Find Similar Elements: Automatically locate elements similar to the element you found! Smart Content Scraping: Extract data from multiple websites without specific selectors using Scrapling powerful features. High Performance¶ Lightning Fast: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries. Memory Efficient: Optimized data structures for minimal memory footprint. Fast JSON serialization: 10x faster than standard library. Developer Friendly¶ Powerful Navigation API: Easy DOM traversal in all directions. Rich Text Processing: All strings have built-in regex, cleaning methods, and more. All elements' attributes are optimized dictionaries that use less memory than standard dictionaries with added methods. Auto Selectors Generation: Generate robust short and full CSS/XPath selectors for any element. Familiar API: Similar to Scrapy/BeautifulSoup and the same CSS pseudo-elements used in Scrapy. Type hints: Complete type/doc-strings coverage for future-proofing and best autocompletion support. Overview We will start by quickly reviewing the parsing capabilities. Then, we will fetch websites with custom browsers, make requests, and parse the response. Here's an HTML document generated by ChatGPT we will be using as an example throughout this page: Complex Web Page

Products

Product 1

This is product 1

$10.99

Product 2

This is product 2

$20.99

Product 3

This is product 3

$15.99

Customer Reviews

Great product!

John Doe

Good value for money.

Jane Smith
Starting with loading raw HTML above like this from scrapling.parser import Adaptor page = Adaptor(html_doc) page # Complex Web Page</tit...'> Get all text content on the page recursively page.get_all_text(ignore_tags=('script', 'style')) # 'Complex Web Page\nHome\nAbout\nContact\nProducts\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\nCustomer Reviews\nGreat product!\nJohn Doe\nGood value for money.\nJane Smith' Finding elements¶ If there's an element you want to find on the page, you will! Your creativity level is the only limitation! Finding the first HTML section element section_element = page.find('section') # <data='<section id="products" schema='{"jsonabl...' parent='<main><section id="products" schema='{"j...'> Find all section elements section_elements = page.find_all('section') # [<data='<section id="products" schema='{"jsonabl...' parent='<main><section id="products" schema='{"j...'>, <data='<section id="reviews"><h2>Customer Revie...' parent='<main><section id="products" schema='{"j...'>] Find all section elements whose id attribute value is products section_elements = page.find_all('section', {'id':"products"}) # Same as section_elements = page.find_all('section', id="products") # [<data='<section id="products" schema='{"jsonabl...' parent='<main><section id="products" schema='{"j...'>] Find all section elements that its id attribute value contains product section_elements = page.find_all('section', {'id*':"product"}) Find all h3 elements whose text content matches this regex Product \d page.find_all('h3', re.compile(r'Product \d')) # [<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>, <data='<h3>Product 2</h3>' parent='<article class="product" data-id="2"><h3...'>, <data='<h3>Product 3</h3>' parent='<article class="product" data-id="3"><h3...'>] Find all h3 and h2 elements whose text content matches regex Product only page.find_all(['h3', 'h2'], re.compile(r'Product')) # [<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>, <data='<h3>Product 2</h3>' parent='<article class="product" data-id="2"><h3...'>, <data='<h3>Product 3</h3>' parent='<article class="product" data-id="3"><h3...'>, <data='<h2>Products</h2>' parent='<section id="products" schema='{"jsonabl...'>] Find all elements that its text content matches exactly Products (Whitespaces are not taken into consideration) page.find_by_text('Products', first_match=False) # [<data='<h2>Products</h2>' parent='<section id="products" schema='{"jsonabl...'>] Or find all elements whose text content matches regex Product \d page.find_by_regex(r'Product \d', first_match=False) # [<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>, <data='<h3>Product 2</h3>' parent='<article class="product" data-id="2"><h3...'>, <data='<h3>Product 3</h3>' parent='<article class="product" data-id="3"><h3...'>] Find all elements that are similar to the element you want target_element = page.find_by_regex(r'Product \d', first_match=True) # <data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'> target_element.find_similar() # [<data='<h3>Product 2</h3>' parent='<article class="product" data-id="2"><h3...'>, <data='<h3>Product 3</h3>' parent='<article class="product" data-id="3"><h3...'>] Find the first element that matches a CSS selector page.css_first('.product-list [data-id="1"]') # <data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'> Find all elements that match a CSS selector page.css('.product-list article') # [<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...'>] Find the first element that matches an XPath selector page.xpath_first("//*[@id='products']/div/article") # <data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'> Find all elements that match an XPath selector page.xpath("//*[@id='products']/div/article") # [<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...'>] With this, we just scratched the surface of these functions; more advanced options with these selection methods are shown later. Accessing elements' data¶ It's as simple as >>> section_element.tag 'section' >>> print(section_element.attrib) {'id': 'products', 'schema': '{"jsonable": "data"}'} >>> section_element.attrib['schema'].json() # If an attribute value can be converted to json, then use `.json()` to convert it {'jsonable': 'data'} >>> section_element.text # Direct text content '' >>> section_element.get_all_text() # All text content recursively 'Products\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' >>> section_element.html_content # The HTML content of the element '<section id="products" schema=\'{"jsonable": "data"}\'><h2>Products</h2>\n <div class="product-list">\n <article class="product" data-id="1"><h3>Product 1</h3>\n <p class="description">This is product 1</p>\n <span class="price">$10.99</span>\n <div class="hidden stock">In stock: 5</div>\n </article><article class="product" data-id="2"><h3>Product 2</h3>\n <p class="description">This is product 2</p>\n <span class="price">$20.99</span>\n <div class="hidden stock">In stock: 3</div>\n </article><article class="product" data-id="3"><h3>Product 3</h3>\n <p class="description">This is product 3</p>\n <span class="price">$15.99</span>\n <div class="hidden stock">Out of stock</div>\n </article></div>\n </section>' >>> print(section_element.prettify()) # The prettified version ''' <section id="products" schema='{"jsonable": "data"}'><h2>Products</h2> <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> </section> ''' >>> section_element.path # All the ancestors in the DOM tree of this element [<data='<main><section id="products" schema='{"j...' parent='<body> <header><nav><ul><li> <a href="#h...'>, <data='<body> <header><nav><ul><li> <a href="#h...' parent='<html><head><title>Complex Web Page</tit...'>, <data='<html><head><title>Complex Web Page</tit...'>] >>> section_element.generate_css_selector '#products' >>> section_element.generate_full_css_selector 'body > main > #products > #products' >>> section_element.generate_xpath_selector "//*[@id='products']" >>> section_element.generate_full_xpath_selector "//body/main/*[@id='products']" Navigation¶ Using the elements we found above >>> section_element.parent <data='<main><section id="products" schema='{"j...' parent='<body> <header><nav><ul><li> <a href="#h...'> >>> section_element.parent.tag 'main' >>> section_element.parent.parent.tag 'body' >>> section_element.children [<data='<h2>Products</h2>' parent='<section id="products" schema='{"jsonabl...'>, <data='<div class="product-list"> <article clas...' parent='<section id="products" schema='{"jsonabl...'>] >>> section_element.siblings [<data='<section id="reviews"><h2>Customer Revie...' parent='<main><section id="products" schema='{"j...'>] >>> section_element.next # gets the next element, the same logic applies to `quote.previous` <data='<section id="reviews"><h2>Customer Revie...' parent='<main><section id="products" schema='{"j...'> >>> section_element.children.css('h2::text') ['Products'] >>> page.css_first('[data-id="1"]').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 one below for ancestor in quote.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 object as an argument and return True if the condition satisfies or False otherwise like below: >>> section_element.find_ancestor(lambda ancestor: ancestor.css('nav')) <data='<body> <header><nav><ul><li> <a href="#h...' parent='<html><head><title>Complex Web Page</tit...'> Fetching websites¶ Instead of passing the raw HTML to Scrapling, you can get a website's response directly through HTTP requests or by fetching it from browsers. A fetcher is made for every use case. HTTP Requests¶ For simple HTTP requests, there's a Fetcher class that can be imported as below: from scrapling.fetchers import Fetcher But that's class, so you will need to create an instance of the Fetcher first like this: from scrapling.fetchers import Fetcher fetcher = Fetcher() page = fetcher.get('https://httpbin.org/get') This is intended, and you will find it with all fetchers because there are settings you can pass to Fetcher() initialization, but more on this later. If you are going to use the default settings anyway, you can do this instead for a cleaner approach: from scrapling.fetchers import Fetcher page = Fetcher.get('https://httpbin.org/get') With that out of the way, here's how to do all HTTP methods: >>> from scrapling.fetchers import Fetcher >>> page = Fetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) >>> page = Fetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') >>> page = Fetcher.put('https://httpbin.org/put', data={'key': 'value'}) >>> page = Fetcher.delete('https://httpbin.org/delete') For Async requests, you will just replace the import like below: >>> from scrapling.fetchers import AsyncFetcher >>> page = await AsyncFetcher.get('https://httpbin.org/get', stealthy_headers=True, follow_redirects=True) >>> page = await AsyncFetcher.post('https://httpbin.org/post', data={'key': 'value'}, proxy='http://username:password@localhost:8030') >>> page = await AsyncFetcher.put('https://httpbin.org/put', data={'key': 'value'}) >>> page = await AsyncFetcher.delete('https://httpbin.org/delete') Note: You have the stealthy_headers argument, which, when enabled, makes requests to generate real browser headers and use them, including a referer header, as if this request came from Google's search of this URL's domain. It's enabled by default. This is just the tip of this fetcher; check the full page from here Dynamic loading¶ We have you covered if you deal with dynamic websites like most today! The PlayWrightFetcher class provides many options to fetch/load websites' pages through browsers. >>> from scrapling.fetchers import PlayWrightFetcher >>> page = PlayWrightFetcher.fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # Vanilla Playwright option >>> page.css_first("#search a::attr(href)") 'https://github.com/D4Vinci/Scrapling' >>> # The async version of fetch >>> page = await PlayWrightFetcher.async_fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) >>> page.css_first("#search a::attr(href)") 'https://github.com/D4Vinci/Scrapling' It's named like that because it's built on top of Playwright, and it currently provides 4 main run options that can be mixed as you want: Vanilla Playwright without any modifications other than the ones you chose. Stealthy Playwright with custom stealth mode explicitly written for it. It's not top-tier stealth mode but bypasses many online tests like Sannysoft's. Check out the StealthyFetcher class below for more advanced stealth mode. Real browsers by passing the real_chrome argument or the CDP URL of your browser to be controlled by the Fetcher, and most of the options can be enabled on it. NSTBrowser's docker browserless option by passing the CDP URL and enabling nstbrowser_mode option. Note: All requests done by this fetcher are waited by default for all javascript to be fully loaded and executed. In detail, it waits for the load and domcontentloaded load states to be reached; you can make it wait for the networkidle load state by passing 'network_idle=True', as you will see later. Again, this is just the tip of this fetcher. Check out the full page from here for all details and the complete list of arguments. Dynamic anti-protection loading¶ We also have you covered if you deal with dynamic websites with annoying anti-protections! The StealthyFetcher class uses a modified Firefox browser called Camoufox, bypassing most anti-bot protections by default. Scrapling adds extra layers of flavors and configurations to further increase performance and undetectability. >>> page = StealthyFetcher().fetch('https://www.browserscan.net/bot-detection') # Running headless by default >>> page.status == 200 True >>> page = StealthyFetcher().fetch('https://www.browserscan.net/bot-detection', humanize=True, os_randomize=True) # and the rest of arguments... >>> # The async version of fetch >>> page = await StealthyFetcher().async_fetch('https://www.browserscan.net/bot-detection') >>> page.status == 200 True Note: All requests done by this fetcher are waited by default for all javascript to be fully loaded and executed. In detail, it waits for the load and domcontentloaded load states to be reached; you can make it wait for the networkidle load state by passing 'network_idle=True', as you will see later. Again, this is just the tip of this fetcher. Check out the full page from here for all details and the complete list of arguments. That's Scrapling at a glance. If you want to learn more about it, continue to the next section. Querying elements 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: CSS3 Selectors XPath Selectors Finding elements based on filters/conditions. Finding elements whose content contains specific text 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. 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. CSS/XPath selectors¶ What are CSS selectors?¶ 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. CSS selectors support comes from cssselect, so it's better to read about which selectors are supported from cssselect 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 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. 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 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 product = page.css_first('.product') product = page.xpath_first('//*[@class="product"]') Which would be the same as doing product = page.css('.product')[0] product = page.xpath('//*[@class="product"]')[0] Get the text of the first element with the h1 tag name title = page.css_first('h1::text') title = page.xpath_first('//h1//text()') Which is again the same as doing title = page.css_first('h1').text title = page.xpath_first('//h1').text Get the href attribute of the first element with a tag name 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' 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 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 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: Elements whose direct text content contains given text with many options through the find_by_text method. 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: Find all page elements with the same tree depth as this element. All found elements will be checked, and those without the same tag name, parent tag name, and grandparent tag name will be dropped. 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. from scrapling.fetchers import Fetcher page = Fetcher.get('https://books.toscrape.com/index.html') Find the first element whose text fully matches this text >>> 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 >>> 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) >>> 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) >>> 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. >>> 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 >>> 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: >>> 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 >>> 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 >>> 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. >>> len(element.find_similar(ignore_attributes=['title'])) 19 Get the href attribute from all similar elements >>> [ 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 >>> 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 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 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 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 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: All elements with the passed tag name(s) get collected. All elements that match all passed attribute(s) are collected; if a previous filter is used, then previously collected elements are filtered. All elements that match all passed regex patterns are collected, or if previous filter(s) are used, then previously collected elements are filtered. All elements that fulfill all passed function(s) are collected; if a previous filter(s) is used, then previously collected elements are filtered. Notes: 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. 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¶ >>> from scrapling.fetchers import Fetcher >>> page = Fetcher.get('https://quotes.toscrape.com/') Find all elements with the tag name div. >>> 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. >>> 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. >>> 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. >>> 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. >>> 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. >>> 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. >>> 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 >>> 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) >>> 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 >>> 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'. >>> 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 >>> 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) >>> 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 >>> 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) >>> 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 >>> url_element.generate_full_xpath_selector '//body/div/div[2]/div/div/span[2]/a' Note: 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 class, but in general, it works like the below examples: >>> 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 class. Main classes Introduction¶ After exploring the various ways to select elements with Scrapling and related features, Let's take a step back and examine the Adaptor class generally and other objects to better understand the parsing engine. The 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 from scrapling import Adaptor from scrapling.parser import Adaptor then use it directly as you already learned in the overview page 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 object. Any operation you do, like selection, navigation, etc., will return either an Adaptor object or an 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 object, and the elements within are Adaptor objects, and so on. Any text, such as the text content inside elements or the text inside element attributes, is a TextHandler object, and the attributes of each element are stored as AttributesHandler. We will return to both objects later, so let's focus on the 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 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 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 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 section. Let's say we are parsing this HTML page for simplicity: <html> <head> <title>Some page

Product 1

This is product 1

$10.99

Product 2

This is product 2

$20.99

Product 3

This is product 3

$15.99
Load the page directly as shown before: from scrapling import Adaptor page = Adaptor(html_doc) Get all text content on the page recursively >>> 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 article = page.find('article') With the same logic, get all text content on the element recursively >>> 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 >>> article.text '' The get_all_text method has the following optional arguments: separator: All strings collected will be concatenated using this separator. The default is '\n' strip: If enabled, strings will be stripped before concatenation. Disabled by default. ignore_tags: A tuple of all tag names you want to ignore in the final results. The default is ('script', 'style',). 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; we will get to this in detail later, so if the text content can be serialized to JSON, then use .json() on it >>> script = page.find('script') >>> script.json() {'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3} Let's continue to get the element tag >>> article.tag 'article' If you used it on the page directly, you will find you are operating on the root html element >>> 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 >>> print(article.attrib) {'class': 'product', 'data-id': '1'} Get the HTML content of the element >>> article.html_content '

Product 1

\n

This is product 1

\n $10.99\n \n
' It's the same if you used the .body property >>> article.body '

Product 1

\n

This is product 1

\n $10.99\n \n
' Get the prettified version of the HTML content of the element >>> print(article.prettify())

Product 1

This is product 1

$10.99
To get all the ancestors in the DOM tree of this element >>> article.path [
,
, Some page] Generate a CSS shortened selector if possible, or generate the full selector >>> article.generate_css_selector 'body > div > article' >>> article.generate_full_css_selector 'body > div > article' Same case with XPath >>> 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. Simply put, the html element is the root of the website's tree, as every page starts with an html element. 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 >>> article.parent
>>> article.parent.tag 'div' You can chain it as you want, which applies to all similar properties/methods we will review. >>> article.parent.parent.tag 'body' Get the children of an element >>> article.children [Product 1' parent='
, This is product 1...' parent='
, $10.99' parent='
,