From a721073b7c5a05f66bbfc0e8d1fae27495739a10 Mon Sep 17 00:00:00 2001 From: Karim shoair Date: Sun, 13 Oct 2024 23:38:48 +0300 Subject: [PATCH] Going online (first public version) --- .bandit.yml | 5 + .flake8 | 3 + .github/FUNDING.yml | 1 + .github/ISSUE_TEMPLATE/01-bug_report.yml | 82 ++ .github/ISSUE_TEMPLATE/02-feature_request.yml | 19 + .github/ISSUE_TEMPLATE/03-other.yml | 19 + .github/ISSUE_TEMPLATE/config.yml | 1 + .github/PULL_REQUEST_TEMPLATE.md | 53 + .github/workflows/publish.yml | 31 + .github/workflows/tests.yml | 48 + .gitignore | 217 ++--- .pre-commit-config.yaml | 14 + CONTRIBUTING.md | 30 + MANIFEST.in | 5 + README.md | 434 +++++++++ ROADMAP.md | 13 + benchmarks.py | 139 +++ docs/Core/using scrapling custom types.md | 21 + docs/Examples/selectorless_stackoverflow.py | 23 + .../writing storage system.md | 17 + docs/index.md | 2 + images/logo.png | Bin 0 -> 435433 bytes pytest.ini | 2 + scrapling/__init__.py | 10 + scrapling/custom_types.py | 146 +++ scrapling/mixins.py | 74 ++ scrapling/parser.py | 903 ++++++++++++++++++ scrapling/py.typed | 0 scrapling/storage_adaptors.py | 149 +++ scrapling/translator.py | 148 +++ scrapling/utils.py | 164 ++++ setup.cfg | 8 + setup.py | 65 ++ tests/__init__.py | 1 + tests/requirements.txt | 2 + tests/test_all_functions.py | 336 +++++++ tox.ini | 20 + 37 files changed, 3065 insertions(+), 140 deletions(-) create mode 100644 .bandit.yml create mode 100644 .flake8 create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/01-bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/02-feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/03-other.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 benchmarks.py create mode 100644 docs/Core/using scrapling custom types.md create mode 100644 docs/Examples/selectorless_stackoverflow.py create mode 100644 docs/Extending Scrapling/writing storage system.md create mode 100644 docs/index.md create mode 100644 images/logo.png create mode 100644 pytest.ini create mode 100644 scrapling/__init__.py create mode 100644 scrapling/custom_types.py create mode 100644 scrapling/mixins.py create mode 100644 scrapling/parser.py create mode 100644 scrapling/py.typed create mode 100644 scrapling/storage_adaptors.py create mode 100644 scrapling/translator.py create mode 100644 scrapling/utils.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/requirements.txt create mode 100644 tests/test_all_functions.py create mode 100644 tox.ini diff --git a/.bandit.yml b/.bandit.yml new file mode 100644 index 0000000..57a5cc4 --- /dev/null +++ b/.bandit.yml @@ -0,0 +1,5 @@ +skips: +- B101 +- B311 +- B320 +- B410 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5a89b58 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +ignore = E501 # line too long +exclude = .git,__pycache__,docs,.github,build,dist \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d52044e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: D4Vinci \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/01-bug_report.yml b/.github/ISSUE_TEMPLATE/01-bug_report.yml new file mode 100644 index 0000000..2c3d594 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug_report.yml @@ -0,0 +1,82 @@ +name: Bug report +description: Create a bug report to help us address errors in the repository +labels: [bug] +body: + - type: checkboxes + attributes: + label: Have you searched if there an existing issue for this? + description: Please search [existing issues](https://github.com/D4Vinci/Scrapling/labels/bug). + options: + - label: I have searched the existing issues + required: true + + - type: input + attributes: + label: "Python version (python --version)" + placeholder: "Python 3.8" + validations: + required: true + + - type: input + attributes: + label: "Scrapling version (scrapling.__version__)" + placeholder: "0.1" + validations: + required: true + + - type: textarea + attributes: + label: "Dependencies version (pip3 freeze)" + description: > + This is the output of the command `pip3 freeze --all`. Note that the + actual output might be different as compared to the placeholder text. + placeholder: | + cssselect==1.2.0 + lxml==5.3.0 + orjson==3.10.7 + ... + validations: + required: true + + - type: input + attributes: + label: "What's your operating system?" + placeholder: "Windows 10" + validations: + required: true + + - type: dropdown + attributes: + label: 'Are you using a separate virtual environment?' + description: "Please pay attention to this question" + options: + - No + - Yes + default: 0 + validations: + required: true + + - type: textarea + attributes: + label: "Expected behavior" + description: "Describe the behavior you expect. May include images or videos." + validations: + required: true + + - type: textarea + attributes: + label: "Actual behavior (Remember to use `debug` parameter)" + validations: + required: true + + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/02-feature_request.yml b/.github/ISSUE_TEMPLATE/02-feature_request.yml new file mode 100644 index 0000000..8d07e86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-feature_request.yml @@ -0,0 +1,19 @@ +name: Feature request +description: Suggest features, propose improvements, discuss new ideas. +labels: [enhancement] +body: + - type: checkboxes + attributes: + label: Have you searched if there an existing feature request for this? + description: Please search [existing requests](https://github.com/D4Vinci/Scrapling/labels/enhancement). + options: + - label: I have searched the existing requests + required: true + + - type: textarea + attributes: + label: "Feature description" + description: > + This could include new topics or improving any existing features/implementations. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03-other.yml b/.github/ISSUE_TEMPLATE/03-other.yml new file mode 100644 index 0000000..6549352 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03-other.yml @@ -0,0 +1,19 @@ +name: Other +description: Use this for any other issues. PLEASE do not create blank issues +labels: ["awaiting triage"] +body: + - type: textarea + id: issuedescription + attributes: + label: What would you like to share? + description: Provide a clear and concise explanation of your issue. + validations: + required: true + + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this issue? + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..685eed6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,53 @@ + + +## Proposed change + + + +### Type of change: + + + + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New integration (thank you!) +- [ ] New feature (which adds functionality to an existing integration) +- [ ] Deprecation (breaking change to happen in the future) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests +- [ ] Add or change doctests? -- Note: Please avoid changing both code and tests in a single pull request. +- [ ] Documentation change? + +### Additional information + + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: +- Link to documentation pull request: ** + +### Checklist: +* [ ] I have read [CONTRIBUTING.md](/CONTRIBUTING.md). +* [ ] This pull request is all my own work -- I have not plagiarized. +* [ ] I know that pull requests will not be merged if they fail the automated tests. +* [ ] All new Python files are placed inside an existing directory. +* [ ] All filenames are in all lowercase characters with no spaces or dashes. +* [ ] All functions and variable names follow Python naming conventions. +* [ ] All function parameters and return values are annotated with Python [type hints](https://docs.python.org/3/library/typing.html). +* [ ] All functions have doc-strings. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0183379 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish Python 🐍 distributions πŸ“¦ to PyPI + +on: + release: + types: [created] + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions πŸ“¦ to PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" # Latest available Python version + + - name: Upgrade pip + run: python3 -m pip install --upgrade pip + + - name: Install build + run: python3 -m pip install --upgrade build twine setuptools + + - name: Build a binary wheel and a source tarball + run: python3 -m build --sdist --wheel --outdir dist/ + + - name: Publish distribution πŸ“¦ to PyPI + uses: pypa/gh-action-pypi-publish@release/v1.10.3 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ec7980f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,48 @@ +name: Tests +on: [push, pull_request] + +concurrency: + group: ${{github.workflow}}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.6" + env: + TOXENV: py + - python-version: "3.7" + env: + TOXENV: py + - python-version: "3.8" + env: + TOXENV: py + - python-version: "3.9" + env: + TOXENV: py + - python-version: "3.10" + env: + TOXENV: py + - python-version: "3.11" + env: + TOXENV: py + - python-version: "3.12" + env: + TOXENV: py + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Run tests + env: ${{ matrix.env }} + run: | + pip install -U tox + tox diff --git a/.gitignore b/.gitignore index 82f9275..c7b104c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,128 +1,25 @@ -# Byte-compiled / optimized / DLL files +# cached files __pycache__/ *.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* .cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +.DS_Store +*~ +.*.sw[po] +.build +.ve .env +.pytest +.benchmarks +.bootstrap +.appveyor.token +*.bak + +# installation package +*.egg-info/ +dist/ +build/ + +# environments .venv env/ venv/ @@ -130,33 +27,73 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject +# C extensions +*.so -# Rope project settings -.ropeproject +# pycharm +.idea/ -# mkdocs documentation -/site +# vscode +*.code-workspace + +# Packages +*.egg +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +wheelhouse +develop-eggs +.installed.cfg +lib +lib64 +venv*/ +.venv*/ +pyvenv*/ +pip-wheel-metadata/ +poetry.lock + +# Installer logs +pip-log.txt # mypy .mypy_cache/ .dmypy.json dmypy.json +mypy.ini -# Pyre type checker -.pyre/ +# test caches +.tox/ +.pytest_cache/ +.coverage +htmlcov +report.xml +nosetests.xml +coverage.xml -# pytype static type analyzer -.pytype/ +# Translations +*.mo -# Cython debug symbols -cython_debug/ +# Buildout +.mr.developer.cfg -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# IDE project files +.project +.pydevproject +.idea +*.iml +*.komodoproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build +public/ +web/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0b22d4a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/PyCQA/bandit + rev: 1.7.8 + hooks: + - id: bandit + args: [-r, -c, .bandit.yml] +- repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..131846b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing to Scrapling +Everybody is invited and welcome to contribute to Scrapling. Smaller changes have a better chance to get included in a timely manner. Adding unit tests for new features or test cases for bugs you've fixed help us to ensure that the Pull Request (PR) is fine. + +There is a lot to do... +- If you are not a developer perhaps you would like to help with the [documentation](/docs)? +- If you are a developer, most of the features I'm planning to add in the future are moved to [roadmap file](/ROADMAP.md) so consider reading it. + +Scrapling includes a comprehensive test suite which can be executed with pytest: +```bash +$ pytest +=============================== test session starts =============================== +platform darwin -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0 +rootdir: //Scrapling +configfile: pytest.ini +plugins: cov-5.0.0, anyio-4.6.0 +collected 16 items + +tests/test_all_functions.py ................ [100%] + +=============================== 16 passed in 0.22s ================================ +``` +Also, consider setting `debug` to `True` while initializing the Adaptor object so it's easier to know what's happening in the background. + +### The process is straight-forward. + + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1) + - Fork Scrapling [git repository](https://github.com/D4Vinci/Scrapling). + - Make your changes. + - Ensure tests work. + - Create a Pull Request against the [**dev**](https://github.com/D4Vinci/Scraplin/tree/dev) branch of Scrapling. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..98d462a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include scrapling/py.typed + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0fa7b8 --- /dev/null +++ b/README.md @@ -0,0 +1,434 @@ +# πŸ•·οΈ Scrapling: Lightning-Fast, Adaptive Web Scraping for Python +[![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/) [![License](https://img.shields.io/badge/License-BSD--3-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) + +Dealing with failing web scrapers due to website changes? Meet Scrapling. + +Scrapling is a high-performance, intelligent web scraping library for Python that automatically adapts to website changes while significantly outperforming popular alternatives. Whether you're a beginner or an expert, Scrapling provides powerful features while maintaining simplicity. + +```python +from scrapling import Adaptor + +# Scrape data that survives website changes +page = Adaptor(html, auto_match=True) +products = page.css('.product', auto_save=True) +# Later, even if selectors change: +products = page.css('.product', auto_match=True) # Still finds them! +``` + +## Key Features + +### Adaptive Scraping +- πŸ”„ **Smart Element Tracking**: Locate previously identified elements after website structure changes, using an intelligent similarity system and integrated storage. +- 🎯 **Flexible Querying**: Use CSS selectors, XPath, text search, or regex - chain them however you want! +- πŸ” **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). +- 🧠 **Smart Content Scraping**: Extract data from multiple websites without specific selectors using its powerful features. + +### Performance +- πŸš€ **Lightning Fast**: Built from the ground up with performance in mind, outperforming most popular Python scraping libraries (outperforming BeautifulSoup by up to 237x in our tests). +- πŸ”‹ **Memory Efficient**: Optimized data structures for minimal memory footprint. +- ⚑ **Fast JSON serialization**: 10x faster JSON serialization than the standard json library with more options. + +### Developing Experience +- πŸ› οΈ **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). +- 🧬 **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. +- πŸ“ **Automatic Selector Generation**: Create robust CSS/XPath selectors for any element. +- πŸ”Œ **Scrapy-Compatible API**: Familiar methods and similar pseudo-elements for Scrapy users. +- πŸ“˜ **Type hints**: Complete type coverage for better IDE support and fewer bugs. + +## Getting Started + +Let's walk through a basic example that demonstrates small group of Scrapling's core features: + +```python +import requests +from scrapling import Adaptor + +# Fetch a web page +url = 'https://quotes.toscrape.com/' +response = requests.get(url) + +# Create an Adaptor instance +page = Adaptor(response.text, url=url) +# Get all strings in the full page +page.get_all_text(ignore_tags=('script', 'style')) + +# Get all quotes, any of these methods will return a list of strings (TextHandlers) +quotes = page.css('.quote .text::text') # CSS selector +quotes = page.xpath('//span[@class="text"]/text()') # XPath +quotes = page.css('.quote').css('.text::text') # Chained selectors +quotes = [element.text for element in page.css('.quote').css('.text')] # Slower than bulk query above + +# Get the first quote element +quote = page.css('.quote').first # or [0] or .get() + +# Working with elements +quote.html_content # Inner HTML +quote.prettify() # Prettified version of Inner HTML +quote.attrib # Element attributes +quote.path # DOM path to element (List) +``` +To keep it simple, all methods can be chained on top of each other as long as you are chaining methods that return an element (It's called an `Adaptor` object) or a List of Adaptors (It's called `Adaptors` object) + +### Installation +Scrapling is a breeze to get started with - We only require at least Python 3.6 to work and the rest of the requirements are installed automatically with the package. +```bash +# Using pip +pip install scrapling + +# Or the latest from GitHub +pip install git+https://github.com/D4Vinci/Scrapling.git@master +``` + +## Performance + +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. +Here are benchmarks comparing Scrapling to popular Python libraries in two tests. + +### Text Extraction Speed Test (5000 nested elements). + +| # | Library | Time (ms) | vs Scrapling | +|---|:-----------------:|:---------:|:------------:| +| 1 | Scrapling | 5.44 | 1.0x | +| 2 | Parsel/Scrapy | 5.53 | 1.017x | +| 3 | Raw Lxml | 6.76 | 1.243x | +| 4 | PyQuery | 21.96 | 4.037x | +| 5 | Selectolax | 67.12 | 12.338x | +| 6 | BS4 with Lxml | 1307.03 | 240.263x | +| 7 | MechanicalSoup | 1322.64 | 243.132x | +| 8 | BS4 with html5lib | 3373.75 | 620.175x | + +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. + +### Extraction By Text Speed Test + +| Library | Time (ms) | vs Scrapling | +|:-----------:|:---------:|:------------:| +| Scrapling | 2.51 | 1.0x | +| AutoScraper | 11.41 | 4.546x | + +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 same task. + +> All benchmarks' results are an average of 100 runs. See our [benchmarks.py](/benchmarks.py) for methodology and to run your comparisons. + +## Advanced Features +### Smart Navigation +```python +>>> quote.tag +'div' + +>>> quote.parent +
...'> + +>>> quote.parent.tag +'div' + +>>> quote.children +[β€œThe...' parent='
, + Tags: