scrapling 0.1.2__py3-none-any.whl → 0.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,807 @@
1
+ Metadata-Version: 2.1
2
+ Name: scrapling
3
+ Version: 0.2
4
+ Summary: Scrapling is a powerful, flexible, and high-performance web scraping library for Python. It
5
+ Home-page: https://github.com/D4Vinci/Scrapling
6
+ Author: Karim Shoair
7
+ Author-email: karim.shoair@pm.me
8
+ License: BSD
9
+ Project-URL: Documentation, https://github.com/D4Vinci/Scrapling/tree/main/docs
10
+ Project-URL: Source, https://github.com/D4Vinci/Scrapling
11
+ Project-URL: Tracker, https://github.com/D4Vinci/Scrapling/issues
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Text Processing :: Markup
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
20
+ Classifier: Topic :: Text Processing :: Markup :: HTML
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3 :: Only
24
+ Classifier: Programming Language :: Python :: 3.8
25
+ Classifier: Programming Language :: Python :: 3.9
26
+ Classifier: Programming Language :: Python :: 3.10
27
+ Classifier: Programming Language :: Python :: 3.11
28
+ Classifier: Programming Language :: Python :: 3.12
29
+ Classifier: Programming Language :: Python :: 3.13
30
+ Classifier: Programming Language :: Python :: Implementation :: CPython
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: requests >=2.3
36
+ Requires-Dist: lxml >=4.5
37
+ Requires-Dist: cssselect >=1.2
38
+ Requires-Dist: w3lib
39
+ Requires-Dist: orjson >=3
40
+ Requires-Dist: tldextract
41
+ Requires-Dist: httpx[brotli,zstd]
42
+ Requires-Dist: playwright
43
+ Requires-Dist: rebrowser-playwright
44
+ Requires-Dist: camoufox >=0.3.7
45
+ Requires-Dist: browserforge
46
+
47
+ # 🕷️ Scrapling: Undetectable, Lightning-Fast, and Adaptive Web Scraping for Python
48
+ [![Tests](https://github.com/D4Vinci/Scrapling/actions/workflows/tests.yml/badge.svg)](https://github.com/D4Vinci/Scrapling/actions/workflows/tests.yml) [![PyPI version](https://badge.fury.io/py/Scrapling.svg)](https://badge.fury.io/py/Scrapling) [![Supported Python versions](https://img.shields.io/pypi/pyversions/scrapling.svg)](https://pypi.org/project/scrapling/) [![PyPI Downloads](https://static.pepy.tech/badge/scrapling)](https://pepy.tech/project/scrapling)
49
+
50
+ Dealing with failing web scrapers due to anti-bot protections or website changes? Meet Scrapling.
51
+
52
+ Scrapling is a high-performance, intelligent web scraping library for Python that automatically adapts to website changes while significantly outperforming popular alternatives. For both beginners and experts, Scrapling provides powerful features while maintaining simplicity.
53
+
54
+ ```python
55
+ >> from scrapling import Fetcher, StealthyFetcher, PlayWrightFetcher
56
+ # Fetch websites' source under the radar!
57
+ >> fetcher = StealthyFetcher().fetch('https://example.com', headless=True, disable_resources=True)
58
+ >> print(fetcher.status)
59
+ 200
60
+ >> page = fetcher.adaptor
61
+ >> products = page.css('.product', auto_save=True) # Scrape data that survives website design changes!
62
+ >> # Later, if the website structure changes, pass `auto_match=True`
63
+ >> products = page.css('.product', auto_match=True) # and Scrapling still finds them!
64
+ ```
65
+
66
+ ## Table of content
67
+ * [Key Features](#key-features)
68
+ * [Fetch websites as you prefer](#fetch-websites-as-you-prefer)
69
+ * [Adaptive Scraping](#adaptive-scraping)
70
+ * [Performance](#performance)
71
+ * [Developing Experience](#developing-experience)
72
+ * [Getting Started](#getting-started)
73
+ * [Parsing Performance](#parsing-performance)
74
+ * [Text Extraction Speed Test (5000 nested elements).](#text-extraction-speed-test-5000-nested-elements)
75
+ * [Extraction By Text Speed Test](#extraction-by-text-speed-test)
76
+ * [Installation](#installation)
77
+ * [Fetching Websites Features](#fetching-websites-features)
78
+ * [Fetcher](#fetcher)
79
+ * [StealthyFetcher](#stealthyfetcher)
80
+ * [PlayWrightFetcher](#playwrightfetcher)
81
+ * [Advanced Parsing Features](#advanced-parsing-features)
82
+ * [Smart Navigation](#smart-navigation)
83
+ * [Content-based Selection & Finding Similar Elements](#content-based-selection--finding-similar-elements)
84
+ * [Handling Structural Changes](#handling-structural-changes)
85
+ * [Real World Scenario](#real-world-scenario)
86
+ * [Find elements by filters](#find-elements-by-filters)
87
+ * [Is That All?](#is-that-all)
88
+ * [More Advanced Usage](#more-advanced-usage)
89
+ * [⚡ Enlightening Questions and FAQs](#-enlightening-questions-and-faqs)
90
+ * [How does auto-matching work?](#how-does-auto-matching-work)
91
+ * [How does the auto-matching work if I didn't pass a URL while initializing the Adaptor object?](#how-does-the-auto-matching-work-if-i-didnt-pass-a-url-while-initializing-the-adaptor-object)
92
+ * [If all things about an element can change or get removed, what are the unique properties to be saved?](#if-all-things-about-an-element-can-change-or-get-removed-what-are-the-unique-properties-to-be-saved)
93
+ * [I have enabled the `auto_save`/`auto_match` parameter while selecting and it got completely ignored with a warning message](#i-have-enabled-the-auto_saveauto_match-parameter-while-selecting-and-it-got-completely-ignored-with-a-warning-message)
94
+ * [I have done everything as the docs but the auto-matching didn't return anything, what's wrong?](#i-have-done-everything-as-the-docs-but-the-auto-matching-didnt-return-anything-whats-wrong)
95
+ * [Can Scrapling replace code built on top of BeautifulSoup4?](#can-scrapling-replace-code-built-on-top-of-beautifulsoup4)
96
+ * [Can Scrapling replace code built on top of AutoScraper?](#can-scrapling-replace-code-built-on-top-of-autoscraper)
97
+ * [Is Scrapling thread-safe?](#is-scrapling-thread-safe)
98
+ * [Sponsors](#sponsors)
99
+ * [Contributing](#contributing)
100
+ * [Disclaimer for Scrapling Project](#disclaimer-for-scrapling-project)
101
+ * [License](#license)
102
+ * [Acknowledgments](#acknowledgments)
103
+ * [Thanks and References](#thanks-and-references)
104
+ * [Known Issues](#known-issues)
105
+
106
+ ## Key Features
107
+
108
+ ### Fetch websites as you prefer
109
+ - **HTTP requests**: Stealthy and fast HTTP requests with `Fetcher`
110
+ - **Stealthy fetcher**: Annoying anti-bot protection? No problem! Scrapling can bypass almost all of them with `StealthyFetcher` with default configuration!
111
+ - **Your preferred browser**: Use your real browser with CDP, [NSTbrowser](https://app.nstbrowser.io/r/1vO5e5)'s browserless, PlayWright with stealth mode, or even vanilla PlayWright - All is possible with `PlayWrightFetcher`!
112
+
113
+ ### Adaptive Scraping
114
+ - 🔄 **Smart Element Tracking**: Locate previously identified elements after website structure changes, using an intelligent similarity system and integrated storage.
115
+ - 🎯 **Flexible Querying**: Use CSS selectors, XPath, Elements filters, text search, or regex - chain them however you want!
116
+ - 🔍 **Find Similar Elements**: Automatically locate elements similar to the element you want on the page (Ex: other products like the product you found on the page).
117
+ - 🧠 **Smart Content Scraping**: Extract data from multiple websites without specific selectors using Scrapling powerful features.
118
+
119
+ ### Performance
120
+ - 🚀 **Lightning Fast**: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries (outperforming BeautifulSoup in parsing by up to 620x in our tests).
121
+ - 🔋 **Memory Efficient**: Optimized data structures for minimal memory footprint.
122
+ - ⚡ **Fast JSON serialization**: 10x faster JSON serialization than the standard json library with more options.
123
+
124
+ ### Developing Experience
125
+ - 🛠️ **Powerful Navigation API**: Traverse the DOM tree easily in all directions and get the info you want (parent, ancestors, sibling, children, next/previous element, and more).
126
+ - 🧬 **Rich Text Processing**: All strings have built-in methods for regex matching, cleaning, and more. All elements' attributes are read-only dictionaries that are faster than standard dictionaries with added methods.
127
+ - 📝 **Automatic Selector Generation**: Create robust CSS/XPath selectors for any element.
128
+ - 🔌 **API Similar to Scrapy/BeautifulSoup**: Familiar methods and similar pseudo-elements for Scrapy and BeautifulSoup users.
129
+ - 📘 **Type hints and test coverage**: Complete type coverage and almost full test coverage for better IDE support and fewer bugs, respectively.
130
+
131
+ ## Getting Started
132
+
133
+ ```python
134
+ from scrapling import Fetcher
135
+
136
+ fetcher = Fetcher(auto_match=False)
137
+
138
+ # Fetch a web page and create an Adaptor instance
139
+ page = fetcher.get('https://quotes.toscrape.com/', stealthy_headers=True).adaptor
140
+ # Get all strings in the full page
141
+ page.get_all_text(ignore_tags=('script', 'style'))
142
+
143
+ # Get all quotes, any of these methods will return a list of strings (TextHandlers)
144
+ quotes = page.css('.quote .text::text') # CSS selector
145
+ quotes = page.xpath('//span[@class="text"]/text()') # XPath
146
+ quotes = page.css('.quote').css('.text::text') # Chained selectors
147
+ quotes = [element.text for element in page.css('.quote .text')] # Slower than bulk query above
148
+
149
+ # Get the first quote element
150
+ quote = page.css_first('.quote') # / page.css('.quote').first / page.css('.quote')[0]
151
+
152
+ # Tired of selectors? Use find_all/find
153
+ quotes = page.find_all('div', {'class': 'quote'})
154
+ # Same as
155
+ quotes = page.find_all('div', class_='quote')
156
+ quotes = page.find_all(['div'], class_='quote')
157
+ quotes = page.find_all(class_='quote') # and so on...
158
+
159
+ # Working with elements
160
+ quote.html_content # Inner HTML
161
+ quote.prettify() # Prettified version of Inner HTML
162
+ quote.attrib # Element attributes
163
+ quote.path # DOM path to element (List)
164
+ ```
165
+ To keep it simple, all methods can be chained on top of each other!
166
+
167
+ ## Parsing Performance
168
+
169
+ Scrapling isn't just powerful - it's also blazing fast. Scrapling implements many best practices, design patterns, and numerous optimizations to save fractions of seconds. All of that while focusing exclusively on parsing HTML documents.
170
+ Here are benchmarks comparing Scrapling to popular Python libraries in two tests.
171
+
172
+ ### Text Extraction Speed Test (5000 nested elements).
173
+
174
+ | # | Library | Time (ms) | vs Scrapling |
175
+ |---|:-----------------:|:---------:|:------------:|
176
+ | 1 | Scrapling | 5.44 | 1.0x |
177
+ | 2 | Parsel/Scrapy | 5.53 | 1.017x |
178
+ | 3 | Raw Lxml | 6.76 | 1.243x |
179
+ | 4 | PyQuery | 21.96 | 4.037x |
180
+ | 5 | Selectolax | 67.12 | 12.338x |
181
+ | 6 | BS4 with Lxml | 1307.03 | 240.263x |
182
+ | 7 | MechanicalSoup | 1322.64 | 243.132x |
183
+ | 8 | BS4 with html5lib | 3373.75 | 620.175x |
184
+
185
+ As you see, Scrapling is on par with Scrapy and slightly faster than Lxml which both libraries are built on top of. These are the closest results to Scrapling. PyQuery is also built on top of Lxml but still, Scrapling is 4 times faster.
186
+
187
+ ### Extraction By Text Speed Test
188
+
189
+ | Library | Time (ms) | vs Scrapling |
190
+ |:-----------:|:---------:|:------------:|
191
+ | Scrapling | 2.51 | 1.0x |
192
+ | AutoScraper | 11.41 | 4.546x |
193
+
194
+ Scrapling can find elements with more methods and it returns full element `Adaptor` objects not only the text like AutoScraper. So, to make this test fair, both libraries will extract an element with text, find similar elements, and then extract the text content for all of them. As you see, Scrapling is still 4.5 times faster at the same task.
195
+
196
+ > All benchmarks' results are an average of 100 runs. See our [benchmarks.py](https://github.com/D4Vinci/Scrapling/blob/main/benchmarks.py) for methodology and to run your comparisons.
197
+
198
+ ## Installation
199
+ Scrapling is a breeze to get started with - Starting from version 0.2, we require at least Python 3.8 to work.
200
+ ```bash
201
+ pip3 install scrapling
202
+ ```
203
+ - For using the `StealthyFetcher`, go to the command line and download the browser with
204
+ <details><summary>Windows OS</summary>
205
+
206
+ ```bash
207
+ camoufox fetch --browserforge
208
+ ```
209
+ </details>
210
+ <details><summary>MacOS</summary>
211
+
212
+ ```bash
213
+ python3 -m camoufox fetch --browserforge
214
+ ```
215
+ </details>
216
+ <details><summary>Linux</summary>
217
+
218
+ ```bash
219
+ python -m camoufox fetch --browserforge
220
+ ```
221
+ On a fresh installation of Linux, you may also need the following Firefox dependencies:
222
+ - Debian-based distros
223
+ ```bash
224
+ sudo apt install -y libgtk-3-0 libx11-xcb1 libasound2
225
+ ```
226
+ - Arch-based distros
227
+ ```bash
228
+ sudo pacman -S gtk3 libx11 libxcb cairo libasound alsa-lib
229
+ ```
230
+ </details>
231
+
232
+ <small> See the official <a href="https://camoufox.com/python/installation/#download-the-browser">Camoufox documentation</a> for more info on installation</small>
233
+
234
+ - If you are going to use the `PlayWrightFetcher` options, then install Playwright's Chromium browser with:
235
+ ```commandline
236
+ playwright install chromium
237
+ ```
238
+ - If you are going to use normal requests only with the `Fetcher` class then update the fingerprints files with:
239
+ ```commandline
240
+ python -m browserforge update
241
+ ```
242
+
243
+ ## Fetching Websites Features
244
+ All fetcher-type classes are imported in the same way
245
+ ```python
246
+ from scrapling import Fetcher, StealthyFetcher, PlayWrightFetcher
247
+ ```
248
+ And all of them can take these initialization arguments: `auto_match`, `huge_tree`, `keep_comments`, `storage`, `storage_args`, and `debug` which are the same ones you give to the `Adaptor` class.
249
+ > [!NOTE]
250
+ > The `auto_match` argument is enabled by default which is the one you should care about the most as you will see later.
251
+ ### Fetcher
252
+ This class is built on top of [httpx](https://www.python-httpx.org/) with additional configuration options, here you can do `GET`, `POST`, `PUT`, and `DELETE` requests.
253
+
254
+ For all methods, you have `stealth_headers` which makes `Fetcher` create and use real browser's headers then create a referer header as if this request came from Google's search of this URL's domain. It's enabled by default.
255
+ ```python
256
+ >> page = Fetcher().get('https://httpbin.org/get', stealth_headers=True, follow_redirects=True)
257
+ >> page = Fetcher().post('https://httpbin.org/post', data={'key': 'value'})
258
+ >> page = Fetcher().put('https://httpbin.org/put', data={'key': 'value'})
259
+ >> page = Fetcher().delete('https://httpbin.org/delete')
260
+ ```
261
+ ### StealthyFetcher
262
+ This class is built on top of [Camoufox](https://github.com/daijro/camoufox) which by default bypasses most of the anti-bot protections. Scrapling adds extra layers of flavors and configurations to increase performance and undetectability even further.
263
+ ```python
264
+ >> page = StealthyFetcher().fetch('https://www.browserscan.net/bot-detection') # Running headless by default
265
+ >> page.status == 200
266
+ True
267
+ ```
268
+ <details><summary><strong>For the sake of simplicity, expand this for the complete list of arguments</strong></summary>
269
+
270
+ | Argument | Description | Optional |
271
+ |:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
272
+ | url | Target url | ❌ |
273
+ | headless | Pass `True` to run the browser in headless/hidden (**default**), `virtual` to run it in virtual screen mode, or `False` for headful/visible mode. The `virtual` mode requires having `xvfb` installed. | ✔️ |
274
+ | block_images | Prevent the loading of images through Firefox preferences. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | ✔️ |
275
+ | disable_resources | Drop requests of unnecessary resources for a speed boost. It depends but it made requests ~25% faster in my tests for some websites.<br/>Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | ✔️ |
276
+ | google_search | Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. | ✔️ |
277
+ | extra_headers | A dictionary of extra headers to add to the request. _The referer set by the `google_search` argument takes priority over the referer set here if used together._ | ✔️ |
278
+ | block_webrtc | Blocks WebRTC entirely. | ✔️ |
279
+ | page_action | Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. | ✔️ |
280
+ | addons | List of Firefox addons to use. **Must be paths to extracted addons.** | ✔️ |
281
+ | humanize | Humanize the cursor movement. Takes either True or the MAX duration in seconds of the cursor movement. The cursor typically takes up to 1.5 seconds to move across the window. | ✔️ |
282
+ | allow_webgl | Whether to allow WebGL. To prevent leaks, only use this for special cases. | ✔️ |
283
+ | network_idle | Wait for the page until there are no network connections for at least 500 ms. | ✔️ |
284
+ | timeout | The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. | ✔️ |
285
+ | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ |
286
+ | wait_selector_state | The state to wait for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ |
287
+
288
+ </details>
289
+
290
+ This list isn't final so expect a lot more additions and flexibility to be added in the next versions!
291
+
292
+ ### PlayWrightFetcher
293
+ This class is built on top of [Playwright](https://playwright.dev/python/) which currently provides 4 main run options but they can be mixed as you want.
294
+ ```python
295
+ >> page = PlayWrightFetcher().fetch('https://www.google.com/search?q=%22Scrapling%22', disable_resources=True) # Vanilla Playwright option
296
+ >> page.adaptor.css_first("#search a::attr(href)")
297
+ 'https://github.com/D4Vinci/Scrapling'
298
+ ```
299
+ Using this Fetcher class, you can make requests with:
300
+ 1) Vanilla Playwright without any modifications other than the ones you chose.
301
+ 2) Stealthy Playwright with the stealth mode I wrote for it. It's still a WIP but it bypasses many online tests like [Sannysoft's](https://bot.sannysoft.com/).</br> Some of the things this fetcher's stealth mode does include:
302
+ * Patching the CDP runtime fingerprint.
303
+ * Mimics some of the real browsers' properties by injecting several JS files and using custom options.
304
+ * Using custom flags on launch to hide Playwright even more and make it faster.
305
+ * Generates real browser's headers of the same type and same user OS then append it to the request's headers.
306
+ 3) Real browsers by passing the CDP URL of your browser to be controlled by the Fetcher and most of the options can be enabled on it.
307
+ 4) [NSTBrowser](https://app.nstbrowser.io/r/1vO5e5)'s [docker browserless](https://hub.docker.com/r/nstbrowser/browserless) option by passing the CDP URL and enabling `nstbrowser_mode` option.
308
+
309
+ Add that to a lot of controlling/hiding options as you will see in the arguments list below.
310
+
311
+ <details><summary><strong>Expand this for the complete list of arguments</strong></summary>
312
+
313
+ | Argument | Description | Optional |
314
+ |:-------------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
315
+ | url | Target url | ❌ |
316
+ | headless | Pass `True` to run the browser in headless/hidden (**default**), or `False` for headful/visible mode. | ✔️ |
317
+ | disable_resources | Drop requests of unnecessary resources for a speed boost. It depends but it made requests ~25% faster in my tests for some websites.<br/>Requests dropped are of type `font`, `image`, `media`, `beacon`, `object`, `imageset`, `texttrack`, `websocket`, `csp_report`, and `stylesheet`. _This can help save your proxy usage but be careful with this option as it makes some websites never finish loading._ | ✔️ |
318
+ | useragent | Pass a useragent string to be used. **Otherwise the fetcher will generate a real Useragent of the same browser and use it.** | ✔️ |
319
+ | network_idle | Wait for the page until there are no network connections for at least 500 ms. | ✔️ |
320
+ | timeout | The timeout in milliseconds that is used in all operations and waits through the page. The default is 30000. | ✔️ |
321
+ | page_action | Added for automation. A function that takes the `page` object, does the automation you need, then returns `page` again. | ✔️ |
322
+ | wait_selector | Wait for a specific css selector to be in a specific state. | ✔️ |
323
+ | wait_selector_state | The state to wait for the selector given with `wait_selector`. _Default state is `attached`._ | ✔️ |
324
+ | google_search | Enabled by default, Scrapling will set the referer header to be as if this request came from a Google search for this website's domain name. | ✔️ |
325
+ | extra_headers | A dictionary of extra headers to add to the request. The referer set by the `google_search` argument takes priority over the referer set here if used together. | ✔️ |
326
+ | hide_canvas | Add random noise to canvas operations to prevent fingerprinting. | ✔️ |
327
+ | disable_webgl | Disables WebGL and WebGL 2.0 support entirely. | ✔️ |
328
+ | stealth | Enables stealth mode, always check the documentation to see what stealth mode does currently. | ✔️ |
329
+ | cdp_url | Instead of launching a new browser instance, connect to this CDP URL to control real browsers/NSTBrowser through CDP. | ✔️ |
330
+ | nstbrowser_mode | Enables NSTBrowser mode, **it have to be used with `cdp_url` argument or it will get completely ignored.** | ✔️ |
331
+ | nstbrowser_config | The config you want to send with requests to the NSTBrowser. _If left empty, Scrapling defaults to an optimized NSTBrowser's docker browserless config._ | ✔️ |
332
+
333
+ </details>
334
+
335
+ This list isn't final so expect a lot more additions and flexibility to be added in the next versions!
336
+
337
+ ## Advanced Parsing Features
338
+ ### Smart Navigation
339
+ ```python
340
+ >>> quote.tag
341
+ 'div'
342
+
343
+ >>> quote.parent
344
+ <data='<div class="col-md-8"> <div class="quote...' parent='<div class="row"> <div class="col-md-8">...'>
345
+
346
+ >>> quote.parent.tag
347
+ 'div'
348
+
349
+ >>> quote.children
350
+ [<data='<span class="text" itemprop="text">“The...' parent='<div class="quote" itemscope itemtype="h...'>,
351
+ <data='<span>by <small class="author" itemprop=...' parent='<div class="quote" itemscope itemtype="h...'>,
352
+ <data='<div class="tags"> Tags: <meta class="ke...' parent='<div class="quote" itemscope itemtype="h...'>]
353
+
354
+ >>> quote.siblings
355
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
356
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
357
+ ...]
358
+
359
+ >>> quote.next # gets the next element, the same logic applies to `quote.previous`
360
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>
361
+
362
+ >>> quote.children.css_first(".author::text")
363
+ 'Albert Einstein'
364
+
365
+ >>> quote.has_class('quote')
366
+ True
367
+
368
+ # Generate new selectors for any element
369
+ >>> quote.generate_css_selector
370
+ 'body > div > div:nth-of-type(2) > div > div'
371
+
372
+ # Test these selectors on your favorite browser or reuse them again in the library's methods!
373
+ >>> quote.generate_xpath_selector
374
+ '//body/div/div[2]/div/div'
375
+ ```
376
+ If your case needs more than the element's parent, you can iterate over the whole ancestors' tree of any element like below
377
+ ```python
378
+ for ancestor in quote.iterancestors():
379
+ # do something with it...
380
+ ```
381
+ 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:
382
+ ```python
383
+ >>> quote.find_ancestor(lambda ancestor: ancestor.has_class('row'))
384
+ <data='<div class="row"> <div class="col-md-8">...' parent='<div class="container"> <div class="row...'>
385
+ ```
386
+
387
+ ### Content-based Selection & Finding Similar Elements
388
+ You can select elements by their text content in multiple ways, here's a full example on another website:
389
+ ```python
390
+ >>> page = Fetcher().get('https://books.toscrape.com/index.html').adaptor
391
+
392
+ >>> page.find_by_text('Tipping the Velvet') # Find the first element whose text fully matches this text
393
+ <data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>
394
+
395
+ >>> page.find_by_text('Tipping the Velvet', first_match=False) # Get all matches if there are more
396
+ [<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>]
397
+
398
+ >>> page.find_by_regex(r'£[\d\.]+') # Get the first element that its text content matches my price regex
399
+ <data='<p class="price_color">£51.77</p>' parent='<div class="product_price"> <p class="pr...'>
400
+
401
+ >>> page.find_by_regex(r'£[\d\.]+', first_match=False) # Get all elements that matches my price regex
402
+ [<data='<p class="price_color">£51.77</p>' parent='<div class="product_price"> <p class="pr...'>,
403
+ <data='<p class="price_color">£53.74</p>' parent='<div class="product_price"> <p class="pr...'>,
404
+ <data='<p class="price_color">£50.10</p>' parent='<div class="product_price"> <p class="pr...'>,
405
+ <data='<p class="price_color">£47.82</p>' parent='<div class="product_price"> <p class="pr...'>,
406
+ ...]
407
+ ```
408
+ Find all elements that are similar to the current element in location and attributes
409
+ ```python
410
+ # For this case, ignore the 'title' attribute while matching
411
+ >>> page.find_by_text('Tipping the Velvet').find_similar(ignore_attributes=['title'])
412
+ [<data='<a href="catalogue/a-light-in-the-attic_...' parent='<h3><a href="catalogue/a-light-in-the-at...'>,
413
+ <data='<a href="catalogue/soumission_998/index....' parent='<h3><a href="catalogue/soumission_998/in...'>,
414
+ <data='<a href="catalogue/sharp-objects_997/ind...' parent='<h3><a href="catalogue/sharp-objects_997...'>,
415
+ ...]
416
+
417
+ # You will notice that the number of elements is 19 not 20 because the current element is not included.
418
+ >>> len(page.find_by_text('Tipping the Velvet').find_similar(ignore_attributes=['title']))
419
+ 19
420
+
421
+ # Get the `href` attribute from all similar elements
422
+ >>> [element.attrib['href'] for element in page.find_by_text('Tipping the Velvet').find_similar(ignore_attributes=['title'])]
423
+ ['catalogue/a-light-in-the-attic_1000/index.html',
424
+ 'catalogue/soumission_998/index.html',
425
+ 'catalogue/sharp-objects_997/index.html',
426
+ ...]
427
+ ```
428
+ 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
429
+ ```python
430
+ >>> for product in page.find_by_text('Tipping the Velvet').parent.parent.find_similar():
431
+ print({
432
+ "name": product.css_first('h3 a::text'),
433
+ "price": product.css_first('.price_color').re_first(r'[\d\.]+'),
434
+ "stock": product.css('.availability::text')[-1].clean()
435
+ })
436
+ {'name': 'A Light in the ...', 'price': '51.77', 'stock': 'In stock'}
437
+ {'name': 'Soumission', 'price': '50.10', 'stock': 'In stock'}
438
+ {'name': 'Sharp Objects', 'price': '47.82', 'stock': 'In stock'}
439
+ ...
440
+ ```
441
+ The [documentation](https://github.com/D4Vinci/Scrapling/tree/main/docs/Examples) will provide more advanced examples.
442
+
443
+ ### Handling Structural Changes
444
+ Let's say you are scraping a page with a structure like this:
445
+ ```html
446
+ <div class="container">
447
+ <section class="products">
448
+ <article class="product" id="p1">
449
+ <h3>Product 1</h3>
450
+ <p class="description">Description 1</p>
451
+ </article>
452
+ <article class="product" id="p2">
453
+ <h3>Product 2</h3>
454
+ <p class="description">Description 2</p>
455
+ </article>
456
+ </section>
457
+ </div>
458
+ ```
459
+ And you want to scrape the first product, the one with the `p1` ID. You will probably write a selector like this
460
+ ```python
461
+ page.css('#p1')
462
+ ```
463
+ When website owners implement structural changes like
464
+ ```html
465
+ <div class="new-container">
466
+ <div class="product-wrapper">
467
+ <section class="products">
468
+ <article class="product new-class" data-id="p1">
469
+ <div class="product-info">
470
+ <h3>Product 1</h3>
471
+ <p class="new-description">Description 1</p>
472
+ </div>
473
+ </article>
474
+ <article class="product new-class" data-id="p2">
475
+ <div class="product-info">
476
+ <h3>Product 2</h3>
477
+ <p class="new-description">Description 2</p>
478
+ </div>
479
+ </article>
480
+ </section>
481
+ </div>
482
+ </div>
483
+ ```
484
+ The selector will no longer function and your code needs maintenance. That's where Scrapling's auto-matching feature comes into play.
485
+
486
+ ```python
487
+ from scrapling import Adaptor
488
+ # Before the change
489
+ page = Adaptor(page_source, url='example.com')
490
+ element = page.css('#p1' auto_save=True)
491
+ if not element: # One day website changes?
492
+ element = page.css('#p1', auto_match=True) # Scrapling still finds it!
493
+ # the rest of the code...
494
+ ```
495
+ > How does the auto-matching work? Check the [FAQs](#-enlightening-questions-and-faqs) section for that and other possible issues while auto-matching.
496
+
497
+ #### Real-World Scenario
498
+ 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 change its design/structure soon, take a copy of its source 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.
499
+
500
+ 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 huh?</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 :)
501
+
502
+ 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.
503
+ Now let's test the same selector in both versions
504
+ ```python
505
+ >> from scrapling import Fetcher
506
+ >> selector = '#hmenus > div:nth-child(1) > ul > li:nth-child(1) > a'
507
+ >> old_url = "https://web.archive.org/web/20100102003420/http://stackoverflow.com/"
508
+ >> new_url = "https://stackoverflow.com/"
509
+ >>
510
+ >> page = Fetcher(automatch_domain='stackoverflow.com').get(old_url, timeout=30).adaptor
511
+ >> element1 = page.css_first(selector, auto_save=True)
512
+ >>
513
+ >> # Same selector but used in the updated website
514
+ >> page = Fetcher(automatch_domain="stackoverflow.com").get(new_url).adaptor
515
+ >> element2 = page.css_first(selector, auto_match=True)
516
+ >>
517
+ >> if element1.text == element2.text:
518
+ ... print('Scrapling found the same element in the old design and the new design!')
519
+ 'Scrapling found the same element in the old design and the new design!'
520
+ ```
521
+ Note that I used a new argument called `automatch_domain`, this is because for Scrapling these are two different URLs, not the website so it isolates their data. To tell Scrapling they are the same website, we then pass the domain we want to use for saving auto-match data for them both so Scrapling doesn't isolate them.
522
+
523
+ In a real-world scenario, the code will be the same 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 :)
524
+
525
+ **Notes:**
526
+ 1. For the two examples above I used one time the `Adaptor` class and the second time the `Fetcher` class just to show you that you can create the `Adaptor` object by yourself if you have the source or fetch the source using any `Fetcher` class then it will create the `Adaptor` object for you on the `.adaptor` property.
527
+ 2. Passing the `auto_save` argument with the `auto_match` argument set to `False` while initializing the Adaptor/Fetcher object will only result in ignoring the `auto_save` argument value and the following warning message
528
+ ```text
529
+ Argument `auto_save` will be ignored because `auto_match` wasn't enabled on initialization. Check docs for more info.
530
+ ```
531
+ This behavior is purely for performance reasons so the database gets created/connected only when you are planning to use the auto-matching features. Same case with the `auto_match` argument.
532
+
533
+ 3. The `auto_match` parameter works only for `Adaptor` instances not `Adaptors` so if you do something like this you will get an error
534
+ ```python
535
+ page.css('body').css('#p1', auto_match=True)
536
+ ```
537
+ because you can't auto-match a whole list, you have to be specific and do something like
538
+ ```python
539
+ page.css_first('body').css('#p1', auto_match=True)
540
+ ```
541
+
542
+ ### Find elements by filters
543
+ Inspired by BeautifulSoup's `find_all` function you can find elements by using `find_all`/`find` methods. Both methods can take multiple types of filters and return all elements in the pages that all these filters apply to.
544
+
545
+ * To be more specific:
546
+ * Any string passed is considered a tag name
547
+ * Any iterable passed like List/Tuple/Set is considered an iterable of tag names.
548
+ * Any dictionary is considered a mapping of HTML element(s) attribute names and attribute values.
549
+ * Any regex patterns passed are used as filters
550
+ * Any functions passed are used as filters
551
+ * Any keyword argument passed is considered as an HTML element attribute with its value.
552
+
553
+ So the way it works is after collecting all passed arguments and keywords, each filter passes its results to the following filter in a waterfall-like filtering system.
554
+ <br/>It filters all elements in the current page/element in the following order:
555
+
556
+ 1. All elements with the passed tag name(s).
557
+ 2. All elements that match all passed attribute(s).
558
+ 3. All elements that match all passed regex patterns.
559
+ 4. All elements that fulfill all passed function(s).
560
+
561
+ Note: 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. **But the order in which you pass the arguments doesn't matter.**
562
+
563
+ Examples to clear any confusion :)
564
+
565
+ ```python
566
+ >> from scrapling import Fetcher
567
+ >> page = Fetcher().get('https://quotes.toscrape.com/').adaptor
568
+ # Find all elements with tag name `div`.
569
+ >> page.find_all('div')
570
+ [<data='<div class="container"> <div class="row...' parent='<body> <div class="container"> <div clas...'>,
571
+ <data='<div class="row header-box"> <div class=...' parent='<div class="container"> <div class="row...'>,
572
+ ...]
573
+
574
+ # Find all div elements with a class that equals `quote`.
575
+ >> page.find_all('div', class_='quote')
576
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
577
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
578
+ ...]
579
+
580
+ # Same as above.
581
+ >> page.find_all('div', {'class': 'quote'})
582
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
583
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
584
+ ...]
585
+
586
+ # Find all elements with a class that equals `quote`.
587
+ >> page.find_all({'class': 'quote'})
588
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
589
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
590
+ ...]
591
+
592
+ # Find all div elements with a class that equals `quote`, and contains the element `.text` which contains the word 'world' in its content.
593
+ >> page.find_all('div', {'class': 'quote'}, lambda e: "world" in e.css_first('.text::text'))
594
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>]
595
+
596
+ # Find all elements that don't have children.
597
+ >> page.find_all(lambda element: len(element.children) > 0)
598
+ [<data='<html lang="en"><head><meta charset="UTF...'>,
599
+ <data='<head><meta charset="UTF-8"><title>Quote...' parent='<html lang="en"><head><meta charset="UTF...'>,
600
+ <data='<body> <div class="container"> <div clas...' parent='<html lang="en"><head><meta charset="UTF...'>,
601
+ ...]
602
+
603
+ # Find all elements that contain the word 'world' in its content.
604
+ >> page.find_all(lambda element: "world" in element.text)
605
+ [<data='<span class="text" itemprop="text">“The...' parent='<div class="quote" itemscope itemtype="h...'>,
606
+ <data='<a class="tag" href="/tag/world/page/1/"...' parent='<div class="tags"> Tags: <meta class="ke...'>]
607
+
608
+ # Find all span elements that match the given regex
609
+ >> page.find_all('span', re.compile(r'world'))
610
+ [<data='<span class="text" itemprop="text">“The...' parent='<div class="quote" itemscope itemtype="h...'>]
611
+
612
+ # Find all div and span elements with class 'quote' (No span elements like that so only div returned)
613
+ >> page.find_all(['div', 'span'], {'class': 'quote'})
614
+ [<data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
615
+ <data='<div class="quote" itemscope itemtype="h...' parent='<div class="col-md-8"> <div class="quote...'>,
616
+ ...]
617
+
618
+ # Mix things up
619
+ >> page.find_all({'itemtype':"http://schema.org/CreativeWork"}, 'div').css('.author::text')
620
+ ['Albert Einstein',
621
+ 'J.K. Rowling',
622
+ ...]
623
+ ```
624
+
625
+ ### Is That All?
626
+ Here's what else you can do with Scrapling:
627
+
628
+ - Accessing the `lxml.etree` object itself of any element directly
629
+ ```python
630
+ >>> quote._root
631
+ <Element div at 0x107f98870>
632
+ ```
633
+ - Saving and retrieving elements manually to auto-match them outside the `css` and the `xpath` methods but you have to set the identifier by yourself.
634
+
635
+ - To save an element to the database:
636
+ ```python
637
+ >>> element = page.find_by_text('Tipping the Velvet', first_match=True)
638
+ >>> page.save(element, 'my_special_element')
639
+ ```
640
+ - Now later when you want to retrieve it and relocate it inside the page with auto-matching, it would be like this
641
+ ```python
642
+ >>> element_dict = page.retrieve('my_special_element')
643
+ >>> page.relocate(element_dict, adaptor_type=True)
644
+ [<data='<a href="catalogue/tipping-the-velvet_99...' parent='<h3><a href="catalogue/tipping-the-velve...'>]
645
+ >>> page.relocate(element_dict, adaptor_type=True).css('::text')
646
+ ['Tipping the Velvet']
647
+ ```
648
+ - if you want to keep it as `lxml.etree` object, leave the `adaptor_type` argument
649
+ ```python
650
+ >>> page.relocate(element_dict)
651
+ [<Element a at 0x105a2a7b0>]
652
+ ```
653
+
654
+ - Filtering results based on a function
655
+ ```python
656
+ # Find all products over $50
657
+ expensive_products = page.css('.product_pod').filter(
658
+ lambda p: float(p.css('.price_color').re_first(r'[\d\.]+')) > 50
659
+ )
660
+ ```
661
+
662
+ - Searching results for the first one that matches a function
663
+ ```python
664
+ # Find all the products with price '53.23'
665
+ page.css('.product_pod').search(
666
+ lambda p: float(p.css('.price_color').re_first(r'[\d\.]+')) == 54.23
667
+ )
668
+ ```
669
+
670
+ - Doing operations on element content is the same as scrapy
671
+ ```python
672
+ quote.re(r'regex_pattern') # Get all strings (TextHandlers) that match the regex pattern
673
+ quote.re_first(r'regex_pattern') # Get the first string (TextHandler) only
674
+ quote.json() # If the content text is jsonable, then convert it to json using `orjson` which is 10x faster than the standard json library and provides more options
675
+ ```
676
+ except that you can do more with them like
677
+ ```python
678
+ quote.re(
679
+ r'regex_pattern',
680
+ replace_entities=True, # Character entity references are replaced by their corresponding character
681
+ clean_match=True, # This will ignore all whitespaces and consecutive spaces while matching
682
+ case_sensitive= False, # Set the regex to ignore letters case while compiling it
683
+ )
684
+ ```
685
+ Hence all of these methods are methods from the `TextHandler` within that contains the text content so the same can be done directly if you call the `.text` property or equivalent selector function.
686
+
687
+
688
+ - Doing operations on the text content itself includes
689
+ - Cleaning the text from any white spaces and replacing consecutive spaces with single space
690
+ ```python
691
+ quote.clean()
692
+ ```
693
+ - You already know about the regex matching and the fast json parsing but did you know that all strings returned from the regex search are actually `TextHandler` objects too? so in cases where you have for example a JS object assigned to a JS variable inside JS code and want to extract it with regex and then convert it to json object, in other libraries, these would be more than 1 line of code but here you can do it in 1 line like this
694
+ ```python
695
+ page.xpath('//script/text()').re_first(r'var dataLayer = (.+);').json()
696
+ ```
697
+ - Sort all characters in the string as if it were a list and return the new string
698
+ ```python
699
+ quote.sort(reverse=False)
700
+ ```
701
+ > To be clear, `TextHandler` is a sub-class of Python's `str` so all normal operations/methods that work with Python strings will work with it.
702
+
703
+ - Any element's attributes are not exactly a dictionary but a sub-class of [mapping](https://docs.python.org/3/glossary.html#term-mapping) called `AttributesHandler` that's read-only so it's faster and string values returned are actually `TextHandler` objects so all operations above can be done on them, standard dictionary operations that don't modify the data, and more :)
704
+ - Unlike standard dictionaries, here you can search by values too and can do partial searches. It might be handy in some cases (returns a generator of matches)
705
+ ```python
706
+ >>> for item in element.attrib.search_values('catalogue', partial=True):
707
+ print(item)
708
+ {'href': 'catalogue/tipping-the-velvet_999/index.html'}
709
+ ```
710
+ - Serialize the current attributes to JSON bytes:
711
+ ```python
712
+ >>> element.attrib.json_string
713
+ b'{"href":"catalogue/tipping-the-velvet_999/index.html","title":"Tipping the Velvet"}'
714
+ ```
715
+ - Converting it to a normal dictionary
716
+ ```python
717
+ >>> dict(element.attrib)
718
+ {'href': 'catalogue/tipping-the-velvet_999/index.html',
719
+ 'title': 'Tipping the Velvet'}
720
+ ```
721
+
722
+ Scrapling is under active development so expect many more features coming soon :)
723
+
724
+ ## More Advanced Usage
725
+
726
+ There are a lot of deep details skipped here to make this as short as possible so to take a deep dive, head to the [docs](https://github.com/D4Vinci/Scrapling/tree/main/docs) section. I will try to keep it updated as possible and add complex examples. There I will explain points like how to write your storage system, write spiders that don't depend on selectors at all, and more...
727
+
728
+ Note that implementing your storage system can be complex as there are some strict rules such as inheriting from the same abstract class, following the singleton design pattern used in other classes, and more. So make sure to read the docs first.
729
+
730
+ To give detailed documentation of the library, it will need a website. I'm trying to rush creating the website, researching new ideas, and adding more features/tests/benchmarks but time is tight with too many spinning plates between work, personal life, and working on Scrapling. But you can help by using the [sponsor button](https://github.com/sponsors/D4Vinci) above :)
731
+
732
+ ## ⚡ Enlightening Questions and FAQs
733
+ This section addresses common questions about Scrapling, please read this section before opening an issue.
734
+
735
+ ### How does auto-matching work?
736
+ 1. You need to get a working selector and run it at least once with methods `css` or `xpath` with the `auto_save` parameter set to `True` before structural changes happen.
737
+ 2. Before returning results for you, Scrapling uses its configured database and saves unique properties about that element.
738
+ 3. Now because everything about the element can be changed or removed, 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:
739
+ 1. The domain of the URL you gave while initializing the first Adaptor object
740
+ 2. The `identifier` parameter you passed to the method while selecting. If you didn't pass one, then the selector string itself will be used as an identifier but remember you will have to use it as an identifier value later when the structure changes and you want to pass the new selector.
741
+
742
+ Together both are used to retrieve the element's unique properties from the database later.
743
+ 4. Now later when you enable the `auto_match` parameter for both the Adaptor instance and the method call. The element properties are retrieved and Scrapling loops over all elements in the page and compares each one's unique properties to the unique properties we already have for this element and a score is calculated for each one.
744
+ 5. The comparison between elements is not exact but more about finding how similar these values are, so everything is taken into consideration 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.
745
+ 6. The score for each element is stored in the table, and in the end, the element(s) with the highest combined similarity scores are returned.
746
+
747
+ ### How does the auto-matching work if I didn't pass a URL while initializing the Adaptor object?
748
+ Not a big problem as it depends on your usage. 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 that you didn't pass the URL parameter while initializing it as well. The save process will overwrite the previous data and auto-matching uses the latest saved properties only.
749
+
750
+ ### If all things about an element can change or get removed, what are the unique properties to be saved?
751
+ For each element, Scrapling will extract:
752
+ - Element tag name, text, attributes (names and values), siblings (tag names only), and path (tag names only).
753
+ - Element's parent tag name, attributes (names and values), and text.
754
+
755
+ ### I have enabled the `auto_save`/`auto_match` parameter while selecting and it got completely ignored with a warning message
756
+ That's because passing the `auto_save`/`auto_match` argument without setting `auto_match` to `True` while initializing the Adaptor object will only result in ignoring the `auto_save`/`auto_match` argument value. This behavior is purely for performance reasons so the database gets created only when you are planning to use the auto-matching features.
757
+
758
+ ### I have done everything as the docs but the auto-matching didn't return anything, what's wrong?
759
+ It could be one of these reasons:
760
+ 1. No data were saved/stored for this element before.
761
+ 2. The selector passed is not the one used while storing element data. The solution is simple
762
+ - Pass the old selector again as an identifier to the method called.
763
+ - Retrieve the element with the retrieve method using the old selector as identifier then save it again with the save method and the new selector as identifier.
764
+ - Start using the identifier argument more often if you are planning to use every new selector from now on.
765
+ 3. The website had some extreme structural changes like a new full design. If this happens a lot with this website, the solution would be to make your code as selector-free as possible using Scrapling features.
766
+
767
+ ### Can Scrapling replace code built on top of BeautifulSoup4?
768
+ Pretty much yeah, almost all features you get from BeautifulSoup can be found or achieved in Scrapling one way or another. In fact, if you see there's a feature in bs4 that is missing in Scrapling, please make a feature request from the issues tab to let me know.
769
+
770
+ ### Can Scrapling replace code built on top of AutoScraper?
771
+ Of course, you can find elements by text/regex, find similar elements in a more reliable way than AutoScraper, and finally save/retrieve elements manually to use later as the model feature in AutoScraper. I have pulled all top articles about AutoScraper from Google and tested Scrapling against examples in them. In all examples, Scrapling got the same results as AutoScraper in much less time.
772
+
773
+ ### Is Scrapling thread-safe?
774
+ Yes, Scrapling instances are thread-safe. Each Adaptor instance maintains its state.
775
+
776
+ ## Sponsors
777
+ [![Capsolver Banner](https://raw.githubusercontent.com/D4Vinci/Scrapling/main/images/CapSolver.png)](https://www.capsolver.com/?utm_source=github&utm_medium=repo&utm_campaign=scraping&utm_term=Scrapling)
778
+
779
+ ## Contributing
780
+ Everybody is invited and welcome to contribute to Scrapling. There is a lot to do!
781
+
782
+ Please read the [contributing file](https://github.com/D4Vinci/Scrapling/blob/main/CONTRIBUTING.md) before doing anything.
783
+
784
+ ## Disclaimer for Scrapling Project
785
+ > [!CAUTION]
786
+ > This library is provided for educational and research purposes only. By using this library, you agree to comply with local and international laws regarding data scraping and privacy. The authors and contributors are not responsible for any misuse of this software. This library should not be used to violate the rights of others, for unethical purposes, or to use data in an unauthorized or illegal manner. Do not use it on any website unless you have permission from the website owner or within their allowed rules like the `robots.txt` file, for example.
787
+
788
+ ## License
789
+ This work is licensed under BSD-3
790
+
791
+ ## Acknowledgments
792
+ This project includes code adapted from:
793
+ - Parsel (BSD License) - Used for [translator](https://github.com/D4Vinci/Scrapling/blob/main/scrapling/translator.py) submodule
794
+
795
+ ## Thanks and References
796
+ - [Daijro](https://github.com/daijro)'s brilliant work on both [BrowserForge](https://github.com/daijro/browserforge) and [Camoufox](https://github.com/daijro/camoufox)
797
+ - [Vinyzu](https://github.com/Vinyzu)'s work on Playwright's mock on [Botright](https://github.com/Vinyzu/Botright)
798
+ - [brotector](https://github.com/kaliiiiiiiiii/brotector)
799
+ - [fakebrowser](https://github.com/kkoooqq/fakebrowser)
800
+ - [rebrowser-patches](https://github.com/rebrowser/rebrowser-patches)
801
+
802
+ ## Known Issues
803
+ - 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 that are in different locations, auto-matching will probably return to you the first element 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.
804
+ - Currently, Scrapling is not compatible with async/await.
805
+
806
+ ---
807
+ <div align="center"><small>Designed & crafted with ❤️ by Karim Shoair.</small></div><br>