Skip to main content

buildaspider

A simple, configurable web crawler written in Python.

Designed to be a jumping off point for:

  • understanding and implementing your own crawler

  • parsing markup with bs4

  • working with requests

While its aims are more educational than industrial, it may still be suitable for crawling sites of moderate size (<1000 unique pages).

Written such that it can either be used as-is for small sites, or extended for any number of crawling applications.

buildaspider is intended as a platform to learn to build tools for your own quality assurance purposes.

Installation

Option 1:

pip install buildaspider

Option 2:

git clone git@github.com:joedougherty/buildaspider.git
cd buildaspider/
python3 setup.py install

Example Config File

A config file is required. In addition to the sample given below, you can find an example file in examples/cgf.ini.

[buildaspider]

; login = true
; In order to programatically login, uncomment the line above and ensure login = true
;
; You will also need to ensure that:
;   + the username line is uncommented and set correctly
;   + the password line is uncommented and set correctly
;   + the login_url line is uncommented and set correctly

; username = <USERNAME>
; password = <PASSWORD>
; login_url = http://example.com/login

; Absolute path to directory containing per-run logs
; log_dir = /path/to/logs

; Literal URLs to visit -- there must be at least one!
seed_urls =
    http://httpbin.org/

; List of regex patterns to include
include_patterns =
    httpbin.org

; List of regex patterns to exclude
exclude_patterns =
    ^#$
    ^javascript

max_num_retries = 5

Basic Usage

Once the config file is created and ready to go, it is time to create a Spider instance.

from buildaspider import Spider


myspider = Spider(
    '/path/to/cfg.ini',
    # These are the default settings
    max_workers=8,
    time_format="%Y-%m-%d_%H:%M",
)

myspider.weave()

This will start the web crawling process, beginning with the URLs specified in seed_urls in the config file.

Logging

By default, each run generates four logs:

  • status log

  • broken links log

  • checked links log

  • exception links log

The implementation lives in the setup_logging method of the Spider base class:

