silk-scraper 0.1.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 (29) hide show
  1. silk_scraper-0.1.0/LICENSE +19 -0
  2. silk_scraper-0.1.0/PKG-INFO +647 -0
  3. silk_scraper-0.1.0/README.md +615 -0
  4. silk_scraper-0.1.0/pyproject.toml +69 -0
  5. silk_scraper-0.1.0/src/silk/__init__.py +14 -0
  6. silk_scraper-0.1.0/src/silk/actions/__init__.py +48 -0
  7. silk_scraper-0.1.0/src/silk/actions/base.py +419 -0
  8. silk_scraper-0.1.0/src/silk/actions/composition.py +347 -0
  9. silk_scraper-0.1.0/src/silk/actions/context.py +268 -0
  10. silk_scraper-0.1.0/src/silk/actions/decorators.py +246 -0
  11. silk_scraper-0.1.0/src/silk/actions/extraction.py +654 -0
  12. silk_scraper-0.1.0/src/silk/actions/flow.py +458 -0
  13. silk_scraper-0.1.0/src/silk/actions/input.py +827 -0
  14. silk_scraper-0.1.0/src/silk/actions/navigation.py +470 -0
  15. silk_scraper-0.1.0/src/silk/actions/py.typed +0 -0
  16. silk_scraper-0.1.0/src/silk/browsers/__init__.py +18 -0
  17. silk_scraper-0.1.0/src/silk/browsers/context.py +310 -0
  18. silk_scraper-0.1.0/src/silk/browsers/driver.py +716 -0
  19. silk_scraper-0.1.0/src/silk/browsers/driver_factory.py +74 -0
  20. silk_scraper-0.1.0/src/silk/browsers/drivers/playwright.py +1378 -0
  21. silk_scraper-0.1.0/src/silk/browsers/element.py +433 -0
  22. silk_scraper-0.1.0/src/silk/browsers/manager.py +262 -0
  23. silk_scraper-0.1.0/src/silk/browsers/py.typed +0 -0
  24. silk_scraper-0.1.0/src/silk/models/actions.py +0 -0
  25. silk_scraper-0.1.0/src/silk/models/browser.py +252 -0
  26. silk_scraper-0.1.0/src/silk/py.typed +0 -0
  27. silk_scraper-0.1.0/src/silk/selectors/__init__.py +7 -0
  28. silk_scraper-0.1.0/src/silk/selectors/py.typed +0 -0
  29. silk_scraper-0.1.0/src/silk/selectors/selector.py +143 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2025 Galad Dirie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
19
+ OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,647 @@
1
+ Metadata-Version: 2.3
2
+ Name: silk-scraper
3
+ Version: 0.1.0
4
+ Summary: A flexible browser automation library with support for multiple drivers
5
+ License: MIT
6
+ Author: Galad Dirie
7
+ Author-email: hello@galad.ca
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
18
+ Provides-Extra: all
19
+ Provides-Extra: patchright
20
+ Provides-Extra: playwright
21
+ Requires-Dist: expression (>=5.6.0,<6.0.0)
22
+ Requires-Dist: patchright (>=1.51.0,<2.0.0) ; extra == "all"
23
+ Requires-Dist: patchright (>=1.51.0,<2.0.0) ; extra == "patchright"
24
+ Requires-Dist: playwright (>=1.51.0,<2.0.0)
25
+ Requires-Dist: playwright (>=1.51.0,<2.0.0) ; extra == "all"
26
+ Requires-Dist: playwright (>=1.51.0,<2.0.0) ; extra == "playwright"
27
+ Requires-Dist: pydantic (>=2.11.2,<3.0.0)
28
+ Requires-Dist: pydantic-settings (>=2.8.1,<3.0.0)
29
+ Project-URL: Bug Tracker, https://github.com/galdirie/silk-py/issues
30
+ Project-URL: Homepage, https://github.com/galdirie/silk-py
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Silk
34
+
35
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.1.0-blue.svg)](https://pypi.org/project/silk-scraper/)
36
+ [![Python versions](https://img.shields.io/badge/python-3.10%2B-blue)](https://pypi.org/project/silk-scraper/)
37
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
38
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
39
+ [![Type check: mypy](https://img.shields.io/badge/type%20check-mypy-blue)](https://github.com/python/mypy)
40
+
41
+ **Silk** is a functional web scraping framework for Python that reimagines how web automation should work. Built around composable "Actions" and the [Expression](https://github.com/dbrattli/Expression) library, Silk enables you to write elegant, maintainable, and resilient web scrapers with true functional programming patterns.
42
+
43
+ Unlike traditional scraping libraries, Silk embraces Railway-Oriented Programming for robust error handling, uses immutable data structures for predictability, and provides an expressive, composable API that makes even complex scraping workflows readable and maintainable.
44
+
45
+ ## Why Silk?
46
+
47
+ Traditional web scraping approaches in Python often lead to complex, brittle code that's difficult to maintain. Silk solves these common challenges:
48
+
49
+ - **No More Callback Hell**: Replace nested try/except blocks with elegant Railway-Oriented Programming
50
+ - **Resilient Scraping**: Built-in retry mechanisms, fallback selectors, and error recovery
51
+ - **Composable Actions**: Chain operations with intuitive operators (`>>`, `&`, `|`) for cleaner code
52
+ - **Type-Safe**: Full typing support with Mypy and Pydantic for fewer runtime errors
53
+ - **Browser Agnostic**: Same API for Playwright, Selenium, or any other browser automation tool
54
+ - **Parallelization Made Easy**: Run operations concurrently with the `&` operator
55
+
56
+ Whether you're building a small data collection script or a large-scale scraping system, Silk's functional approach scales with your needs while keeping your codebase clean and maintainable.
57
+
58
+ ## Features
59
+
60
+ - **Purely Functional Design**: Built on Expression library for robust functional programming in Python
61
+ - **Immutable Data Structures**: Uses immutable collections for thread-safety and predictability
62
+ - **Railway-Oriented Programming**: Elegant error handling with Result types
63
+ - **Functional & Composable API**: Build pipelines with intuitive operators (`>>`, `&`, `|`)
64
+ - **Browser Abstraction**: Works with Playwright, Selenium, or any other browser automation tool
65
+ - **Resilient Selectors**: Fallback mechanisms to handle changing website structures
66
+ - **Type Safety**: Leverages Pydantic, Mypy and Python's type hints for static type checking
67
+ - **Parallel Execution**: Easy concurrent scraping with functional composition
68
+
69
+ ## Installation
70
+
71
+ You can install Silk with your preferred browser driver:
72
+
73
+ ```bash
74
+ # Base installation (no drivers)
75
+ pip install silk-scraper
76
+
77
+ # With Playwright support
78
+ pip install silk-scraper[playwright]
79
+
80
+ # With Selenium support
81
+ pip install silk-scraper[selenium]
82
+
83
+ # With Puppeteer support
84
+ pip install silk-scraper[puppeteer]
85
+
86
+ # With all drivers
87
+ pip install silk-scraper[all]
88
+ ```
89
+
90
+ ## Quick Start
91
+
92
+ ### Basic Example
93
+
94
+ Here's a minimal example to get you started with Silk:
95
+
96
+ ```python
97
+ import asyncio
98
+ from silk.actions.navigation import Navigate
99
+ from silk.actions.extraction import GetText
100
+ from silk.browsers.manager import BrowserManager
101
+
102
+ async def main():
103
+ # Create a browser manager (defaults to Playwright)
104
+ async with BrowserManager() as manager:
105
+ # Define a simple scraping pipeline
106
+ pipeline = (
107
+ Navigate("https://example.com")
108
+ >> GetText("h1")
109
+ )
110
+
111
+ # Execute the pipeline
112
+ result = await manager.execute_action(pipeline)
113
+
114
+ if result.is_ok():
115
+ print(f"Page title: {result.unwrap()}")
116
+ else:
117
+ print(f"Error: {result.error}")
118
+
119
+ if __name__ == "__main__":
120
+ asyncio.run(main())
121
+ ```
122
+
123
+ ### Configuring the Browser
124
+
125
+ Silk supports different browser drivers. You can configure them like this:
126
+
127
+ ```python
128
+ from silk.models.browser import BrowserOptions
129
+ from silk.browsers.manager import BrowserManager
130
+
131
+ # Configure browser options
132
+ options = BrowserOptions(
133
+ headless=False, # Set to False to see the browser UI
134
+ browser_name="chromium", # Choose "chromium", "firefox", or "webkit"
135
+ slow_mo=50, # Slow down operations by 50ms (useful for debugging)
136
+ viewport={"width": 1280, "height": 800}
137
+ )
138
+
139
+ # Create a manager with specific driver and options
140
+ manager = BrowserManager(driver_type="playwright", default_options=options)
141
+ ```
142
+
143
+ ### Creating Custom Actions
144
+
145
+ You can easily create your own actions for reusable scraping logic:
146
+
147
+ ```python
148
+ from silk.actions.base import Action
149
+ from silk.actions.decorators import action
150
+ from expression.core import Ok, Error
151
+ from silk.models.browser import ActionContext
152
+
153
+ @action()
154
+ async def extract_price(context, selector):
155
+ """Extract and parse a price from the page"""
156
+ page_result = await context.get_page()
157
+ if page_result.is_error():
158
+ return page_result
159
+
160
+ page = page_result.unwrap()
161
+ element_result = await page.query_selector(selector)
162
+
163
+ if element_result.is_error():
164
+ return Error(f"Element not found: {selector}")
165
+
166
+ element = element_result.unwrap()
167
+ text_result = await element.get_text()
168
+
169
+ if text_result.is_error():
170
+ return text_result
171
+
172
+ text = text_result.unwrap()
173
+
174
+ try:
175
+ # Remove currency symbol and convert to float
176
+ price = float(text.replace('$', '').strip())
177
+ return Ok(price)
178
+ except ValueError:
179
+ return Error(f"Failed to parse price from: {text}")
180
+ ```
181
+
182
+ ## Core Concepts
183
+
184
+ ### Actions
185
+
186
+ The fundamental building block in Silk is the `Action`. An Action represents a pure operation that can be composed with other actions using functional programming patterns. Each Action takes an `ActionContext` and returns a `Result` containing either the operation's result or an error.
187
+
188
+ ```python
189
+ class FindElement(Action[ElementHandle]):
190
+ """Action to find an element on the page"""
191
+
192
+ def __init__(self, selector: str):
193
+ self.selector = selector
194
+
195
+ async def execute(self, context: ActionContext) -> Result[ElementHandle, Exception]:
196
+ try:
197
+ page_result = await context.get_page()
198
+ if page_result.is_error():
199
+ return page_result
200
+
201
+ page = page_result.unwrap()
202
+ return await page.query_selector(self.selector)
203
+ except Exception as e:
204
+ return Error(e)
205
+ ```
206
+
207
+ ### ActionContext
208
+
209
+ The `ActionContext` carries references to the browser, page, and other execution context information. Actions use this context to interact with the browser.
210
+
211
+ ### Result Type
212
+
213
+ Silk uses the `Result[T, E]` type from the Expression library for error handling. Rather than relying on exceptions, actions return `Ok(value)` for success or `Error(exception)` for failures.
214
+
215
+ ### Composition Operators
216
+
217
+ Silk provides powerful operators for composing actions:
218
+
219
+ - **`>>`** (then): Chain actions sequentially
220
+ - **`&`** (and): Run actions in parallel
221
+ - **`|`** (or): Try one action, fall back to another if it fails
222
+
223
+ These operators make it easy to build complex scraping workflows with clear, readable code.
224
+
225
+ ## Detailed Examples
226
+
227
+ ### Handling Complex Selectors
228
+
229
+ Silk provides robust ways to handle changing website structures with selector groups. Selector groups are a collection of selectors that are tried in order until one succeeds.
230
+
231
+ ```python
232
+ from silk.selectors.selector import SelectorGroup, css, xpath
233
+
234
+ # Create a selector group with fallback options
235
+ product_price = SelectorGroup.create(
236
+ "product_price",
237
+ css(".current-price"), # Try this first
238
+ css(".product-price .amount"), # Fall back to this
239
+ xpath("//div[contains(@class, 'price')]//span") # Last resort
240
+ )
241
+
242
+ # Use it in an extraction action
243
+ extract_price = GetText(product_price)
244
+ ```
245
+
246
+ ### Resilient Scraping with Retry and Fallbacks
247
+
248
+ ```python
249
+ from silk.actions.flow import retry, fallback
250
+ from silk.actions.extraction import GetText
251
+ from silk.actions.navigation import Navigate
252
+
253
+ # Retry navigation up to 3 times with 2s delay
254
+ resilient_navigation = retry(
255
+ Navigate("https://example.com"),
256
+ max_attempts=3,
257
+ delay_ms=2000
258
+ )
259
+
260
+ # Try multiple selectors for extracting data
261
+ extract_title = fallback(
262
+ GetText(".main-title"),
263
+ GetText("h1.title"),
264
+ GetText("#product-name")
265
+ )
266
+
267
+ # Combine into a pipeline
268
+ pipeline = resilient_navigation >> extract_title
269
+ ```
270
+
271
+ ### Parallel Extraction
272
+
273
+ Extract multiple pieces of information at once:
274
+
275
+ ```python
276
+ from silk.actions.composition import parallel
277
+ from silk.actions.extraction import GetText, GetAttribute
278
+
279
+ # Extract product details in parallel
280
+ product_details = parallel(
281
+ GetText(".product-name"),
282
+ GetText(".product-price"),
283
+ GetAttribute(".product-image", "src"),
284
+ GetText(".product-description")
285
+ )
286
+
287
+ # Use in a pipeline
288
+ pipeline = Navigate(product_url) >> product_details
289
+
290
+ # Results come back as a Block collection
291
+ result = await manager.execute_action(pipeline)
292
+ if result.is_ok():
293
+ [name, price, image_url, description] = result.unwrap()
294
+ print(f"Product: {name}, Price: {price}")
295
+ ```
296
+
297
+ ### Form Filling and Submission
298
+
299
+ ```python
300
+ from silk.actions.input import Fill, Click
301
+ from silk.actions.flow import compose
302
+
303
+ login_action = compose(
304
+ Navigate("https://example.com/login"),
305
+ Fill("#username", "user@example.com"),
306
+ Fill("#password", "password123"),
307
+ Click("button[type='submit']")
308
+ )
309
+ ```
310
+
311
+ ### Handling Dynamic Content
312
+
313
+ ```python
314
+ from silk.actions.flow import wait, loop_until
315
+ from silk.actions.conditions import ElementExists
316
+
317
+ # Wait for dynamic content to load
318
+ wait_for_results = wait(1000) >> ElementExists(".search-results-item")
319
+
320
+ # Loop until a condition is met
321
+ load_all_results = loop_until(
322
+ condition=ElementExists(".no-more-results"),
323
+ body=Click(".load-more-button"),
324
+ max_iterations=10,
325
+ delay_ms=1000
326
+ )
327
+
328
+ # Use in a pipeline
329
+ search_pipeline = (
330
+ Navigate("https://example.com/search?q=example")
331
+ >> wait_for_results
332
+ >> load_all_results
333
+ >> GetText(".search-results-count")
334
+ )
335
+ ```
336
+
337
+ ### Action Decorator for Custom Functions
338
+
339
+ Easily convert any function into a composable Action using the `@action` decorator:
340
+
341
+ ```python
342
+ from silk import action, Ok, Error
343
+
344
+ @action
345
+ async def scroll_to_element(driver, selector, smooth=True):
346
+ """Scrolls the page to bring the element into view"""
347
+ try:
348
+ element = await driver.query_selector(selector)
349
+ await element.scroll_into_view({"behavior": "smooth" if smooth else "auto"})
350
+ return "Element scrolled into view"
351
+ except Exception as e:
352
+ raise e
353
+
354
+ # Use it in a pipeline - the function is now a composable Action!
355
+ pipeline = (
356
+ Navigate(url)
357
+ >> scroll_to_element("#my-element")
358
+ >> extract_text("#my-element")
359
+ )
360
+
361
+ result = await pipeline(browser)
362
+ if result.is_ok():
363
+ print(f"Extracted text after scrolling: {result.unwrap()}")
364
+ ```
365
+
366
+ ## Composable Operations
367
+
368
+ Silk provides intuitive operators for composable scraping:
369
+
370
+ ### Sequential Operations (`>>`)
371
+
372
+ ```python
373
+ # Navigate to a page, then extract the title
374
+ Navigate(url) >> Click(title_selector)
375
+ ```
376
+
377
+ ### Parallel Operations (`&`)
378
+
379
+ ```python
380
+ # Extract name, price, and description in parallel
381
+ # Each action is executed in a new context when using the & operator
382
+ Navigate(url) & Navigate(url2) & Navigate(url3)
383
+ ```
384
+
385
+ ```python
386
+ # Combining parallel and sequential operations
387
+ # Each parallel branch can contain its own chain of sequential actions
388
+ (
389
+ # First website: Get product details
390
+ (Navigate("https://site1.com/product")
391
+ >> Wait(1000)
392
+ >> GetText(".product-name"))
393
+ &
394
+ # Second website: Search and extract first result
395
+ (Navigate("https://site2.com")
396
+ >> Fill("#search-input", "smartphone")
397
+ >> Click("#search-button")
398
+ >> Wait(2000)
399
+ >> GetText(".first-result .name"))
400
+ &
401
+ # Third website: Login and get account info
402
+ (Navigate("https://site3.com/login")
403
+ >> Fill("#username", "user@example.com")
404
+ >> Fill("#password", "password123")
405
+ >> Click(".login-button")
406
+ >> Wait(1500)
407
+ >> GetText(".account-info"))
408
+ )
409
+ # Results are collected as a Block of 3 items, one from each parallel branch
410
+ ```
411
+
412
+ ### Fallback Operations (`|`)
413
+
414
+ ```python
415
+ # Try to extract with one selector, fall back to another if it fails
416
+ GetText(primary_selector) | GetText(fallback_selector)
417
+ ```
418
+
419
+ ## API Reference
420
+
421
+ ### Core Modules
422
+
423
+ - **`silk.actions`**: Core action classes for browser automation
424
+ - **`silk.actions.base`**: Base Action class and core utilities
425
+ - **`silk.actions.navigation`**: Actions for navigating between pages
426
+ - **`silk.actions.extraction`**: Actions for extracting data from pages
427
+ - **`silk.actions.input`**: Actions for interacting with forms and elements
428
+ - **`silk.actions.flow`**: Control flow actions like branch, retry, and loop
429
+ - **`silk.actions.composition`**: Utilities for composing actions (sequence, parallel, pipe)
430
+ - **`silk.actions.decorators`**: Decorators like @action for creating custom actions
431
+
432
+ - **`silk.browsers`**: Browser management and abstraction layer
433
+ - **`silk.browsers.manager`**: BrowserManager for session handling
434
+ - **`silk.browsers.driver`**: Abstract BrowserDriver interface
435
+ - **`silk.browsers.element`**: ElementHandle for working with DOM elements
436
+
437
+ - **`silk.selectors`**: Selector utilities
438
+ - **`silk.selectors.selector`**: Selector and SelectorGroup classes
439
+
440
+ - **`silk.models`**: Data models using Pydantic
441
+ - **`silk.models.browser`**: BrowserOptions, ActionContext, etc.
442
+
443
+ ### Common Action Classes
444
+
445
+ - **Navigation**
446
+ - `Navigate(url)`: Navigate to a URL
447
+ - `Reload()`: Reload the current page
448
+ - `GoBack()`: Navigate back in history
449
+ - `GoForward()`: Navigate forward in history
450
+
451
+ - **Extraction**
452
+ - `Query(selector)`: Find an element
453
+ - `QueryAll(selector)`: Find all matching elements
454
+ - `GetText(selector)`: Extract text from an element
455
+ - `GetAttribute(selector, attribute)`: Get an attribute value
456
+ - `GetHtml(selector, outer=True)`: Get element HTML
457
+ - `ExtractTable(table_selector)`: Extract data from an HTML table
458
+
459
+ - **Input**
460
+ - `Click(target)`: Click an element
461
+ - `DoubleClick(target)`: Double-click an element
462
+ - `Fill(target, text)`: Fill a form field
463
+ - `Type(target, text)`: Type text (alias for Fill)
464
+ - `Select(target, value/text)`: Select an option from a dropdown
465
+ - `MouseMove(target)`: Move the mouse to an element
466
+ - `KeyPress(key, modifiers)`: Press a key or key combination
467
+
468
+ - **Flow Control**
469
+ - `branch(condition, if_true, if_false)`: Conditional branching
470
+ - `loop_until(condition, body, max_iterations)`: Loop until condition is met
471
+ - `retry(action, max_attempts, delay_ms)`: Retry an action on failure
472
+ - `retry_with_backoff(action)`: Retry with exponential backoff
473
+ - `with_timeout(action, timeout_ms)`: Apply a timeout to an action
474
+
475
+ - **Composition**
476
+ - `sequence(*actions)`: Run actions in sequence, collect all results
477
+ - `parallel(*actions)`: Run actions in parallel, collect all results
478
+ - `pipe(*actions)`: Create a pipeline where each action uses the previous result
479
+ - `fallback(*actions)`: Try actions in sequence until one succeeds
480
+ - `compose(*actions)`: Compose actions sequentially, return only the last result
481
+
482
+ For a complete API reference, please see the [API documentation](https://silk-docs.example.com).
483
+
484
+ ## Best Practices
485
+
486
+ ### Error Handling
487
+
488
+ Silk uses Railway-Oriented Programming for error handling. Instead of using try/except, leverage the Result type:
489
+
490
+ ```python
491
+ result = await manager.execute_action(pipeline)
492
+ if result.is_ok():
493
+ data = result.unwrap()
494
+ # Process the data
495
+ else:
496
+ # Handle the error
497
+ error = result.error
498
+ logger.error(f"Scraping failed: {error}")
499
+ ```
500
+
501
+ ### Browser Resources
502
+
503
+ Always use context managers to ensure browser resources are properly cleaned up:
504
+
505
+ ```python
506
+ async with BrowserManager() as manager:
507
+ # Your scraping code here
508
+ pass # Resources automatically cleaned up
509
+ ```
510
+
511
+ ### Selector Resilience
512
+
513
+ Use selector groups for resilient scraping that can handle UI changes:
514
+
515
+ ```python
516
+ # Instead of a single brittle selector:
517
+ extract_price = GetText(".price-box .price")
518
+
519
+ # Use a group with fallbacks:
520
+ price_selector = SelectorGroup.create(
521
+ "price",
522
+ css(".price-box .price"),
523
+ css(".product-price"),
524
+ xpath("//span[contains(@class, 'price')]")
525
+ )
526
+ extract_price = GetText(price_selector)
527
+ ```
528
+
529
+ ### Action Composition
530
+
531
+ Build reusable pipelines through composition instead of large monolithic functions:
532
+
533
+ ```python
534
+ # Define reusable components
535
+ navigate_to_product = Navigate("https://example.com/product")
536
+ extract_product_info = parallel(
537
+ GetText(".product-name"),
538
+ GetText(".product-price"),
539
+ GetText(".product-description")
540
+ )
541
+ extract_related_products = QueryAll(".related-product") >> extract_text_from_elements
542
+
543
+ # Compose them in different ways
544
+ full_scraper = navigate_to_product >> extract_product_info >> extract_related_products
545
+ minimal_scraper = navigate_to_product >> extract_product_info
546
+ ```
547
+
548
+ ### Logging
549
+
550
+ Enable logging to better debug your scraping pipelines:
551
+
552
+ ```python
553
+ import logging
554
+ logging.basicConfig(level=logging.DEBUG)
555
+ logging.getLogger("silk").setLevel(logging.DEBUG)
556
+ ```
557
+
558
+ ## Contributing
559
+
560
+ Contributions to Silk are welcome! Please feel free to submit a Pull Request.
561
+
562
+ ### Development Setup
563
+
564
+ 1. Clone the repository
565
+ ```bash
566
+ git clone https://github.com/yourusername/silk.git
567
+ cd silk
568
+ ```
569
+
570
+ 2. Create a virtual environment
571
+ ```bash
572
+ python -m venv venv
573
+ source venv/bin/activate # On Windows: venv\Scripts\activate
574
+ ```
575
+
576
+ 3. Install development dependencies
577
+ ```bash
578
+ pip install -e ".[dev,test,all]"
579
+ ```
580
+
581
+ 4. Run tests
582
+ ```bash
583
+ pytest
584
+ ```
585
+
586
+ ### Guidelines
587
+
588
+ - Follow PEP 8 and use Black for code formatting
589
+ - Write tests for new features
590
+ - Keep the functional programming paradigm in mind
591
+ - Update documentation with new features
592
+
593
+ ## Acknowledgements
594
+
595
+ Silk builds upon several excellent libraries:
596
+ - [Expression](https://github.com/dbrattli/Expression) for functional programming patterns in Python
597
+ - [Playwright](https://playwright.dev/) and [Selenium](https://www.selenium.dev/) for browser automation
598
+ - [Pydantic](https://pydantic-docs.helpmanual.io/) for data validation and settings management
599
+
600
+ ## Roadmap
601
+
602
+ - [x] Initial release with Playwright support
603
+ - [x] Selenium integration
604
+ - [ ] Puppeteer integration
605
+ - [ ] Data extraction DSL for declarative scraping
606
+ - [ ] Enhanced caching mechanisms
607
+ - [ ] Distributed scraping support
608
+ - [ ] Rate limiting and polite scraping utilities
609
+ - [ ] Integration with popular data processing libraries (Pandas, etc.)
610
+ - [ ] CLI tool for quick scraping tasks
611
+
612
+ ## FAQ
613
+
614
+ ### How does Silk compare to other scraping libraries?
615
+
616
+ Silk differs from traditional scraping libraries like Scrapy, Beautiful Soup, or plain Selenium/Playwright in its functional approach. While these tools focus on imperative code with callbacks and exceptions, Silk embraces functional composition, immutable data structures, and Railway-Oriented Programming for cleaner, more maintainable code.
617
+
618
+ ### Can I use Silk with my existing Playwright/Selenium code?
619
+
620
+ Yes, Silk is designed to work alongside existing browser automation code. You can gradually adopt Silk's patterns while keeping your existing code.
621
+
622
+ ### Is Silk suitable for large-scale scraping?
623
+
624
+ Absolutely. Silk's composable nature makes it excellent for large-scale scraping projects. Its built-in error handling, retries, and parallel execution capabilities are particularly valuable for robust production systems.
625
+
626
+ ### How can I handle authentication in Silk?
627
+
628
+ You can handle authentication like any other browser interaction:
629
+
630
+ ```python
631
+ login_action = compose(
632
+ Navigate("https://example.com/login"),
633
+ Fill("#username", "user@example.com"),
634
+ Fill("#password", "password123"),
635
+ Click("button[type='submit']"),
636
+ wait(1000) # Wait for login to complete
637
+ )
638
+
639
+ # Then use the authenticated context for further actions
640
+ pipeline = login_action >> Navigate("https://example.com/protected-content") >> GetText("#protected-data")
641
+ ```
642
+
643
+ You can also save and reuse authentication state with browser context options.
644
+
645
+ ## License
646
+
647
+ Silk is released under the MIT License. See the [LICENSE](LICENSE) file for details.