philiprehberger-web-scraper
Lightweight web scraper with rate limiting and CSS selectors.
Installation
pip install philiprehberger-web-scraper
Usage
from philiprehberger_web_scraper import Scraper
scraper = Scraper(rate_limit=2.0, retry_attempts=3)
# Fetch a single page
page = scraper.get("https://example.com")
titles = page.select_all("h2.title")
link = page.select_one("a.next")
all_links = page.links()
# Extract data
for el in page.select_all(".product"):
print(el.select_one(".name").text)
print(el.select_one("a").attr("href"))
# Crawl multiple pages
for page in scraper.crawl("https://example.com/blog", max_pages=20):
for article in page.select_all("article"):
print(article.select_one("h2").text)
# Export
Scraper.export_csv(data, "output.csv")
Scraper.export_json(data, "output.json")
Retry with Backoff
Transient HTTP errors (429 and 503) are retried automatically with exponential backoff. Configure the number of attempts and base delay:
from philiprehberger_web_scraper import Scraper
scraper = Scraper(retry_attempts=5, retry_delay=2.0)
page = scraper.get("https://example.com/api")
Response Caching
Cache fetched pages to disk so repeated requests for the same URL skip the network entirely:
from philiprehberger_web_scraper import Scraper, ResponseCache
cache = ResponseCache(cache_dir=".scraper_cache")
scraper = Scraper(cache=cache)
page = scraper.get("https://example.com") # fetches from network
page = scraper.get("https://example.com") # served from disk cache
cache.clear() # remove all cached responses
Cache TTL
Expire cached entries after a number of seconds. Stale files are deleted on the next read so the cache directory does not grow unbounded:
from philiprehberger_web_scraper import ResponseCache
cache = ResponseCache(cache_dir=".scraper_cache", ttl=3600) # 1 hour
Table Extraction
Pull an HTML table into a list of dicts using extract_table(). Use extract_tables() to pull every matching table on the page:
from philiprehberger_web_scraper import Scraper, extract_table, extract_tables
scraper = Scraper()
page = scraper.get("https://example.com/data")
# First matching table
rows = extract_table(page, "table#prices")
# [{"Product": "Widget", "Price": "$9.99"}, ...]
# All tables on the page
all_tables = extract_tables(page, "table")
# [[{...}, ...], [{...}, ...]]
Following Paginated Links
Use follow_links() to crawl paginated content by following a CSS-selected link on each page:
from philiprehberger_web_scraper import Scraper
scraper = Scraper()
for page in scraper.follow_links("https://example.com/page/1", "a.next-page", max_pages=10):
for item in page.select_all(".result"):
print(item.text)
Proxy Rotation
Distribute requests across multiple proxies by passing a list of proxy URLs:
from philiprehberger_web_scraper import Scraper
scraper = Scraper(proxies=[
"http://proxy1:8080",
"http://proxy2:8080",
"http://proxy3:8080",
])
page = scraper.get("https://example.com") # uses proxy1
page = scraper.get("https://example.com/2") # uses proxy2
Rotating User Agents
Rotate the User-Agent header round-robin across requests:
from philiprehberger_web_scraper import Scraper
scraper = Scraper(user_agents=[
"Mozilla/5.0 (X11; Linux x86_64) Firefox/130.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) Safari/17.5",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/127.0.0.0",
])
page = scraper.get("https://example.com") # uses agent #1
page = scraper.get("https://example.com/2") # uses agent #2
Respecting robots.txt
Enable respect_robots to honor each origin's robots.txt. The file is fetched
once per origin and cached; disallowed URLs raise RobotsDisallowedError from
get() (and are skipped automatically during crawl()/follow_links()):
from philiprehberger_web_scraper import Scraper, RobotsDisallowedError
scraper = Scraper(respect_robots=True)
try:
page = scraper.get("https://example.com/private")
except RobotsDisallowedError as e:
print(f"blocked: {e.url}")
Meta Tags
Read <meta> tags (including Open Graph og:* properties) from a page:
from philiprehberger_web_scraper import Scraper
scraper = Scraper()
page = scraper.get("https://example.com")
page.meta("description") # "Example description"
page.meta("og:title") # "Example — Open Graph title"
page.meta_tags() # {"description": "...", "og:title": "...", ...}
Structured Data (JSON-LD)
Parse every <script type="application/ld+json"> block on a page. A block holding a JSON array contributes its items; invalid blocks are skipped:
from philiprehberger_web_scraper import Scraper
page = Scraper().get("https://example.com/product")
for item in page.json_ld():
if isinstance(item, dict) and item.get("@type") == "Product":
print(item["name"])
Selecting Text
Get the stripped text of every element matching a selector:
page.select_text("h2.title") # ["First post", "Second post", ...]
Sitemaps
Read every page URL from an XML sitemap. Sitemap indexes are followed up to max_depth levels, URLs are deduplicated in order, and every fetch goes through get() (rate limiting, retries, robots.txt, caching, proxies, User-Agent rotation). A root document that fails, is malformed, or is not a sitemap raises ValueError; broken nested sitemaps are skipped:
from philiprehberger_web_scraper import Scraper
scraper = Scraper(rate_limit=1.0)
urls = scraper.sitemap_urls("https://example.com/sitemap.xml", max_depth=2)
for page in (scraper.get(u) for u in urls[:10]):
print(page.title)
API
| Function / Class | Description |
|---|---|
Scraper(rate_limit, retry_attempts, retry_delay, timeout, headers, respect_robots, cache, proxies, user_agents) |
Web scraper with rate limiting, retry, caching, proxy rotation, User-Agent rotation, and optional robots.txt enforcement |
Scraper.get(url) |
Fetch a single page with retry and optional caching |
Scraper.get_json(url) |
Fetch JSON from a URL |
Scraper.sitemap_urls(url, *, max_depth=3) |
Return <loc> URLs from an XML sitemap, following nested sitemap indexes; raises ValueError for a failed, malformed, or non-sitemap root |
Scraper.follow_links(start_url, selector, max_pages) |
Follow paginated links matching a CSS selector |
Scraper.crawl(start_url, max_pages, same_domain, next_selector) |
Crawl pages starting from a URL |
Scraper.export_csv(data, path) |
Export list of dicts to CSV |
Scraper.export_json(data, path, indent) |
Export data to JSON |
Page |
A fetched web page with select_one(), select_all(), links(), images(), meta(), meta_tags(), json_ld(), select_text(), and title/text properties |
Page.meta(name) |
Return a <meta> tag's content by name or property (e.g. og:title), or None |
Page.meta_tags() |
Return all <meta> tags as a {name/property: content} dict |
Page.json_ld() |
Parse all application/ld+json script blocks into a list; arrays are flattened and invalid blocks skipped |
Page.select_text(selector) |
Return the stripped text of every element matching a CSS selector |
Element |
Wrapper around a parsed element with text, html, attr(), select_one(), select_all() |
ResponseCache(cache_dir, ttl=None) |
Disk-backed response cache with optional TTL; get(), put(), and clear() methods |
RobotsDisallowedError |
Raised by get() when respect_robots blocks a URL disallowed by robots.txt |
extract_table(page, selector) |
Extract the first matching HTML table into a list of dicts |
extract_tables(page, selector) |
Extract every matching HTML table; returns a list of row-dict lists |
Development
pip install -e .
python -m pytest tests/ -v
Support
If you find this project useful:
License
Metadata
Release files for philiprehberger-web-scraper 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| philiprehberger_web_scraper-0.5.0.tar.gz | 198.1 kB | Without provenance | Details for philiprehberger_web_scraper-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| philiprehberger_web_scraper-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for philiprehberger_web_scraper-0.5.0-py3-none-any.whl |
Total release size: 210.6 kB
Release files / philiprehberger_web_scraper-0.5.0.tar.gz
| Download URL | philiprehberger_web_scraper-0.5.0.tar.gz |
|---|---|
| Size | 198.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
efad676164d1c5b38165e8650ecace5c4d88dd2169ac8032b6e627bf5ab6667d
|
|
BLAKE2b-256 checksum How to use checksums |
ea4ede92e2d5b1ef37e0c5683d5dec66907c2c4e879eaed8761a080c04d35b89
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Release files / philiprehberger_web_scraper-0.5.0-py3-none-any.whl
| Download URL | philiprehberger_web_scraper-0.5.0-py3-none-any.whl |
|---|---|
| Size | 12.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6c951020360d3e31dbfe376b6698c0ae5bfff95bec814c07e13ac54c061e9e55
|
|
BLAKE2b-256 checksum How to use checksums |
e4ffd4c6ca62063f2a0d6106d97070fee5e56d9540b40c3013747a31e9ade413
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Security for philiprehberger-web-scraper 0.5.0
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.