def setup_logging(self):
    now = datetime.now().strftime(self.time_format)

    logging.basicConfig(
        filename=os.path.join(self.cfg.log_dir, f"spider_{now}.log"),
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    self.status_logger = logging.getLogger(__name__)

    self.broken_links_logpath = os.path.join(
        self.cfg.log_dir, f"broken_links_{now}.log"
    )
    self.checked_links_logpath = os.path.join(
        self.cfg.log_dir, f"checked_links_{now}.log"
    )
    self.exception_links_logpath = os.path.join(
        self.cfg.log_dir, f"exception_links_{now}.log"
    )

There are three rudimentary methods provided that write to each of the above logs:

  • log_checked_link

  • log_broken_link

  • log_exception_link

For example:

def log_checked_link(self, link):
    append_line_to_log(self.checked_links_logpath, f'{link}')

This can be overridden to extend logging capabilities.

These methods can also can be overriden to trigger custom behavior when:

  • a link is checked

  • a broken link is found

  • a link that threw an exception is found

Beyond Basic Usage

Adding the Ability to Login

You can extend the functionality of buildaspider by inheriting from the Spider class and overriding methods.

This is how you implement the ability for your spider to programmatically login.

Here’s the documentation from the base Spider class:

def login(self):
    # If your session doesn't require logging in, you can leave this method unimplemented.
    #
    # Otherwise, this method needs to return an instance of `requests.Session`.
    #
    # A new session can be obtained by calling `mint_new_session()`.
    #
    raise NotImplementedError("You'll need to implement the login method.")

Here’s an example of a fleshed-out login method to POST credentials (as obtained from the config file) to the login_url. (For more details on logging in with requests see: https://pybit.es/requests-session.html.)

from buildaspider import Spider, mint_new_session, FailedLoginError


class MySpider(Spider):
    def login(self):
        new_session = mint_new_session()

        login_payload = {
            'username': self.cfg.username,
            'password': self.cfg.password,
        }

        response = new_session.post(self.cfg.login_url, data=login_payload)

        if response.status_code != 200:
            raise FailedLoginError("Login Failed :(")

        return response



myspider = MySpider('/path/to/cfg.ini')

myspider.weave()

Providing Custom Functionality by Attaching to Event Hooks

There are a few events that occur during the crawling process that you may want to attach some additional functionality to.

There are pre-visit and post-visit methods you can override/extend.

Event

Method

link visit is about to begin

.pre_visit_hook()

link visit is about to end

.post_visit_hook()

a link has been marked as checked

.log_checked_link()

a link has been marked as broken

.log_broken_link()

a link has been marked as causing an exception

.log_exception_link()

crawling is complete

.cleanup()

Spider.pre_visit_hook() provides the ability to run code when .visit() is called. Code specified in .pre_visit_hook() will execute prior to library-provided functionality in .visit().

Spider.post_visit_hook() provides the ability to run code right before .visit() finishes.

The overridden methods .pre_visit_hook() and .post_visit_hook() ought to pass in link in order to keep the current link in scope and available as a variable with that name.

You may choose to store visited links in some custom container:

custom_visited_links = list()

def pre_visit_hook(self, link):
    # The `link` being referenced here
    # is the link about to be visited
    custom_visited_links.append(link)

NOTE: this provides direct access to the current Link object in scope.

A safe strategy is to make a copy of the current Link using deepcopy.

from copy import deepcopy


custom_visited_links = list()


def pre_visit_hook(self, link):
    current_link_copy = deepcopy(link)
    custom_visited_links.append(current_link_copy)

Extending/Overriding Pre-Defined Events

By default, broken links are logged to the location specified by self.broken_links_logpath.

We can see this in the Spider class:

def log_broken_link(self, link):
    append_line_to_log(self.broken_links_logpath, f'{link} :: {link.http_code}')

What if you want to extend (not merely override) the functionality of .log_broken_link()?

def log_broken_link(self, link):
    super().log_broken_link(link)
    # You've now retained the original functionality
    # by running the method as defined on the parent instance

    # Perhaps now you want to:
    #   + cache this value?
    #   + run some action(s) as a result of this event firing?
    #   + ???

Running the Test Suite

NOTE: You will need to ensure that the log_dir config file field is set correctly before you run the test suite.

cd tests/
pytest

Additional Resources

Official Retry Documentation

https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#module-urllib3.util.retry

Advanced usage of Python requests - timeouts, retries, hooks

https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/#retry-on-failure

Python stdlib Logging: basicConfig

https://docs.python.org/3.8/library/logging.html#logging.basicConfig

BFS / FIFO Queue

https://en.wikipedia.org/wiki/Breadth-first_search#Pseudocode

Python: A quick introduction to the concurrent.futures module

http://masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html

Using Python Requests on a Page Behind a Login

https://pybit.es/requests-session.html

The Offical collections.deque Documentation

https://docs.python.org/3.8/library/collections.html#collections.deque

Metadata

Release files for buildaspider 0.9.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for buildaspider 0.9.3
File Size Uploaded Provenance Actions
buildaspider-0.9.3.tar.gz 12.4 kB Without provenance Details for buildaspider-0.9.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for buildaspider 0.9.3
File Interpreter ABI Platform Actions
buildaspider-0.9.3-py3-none-any.whl Python 3 none any Details for buildaspider-0.9.3-py3-none-any.whl

Total release size: 22.2 kB

Release files / buildaspider-0.9.3.tar.gz

Download URL buildaspider-0.9.3.tar.gz
Size 12.4 kB
Tags Source
SHA-256 checksum
How to use checksums
d7c116a6131e74ab94c6a782eebf159c7630890716c11d722fddcc817499bc51
BLAKE2b-256 checksum
How to use checksums
734347ef3174c928f40c3f86c9ca4878fd99dae9a695c7fda37151851d977468
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.10 CPython/2.7.17 Linux/4.15.0-115-generic

Release files / buildaspider-0.9.3-py3-none-any.whl

Download URL buildaspider-0.9.3-py3-none-any.whl
Size 9.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c2302371991f7dabfc63c487541a405dace2c1b01931e7ecf1d7ecb19e19076
BLAKE2b-256 checksum
How to use checksums
047b30c0e197087a831654ca7168a5cf8bd5fe84bbcdf4cb69e3ebbf6ed391bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.0.10 CPython/2.7.17 Linux/4.15.0-115-generic

Security for buildaspider 0.9.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page