scrapeflow-py 0.1.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {scrapeflow_py-0.1.2/scrapeflow_py.egg-info → scrapeflow_py-0.3.0}/PKG-INFO +425 -25
  2. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/README.md +421 -24
  3. scrapeflow_py-0.3.0/examples/authenticated_quotes_example.py +77 -0
  4. scrapeflow_py-0.3.0/examples/hybrid_extraction_example.py +68 -0
  5. scrapeflow_py-0.3.0/examples/llm_extraction_example.py +73 -0
  6. scrapeflow_py-0.3.0/examples/session_persistence_example.py +73 -0
  7. scrapeflow_py-0.3.0/examples/specification_driven_example.py +80 -0
  8. scrapeflow_py-0.3.0/requirements.txt +7 -0
  9. scrapeflow_py-0.3.0/scrapeflow/__init__.py +87 -0
  10. scrapeflow_py-0.3.0/scrapeflow/browser_runtime.py +106 -0
  11. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/config.py +36 -0
  12. scrapeflow_py-0.3.0/scrapeflow/content_utils.py +49 -0
  13. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/engine.py +112 -98
  14. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/exceptions.py +6 -0
  15. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/extractors.py +13 -10
  16. scrapeflow_py-0.3.0/scrapeflow/llm_extract.py +184 -0
  17. scrapeflow_py-0.3.0/scrapeflow/mcp_backend.py +230 -0
  18. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/monitoring.py +54 -3
  19. scrapeflow_py-0.3.0/scrapeflow/pagination.py +81 -0
  20. scrapeflow_py-0.3.0/scrapeflow/ports.py +71 -0
  21. scrapeflow_py-0.3.0/scrapeflow/registry.py +189 -0
  22. scrapeflow_py-0.3.0/scrapeflow/robots.py +113 -0
  23. scrapeflow_py-0.3.0/scrapeflow/schema_library.py +49 -0
  24. scrapeflow_py-0.3.0/scrapeflow/specifications.py +320 -0
  25. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/workflow.py +5 -49
  26. scrapeflow_py-0.3.0/scrapeflow/workflow_executor.py +49 -0
  27. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0/scrapeflow_py.egg-info}/PKG-INFO +437 -25
  28. scrapeflow_py-0.3.0/scrapeflow_py.egg-info/SOURCES.txt +50 -0
  29. scrapeflow_py-0.3.0/scrapeflow_py.egg-info/requires.txt +6 -0
  30. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/setup.py +4 -1
  31. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/tests/test_config.py +13 -0
  32. scrapeflow_py-0.3.0/tests/test_content_utils.py +37 -0
  33. scrapeflow_py-0.3.0/tests/test_engine_orchestration.py +125 -0
  34. scrapeflow_py-0.3.0/tests/test_hybrid_extractor.py +44 -0
  35. scrapeflow_py-0.3.0/tests/test_public_api_compat.py +18 -0
  36. scrapeflow_py-0.3.0/tests/test_registry.py +16 -0
  37. scrapeflow_py-0.3.0/tests/test_robots.py +20 -0
  38. scrapeflow_py-0.3.0/tests/test_specifications.py +54 -0
  39. scrapeflow_py-0.1.2/requirements.txt +0 -4
  40. scrapeflow_py-0.1.2/scrapeflow/__init__.py +0 -30
  41. scrapeflow_py-0.1.2/scrapeflow_py.egg-info/SOURCES.txt +0 -27
  42. scrapeflow_py-0.1.2/scrapeflow_py.egg-info/requires.txt +0 -3
  43. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/LICENSE +0 -0
  44. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/MANIFEST.in +0 -0
  45. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/examples/__init__.py +0 -0
  46. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/examples/advanced_example.py +0 -0
  47. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/examples/basic_usage.py +0 -0
  48. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/examples/workflow_example.py +0 -0
  49. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/pyproject.toml +0 -0
  50. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/anti_detection.py +0 -0
  51. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/rate_limiter.py +0 -0
  52. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow/retry.py +0 -0
  53. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow_py.egg-info/dependency_links.txt +0 -0
  54. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/scrapeflow_py.egg-info/top_level.txt +0 -0
  55. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/setup.cfg +0 -0
  56. {scrapeflow_py-0.1.2 → scrapeflow_py-0.3.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeflow-py
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: An opinionated scraping workflow engine built on Playwright
5
5
  Home-page: https://github.com/irfanalidv/scrapeflow-py
6
6
  Author: Irfan Ali
@@ -20,6 +20,9 @@ License-File: LICENSE
20
20
  Requires-Dist: playwright>=1.40.0
21
21
  Requires-Dist: aiohttp>=3.9.0
22
22
  Requires-Dist: tenacity>=8.2.0
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Requires-Dist: mistralai>=1.0.0
25
+ Requires-Dist: python-dotenv>=1.0.0
23
26
  Dynamic: author
24
27
  Dynamic: classifier
25
28
  Dynamic: description
@@ -35,17 +38,7 @@ Dynamic: summary
35
38
 
36
39
  **An opinionated scraping workflow engine built on Playwright**
37
40
 
38
- [![GitHub](https://img.shields.io/github/license/irfanalidv/scrapeflow-py)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
39
-
40
- [![PyPI](https://img.shields.io/pypi/v/scrapeflow-py)](https://pypi.org/project/scrapeflow-py/)
41
-
42
- [![PyPI Downloads](https://static.pepy.tech/personalized-badge/scrapeflow-py?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/scrapeflow-py)
43
-
44
- [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/)
45
-
46
- [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/)
47
-
48
- [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/irfanalidv/scrapeflow-py)
41
+ [![GitHub](https://img.shields.io/github/license/irfanalidv/scrapeflow-py)](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) [![PyPI](https://img.shields.io/pypi/v/scrapeflow-py)](https://pypi.org/project/scrapeflow-py/) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/scrapeflow-py?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/scrapeflow-py) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/) [![Playwright](https://img.shields.io/badge/Playwright-1.40%2B-green)](https://playwright.dev/) [![Status](https://img.shields.io/badge/status-active-success)](https://github.com/irfanalidv/scrapeflow-py)
49
42
 
50
43
  ---
51
44
 
@@ -53,6 +46,17 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
53
46
 
54
47
  ## 🚀 Features
55
48
 
49
+ - **📋 Specification-Driven Extraction**: Declarative Pydantic models define fields, types, and validation—decouple field definitions from page structure
50
+ - **🤖 robots.txt Compliance**: Built-in robots.txt parsing and enforcement; ethical crawling by design
51
+ - **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
52
+ - **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
53
+ - **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
54
+ - **🔌 MCP Extensibility**: Pluggable backends for Scrapy MCP Server, Playwright MCP, or LLM-based semantic extraction
55
+ - **🤖 Mistral LLM Extraction**: Generate schemas from natural language, extract without selectors—uses `MISTRAL_API_KEY`
56
+ - **🔄 Hybrid Extraction**: Selector-first, LLM fallback when validation fails—self-healing spiders
57
+ - **🛡️ Selector Fallback Chain**: Try multiple selectors per field (`[".price", ".cost"]`) for resilience
58
+ - **🫙 Session Persistence**: Save/load cookies and localStorage via `save_storage_state()` and `storage_state_path`
59
+ - **🧹 Content Cleaning**: Strip scripts/styles before LLM extraction for better accuracy and token usage
56
60
  - **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
57
61
  - **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
58
62
  - **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
@@ -62,6 +66,10 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
62
66
  - **🔧 Error Handling**: Comprehensive error classification and recovery
63
67
  - **📝 Type Hints**: Full type support for better IDE experience
64
68
 
69
+ <p align="center">
70
+ <img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
71
+ </p>
72
+
65
73
  ## 📦 Installation
66
74
 
67
75
  ```bash
@@ -370,6 +378,132 @@ print(f"💼 Found {len(quotes)} quotes across pages")
370
378
  💼 Found 20 quotes across pages
371
379
  ```
372
380
 
381
+ ### Use Case 5: LLM-Powered Extraction (No Selectors)
382
+
383
+ **Real-world scenario:** Extract structured data using Mistral AI—no CSS selectors, just natural language field descriptions. Ideal for pages with inconsistent markup or when you want semantic understanding.
384
+
385
+ **Requires:** `MISTRAL_API_KEY` in `.env` or environment.
386
+
387
+ ```python
388
+ import asyncio
389
+ import os
390
+
391
+ try:
392
+ from dotenv import load_dotenv
393
+ load_dotenv()
394
+ except ImportError:
395
+ pass
396
+
397
+ from scrapeflow import ScrapeFlow, create_mcp_backend
398
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
399
+ from scrapeflow.llm_extract import generate_schema_from_prompt, extract_with_schema
400
+
401
+ async def run_llm_extraction():
402
+ if not os.environ.get("MISTRAL_API_KEY"):
403
+ print("Set MISTRAL_API_KEY in .env to run this example")
404
+ return
405
+
406
+ config = ScrapeFlowConfig(
407
+ ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
408
+ )
409
+
410
+ async with ScrapeFlow(config) as scraper:
411
+ await scraper.navigate("https://quotes.toscrape.com/")
412
+
413
+ # Option 1: Generate schema from natural language
414
+ schema = generate_schema_from_prompt(
415
+ "Extract quote text, author name, and list of tags"
416
+ )
417
+
418
+ # Option 2: Extract using schema + page content (no selectors!)
419
+ content = await scraper.page.evaluate("() => document.body.innerText")
420
+ data = extract_with_schema(
421
+ content,
422
+ schema,
423
+ prompt="Extract all quotes from this page with text, author, and tags",
424
+ )
425
+ print("LLM extracted:", data)
426
+
427
+ # Option 3: MistralLLMBackend—field descriptions only
428
+ backend = create_mcp_backend("mistral")
429
+ semantic_data = await backend.extract_with_semantics(
430
+ scraper.page,
431
+ field_descriptions={
432
+ "quote": "The quote text",
433
+ "author": "Author name",
434
+ "tags": "Comma-separated tags",
435
+ },
436
+ context="Extract the first quote on the page",
437
+ )
438
+ print("Semantic extraction:", semantic_data)
439
+
440
+ asyncio.run(run_llm_extraction())
441
+ ```
442
+
443
+ **Real Output:**
444
+
445
+ ```
446
+ Generated schema: {'type': 'object', 'properties': {'quote_text': {...}, 'author_name': {...}, 'tags': {...}}, 'required': ['quote_text', 'author_name', 'tags']}
447
+
448
+ LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
449
+
450
+ Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
451
+ ```
452
+
453
+ **What you get:** Schema generation from prompts, extraction without brittle selectors, and semantic understanding of page content—powerful for dynamic or inconsistent sites.
454
+
455
+ ### Use Case 6: Login + Session Persistence
456
+
457
+ **Real-world scenario:** Log in once, save cookies, then reuse the session on future runs—no re-login needed.
458
+
459
+ ```python
460
+ import asyncio
461
+ import os
462
+ from scrapeflow import ScrapeFlow, SpecificationExtractor, get_registry, register_quotes_login_handler
463
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig, BrowserConfig
464
+ from scrapeflow.specifications import FieldSpec, ItemSpec
465
+ from pydantic import BaseModel
466
+
467
+ class QuoteItem(BaseModel):
468
+ text: str
469
+ author: str
470
+
471
+ class QuotesPage(BaseModel):
472
+ quotes: list[QuoteItem]
473
+
474
+ async def run():
475
+ state_file = "/tmp/scrapeflow_session.json"
476
+ schema = {"quotes": ItemSpec(items_selector=".quote", fields={"text": ".text", "author": ".author"})}
477
+
478
+ # Step 1: Login and save (if first run)
479
+ if not os.path.exists(state_file):
480
+ async with ScrapeFlow(ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True))) as scraper:
481
+ register_quotes_login_handler(get_registry())
482
+ handler = get_registry().get_login("quotes_login")
483
+ await scraper.login_with_handler("https://quotes.toscrape.com/login", "admin", "amdin", handler)
484
+ await scraper.save_storage_state(state_file)
485
+
486
+ # Step 2: Use saved session (no login)
487
+ config = ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
488
+ browser=BrowserConfig(storage_state_path=state_file))
489
+ async with ScrapeFlow(config) as scraper:
490
+ await scraper.navigate("https://quotes.toscrape.com/")
491
+ data = await SpecificationExtractor(QuotesPage, schema=schema).extract(scraper.page)
492
+ print(f"Extracted {len(data.quotes)} quotes (authenticated)")
493
+
494
+ asyncio.run(run())
495
+ ```
496
+
497
+ **Real Output:**
498
+
499
+ ```
500
+ Extracted 10 quotes (authenticated)
501
+ ```
502
+
503
+ *(On first run: login + save; on subsequent runs: load session and extract without re-login.)*
504
+
505
+ **What you get:** `login_with_handler`, `save_storage_state`, `storage_state_path`—reusable authenticated sessions.
506
+
373
507
  ## 📚 Documentation
374
508
 
375
509
  ### Configuration
@@ -414,6 +548,63 @@ config = ScrapeFlowConfig(
414
548
  )
415
549
  ```
416
550
 
551
+ ### Specification-Driven Extraction (Pydantic)
552
+
553
+ **Use Case:** Declarative extraction with validation—fields, types, and rules in specs, not fragile XPaths.
554
+
555
+ ```python
556
+ from pydantic import BaseModel
557
+ from scrapeflow import ScrapeFlow, SpecificationExtractor
558
+ from scrapeflow.specifications import FieldSpec, ItemSpec, ProductPriceSpec
559
+
560
+ # Model for list of products
561
+ class BookListing(BaseModel):
562
+ books: list[ProductPriceSpec]
563
+
564
+ # Schema maps fields to selectors
565
+ schema = {
566
+ "books": ItemSpec(
567
+ items_selector="article.product_pod",
568
+ fields={
569
+ "title": FieldSpec(selector="h3 a"),
570
+ "price": FieldSpec(selector=".price_color"),
571
+ "availability": FieldSpec(selector=".instock.availability", default=""),
572
+ "url": FieldSpec(selector="h3 a", type="attribute", attribute="href"),
573
+ },
574
+ )
575
+ }
576
+
577
+ async with ScrapeFlow() as scraper:
578
+ await scraper.navigate("https://books.toscrape.com/")
579
+ extractor = SpecificationExtractor(BookListing, schema=schema)
580
+ # Extract and validate in one step
581
+ data = await extractor.extract(scraper.page)
582
+ for book in data.books:
583
+ print(f"{book.title}: {book.price}")
584
+ ```
585
+
586
+ ### Ethical Crawling & robots.txt
587
+
588
+ **Use Case:** GDPR/CCPA compliance and robots.txt respect built into the specification layer.
589
+
590
+ ```python
591
+ from scrapeflow import ScrapeFlow
592
+ from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
593
+
594
+ config = ScrapeFlowConfig(
595
+ ethical_crawling=EthicalCrawlingConfig(
596
+ respect_robots_txt=True, # Check robots.txt before each request
597
+ user_agent_for_robots="ScrapeFlow",
598
+ anonymize_ip=True, # GDPR: minimize personal data
599
+ data_retention_days=30, # Document retention policy
600
+ )
601
+ )
602
+
603
+ async with ScrapeFlow(config) as scraper:
604
+ # navigate() automatically checks robots.txt
605
+ await scraper.navigate("https://example.com/page")
606
+ ```
607
+
417
608
  ### Anti-Detection
418
609
 
419
610
  **Use Case:** Scraping protected e-commerce sites that block automated access.
@@ -510,6 +701,40 @@ async with ScrapeFlow(config) as scraper:
510
701
  # - Temporary blocks
511
702
  ```
512
703
 
704
+ ### Pagination
705
+
706
+ **Use Case:** Scrape multiple pages (e.g. search results, product listings) with limits.
707
+
708
+ ```python
709
+ import asyncio
710
+ from scrapeflow import ScrapeFlow
711
+ from scrapeflow.config import ScrapeFlowConfig, PaginationConfig
712
+ from scrapeflow.pagination import paginate
713
+ from scrapeflow.registry import get_registry
714
+
715
+ async def extract_quotes(page, context):
716
+ quotes = []
717
+ for i in range(await page.locator(".quote").count()):
718
+ q = page.locator(".quote").nth(i)
719
+ text = await q.locator(".text").text_content()
720
+ author = await q.locator(".author").text_content()
721
+ quotes.append({"text": text or "", "author": author or ""})
722
+ return quotes
723
+
724
+ async def main():
725
+ get_registry().register_pagination("quotes", next_selector="li.next a", has_next="li.next")
726
+ handler = get_registry().get_pagination("quotes")
727
+ config = PaginationConfig(max_pages=3, max_results=25)
728
+
729
+ async with ScrapeFlow(ScrapeFlowConfig()) as scraper:
730
+ async for page_data in paginate(scraper, "https://quotes.toscrape.com/", handler, extract_quotes, config):
731
+ print(f"Page: {len(page_data)} quotes")
732
+ for q in page_data[:2]:
733
+ print(f" - {q['author']}: {q['text'][:40]}...")
734
+
735
+ asyncio.run(main())
736
+ ```
737
+
513
738
  ### Data Extraction
514
739
 
515
740
  **Use Case:** Extracting structured data from [quotes.toscrape.com](https://quotes.toscrape.com/) and [books.toscrape.com](https://books.toscrape.com/).
@@ -740,13 +965,175 @@ asyncio.run(scrape_with_error_handling())
740
965
  ✅ Successfully scraped: Quotes to Scrape
741
966
  ```
742
967
 
968
+ ## 📋 Feature Coverage—Nothing Hidden
969
+
970
+ Every ScrapeFlow feature is demonstrated somewhere. Quick reference:
971
+
972
+ | Feature | Where to See It |
973
+ |---------|-----------------|
974
+ | **Extractors** | Use Case 1, `basic_usage.py`, `advanced_example.py` |
975
+ | **StructuredExtractor** | Use Case 2, `workflow_example.py`, `advanced_example.py` |
976
+ | **SpecificationExtractor** | Use Case 6, Doc section, `specification_driven_example.py` |
977
+ | **HybridExtractor** | `hybrid_extraction_example.py` |
978
+ | **FieldSpec, ItemSpec** | Doc section, `specification_driven_example.py`, `hybrid_extraction_example.py` |
979
+ | **Selector fallback** `[".a", ".b"]` | `hybrid_extraction_example.py` |
980
+ | **LLM extraction** | Use Case 5, `llm_extraction_example.py` |
981
+ | **Login** | Use Case 6, `authenticated_quotes_example.py`, `session_persistence_example.py` |
982
+ | **Session persistence** | Use Case 6, `session_persistence_example.py` |
983
+ | **Registry, LoginHandler** | `authenticated_quotes_example.py`, `specification_driven_example.py` |
984
+ | **Workflow** | Use Case 2, `workflow_example.py` |
985
+ | **Rate limit, Retry** | Use Cases 1, 4, `basic_usage.py`, `advanced_example.py` |
986
+ | **Anti-detection** | Use Case 3, `advanced_example.py` |
987
+ | **Ethical crawling, robots.txt** | `specification_driven_example.py`, `authenticated_quotes_example.py` |
988
+ | **Pagination** | Doc section, `scrapeflow.pagination.paginate`, `PaginationConfig` |
989
+ | **Content cleaning** | Used internally by MistralLLMBackend; `scrapeflow.content_utils` |
990
+
743
991
  ## 🎨 Complete Examples
744
992
 
745
- Check out the `examples/` directory for more examples:
993
+ Check out the `examples/` directory—each example showcases specific features:
994
+
995
+ | Example | Features Demonstrated |
996
+ |---------|------------------------|
997
+ | `basic_usage.py` | ScrapeFlow, Extractor, rate limit, retry, metrics |
998
+ | `workflow_example.py` | Workflow, steps, on_success/on_error, StructuredExtractor |
999
+ | `advanced_example.py` | Anti-detection, rate limit, retry, StructuredExtractor |
1000
+ | `specification_driven_example.py` | SpecificationExtractor, FieldSpec, ItemSpec, ethical crawling, registry |
1001
+ | `authenticated_quotes_example.py` | Login, `login_with_handler`, registry, LoginHandler |
1002
+ | `llm_extraction_example.py` | Mistral LLM, `generate_schema_from_prompt`, `extract_with_schema`, MistralLLMBackend *(requires `MISTRAL_API_KEY`)* |
1003
+ | `hybrid_extraction_example.py` | HybridExtractor, selector fallback chain `[".a", ".b"]`, LLM fallback |
1004
+ | `session_persistence_example.py` | `save_storage_state`, `storage_state_path`, skip login on reuse |
1005
+
1006
+ ### Real Outputs from Examples
1007
+
1008
+ Outputs below are from running each example against live sites (quotes.toscrape.com, books.toscrape.com).
1009
+
1010
+ **`basic_usage.py`**
1011
+
1012
+ ```
1013
+ Page title: Quotes to Scrape
1014
+
1015
+ Extracted 5 quotes:
1016
+
1017
+ 1. "The world as we have created it is a process of our thinkin...
1018
+ Author: Albert Einstein
1019
+ Tags: change, deep-thoughts, thinking, world
1020
+
1021
+ 2. "It is our choices, Harry, that show what we truly are, far ...
1022
+ Author: J.K. Rowling
1023
+ Tags: abilities, choices
1024
+
1025
+ 3. "There are only two ways to live your life. One is as though...
1026
+ Author: Albert Einstein
1027
+ Tags: inspirational, life, live, miracle, miracles
1028
+
1029
+ 4. "The person, be it gentleman or lady, who has not pleasure i...
1030
+ Author: Jane Austen
1031
+ Tags: aliteracy, books, classic, humor
1032
+
1033
+ 5. "Imperfection is beauty, madness is genius and it's better t...
1034
+ Author: Marilyn Monroe
1035
+ Tags: be-yourself, inspirational
1036
+
1037
+ 📊 Metrics:
1038
+ Success rate: 100.00%
1039
+ Total requests: 1
1040
+ Average response time: 1.72s
1041
+ ```
1042
+
1043
+ **`advanced_example.py`**
1044
+
1045
+ ```
1046
+ 📝 Extracted quotes:
1047
+
1048
+ 1. "The world as we have created it is a process of our thinkin...
1049
+ Author: Albert Einstein
1050
+ Tags: change, deep-thoughts, thinking, world
1051
+
1052
+ 2. "It is our choices, Harry, that show what we truly are, far ...
1053
+ Author: J.K. Rowling
1054
+ Tags: abilities, choices
1055
+
1056
+ 3. "There are only two ways to live your life. One is as though...
1057
+ Author: Albert Einstein
1058
+ Tags: inspirational, life, live, miracle, miracles
1059
+
1060
+ 4. "The person, be it gentleman or lady, who has not pleasure i...
1061
+ Author: Jane Austen
1062
+ Tags: aliteracy, books, classic, humor
1063
+
1064
+ 5. "Imperfection is beauty, madness is genius and it's better t...
1065
+ Author: Marilyn Monroe
1066
+ Tags: be-yourself, inspirational
1067
+
1068
+ Scraping completed:
1069
+ Success rate: 100.00%
1070
+ Total requests: 1
1071
+ Retries: 0
1072
+ ```
1073
+
1074
+ **`workflow_example.py`**
1075
+
1076
+ ```
1077
+ 💾 Saving 20 books to database...
1078
+ - A Light in the ...... - £51.77
1079
+ - Tipping the Velvet... - £53.74
1080
+ - Soumission... - £50.10
1081
+
1082
+ ✅ Workflow completed successfully!
1083
+ 📚 Extracted 20 books
1084
+
1085
+ 📊 Metrics:
1086
+ Total requests: 3
1087
+ Success rate: 100.00%
1088
+ Average response time: 5.85s
1089
+ ```
1090
+
1091
+ **`specification_driven_example.py`**
1092
+
1093
+ ```
1094
+ Extracted 10 items (validated via Pydantic)
1095
+ 1. "The world as we have created it is a process of o... | Albert Einstein
1096
+ 2. "It is our choices, Harry, that show what we truly... | J.K. Rowling
1097
+ 3. "There are only two ways to live your life. One is... | Albert Einstein
1098
+
1099
+ Metrics: 1 requests, 100.0% success
1100
+ ```
746
1101
 
747
- - `basic_usage.py` - Simple scraping example
748
- - `workflow_example.py` - Workflow orchestration
749
- - `advanced_example.py` - All features combined
1102
+ **`authenticated_quotes_example.py`**
1103
+
1104
+ ```
1105
+ Authenticated login: True
1106
+ Quotes extracted: 10
1107
+ 1. Albert Einstein: "The world as we have created it is a process of our thinking. It cannot be chan...
1108
+ 2. J.K. Rowling: "It is our choices, Harry, that show what we truly are, far more than our abilit...
1109
+ 3. Albert Einstein: "There are only two ways to live your life. One is as though nothing is a miracl...
1110
+ ```
1111
+
1112
+ **`hybrid_extraction_example.py`**
1113
+
1114
+ ```
1115
+ Extracted (selector or LLM):
1116
+ Title: A Light in the Attic
1117
+ Price: £51.77
1118
+ Availability: In stock (22 available)
1119
+ ```
1120
+
1121
+ **`llm_extraction_example.py`** *(requires MISTRAL_API_KEY)*
1122
+
1123
+ ```
1124
+ Generated schema: {'type': 'object', 'properties': {'quote_text': {'type': 'string', 'description': 'The text of the quote'}, 'author_name': {'type': 'string', 'description': 'The name of the author of the quote'}, 'tags': {'type': 'array', 'description': 'A list of tags associated with the quote'}}, 'required': ['quote_text', 'author_name', 'tags']}
1125
+
1126
+ LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
1127
+
1128
+ Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
1129
+ ```
1130
+
1131
+ **`session_persistence_example.py`**
1132
+
1133
+ ```
1134
+ Session saved to /tmp/scrapeflow_quotes_session.json
1135
+ Loaded session: extracted 10 quotes (authenticated)
1136
+ ```
750
1137
 
751
1138
  ### Example: Complete Book Scraper with All Features
752
1139
 
@@ -838,17 +1225,30 @@ ScrapeFlow is built with a modular architecture:
838
1225
 
839
1226
  ```
840
1227
  scrapeflow/
841
- ├── engine.py # Main ScrapeFlow engine
842
- ├── workflow.py # Workflow definition and execution
843
- ├── config.py # Configuration classes
844
- ├── anti_detection.py # Anti-detection utilities
845
- ├── rate_limiter.py # Rate limiting implementation
846
- ├── retry.py # Retry logic and error classification
847
- ├── monitoring.py # Metrics and logging
848
- ├── extractors.py # Data extraction utilities
849
- └── exceptions.py # Custom exceptions
1228
+ ├── engine.py # Main ScrapeFlow engine
1229
+ ├── ports.py # Protocols for dependency inversion
1230
+ ├── browser_runtime.py # Playwright runtime adapter
1231
+ ├── workflow.py # Workflow definition entities
1232
+ ├── workflow_executor.py # Workflow execution service
1233
+ ├── config.py # Configuration (EthicalCrawling, Pagination, etc.)
1234
+ ├── specifications.py # SpecificationExtractor, HybridExtractor, FieldSpec
1235
+ ├── schema_library.py # Reusable schema definitions
1236
+ ├── extractors.py # Extractor, StructuredExtractor
1237
+ ├── llm_extract.py # Mistral LLM schema + extraction
1238
+ ├── content_utils.py # HTML cleaning for LLM
1239
+ ├── mcp_backend.py # MCPBackend, MistralLLMBackend
1240
+ ├── pagination.py # paginate() helper
1241
+ ├── robots.py # robots.txt parsing and enforcement
1242
+ ├── registry.py # Selectors, login handlers, pagination
1243
+ ├── anti_detection.py # Stealth mode, user agent rotation
1244
+ ├── rate_limiter.py # Rate limiting implementation
1245
+ ├── retry.py # Retry logic and error classification
1246
+ ├── monitoring.py # Metrics, logging, alerting
1247
+ └── exceptions.py # Custom exceptions
850
1248
  ```
851
1249
 
1250
+ For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
1251
+
852
1252
  ## 🤝 Contributing
853
1253
 
854
1254
  Contributions are welcome! Please feel free to submit a Pull Request.