From 768aa06ceb45682eb62e148526b3c5c755be0146 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Thu, 17 Oct 2024 21:37:48 +0800 Subject: [PATCH 01/14] feat(crawler): Enhance stealth and flexibility, improve error handling - Implement playwright_stealth for better bot detection avoidance - Add user simulation and navigator override options - Improve iframe processing and browser selection - Enhance error reporting and debugging capabilities - Optimize image processing and parallel crawling - Add new example for user simulation feature - Added support for including links in Markdown content, by definin g a new flag `include_links_on_markdown` in `crawl` method. --- .gitignore | 2 + CHANGELOG.md | 47 ++ crawl4ai/async_crawler_strategy copy.py | 558 ++++++++++++++++++++++++ crawl4ai/async_crawler_strategy.py | 221 +++++----- crawl4ai/async_webcrawler.py | 1 + crawl4ai/content_scrapping_strategy.py | 36 +- crawl4ai/utils.py | 2 +- docs/examples/quickstart_async.py | 12 + 8 files changed, 777 insertions(+), 102 deletions(-) create mode 100644 crawl4ai/async_crawler_strategy copy.py diff --git a/.gitignore b/.gitignore index 8b8f014c..1793e24c 100644 --- a/.gitignore +++ b/.gitignore @@ -202,5 +202,7 @@ todo.md git_changes.py git_changes.md pypi_build.sh +git_issues.py +git_issues.md .tests/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a377d794..8b0513d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## [v0.3.7] - 2024-10-17 + +### New Features +1. **Enhanced Browser Stealth**: + - Implemented `playwright_stealth` for improved bot detection avoidance. + - Added `StealthConfig` for fine-tuned control over stealth parameters. + +2. **User Simulation**: + - New `simulate_user` option to mimic human-like interactions (mouse movements, clicks, keyboard presses). + +3. **Navigator Override**: + - Added `override_navigator` option to modify navigator properties, further improving bot detection evasion. + +4. **Improved iframe Handling**: + - New `process_iframes` parameter to extract and integrate iframe content into the main page. + +5. **Flexible Browser Selection**: + - Support for choosing between Chromium, Firefox, and WebKit browsers. + +6. **Include Links in Markdown**: + - Added support for including links in Markdown content, by definin g a new flag `include_links_on_markdown` in `crawl` method. + +### Improvements +1. **Better Error Handling**: + - Enhanced error reporting in WebScrappingStrategy with detailed error messages and suggestions. + - Added console message and error logging for better debugging. + +2. **Image Processing Enhancements**: + - Improved image dimension updating and filtering logic. + +3. **Crawling Flexibility**: + - Added support for custom viewport sizes. + - Implemented delayed content retrieval with `delay_before_return_html` parameter. + +4. **Performance Optimization**: + - Adjusted default semaphore count for parallel crawling. + +### Bug Fixes +- Fixed an issue where the HTML content could be empty after processing. + +### Examples +- Added new example `crawl_with_user_simulation()` demonstrating the use of user simulation and navigator override features. + +### Developer Notes +- Refactored code for better maintainability and readability. +- Updated browser launch arguments for improved compatibility and performance. + ## [v0.3.6] - 2024-10-12 ### 1. Improved Crawling Control diff --git a/crawl4ai/async_crawler_strategy copy.py b/crawl4ai/async_crawler_strategy copy.py new file mode 100644 index 00000000..507c9247 --- /dev/null +++ b/crawl4ai/async_crawler_strategy copy.py @@ -0,0 +1,558 @@ +import asyncio +import base64 +import time +from abc import ABC, abstractmethod +from typing import Callable, Dict, Any, List, Optional, Awaitable +import os +from playwright.async_api import async_playwright, Page, Browser, Error +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont +from pathlib import Path +from playwright.async_api import ProxySettings +from pydantic import BaseModel +import hashlib +import json +import uuid +from playwright_stealth import stealth_async + +class AsyncCrawlResponse(BaseModel): + html: str + response_headers: Dict[str, str] + status_code: int + screenshot: Optional[str] = None + get_delayed_content: Optional[Callable[[Optional[float]], Awaitable[str]]] = None + + class Config: + arbitrary_types_allowed = True + +class AsyncCrawlerStrategy(ABC): + @abstractmethod + async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: + pass + + @abstractmethod + async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: + pass + + @abstractmethod + async def take_screenshot(self, url: str) -> str: + pass + + @abstractmethod + def update_user_agent(self, user_agent: str): + pass + + @abstractmethod + def set_hook(self, hook_type: str, hook: Callable): + pass + +class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): + def __init__(self, use_cached_html=False, js_code=None, **kwargs): + self.use_cached_html = use_cached_html + self.user_agent = kwargs.get( + "user_agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) + self.proxy = kwargs.get("proxy") + self.headless = kwargs.get("headless", True) + self.browser_type = kwargs.get("browser_type", "chromium") + self.headers = kwargs.get("headers", {}) + self.sessions = {} + self.session_ttl = 1800 + self.js_code = js_code + self.verbose = kwargs.get("verbose", False) + self.playwright = None + self.browser = None + self.hooks = { + 'on_browser_created': None, + 'on_user_agent_updated': None, + 'on_execution_started': None, + 'before_goto': None, + 'after_goto': None, + 'before_return_html': None, + 'before_retrieve_html': None + } + + async def __aenter__(self): + await self.start() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + async def start(self): + if self.playwright is None: + self.playwright = await async_playwright().start() + if self.browser is None: + browser_args = { + "headless": self.headless, + "args": [ + "--disable-gpu", + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-blink-features=AutomationControlled", + "--disable-infobars", + "--window-position=0,0", + "--ignore-certificate-errors", + "--ignore-certificate-errors-spki-list", + # "--headless=new", # Use the new headless mode + ] + } + + # Add proxy settings if a proxy is specified + if self.proxy: + proxy_settings = ProxySettings(server=self.proxy) + browser_args["proxy"] = proxy_settings + + # Select the appropriate browser based on the browser_type + if self.browser_type == "firefox": + self.browser = await self.playwright.firefox.launch(**browser_args) + elif self.browser_type == "webkit": + self.browser = await self.playwright.webkit.launch(**browser_args) + else: + self.browser = await self.playwright.chromium.launch(**browser_args) + + await self.execute_hook('on_browser_created', self.browser) + + async def close(self): + if self.browser: + await self.browser.close() + self.browser = None + if self.playwright: + await self.playwright.stop() + self.playwright = None + + def __del__(self): + if self.browser or self.playwright: + asyncio.get_event_loop().run_until_complete(self.close()) + + def set_hook(self, hook_type: str, hook: Callable): + if hook_type in self.hooks: + self.hooks[hook_type] = hook + else: + raise ValueError(f"Invalid hook type: {hook_type}") + + async def execute_hook(self, hook_type: str, *args): + hook = self.hooks.get(hook_type) + if hook: + if asyncio.iscoroutinefunction(hook): + return await hook(*args) + else: + return hook(*args) + return args[0] if args else None + + def update_user_agent(self, user_agent: str): + self.user_agent = user_agent + + def set_custom_headers(self, headers: Dict[str, str]): + self.headers = headers + + async def kill_session(self, session_id: str): + if session_id in self.sessions: + context, page, _ = self.sessions[session_id] + await page.close() + await context.close() + del self.sessions[session_id] + + def _cleanup_expired_sessions(self): + current_time = time.time() + expired_sessions = [ + sid for sid, (_, _, last_used) in self.sessions.items() + if current_time - last_used > self.session_ttl + ] + for sid in expired_sessions: + asyncio.create_task(self.kill_session(sid)) + + async def smart_wait(self, page: Page, wait_for: str, timeout: float = 30000): + wait_for = wait_for.strip() + + if wait_for.startswith('js:'): + # Explicitly specified JavaScript + js_code = wait_for[3:].strip() + return await self.csp_compliant_wait(page, js_code, timeout) + elif wait_for.startswith('css:'): + # Explicitly specified CSS selector + css_selector = wait_for[4:].strip() + try: + await page.wait_for_selector(css_selector, timeout=timeout) + except Error as e: + if 'Timeout' in str(e): + raise TimeoutError(f"Timeout after {timeout}ms waiting for selector '{css_selector}'") + else: + raise ValueError(f"Invalid CSS selector: '{css_selector}'") + else: + # Auto-detect based on content + if wait_for.startswith('()') or wait_for.startswith('function'): + # It's likely a JavaScript function + return await self.csp_compliant_wait(page, wait_for, timeout) + else: + # Assume it's a CSS selector first + try: + await page.wait_for_selector(wait_for, timeout=timeout) + except Error as e: + if 'Timeout' in str(e): + raise TimeoutError(f"Timeout after {timeout}ms waiting for selector '{wait_for}'") + else: + # If it's not a timeout error, it might be an invalid selector + # Let's try to evaluate it as a JavaScript function as a fallback + try: + return await self.csp_compliant_wait(page, f"() => {{{wait_for}}}", timeout) + except Error: + raise ValueError(f"Invalid wait_for parameter: '{wait_for}'. " + "It should be either a valid CSS selector, a JavaScript function, " + "or explicitly prefixed with 'js:' or 'css:'.") + + async def csp_compliant_wait(self, page: Page, user_wait_function: str, timeout: float = 30000): + wrapper_js = f""" + async () => {{ + const userFunction = {user_wait_function}; + const startTime = Date.now(); + while (true) {{ + if (await userFunction()) {{ + return true; + }} + if (Date.now() - startTime > {timeout}) {{ + throw new Error('Timeout waiting for condition'); + }} + await new Promise(resolve => setTimeout(resolve, 100)); + }} + }} + """ + + try: + await page.evaluate(wrapper_js) + except TimeoutError: + raise TimeoutError(f"Timeout after {timeout}ms waiting for condition") + except Exception as e: + raise RuntimeError(f"Error in wait condition: {str(e)}") + + async def process_iframes(self, page): + # Find all iframes + iframes = await page.query_selector_all('iframe') + + for i, iframe in enumerate(iframes): + try: + # Add a unique identifier to the iframe + await iframe.evaluate(f'(element) => element.id = "iframe-{i}"') + + # Get the frame associated with this iframe + frame = await iframe.content_frame() + + if frame: + # Wait for the frame to load + await frame.wait_for_load_state('load', timeout=30000) # 30 seconds timeout + + # Extract the content of the iframe's body + iframe_content = await frame.evaluate('() => document.body.innerHTML') + + # Generate a unique class name for this iframe + class_name = f'extracted-iframe-content-{i}' + + # Replace the iframe with a div containing the extracted content + _iframe = iframe_content.replace('`', '\\`') + await page.evaluate(f""" + () => {{ + const iframe = document.getElementById('iframe-{i}'); + const div = document.createElement('div'); + div.innerHTML = `{_iframe}`; + div.className = '{class_name}'; + iframe.replaceWith(div); + }} + """) + else: + print(f"Warning: Could not access content frame for iframe {i}") + except Exception as e: + print(f"Error processing iframe {i}: {str(e)}") + + # Return the page object + return page + + async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: + response_headers = {} + status_code = None + + self._cleanup_expired_sessions() + session_id = kwargs.get("session_id") + if session_id: + context, page, _ = self.sessions.get(session_id, (None, None, None)) + if not context: + context = await self.browser.new_context( + user_agent=self.user_agent, + viewport={"width": 1920, "height": 1080}, + proxy={"server": self.proxy} if self.proxy else None + ) + await context.set_extra_http_headers(self.headers) + page = await context.new_page() + self.sessions[session_id] = (context, page, time.time()) + else: + context = await self.browser.new_context( + user_agent=self.user_agent, + viewport={"width": 1920, "height": 1080}, + proxy={"server": self.proxy} if self.proxy else None + ) + await context.set_extra_http_headers(self.headers) + + if kwargs.get("override_navigator", False): + # Inject scripts to override navigator properties + await context.add_init_script(""" + // Pass the Permissions Test. + const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + ); + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined + }); + window.navigator.chrome = { + runtime: {}, + // Add other properties if necessary + }; + Object.defineProperty(navigator, 'plugins', { + get: () => [1, 2, 3, 4, 5], + }); + Object.defineProperty(navigator, 'languages', { + get: () => ['en-US', 'en'], + }); + Object.defineProperty(document, 'hidden', { + get: () => false + }); + Object.defineProperty(document, 'visibilityState', { + get: () => 'visible' + }); + """) + + page = await context.new_page() + + try: + if self.verbose: + print(f"[LOG] πŸ•ΈοΈ Crawling {url} using AsyncPlaywrightCrawlerStrategy...") + + if self.use_cached_html: + cache_file_path = os.path.join( + Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest() + ) + if os.path.exists(cache_file_path): + html = "" + with open(cache_file_path, "r") as f: + html = f.read() + # retrieve response headers and status code from cache + with open(cache_file_path + ".meta", "r") as f: + meta = json.load(f) + response_headers = meta.get("response_headers", {}) + status_code = meta.get("status_code") + response = AsyncCrawlResponse( + html=html, response_headers=response_headers, status_code=status_code + ) + return response + + if not kwargs.get("js_only", False): + await self.execute_hook('before_goto', page) + + response = await page.goto("about:blank") + await stealth_async(page) + response = await page.goto( + url, wait_until="domcontentloaded", timeout=kwargs.get("page_timeout", 60000) + ) + + # await stealth_async(page) + # response = await page.goto("about:blank") + # await stealth_async(page) + # await page.evaluate(f"window.location.href = '{url}'") + + await self.execute_hook('after_goto', page) + + # Get status code and headers + status_code = response.status + response_headers = response.headers + else: + status_code = 200 + response_headers = {} + + await page.wait_for_selector('body') + await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") + + js_code = kwargs.get("js_code", kwargs.get("js", self.js_code)) + if js_code: + if isinstance(js_code, str): + await page.evaluate(js_code) + elif isinstance(js_code, list): + for js in js_code: + await page.evaluate(js) + + await page.wait_for_load_state('networkidle') + # Check for on execution event + await self.execute_hook('on_execution_started', page) + + if kwargs.get("simulate_user", False): + # Simulate user interactions + await page.mouse.move(100, 100) + await page.mouse.down() + await page.mouse.up() + await page.keyboard.press('ArrowDown') + + # Handle the wait_for parameter + wait_for = kwargs.get("wait_for") + if wait_for: + try: + await self.smart_wait(page, wait_for, timeout=kwargs.get("page_timeout", 60000)) + except Exception as e: + raise RuntimeError(f"Wait condition failed: {str(e)}") + + + + # Update image dimensions + update_image_dimensions_js = """ + () => { + return new Promise((resolve) => { + const filterImage = (img) => { + // Filter out images that are too small + if (img.width < 100 && img.height < 100) return false; + + // Filter out images that are not visible + const rect = img.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return false; + + // Filter out images with certain class names (e.g., icons, thumbnails) + if (img.classList.contains('icon') || img.classList.contains('thumbnail')) return false; + + // Filter out images with certain patterns in their src (e.g., placeholder images) + if (img.src.includes('placeholder') || img.src.includes('icon')) return false; + + return true; + }; + + const images = Array.from(document.querySelectorAll('img')).filter(filterImage); + let imagesLeft = images.length; + + if (imagesLeft === 0) { + resolve(); + return; + } + + const checkImage = (img) => { + if (img.complete && img.naturalWidth !== 0) { + img.setAttribute('width', img.naturalWidth); + img.setAttribute('height', img.naturalHeight); + imagesLeft--; + if (imagesLeft === 0) resolve(); + } + }; + + images.forEach(img => { + checkImage(img); + if (!img.complete) { + img.onload = () => { + checkImage(img); + }; + img.onerror = () => { + imagesLeft--; + if (imagesLeft === 0) resolve(); + }; + } + }); + + // Fallback timeout of 5 seconds + setTimeout(() => resolve(), 5000); + }); + } + """ + await page.evaluate(update_image_dimensions_js) + + # Wait a bit for any onload events to complete + await page.wait_for_timeout(100) + + # Process iframes + if kwargs.get("process_iframes", False): + page = await self.process_iframes(page) + + await self.execute_hook('before_retrieve_html', page) + # Check if delay_before_return_html is set then wait for that time + delay_before_return_html = kwargs.get("delay_before_return_html") + if delay_before_return_html: + await asyncio.sleep(delay_before_return_html) + + html = await page.content() + await self.execute_hook('before_return_html', page, html) + + # Check if kwargs has screenshot=True then take screenshot + screenshot_data = None + if kwargs.get("screenshot"): + screenshot_data = await self.take_screenshot(url) + + if self.verbose: + print(f"[LOG] βœ… Crawled {url} successfully!") + + if self.use_cached_html: + cache_file_path = os.path.join( + Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest() + ) + with open(cache_file_path, "w", encoding="utf-8") as f: + f.write(html) + # store response headers and status code in cache + with open(cache_file_path + ".meta", "w", encoding="utf-8") as f: + json.dump({ + "response_headers": response_headers, + "status_code": status_code + }, f) + + async def get_delayed_content(delay: float = 5.0) -> str: + if self.verbose: + print(f"[LOG] Waiting for {delay} seconds before retrieving content for {url}") + await asyncio.sleep(delay) + return await page.content() + + response = AsyncCrawlResponse( + html=html, + response_headers=response_headers, + status_code=status_code, + screenshot=screenshot_data, + get_delayed_content=get_delayed_content + ) + return response + except Error as e: + raise Error(f"Failed to crawl {url}: {str(e)}") + finally: + if not session_id: + await page.close() + await context.close() + + async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: + semaphore_count = kwargs.get('semaphore_count', 5) # Adjust as needed + semaphore = asyncio.Semaphore(semaphore_count) + + async def crawl_with_semaphore(url): + async with semaphore: + return await self.crawl(url, **kwargs) + + tasks = [crawl_with_semaphore(url) for url in urls] + results = await asyncio.gather(*tasks, return_exceptions=True) + return [result if not isinstance(result, Exception) else str(result) for result in results] + + async def take_screenshot(self, url: str, wait_time=1000) -> str: + async with await self.browser.new_context(user_agent=self.user_agent) as context: + page = await context.new_page() + try: + await page.goto(url, wait_until="domcontentloaded", timeout=30000) + # Wait for a specified time (default is 1 second) + await page.wait_for_timeout(wait_time) + screenshot = await page.screenshot(full_page=True) + return base64.b64encode(screenshot).decode('utf-8') + except Exception as e: + error_message = f"Failed to take screenshot: {str(e)}" + print(error_message) + + # Generate an error image + img = Image.new('RGB', (800, 600), color='black') + draw = ImageDraw.Draw(img) + font = ImageFont.load_default() + draw.text((10, 10), error_message, fill=(255, 255, 255), font=font) + + buffered = BytesIO() + img.save(buffered, format="JPEG") + return base64.b64encode(buffered.getvalue()).decode('utf-8') + finally: + await page.close() + diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index e9699953..d4c94fee 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -1,17 +1,35 @@ import asyncio -import base64, time +import base64 +import time from abc import ABC, abstractmethod from typing import Callable, Dict, Any, List, Optional, Awaitable import os from playwright.async_api import async_playwright, Page, Browser, Error from io import BytesIO from PIL import Image, ImageDraw, ImageFont -from .utils import sanitize_input_encode, calculate_semaphore_count -import json, uuid -import hashlib from pathlib import Path from playwright.async_api import ProxySettings from pydantic import BaseModel +import hashlib +import json +import uuid +from playwright_stealth import StealthConfig, stealth_async + +stealth_config = StealthConfig( + webdriver=True, + chrome_app=True, + chrome_csi=True, + chrome_load_times=True, + chrome_runtime=True, + navigator_languages=True, + navigator_plugins=True, + navigator_permissions=True, + webgl_vendor=True, + outerdimensions=True, + navigator_hardware_concurrency=True, + media_codecs=True, +) + class AsyncCrawlResponse(BaseModel): html: str @@ -47,10 +65,14 @@ class AsyncCrawlerStrategy(ABC): class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): def __init__(self, use_cached_html=False, js_code=None, **kwargs): self.use_cached_html = use_cached_html - self.user_agent = kwargs.get("user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + self.user_agent = kwargs.get( + "user_agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + ) self.proxy = kwargs.get("proxy") self.headless = kwargs.get("headless", True) - self.browser_type = kwargs.get("browser_type", "chromium") # New parameter + self.browser_type = kwargs.get("browser_type", "chromium") self.headers = kwargs.get("headers", {}) self.sessions = {} self.session_ttl = 1800 @@ -83,9 +105,14 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): "headless": self.headless, "args": [ "--disable-gpu", - "--disable-dev-shm-usage", - "--disable-setuid-sandbox", "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-blink-features=AutomationControlled", + "--disable-infobars", + "--window-position=0,0", + "--ignore-certificate-errors", + "--ignore-certificate-errors-spki-list", + # "--headless=new", # Use the new headless mode ] } @@ -94,7 +121,6 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): proxy_settings = ProxySettings(server=self.proxy) browser_args["proxy"] = proxy_settings - # Select the appropriate browser based on the browser_type if self.browser_type == "firefox": self.browser = await self.playwright.firefox.launch(**browser_args) @@ -147,8 +173,10 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): def _cleanup_expired_sessions(self): current_time = time.time() - expired_sessions = [sid for sid, (_, _, last_used) in self.sessions.items() - if current_time - last_used > self.session_ttl] + expired_sessions = [ + sid for sid, (_, _, last_used) in self.sessions.items() + if current_time - last_used > self.session_ttl + ] for sid in expired_sessions: asyncio.create_task(self.kill_session(sid)) @@ -188,8 +216,8 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): return await self.csp_compliant_wait(page, f"() => {{{wait_for}}}", timeout) except Error: raise ValueError(f"Invalid wait_for parameter: '{wait_for}'. " - "It should be either a valid CSS selector, a JavaScript function, " - "or explicitly prefixed with 'js:' or 'css:'.") + "It should be either a valid CSS selector, a JavaScript function, " + "or explicitly prefixed with 'js:' or 'css:'.") async def csp_compliant_wait(self, page: Page, user_wait_function: str, timeout: float = 30000): wrapper_js = f""" @@ -254,8 +282,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): print(f"Error processing iframe {i}: {str(e)}") # Return the page object - return page - + return page async def crawl(self, url: str, **kwargs) -> AsyncCrawlResponse: response_headers = {} @@ -268,6 +295,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): if not context: context = await self.browser.new_context( user_agent=self.user_agent, + viewport={"width": 1920, "height": 1080}, proxy={"server": self.proxy} if self.proxy else None ) await context.set_extra_http_headers(self.headers) @@ -275,18 +303,58 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): self.sessions[session_id] = (context, page, time.time()) else: context = await self.browser.new_context( - user_agent=self.user_agent, - proxy={"server": self.proxy} if self.proxy else None + user_agent=self.user_agent, + viewport={"width": 1920, "height": 1080}, + proxy={"server": self.proxy} if self.proxy else None ) await context.set_extra_http_headers(self.headers) + + if kwargs.get("override_navigator", False): + # Inject scripts to override navigator properties + await context.add_init_script(""" + // Pass the Permissions Test. + const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + ); + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined + }); + window.navigator.chrome = { + runtime: {}, + // Add other properties if necessary + }; + Object.defineProperty(navigator, 'plugins', { + get: () => [1, 2, 3, 4, 5], + }); + Object.defineProperty(navigator, 'languages', { + get: () => ['en-US', 'en'], + }); + Object.defineProperty(document, 'hidden', { + get: () => false + }); + Object.defineProperty(document, 'visibilityState', { + get: () => 'visible' + }); + """) + page = await context.new_page() + # await stealth_async(page) #, stealth_config) + # Add console message and error logging + page.on("console", lambda msg: print(f"Console: {msg.text}")) + page.on("pageerror", lambda exc: print(f"Page Error: {exc}")) + try: if self.verbose: print(f"[LOG] πŸ•ΈοΈ Crawling {url} using AsyncPlaywrightCrawlerStrategy...") if self.use_cached_html: - cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) + cache_file_path = os.path.join( + Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest() + ) if os.path.exists(cache_file_path): html = "" with open(cache_file_path, "r") as f: @@ -296,12 +364,21 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): meta = json.load(f) response_headers = meta.get("response_headers", {}) status_code = meta.get("status_code") - response = AsyncCrawlResponse(html=html, response_headers=response_headers, status_code=status_code) + response = AsyncCrawlResponse( + html=html, response_headers=response_headers, status_code=status_code + ) return response if not kwargs.get("js_only", False): await self.execute_hook('before_goto', page) - response = await page.goto(url, wait_until="domcontentloaded", timeout=kwargs.get("page_timeout", 60000)) + + response = await page.goto( + url, wait_until="domcontentloaded", timeout=kwargs.get("page_timeout", 60000) + ) + + # response = await page.goto("about:blank") + # await page.evaluate(f"window.location.href = '{url}'") + await self.execute_hook('after_goto', page) # Get status code and headers @@ -311,37 +388,29 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): status_code = 200 response_headers = {} - await page.wait_for_selector('body') await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") js_code = kwargs.get("js_code", kwargs.get("js", self.js_code)) if js_code: if isinstance(js_code, str): - r = await page.evaluate(js_code) + await page.evaluate(js_code) elif isinstance(js_code, list): for js in js_code: await page.evaluate(js) - # await page.wait_for_timeout(100) await page.wait_for_load_state('networkidle') - # Check for on execution even + # Check for on execution event await self.execute_hook('on_execution_started', page) - # New code to handle the wait_for parameter - # Example usage: - # await crawler.crawl( - # url, - # js_code="// some JavaScript code", - # wait_for="""() => { - # return document.querySelector('#my-element') !== null; - # }""" - # ) - # Example of using a CSS selector: - # await crawler.crawl( - # url, - # wait_for="#my-element" - # ) + if kwargs.get("simulate_user", False): + # Simulate user interactions + await page.mouse.move(100, 100) + await page.mouse.down() + await page.mouse.up() + await page.keyboard.press('ArrowDown') + + # Handle the wait_for parameter wait_for = kwargs.get("wait_for") if wait_for: try: @@ -349,13 +418,9 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): except Exception as e: raise RuntimeError(f"Wait condition failed: {str(e)}") - # Check if kwargs has screenshot=True then take screenshot - screenshot_data = None - if kwargs.get("screenshot"): - screenshot_data = await self.take_screenshot(url) + - - # New code to update image dimensions + # Update image dimensions update_image_dimensions_js = """ () => { return new Promise((resolve) => { @@ -428,12 +493,19 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): html = await page.content() await self.execute_hook('before_return_html', page, html) + + # Check if kwargs has screenshot=True then take screenshot + screenshot_data = None + if kwargs.get("screenshot"): + screenshot_data = await self.take_screenshot(url) if self.verbose: print(f"[LOG] βœ… Crawled {url} successfully!") if self.use_cached_html: - cache_file_path = os.path.join(Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest()) + cache_file_path = os.path.join( + Path.home(), ".crawl4ai", "cache", hashlib.md5(url.encode()).hexdigest() + ) with open(cache_file_path, "w", encoding="utf-8") as f: f.write(html) # store response headers and status code in cache @@ -443,7 +515,6 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): "status_code": status_code }, f) - async def get_delayed_content(delay: float = 5.0) -> str: if self.verbose: print(f"[LOG] Waiting for {delay} seconds before retrieving content for {url}") @@ -463,59 +534,10 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): finally: if not session_id: await page.close() + await context.close() - # try: - # html = await _crawl() - # return sanitize_input_encode(html) - # except Error as e: - # raise Error(f"Failed to crawl {url}: {str(e)}") - # except Exception as e: - # raise Exception(f"Failed to crawl {url}: {str(e)}") - - async def execute_js(self, session_id: str, js_code: str, wait_for_js: str = None, wait_for_css: str = None) -> AsyncCrawlResponse: - """ - Execute JavaScript code in a specific session and optionally wait for a condition. - - :param session_id: The ID of the session to execute the JS code in. - :param js_code: The JavaScript code to execute. - :param wait_for_js: JavaScript condition to wait for after execution. - :param wait_for_css: CSS selector to wait for after execution. - :return: AsyncCrawlResponse containing the page's HTML and other information. - :raises ValueError: If the session does not exist. - """ - if not session_id: - raise ValueError("Session ID must be provided") - - if session_id not in self.sessions: - raise ValueError(f"No active session found for session ID: {session_id}") - - context, page, last_used = self.sessions[session_id] - - try: - await page.evaluate(js_code) - - if wait_for_js: - await page.wait_for_function(wait_for_js) - - if wait_for_css: - await page.wait_for_selector(wait_for_css) - - # Get the updated HTML content - html = await page.content() - - # Get response headers and status code (assuming these are available) - response_headers = await page.evaluate("() => JSON.stringify(performance.getEntriesByType('resource')[0].responseHeaders)") - status_code = await page.evaluate("() => performance.getEntriesByType('resource')[0].responseStatus") - - # Update the last used time for this session - self.sessions[session_id] = (context, page, time.time()) - - return AsyncCrawlResponse(html=html, response_headers=response_headers, status_code=status_code) - except Error as e: - raise Error(f"Failed to execute JavaScript or wait for condition in session {session_id}: {str(e)}") - async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: - semaphore_count = kwargs.get('semaphore_count', calculate_semaphore_count()) + semaphore_count = kwargs.get('semaphore_count', 5) # Adjust as needed semaphore = asyncio.Semaphore(semaphore_count) async def crawl_with_semaphore(url): @@ -526,7 +548,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): results = await asyncio.gather(*tasks, return_exceptions=True) return [result if not isinstance(result, Exception) else str(result) for result in results] - async def take_screenshot(self, url: str, wait_time = 1000) -> str: + async def take_screenshot(self, url: str, wait_time=1000) -> str: async with await self.browser.new_context(user_agent=self.user_agent) as context: page = await context.new_page() try: @@ -549,4 +571,5 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): img.save(buffered, format="JPEG") return base64.b64encode(buffered.getvalue()).decode('utf-8') finally: - await page.close() \ No newline at end of file + await page.close() + diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index ba82d28f..76846fe9 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -195,6 +195,7 @@ class AsyncWebCrawler: image_description_min_word_threshold=kwargs.get( "image_description_min_word_threshold", IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD ), + **kwargs, ) if verbose: print( diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 68f03412..64707f74 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -33,6 +33,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): return await asyncio.to_thread(self._get_content_of_website_optimized, url, html, **kwargs) def _get_content_of_website_optimized(self, url: str, html: str, word_count_threshold: int = MIN_WORD_THRESHOLD, css_selector: str = None, **kwargs) -> Dict[str, Any]: + success = True if not html: return None @@ -273,10 +274,41 @@ class WebScrappingStrategy(ContentScrappingStrategy): if base64_pattern.match(src): # Replace base64 data with empty string img['src'] = base64_pattern.sub('', src) + + try: + str(body) + except Exception as e: + # Reset body to the original HTML + success = False + body = BeautifulSoup(html, 'html.parser') + + # Create a new div with a special ID + error_div = body.new_tag('div', id='crawl4ai_error_message') + error_div.string = ''' + Crawl4AI Error: This page is not fully supported. + + Possible reasons: + 1. The page may have restrictions that prevent crawling. + 2. The page might not be fully loaded. + + Suggestions: + - Try calling the crawl function with these parameters: + simulate_user=True, override_navigator=True + - Set headless=False to visualize what's happening on the page. + + If the issue persists, please check the page's structure and any potential anti-crawling measures. + ''' + + # Append the error div to the body + body.body.append(error_div) + + print(f"[LOG] 😧 Error: After processing the crawled HTML and removing irrelevant tags, nothing was left in the page. Check the markdown for further details.") + + cleaned_html = str(body).replace('\n\n', '\n').replace(' ', ' ') h = CustomHTML2Text() - h.ignore_links = True + h.ignore_links = not kwargs.get('include_links_on_markdown', False) h.body_width = 0 try: markdown = h.handle(cleaned_html) @@ -294,7 +326,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): return { 'markdown': markdown, 'cleaned_html': cleaned_html, - 'success': True, + 'success': success, 'media': media, 'links': links, 'metadata': meta diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index efb5d79b..711fd2c4 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -692,8 +692,8 @@ def get_content_of_website_optimized(url: str, html: str, word_count_threshold: for img in imgs: src = img.get('src', '') if base64_pattern.match(src): - # Replace base64 data with empty string img['src'] = base64_pattern.sub('', src) + cleaned_html = str(body).replace('\n\n', '\n').replace(' ', ' ') cleaned_html = sanitize_html(cleaned_html) diff --git a/docs/examples/quickstart_async.py b/docs/examples/quickstart_async.py index f6c16a4e..a3837406 100644 --- a/docs/examples/quickstart_async.py +++ b/docs/examples/quickstart_async.py @@ -379,6 +379,18 @@ async def crawl_custom_browser_type(): print(result.markdown[:500]) print("Time taken: ", time.time() - start) +async def crawl_with_user_simultion(): + async with AsyncWebCrawler(verbose=True, headless=True) as crawler: + url = "YOUR-URL-HERE" + result = await crawler.arun( + url=url, + bypass_cache=True, + simulate_user = True,# Causes a series of random mouse movements and clicks to simulate user interaction + override_navigator = True # Overrides the navigator object to make it look like a real user + ) + + print(result.markdown) + async def speed_comparison(): # print("\n--- Speed Comparison ---") # print("Firecrawl (simulated):") From dd17ed0e63df84fd6e6cd76f4f131bb56121a0f5 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Fri, 18 Oct 2024 12:35:09 +0800 Subject: [PATCH 02/14] Rename some flags name, introducing magic flag. --- crawl4ai/async_crawler_strategy.py | 9 +++++---- crawl4ai/content_scrapping_strategy.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index d4c94fee..307dee20 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -309,7 +309,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): ) await context.set_extra_http_headers(self.headers) - if kwargs.get("override_navigator", False): + if kwargs.get("override_navigator", False) or kwargs.get("simulate_user", False) or kwargs.get("magic", False): # Inject scripts to override navigator properties await context.add_init_script(""" // Pass the Permissions Test. @@ -344,8 +344,9 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): # await stealth_async(page) #, stealth_config) # Add console message and error logging - page.on("console", lambda msg: print(f"Console: {msg.text}")) - page.on("pageerror", lambda exc: print(f"Page Error: {exc}")) + if kwargs.get("log_console", False): + page.on("console", lambda msg: print(f"Console: {msg.text}")) + page.on("pageerror", lambda exc: print(f"Page Error: {exc}")) try: if self.verbose: @@ -403,7 +404,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): # Check for on execution event await self.execute_hook('on_execution_started', page) - if kwargs.get("simulate_user", False): + if kwargs.get("simulate_user", False) or kwargs.get("magic", False): # Simulate user interactions await page.mouse.move(100, 100) await page.mouse.down() diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 64707f74..8a5cc8ad 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -293,7 +293,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): Suggestions: - Try calling the crawl function with these parameters: - simulate_user=True, override_navigator=True + magic=True, - Set headless=False to visualize what's happening on the page. If the issue persists, please check the page's structure and any potential anti-crawling measures. From aab6ea022e76444effde0cfa3a890f5b7cc75bb3 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Fri, 18 Oct 2024 12:51:23 +0800 Subject: [PATCH 03/14] Update requirements and switch to 0.3.8 --- crawl4ai/__init__.py | 2 +- requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crawl4ai/__init__.py b/crawl4ai/__init__.py index 04da30f8..f349fb94 100644 --- a/crawl4ai/__init__.py +++ b/crawl4ai/__init__.py @@ -3,7 +3,7 @@ from .async_webcrawler import AsyncWebCrawler from .models import CrawlResult -__version__ = "0.3.6" +__version__ = "0.3.8" __all__ = [ "AsyncWebCrawler", diff --git a/requirements.txt b/requirements.txt index 7d21f5ae..01aafc91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ pillow==10.4.0 playwright==1.47.0 python-dotenv==1.0.1 requests>=2.26.0,<2.32.3 -beautifulsoup4==4.12.3 \ No newline at end of file +beautifulsoup4==4.12.3 +playwright_stealth==1.0.6 \ No newline at end of file From b8147b64e00629d2effaf025020c2fe5c9842a43 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Fri, 18 Oct 2024 13:31:12 +0800 Subject: [PATCH 04/14] chore: Bump version to 0.3.71 and improve error handling - Update version number to 0.3.71 - Add sleep_on_close option to AsyncPlaywrightCrawlerStrategy - Enhance context creation with additional options - Improve error message formatting and visibility - Update quickstart documentation --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ crawl4ai/__init__.py | 2 +- crawl4ai/async_crawler_strategy.py | 20 ++++++++++++-------- crawl4ai/async_webcrawler.py | 4 ++-- docs/examples/quickstart.ipynb | 5 ++--- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b0513d4..07ca786c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [v0.3.71] - 2024-10-18 + +### Changes +1. **Version Update**: + - Updated version number from 0.3.7 to 0.3.71. + +2. **Crawler Enhancements**: + - Added `sleep_on_close` option to AsyncPlaywrightCrawlerStrategy for delayed browser closure. + - Improved context creation with additional options: + - Enabled `accept_downloads` and `java_script_enabled`. + - Added a cookie to enable cookies by default. + +3. **Error Handling Improvements**: + - Enhanced error messages in AsyncWebCrawler's `arun` method. + - Updated error reporting format for better visibility and consistency. + +4. **Performance Optimization**: + - Commented out automatic page and context closure in `crawl` method to potentially improve performance in certain scenarios. + +### Documentation +- Updated quickstart notebook: + - Changed installation command to use the released package instead of GitHub repository. + - Updated kernel display name. + +### Developer Notes +- Minor code refactoring and cleanup. + ## [v0.3.7] - 2024-10-17 ### New Features diff --git a/crawl4ai/__init__.py b/crawl4ai/__init__.py index f349fb94..1ecff4b0 100644 --- a/crawl4ai/__init__.py +++ b/crawl4ai/__init__.py @@ -3,7 +3,7 @@ from .async_webcrawler import AsyncWebCrawler from .models import CrawlResult -__version__ = "0.3.8" +__version__ = "0.3.71" __all__ = [ "AsyncWebCrawler", diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index 307dee20..f87b6243 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -80,6 +80,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): self.verbose = kwargs.get("verbose", False) self.playwright = None self.browser = None + self.sleep_on_close = kwargs.get("sleep_on_close", False) self.hooks = { 'on_browser_created': None, 'on_user_agent_updated': None, @@ -132,6 +133,8 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): await self.execute_hook('on_browser_created', self.browser) async def close(self): + if self.sleep_on_close: + await asyncio.sleep(500) if self.browser: await self.browser.close() self.browser = None @@ -296,8 +299,11 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): context = await self.browser.new_context( user_agent=self.user_agent, viewport={"width": 1920, "height": 1080}, - proxy={"server": self.proxy} if self.proxy else None + proxy={"server": self.proxy} if self.proxy else None, + accept_downloads=True, + java_script_enabled=True ) + await context.add_cookies([{"name": "cookiesEnabled", "value": "true", "url": url}]) await context.set_extra_http_headers(self.headers) page = await context.new_page() self.sessions[session_id] = (context, page, time.time()) @@ -419,8 +425,6 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): except Exception as e: raise RuntimeError(f"Wait condition failed: {str(e)}") - - # Update image dimensions update_image_dimensions_js = """ () => { @@ -531,11 +535,11 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): ) return response except Error as e: - raise Error(f"Failed to crawl {url}: {str(e)}") - finally: - if not session_id: - await page.close() - await context.close() + raise Error(f"[ERROR] 🚫 crawl(): Failed to crawl {url}: {str(e)}") + # finally: + # if not session_id: + # await page.close() + # await context.close() async def crawl_many(self, urls: List[str], **kwargs) -> List[AsyncCrawlResponse]: semaphore_count = kwargs.get('semaphore_count', 5) # Adjust as needed diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 76846fe9..9a57048d 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -133,8 +133,8 @@ class AsyncWebCrawler: except Exception as e: if not hasattr(e, "msg"): e.msg = str(e) - print(f"[ERROR] 🚫 Failed to crawl {url}, error: {e.msg}") - return CrawlResult(url=url, html="", success=False, error_message=e.msg) + print(f"[ERROR] 🚫 arun(): Failed to crawl {url}, error: {e.msg}") + return CrawlResult(url=url, html="", markdown = f"[ERROR] 🚫 arun(): Failed to crawl {url}, error: {e.msg}", success=False, error_message=e.msg) async def arun_many( self, diff --git a/docs/examples/quickstart.ipynb b/docs/examples/quickstart.ipynb index 09ad623b..71f23acb 100644 --- a/docs/examples/quickstart.ipynb +++ b/docs/examples/quickstart.ipynb @@ -47,8 +47,7 @@ }, "outputs": [], "source": [ - "# !pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git\"\n", - "!pip install \"crawl4ai @ git+https://github.com/unclecode/crawl4ai.git@staging\"\n", + "!pip install crawl4ai\n", "!pip install nest-asyncio\n", "!playwright install" ] @@ -714,7 +713,7 @@ "provenance": [] }, "kernelspec": { - "display_name": "Python 3", + "display_name": "venv", "language": "python", "name": "python3" }, From b309bc34e1798ece9af725afec6051f87b2ced8b Mon Sep 17 00:00:00 2001 From: UncleCode Date: Fri, 18 Oct 2024 15:32:25 +0800 Subject: [PATCH 05/14] Fix the model nam ein quick start example --- docs/examples/quickstart_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/quickstart_async.py b/docs/examples/quickstart_async.py index a3837406..1bd738bf 100644 --- a/docs/examples/quickstart_async.py +++ b/docs/examples/quickstart_async.py @@ -467,7 +467,7 @@ async def main(): # LLM extraction examples await extract_structured_data_using_llm() await extract_structured_data_using_llm("huggingface/meta-llama/Meta-Llama-3.1-8B-Instruct", os.getenv("HUGGINGFACE_API_KEY")) - await extract_structured_data_using_llm("openai/gpt-4", os.getenv("OPENAI_API_KEY")) + await extract_structured_data_using_llm("openai/gpt-4o", os.getenv("OPENAI_API_KEY")) await extract_structured_data_using_llm("ollama/llama3.2") # You always can pass custom headers to the extraction strategy From 4e2852d5ff3d30123a78bd57274df649d6b5ca09 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sat, 19 Oct 2024 18:36:59 +0800 Subject: [PATCH 06/14] [v0.3.71] Enhance chunking strategies and improve overall performance - Add OverlappingWindowChunking and improve SlidingWindowChunking - Update CHUNK_TOKEN_THRESHOLD to 2048 tokens - Optimize AsyncPlaywrightCrawlerStrategy close method - Enhance flexibility in CosineStrategy with generic embedding model loading - Improve JSON-based extraction strategies - Add knowledge graph generation example --- .gitignore | 3 +- crawl4ai/async_crawler_strategy.py | 2 +- crawl4ai/chunking_strategy.py | 60 +++++++++++++++++++++++++++++- crawl4ai/config.py | 2 +- crawl4ai/extraction_strategy.py | 18 ++++----- crawl4ai/model_loader.py | 14 +++++-- docs/examples/quickstart_async.py | 37 +++++++++++++++++- 7 files changed, 118 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 1793e24c..9b669c25 100644 --- a/.gitignore +++ b/.gitignore @@ -205,4 +205,5 @@ pypi_build.sh git_issues.py git_issues.md -.tests/ \ No newline at end of file +.tests/ +.issues/ \ No newline at end of file diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index f87b6243..7046200e 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -134,7 +134,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): async def close(self): if self.sleep_on_close: - await asyncio.sleep(500) + await asyncio.sleep(0.5) if self.browser: await self.browser.close() self.browser = None diff --git a/crawl4ai/chunking_strategy.py b/crawl4ai/chunking_strategy.py index d16e4f48..af857947 100644 --- a/crawl4ai/chunking_strategy.py +++ b/crawl4ai/chunking_strategy.py @@ -84,6 +84,12 @@ class TopicSegmentationChunking(ChunkingStrategy): # Fixed-length word chunks class FixedLengthWordChunking(ChunkingStrategy): def __init__(self, chunk_size=100, **kwargs): + """ + Initialize the fixed-length word chunking strategy with the given chunk size. + + Args: + chunk_size (int): The size of each chunk in words. + """ self.chunk_size = chunk_size def chunk(self, text: str) -> list: @@ -93,14 +99,64 @@ class FixedLengthWordChunking(ChunkingStrategy): # Sliding window chunking class SlidingWindowChunking(ChunkingStrategy): def __init__(self, window_size=100, step=50, **kwargs): + """ + Initialize the sliding window chunking strategy with the given window size and + step size. + + Args: + window_size (int): The size of the sliding window in words. + step (int): The step size for sliding the window in words. + """ self.window_size = window_size self.step = step def chunk(self, text: str) -> list: words = text.split() chunks = [] - for i in range(0, len(words), self.step): - chunks.append(' '.join(words[i:i + self.window_size])) + + if len(words) <= self.window_size: + return [text] + + for i in range(0, len(words) - self.window_size + 1, self.step): + chunk = ' '.join(words[i:i + self.window_size]) + chunks.append(chunk) + + # Handle the last chunk if it doesn't align perfectly + if i + self.window_size < len(words): + chunks.append(' '.join(words[-self.window_size:])) + return chunks +class OverlappingWindowChunking(ChunkingStrategy): + def __init__(self, window_size=1000, overlap=100, **kwargs): + """ + Initialize the overlapping window chunking strategy with the given window size and + overlap size. + + Args: + window_size (int): The size of the window in words. + overlap (int): The size of the overlap between consecutive chunks in words. + """ + self.window_size = window_size + self.overlap = overlap + + def chunk(self, text: str) -> list: + words = text.split() + chunks = [] + + if len(words) <= self.window_size: + return [text] + + start = 0 + while start < len(words): + end = start + self.window_size + chunk = ' '.join(words[start:end]) + chunks.append(chunk) + + if end >= len(words): + break + + start = end - self.overlap + + return chunks \ No newline at end of file diff --git a/crawl4ai/config.py b/crawl4ai/config.py index 00b1eb46..862ebfe9 100644 --- a/crawl4ai/config.py +++ b/crawl4ai/config.py @@ -21,7 +21,7 @@ PROVIDER_MODELS = { # Chunk token threshold -CHUNK_TOKEN_THRESHOLD = 500 +CHUNK_TOKEN_THRESHOLD = 2 ** 11 # 2048 tokens OVERLAP_RATE = 0.1 WORD_TOKEN_RATE = 1.3 diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 210a360b..7426f94e 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -234,11 +234,12 @@ class CosineStrategy(ExtractionStrategy): """ Initialize the strategy with clustering parameters. - :param semantic_filter: A keyword filter for document filtering. - :param word_count_threshold: Minimum number of words per cluster. - :param max_dist: The maximum cophenetic distance on the dendrogram to form clusters. - :param linkage_method: The linkage method for hierarchical clustering. - :param top_k: Number of top categories to extract. + Args: + semantic_filter (str): A keyword filter for document filtering. + word_count_threshold (int): Minimum number of words per cluster. + max_dist (float): The maximum cophenetic distance on the dendrogram to form clusters. + linkage_method (str): The linkage method for hierarchical clustering. + top_k (int): Number of top categories to extract. """ super().__init__() @@ -257,8 +258,8 @@ class CosineStrategy(ExtractionStrategy): self.get_embedding_method = "direct" self.device = get_device() - import torch - self.device = torch.device('cpu') + # import torch + # self.device = torch.device('cpu') self.default_batch_size = calculate_batch_size(self.device) @@ -271,7 +272,7 @@ class CosineStrategy(ExtractionStrategy): # self.get_embedding_method = "direct" # else: - self.tokenizer, self.model = load_bge_small_en_v1_5() + self.tokenizer, self.model = load_HF_embedding_model(model_name) self.model.to(self.device) self.model.eval() @@ -738,7 +739,6 @@ class JsonCssExtractionStrategy(ExtractionStrategy): combined_html = self.DEL.join(sections) return self.extract(url, combined_html, **kwargs) - class JsonXPATHExtractionStrategy(ExtractionStrategy): def __init__(self, schema: Dict[str, Any], **kwargs): super().__init__(**kwargs) diff --git a/crawl4ai/model_loader.py b/crawl4ai/model_loader.py index e33e53f4..7b3a2846 100644 --- a/crawl4ai/model_loader.py +++ b/crawl4ai/model_loader.py @@ -72,10 +72,18 @@ def load_bert_base_uncased(): return tokenizer, model @lru_cache() -def load_bge_small_en_v1_5(): +def load_HF_embedding_model(model_name="BAAI/bge-small-en-v1.5") -> tuple: + """Load the Hugging Face model for embedding. + + Args: + model_name (str, optional): The model name to load. Defaults to "BAAI/bge-small-en-v1.5". + + Returns: + tuple: The tokenizer and model. + """ from transformers import BertTokenizer, BertModel, AutoTokenizer, AutoModel - tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-small-en-v1.5', resume_download=None) - model = AutoModel.from_pretrained('BAAI/bge-small-en-v1.5', resume_download=None) + tokenizer = AutoTokenizer.from_pretrained(model_name, resume_download=None) + model = AutoModel.from_pretrained(model_name, resume_download=None) model.eval() model, device = set_model_device(model) return tokenizer, model diff --git a/docs/examples/quickstart_async.py b/docs/examples/quickstart_async.py index 1bd738bf..9b88b332 100644 --- a/docs/examples/quickstart_async.py +++ b/docs/examples/quickstart_async.py @@ -10,7 +10,7 @@ import time import json import os import re -from typing import Dict +from typing import Dict, List from bs4 import BeautifulSoup from pydantic import BaseModel, Field from crawl4ai import AsyncWebCrawler @@ -456,6 +456,41 @@ async def speed_comparison(): print("If you run these tests in an environment with better network conditions,") print("you may observe an even more significant speed advantage for Crawl4AI.") + +async def generate_knowledge_graph(): + class Entity(BaseModel): + name: str + description: str + + class Relationship(BaseModel): + entity1: Entity + entity2: Entity + description: str + relation_type: str + + class KnowledgeGraph(BaseModel): + entities: List[Entity] + relationships: List[Relationship] + + extraction_strategy = LLMExtractionStrategy( + provider='openai/gpt-4o-mini', + api_token=os.getenv('OPENAI_API_KEY'), + schema=KnowledgeGraph.model_json_schema(), + extraction_type="schema", + instruction="""Extract entities and relationships from the given text.""" + ) + async with AsyncWebCrawler() as crawler: + url = "https://paulgraham.com/love.html" + result = await crawler.arun( + url=url, + bypass_cache=True, + extraction_strategy=extraction_strategy, + # magic=True + ) + # print(result.extracted_content) + with open(os.path.join(__location__, "kb.json"), "w") as f: + f.write(result.extracted_content) + async def main(): await simple_crawl() await simple_example_with_running_js_code() From e7cd8a1c2d4325942c3f4f1b7c74278193754598 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sat, 19 Oct 2024 18:37:12 +0800 Subject: [PATCH 07/14] Update Changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ca786c..41160faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [v0.3.71] - 2024-10-19 + +### Added +- New chunking strategies: + - `OverlappingWindowChunking`: Allows for overlapping chunks of text, useful for maintaining context between chunks. + - Enhanced `SlidingWindowChunking`: Improved to handle edge cases and last chunks more effectively. + +### Changed +- Updated `CHUNK_TOKEN_THRESHOLD` in config to 2048 tokens (2^11) for better compatibility with most LLM models. +- Improved `AsyncPlaywrightCrawlerStrategy.close()` method to use a shorter sleep time (0.5 seconds instead of 500), significantly reducing wait time when closing the crawler. +- Enhanced flexibility in `CosineStrategy`: + - Now uses a more generic `load_HF_embedding_model` function, allowing for easier swapping of embedding models. +- Updated `JsonCssExtractionStrategy` and `JsonXPATHExtractionStrategy` for better JSON-based extraction. + +### Fixed +- Addressed potential issues with the sliding window chunking strategy to ensure all text is properly chunked. + +### Developer Notes +- Added more comprehensive docstrings to chunking strategies for better code documentation. +- Removed hardcoded device setting in `CosineStrategy`, now using the automatically detected device. +- Added a new example in `quickstart_async.py` for generating a knowledge graph from crawled content. + +These updates aim to provide more flexibility in text processing, improve performance, and enhance the overall capabilities of the crawl4ai library. The new chunking strategies, in particular, offer more options for handling large texts in various scenarios. + ## [v0.3.71] - 2024-10-18 ### Changes From 6ec4cb33ca9a6c7b644a50c7a8e4729881a90e85 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sun, 20 Oct 2024 18:56:58 +0800 Subject: [PATCH 08/14] Enhance Markdown generation and external content control - Integrate customized html2text library for flexible Markdown output - Add options to exclude external links and images - Improve content scraping efficiency and error handling - Update AsyncPlaywrightCrawlerStrategy for faster closing - Enhance CosineStrategy with generic embedding model loading --- CHANGELOG.md | 25 + crawl4ai/async_crawler_strategy.py | 4 +- crawl4ai/async_webcrawler.py | 3 +- crawl4ai/config.py | 28 +- crawl4ai/content_scrapping_strategy.py | 75 +- crawl4ai/extraction_strategy.py | 2 +- crawl4ai/html2text/__init__.py | 1015 ++++++++++++++++++++++++ crawl4ai/html2text/__main__.py | 3 + crawl4ai/html2text/_typing.py | 2 + crawl4ai/html2text/cli.py | 330 ++++++++ crawl4ai/html2text/config.py | 172 ++++ crawl4ai/html2text/elements.py | 18 + crawl4ai/html2text/utils.py | 303 +++++++ crawl4ai/utils.py | 22 +- 14 files changed, 1981 insertions(+), 21 deletions(-) create mode 100644 crawl4ai/html2text/__init__.py create mode 100644 crawl4ai/html2text/__main__.py create mode 100644 crawl4ai/html2text/_typing.py create mode 100644 crawl4ai/html2text/cli.py create mode 100644 crawl4ai/html2text/config.py create mode 100644 crawl4ai/html2text/elements.py create mode 100644 crawl4ai/html2text/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 41160faa..59e77217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [v0.3.72] - 2024-10-20 + +### Added +- Forked and integrated a customized version of the html2text library for more control over Markdown generation +- New configuration options for controlling external content: + - Ability to exclude all external links + - Option to specify domains to exclude (default includes major social media platforms) + - Control over excluding external images + +### Changed +- Improved Markdown generation process: + - Added fine-grained control over character escaping in Markdown output + - Enhanced handling of code blocks and pre-formatted text +- Updated `AsyncPlaywrightCrawlerStrategy.close()` method to use a shorter sleep time (0.5 seconds instead of 500) +- Enhanced flexibility in `CosineStrategy` with a more generic `load_HF_embedding_model` function + +### Improved +- Optimized content scraping and processing for better efficiency +- Enhanced error handling and logging in various components + +### Developer Notes +- The customized html2text library is now located within the crawl4ai package +- New configuration options are available in the `config.py` file for external content handling +- The `WebScrappingStrategy` class has been updated to accommodate new external content exclusion options + ## [v0.3.71] - 2024-10-19 ### Added diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index 7046200e..ca6e4c5b 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -396,6 +396,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): response_headers = {} await page.wait_for_selector('body') + await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") js_code = kwargs.get("js_code", kwargs.get("js", self.js_code)) @@ -477,7 +478,8 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): }); // Fallback timeout of 5 seconds - setTimeout(() => resolve(), 5000); + // setTimeout(() => resolve(), 5000); + resolve(); }); } """ diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 9a57048d..94938b60 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -186,7 +186,8 @@ class AsyncWebCrawler: try: t1 = time.time() scrapping_strategy = WebScrappingStrategy() - result = await scrapping_strategy.ascrap( + # result = await scrapping_strategy.ascrap( + result = await scrapping_strategy.scrap( url, html, word_count_threshold=word_count_threshold, diff --git a/crawl4ai/config.py b/crawl4ai/config.py index 862ebfe9..2f33a8a9 100644 --- a/crawl4ai/config.py +++ b/crawl4ai/config.py @@ -4,22 +4,21 @@ from dotenv import load_dotenv load_dotenv() # Load environment variables from .env file # Default provider, ONLY used when the extraction strategy is LLMExtractionStrategy -DEFAULT_PROVIDER = "openai/gpt-4-turbo" +DEFAULT_PROVIDER = "openai/gpt-4o-mini" MODEL_REPO_BRANCH = "new-release-0.0.2" # Provider-model dictionary, ONLY used when the extraction strategy is LLMExtractionStrategy PROVIDER_MODELS = { "ollama/llama3": "no-token-needed", # Any model from Ollama no need for API token "groq/llama3-70b-8192": os.getenv("GROQ_API_KEY"), "groq/llama3-8b-8192": os.getenv("GROQ_API_KEY"), - "openai/gpt-3.5-turbo": os.getenv("OPENAI_API_KEY"), - "openai/gpt-4-turbo": os.getenv("OPENAI_API_KEY"), + "openai/gpt-4o-mini": os.getenv("OPENAI_API_KEY"), "openai/gpt-4o": os.getenv("OPENAI_API_KEY"), "anthropic/claude-3-haiku-20240307": os.getenv("ANTHROPIC_API_KEY"), "anthropic/claude-3-opus-20240229": os.getenv("ANTHROPIC_API_KEY"), "anthropic/claude-3-sonnet-20240229": os.getenv("ANTHROPIC_API_KEY"), + "anthropic/claude-3-5-sonnet-20240620": os.getenv("ANTHROPIC_API_KEY"), } - # Chunk token threshold CHUNK_TOKEN_THRESHOLD = 2 ** 11 # 2048 tokens OVERLAP_RATE = 0.1 @@ -29,6 +28,27 @@ WORD_TOKEN_RATE = 1.3 MIN_WORD_THRESHOLD = 1 IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD = 1 +IMPORTANT_ATTRS = ['src', 'href', 'alt', 'title', 'width', 'height'] +ONLY_TEXT_ELIGIBLE_TAGS = ['b', 'i', 'u', 'span', 'del', 'ins', 'sub', 'sup', 'strong', 'em', 'code', 'kbd', 'var', 's', 'q', 'abbr', 'cite', 'dfn', 'time', 'small', 'mark'] +SOCIAL_MEDIA_DOMAINS = [ + 'facebook.com', + 'twitter.com', + 'x.com', + 'linkedin.com', + 'instagram.com', + 'pinterest.com', + 'youtube.com', + 'tiktok.com', + 'snapchat.com', + 'whatsapp.com', + 'messenger.com', + 'reddit.com', + 'tumblr.com', + 'buffer.com', + 'xing.com', + 'flipboard.com', + ] + # Threshold for the Image extraction - Range is 1 to 6 # Images are scored based on point based system, to filter based on usefulness. Points are assigned # to each image based on the following aspects. diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 8a5cc8ad..640d1d15 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -40,6 +40,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): soup = BeautifulSoup(html, 'html.parser') body = soup.body + image_description_min_word_threshold = kwargs.get('image_description_min_word_threshold', IMAGE_DESCRIPTION_MIN_WORD_THRESHOLD) for tag in kwargs.get('excluded_tags', []) or []: @@ -150,6 +151,8 @@ class WebScrappingStrategy(ContentScrappingStrategy): score+=1 return score + + if not is_valid_image(img, img.parent, img.parent.get('class', [])): return None score = score_image_for_usefulness(img, url, index, total_images) @@ -164,6 +167,19 @@ class WebScrappingStrategy(ContentScrappingStrategy): 'type': 'image' } + def remove_unwanted_attributes(element, important_attrs, keep_data_attributes=False): + attrs_to_remove = [] + for attr in element.attrs: + if attr not in important_attrs: + if keep_data_attributes: + if not attr.startswith('data-'): + attrs_to_remove.append(attr) + else: + attrs_to_remove.append(attr) + + for attr in attrs_to_remove: + del element[attr] + def process_element(element: element.PageElement) -> bool: try: if isinstance(element, NavigableString): @@ -190,8 +206,39 @@ class WebScrappingStrategy(ContentScrappingStrategy): else: links['internal'].append(link_data) keep_element = True + + if kwargs.get('exclude_external_links', True): + href_url_base = href.split('/')[2] + if url_base not in href_url_base: + element.decompose() + return False + + # Check if we should esclude links to all major social media platforms + if not kwargs.get('exclude_external_links', False) and kwargs.get('exclude_social_media_links', True): + social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('social_media_domains', []) + social_media_domains = list(set(social_media_domains)) + if any(domain in href for domain in social_media_domains): + element.decompose() + return False elif element.name == 'img': + # Check flag if we should remove external images + if kwargs.get('exclude_external_images', False): + src = element.get('src', '') + src_url_base = src.split('/')[2] + url_base = url.split('/')[2] + if url_base not in src_url_base: + element.decompose() + return False + + if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', True): + src = element.get('src', '') + src_url_base = src.split('/')[2] + url_base = url.split('/')[2] + if any(domain in src for domain in SOCIAL_MEDIA_DOMAINS): + element.decompose() + return False + return True # Always keep image elements elif element.name in ['video', 'audio']: @@ -211,14 +258,17 @@ class WebScrappingStrategy(ContentScrappingStrategy): }) return True # Always keep video and audio elements - if element.name != 'pre': - if element.name in ['b', 'i', 'u', 'span', 'del', 'ins', 'sub', 'sup', 'strong', 'em', 'code', 'kbd', 'var', 's', 'q', 'abbr', 'cite', 'dfn', 'time', 'small', 'mark']: - if kwargs.get('only_text', False): - element.replace_with(element.get_text()) - else: - element.unwrap() - elif element.name != 'img': - element.attrs = {} + if element.name in ONLY_TEXT_ELIGIBLE_TAGS: + if kwargs.get('only_text', False): + element.replace_with(element.get_text()) + + remove_unwanted_attributes(element, IMPORTANT_ATTRS, kwargs.get('keep_data_attributes', False)) + # for attr in element.attrs: + # if attr not in IMPORTANT_ATTRS or (attr.startswith('data-') and not kwargs.get('keep_data_attributes', False)): + # del element[attr] + + # Print element name and attributes + print(element.name, element.attrs) # Process children for child in list(element.children): @@ -254,7 +304,11 @@ class WebScrappingStrategy(ContentScrappingStrategy): process_element(body) # # Process images using ThreadPoolExecutor + + + imgs = body.find_all('img') + with ThreadPoolExecutor() as executor: image_results = list(executor.map(process_image, imgs, [url]*len(imgs), range(len(imgs)), [len(imgs)]*len(imgs))) media['images'] = [result for result in image_results if result is not None] @@ -307,10 +361,9 @@ class WebScrappingStrategy(ContentScrappingStrategy): cleaned_html = str(body).replace('\n\n', '\n').replace(' ', ' ') - h = CustomHTML2Text() - h.ignore_links = not kwargs.get('include_links_on_markdown', False) - h.body_width = 0 try: + h = CustomHTML2Text() + h.update_params(**kwargs.get('html2text', {})) markdown = h.handle(cleaned_html) except Exception as e: markdown = h.handle(sanitize_html(cleaned_html)) diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 7426f94e..046067d8 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -68,7 +68,7 @@ class LLMExtractionStrategy(ExtractionStrategy): """ super().__init__() self.provider = provider - self.api_token = api_token or PROVIDER_MODELS.get(provider, None) or os.getenv("OPENAI_API_KEY") + self.api_token = api_token or PROVIDER_MODELS.get(provider, "no-token") or os.getenv("OPENAI_API_KEY") self.instruction = instruction self.extract_type = extraction_type self.schema = schema diff --git a/crawl4ai/html2text/__init__.py b/crawl4ai/html2text/__init__.py new file mode 100644 index 00000000..c1effe6b --- /dev/null +++ b/crawl4ai/html2text/__init__.py @@ -0,0 +1,1015 @@ +"""html2text: Turn HTML into equivalent Markdown-structured text.""" + +import html.entities +import html.parser +import re +import string +import urllib.parse as urlparse +from textwrap import wrap +from typing import Dict, List, Optional, Tuple, Union + +from . import config +from ._typing import OutCallback +from .elements import AnchorElement, ListElement +from .utils import ( + dumb_css_parser, + element_style, + escape_md, + escape_md_section, + google_fixed_width_font, + google_has_height, + google_list_style, + google_text_emphasis, + hn, + list_numbering_start, + pad_tables_in_text, + skipwrap, + unifiable_n, +) + +__version__ = (2024, 2, 26) + + +# TODO: +# Support decoded entities with UNIFIABLE. + + +class HTML2Text(html.parser.HTMLParser): + def __init__( + self, + out: Optional[OutCallback] = None, + baseurl: str = "", + bodywidth: int = config.BODY_WIDTH, + ) -> None: + """ + Input parameters: + out: possible custom replacement for self.outtextf (which + appends lines of text). + baseurl: base URL of the document we process + """ + super().__init__(convert_charrefs=False) + + # Config options + self.split_next_td = False + self.td_count = 0 + self.table_start = False + self.unicode_snob = config.UNICODE_SNOB # covered in cli + + self.escape_snob = config.ESCAPE_SNOB # covered in cli + self.escape_backslash = config.ESCAPE_BACKSLASH # covered in cli + self.escape_dot = config.ESCAPE_DOT # covered in cli + self.escape_plus = config.ESCAPE_PLUS # covered in cli + self.escape_dash = config.ESCAPE_DASH # covered in cli + + self.links_each_paragraph = config.LINKS_EACH_PARAGRAPH + self.body_width = bodywidth # covered in cli + self.skip_internal_links = config.SKIP_INTERNAL_LINKS # covered in cli + self.inline_links = config.INLINE_LINKS # covered in cli + self.protect_links = config.PROTECT_LINKS # covered in cli + self.google_list_indent = config.GOOGLE_LIST_INDENT # covered in cli + self.ignore_links = config.IGNORE_ANCHORS # covered in cli + self.ignore_mailto_links = config.IGNORE_MAILTO_LINKS # covered in cli + self.ignore_images = config.IGNORE_IMAGES # covered in cli + self.images_as_html = config.IMAGES_AS_HTML # covered in cli + self.images_to_alt = config.IMAGES_TO_ALT # covered in cli + self.images_with_size = config.IMAGES_WITH_SIZE # covered in cli + self.ignore_emphasis = config.IGNORE_EMPHASIS # covered in cli + self.bypass_tables = config.BYPASS_TABLES # covered in cli + self.ignore_tables = config.IGNORE_TABLES # covered in cli + self.google_doc = False # covered in cli + self.ul_item_mark = "*" # covered in cli + self.emphasis_mark = "_" # covered in cli + self.strong_mark = "**" + self.single_line_break = config.SINGLE_LINE_BREAK # covered in cli + self.use_automatic_links = config.USE_AUTOMATIC_LINKS # covered in cli + self.hide_strikethrough = False # covered in cli + self.mark_code = config.MARK_CODE + self.wrap_list_items = config.WRAP_LIST_ITEMS # covered in cli + self.wrap_links = config.WRAP_LINKS # covered in cli + self.wrap_tables = config.WRAP_TABLES + self.pad_tables = config.PAD_TABLES # covered in cli + self.default_image_alt = config.DEFAULT_IMAGE_ALT # covered in cli + self.tag_callback = None + self.open_quote = config.OPEN_QUOTE # covered in cli + self.close_quote = config.CLOSE_QUOTE # covered in cli + self.include_sup_sub = config.INCLUDE_SUP_SUB # covered in cli + + if out is None: + self.out = self.outtextf + else: + self.out = out + + # empty list to store output characters before they are "joined" + self.outtextlist: List[str] = [] + + self.quiet = 0 + self.p_p = 0 # number of newline character to print before next output + self.outcount = 0 + self.start = True + self.space = False + self.a: List[AnchorElement] = [] + self.astack: List[Optional[Dict[str, Optional[str]]]] = [] + self.maybe_automatic_link: Optional[str] = None + self.empty_link = False + self.absolute_url_matcher = re.compile(r"^[a-zA-Z+]+://") + self.acount = 0 + self.list: List[ListElement] = [] + self.blockquote = 0 + self.pre = False + self.startpre = False + self.code = False + self.quote = False + self.br_toggle = "" + self.lastWasNL = False + self.lastWasList = False + self.style = 0 + self.style_def: Dict[str, Dict[str, str]] = {} + self.tag_stack: List[Tuple[str, Dict[str, Optional[str]], Dict[str, str]]] = [] + self.emphasis = 0 + self.drop_white_space = 0 + self.inheader = False + # Current abbreviation definition + self.abbr_title: Optional[str] = None + # Last inner HTML (for abbr being defined) + self.abbr_data: Optional[str] = None + # Stack of abbreviations to write later + self.abbr_list: Dict[str, str] = {} + self.baseurl = baseurl + self.stressed = False + self.preceding_stressed = False + self.preceding_data = "" + self.current_tag = "" + + config.UNIFIABLE["nbsp"] = " _place_holder;" + + def update_params(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + def feed(self, data: str) -> None: + data = data.replace("", "") + super().feed(data) + + def handle(self, data: str) -> str: + self.start = True + self.feed(data) + self.feed("") + markdown = self.optwrap(self.finish()) + if self.pad_tables: + return pad_tables_in_text(markdown) + else: + return markdown + + def outtextf(self, s: str) -> None: + self.outtextlist.append(s) + if s: + self.lastWasNL = s[-1] == "\n" + + def finish(self) -> str: + self.close() + + self.pbr() + self.o("", force="end") + + outtext = "".join(self.outtextlist) + + if self.unicode_snob: + nbsp = html.entities.html5["nbsp;"] + else: + nbsp = " " + outtext = outtext.replace(" _place_holder;", nbsp) + + # Clear self.outtextlist to avoid memory leak of its content to + # the next handling. + self.outtextlist = [] + + return outtext + + def handle_charref(self, c: str) -> None: + self.handle_data(self.charref(c), True) + + def handle_entityref(self, c: str) -> None: + ref = self.entityref(c) + + # ref may be an empty string (e.g. for ‎/‏ markers that should + # not contribute to the final output). + # self.handle_data cannot handle a zero-length string right after a + # stressed tag or mid-text within a stressed tag (text get split and + # self.stressed/self.preceding_stressed gets switched after the first + # part of that text). + if ref: + self.handle_data(ref, True) + + def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None: + self.handle_tag(tag, dict(attrs), start=True) + + def handle_endtag(self, tag: str) -> None: + self.handle_tag(tag, {}, start=False) + + def previousIndex(self, attrs: Dict[str, Optional[str]]) -> Optional[int]: + """ + :type attrs: dict + + :returns: The index of certain set of attributes (of a link) in the + self.a list. If the set of attributes is not found, returns None + :rtype: int + """ + if "href" not in attrs: + return None + + match = False + for i, a in enumerate(self.a): + if "href" in a.attrs and a.attrs["href"] == attrs["href"]: + if "title" in a.attrs or "title" in attrs: + if ( + "title" in a.attrs + and "title" in attrs + and a.attrs["title"] == attrs["title"] + ): + match = True + else: + match = True + + if match: + return i + return None + + def handle_emphasis( + self, start: bool, tag_style: Dict[str, str], parent_style: Dict[str, str] + ) -> None: + """ + Handles various text emphases + """ + tag_emphasis = google_text_emphasis(tag_style) + parent_emphasis = google_text_emphasis(parent_style) + + # handle Google's text emphasis + strikethrough = "line-through" in tag_emphasis and self.hide_strikethrough + + # google and others may mark a font's weight as `bold` or `700` + bold = False + for bold_marker in config.BOLD_TEXT_STYLE_VALUES: + bold = bold_marker in tag_emphasis and bold_marker not in parent_emphasis + if bold: + break + + italic = "italic" in tag_emphasis and "italic" not in parent_emphasis + fixed = ( + google_fixed_width_font(tag_style) + and not google_fixed_width_font(parent_style) + and not self.pre + ) + + if start: + # crossed-out text must be handled before other attributes + # in order not to output qualifiers unnecessarily + if bold or italic or fixed: + self.emphasis += 1 + if strikethrough: + self.quiet += 1 + if italic: + self.o(self.emphasis_mark) + self.drop_white_space += 1 + if bold: + self.o(self.strong_mark) + self.drop_white_space += 1 + if fixed: + self.o("`") + self.drop_white_space += 1 + self.code = True + else: + if bold or italic or fixed: + # there must not be whitespace before closing emphasis mark + self.emphasis -= 1 + self.space = False + if fixed: + if self.drop_white_space: + # empty emphasis, drop it + self.drop_white_space -= 1 + else: + self.o("`") + self.code = False + if bold: + if self.drop_white_space: + # empty emphasis, drop it + self.drop_white_space -= 1 + else: + self.o(self.strong_mark) + if italic: + if self.drop_white_space: + # empty emphasis, drop it + self.drop_white_space -= 1 + else: + self.o(self.emphasis_mark) + # space is only allowed after *all* emphasis marks + if (bold or italic) and not self.emphasis: + self.o(" ") + if strikethrough: + self.quiet -= 1 + + def handle_tag( + self, tag: str, attrs: Dict[str, Optional[str]], start: bool + ) -> None: + self.current_tag = tag + + if self.tag_callback is not None: + if self.tag_callback(self, tag, attrs, start) is True: + return + + # first thing inside the anchor tag is another tag + # that produces some output + if ( + start + and self.maybe_automatic_link is not None + and tag not in ["p", "div", "style", "dl", "dt"] + and (tag != "img" or self.ignore_images) + ): + self.o("[") + self.maybe_automatic_link = None + self.empty_link = False + + if self.google_doc: + # the attrs parameter is empty for a closing tag. in addition, we + # need the attributes of the parent nodes in order to get a + # complete style description for the current element. we assume + # that google docs export well formed html. + parent_style: Dict[str, str] = {} + if start: + if self.tag_stack: + parent_style = self.tag_stack[-1][2] + tag_style = element_style(attrs, self.style_def, parent_style) + self.tag_stack.append((tag, attrs, tag_style)) + else: + dummy, attrs, tag_style = ( + self.tag_stack.pop() if self.tag_stack else (None, {}, {}) + ) + if self.tag_stack: + parent_style = self.tag_stack[-1][2] + + if hn(tag): + # check if nh is inside of an 'a' tag (incorrect but found in the wild) + if self.astack: + if start: + self.inheader = True + # are inside link name, so only add '#' if it can appear before '[' + if self.outtextlist and self.outtextlist[-1] == "[": + self.outtextlist.pop() + self.space = False + self.o(hn(tag) * "#" + " ") + self.o("[") + else: + self.p_p = 0 # don't break up link name + self.inheader = False + return # prevent redundant emphasis marks on headers + else: + self.p() + if start: + self.inheader = True + self.o(hn(tag) * "#" + " ") + else: + self.inheader = False + return # prevent redundant emphasis marks on headers + + if tag in ["p", "div"]: + if self.google_doc: + if start and google_has_height(tag_style): + self.p() + else: + self.soft_br() + elif self.astack: + pass + elif self.split_next_td: + pass + else: + self.p() + + if tag == "br" and start: + if self.blockquote > 0: + self.o(" \n> ") + else: + self.o(" \n") + + if tag == "hr" and start: + self.p() + self.o("* * *") + self.p() + + if tag in ["head", "style", "script"]: + if start: + self.quiet += 1 + else: + self.quiet -= 1 + + if tag == "style": + if start: + self.style += 1 + else: + self.style -= 1 + + if tag in ["body"]: + self.quiet = 0 # sites like 9rules.com never close + + if tag == "blockquote": + if start: + self.p() + self.o("> ", force=True) + self.start = True + self.blockquote += 1 + else: + self.blockquote -= 1 + self.p() + + if tag in ["em", "i", "u"] and not self.ignore_emphasis: + # Separate with a space if we immediately follow an alphanumeric + # character, since otherwise Markdown won't render the emphasis + # marks, and we'll be left with eg 'foo_bar_' visible. + # (Don't add a space otherwise, though, since there isn't one in the + # original HTML.) + if ( + start + and self.preceding_data + and self.preceding_data[-1] not in string.whitespace + and self.preceding_data[-1] not in string.punctuation + ): + emphasis = " " + self.emphasis_mark + self.preceding_data += " " + else: + emphasis = self.emphasis_mark + + self.o(emphasis) + if start: + self.stressed = True + + if tag in ["strong", "b"] and not self.ignore_emphasis: + # Separate with space if we immediately follow an * character, since + # without it, Markdown won't render the resulting *** correctly. + # (Don't add a space otherwise, though, since there isn't one in the + # original HTML.) + if ( + start + and self.preceding_data + # When `self.strong_mark` is set to empty, the next condition + # will cause IndexError since it's trying to match the data + # with the first character of the `self.strong_mark`. + and len(self.strong_mark) > 0 + and self.preceding_data[-1] == self.strong_mark[0] + ): + strong = " " + self.strong_mark + self.preceding_data += " " + else: + strong = self.strong_mark + + self.o(strong) + if start: + self.stressed = True + + if tag in ["del", "strike", "s"]: + if start and self.preceding_data and self.preceding_data[-1] == "~": + strike = " ~~" + self.preceding_data += " " + else: + strike = "~~" + + self.o(strike) + if start: + self.stressed = True + + if self.google_doc: + if not self.inheader: + # handle some font attributes, but leave headers clean + self.handle_emphasis(start, tag_style, parent_style) + + if tag in ["kbd", "code", "tt"] and not self.pre: + self.o("`") # TODO: `` `this` `` + self.code = not self.code + + if tag == "abbr": + if start: + self.abbr_title = None + self.abbr_data = "" + if "title" in attrs: + self.abbr_title = attrs["title"] + else: + if self.abbr_title is not None: + assert self.abbr_data is not None + self.abbr_list[self.abbr_data] = self.abbr_title + self.abbr_title = None + self.abbr_data = None + + if tag == "q": + if not self.quote: + self.o(self.open_quote) + else: + self.o(self.close_quote) + self.quote = not self.quote + + def link_url(self: HTML2Text, link: str, title: str = "") -> None: + url = urlparse.urljoin(self.baseurl, link) + title = ' "{}"'.format(title) if title.strip() else "" + self.o("]({url}{title})".format(url=escape_md(url), title=title)) + + if tag == "a" and not self.ignore_links: + if start: + if ( + "href" in attrs + and attrs["href"] is not None + and not (self.skip_internal_links and attrs["href"].startswith("#")) + and not ( + self.ignore_mailto_links and attrs["href"].startswith("mailto:") + ) + ): + self.astack.append(attrs) + self.maybe_automatic_link = attrs["href"] + self.empty_link = True + if self.protect_links: + attrs["href"] = "<" + attrs["href"] + ">" + else: + self.astack.append(None) + else: + if self.astack: + a = self.astack.pop() + if self.maybe_automatic_link and not self.empty_link: + self.maybe_automatic_link = None + elif a: + assert a["href"] is not None + if self.empty_link: + self.o("[") + self.empty_link = False + self.maybe_automatic_link = None + if self.inline_links: + self.p_p = 0 + title = a.get("title") or "" + title = escape_md(title) + link_url(self, a["href"], title) + else: + i = self.previousIndex(a) + if i is not None: + a_props = self.a[i] + else: + self.acount += 1 + a_props = AnchorElement(a, self.acount, self.outcount) + self.a.append(a_props) + self.o("][" + str(a_props.count) + "]") + + if tag == "img" and start and not self.ignore_images: + if "src" in attrs and attrs["src"] is not None: + if not self.images_to_alt: + attrs["href"] = attrs["src"] + alt = attrs.get("alt") or self.default_image_alt + + # If we have images_with_size, write raw html including width, + # height, and alt attributes + if self.images_as_html or ( + self.images_with_size and ("width" in attrs or "height" in attrs) + ): + self.o("") + return + + # If we have a link to create, output the start + if self.maybe_automatic_link is not None: + href = self.maybe_automatic_link + if ( + self.images_to_alt + and escape_md(alt) == href + and self.absolute_url_matcher.match(href) + ): + self.o("<" + escape_md(alt) + ">") + self.empty_link = False + return + else: + self.o("[") + self.maybe_automatic_link = None + self.empty_link = False + + # If we have images_to_alt, we discard the image itself, + # considering only the alt text. + if self.images_to_alt: + self.o(escape_md(alt)) + else: + self.o("![" + escape_md(alt) + "]") + if self.inline_links: + href = attrs.get("href") or "" + self.o( + "(" + escape_md(urlparse.urljoin(self.baseurl, href)) + ")" + ) + else: + i = self.previousIndex(attrs) + if i is not None: + a_props = self.a[i] + else: + self.acount += 1 + a_props = AnchorElement(attrs, self.acount, self.outcount) + self.a.append(a_props) + self.o("[" + str(a_props.count) + "]") + + if tag == "dl" and start: + self.p() + if tag == "dt" and not start: + self.pbr() + if tag == "dd" and start: + self.o(" ") + if tag == "dd" and not start: + self.pbr() + + if tag in ["ol", "ul"]: + # Google Docs create sub lists as top level lists + if not self.list and not self.lastWasList: + self.p() + if start: + if self.google_doc: + list_style = google_list_style(tag_style) + else: + list_style = tag + numbering_start = list_numbering_start(attrs) + self.list.append(ListElement(list_style, numbering_start)) + else: + if self.list: + self.list.pop() + if not self.google_doc and not self.list: + self.o("\n") + self.lastWasList = True + else: + self.lastWasList = False + + if tag == "li": + self.pbr() + if start: + if self.list: + li = self.list[-1] + else: + li = ListElement("ul", 0) + if self.google_doc: + self.o(" " * self.google_nest_count(tag_style)) + else: + # Indent two spaces per list, except use three spaces for an + # unordered list inside an ordered list. + # https://spec.commonmark.org/0.28/#motivation + # TODO: line up
  1. s > 9 correctly. + parent_list = None + for list in self.list: + self.o( + " " if parent_list == "ol" and list.name == "ul" else " " + ) + parent_list = list.name + + if li.name == "ul": + self.o(self.ul_item_mark + " ") + elif li.name == "ol": + li.num += 1 + self.o(str(li.num) + ". ") + self.start = True + + if tag in ["table", "tr", "td", "th"]: + if self.ignore_tables: + if tag == "tr": + if start: + pass + else: + self.soft_br() + else: + pass + + elif self.bypass_tables: + if start: + self.soft_br() + if tag in ["td", "th"]: + if start: + self.o("<{}>\n\n".format(tag)) + else: + self.o("\n".format(tag)) + else: + if start: + self.o("<{}>".format(tag)) + else: + self.o("".format(tag)) + + else: + if tag == "table": + if start: + self.table_start = True + if self.pad_tables: + self.o("<" + config.TABLE_MARKER_FOR_PAD + ">") + self.o(" \n") + else: + if self.pad_tables: + # add break in case the table is empty or its 1 row table + self.soft_br() + self.o("") + self.o(" \n") + if tag in ["td", "th"] and start: + if self.split_next_td: + self.o("| ") + self.split_next_td = True + + if tag == "tr" and start: + self.td_count = 0 + if tag == "tr" and not start: + self.split_next_td = False + self.soft_br() + if tag == "tr" and not start and self.table_start: + # Underline table header + self.o("|".join(["---"] * self.td_count)) + self.soft_br() + self.table_start = False + if tag in ["td", "th"] and start: + self.td_count += 1 + + if tag == "pre": + if start: + self.startpre = True + self.pre = True + else: + self.pre = False + if self.mark_code: + self.out("\n[/code]") + self.p() + + if tag in ["sup", "sub"] and self.include_sup_sub: + if start: + self.o("<{}>".format(tag)) + else: + self.o("".format(tag)) + + # TODO: Add docstring for these one letter functions + def pbr(self) -> None: + "Pretty print has a line break" + if self.p_p == 0: + self.p_p = 1 + + def p(self) -> None: + "Set pretty print to 1 or 2 lines" + self.p_p = 1 if self.single_line_break else 2 + + def soft_br(self) -> None: + "Soft breaks" + self.pbr() + self.br_toggle = " " + + def o( + self, data: str, puredata: bool = False, force: Union[bool, str] = False + ) -> None: + """ + Deal with indentation and whitespace + """ + if self.abbr_data is not None: + self.abbr_data += data + + if not self.quiet: + if self.google_doc: + # prevent white space immediately after 'begin emphasis' + # marks ('**' and '_') + lstripped_data = data.lstrip() + if self.drop_white_space and not (self.pre or self.code): + data = lstripped_data + if lstripped_data != "": + self.drop_white_space = 0 + + if puredata and not self.pre: + # This is a very dangerous call ... it could mess up + # all handling of   when not handled properly + # (see entityref) + data = re.sub(r"\s+", r" ", data) + if data and data[0] == " ": + self.space = True + data = data[1:] + if not data and not force: + return + + if self.startpre: + # self.out(" :") #TODO: not output when already one there + if not data.startswith("\n") and not data.startswith("\r\n"): + #
    stuff...
    +                    data = "\n" + data
    +                if self.mark_code:
    +                    self.out("\n[code]")
    +                    self.p_p = 0
    +
    +            bq = ">" * self.blockquote
    +            if not (force and data and data[0] == ">") and self.blockquote:
    +                bq += " "
    +
    +            if self.pre:
    +                if not self.list:
    +                    bq += "    "
    +                # else: list content is already partially indented
    +                bq += "    " * len(self.list)
    +                data = data.replace("\n", "\n" + bq)
    +
    +            if self.startpre:
    +                self.startpre = False
    +                if self.list:
    +                    # use existing initial indentation
    +                    data = data.lstrip("\n")
    +
    +            if self.start:
    +                self.space = False
    +                self.p_p = 0
    +                self.start = False
    +
    +            if force == "end":
    +                # It's the end.
    +                self.p_p = 0
    +                self.out("\n")
    +                self.space = False
    +
    +            if self.p_p:
    +                self.out((self.br_toggle + "\n" + bq) * self.p_p)
    +                self.space = False
    +                self.br_toggle = ""
    +
    +            if self.space:
    +                if not self.lastWasNL:
    +                    self.out(" ")
    +                self.space = False
    +
    +            if self.a and (
    +                (self.p_p == 2 and self.links_each_paragraph) or force == "end"
    +            ):
    +                if force == "end":
    +                    self.out("\n")
    +
    +                newa = []
    +                for link in self.a:
    +                    if self.outcount > link.outcount:
    +                        self.out(
    +                            "   ["
    +                            + str(link.count)
    +                            + "]: "
    +                            + urlparse.urljoin(self.baseurl, link.attrs["href"])
    +                        )
    +                        if "title" in link.attrs and link.attrs["title"] is not None:
    +                            self.out(" (" + link.attrs["title"] + ")")
    +                        self.out("\n")
    +                    else:
    +                        newa.append(link)
    +
    +                # Don't need an extra line when nothing was done.
    +                if self.a != newa:
    +                    self.out("\n")
    +
    +                self.a = newa
    +
    +            if self.abbr_list and force == "end":
    +                for abbr, definition in self.abbr_list.items():
    +                    self.out("  *[" + abbr + "]: " + definition + "\n")
    +
    +            self.p_p = 0
    +            self.out(data)
    +            self.outcount += 1
    +
    +    def handle_data(self, data: str, entity_char: bool = False) -> None:
    +        if not data:
    +            # Data may be empty for some HTML entities. For example,
    +            # LEFT-TO-RIGHT MARK.
    +            return
    +
    +        if self.stressed:
    +            data = data.strip()
    +            self.stressed = False
    +            self.preceding_stressed = True
    +        elif self.preceding_stressed:
    +            if (
    +                re.match(r"[^][(){}\s.!?]", data[0])
    +                and not hn(self.current_tag)
    +                and self.current_tag not in ["a", "code", "pre"]
    +            ):
    +                # should match a letter or common punctuation
    +                data = " " + data
    +            self.preceding_stressed = False
    +
    +        if self.style:
    +            self.style_def.update(dumb_css_parser(data))
    +
    +        if self.maybe_automatic_link is not None:
    +            href = self.maybe_automatic_link
    +            if (
    +                href == data
    +                and self.absolute_url_matcher.match(href)
    +                and self.use_automatic_links
    +            ):
    +                self.o("<" + data + ">")
    +                self.empty_link = False
    +                return
    +            else:
    +                self.o("[")
    +                self.maybe_automatic_link = None
    +                self.empty_link = False
    +
    +        if not self.code and not self.pre and not entity_char:
    +            data = escape_md_section(data, snob=self.escape_snob, escape_dot=self.escape_dot, escape_plus=self.escape_plus, escape_dash=self.escape_dash)
    +        self.preceding_data = data
    +        self.o(data, puredata=True)
    +
    +    def charref(self, name: str) -> str:
    +        if name[0] in ["x", "X"]:
    +            c = int(name[1:], 16)
    +        else:
    +            c = int(name)
    +
    +        if not self.unicode_snob and c in unifiable_n:
    +            return unifiable_n[c]
    +        else:
    +            try:
    +                return chr(c)
    +            except ValueError:  # invalid unicode
    +                return ""
    +
    +    def entityref(self, c: str) -> str:
    +        if not self.unicode_snob and c in config.UNIFIABLE:
    +            return config.UNIFIABLE[c]
    +        try:
    +            ch = html.entities.html5[c + ";"]
    +        except KeyError:
    +            return "&" + c + ";"
    +        return config.UNIFIABLE[c] if c == "nbsp" else ch
    +
    +    def google_nest_count(self, style: Dict[str, str]) -> int:
    +        """
    +        Calculate the nesting count of google doc lists
    +
    +        :type style: dict
    +
    +        :rtype: int
    +        """
    +        nest_count = 0
    +        if "margin-left" in style:
    +            nest_count = int(style["margin-left"][:-2]) // self.google_list_indent
    +
    +        return nest_count
    +
    +    def optwrap(self, text: str) -> str:
    +        """
    +        Wrap all paragraphs in the provided text.
    +
    +        :type text: str
    +
    +        :rtype: str
    +        """
    +        if not self.body_width:
    +            return text
    +
    +        result = ""
    +        newlines = 0
    +        # I cannot think of a better solution for now.
    +        # To avoid the non-wrap behaviour for entire paras
    +        # because of the presence of a link in it
    +        if not self.wrap_links:
    +            self.inline_links = False
    +        for para in text.split("\n"):
    +            if len(para) > 0:
    +                if not skipwrap(
    +                    para, self.wrap_links, self.wrap_list_items, self.wrap_tables
    +                ):
    +                    indent = ""
    +                    if para.startswith("  " + self.ul_item_mark):
    +                        # list item continuation: add a double indent to the
    +                        # new lines
    +                        indent = "    "
    +                    elif para.startswith("> "):
    +                        # blockquote continuation: add the greater than symbol
    +                        # to the new lines
    +                        indent = "> "
    +                    wrapped = wrap(
    +                        para,
    +                        self.body_width,
    +                        break_long_words=False,
    +                        subsequent_indent=indent,
    +                    )
    +                    result += "\n".join(wrapped)
    +                    if para.endswith("  "):
    +                        result += "  \n"
    +                        newlines = 1
    +                    elif indent:
    +                        result += "\n"
    +                        newlines = 1
    +                    else:
    +                        result += "\n\n"
    +                        newlines = 2
    +                else:
    +                    # Warning for the tempted!!!
    +                    # Be aware that obvious replacement of this with
    +                    # line.isspace()
    +                    # DOES NOT work! Explanations are welcome.
    +                    if not config.RE_SPACE.match(para):
    +                        result += para + "\n"
    +                        newlines = 1
    +            else:
    +                if newlines < 2:
    +                    result += "\n"
    +                    newlines += 1
    +        return result
    +
    +
    +def html2text(html: str, baseurl: str = "", bodywidth: Optional[int] = None) -> str:
    +    if bodywidth is None:
    +        bodywidth = config.BODY_WIDTH
    +    h = HTML2Text(baseurl=baseurl, bodywidth=bodywidth)
    +
    +    return h.handle(html)
    diff --git a/crawl4ai/html2text/__main__.py b/crawl4ai/html2text/__main__.py
    new file mode 100644
    index 00000000..4e28416e
    --- /dev/null
    +++ b/crawl4ai/html2text/__main__.py
    @@ -0,0 +1,3 @@
    +from .cli import main
    +
    +main()
    diff --git a/crawl4ai/html2text/_typing.py b/crawl4ai/html2text/_typing.py
    new file mode 100644
    index 00000000..eed83251
    --- /dev/null
    +++ b/crawl4ai/html2text/_typing.py
    @@ -0,0 +1,2 @@
    +class OutCallback:
    +    def __call__(self, s: str) -> None: ...
    diff --git a/crawl4ai/html2text/cli.py b/crawl4ai/html2text/cli.py
    new file mode 100644
    index 00000000..01532274
    --- /dev/null
    +++ b/crawl4ai/html2text/cli.py
    @@ -0,0 +1,330 @@
    +import argparse
    +import sys
    +
    +from . import HTML2Text, __version__, config
    +
    +
    +def main() -> None:
    +    baseurl = ""
    +
    +    class bcolors:
    +        HEADER = "\033[95m"
    +        OKBLUE = "\033[94m"
    +        OKGREEN = "\033[92m"
    +        WARNING = "\033[93m"
    +        FAIL = "\033[91m"
    +        ENDC = "\033[0m"
    +        BOLD = "\033[1m"
    +        UNDERLINE = "\033[4m"
    +
    +    p = argparse.ArgumentParser()
    +    p.add_argument(
    +        "--default-image-alt",
    +        dest="default_image_alt",
    +        default=config.DEFAULT_IMAGE_ALT,
    +        help="The default alt string for images with missing ones",
    +    )
    +    p.add_argument(
    +        "--pad-tables",
    +        dest="pad_tables",
    +        action="store_true",
    +        default=config.PAD_TABLES,
    +        help="pad the cells to equal column width in tables",
    +    )
    +    p.add_argument(
    +        "--no-wrap-links",
    +        dest="wrap_links",
    +        action="store_false",
    +        default=config.WRAP_LINKS,
    +        help="don't wrap links during conversion",
    +    )
    +    p.add_argument(
    +        "--wrap-list-items",
    +        dest="wrap_list_items",
    +        action="store_true",
    +        default=config.WRAP_LIST_ITEMS,
    +        help="wrap list items during conversion",
    +    )
    +    p.add_argument(
    +        "--wrap-tables",
    +        dest="wrap_tables",
    +        action="store_true",
    +        default=config.WRAP_TABLES,
    +        help="wrap tables",
    +    )
    +    p.add_argument(
    +        "--ignore-emphasis",
    +        dest="ignore_emphasis",
    +        action="store_true",
    +        default=config.IGNORE_EMPHASIS,
    +        help="don't include any formatting for emphasis",
    +    )
    +    p.add_argument(
    +        "--reference-links",
    +        dest="inline_links",
    +        action="store_false",
    +        default=config.INLINE_LINKS,
    +        help="use reference style links instead of inline links",
    +    )
    +    p.add_argument(
    +        "--ignore-links",
    +        dest="ignore_links",
    +        action="store_true",
    +        default=config.IGNORE_ANCHORS,
    +        help="don't include any formatting for links",
    +    )
    +    p.add_argument(
    +        "--ignore-mailto-links",
    +        action="store_true",
    +        dest="ignore_mailto_links",
    +        default=config.IGNORE_MAILTO_LINKS,
    +        help="don't include mailto: links",
    +    )
    +    p.add_argument(
    +        "--protect-links",
    +        dest="protect_links",
    +        action="store_true",
    +        default=config.PROTECT_LINKS,
    +        help="protect links from line breaks surrounding them with angle brackets",
    +    )
    +    p.add_argument(
    +        "--ignore-images",
    +        dest="ignore_images",
    +        action="store_true",
    +        default=config.IGNORE_IMAGES,
    +        help="don't include any formatting for images",
    +    )
    +    p.add_argument(
    +        "--images-as-html",
    +        dest="images_as_html",
    +        action="store_true",
    +        default=config.IMAGES_AS_HTML,
    +        help=(
    +            "Always write image tags as raw html; preserves `height`, `width` and "
    +            "`alt` if possible."
    +        ),
    +    )
    +    p.add_argument(
    +        "--images-to-alt",
    +        dest="images_to_alt",
    +        action="store_true",
    +        default=config.IMAGES_TO_ALT,
    +        help="Discard image data, only keep alt text",
    +    )
    +    p.add_argument(
    +        "--images-with-size",
    +        dest="images_with_size",
    +        action="store_true",
    +        default=config.IMAGES_WITH_SIZE,
    +        help=(
    +            "Write image tags with height and width attrs as raw html to retain "
    +            "dimensions"
    +        ),
    +    )
    +    p.add_argument(
    +        "-g",
    +        "--google-doc",
    +        action="store_true",
    +        dest="google_doc",
    +        default=False,
    +        help="convert an html-exported Google Document",
    +    )
    +    p.add_argument(
    +        "-d",
    +        "--dash-unordered-list",
    +        action="store_true",
    +        dest="ul_style_dash",
    +        default=False,
    +        help="use a dash rather than a star for unordered list items",
    +    )
    +    p.add_argument(
    +        "-e",
    +        "--asterisk-emphasis",
    +        action="store_true",
    +        dest="em_style_asterisk",
    +        default=False,
    +        help="use an asterisk rather than an underscore for emphasized text",
    +    )
    +    p.add_argument(
    +        "-b",
    +        "--body-width",
    +        dest="body_width",
    +        type=int,
    +        default=config.BODY_WIDTH,
    +        help="number of characters per output line, 0 for no wrap",
    +    )
    +    p.add_argument(
    +        "-i",
    +        "--google-list-indent",
    +        dest="list_indent",
    +        type=int,
    +        default=config.GOOGLE_LIST_INDENT,
    +        help="number of pixels Google indents nested lists",
    +    )
    +    p.add_argument(
    +        "-s",
    +        "--hide-strikethrough",
    +        action="store_true",
    +        dest="hide_strikethrough",
    +        default=False,
    +        help="hide strike-through text. only relevant when -g is " "specified as well",
    +    )
    +    p.add_argument(
    +        "--escape-all",
    +        action="store_true",
    +        dest="escape_snob",
    +        default=False,
    +        help=(
    +            "Escape all special characters.  Output is less readable, but avoids "
    +            "corner case formatting issues."
    +        ),
    +    )
    +    p.add_argument(
    +        "--bypass-tables",
    +        action="store_true",
    +        dest="bypass_tables",
    +        default=config.BYPASS_TABLES,
    +        help="Format tables in HTML rather than Markdown syntax.",
    +    )
    +    p.add_argument(
    +        "--ignore-tables",
    +        action="store_true",
    +        dest="ignore_tables",
    +        default=config.IGNORE_TABLES,
    +        help="Ignore table-related tags (table, th, td, tr) " "while keeping rows.",
    +    )
    +    p.add_argument(
    +        "--single-line-break",
    +        action="store_true",
    +        dest="single_line_break",
    +        default=config.SINGLE_LINE_BREAK,
    +        help=(
    +            "Use a single line break after a block element rather than two line "
    +            "breaks. NOTE: Requires --body-width=0"
    +        ),
    +    )
    +    p.add_argument(
    +        "--unicode-snob",
    +        action="store_true",
    +        dest="unicode_snob",
    +        default=config.UNICODE_SNOB,
    +        help="Use unicode throughout document",
    +    )
    +    p.add_argument(
    +        "--no-automatic-links",
    +        action="store_false",
    +        dest="use_automatic_links",
    +        default=config.USE_AUTOMATIC_LINKS,
    +        help="Do not use automatic links wherever applicable",
    +    )
    +    p.add_argument(
    +        "--no-skip-internal-links",
    +        action="store_false",
    +        dest="skip_internal_links",
    +        default=config.SKIP_INTERNAL_LINKS,
    +        help="Do not skip internal links",
    +    )
    +    p.add_argument(
    +        "--links-after-para",
    +        action="store_true",
    +        dest="links_each_paragraph",
    +        default=config.LINKS_EACH_PARAGRAPH,
    +        help="Put links after each paragraph instead of document",
    +    )
    +    p.add_argument(
    +        "--mark-code",
    +        action="store_true",
    +        dest="mark_code",
    +        default=config.MARK_CODE,
    +        help="Mark program code blocks with [code]...[/code]",
    +    )
    +    p.add_argument(
    +        "--decode-errors",
    +        dest="decode_errors",
    +        default=config.DECODE_ERRORS,
    +        help=(
    +            "What to do in case of decode errors.'ignore', 'strict' and 'replace' are "
    +            "acceptable values"
    +        ),
    +    )
    +    p.add_argument(
    +        "--open-quote",
    +        dest="open_quote",
    +        default=config.OPEN_QUOTE,
    +        help="The character used to open quotes",
    +    )
    +    p.add_argument(
    +        "--close-quote",
    +        dest="close_quote",
    +        default=config.CLOSE_QUOTE,
    +        help="The character used to close quotes",
    +    )
    +    p.add_argument(
    +        "--version", action="version", version=".".join(map(str, __version__))
    +    )
    +    p.add_argument("filename", nargs="?")
    +    p.add_argument("encoding", nargs="?", default="utf-8")
    +    p.add_argument(
    +        "--include-sup-sub",
    +        dest="include_sup_sub",
    +        action="store_true",
    +        default=config.INCLUDE_SUP_SUB,
    +        help="Include the sup and sub tags",
    +    )
    +    args = p.parse_args()
    +
    +    if args.filename and args.filename != "-":
    +        with open(args.filename, "rb") as fp:
    +            data = fp.read()
    +    else:
    +        data = sys.stdin.buffer.read()
    +
    +    try:
    +        html = data.decode(args.encoding, args.decode_errors)
    +    except UnicodeDecodeError as err:
    +        warning = bcolors.WARNING + "Warning:" + bcolors.ENDC
    +        warning += " Use the " + bcolors.OKGREEN
    +        warning += "--decode-errors=ignore" + bcolors.ENDC + " flag."
    +        print(warning)
    +        raise err
    +
    +    h = HTML2Text(baseurl=baseurl)
    +    # handle options
    +    if args.ul_style_dash:
    +        h.ul_item_mark = "-"
    +    if args.em_style_asterisk:
    +        h.emphasis_mark = "*"
    +        h.strong_mark = "__"
    +
    +    h.body_width = args.body_width
    +    h.google_list_indent = args.list_indent
    +    h.ignore_emphasis = args.ignore_emphasis
    +    h.ignore_links = args.ignore_links
    +    h.ignore_mailto_links = args.ignore_mailto_links
    +    h.protect_links = args.protect_links
    +    h.ignore_images = args.ignore_images
    +    h.images_as_html = args.images_as_html
    +    h.images_to_alt = args.images_to_alt
    +    h.images_with_size = args.images_with_size
    +    h.google_doc = args.google_doc
    +    h.hide_strikethrough = args.hide_strikethrough
    +    h.escape_snob = args.escape_snob
    +    h.bypass_tables = args.bypass_tables
    +    h.ignore_tables = args.ignore_tables
    +    h.single_line_break = args.single_line_break
    +    h.inline_links = args.inline_links
    +    h.unicode_snob = args.unicode_snob
    +    h.use_automatic_links = args.use_automatic_links
    +    h.skip_internal_links = args.skip_internal_links
    +    h.links_each_paragraph = args.links_each_paragraph
    +    h.mark_code = args.mark_code
    +    h.wrap_links = args.wrap_links
    +    h.wrap_list_items = args.wrap_list_items
    +    h.wrap_tables = args.wrap_tables
    +    h.pad_tables = args.pad_tables
    +    h.default_image_alt = args.default_image_alt
    +    h.open_quote = args.open_quote
    +    h.close_quote = args.close_quote
    +    h.include_sup_sub = args.include_sup_sub
    +
    +    sys.stdout.write(h.handle(html))
    diff --git a/crawl4ai/html2text/config.py b/crawl4ai/html2text/config.py
    new file mode 100644
    index 00000000..d14ed64f
    --- /dev/null
    +++ b/crawl4ai/html2text/config.py
    @@ -0,0 +1,172 @@
    +import re
    +
    +# Use Unicode characters instead of their ascii pseudo-replacements
    +UNICODE_SNOB = False
    +
    +# Marker to use for marking tables for padding post processing
    +TABLE_MARKER_FOR_PAD = "special_marker_for_table_padding"
    +# Escape all special characters.  Output is less readable, but avoids
    +# corner case formatting issues.
    +ESCAPE_SNOB = False
    +ESCAPE_BACKSLASH = False
    +ESCAPE_DOT = False
    +ESCAPE_PLUS = False
    +ESCAPE_DASH = False
    +
    +# Put the links after each paragraph instead of at the end.
    +LINKS_EACH_PARAGRAPH = False
    +
    +# Wrap long lines at position. 0 for no wrapping.
    +BODY_WIDTH = 78
    +
    +# Don't show internal links (href="#local-anchor") -- corresponding link
    +# targets won't be visible in the plain text file anyway.
    +SKIP_INTERNAL_LINKS = True
    +
    +# Use inline, rather than reference, formatting for images and links
    +INLINE_LINKS = True
    +
    +# Protect links from line breaks surrounding them with angle brackets (in
    +# addition to their square brackets)
    +PROTECT_LINKS = False
    +# WRAP_LINKS = True
    +WRAP_LINKS = True
    +
    +# Wrap list items.
    +WRAP_LIST_ITEMS = False
    +
    +# Wrap tables
    +WRAP_TABLES = False
    +
    +# Number of pixels Google indents nested lists
    +GOOGLE_LIST_INDENT = 36
    +
    +# Values Google and others may use to indicate bold text
    +BOLD_TEXT_STYLE_VALUES = ("bold", "700", "800", "900")
    +
    +IGNORE_ANCHORS = False
    +IGNORE_MAILTO_LINKS = False
    +IGNORE_IMAGES = False
    +IMAGES_AS_HTML = False
    +IMAGES_TO_ALT = False
    +IMAGES_WITH_SIZE = False
    +IGNORE_EMPHASIS = False
    +MARK_CODE = False
    +DECODE_ERRORS = "strict"
    +DEFAULT_IMAGE_ALT = ""
    +PAD_TABLES = False
    +
    +# Convert links with same href and text to  format
    +# if they are absolute links
    +USE_AUTOMATIC_LINKS = True
    +
    +# For checking space-only lines on line 771
    +RE_SPACE = re.compile(r"\s\+")
    +
    +RE_ORDERED_LIST_MATCHER = re.compile(r"\d+\.\s")
    +RE_UNORDERED_LIST_MATCHER = re.compile(r"[-\*\+]\s")
    +RE_MD_CHARS_MATCHER = re.compile(r"([\\\[\]\(\)])")
    +RE_MD_CHARS_MATCHER_ALL = re.compile(r"([`\*_{}\[\]\(\)#!])")
    +
    +# to find links in the text
    +RE_LINK = re.compile(r"(\[.*?\] ?\(.*?\))|(\[.*?\]:.*?)")
    +
    +# to find table separators
    +RE_TABLE = re.compile(r" \| ")
    +
    +RE_MD_DOT_MATCHER = re.compile(
    +    r"""
    +    ^             # start of line
    +    (\s*\d+)      # optional whitespace and a number
    +    (\.)          # dot
    +    (?=\s)        # lookahead assert whitespace
    +    """,
    +    re.MULTILINE | re.VERBOSE,
    +)
    +RE_MD_PLUS_MATCHER = re.compile(
    +    r"""
    +    ^
    +    (\s*)
    +    (\+)
    +    (?=\s)
    +    """,
    +    flags=re.MULTILINE | re.VERBOSE,
    +)
    +RE_MD_DASH_MATCHER = re.compile(
    +    r"""
    +    ^
    +    (\s*)
    +    (-)
    +    (?=\s|\-)     # followed by whitespace (bullet list, or spaced out hr)
    +                  # or another dash (header or hr)
    +    """,
    +    flags=re.MULTILINE | re.VERBOSE,
    +)
    +RE_SLASH_CHARS = r"\`*_{}[]()#+-.!"
    +RE_MD_BACKSLASH_MATCHER = re.compile(
    +    r"""
    +    (\\)          # match one slash
    +    (?=[%s])      # followed by a char that requires escaping
    +    """
    +    % re.escape(RE_SLASH_CHARS),
    +    flags=re.VERBOSE,
    +)
    +
    +UNIFIABLE = {
    +    "rsquo": "'",
    +    "lsquo": "'",
    +    "rdquo": '"',
    +    "ldquo": '"',
    +    "copy": "(C)",
    +    "mdash": "--",
    +    "nbsp": " ",
    +    "rarr": "->",
    +    "larr": "<-",
    +    "middot": "*",
    +    "ndash": "-",
    +    "oelig": "oe",
    +    "aelig": "ae",
    +    "agrave": "a",
    +    "aacute": "a",
    +    "acirc": "a",
    +    "atilde": "a",
    +    "auml": "a",
    +    "aring": "a",
    +    "egrave": "e",
    +    "eacute": "e",
    +    "ecirc": "e",
    +    "euml": "e",
    +    "igrave": "i",
    +    "iacute": "i",
    +    "icirc": "i",
    +    "iuml": "i",
    +    "ograve": "o",
    +    "oacute": "o",
    +    "ocirc": "o",
    +    "otilde": "o",
    +    "ouml": "o",
    +    "ugrave": "u",
    +    "uacute": "u",
    +    "ucirc": "u",
    +    "uuml": "u",
    +    "lrm": "",
    +    "rlm": "",
    +}
    +
    +# Format tables in HTML rather than Markdown syntax
    +BYPASS_TABLES = False
    +# Ignore table-related tags (table, th, td, tr) while keeping rows
    +IGNORE_TABLES = False
    +
    +
    +# Use a single line break after a block element rather than two line breaks.
    +# NOTE: Requires body width setting to be 0.
    +SINGLE_LINE_BREAK = False
    +
    +
    +# Use double quotation marks when converting the  tag.
    +OPEN_QUOTE = '"'
    +CLOSE_QUOTE = '"'
    +
    +# Include the  and  tags
    +INCLUDE_SUP_SUB = False
    diff --git a/crawl4ai/html2text/elements.py b/crawl4ai/html2text/elements.py
    new file mode 100644
    index 00000000..2533ec08
    --- /dev/null
    +++ b/crawl4ai/html2text/elements.py
    @@ -0,0 +1,18 @@
    +from typing import Dict, Optional
    +
    +
    +class AnchorElement:
    +    __slots__ = ["attrs", "count", "outcount"]
    +
    +    def __init__(self, attrs: Dict[str, Optional[str]], count: int, outcount: int):
    +        self.attrs = attrs
    +        self.count = count
    +        self.outcount = outcount
    +
    +
    +class ListElement:
    +    __slots__ = ["name", "num"]
    +
    +    def __init__(self, name: str, num: int):
    +        self.name = name
    +        self.num = num
    diff --git a/crawl4ai/html2text/utils.py b/crawl4ai/html2text/utils.py
    new file mode 100644
    index 00000000..1909d2cf
    --- /dev/null
    +++ b/crawl4ai/html2text/utils.py
    @@ -0,0 +1,303 @@
    +import html.entities
    +from typing import Dict, List, Optional
    +
    +from . import config
    +
    +unifiable_n = {
    +    html.entities.name2codepoint[k]: v
    +    for k, v in config.UNIFIABLE.items()
    +    if k != "nbsp"
    +}
    +
    +
    +def hn(tag: str) -> int:
    +    if tag[0] == "h" and len(tag) == 2:
    +        n = tag[1]
    +        if "0" < n <= "9":
    +            return int(n)
    +    return 0
    +
    +
    +def dumb_property_dict(style: str) -> Dict[str, str]:
    +    """
    +    :returns: A hash of css attributes
    +    """
    +    return {
    +        x.strip().lower(): y.strip().lower()
    +        for x, y in [z.split(":", 1) for z in style.split(";") if ":" in z]
    +    }
    +
    +
    +def dumb_css_parser(data: str) -> Dict[str, Dict[str, str]]:
    +    """
    +    :type data: str
    +
    +    :returns: A hash of css selectors, each of which contains a hash of
    +    css attributes.
    +    :rtype: dict
    +    """
    +    # remove @import sentences
    +    data += ";"
    +    importIndex = data.find("@import")
    +    while importIndex != -1:
    +        data = data[0:importIndex] + data[data.find(";", importIndex) + 1 :]
    +        importIndex = data.find("@import")
    +
    +    # parse the css. reverted from dictionary comprehension in order to
    +    # support older pythons
    +    pairs = [x.split("{") for x in data.split("}") if "{" in x.strip()]
    +    try:
    +        elements = {a.strip(): dumb_property_dict(b) for a, b in pairs}
    +    except ValueError:
    +        elements = {}  # not that important
    +
    +    return elements
    +
    +
    +def element_style(
    +    attrs: Dict[str, Optional[str]],
    +    style_def: Dict[str, Dict[str, str]],
    +    parent_style: Dict[str, str],
    +) -> Dict[str, str]:
    +    """
    +    :type attrs: dict
    +    :type style_def: dict
    +    :type style_def: dict
    +
    +    :returns: A hash of the 'final' style attributes of the element
    +    :rtype: dict
    +    """
    +    style = parent_style.copy()
    +    if "class" in attrs:
    +        assert attrs["class"] is not None
    +        for css_class in attrs["class"].split():
    +            css_style = style_def.get("." + css_class, {})
    +            style.update(css_style)
    +    if "style" in attrs:
    +        assert attrs["style"] is not None
    +        immediate_style = dumb_property_dict(attrs["style"])
    +        style.update(immediate_style)
    +
    +    return style
    +
    +
    +def google_list_style(style: Dict[str, str]) -> str:
    +    """
    +    Finds out whether this is an ordered or unordered list
    +
    +    :type style: dict
    +
    +    :rtype: str
    +    """
    +    if "list-style-type" in style:
    +        list_style = style["list-style-type"]
    +        if list_style in ["disc", "circle", "square", "none"]:
    +            return "ul"
    +
    +    return "ol"
    +
    +
    +def google_has_height(style: Dict[str, str]) -> bool:
    +    """
    +    Check if the style of the element has the 'height' attribute
    +    explicitly defined
    +
    +    :type style: dict
    +
    +    :rtype: bool
    +    """
    +    return "height" in style
    +
    +
    +def google_text_emphasis(style: Dict[str, str]) -> List[str]:
    +    """
    +    :type style: dict
    +
    +    :returns: A list of all emphasis modifiers of the element
    +    :rtype: list
    +    """
    +    emphasis = []
    +    if "text-decoration" in style:
    +        emphasis.append(style["text-decoration"])
    +    if "font-style" in style:
    +        emphasis.append(style["font-style"])
    +    if "font-weight" in style:
    +        emphasis.append(style["font-weight"])
    +
    +    return emphasis
    +
    +
    +def google_fixed_width_font(style: Dict[str, str]) -> bool:
    +    """
    +    Check if the css of the current element defines a fixed width font
    +
    +    :type style: dict
    +
    +    :rtype: bool
    +    """
    +    font_family = ""
    +    if "font-family" in style:
    +        font_family = style["font-family"]
    +    return "courier new" == font_family or "consolas" == font_family
    +
    +
    +def list_numbering_start(attrs: Dict[str, Optional[str]]) -> int:
    +    """
    +    Extract numbering from list element attributes
    +
    +    :type attrs: dict
    +
    +    :rtype: int or None
    +    """
    +    if "start" in attrs:
    +        assert attrs["start"] is not None
    +        try:
    +            return int(attrs["start"]) - 1
    +        except ValueError:
    +            pass
    +
    +    return 0
    +
    +
    +def skipwrap(
    +    para: str, wrap_links: bool, wrap_list_items: bool, wrap_tables: bool
    +) -> bool:
    +    # If it appears to contain a link
    +    # don't wrap
    +    if not wrap_links and config.RE_LINK.search(para):
    +        return True
    +    # If the text begins with four spaces or one tab, it's a code block;
    +    # don't wrap
    +    if para[0:4] == "    " or para[0] == "\t":
    +        return True
    +
    +    # If the text begins with only two "--", possibly preceded by
    +    # whitespace, that's an emdash; so wrap.
    +    stripped = para.lstrip()
    +    if stripped[0:2] == "--" and len(stripped) > 2 and stripped[2] != "-":
    +        return False
    +
    +    # I'm not sure what this is for; I thought it was to detect lists,
    +    # but there's a 
    -inside- case in one of the tests that + # also depends upon it. + if stripped[0:1] in ("-", "*") and not stripped[0:2] == "**": + return not wrap_list_items + + # If text contains a pipe character it is likely a table + if not wrap_tables and config.RE_TABLE.search(para): + return True + + # If the text begins with a single -, *, or +, followed by a space, + # or an integer, followed by a ., followed by a space (in either + # case optionally proceeded by whitespace), it's a list; don't wrap. + return bool( + config.RE_ORDERED_LIST_MATCHER.match(stripped) + or config.RE_UNORDERED_LIST_MATCHER.match(stripped) + ) + + +def escape_md(text: str) -> str: + """ + Escapes markdown-sensitive characters within other markdown + constructs. + """ + return config.RE_MD_CHARS_MATCHER.sub(r"\\\1", text) + + +def escape_md_section( + text: str, + escape_backslash: bool = True, + snob: bool = False, + escape_dot: bool = True, + escape_plus: bool = True, + escape_dash: bool = True +) -> str: + """ + Escapes markdown-sensitive characters across whole document sections. + Each escaping operation can be controlled individually. + """ + if escape_backslash: + text = config.RE_MD_BACKSLASH_MATCHER.sub(r"\\\1", text) + + if snob: + text = config.RE_MD_CHARS_MATCHER_ALL.sub(r"\\\1", text) + + if escape_dot: + text = config.RE_MD_DOT_MATCHER.sub(r"\1\\\2", text) + + if escape_plus: + text = config.RE_MD_PLUS_MATCHER.sub(r"\1\\\2", text) + + if escape_dash: + text = config.RE_MD_DASH_MATCHER.sub(r"\1\\\2", text) + + return text + +def reformat_table(lines: List[str], right_margin: int) -> List[str]: + """ + Given the lines of a table + padds the cells and returns the new lines + """ + # find the maximum width of the columns + max_width = [len(x.rstrip()) + right_margin for x in lines[0].split("|")] + max_cols = len(max_width) + for line in lines: + cols = [x.rstrip() for x in line.split("|")] + num_cols = len(cols) + + # don't drop any data if colspan attributes result in unequal lengths + if num_cols < max_cols: + cols += [""] * (max_cols - num_cols) + elif max_cols < num_cols: + max_width += [len(x) + right_margin for x in cols[-(num_cols - max_cols) :]] + max_cols = num_cols + + max_width = [ + max(len(x) + right_margin, old_len) for x, old_len in zip(cols, max_width) + ] + + # reformat + new_lines = [] + for line in lines: + cols = [x.rstrip() for x in line.split("|")] + if set(line.strip()) == set("-|"): + filler = "-" + new_cols = [ + x.rstrip() + (filler * (M - len(x.rstrip()))) + for x, M in zip(cols, max_width) + ] + new_lines.append("|-" + "|".join(new_cols) + "|") + else: + filler = " " + new_cols = [ + x.rstrip() + (filler * (M - len(x.rstrip()))) + for x, M in zip(cols, max_width) + ] + new_lines.append("| " + "|".join(new_cols) + "|") + return new_lines + + +def pad_tables_in_text(text: str, right_margin: int = 1) -> str: + """ + Provide padding for tables in the text + """ + lines = text.split("\n") + table_buffer = [] # type: List[str] + table_started = False + new_lines = [] + for line in lines: + # Toggle table started + if config.TABLE_MARKER_FOR_PAD in line: + table_started = not table_started + if not table_started: + table = reformat_table(table_buffer, right_margin) + new_lines.extend(table) + table_buffer = [] + new_lines.append("") + continue + # Process lines + if table_started: + table_buffer.append(line) + else: + new_lines.append(line) + return "\n".join(new_lines) diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 711fd2c4..34ab219b 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -1,13 +1,12 @@ import time from concurrent.futures import ThreadPoolExecutor, as_completed from bs4 import BeautifulSoup, Comment, element, Tag, NavigableString -import html2text import json import html import re import os import platform -from html2text import HTML2Text +from .html2text import HTML2Text from .prompts import PROMPT_EXTRACT_BLOCKS from .config import * from pathlib import Path @@ -182,9 +181,22 @@ def escape_json_string(s): class CustomHTML2Text(HTML2Text): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.ignore_links = True self.inside_pre = False self.inside_code = False + + self.skip_internal_links = False + self.single_line_break = False + self.mark_code = False + self.include_sup_sub = False + self.body_width = 0 + self.ignore_mailto_links = True + self.ignore_links = False + self.escape_backslash = False + self.escape_dot = False + self.escape_plus = False + self.escape_dash = False + self.escape_snob = False + def handle_tag(self, tag, attrs, start): if tag == 'pre': @@ -194,6 +206,10 @@ class CustomHTML2Text(HTML2Text): else: self.o('\n```') self.inside_pre = False + elif tag in ["h1", "h2", "h3", "h4", "h5", "h6"]: + pass + + # elif tag == 'code' and not self.inside_pre: # if start: # if not self.inside_pre: From 1dd36f90359b2a487c9d3faf89711b4818c7c833 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sun, 20 Oct 2024 19:11:18 +0800 Subject: [PATCH 09/14] Refactor content scrapping strategy and improve error handling --- crawl4ai/async_webcrawler.py | 2 +- crawl4ai/content_scrapping_strategy.py | 112 +++++++++++++------------ 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 94938b60..2a12c775 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -187,7 +187,7 @@ class AsyncWebCrawler: t1 = time.time() scrapping_strategy = WebScrappingStrategy() # result = await scrapping_strategy.ascrap( - result = await scrapping_strategy.scrap( + result = scrapping_strategy.scrap( url, html, word_count_threshold=word_count_threshold, diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 640d1d15..da163539 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -196,52 +196,65 @@ class WebScrappingStrategy(ContentScrappingStrategy): return False keep_element = False + + social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('social_media_domains', []) + social_media_domains = list(set(social_media_domains)) - if element.name == 'a' and element.get('href'): - href = element['href'] - url_base = url.split('/')[2] - link_data = {'href': href, 'text': element.get_text()} - if href.startswith('http') and url_base not in href: - links['external'].append(link_data) - else: - links['internal'].append(link_data) - keep_element = True - - if kwargs.get('exclude_external_links', True): - href_url_base = href.split('/')[2] - if url_base not in href_url_base: - element.decompose() - return False - - # Check if we should esclude links to all major social media platforms - if not kwargs.get('exclude_external_links', False) and kwargs.get('exclude_social_media_links', True): - social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('social_media_domains', []) - social_media_domains = list(set(social_media_domains)) - if any(domain in href for domain in social_media_domains): - element.decompose() - return False - - elif element.name == 'img': - # Check flag if we should remove external images - if kwargs.get('exclude_external_images', False): - src = element.get('src', '') - src_url_base = src.split('/')[2] + try: + if element.name == 'a' and element.get('href'): + href = element['href'] url_base = url.split('/')[2] - if url_base not in src_url_base: - element.decompose() - return False + link_data = {'href': href, 'text': element.get_text()} + if href.startswith('http') and url_base not in href: + links['external'].append(link_data) + else: + links['internal'].append(link_data) + keep_element = True - if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', True): - src = element.get('src', '') - src_url_base = src.split('/')[2] - url_base = url.split('/')[2] - if any(domain in src for domain in SOCIAL_MEDIA_DOMAINS): - element.decompose() - return False - - return True # Always keep image elements + if kwargs.get('exclude_external_links', True): + href_parts = href.split('/') + href_url_base = href_parts[2] if len(href_parts) > 2 else href + if url_base not in href_url_base: + element.decompose() + return False + + if not kwargs.get('exclude_external_links', False) and kwargs.get('exclude_social_media_links', True): + if any(domain in href for domain in social_media_domains): + element.decompose() + return False + except Exception as e: + raise "Error processing links" - elif element.name in ['video', 'audio']: + try: + if element.name == 'img': + # Check flag if we should remove external images + if kwargs.get('exclude_external_images', False): + src = element.get('src', '') + src_url_base = src.split('/')[2] + url_base = url.split('/')[2] + if url_base not in src_url_base: + element.decompose() + return False + + if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', True): + src = element.get('src', '') + src_url_base = src.split('/')[2] + url_base = url.split('/')[2] + if any(domain in src for domain in social_media_domains): + element.decompose() + return False + + return True # Always keep image elements + except Exception as e: + raise "Error processing images" + + + # Check if flag to remove all forms is set + if kwargs.get('remove_forms', False) and element.name == 'form': + element.decompose() + return False + + if element.name in ['video', 'audio']: media[f"{element.name}s"].append({ 'src': element.get('src'), 'alt': element.get('alt'), @@ -262,13 +275,11 @@ class WebScrappingStrategy(ContentScrappingStrategy): if kwargs.get('only_text', False): element.replace_with(element.get_text()) - remove_unwanted_attributes(element, IMPORTANT_ATTRS, kwargs.get('keep_data_attributes', False)) - # for attr in element.attrs: - # if attr not in IMPORTANT_ATTRS or (attr.startswith('data-') and not kwargs.get('keep_data_attributes', False)): - # del element[attr] - - # Print element name and attributes - print(element.name, element.attrs) + try: + remove_unwanted_attributes(element, IMPORTANT_ATTRS, kwargs.get('keep_data_attributes', False)) + except Exception as e: + print('Error removing unwanted attributes:', str(e)) + # Process children for child in list(element.children): @@ -304,9 +315,6 @@ class WebScrappingStrategy(ContentScrappingStrategy): process_element(body) # # Process images using ThreadPoolExecutor - - - imgs = body.find_all('img') with ThreadPoolExecutor() as executor: From 04d16e6d2bf84449550f748f2274b8513e5ee1f8 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sun, 20 Oct 2024 19:25:25 +0800 Subject: [PATCH 10/14] Fix Base64 image parsing in WebScrappingStrategy (issue 182) - Add support for extracting Base64 encoded images - Improve image format detection to include Base64 images - Enhance compatibility with locally saved HTML files using Base64 image encoding --- CHANGELOG.md | 3 +++ crawl4ai/content_scrapping_strategy.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e77217..b79b37ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [v0.3.72] - 2024-10-20 +### Fixed +- Added support for parsing Base64 encoded images in WebScrappingStrategy + ### Added - Forked and integrated a customized version of the html2text library for more control over Markdown generation - New configuration options for controlling external content: diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index da163539..139779ea 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -127,7 +127,11 @@ class WebScrappingStrategy(ContentScrappingStrategy): image_width = img.get('width') width_value, width_unit = parse_dimension(image_width) image_size = 0 #int(fetch_image_file_size(img,base_url) or 0) - image_format = os.path.splitext(img.get('src',''))[1].lower() + image_src = img.get('src','') + if "data:image/" in image_src: + image_format = image_src.split(',')[0].split(';')[0].split('/')[1] + else: + image_format = os.path.splitext(img.get('src',''))[1].lower() # Remove . from format image_format = image_format.strip('.').split('?')[0] score = 0 From 60ba131ac8a18843cb2239555a0a73ecc98a9da5 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Tue, 22 Oct 2024 20:19:22 +0800 Subject: [PATCH 11/14] [v0.3.72] Enhance content extraction and proxy support - Add ContentCleaningStrategy for improved content extraction - Implement advanced proxy configuration with authentication - Enhance image source detection and handling - Add fit_markdown and fit_html for refined content output - Improve external link and image handling flexibility --- CHANGELOG.md | 34 +++++ crawl4ai/async_crawler_strategy.py | 4 + crawl4ai/async_webcrawler.py | 4 + crawl4ai/content_cleaning_strategy.py | 196 +++++++++++++++++++++++++ crawl4ai/content_scrapping_strategy.py | 23 ++- crawl4ai/models.py | 2 + 6 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 crawl4ai/content_cleaning_strategy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b79b37ed..6ab493ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [v0.3.72] - 2024-10-22 + +### Added +- New `ContentCleaningStrategy` class: + - Smart content extraction based on text density and element scoring + - Automatic removal of boilerplate content + - DOM tree analysis for better content identification + - Configurable thresholds for content detection +- Advanced proxy support: + - Added `proxy_config` option for authenticated proxy connections + - Support for username/password in proxy configuration +- New content output formats: + - `fit_markdown`: Optimized markdown output with main content focus + - `fit_html`: Clean HTML with only essential content + +### Enhanced +- Image source detection: + - Support for multiple image source attributes (`src`, `data-src`, `srcset`, etc.) + - Automatic fallback through potential source attributes + - Smart handling of srcset attribute +- External content handling: + - Made external link exclusion optional (disabled by default) + - Improved detection and handling of social media links + - Better control over external image filtering + +### Fixed +- Image extraction reliability with multiple source attribute checks +- External link and image handling logic for better accuracy + +### Developer Notes +- The new `ContentCleaningStrategy` uses configurable thresholds for customization +- Proxy configuration now supports more complex authentication scenarios +- Content extraction process now provides both regular and optimized outputs + ## [v0.3.72] - 2024-10-20 ### Fixed diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index ca6e4c5b..1ddb32da 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -71,6 +71,7 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" ) self.proxy = kwargs.get("proxy") + self.proxy_config = kwargs.get("proxy_config") self.headless = kwargs.get("headless", True) self.browser_type = kwargs.get("browser_type", "chromium") self.headers = kwargs.get("headers", {}) @@ -121,6 +122,9 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): if self.proxy: proxy_settings = ProxySettings(server=self.proxy) browser_args["proxy"] = proxy_settings + elif self.proxy_config: + proxy_settings = ProxySettings(server=self.proxy_config.get("server"), username=self.proxy_config.get("username"), password=self.proxy_config.get("password")) + browser_args["proxy"] = proxy_settings # Select the appropriate browser based on the browser_type if self.browser_type == "firefox": diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 2a12c775..005523eb 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -212,6 +212,8 @@ class AsyncWebCrawler: cleaned_html = sanitize_input_encode(result.get("cleaned_html", "")) markdown = sanitize_input_encode(result.get("markdown", "")) + fit_markdown = sanitize_input_encode(result.get("fit_markdown", "")) + fit_html = sanitize_input_encode(result.get("fit_html", "")) media = result.get("media", []) links = result.get("links", []) metadata = result.get("metadata", {}) @@ -258,6 +260,8 @@ class AsyncWebCrawler: html=html, cleaned_html=format_html(cleaned_html), markdown=markdown, + fit_markdown=fit_markdown, + fit_html= fit_html, media=media, links=links, metadata=metadata, diff --git a/crawl4ai/content_cleaning_strategy.py b/crawl4ai/content_cleaning_strategy.py new file mode 100644 index 00000000..2f052f76 --- /dev/null +++ b/crawl4ai/content_cleaning_strategy.py @@ -0,0 +1,196 @@ +from bs4 import BeautifulSoup, Tag +import re +from typing import Optional + +class ContentCleaningStrategy: + def __init__(self): + # Precompile regex patterns for performance + self.negative_patterns = re.compile(r'nav|footer|header|sidebar|ads|comment', re.I) + self.positive_patterns = re.compile(r'content|article|main|post', re.I) + self.priority_tags = {'article', 'main', 'section', 'div'} + self.non_content_tags = {'nav', 'footer', 'header', 'aside'} + # Thresholds + self.text_density_threshold = 9.0 + self.min_word_count = 50 + self.link_density_threshold = 0.2 + self.max_dom_depth = 10 # To prevent excessive DOM traversal + + def clean(self, clean_html: str) -> str: + """ + Main function that takes cleaned HTML and returns super cleaned HTML. + + Args: + clean_html (str): The cleaned HTML content. + + Returns: + str: The super cleaned HTML containing only the main content. + """ + try: + if not clean_html or not isinstance(clean_html, str): + return '' + soup = BeautifulSoup(clean_html, 'html.parser') + main_content = self.extract_main_content(soup) + if main_content: + super_clean_element = self.clean_element(main_content) + return str(super_clean_element) + else: + return '' + except Exception: + # Handle exceptions silently or log them as needed + return '' + + def extract_main_content(self, soup: BeautifulSoup) -> Optional[Tag]: + """ + Identifies and extracts the main content element from the HTML. + + Args: + soup (BeautifulSoup): The parsed HTML soup. + + Returns: + Optional[Tag]: The Tag object containing the main content, or None if not found. + """ + candidates = [] + for element in soup.find_all(self.priority_tags): + if self.is_non_content_tag(element): + continue + if self.has_negative_class_id(element): + continue + score = self.calculate_content_score(element) + candidates.append((score, element)) + + if not candidates: + return None + + # Sort candidates by score in descending order + candidates.sort(key=lambda x: x[0], reverse=True) + # Select the element with the highest score + best_element = candidates[0][1] + return best_element + + def calculate_content_score(self, element: Tag) -> float: + """ + Calculates a score for an element based on various heuristics. + + Args: + element (Tag): The HTML element to score. + + Returns: + float: The content score of the element. + """ + score = 0.0 + + if self.is_priority_tag(element): + score += 5.0 + if self.has_positive_class_id(element): + score += 3.0 + if self.has_negative_class_id(element): + score -= 3.0 + if self.is_high_text_density(element): + score += 2.0 + if self.is_low_link_density(element): + score += 2.0 + if self.has_sufficient_content(element): + score += 2.0 + if self.has_headings(element): + score += 3.0 + + dom_depth = self.calculate_dom_depth(element) + score += min(dom_depth, self.max_dom_depth) * 0.5 # Adjust weight as needed + + return score + + def is_priority_tag(self, element: Tag) -> bool: + """Checks if the element is a priority tag.""" + return element.name in self.priority_tags + + def is_non_content_tag(self, element: Tag) -> bool: + """Checks if the element is a non-content tag.""" + return element.name in self.non_content_tags + + def has_negative_class_id(self, element: Tag) -> bool: + """Checks if the element has negative indicators in its class or id.""" + class_id = ' '.join(filter(None, [ + self.get_attr_str(element.get('class')), + element.get('id', '') + ])) + return bool(self.negative_patterns.search(class_id)) + + def has_positive_class_id(self, element: Tag) -> bool: + """Checks if the element has positive indicators in its class or id.""" + class_id = ' '.join(filter(None, [ + self.get_attr_str(element.get('class')), + element.get('id', '') + ])) + return bool(self.positive_patterns.search(class_id)) + + @staticmethod + def get_attr_str(attr) -> str: + """Converts an attribute value to a string.""" + if isinstance(attr, list): + return ' '.join(attr) + elif isinstance(attr, str): + return attr + else: + return '' + + def is_high_text_density(self, element: Tag) -> bool: + """Determines if the element has high text density.""" + text_density = self.calculate_text_density(element) + return text_density > self.text_density_threshold + + def calculate_text_density(self, element: Tag) -> float: + """Calculates the text density of an element.""" + text_length = len(element.get_text(strip=True)) + tag_count = len(element.find_all()) + tag_count = tag_count or 1 # Prevent division by zero + return text_length / tag_count + + def is_low_link_density(self, element: Tag) -> bool: + """Determines if the element has low link density.""" + link_density = self.calculate_link_density(element) + return link_density < self.link_density_threshold + + def calculate_link_density(self, element: Tag) -> float: + """Calculates the link density of an element.""" + text = element.get_text(strip=True) + if not text: + return 0.0 + link_text = ' '.join(a.get_text(strip=True) for a in element.find_all('a')) + return len(link_text) / len(text) if text else 0.0 + + def has_sufficient_content(self, element: Tag) -> bool: + """Checks if the element has sufficient word count.""" + word_count = len(element.get_text(strip=True).split()) + return word_count >= self.min_word_count + + def calculate_dom_depth(self, element: Tag) -> int: + """Calculates the depth of an element in the DOM tree.""" + depth = 0 + current_element = element + while current_element.parent and depth < self.max_dom_depth: + depth += 1 + current_element = current_element.parent + return depth + + def has_headings(self, element: Tag) -> bool: + """Checks if the element contains heading tags.""" + return bool(element.find(['h1', 'h2', 'h3'])) + + def clean_element(self, element: Tag) -> Tag: + """ + Cleans the selected element by removing unnecessary attributes and nested non-content elements. + + Args: + element (Tag): The HTML element to clean. + + Returns: + Tag: The cleaned HTML element. + """ + for tag in element.find_all(['script', 'style', 'aside']): + tag.decompose() + for tag in element.find_all(): + attrs = dict(tag.attrs) + for attr in attrs: + if attr in ['style', 'onclick', 'onmouseover', 'align', 'bgcolor']: + del tag.attrs[attr] + return element diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 139779ea..0c472f0d 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -7,6 +7,7 @@ from .config import * from bs4 import element, NavigableString, Comment from urllib.parse import urljoin from requests.exceptions import InvalidSchema +from .content_cleaning_strategy import ContentCleaningStrategy from .utils import ( sanitize_input_encode, @@ -215,7 +216,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): links['internal'].append(link_data) keep_element = True - if kwargs.get('exclude_external_links', True): + if kwargs.get('exclude_external_links', False): href_parts = href.split('/') href_url_base = href_parts[2] if len(href_parts) > 2 else href if url_base not in href_url_base: @@ -231,9 +232,20 @@ class WebScrappingStrategy(ContentScrappingStrategy): try: if element.name == 'img': + potential_sources = ['src', 'data-src', 'srcset' 'data-lazy-src', 'data-original'] + src = element.get('src', '') + while not src and potential_sources: + src = element.get(potential_sources.pop(0), '') + if not src: + element.decompose() + return False + + # If it is srcset pick up the first image + if 'srcset' in element.attrs: + src = element.attrs['srcset'].split(',')[0].split(' ')[0] + # Check flag if we should remove external images if kwargs.get('exclude_external_images', False): - src = element.get('src', '') src_url_base = src.split('/')[2] url_base = url.split('/')[2] if url_base not in src_url_base: @@ -241,7 +253,6 @@ class WebScrappingStrategy(ContentScrappingStrategy): return False if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', True): - src = element.get('src', '') src_url_base = src.split('/')[2] url_base = url.split('/')[2] if any(domain in src for domain in social_media_domains): @@ -386,10 +397,16 @@ class WebScrappingStrategy(ContentScrappingStrategy): except Exception as e: print('Error extracting metadata:', str(e)) meta = {} + + cleaner = ContentCleaningStrategy() + fit_html = cleaner.clean(cleaned_html) + fit_markdown = h.handle(fit_html) cleaned_html = sanitize_html(cleaned_html) return { 'markdown': markdown, + 'fit_markdown': fit_markdown, + 'fit_html': fit_html, 'cleaned_html': cleaned_html, 'success': success, 'media': media, diff --git a/crawl4ai/models.py b/crawl4ai/models.py index 151ccb4f..4ac06797 100644 --- a/crawl4ai/models.py +++ b/crawl4ai/models.py @@ -14,6 +14,8 @@ class CrawlResult(BaseModel): links: Dict[str, List[Dict]] = {} screenshot: Optional[str] = None markdown: Optional[str] = None + fit_markdown: Optional[str] = None + fit_html: Optional[str] = None extracted_content: Optional[str] = None metadata: Optional[dict] = None error_message: Optional[str] = None From bcfe83f70238aa78fd8e30283a3daaee5631dcf9 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Thu, 24 Oct 2024 20:22:47 +0800 Subject: [PATCH 12/14] feat: enhance crawler with overlay removal and improved screenshot capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit β€’ Add smart overlay removal system for handling popups and modals β€’ Improve screenshot functionality with configurable timing controls β€’ Implement URL normalization and enhanced link processing β€’ Add custom base directory support for cache storage β€’ Refine external content filtering and social media domain handling This commit significantly improves the crawler's ability to handle modern websites by automatically removing intrusive overlays and providing better screenshot capabilities. URL handling is now more robust with proper normalization and duplicate detection. The cache system is more flexible with customizable base directory support. Breaking changes: None Issue numbers: None --- CHANGELOG.md | 47 +++++++ crawl4ai/async_crawler_strategy.py | 184 +++++++++++++++++++++---- crawl4ai/async_webcrawler.py | 4 +- crawl4ai/config.py | 7 - crawl4ai/content_scrapping_strategy.py | 75 +++++++--- crawl4ai/extraction_strategy.py | 3 +- crawl4ai/utils.py | 49 +++++++ 7 files changed, 319 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab493ff..9c5d35bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## [v0.3.73] - 2024-10-24 + +### Added +- Smart overlay removal system in AsyncPlaywrightCrawlerStrategy: + - Automatic removal of popups, modals, and cookie notices + - Detection and removal of fixed/sticky position elements + - Cleaning of empty block elements + - Configurable via `remove_overlay_elements` parameter +- Enhanced screenshot capabilities: + - Added `screenshot_wait_for` parameter to control timing + - Improved screenshot handling with existing page context + - Better error handling with fallback error images +- New URL normalization utilities: + - `normalize_url` function for consistent URL formatting + - `is_external_url` function for better link classification +- Custom base directory support for cache storage: + - New `base_directory` parameter in AsyncWebCrawler + - Allows specifying alternative locations for `.crawl4ai` folder + +### Enhanced +- Link handling improvements: + - Better duplicate link detection + - Enhanced internal/external link classification + - Improved handling of special URL protocols + - Support for anchor links and protocol-relative URLs +- Configuration refinements: + - Streamlined social media domain list + - More focused external content filtering +- LLM extraction strategy: + - Added support for separate API base URL via `api_base` parameter + - Better handling of base URLs in configuration + +### Fixed +- Screenshot functionality: + - Resolved issues with screenshot timing and context + - Improved error handling and recovery +- Link processing: + - Fixed URL normalization edge cases + - Better handling of invalid URLs + - Improved error messages for link processing failures + +### Developer Notes +- The overlay removal system uses advanced JavaScript injection for better compatibility +- URL normalization handles special cases like mailto:, tel:, and protocol-relative URLs +- Screenshot system now reuses existing page context for better performance +- Link processing maintains separate dictionaries for internal and external links to ensure uniqueness + ## [v0.3.72] - 2024-10-22 ### Added diff --git a/crawl4ai/async_crawler_strategy.py b/crawl4ai/async_crawler_strategy.py index 1ddb32da..fe0b6767 100644 --- a/crawl4ai/async_crawler_strategy.py +++ b/crawl4ai/async_crawler_strategy.py @@ -51,7 +51,7 @@ class AsyncCrawlerStrategy(ABC): pass @abstractmethod - async def take_screenshot(self, url: str) -> str: + async def take_screenshot(self, **kwargs) -> str: pass @abstractmethod @@ -502,13 +502,21 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): if delay_before_return_html: await asyncio.sleep(delay_before_return_html) + # Check for remove_overlay_elements parameter + if kwargs.get("remove_overlay_elements", False): + await self.remove_overlay_elements(page) + html = await page.content() await self.execute_hook('before_return_html', page, html) # Check if kwargs has screenshot=True then take screenshot screenshot_data = None if kwargs.get("screenshot"): - screenshot_data = await self.take_screenshot(url) + # Check we have screenshot_wait_for parameter, if we have simply wait for that time + screenshot_wait_for = kwargs.get("screenshot_wait_for") + if screenshot_wait_for: + await asyncio.sleep(screenshot_wait_for) + screenshot_data = await self.take_screenshot(page) if self.verbose: print(f"[LOG] βœ… Crawled {url} successfully!") @@ -559,28 +567,156 @@ class AsyncPlaywrightCrawlerStrategy(AsyncCrawlerStrategy): results = await asyncio.gather(*tasks, return_exceptions=True) return [result if not isinstance(result, Exception) else str(result) for result in results] - async def take_screenshot(self, url: str, wait_time=1000) -> str: - async with await self.browser.new_context(user_agent=self.user_agent) as context: - page = await context.new_page() - try: - await page.goto(url, wait_until="domcontentloaded", timeout=30000) - # Wait for a specified time (default is 1 second) - await page.wait_for_timeout(wait_time) - screenshot = await page.screenshot(full_page=True) - return base64.b64encode(screenshot).decode('utf-8') - except Exception as e: - error_message = f"Failed to take screenshot: {str(e)}" - print(error_message) + async def remove_overlay_elements(self, page: Page) -> None: + """ + Removes popup overlays, modals, cookie notices, and other intrusive elements from the page. + + Args: + page (Page): The Playwright page instance + """ + remove_overlays_js = """ + async () => { + // Function to check if element is visible + const isVisible = (elem) => { + const style = window.getComputedStyle(elem); + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0'; + }; - # Generate an error image - img = Image.new('RGB', (800, 600), color='black') - draw = ImageDraw.Draw(img) - font = ImageFont.load_default() - draw.text((10, 10), error_message, fill=(255, 255, 255), font=font) + // Common selectors for popups and overlays + const commonSelectors = [ + // Close buttons first + 'button[class*="close" i]', 'button[class*="dismiss" i]', + 'button[aria-label*="close" i]', 'button[title*="close" i]', + 'a[class*="close" i]', 'span[class*="close" i]', - buffered = BytesIO() - img.save(buffered, format="JPEG") - return base64.b64encode(buffered.getvalue()).decode('utf-8') - finally: - await page.close() + // Cookie notices + '[class*="cookie-banner" i]', '[id*="cookie-banner" i]', + '[class*="cookie-consent" i]', '[id*="cookie-consent" i]', + + // Newsletter/subscription dialogs + '[class*="newsletter" i]', '[class*="subscribe" i]', + + // Generic popups/modals + '[class*="popup" i]', '[class*="modal" i]', + '[class*="overlay" i]', '[class*="dialog" i]', + '[role="dialog"]', '[role="alertdialog"]' + ]; + + // Try to click close buttons first + for (const selector of commonSelectors.slice(0, 6)) { + const closeButtons = document.querySelectorAll(selector); + for (const button of closeButtons) { + if (isVisible(button)) { + try { + button.click(); + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (e) { + console.log('Error clicking button:', e); + } + } + } + } + + // Remove remaining overlay elements + const removeOverlays = () => { + // Find elements with high z-index + const allElements = document.querySelectorAll('*'); + for (const elem of allElements) { + const style = window.getComputedStyle(elem); + const zIndex = parseInt(style.zIndex); + const position = style.position; + + if ( + isVisible(elem) && + (zIndex > 999 || position === 'fixed' || position === 'absolute') && + ( + elem.offsetWidth > window.innerWidth * 0.5 || + elem.offsetHeight > window.innerHeight * 0.5 || + style.backgroundColor.includes('rgba') || + parseFloat(style.opacity) < 1 + ) + ) { + elem.remove(); + } + } + + // Remove elements matching common selectors + for (const selector of commonSelectors) { + const elements = document.querySelectorAll(selector); + elements.forEach(elem => { + if (isVisible(elem)) { + elem.remove(); + } + }); + } + }; + + // Remove overlay elements + removeOverlays(); + + // Remove any fixed/sticky position elements at the top/bottom + const removeFixedElements = () => { + const elements = document.querySelectorAll('*'); + elements.forEach(elem => { + const style = window.getComputedStyle(elem); + if ( + (style.position === 'fixed' || style.position === 'sticky') && + isVisible(elem) + ) { + elem.remove(); + } + }); + }; + + removeFixedElements(); + + // Remove empty block elements as: div, p, span, etc. + const removeEmptyBlockElements = () => { + const blockElements = document.querySelectorAll('div, p, span, section, article, header, footer, aside, nav, main, ul, ol, li, dl, dt, dd, h1, h2, h3, h4, h5, h6'); + blockElements.forEach(elem => { + if (elem.innerText.trim() === '') { + elem.remove(); + } + }); + }; + + // Remove margin-right and padding-right from body (often added by modal scripts) + document.body.style.marginRight = '0px'; + document.body.style.paddingRight = '0px'; + document.body.style.overflow = 'auto'; + + // Wait a bit for any animations to complete + await new Promise(resolve => setTimeout(resolve, 100)); + } + """ + + try: + await page.evaluate(remove_overlays_js) + await page.wait_for_timeout(500) # Wait for any animations to complete + except Exception as e: + if self.verbose: + print(f"Warning: Failed to remove overlay elements: {str(e)}") + + async def take_screenshot(self, page: Page) -> str: + try: + # The page is already loaded, just take the screenshot + screenshot = await page.screenshot(full_page=True) + return base64.b64encode(screenshot).decode('utf-8') + except Exception as e: + error_message = f"Failed to take screenshot: {str(e)}" + print(error_message) + + # Generate an error image + img = Image.new('RGB', (800, 600), color='black') + draw = ImageDraw.Draw(img) + font = ImageFont.load_default() + draw.text((10, 10), error_message, fill=(255, 255, 255), font=font) + + buffered = BytesIO() + img.save(buffered, format="JPEG") + return base64.b64encode(buffered.getvalue()).decode('utf-8') + finally: + await page.close() diff --git a/crawl4ai/async_webcrawler.py b/crawl4ai/async_webcrawler.py index 005523eb..b66173a7 100644 --- a/crawl4ai/async_webcrawler.py +++ b/crawl4ai/async_webcrawler.py @@ -23,13 +23,15 @@ class AsyncWebCrawler: self, crawler_strategy: Optional[AsyncCrawlerStrategy] = None, always_by_pass_cache: bool = False, + base_directory: str = str(Path.home()), **kwargs, ): self.crawler_strategy = crawler_strategy or AsyncPlaywrightCrawlerStrategy( **kwargs ) self.always_by_pass_cache = always_by_pass_cache - self.crawl4ai_folder = os.path.join(Path.home(), ".crawl4ai") + # self.crawl4ai_folder = os.path.join(Path.home(), ".crawl4ai") + self.crawl4ai_folder = os.path.join(base_directory, ".crawl4ai") os.makedirs(self.crawl4ai_folder, exist_ok=True) os.makedirs(f"{self.crawl4ai_folder}/cache", exist_ok=True) self.ready = False diff --git a/crawl4ai/config.py b/crawl4ai/config.py index 2f33a8a9..a07ca977 100644 --- a/crawl4ai/config.py +++ b/crawl4ai/config.py @@ -37,16 +37,9 @@ SOCIAL_MEDIA_DOMAINS = [ 'linkedin.com', 'instagram.com', 'pinterest.com', - 'youtube.com', 'tiktok.com', 'snapchat.com', - 'whatsapp.com', - 'messenger.com', 'reddit.com', - 'tumblr.com', - 'buffer.com', - 'xing.com', - 'flipboard.com', ] # Threshold for the Image extraction - Range is 1 to 6 diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 0c472f0d..7799de66 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -14,7 +14,10 @@ from .utils import ( sanitize_html, extract_metadata, InvalidCSSSelectorError, - CustomHTML2Text + CustomHTML2Text, + normalize_url, + is_external_url + ) class ContentScrappingStrategy(ABC): @@ -67,6 +70,8 @@ class WebScrappingStrategy(ContentScrappingStrategy): links = {'internal': [], 'external': []} media = {'images': [], 'videos': [], 'audios': []} + internal_links_dict = {} + external_links_dict = {} # Extract meaningful text for media files from closest parent def find_closest_parent_with_useful_text(tag): @@ -205,30 +210,55 @@ class WebScrappingStrategy(ContentScrappingStrategy): social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('social_media_domains', []) social_media_domains = list(set(social_media_domains)) + try: if element.name == 'a' and element.get('href'): - href = element['href'] + href = element.get('href', '').strip() + if not href: # Skip empty hrefs + return False + url_base = url.split('/')[2] - link_data = {'href': href, 'text': element.get_text()} - if href.startswith('http') and url_base not in href: - links['external'].append(link_data) + + # Normalize the URL + try: + normalized_href = normalize_url(href, url) + except ValueError as e: + # logging.warning(f"Invalid URL format: {href}, Error: {str(e)}") + return False + + link_data = { + 'href': normalized_href, + 'text': element.get_text().strip(), + 'title': element.get('title', '').strip() + } + + # Check for duplicates and add to appropriate dictionary + is_external = is_external_url(normalized_href, url_base) + if is_external: + if normalized_href not in external_links_dict: + external_links_dict[normalized_href] = link_data else: - links['internal'].append(link_data) + if normalized_href not in internal_links_dict: + internal_links_dict[normalized_href] = link_data + keep_element = True - if kwargs.get('exclude_external_links', False): - href_parts = href.split('/') - href_url_base = href_parts[2] if len(href_parts) > 2 else href - if url_base not in href_url_base: - element.decompose() - return False - - if not kwargs.get('exclude_external_links', False) and kwargs.get('exclude_social_media_links', True): - if any(domain in href for domain in social_media_domains): + # Handle external link exclusions + if is_external: + if kwargs.get('exclude_external_links', False): element.decompose() return False + elif kwargs.get('exclude_social_media_links', False): + if any(domain in normalized_href.lower() for domain in social_media_domains): + element.decompose() + return False + elif kwargs.get('exclude_domains', []): + if any(domain in normalized_href.lower() for domain in kwargs.get('exclude_domains', [])): + element.decompose() + return False + except Exception as e: - raise "Error processing links" + raise Exception(f"Error processing links: {str(e)}") try: if element.name == 'img': @@ -252,12 +282,18 @@ class WebScrappingStrategy(ContentScrappingStrategy): element.decompose() return False - if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', True): + if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', False): src_url_base = src.split('/')[2] url_base = url.split('/')[2] if any(domain in src for domain in social_media_domains): element.decompose() return False + + # Handle exclude domains + if kwargs.get('exclude_domains', []): + if any(domain in src for domain in kwargs.get('exclude_domains', [])): + element.decompose() + return False return True # Always keep image elements except Exception as e: @@ -328,6 +364,11 @@ class WebScrappingStrategy(ContentScrappingStrategy): # ] process_element(body) + + # Update the links dictionary with unique links + links['internal'] = list(internal_links_dict.values()) + links['external'] = list(external_links_dict.values()) + # # Process images using ThreadPoolExecutor imgs = body.find_all('img') diff --git a/crawl4ai/extraction_strategy.py b/crawl4ai/extraction_strategy.py index 046067d8..b79e0c43 100644 --- a/crawl4ai/extraction_strategy.py +++ b/crawl4ai/extraction_strategy.py @@ -80,6 +80,7 @@ class LLMExtractionStrategy(ExtractionStrategy): self.word_token_rate = kwargs.get("word_token_rate", WORD_TOKEN_RATE) self.apply_chunking = kwargs.get("apply_chunking", True) self.base_url = kwargs.get("base_url", None) + self.api_base = kwargs.get("api_base", kwargs.get("base_url", None)) self.extra_args = kwargs.get("extra_args", {}) if not self.apply_chunking: self.chunk_token_threshold = 1e9 @@ -116,7 +117,7 @@ class LLMExtractionStrategy(ExtractionStrategy): self.provider, prompt_with_variables, self.api_token, - base_url=self.base_url, + base_url=self.api_base or self.base_url, extra_args = self.extra_args ) # , json_response=self.extract_type == "schema") try: diff --git a/crawl4ai/utils.py b/crawl4ai/utils.py index 34ab219b..baa08a0f 100644 --- a/crawl4ai/utils.py +++ b/crawl4ai/utils.py @@ -980,4 +980,53 @@ def format_html(html_string): soup = BeautifulSoup(html_string, 'html.parser') return soup.prettify() +def normalize_url(href, base_url): + """Normalize URLs to ensure consistent format""" + # Extract protocol and domain from base URL + try: + base_parts = base_url.split('/') + protocol = base_parts[0] + domain = base_parts[2] + except IndexError: + raise ValueError(f"Invalid base URL format: {base_url}") + + # Handle special protocols + special_protocols = {'mailto:', 'tel:', 'ftp:', 'file:', 'data:', 'javascript:'} + if any(href.lower().startswith(proto) for proto in special_protocols): + return href.strip() + + # Handle anchor links + if href.startswith('#'): + return f"{base_url}{href}" + + # Handle protocol-relative URLs + if href.startswith('//'): + return f"{protocol}{href}" + + # Handle root-relative URLs + if href.startswith('/'): + return f"{protocol}//{domain}{href}" + + # Handle relative URLs + if not href.startswith(('http://', 'https://')): + # Remove leading './' if present + href = href.lstrip('./') + return f"{protocol}//{domain}/{href}" + + return href.strip() +def is_external_url(url, base_domain): + """Determine if a URL is external""" + special_protocols = {'mailto:', 'tel:', 'ftp:', 'file:', 'data:', 'javascript:'} + if any(url.lower().startswith(proto) for proto in special_protocols): + return True + + try: + # Handle URLs with protocol + if url.startswith(('http://', 'https://')): + url_domain = url.split('/')[2] + return base_domain.lower() not in url_domain.lower() + except IndexError: + return False + + return False From 38474bd66ac4c1f31acd9c6c64498c3fe51f7c27 Mon Sep 17 00:00:00 2001 From: UncleCode Date: Thu, 24 Oct 2024 20:24:21 +0800 Subject: [PATCH 13/14] Update version --- crawl4ai/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crawl4ai/__init__.py b/crawl4ai/__init__.py index 1ecff4b0..b2a195de 100644 --- a/crawl4ai/__init__.py +++ b/crawl4ai/__init__.py @@ -3,7 +3,7 @@ from .async_webcrawler import AsyncWebCrawler from .models import CrawlResult -__version__ = "0.3.71" +__version__ = "0.3.72" __all__ = [ "AsyncWebCrawler", From 4239654722647f90bf2ab5cb3095cc02e40b4a7a Mon Sep 17 00:00:00 2001 From: UncleCode Date: Sun, 27 Oct 2024 19:24:46 +0800 Subject: [PATCH 14/14] Update Documentation --- README.md | 32 +- crawl4ai/content_scrapping_strategy.py | 8 +- docs/details/extraction.md | 157 +++ docs/details/feature_lists.md | 175 +++ docs/details/features.md | 150 ++ docs/details/features_details.md | 457 +++++++ docs/details/input_output.md | 282 ++++ docs/details/realworld_examples.md | 67 + docs/examples/quickstart_async.py | 22 +- .../api/core_classes_and_functions.md | 0 .../api/detailed_api_documentation.md | 0 .../assets/DankMono-Bold.woff2 | Bin .../assets/DankMono-Italic.woff2 | Bin .../assets/DankMono-Regular.woff2 | Bin docs/{md _sync => md_v0}/assets/Monaco.woff | Bin docs/{md _sync => md_v0}/assets/dmvendor.css | 0 docs/{md _sync => md_v0}/assets/highlight.css | 0 .../assets/highlight.min.js | 0 .../assets/highlight_init.js | 0 docs/{md _sync => md_v0}/assets/styles.css | 0 docs/{md _sync => md_v0}/changelog.md | 0 docs/{ => md_v0}/chunking_strategies.json | 0 docs/{md _sync => md_v0}/contact.md | 0 docs/{md _sync => md_v0}/demo.md | 0 .../examples/hooks_auth.md | 0 docs/{md _sync => md_v0}/examples/index.md | 0 .../examples/js_execution_css_filtering.md | 0 .../examples/llm_extraction.md | 0 .../examples/research_assistant.md | 0 .../examples/summarization.md | 0 docs/{ => md_v0}/extraction_strategies.json | 0 .../full_details/advanced_features.md | 0 .../full_details/chunking_strategies.md | 0 .../full_details/crawl_request_parameters.md | 0 .../full_details/crawl_result_class.md | 0 .../full_details/extraction_strategies.md | 0 docs/{md _sync => md_v0}/index.md | 0 docs/{md _sync => md_v0}/installation.md | 0 .../interactive_content.html | 0 docs/{md _sync => md_v0}/introduction.md | 0 docs/{md _sync => md_v0}/quickstart.md | 0 .../api/core_classes_and_functions.md | 0 .../api/detailed_api_documentation.md | 0 docs/{md => md_v1}/assets/DankMono-Bold.woff2 | Bin .../assets/DankMono-Italic.woff2 | Bin .../assets/DankMono-Regular.woff2 | Bin docs/{md => md_v1}/assets/Monaco.woff | Bin docs/{md => md_v1}/assets/dmvendor.css | 0 docs/{md => md_v1}/assets/highlight.css | 0 docs/{md => md_v1}/assets/highlight.min.js | 0 docs/{md => md_v1}/assets/highlight_init.js | 0 docs/{md => md_v1}/assets/styles.css | 0 docs/{md => md_v1}/changelog.md | 0 docs/{md => md_v1}/contact.md | 0 docs/{md => md_v1}/demo.md | 0 docs/{md => md_v1}/examples/hooks_auth.md | 0 docs/{md => md_v1}/examples/index.md | 0 .../examples/js_execution_css_filtering.md | 0 .../examples/json_css_extraction.md | 0 docs/{md => md_v1}/examples/llm_extraction.md | 0 .../examples/research_assistant.md | 0 docs/{md => md_v1}/examples/summarization.md | 0 .../full_details/advanced_features.md | 0 .../advanced_jsoncss_extraction.md | 0 .../full_details/chunking_strategies.md | 0 .../full_details/crawl_request_parameters.md | 0 .../full_details/crawl_result_class.md | 0 .../full_details/extraction_strategies.md | 0 .../full_details/session_based_crawling.md | 0 docs/{md => md_v1}/index.md | 0 docs/{md => md_v1}/installation.md | 0 docs/{md => md_v1}/interactive_content.html | 0 docs/{md => md_v1}/introduction.md | 0 docs/md_v1/mkdocs.yml | 45 + docs/{md => md_v1}/quickstart.md | 10 +- docs/md_v2/advanced/content-processing.md | 223 +++ docs/md_v2/advanced/hooks-auth.md | 110 ++ docs/md_v2/advanced/hooks.md | 0 docs/md_v2/advanced/magic-mode.md | 52 + docs/md_v2/advanced/proxy-security.md | 84 ++ .../advanced/session-management-advanced.md | 276 ++++ docs/md_v2/advanced/session-management.md | 133 ++ docs/md_v2/api/arun.md | 226 +++ docs/md_v2/api/async-webcrawler.md | 320 +++++ docs/md_v2/api/crawl-result.md | 301 ++++ docs/md_v2/api/strategies.md | 255 ++++ docs/md_v2/assets/DankMono-Bold.woff2 | Bin 0 -> 33480 bytes docs/md_v2/assets/DankMono-Italic.woff2 | Bin 0 -> 32468 bytes docs/md_v2/assets/DankMono-Regular.woff2 | Bin 0 -> 30528 bytes docs/md_v2/assets/Monaco.woff | Bin 0 -> 20096 bytes docs/md_v2/assets/dmvendor.css | 127 ++ docs/md_v2/assets/highlight.css | 0 docs/md_v2/assets/highlight.min.js | 1213 +++++++++++++++++ docs/md_v2/assets/highlight_init.js | 6 + docs/md_v2/assets/styles.css | 153 +++ docs/md_v2/basic/browser-config.md | 208 +++ docs/md_v2/basic/content-selection.md | 199 +++ docs/md_v2/basic/installation.md | 92 ++ docs/md_v2/basic/output-formats.md | 195 +++ docs/md_v2/basic/page-interaction.md | 207 +++ docs/md_v2/basic/quickstart.md | 297 ++++ docs/md_v2/basic/simple-crawling.md | 120 ++ docs/md_v2/extraction/chunking.md | 133 ++ docs/md_v2/extraction/cosine.md | 222 +++ docs/md_v2/extraction/css-advanced.md | 282 ++++ docs/md_v2/extraction/css.md | 142 ++ .../md_v2/extraction/extraction_strategies.md | 185 +++ docs/md_v2/extraction/llm.md | 179 +++ docs/md_v2/extraction/overview.md | 197 +++ docs/md_v2/index.md | 113 ++ mkdocs.yml | 78 +- 111 files changed, 7680 insertions(+), 53 deletions(-) create mode 100644 docs/details/extraction.md create mode 100644 docs/details/feature_lists.md create mode 100644 docs/details/features.md create mode 100644 docs/details/features_details.md create mode 100644 docs/details/input_output.md create mode 100644 docs/details/realworld_examples.md rename docs/{md _sync => md_v0}/api/core_classes_and_functions.md (100%) rename docs/{md _sync => md_v0}/api/detailed_api_documentation.md (100%) rename docs/{md _sync => md_v0}/assets/DankMono-Bold.woff2 (100%) rename docs/{md _sync => md_v0}/assets/DankMono-Italic.woff2 (100%) rename docs/{md _sync => md_v0}/assets/DankMono-Regular.woff2 (100%) rename docs/{md _sync => md_v0}/assets/Monaco.woff (100%) rename docs/{md _sync => md_v0}/assets/dmvendor.css (100%) rename docs/{md _sync => md_v0}/assets/highlight.css (100%) rename docs/{md _sync => md_v0}/assets/highlight.min.js (100%) rename docs/{md _sync => md_v0}/assets/highlight_init.js (100%) rename docs/{md _sync => md_v0}/assets/styles.css (100%) rename docs/{md _sync => md_v0}/changelog.md (100%) rename docs/{ => md_v0}/chunking_strategies.json (100%) rename docs/{md _sync => md_v0}/contact.md (100%) rename docs/{md _sync => md_v0}/demo.md (100%) rename docs/{md _sync => md_v0}/examples/hooks_auth.md (100%) rename docs/{md _sync => md_v0}/examples/index.md (100%) rename docs/{md _sync => md_v0}/examples/js_execution_css_filtering.md (100%) rename docs/{md _sync => md_v0}/examples/llm_extraction.md (100%) rename docs/{md _sync => md_v0}/examples/research_assistant.md (100%) rename docs/{md _sync => md_v0}/examples/summarization.md (100%) rename docs/{ => md_v0}/extraction_strategies.json (100%) rename docs/{md _sync => md_v0}/full_details/advanced_features.md (100%) rename docs/{md _sync => md_v0}/full_details/chunking_strategies.md (100%) rename docs/{md _sync => md_v0}/full_details/crawl_request_parameters.md (100%) rename docs/{md _sync => md_v0}/full_details/crawl_result_class.md (100%) rename docs/{md _sync => md_v0}/full_details/extraction_strategies.md (100%) rename docs/{md _sync => md_v0}/index.md (100%) rename docs/{md _sync => md_v0}/installation.md (100%) rename docs/{md _sync => md_v0}/interactive_content.html (100%) rename docs/{md _sync => md_v0}/introduction.md (100%) rename docs/{md _sync => md_v0}/quickstart.md (100%) rename docs/{md => md_v1}/api/core_classes_and_functions.md (100%) rename docs/{md => md_v1}/api/detailed_api_documentation.md (100%) rename docs/{md => md_v1}/assets/DankMono-Bold.woff2 (100%) rename docs/{md => md_v1}/assets/DankMono-Italic.woff2 (100%) rename docs/{md => md_v1}/assets/DankMono-Regular.woff2 (100%) rename docs/{md => md_v1}/assets/Monaco.woff (100%) rename docs/{md => md_v1}/assets/dmvendor.css (100%) rename docs/{md => md_v1}/assets/highlight.css (100%) rename docs/{md => md_v1}/assets/highlight.min.js (100%) rename docs/{md => md_v1}/assets/highlight_init.js (100%) rename docs/{md => md_v1}/assets/styles.css (100%) rename docs/{md => md_v1}/changelog.md (100%) rename docs/{md => md_v1}/contact.md (100%) rename docs/{md => md_v1}/demo.md (100%) rename docs/{md => md_v1}/examples/hooks_auth.md (100%) rename docs/{md => md_v1}/examples/index.md (100%) rename docs/{md => md_v1}/examples/js_execution_css_filtering.md (100%) rename docs/{md => md_v1}/examples/json_css_extraction.md (100%) rename docs/{md => md_v1}/examples/llm_extraction.md (100%) rename docs/{md => md_v1}/examples/research_assistant.md (100%) rename docs/{md => md_v1}/examples/summarization.md (100%) rename docs/{md => md_v1}/full_details/advanced_features.md (100%) rename docs/{md => md_v1}/full_details/advanced_jsoncss_extraction.md (100%) rename docs/{md => md_v1}/full_details/chunking_strategies.md (100%) rename docs/{md => md_v1}/full_details/crawl_request_parameters.md (100%) rename docs/{md => md_v1}/full_details/crawl_result_class.md (100%) rename docs/{md => md_v1}/full_details/extraction_strategies.md (100%) rename docs/{md => md_v1}/full_details/session_based_crawling.md (100%) rename docs/{md => md_v1}/index.md (100%) rename docs/{md => md_v1}/installation.md (100%) rename docs/{md => md_v1}/interactive_content.html (100%) rename docs/{md => md_v1}/introduction.md (100%) create mode 100644 docs/md_v1/mkdocs.yml rename docs/{md => md_v1}/quickstart.md (92%) create mode 100644 docs/md_v2/advanced/content-processing.md create mode 100644 docs/md_v2/advanced/hooks-auth.md create mode 100644 docs/md_v2/advanced/hooks.md create mode 100644 docs/md_v2/advanced/magic-mode.md create mode 100644 docs/md_v2/advanced/proxy-security.md create mode 100644 docs/md_v2/advanced/session-management-advanced.md create mode 100644 docs/md_v2/advanced/session-management.md create mode 100644 docs/md_v2/api/arun.md create mode 100644 docs/md_v2/api/async-webcrawler.md create mode 100644 docs/md_v2/api/crawl-result.md create mode 100644 docs/md_v2/api/strategies.md create mode 100644 docs/md_v2/assets/DankMono-Bold.woff2 create mode 100644 docs/md_v2/assets/DankMono-Italic.woff2 create mode 100644 docs/md_v2/assets/DankMono-Regular.woff2 create mode 100644 docs/md_v2/assets/Monaco.woff create mode 100644 docs/md_v2/assets/dmvendor.css create mode 100644 docs/md_v2/assets/highlight.css create mode 100644 docs/md_v2/assets/highlight.min.js create mode 100644 docs/md_v2/assets/highlight_init.js create mode 100644 docs/md_v2/assets/styles.css create mode 100644 docs/md_v2/basic/browser-config.md create mode 100644 docs/md_v2/basic/content-selection.md create mode 100644 docs/md_v2/basic/installation.md create mode 100644 docs/md_v2/basic/output-formats.md create mode 100644 docs/md_v2/basic/page-interaction.md create mode 100644 docs/md_v2/basic/quickstart.md create mode 100644 docs/md_v2/basic/simple-crawling.md create mode 100644 docs/md_v2/extraction/chunking.md create mode 100644 docs/md_v2/extraction/cosine.md create mode 100644 docs/md_v2/extraction/css-advanced.md create mode 100644 docs/md_v2/extraction/css.md create mode 100644 docs/md_v2/extraction/extraction_strategies.md create mode 100644 docs/md_v2/extraction/llm.md create mode 100644 docs/md_v2/extraction/overview.md create mode 100644 docs/md_v2/index.md diff --git a/README.md b/README.md index a8c2b9b0..5f867cc3 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,14 @@ Crawl4AI simplifies asynchronous web crawling and data extraction, making it accessible for large language models (LLMs) and AI applications. πŸ†“πŸŒ -> Looking for the synchronous version? Check out [README.sync.md](./README.sync.md). You can also access the previous version in the branch [V0.2.76](https://github.com/unclecode/crawl4ai/blob/v0.2.76). +## New in 0.3.72 ✨ -## New update 0.3.6 -- 🌐 Multi-browser support (Chromium, Firefox, WebKit) -- πŸ–ΌοΈ Improved image processing with lazy-loading detection -- πŸ”§ Custom page timeout parameter for better control over crawling behavior -- πŸ•°οΈ Enhanced handling of delayed content loading -- πŸ”‘ Custom headers support for LLM interactions -- πŸ–ΌοΈ iframe content extraction for comprehensive page analysis -- ⏱️ Flexible timeout and delayed content retrieval options +- πŸ“„ Fit markdown generation for extracting main article content. +- πŸͺ„ Magic mode for comprehensive anti-bot detection bypass. +- 🌐 Enhanced multi-browser support with seamless switching (Chromium, Firefox, WebKit) +- πŸ“š New chunking strategies(Sliding window, Overlapping window, Flexible size control) +- πŸ’Ύ Improved caching system for better performance +- ⚑ Optimized batch processing with automatic rate limiting ## Try it Now! @@ -30,22 +28,28 @@ Crawl4AI simplifies asynchronous web crawling and data extraction, making it acc - πŸ†“ Completely free and open-source - πŸš€ Blazing fast performance, outperforming many paid services - πŸ€– LLM-friendly output formats (JSON, cleaned HTML, markdown) +- 🌐 Multi-browser support (Chromium, Firefox, WebKit) - 🌍 Supports crawling multiple URLs simultaneously - 🎨 Extracts and returns all media tags (Images, Audio, and Video) - πŸ”— Extracts all external and internal links - πŸ“š Extracts metadata from the page -- πŸ”„ Custom hooks for authentication, headers, and page modifications before crawling +- πŸ”„ Custom hooks for authentication, headers, and page modifications - πŸ•΅οΈ User-agent customization -- πŸ–ΌοΈ Takes screenshots of the page +- πŸ–ΌοΈ Takes screenshots of pages with enhanced error handling - πŸ“œ Executes multiple custom JavaScripts before crawling - πŸ“Š Generates structured output without LLM using JsonCssExtractionStrategy - πŸ“š Various chunking strategies: topic-based, regex, sentence, and more - 🧠 Advanced extraction strategies: cosine clustering, LLM, and more - 🎯 CSS selector support for precise data extraction - πŸ“ Passes instructions/keywords to refine extraction -- πŸ”’ Proxy support for enhanced privacy and access -- πŸ”„ Session management for complex multi-page crawling scenarios -- 🌐 Asynchronous architecture for improved performance and scalability +- πŸ”’ Proxy support with authentication for enhanced access +- πŸ”„ Session management for complex multi-page crawling +- 🌐 Asynchronous architecture for improved performance +- πŸ–ΌοΈ Improved image processing with lazy-loading detection +- πŸ•°οΈ Enhanced handling of delayed content loading +- πŸ”‘ Custom headers support for LLM interactions +- πŸ–ΌοΈ iframe content extraction for comprehensive analysis +- ⏱️ Flexible timeout and delayed content retrieval options ## Installation πŸ› οΈ diff --git a/crawl4ai/content_scrapping_strategy.py b/crawl4ai/content_scrapping_strategy.py index 7799de66..6119a414 100644 --- a/crawl4ai/content_scrapping_strategy.py +++ b/crawl4ai/content_scrapping_strategy.py @@ -207,8 +207,8 @@ class WebScrappingStrategy(ContentScrappingStrategy): keep_element = False - social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('social_media_domains', []) - social_media_domains = list(set(social_media_domains)) + exclude_social_media_domains = SOCIAL_MEDIA_DOMAINS + kwargs.get('exclude_social_media_domains', []) + exclude_social_media_domains = list(set(exclude_social_media_domains)) try: @@ -249,7 +249,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): element.decompose() return False elif kwargs.get('exclude_social_media_links', False): - if any(domain in normalized_href.lower() for domain in social_media_domains): + if any(domain in normalized_href.lower() for domain in exclude_social_media_domains): element.decompose() return False elif kwargs.get('exclude_domains', []): @@ -285,7 +285,7 @@ class WebScrappingStrategy(ContentScrappingStrategy): if not kwargs.get('exclude_external_images', False) and kwargs.get('exclude_social_media_links', False): src_url_base = src.split('/')[2] url_base = url.split('/')[2] - if any(domain in src for domain in social_media_domains): + if any(domain in src for domain in exclude_social_media_domains): element.decompose() return False diff --git a/docs/details/extraction.md b/docs/details/extraction.md new file mode 100644 index 00000000..25fc4305 --- /dev/null +++ b/docs/details/extraction.md @@ -0,0 +1,157 @@ +### Extraction Strategies + +#### 1. LLMExtractionStrategy +```python +LLMExtractionStrategy( + # Core Parameters + provider: str = DEFAULT_PROVIDER, # LLM provider (e.g., "openai/gpt-4", "huggingface/...", "ollama/...") + api_token: Optional[str] = None, # API token for the provider + instruction: str = None, # Custom instruction for extraction + schema: Dict = None, # Pydantic model schema for structured extraction + extraction_type: str = "block", # Type of extraction: "block" or "schema" + + # Chunking Parameters + chunk_token_threshold: int = CHUNK_TOKEN_THRESHOLD, # Maximum tokens per chunk + overlap_rate: float = OVERLAP_RATE, # Overlap between chunks + word_token_rate: float = WORD_TOKEN_RATE, # Conversion rate from words to tokens + apply_chunking: bool = True, # Whether to apply text chunking + + # API Configuration + base_url: str = None, # Base URL for API calls + api_base: str = None, # Alternative base URL + extra_args: Dict = {}, # Additional provider-specific arguments + + verbose: bool = False # Enable verbose logging +) +``` + +Usage Example: +```python +class NewsArticle(BaseModel): + title: str + content: str + +strategy = LLMExtractionStrategy( + provider="ollama/nemotron", + api_token="your-token", + schema=NewsArticle.schema(), + instruction="Extract news article content with title and main text" +) + +result = await crawler.arun(url="https://example.com", extraction_strategy=strategy) +``` + +#### 2. JsonCssExtractionStrategy +```python +JsonCssExtractionStrategy( + schema: Dict[str, Any], # Schema defining extraction rules + verbose: bool = False # Enable verbose logging +) + +# Schema Structure +schema = { + "name": str, # Name of the extraction schema + "baseSelector": str, # CSS selector for base elements + "fields": [ + { + "name": str, # Field name + "selector": str, # CSS selector + "type": str, # Field type: "text", "attribute", "html", "regex", "nested", "list", "nested_list" + "attribute": str, # For type="attribute" + "pattern": str, # For type="regex" + "transform": str, # Optional: "lowercase", "uppercase", "strip" + "default": Any, # Default value if extraction fails + "fields": List[Dict], # For nested/list types + } + ] +} +``` + +Usage Example: +```python +schema = { + "name": "News Articles", + "baseSelector": "article.news-item", + "fields": [ + { + "name": "title", + "selector": "h1", + "type": "text", + "transform": "strip" + }, + { + "name": "date", + "selector": ".date", + "type": "attribute", + "attribute": "datetime" + } + ] +} + +strategy = JsonCssExtractionStrategy(schema) +result = await crawler.arun(url="https://example.com", extraction_strategy=strategy) +``` + +#### 3. CosineStrategy +```python +CosineStrategy( + # Content Filtering + semantic_filter: str = None, # Keyword filter for document filtering + word_count_threshold: int = 10, # Minimum words per cluster + sim_threshold: float = 0.3, # Similarity threshold for filtering + + # Clustering Parameters + max_dist: float = 0.2, # Maximum distance for clustering + linkage_method: str = 'ward', # Clustering linkage method + top_k: int = 3, # Number of top categories to extract + + # Model Configuration + model_name: str = 'sentence-transformers/all-MiniLM-L6-v2', # Embedding model + + verbose: bool = False # Enable verbose logging +) +``` + +### Chunking Strategies + +#### 1. RegexChunking +```python +RegexChunking( + patterns: List[str] = None # List of regex patterns for splitting text + # Default pattern: [r'\n\n'] +) +``` + +Usage Example: +```python +chunker = RegexChunking(patterns=[r'\n\n', r'\.\s+']) # Split on double newlines and sentences +chunks = chunker.chunk(text) +``` + +#### 2. SlidingWindowChunking +```python +SlidingWindowChunking( + window_size: int = 100, # Size of the window in words + step: int = 50, # Number of words to slide the window +) +``` + +Usage Example: +```python +chunker = SlidingWindowChunking(window_size=200, step=100) +chunks = chunker.chunk(text) # Creates overlapping chunks of 200 words, moving 100 words at a time +``` + +#### 3. OverlappingWindowChunking +```python +OverlappingWindowChunking( + window_size: int = 1000, # Size of each chunk in words + overlap: int = 100 # Number of words to overlap between chunks +) +``` + +Usage Example: +```python +chunker = OverlappingWindowChunking(window_size=500, overlap=50) +chunks = chunker.chunk(text) # Creates 500-word chunks with 50-word overlap +``` diff --git a/docs/details/feature_lists.md b/docs/details/feature_lists.md new file mode 100644 index 00000000..5dc2ad74 --- /dev/null +++ b/docs/details/feature_lists.md @@ -0,0 +1,175 @@ +# Features + +## Current Features +1. Async-first architecture for high-performance web crawling +2. Built-in anti-bot detection bypass ("magic mode") +3. Multiple browser engine support (Chromium, Firefox, WebKit) +4. Smart session management with automatic cleanup +5. Automatic content cleaning and relevance scoring +6. Built-in markdown generation with formatting preservation +7. Intelligent image scoring and filtering +8. Automatic popup and overlay removal +9. Smart wait conditions (CSS/JavaScript based) +10. Multi-provider LLM integration (OpenAI, HuggingFace, Ollama) +11. Schema-based structured data extraction +12. Automated iframe content processing +13. Intelligent link categorization (internal/external) +14. Multiple chunking strategies for large content +15. Real-time HTML cleaning and sanitization +16. Automatic screenshot capabilities +17. Social media link filtering +18. Semantic similarity-based content clustering +19. Human behavior simulation for anti-bot bypass +20. Proxy support with authentication +21. Automatic resource cleanup +22. Custom CSS selector-based extraction +23. Automatic content relevance scoring ("fit" content) +24. Recursive website crawling capabilities +25. Flexible hook system for customization +26. Built-in caching system +27. Domain-based content filtering +28. Dynamic content handling with JavaScript execution +29. Automatic media content extraction and classification +30. Metadata extraction and processing +31. Customizable HTML to Markdown conversion +32. Token-aware content chunking for LLM processing +33. Automatic response header and status code handling +34. Browser fingerprint customization +35. Multiple extraction strategies (LLM, CSS, Cosine, XPATH) +36. Automatic error image generation for failed screenshots +37. Smart content overlap handling for large texts +38. Built-in rate limiting for batch processing +39. Automatic cookie handling +40. Browser Console logging and debugging capabilities + +## Feature Techs +β€’ Browser Management + - Asynchronous browser control + - Multi-browser support (Chromium, Firefox, WebKit) + - Headless mode support + - Browser cleanup and resource management + - Custom browser arguments and configuration + - Context management with `__aenter__` and `__aexit__` + +β€’ Session Handling + - Session management with TTL (Time To Live) + - Session reuse capabilities + - Session cleanup for expired sessions + - Session-based context preservation + +β€’ Stealth Features + - Playwright stealth configuration + - Navigator properties override + - WebDriver detection evasion + - Chrome app simulation + - Plugin simulation + - Language preferences simulation + - Hardware concurrency simulation + - Media codecs simulation + +β€’ Network Features + - Proxy support with authentication + - Custom headers management + - Cookie handling + - Response header capture + - Status code tracking + - Network idle detection + +β€’ Page Interaction + - Smart wait functionality for multiple conditions + - CSS selector-based waiting + - JavaScript condition waiting + - Custom JavaScript execution + - User interaction simulation (mouse/keyboard) + - Page scrolling + - Timeout management + - Load state monitoring + +β€’ Content Processing + - HTML content extraction + - Iframe processing and content extraction + - Delayed content retrieval + - Content caching + - Cache file management + - HTML cleaning and processing + +β€’ Image Handling + - Screenshot capabilities (full page) + - Base64 encoding of screenshots + - Image dimension updating + - Image filtering (size/visibility) + - Error image generation + - Natural width/height preservation + +β€’ Overlay Management + - Popup removal + - Cookie notice removal + - Newsletter dialog removal + - Modal removal + - Fixed position element removal + - Z-index based overlay detection + - Visibility checking + +β€’ Hook System + - Browser creation hooks + - User agent update hooks + - Execution start hooks + - Navigation hooks (before/after goto) + - HTML retrieval hooks + - HTML return hooks + +β€’ Error Handling + - Browser error catching + - Network error handling + - Timeout handling + - Screenshot error recovery + - Invalid selector handling + - General exception management + +β€’ Performance Features + - Concurrent URL processing + - Semaphore-based rate limiting + - Async gathering of results + - Resource cleanup + - Memory management + +β€’ Debug Features + - Console logging + - Page error logging + - Verbose mode + - Error message generation + - Warning system + +β€’ Security Features + - Certificate error handling + - Sandbox configuration + - GPU handling + - CSP (Content Security Policy) compliant waiting + +β€’ Configuration + - User agent customization + - Viewport configuration + - Timeout configuration + - Browser type selection + - Proxy configuration + - Header configuration + +β€’ Data Models + - Pydantic model for responses + - Type hints throughout code + - Structured response format + - Optional response fields + +β€’ File System Integration + - Cache directory management + - File path handling + - Cache metadata storage + - File read/write operations + +β€’ Metadata Handling + - Response headers capture + - Status code tracking + - Cache metadata + - Session tracking + - Timestamp management + diff --git a/docs/details/features.md b/docs/details/features.md new file mode 100644 index 00000000..8ed027c2 --- /dev/null +++ b/docs/details/features.md @@ -0,0 +1,150 @@ +### 1. Basic Web Crawling +```python +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + print(result.markdown) # Get clean markdown content + print(result.html) # Get raw HTML + print(result.cleaned_html) # Get cleaned HTML +``` + +### 2. Browser Control Options +- Multiple Browser Support +```python +# Choose between different browser engines +crawler = AsyncWebCrawler(browser_type="firefox") # or "chromium", "webkit" +crawler = AsyncWebCrawler(headless=False) # For visible browser +``` + +- Proxy Configuration +```python +crawler = AsyncWebCrawler(proxy="http://proxy.example.com:8080") +# Or with authentication +crawler = AsyncWebCrawler(proxy_config={ + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" +}) +``` + +### 3. Content Selection & Filtering +- CSS Selector Support +```python +result = await crawler.arun( + url="https://example.com", + css_selector=".main-content" # Extract specific content +) +``` + +- Content Filtering Options +```python +result = await crawler.arun( + url="https://example.com", + word_count_threshold=10, # Minimum words per block + excluded_tags=['form', 'header'], # Tags to exclude + exclude_external_links=True, # Remove external links + exclude_social_media_links=True, # Remove social media links + exclude_external_images=True # Remove external images +) +``` + +### 4. Dynamic Content Handling +- JavaScript Execution +```python +result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight)" # Execute custom JS +) +``` + +- Wait Conditions +```python +result = await crawler.arun( + url="https://example.com", + wait_for="css:.my-element", # Wait for element + wait_for="js:() => document.readyState === 'complete'" # Wait for condition +) +``` + +### 5. Anti-Bot Protection Handling +```python +result = await crawler.arun( + url="https://example.com", + simulate_user=True, # Simulate human behavior + override_navigator=True, # Mask automation signals + magic=True # Enable all anti-detection features +) +``` + +### 6. Session Management +```python +session_id = "my_session" +result1 = await crawler.arun(url="https://example.com/page1", session_id=session_id) +result2 = await crawler.arun(url="https://example.com/page2", session_id=session_id) +await crawler.crawler_strategy.kill_session(session_id) +``` + +### 7. Media Handling +- Screenshot Capture +```python +result = await crawler.arun( + url="https://example.com", + screenshot=True +) +base64_screenshot = result.screenshot +``` + +- Media Extraction +```python +result = await crawler.arun(url="https://example.com") +print(result.media['images']) # List of images +print(result.media['videos']) # List of videos +print(result.media['audios']) # List of audio files +``` + +### 8. Structured Data Extraction +- CSS-based Extraction +```python +schema = { + "name": "News Articles", + "baseSelector": "article", + "fields": [ + {"name": "title", "selector": "h1", "type": "text"}, + {"name": "date", "selector": ".date", "type": "text"} + ] +} +extraction_strategy = JsonCssExtractionStrategy(schema) +result = await crawler.arun( + url="https://example.com", + extraction_strategy=extraction_strategy +) +structured_data = json.loads(result.extracted_content) +``` + +- LLM-based Extraction (Multiple Providers) +```python +class NewsArticle(BaseModel): + title: str + summary: str + +strategy = LLMExtractionStrategy( + provider="ollama/nemotron", # or "huggingface/...", "ollama/..." + api_token="your-token", + schema=NewsArticle.schema(), + instruction="Extract news article details..." +) +result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy +) +``` + +### 9. Content Cleaning & Processing +```python +result = await crawler.arun( + url="https://example.com", + remove_overlay_elements=True, # Remove popups/modals + process_iframes=True, # Process iframe content +) +print(result.fit_markdown) # Get most relevant content +print(result.fit_html) # Get cleaned HTML +``` diff --git a/docs/details/features_details.md b/docs/details/features_details.md new file mode 100644 index 00000000..ffc6c21c --- /dev/null +++ b/docs/details/features_details.md @@ -0,0 +1,457 @@ +I'll expand the outline with detailed descriptions and examples based on all the provided files. I'll start with the first few sections: + +### 1. Basic Web Crawling +Basic web crawling provides the foundation for extracting content from websites. The library supports both simple single-page crawling and recursive website crawling. + +```python +# Simple page crawling +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + print(result.html) # Raw HTML + print(result.markdown) # Cleaned markdown + print(result.cleaned_html) # Cleaned HTML + +# Recursive website crawling +class SimpleWebsiteScraper: + def __init__(self, crawler: AsyncWebCrawler): + self.crawler = crawler + + async def scrape(self, start_url: str, max_depth: int): + results = await self.scrape_recursive(start_url, max_depth) + return results + +# Usage +async with AsyncWebCrawler() as crawler: + scraper = SimpleWebsiteScraper(crawler) + results = await scraper.scrape("https://example.com", depth=2) +``` + +### 2. Browser Control Options +The library provides extensive control over browser behavior, allowing customization of browser type, headless mode, and proxy settings. + +```python +# Browser Type Selection +async with AsyncWebCrawler( + browser_type="firefox", # Options: "chromium", "firefox", "webkit" + headless=False, # For visible browser + verbose=True # Enable logging +) as crawler: + result = await crawler.arun(url="https://example.com") + +# Proxy Configuration +async with AsyncWebCrawler( + proxy_config={ + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" + }, + headers={ + "User-Agent": "Custom User Agent", + "Accept-Language": "en-US,en;q=0.9" + } +) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +### 3. Content Selection & Filtering +The library offers multiple ways to select and filter content, from CSS selectors to word count thresholds. + +```python +# CSS Selector and Content Filtering +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + css_selector="article.main-content", # Extract specific content + word_count_threshold=10, # Minimum words per block + excluded_tags=['form', 'header'], # Tags to exclude + exclude_external_links=True, # Remove external links + exclude_social_media_links=True, # Remove social media links + exclude_domains=["pinterest.com", "facebook.com"] # Exclude specific domains + ) + +# Custom HTML to Text Options +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + html2text={ + "escape_dot": False, + "links_each_paragraph": True, + "protect_links": True + } + ) +``` + +### 4. Dynamic Content Handling +The library provides sophisticated handling of dynamic content with JavaScript execution and wait conditions. + +```python +# JavaScript Execution and Wait Conditions +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + js_code=[ + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more').click();" + ], + wait_for="css:.dynamic-content", # Wait for element + delay_before_return_html=2.0 # Wait after JS execution + ) + +# Smart Wait Conditions +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + wait_for="""() => { + return document.querySelectorAll('.item').length > 10; + }""", + page_timeout=60000 # 60 seconds timeout + ) +``` + +### 5. Advanced Link Analysis +The library provides comprehensive link analysis capabilities, distinguishing between internal and external links, with options for filtering and processing. + +```python +# Basic Link Analysis +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + + # Access internal and external links + for internal_link in result.links['internal']: + print(f"Internal: {internal_link['href']} - {internal_link['text']}") + + for external_link in result.links['external']: + print(f"External: {external_link['href']} - {external_link['text']}") + +# Advanced Link Filtering +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + exclude_external_links=True, # Remove all external links + exclude_social_media_links=True, # Remove social media links + exclude_social_media_domains=[ # Custom social media domains + "facebook.com", "twitter.com", "instagram.com" + ], + exclude_domains=["pinterest.com"] # Specific domains to exclude + ) +``` + +### 6. Anti-Bot Protection Handling +The library includes sophisticated anti-detection mechanisms to handle websites with bot protection. + +```python +# Basic Anti-Detection +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + simulate_user=True, # Simulate human behavior + override_navigator=True # Override navigator properties + ) + +# Advanced Anti-Detection with Magic Mode +async with AsyncWebCrawler(headless=False) as crawler: + result = await crawler.arun( + url="https://example.com", + magic=True, # Enable all anti-detection features + remove_overlay_elements=True, # Remove popups/modals automatically + # Custom navigator properties + js_code=""" + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined + }); + """ + ) +``` + +### 7. Session Management +Session management allows maintaining state across multiple requests and handling cookies. + +```python +# Basic Session Management +async with AsyncWebCrawler() as crawler: + session_id = "my_session" + + # Login + login_result = await crawler.arun( + url="https://example.com/login", + session_id=session_id, + js_code="document.querySelector('form').submit();" + ) + + # Use same session for subsequent requests + protected_result = await crawler.arun( + url="https://example.com/protected", + session_id=session_id + ) + + # Clean up session + await crawler.crawler_strategy.kill_session(session_id) + +# Advanced Session with Custom Cookies +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + session_id="custom_session", + cookies=[{ + "name": "sessionId", + "value": "abc123", + "domain": "example.com" + }] + ) +``` + +### 8. Screenshot and Media Handling +The library provides comprehensive media handling capabilities, including screenshots and media content extraction. + +```python +# Screenshot Capture +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + screenshot=True, + screenshot_wait_for=2.0 # Wait before taking screenshot + ) + + # Save screenshot + if result.screenshot: + with open("screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) + +# Media Extraction +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + + # Process images with metadata + for image in result.media['images']: + print(f"Image: {image['src']}") + print(f"Alt text: {image['alt']}") + print(f"Context: {image['desc']}") + print(f"Relevance score: {image['score']}") + + # Process videos and audio + for video in result.media['videos']: + print(f"Video: {video['src']}") + for audio in result.media['audios']: + print(f"Audio: {audio['src']}") +``` + +### 9. Structured Data Extraction & Chunking +The library supports multiple strategies for structured data extraction and content chunking. + +```python +# LLM-based Extraction +class NewsArticle(BaseModel): + title: str + content: str + author: str + +extraction_strategy = LLMExtractionStrategy( + provider='openai/gpt-4', + api_token="your-token", + schema=NewsArticle.schema(), + instruction="Extract news article details", + chunk_token_threshold=1000, + overlap_rate=0.1 +) + +# CSS-based Extraction +schema = { + "name": "Product Listing", + "baseSelector": ".product-card", + "fields": [ + { + "name": "title", + "selector": "h2", + "type": "text" + }, + { + "name": "price", + "selector": ".price", + "type": "text", + "transform": "strip" + } + ] +} + +css_strategy = JsonCssExtractionStrategy(schema) + +# Text Chunking +from crawl4ai.chunking_strategy import OverlappingWindowChunking + +chunking_strategy = OverlappingWindowChunking( + window_size=1000, + overlap=100 +) + +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + extraction_strategy=extraction_strategy, + chunking_strategy=chunking_strategy + ) +``` + + +### 10. Content Cleaning & Processing +The library provides extensive content cleaning and processing capabilities, ensuring high-quality output in various formats. + +```python +# Basic Content Cleaning +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + remove_overlay_elements=True, # Remove popups/modals + process_iframes=True, # Process iframe content + word_count_threshold=10 # Minimum words per block + ) + + print(result.cleaned_html) # Clean HTML + print(result.fit_html) # Most relevant HTML content + print(result.fit_markdown) # Most relevant markdown content + +# Advanced Content Processing +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + excluded_tags=['form', 'header', 'footer', 'nav'], + html2text={ + "escape_dot": False, + "body_width": 0, + "protect_links": True, + "unicode_snob": True, + "ignore_links": False, + "ignore_images": False, + "ignore_emphasis": False, + "bypass_tables": False, + "ignore_tables": False + } + ) +``` + +### Advanced Usage Patterns + +#### 1. Combining Multiple Features +```python +async with AsyncWebCrawler( + browser_type="chromium", + headless=False, + verbose=True +) as crawler: + result = await crawler.arun( + url="https://example.com", + # Anti-bot measures + magic=True, + simulate_user=True, + + # Content selection + css_selector="article.main", + word_count_threshold=10, + + # Dynamic content handling + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="css:.dynamic-content", + + # Content filtering + exclude_external_links=True, + exclude_social_media_links=True, + + # Media handling + screenshot=True, + process_iframes=True, + + # Content cleaning + remove_overlay_elements=True + ) +``` + +#### 2. Custom Extraction Pipeline +```python +# Define custom schemas and strategies +class Article(BaseModel): + title: str + content: str + date: str + +# CSS extraction for initial content +css_schema = { + "name": "Article Extraction", + "baseSelector": "article", + "fields": [ + {"name": "title", "selector": "h1", "type": "text"}, + {"name": "content", "selector": ".content", "type": "html"}, + {"name": "date", "selector": ".date", "type": "text"} + ] +} + +# LLM processing for semantic analysis +llm_strategy = LLMExtractionStrategy( + provider="ollama/nemotron", + api_token="your-token", + schema=Article.schema(), + instruction="Extract and clean article content" +) + +# Chunking strategy for large content +chunking = OverlappingWindowChunking(window_size=1000, overlap=100) + +async with AsyncWebCrawler() as crawler: + # First pass: Extract structure + css_result = await crawler.arun( + url="https://example.com", + extraction_strategy=JsonCssExtractionStrategy(css_schema) + ) + + # Second pass: Semantic processing + llm_result = await crawler.arun( + url="https://example.com", + extraction_strategy=llm_strategy, + chunking_strategy=chunking + ) +``` + +#### 3. Website Crawling with Custom Processing +```python +class CustomWebsiteCrawler: + def __init__(self, crawler: AsyncWebCrawler): + self.crawler = crawler + self.results = {} + + async def process_page(self, url: str) -> Dict: + result = await self.crawler.arun( + url=url, + magic=True, + word_count_threshold=10, + exclude_external_links=True, + process_iframes=True, + remove_overlay_elements=True + ) + + # Process internal links + internal_links = [ + link['href'] for link in result.links['internal'] + if self._is_valid_link(link['href']) + ] + + # Extract media + media_urls = [img['src'] for img in result.media['images']] + + return { + 'content': result.markdown, + 'links': internal_links, + 'media': media_urls, + 'metadata': result.metadata + } + + async def crawl_website(self, start_url: str, max_depth: int = 2): + visited = set() + queue = [(start_url, 0)] + + while queue: + url, depth = queue.pop(0) + if depth > max_depth or url in visited: + continue + + visited.add(url) + self.results[url] = await self.process_page(url) +``` + diff --git a/docs/details/input_output.md b/docs/details/input_output.md new file mode 100644 index 00000000..b735e34f --- /dev/null +++ b/docs/details/input_output.md @@ -0,0 +1,282 @@ +### AsyncWebCrawler Constructor Parameters +```python +AsyncWebCrawler( + # Core Browser Settings + browser_type: str = "chromium", # Options: "chromium", "firefox", "webkit" + headless: bool = True, # Whether to run browser in headless mode + verbose: bool = False, # Enable verbose logging + + # Cache Settings + always_by_pass_cache: bool = False, # Always bypass cache regardless of run settings + base_directory: str = str(Path.home()), # Base directory for cache storage + + # Network Settings + proxy: str = None, # Simple proxy URL (e.g., "http://proxy.example.com:8080") + proxy_config: Dict = None, # Advanced proxy settings with auth: {"server": str, "username": str, "password": str} + + # Browser Behavior + sleep_on_close: bool = False, # Wait before closing browser + + # Other Settings passed to AsyncPlaywrightCrawlerStrategy + user_agent: str = None, # Custom user agent string + headers: Dict[str, str] = {}, # Custom HTTP headers + js_code: Union[str, List[str]] = None, # Default JavaScript to execute +) +``` + +### arun() Method Parameters +```python +arun( + # Core Parameters + url: str, # Required: URL to crawl + + # Content Selection + css_selector: str = None, # CSS selector to extract specific content + word_count_threshold: int = MIN_WORD_THRESHOLD, # Minimum words for content blocks + + # Cache Control + bypass_cache: bool = False, # Bypass cache for this request + + # Session Management + session_id: str = None, # Session identifier for persistent browsing + + # Screenshot Options + screenshot: bool = False, # Take page screenshot + screenshot_wait_for: float = None, # Wait time before screenshot + + # Content Processing + process_iframes: bool = False, # Process iframe content + remove_overlay_elements: bool = False, # Remove popups/modals + + # Anti-Bot/Detection + simulate_user: bool = False, # Simulate human-like behavior + override_navigator: bool = False, # Override navigator properties + magic: bool = False, # Enable all anti-detection features + + # Content Filtering + excluded_tags: List[str] = None, # HTML tags to exclude + exclude_external_links: bool = False, # Remove external links + exclude_social_media_links: bool = False, # Remove social media links + exclude_external_images: bool = False, # Remove external images + exclude_social_media_domains: List[str] = None, # Additional social media domains to exclude + remove_forms: bool = False, # Remove all form elements + + # JavaScript Handling + js_code: Union[str, List[str]] = None, # JavaScript to execute + js_only: bool = False, # Only execute JavaScript without reloading page + wait_for: str = None, # Wait condition (CSS selector or JS function) + + # Page Loading + page_timeout: int = 60000, # Page load timeout in milliseconds + delay_before_return_html: float = None, # Wait before returning HTML + + # Debug Options + log_console: bool = False, # Log browser console messages + + # Content Format Control + only_text: bool = False, # Extract only text content + keep_data_attributes: bool = False, # Keep data-* attributes in HTML + + # Markdown Options + include_links_on_markdown: bool = False, # Include links in markdown output + html2text: Dict = {}, # HTML to text conversion options + + # Extraction Strategy + extraction_strategy: ExtractionStrategy = None, # Strategy for structured data extraction + + # Advanced Browser Control + user_agent: str = None, # Override user agent for this request +) +``` + +### Extraction Strategy Parameters +```python +# JsonCssExtractionStrategy +{ + "name": str, # Name of extraction schema + "baseSelector": str, # Base CSS selector + "fields": [ + { + "name": str, # Field name + "selector": str, # CSS selector + "type": str, # Data type ("text", etc.) + "transform": str = None # Optional transformation + } + ] +} + +# LLMExtractionStrategy +{ + "provider": str, # LLM provider (e.g., "openai/gpt-4", "huggingface/...", "ollama/...") + "api_token": str, # API token + "schema": dict, # Pydantic model schema + "extraction_type": str, # Type of extraction ("schema", etc.) + "instruction": str, # Extraction instruction + "extra_args": dict = None, # Additional provider-specific arguments + "extra_headers": dict = None # Additional HTTP headers +} +``` + +### HTML to Text Conversion Options (html2text parameter) +```python +{ + "escape_dot": bool = True, # Escape dots in text + # Other html2text library options +} +``` + + +### CrawlResult Fields + +```python +class CrawlResult(BaseModel): + # Basic Information + url: str # The crawled URL + # Example: "https://example.com" + + success: bool # Whether the crawl was successful + # Example: True/False + + status_code: Optional[int] # HTTP status code + # Example: 200, 404, 500 + + # Content Fields + html: str # Raw HTML content + # Example: "..." + + cleaned_html: Optional[str] # HTML after cleaning and processing + # Example: "

    Clean content...

    " + + fit_html: Optional[str] # Most relevant HTML content after content cleaning strategy + # Example: "

    Most relevant content...

    " + + markdown: Optional[str] # HTML converted to markdown + # Example: "# Title\n\nContent paragraph..." + + fit_markdown: Optional[str] # Most relevant content in markdown + # Example: "# Main Article\n\nKey content..." + + # Media Content + media: Dict[str, List[Dict]] = {} # Extracted media information + # Example: { + # "images": [ + # { + # "src": "https://example.com/image.jpg", + # "alt": "Image description", + # "desc": "Contextual description", + # "score": 5, # Relevance score + # "type": "image" + # } + # ], + # "videos": [ + # { + # "src": "https://example.com/video.mp4", + # "alt": "Video title", + # "type": "video", + # "description": "Video context" + # } + # ], + # "audios": [ + # { + # "src": "https://example.com/audio.mp3", + # "alt": "Audio title", + # "type": "audio", + # "description": "Audio context" + # } + # ] + # } + + # Link Information + links: Dict[str, List[Dict]] = {} # Extracted links + # Example: { + # "internal": [ + # { + # "href": "https://example.com/page", + # "text": "Link text", + # "title": "Link title" + # } + # ], + # "external": [ + # { + # "href": "https://external.com", + # "text": "External link text", + # "title": "External link title" + # } + # ] + # } + + # Extraction Results + extracted_content: Optional[str] # Content from extraction strategy + # Example for JsonCssExtractionStrategy: + # '[{"title": "Article 1", "date": "2024-03-20"}, ...]' + # Example for LLMExtractionStrategy: + # '{"entities": [...], "relationships": [...]}' + + # Additional Information + metadata: Optional[dict] = None # Page metadata + # Example: { + # "title": "Page Title", + # "description": "Meta description", + # "keywords": ["keyword1", "keyword2"], + # "author": "Author Name", + # "published_date": "2024-03-20" + # } + + screenshot: Optional[str] = None # Base64 encoded screenshot + # Example: "iVBORw0KGgoAAAANSUhEUgAA..." + + error_message: Optional[str] = None # Error message if crawl failed + # Example: "Failed to load page: timeout" + + session_id: Optional[str] = None # Session identifier + # Example: "session_123456" + + response_headers: Optional[dict] = None # HTTP response headers + # Example: { + # "content-type": "text/html", + # "server": "nginx/1.18.0", + # "date": "Wed, 20 Mar 2024 12:00:00 GMT" + # } +``` + +### Common Usage Patterns: + +1. Basic Content Extraction: +```python +result = await crawler.arun(url="https://example.com") +print(result.markdown) # Clean, readable content +print(result.cleaned_html) # Cleaned HTML +``` + +2. Media Analysis: +```python +result = await crawler.arun(url="https://example.com") +for image in result.media["images"]: + if image["score"] > 3: # High-relevance images + print(f"High-quality image: {image['src']}") +``` + +3. Link Analysis: +```python +result = await crawler.arun(url="https://example.com") +internal_links = [link["href"] for link in result.links["internal"]] +external_links = [link["href"] for link in result.links["external"]] +``` + +4. Structured Data Extraction: +```python +result = await crawler.arun( + url="https://example.com", + extraction_strategy=my_strategy +) +structured_data = json.loads(result.extracted_content) +``` + +5. Error Handling: +```python +result = await crawler.arun(url="https://example.com") +if not result.success: + print(f"Crawl failed: {result.error_message}") + print(f"Status code: {result.status_code}") +``` + diff --git a/docs/details/realworld_examples.md b/docs/details/realworld_examples.md new file mode 100644 index 00000000..d6613147 --- /dev/null +++ b/docs/details/realworld_examples.md @@ -0,0 +1,67 @@ +1. **E-commerce Product Monitor** + - Scraping product details from multiple e-commerce sites + - Price tracking with structured data extraction + - Handling dynamic content and anti-bot measures + - Features: JsonCssExtraction, session management, anti-bot + +2. **News Aggregator & Summarizer** + - Crawling news websites + - Content extraction and summarization + - Topic classification + - Features: LLMExtraction, CosineStrategy, content cleaning + +3. **Academic Paper Research Assistant** + - Crawling research papers from academic sites + - Extracting citations and references + - Building knowledge graphs + - Features: structured extraction, link analysis, chunking + +4. **Social Media Content Analyzer** + - Handling JavaScript-heavy sites + - Dynamic content loading + - Sentiment analysis integration + - Features: dynamic content handling, session management + +5. **Real Estate Market Analyzer** + - Scraping property listings + - Processing image galleries + - Geolocation data extraction + - Features: media handling, structured data extraction + +6. **Documentation Site Generator** + - Recursive website crawling + - Markdown generation + - Link validation + - Features: website crawling, content cleaning + +7. **Job Board Aggregator** + - Handling pagination + - Structured job data extraction + - Filtering and categorization + - Features: session management, JsonCssExtraction + +8. **Recipe Database Builder** + - Schema-based extraction + - Image processing + - Ingredient parsing + - Features: structured extraction, media handling + +9. **Travel Blog Content Analyzer** + - Location extraction + - Image and map processing + - Content categorization + - Features: CosineStrategy, media handling + +10. **Technical Documentation Scraper** + - API documentation extraction + - Code snippet processing + - Version tracking + - Features: content cleaning, structured extraction + +Each example will include: +- Problem description +- Technical requirements +- Complete implementation +- Error handling +- Output processing +- Performance considerations \ No newline at end of file diff --git a/docs/examples/quickstart_async.py b/docs/examples/quickstart_async.py index 9b88b332..02b5f8bb 100644 --- a/docs/examples/quickstart_async.py +++ b/docs/examples/quickstart_async.py @@ -456,7 +456,6 @@ async def speed_comparison(): print("If you run these tests in an environment with better network conditions,") print("you may observe an even more significant speed advantage for Crawl4AI.") - async def generate_knowledge_graph(): class Entity(BaseModel): name: str @@ -473,8 +472,8 @@ async def generate_knowledge_graph(): relationships: List[Relationship] extraction_strategy = LLMExtractionStrategy( - provider='openai/gpt-4o-mini', - api_token=os.getenv('OPENAI_API_KEY'), + provider='openai/gpt-4o-mini', # Or any other provider, including Ollama and open source models + api_token=os.getenv('OPENAI_API_KEY'), # In case of Ollama just pass "no-token" schema=KnowledgeGraph.model_json_schema(), extraction_type="schema", instruction="""Extract entities and relationships from the given text.""" @@ -491,6 +490,23 @@ async def generate_knowledge_graph(): with open(os.path.join(__location__, "kb.json"), "w") as f: f.write(result.extracted_content) +async def fit_markdown_remove_overlay(): + async with AsyncWebCrawler(headless = False) as crawler: + url = "https://janineintheworld.com/places-to-visit-in-central-mexico" + result = await crawler.arun( + url=url, + bypass_cache=True, + word_count_threshold = 10, + remove_overlay_elements=True, + screenshot = True + ) + # Save markdown to file + with open(os.path.join(__location__, "mexico_places.md"), "w") as f: + f.write(result.fit_markdown) + + print("Done") + + async def main(): await simple_crawl() await simple_example_with_running_js_code() diff --git a/docs/md _sync/api/core_classes_and_functions.md b/docs/md_v0/api/core_classes_and_functions.md similarity index 100% rename from docs/md _sync/api/core_classes_and_functions.md rename to docs/md_v0/api/core_classes_and_functions.md diff --git a/docs/md _sync/api/detailed_api_documentation.md b/docs/md_v0/api/detailed_api_documentation.md similarity index 100% rename from docs/md _sync/api/detailed_api_documentation.md rename to docs/md_v0/api/detailed_api_documentation.md diff --git a/docs/md _sync/assets/DankMono-Bold.woff2 b/docs/md_v0/assets/DankMono-Bold.woff2 similarity index 100% rename from docs/md _sync/assets/DankMono-Bold.woff2 rename to docs/md_v0/assets/DankMono-Bold.woff2 diff --git a/docs/md _sync/assets/DankMono-Italic.woff2 b/docs/md_v0/assets/DankMono-Italic.woff2 similarity index 100% rename from docs/md _sync/assets/DankMono-Italic.woff2 rename to docs/md_v0/assets/DankMono-Italic.woff2 diff --git a/docs/md _sync/assets/DankMono-Regular.woff2 b/docs/md_v0/assets/DankMono-Regular.woff2 similarity index 100% rename from docs/md _sync/assets/DankMono-Regular.woff2 rename to docs/md_v0/assets/DankMono-Regular.woff2 diff --git a/docs/md _sync/assets/Monaco.woff b/docs/md_v0/assets/Monaco.woff similarity index 100% rename from docs/md _sync/assets/Monaco.woff rename to docs/md_v0/assets/Monaco.woff diff --git a/docs/md _sync/assets/dmvendor.css b/docs/md_v0/assets/dmvendor.css similarity index 100% rename from docs/md _sync/assets/dmvendor.css rename to docs/md_v0/assets/dmvendor.css diff --git a/docs/md _sync/assets/highlight.css b/docs/md_v0/assets/highlight.css similarity index 100% rename from docs/md _sync/assets/highlight.css rename to docs/md_v0/assets/highlight.css diff --git a/docs/md _sync/assets/highlight.min.js b/docs/md_v0/assets/highlight.min.js similarity index 100% rename from docs/md _sync/assets/highlight.min.js rename to docs/md_v0/assets/highlight.min.js diff --git a/docs/md _sync/assets/highlight_init.js b/docs/md_v0/assets/highlight_init.js similarity index 100% rename from docs/md _sync/assets/highlight_init.js rename to docs/md_v0/assets/highlight_init.js diff --git a/docs/md _sync/assets/styles.css b/docs/md_v0/assets/styles.css similarity index 100% rename from docs/md _sync/assets/styles.css rename to docs/md_v0/assets/styles.css diff --git a/docs/md _sync/changelog.md b/docs/md_v0/changelog.md similarity index 100% rename from docs/md _sync/changelog.md rename to docs/md_v0/changelog.md diff --git a/docs/chunking_strategies.json b/docs/md_v0/chunking_strategies.json similarity index 100% rename from docs/chunking_strategies.json rename to docs/md_v0/chunking_strategies.json diff --git a/docs/md _sync/contact.md b/docs/md_v0/contact.md similarity index 100% rename from docs/md _sync/contact.md rename to docs/md_v0/contact.md diff --git a/docs/md _sync/demo.md b/docs/md_v0/demo.md similarity index 100% rename from docs/md _sync/demo.md rename to docs/md_v0/demo.md diff --git a/docs/md _sync/examples/hooks_auth.md b/docs/md_v0/examples/hooks_auth.md similarity index 100% rename from docs/md _sync/examples/hooks_auth.md rename to docs/md_v0/examples/hooks_auth.md diff --git a/docs/md _sync/examples/index.md b/docs/md_v0/examples/index.md similarity index 100% rename from docs/md _sync/examples/index.md rename to docs/md_v0/examples/index.md diff --git a/docs/md _sync/examples/js_execution_css_filtering.md b/docs/md_v0/examples/js_execution_css_filtering.md similarity index 100% rename from docs/md _sync/examples/js_execution_css_filtering.md rename to docs/md_v0/examples/js_execution_css_filtering.md diff --git a/docs/md _sync/examples/llm_extraction.md b/docs/md_v0/examples/llm_extraction.md similarity index 100% rename from docs/md _sync/examples/llm_extraction.md rename to docs/md_v0/examples/llm_extraction.md diff --git a/docs/md _sync/examples/research_assistant.md b/docs/md_v0/examples/research_assistant.md similarity index 100% rename from docs/md _sync/examples/research_assistant.md rename to docs/md_v0/examples/research_assistant.md diff --git a/docs/md _sync/examples/summarization.md b/docs/md_v0/examples/summarization.md similarity index 100% rename from docs/md _sync/examples/summarization.md rename to docs/md_v0/examples/summarization.md diff --git a/docs/extraction_strategies.json b/docs/md_v0/extraction_strategies.json similarity index 100% rename from docs/extraction_strategies.json rename to docs/md_v0/extraction_strategies.json diff --git a/docs/md _sync/full_details/advanced_features.md b/docs/md_v0/full_details/advanced_features.md similarity index 100% rename from docs/md _sync/full_details/advanced_features.md rename to docs/md_v0/full_details/advanced_features.md diff --git a/docs/md _sync/full_details/chunking_strategies.md b/docs/md_v0/full_details/chunking_strategies.md similarity index 100% rename from docs/md _sync/full_details/chunking_strategies.md rename to docs/md_v0/full_details/chunking_strategies.md diff --git a/docs/md _sync/full_details/crawl_request_parameters.md b/docs/md_v0/full_details/crawl_request_parameters.md similarity index 100% rename from docs/md _sync/full_details/crawl_request_parameters.md rename to docs/md_v0/full_details/crawl_request_parameters.md diff --git a/docs/md _sync/full_details/crawl_result_class.md b/docs/md_v0/full_details/crawl_result_class.md similarity index 100% rename from docs/md _sync/full_details/crawl_result_class.md rename to docs/md_v0/full_details/crawl_result_class.md diff --git a/docs/md _sync/full_details/extraction_strategies.md b/docs/md_v0/full_details/extraction_strategies.md similarity index 100% rename from docs/md _sync/full_details/extraction_strategies.md rename to docs/md_v0/full_details/extraction_strategies.md diff --git a/docs/md _sync/index.md b/docs/md_v0/index.md similarity index 100% rename from docs/md _sync/index.md rename to docs/md_v0/index.md diff --git a/docs/md _sync/installation.md b/docs/md_v0/installation.md similarity index 100% rename from docs/md _sync/installation.md rename to docs/md_v0/installation.md diff --git a/docs/md _sync/interactive_content.html b/docs/md_v0/interactive_content.html similarity index 100% rename from docs/md _sync/interactive_content.html rename to docs/md_v0/interactive_content.html diff --git a/docs/md _sync/introduction.md b/docs/md_v0/introduction.md similarity index 100% rename from docs/md _sync/introduction.md rename to docs/md_v0/introduction.md diff --git a/docs/md _sync/quickstart.md b/docs/md_v0/quickstart.md similarity index 100% rename from docs/md _sync/quickstart.md rename to docs/md_v0/quickstart.md diff --git a/docs/md/api/core_classes_and_functions.md b/docs/md_v1/api/core_classes_and_functions.md similarity index 100% rename from docs/md/api/core_classes_and_functions.md rename to docs/md_v1/api/core_classes_and_functions.md diff --git a/docs/md/api/detailed_api_documentation.md b/docs/md_v1/api/detailed_api_documentation.md similarity index 100% rename from docs/md/api/detailed_api_documentation.md rename to docs/md_v1/api/detailed_api_documentation.md diff --git a/docs/md/assets/DankMono-Bold.woff2 b/docs/md_v1/assets/DankMono-Bold.woff2 similarity index 100% rename from docs/md/assets/DankMono-Bold.woff2 rename to docs/md_v1/assets/DankMono-Bold.woff2 diff --git a/docs/md/assets/DankMono-Italic.woff2 b/docs/md_v1/assets/DankMono-Italic.woff2 similarity index 100% rename from docs/md/assets/DankMono-Italic.woff2 rename to docs/md_v1/assets/DankMono-Italic.woff2 diff --git a/docs/md/assets/DankMono-Regular.woff2 b/docs/md_v1/assets/DankMono-Regular.woff2 similarity index 100% rename from docs/md/assets/DankMono-Regular.woff2 rename to docs/md_v1/assets/DankMono-Regular.woff2 diff --git a/docs/md/assets/Monaco.woff b/docs/md_v1/assets/Monaco.woff similarity index 100% rename from docs/md/assets/Monaco.woff rename to docs/md_v1/assets/Monaco.woff diff --git a/docs/md/assets/dmvendor.css b/docs/md_v1/assets/dmvendor.css similarity index 100% rename from docs/md/assets/dmvendor.css rename to docs/md_v1/assets/dmvendor.css diff --git a/docs/md/assets/highlight.css b/docs/md_v1/assets/highlight.css similarity index 100% rename from docs/md/assets/highlight.css rename to docs/md_v1/assets/highlight.css diff --git a/docs/md/assets/highlight.min.js b/docs/md_v1/assets/highlight.min.js similarity index 100% rename from docs/md/assets/highlight.min.js rename to docs/md_v1/assets/highlight.min.js diff --git a/docs/md/assets/highlight_init.js b/docs/md_v1/assets/highlight_init.js similarity index 100% rename from docs/md/assets/highlight_init.js rename to docs/md_v1/assets/highlight_init.js diff --git a/docs/md/assets/styles.css b/docs/md_v1/assets/styles.css similarity index 100% rename from docs/md/assets/styles.css rename to docs/md_v1/assets/styles.css diff --git a/docs/md/changelog.md b/docs/md_v1/changelog.md similarity index 100% rename from docs/md/changelog.md rename to docs/md_v1/changelog.md diff --git a/docs/md/contact.md b/docs/md_v1/contact.md similarity index 100% rename from docs/md/contact.md rename to docs/md_v1/contact.md diff --git a/docs/md/demo.md b/docs/md_v1/demo.md similarity index 100% rename from docs/md/demo.md rename to docs/md_v1/demo.md diff --git a/docs/md/examples/hooks_auth.md b/docs/md_v1/examples/hooks_auth.md similarity index 100% rename from docs/md/examples/hooks_auth.md rename to docs/md_v1/examples/hooks_auth.md diff --git a/docs/md/examples/index.md b/docs/md_v1/examples/index.md similarity index 100% rename from docs/md/examples/index.md rename to docs/md_v1/examples/index.md diff --git a/docs/md/examples/js_execution_css_filtering.md b/docs/md_v1/examples/js_execution_css_filtering.md similarity index 100% rename from docs/md/examples/js_execution_css_filtering.md rename to docs/md_v1/examples/js_execution_css_filtering.md diff --git a/docs/md/examples/json_css_extraction.md b/docs/md_v1/examples/json_css_extraction.md similarity index 100% rename from docs/md/examples/json_css_extraction.md rename to docs/md_v1/examples/json_css_extraction.md diff --git a/docs/md/examples/llm_extraction.md b/docs/md_v1/examples/llm_extraction.md similarity index 100% rename from docs/md/examples/llm_extraction.md rename to docs/md_v1/examples/llm_extraction.md diff --git a/docs/md/examples/research_assistant.md b/docs/md_v1/examples/research_assistant.md similarity index 100% rename from docs/md/examples/research_assistant.md rename to docs/md_v1/examples/research_assistant.md diff --git a/docs/md/examples/summarization.md b/docs/md_v1/examples/summarization.md similarity index 100% rename from docs/md/examples/summarization.md rename to docs/md_v1/examples/summarization.md diff --git a/docs/md/full_details/advanced_features.md b/docs/md_v1/full_details/advanced_features.md similarity index 100% rename from docs/md/full_details/advanced_features.md rename to docs/md_v1/full_details/advanced_features.md diff --git a/docs/md/full_details/advanced_jsoncss_extraction.md b/docs/md_v1/full_details/advanced_jsoncss_extraction.md similarity index 100% rename from docs/md/full_details/advanced_jsoncss_extraction.md rename to docs/md_v1/full_details/advanced_jsoncss_extraction.md diff --git a/docs/md/full_details/chunking_strategies.md b/docs/md_v1/full_details/chunking_strategies.md similarity index 100% rename from docs/md/full_details/chunking_strategies.md rename to docs/md_v1/full_details/chunking_strategies.md diff --git a/docs/md/full_details/crawl_request_parameters.md b/docs/md_v1/full_details/crawl_request_parameters.md similarity index 100% rename from docs/md/full_details/crawl_request_parameters.md rename to docs/md_v1/full_details/crawl_request_parameters.md diff --git a/docs/md/full_details/crawl_result_class.md b/docs/md_v1/full_details/crawl_result_class.md similarity index 100% rename from docs/md/full_details/crawl_result_class.md rename to docs/md_v1/full_details/crawl_result_class.md diff --git a/docs/md/full_details/extraction_strategies.md b/docs/md_v1/full_details/extraction_strategies.md similarity index 100% rename from docs/md/full_details/extraction_strategies.md rename to docs/md_v1/full_details/extraction_strategies.md diff --git a/docs/md/full_details/session_based_crawling.md b/docs/md_v1/full_details/session_based_crawling.md similarity index 100% rename from docs/md/full_details/session_based_crawling.md rename to docs/md_v1/full_details/session_based_crawling.md diff --git a/docs/md/index.md b/docs/md_v1/index.md similarity index 100% rename from docs/md/index.md rename to docs/md_v1/index.md diff --git a/docs/md/installation.md b/docs/md_v1/installation.md similarity index 100% rename from docs/md/installation.md rename to docs/md_v1/installation.md diff --git a/docs/md/interactive_content.html b/docs/md_v1/interactive_content.html similarity index 100% rename from docs/md/interactive_content.html rename to docs/md_v1/interactive_content.html diff --git a/docs/md/introduction.md b/docs/md_v1/introduction.md similarity index 100% rename from docs/md/introduction.md rename to docs/md_v1/introduction.md diff --git a/docs/md_v1/mkdocs.yml b/docs/md_v1/mkdocs.yml new file mode 100644 index 00000000..d56ddfec --- /dev/null +++ b/docs/md_v1/mkdocs.yml @@ -0,0 +1,45 @@ +site_name: Crawl4AI Documentation +site_description: πŸ”₯πŸ•·οΈ Crawl4AI, Open-source LLM Friendly Web Crawler & Scrapper +site_url: https://docs.crawl4ai.com +repo_url: https://github.com/unclecode/crawl4ai +repo_name: unclecode/crawl4ai +docs_dir: docs/md +nav: + - Home: index.md + - First Steps: + - Introduction: introduction.md + - Installation: installation.md + - Quick Start: quickstart.md + - Examples: + - Intro: examples/index.md + - Structured Data Extraction: examples/json_css_extraction.md + - LLM Extraction: examples/llm_extraction.md + - JS Execution & CSS Filtering: examples/js_execution_css_filtering.md + - Hooks & Auth: examples/hooks_auth.md + - Summarization: examples/summarization.md + - Research Assistant: examples/research_assistant.md + - Full Details of Using Crawler: + - Crawl Request Parameters: full_details/crawl_request_parameters.md + - Crawl Result Class: full_details/crawl_result_class.md + - Session Based Crawling: full_details/session_based_crawling.md + - Advanced Features: full_details/advanced_features.md + - Advanced JsonCssExtraction: full_details/advanced_jsoncss_extraction.md + - Chunking Strategies: full_details/chunking_strategies.md + - Extraction Strategies: full_details/extraction_strategies.md + - Miscellaneous: + - Change Log: changelog.md + - Contact: contact.md + +theme: + name: terminal + palette: dark + +# Add the css/extra.css +extra_css: + - assets/styles.css + - assets/highlight.css + - assets/dmvendor.css + +extra_javascript: + - assets/highlight.min.js + - assets/highlight_init.js diff --git a/docs/md/quickstart.md b/docs/md_v1/quickstart.md similarity index 92% rename from docs/md/quickstart.md rename to docs/md_v1/quickstart.md index 39ab6367..ef79faca 100644 --- a/docs/md/quickstart.md +++ b/docs/md_v1/quickstart.md @@ -147,8 +147,8 @@ async def main(): url="https://openai.com/api/pricing/", word_count_threshold=1, extraction_strategy=LLMExtractionStrategy( - provider="openai/gpt-4o", - api_token=os.getenv("OPENAI_API_KEY"), + provider="openai/gpt-4o", # Or use open source model like "ollama/nemotron" + api_token=os.getenv("OPENAI_API_KEY"), # Pass "no-token" if using Ollama schema=OpenAIModelFee.schema(), extraction_type="schema", instruction="""From the crawled content, extract all mentioned model names along with their fees for input and output tokens. @@ -196,11 +196,11 @@ In modern web applications, content is often loaded dynamically without changing Here's what makes this approach powerful: -1. **Session Preservation**: By using a `session_id`, we can maintain the state of our crawling session across multiple interactions with the page. This is crucial for navigating through dynamically loaded content. +1.**Session Preservation**: By using a `session_id`, we can maintain the state of our crawling session across multiple interactions with the page. This is crucial for navigating through dynamically loaded content. -2. **Asynchronous JavaScript Execution**: We can execute custom JavaScript to trigger content loading or navigation. In this example, we'll click a "Load More" button to fetch the next page of commits. +2.**Asynchronous JavaScript Execution**: We can execute custom JavaScript to trigger content loading or navigation. In this example, we'll click a "Load More" button to fetch the next page of commits. -3. **Dynamic Content Waiting**: The `wait_for` parameter allows us to specify a condition that must be met before considering the page load complete. This ensures we don't extract data before the new content is fully loaded. +3.**Dynamic Content Waiting**: The `wait_for` parameter allows us to specify a condition that must be met before considering the page load complete. This ensures we don't extract data before the new content is fully loaded. Let's see how this works with a real-world example: crawling multiple pages of commits on a GitHub repository. The URL doesn't change as we load more commits, so we'll use these advanced techniques to navigate and extract data. diff --git a/docs/md_v2/advanced/content-processing.md b/docs/md_v2/advanced/content-processing.md new file mode 100644 index 00000000..71a32438 --- /dev/null +++ b/docs/md_v2/advanced/content-processing.md @@ -0,0 +1,223 @@ +# Content Processing + +Crawl4AI provides powerful content processing capabilities that help you extract clean, relevant content from web pages. This guide covers content cleaning, media handling, link analysis, and metadata extraction. + +## Content Cleaning + +### Understanding Clean Content +When crawling web pages, you often encounter a lot of noise - advertisements, navigation menus, footers, popups, and other irrelevant content. Crawl4AI automatically cleans this noise using several approaches: + +1. **Basic Cleaning**: Removes unwanted HTML elements and attributes +2. **Content Relevance**: Identifies and preserves meaningful content blocks +3. **Layout Analysis**: Understands page structure to identify main content areas + +```python +result = await crawler.arun( + url="https://example.com", + word_count_threshold=10, # Remove blocks with fewer words + excluded_tags=['form', 'nav'], # Remove specific HTML tags + remove_overlay_elements=True # Remove popups/modals +) + +# Get clean content +print(result.cleaned_html) # Cleaned HTML +print(result.markdown) # Clean markdown version +``` + +### Fit Markdown: Smart Content Extraction +One of Crawl4AI's most powerful features is `fit_markdown`. This feature uses advanced heuristics to identify and extract the main content from a webpage while excluding irrelevant elements. + +#### How Fit Markdown Works +- Analyzes content density and distribution +- Identifies content patterns and structures +- Removes boilerplate content (headers, footers, sidebars) +- Preserves the most relevant content blocks +- Maintains content hierarchy and formatting + +#### Perfect For: +- Blog posts and articles +- News content +- Documentation pages +- Any page with a clear main content area + +#### Not Recommended For: +- E-commerce product listings +- Search results pages +- Social media feeds +- Pages with multiple equal-weight content sections + +```python +result = await crawler.arun(url="https://example.com") + +# Get the most relevant content +main_content = result.fit_markdown + +# Compare with regular markdown +all_content = result.markdown + +print(f"Fit Markdown Length: {len(main_content)}") +print(f"Regular Markdown Length: {len(all_content)}") +``` + +#### Example Use Case +```python +async def extract_article_content(url: str) -> str: + """Extract main article content from a blog or news site.""" + async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url=url) + + # fit_markdown will focus on the article content, + # excluding navigation, ads, and other distractions + return result.fit_markdown +``` + +## Media Processing + +Crawl4AI provides comprehensive media extraction and analysis capabilities. It automatically detects and processes various types of media elements while maintaining their context and relevance. + +### Image Processing +The library handles various image scenarios, including: +- Regular images +- Lazy-loaded images +- Background images +- Responsive images +- Image metadata and context + +```python +result = await crawler.arun(url="https://example.com") + +for image in result.media["images"]: + # Each image includes rich metadata + print(f"Source: {image['src']}") + print(f"Alt text: {image['alt']}") + print(f"Description: {image['desc']}") + print(f"Context: {image['context']}") # Surrounding text + print(f"Relevance score: {image['score']}") # 0-10 score +``` + +### Handling Lazy-Loaded Content +Crawl4aai already handles lazy loading for media elements. You can also customize the wait time for lazy-loaded content: + +```python +result = await crawler.arun( + url="https://example.com", + wait_for="css:img[data-src]", # Wait for lazy images + delay_before_return_html=2.0 # Additional wait time +) +``` + +### Video and Audio Content +The library extracts video and audio elements with their metadata: + +```python +# Process videos +for video in result.media["videos"]: + print(f"Video source: {video['src']}") + print(f"Type: {video['type']}") + print(f"Duration: {video.get('duration')}") + print(f"Thumbnail: {video.get('poster')}") + +# Process audio +for audio in result.media["audios"]: + print(f"Audio source: {audio['src']}") + print(f"Type: {audio['type']}") + print(f"Duration: {audio.get('duration')}") +``` + +## Link Analysis + +Crawl4AI provides sophisticated link analysis capabilities, helping you understand the relationship between pages and identify important navigation patterns. + +### Link Classification +The library automatically categorizes links into: +- Internal links (same domain) +- External links (different domains) +- Social media links +- Navigation links +- Content links + +```python +result = await crawler.arun(url="https://example.com") + +# Analyze internal links +for link in result.links["internal"]: + print(f"Internal: {link['href']}") + print(f"Link text: {link['text']}") + print(f"Context: {link['context']}") # Surrounding text + print(f"Type: {link['type']}") # nav, content, etc. + +# Analyze external links +for link in result.links["external"]: + print(f"External: {link['href']}") + print(f"Domain: {link['domain']}") + print(f"Type: {link['type']}") +``` + +### Smart Link Filtering +Control which links are included in the results: + +```python +result = await crawler.arun( + url="https://example.com", + exclude_external_links=True, # Remove external links + exclude_social_media_links=True, # Remove social media links + exclude_social_media_domains=[ # Custom social media domains + "facebook.com", "twitter.com", "instagram.com" + ], + exclude_domains=["ads.example.com"] # Exclude specific domains +) +``` + +## Metadata Extraction + +Crawl4AI automatically extracts and processes page metadata, providing valuable information about the content: + +```python +result = await crawler.arun(url="https://example.com") + +metadata = result.metadata +print(f"Title: {metadata['title']}") +print(f"Description: {metadata['description']}") +print(f"Keywords: {metadata['keywords']}") +print(f"Author: {metadata['author']}") +print(f"Published Date: {metadata['published_date']}") +print(f"Modified Date: {metadata['modified_date']}") +print(f"Language: {metadata['language']}") +``` + +## Best Practices + +1. **Use Fit Markdown for Articles** + ```python + # Perfect for blog posts, news articles, documentation + content = result.fit_markdown + ``` + +2. **Handle Media Appropriately** + ```python + # Filter by relevance score + relevant_images = [ + img for img in result.media["images"] + if img['score'] > 5 + ] + ``` + +3. **Combine Link Analysis with Content** + ```python + # Get content links with context + content_links = [ + link for link in result.links["internal"] + if link['type'] == 'content' + ] + ``` + +4. **Clean Content with Purpose** + ```python + # Customize cleaning based on your needs + result = await crawler.arun( + url=url, + word_count_threshold=20, # Adjust based on content type + keep_data_attributes=False, # Remove data attributes + process_iframes=True # Include iframe content + ) + ``` \ No newline at end of file diff --git a/docs/md_v2/advanced/hooks-auth.md b/docs/md_v2/advanced/hooks-auth.md new file mode 100644 index 00000000..e4b7d7ce --- /dev/null +++ b/docs/md_v2/advanced/hooks-auth.md @@ -0,0 +1,110 @@ +# Hooks & Auth for AsyncWebCrawler + +Crawl4AI's AsyncWebCrawler allows you to customize the behavior of the web crawler using hooks. Hooks are asynchronous functions that are called at specific points in the crawling process, allowing you to modify the crawler's behavior or perform additional actions. This example demonstrates how to use various hooks to customize the asynchronous crawling process. + +## Example: Using Crawler Hooks with AsyncWebCrawler + +Let's see how we can customize the AsyncWebCrawler using hooks! In this example, we'll: + +1. Configure the browser when it's created. +2. Add custom headers before navigating to the URL. +3. Log the current URL after navigation. +4. Perform actions after JavaScript execution. +5. Log the length of the HTML before returning it. + +### Hook Definitions + +```python +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy +from playwright.async_api import Page, Browser + +async def on_browser_created(browser: Browser): + print("[HOOK] on_browser_created") + # Example customization: set browser viewport size + context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) + page = await context.new_page() + + # Example customization: logging in to a hypothetical website + await page.goto('https://example.com/login') + await page.fill('input[name="username"]', 'testuser') + await page.fill('input[name="password"]', 'password123') + await page.click('button[type="submit"]') + await page.wait_for_selector('#welcome') + + # Add a custom cookie + await context.add_cookies([{'name': 'test_cookie', 'value': 'cookie_value', 'url': 'https://example.com'}]) + + await page.close() + await context.close() + +async def before_goto(page: Page): + print("[HOOK] before_goto") + # Example customization: add custom headers + await page.set_extra_http_headers({'X-Test-Header': 'test'}) + +async def after_goto(page: Page): + print("[HOOK] after_goto") + # Example customization: log the URL + print(f"Current URL: {page.url}") + +async def on_execution_started(page: Page): + print("[HOOK] on_execution_started") + # Example customization: perform actions after JS execution + await page.evaluate("console.log('Custom JS executed')") + +async def before_return_html(page: Page, html: str): + print("[HOOK] before_return_html") + # Example customization: log the HTML length + print(f"HTML length: {len(html)}") + return page +``` + +### Using the Hooks with the AsyncWebCrawler + +```python +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.async_crawler_strategy import AsyncPlaywrightCrawlerStrategy + +async def main(): + print("\nπŸ”— Using Crawler Hooks: Let's see how we can customize the AsyncWebCrawler using hooks!") + + crawler_strategy = AsyncPlaywrightCrawlerStrategy(verbose=True) + crawler_strategy.set_hook('on_browser_created', on_browser_created) + crawler_strategy.set_hook('before_goto', before_goto) + crawler_strategy.set_hook('after_goto', after_goto) + crawler_strategy.set_hook('on_execution_started', on_execution_started) + crawler_strategy.set_hook('before_return_html', before_return_html) + + async with AsyncWebCrawler(verbose=True, crawler_strategy=crawler_strategy) as crawler: + result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="footer" + ) + + print("πŸ“¦ Crawler Hooks result:") + print(result) + +asyncio.run(main()) +``` + +### Explanation + +- `on_browser_created`: This hook is called when the Playwright browser is created. It sets up the browser context, logs in to a website, and adds a custom cookie. +- `before_goto`: This hook is called right before Playwright navigates to the URL. It adds custom HTTP headers. +- `after_goto`: This hook is called after Playwright navigates to the URL. It logs the current URL. +- `on_execution_started`: This hook is called after any custom JavaScript is executed. It performs additional JavaScript actions. +- `before_return_html`: This hook is called before returning the HTML content. It logs the length of the HTML content. + +### Additional Ideas + +- **Handling authentication**: Use the `on_browser_created` hook to handle login processes or set authentication tokens. +- **Dynamic header modification**: Modify headers based on the target URL or other conditions in the `before_goto` hook. +- **Content verification**: Use the `after_goto` hook to verify that the expected content is present on the page. +- **Custom JavaScript injection**: Inject and execute custom JavaScript using the `on_execution_started` hook. +- **Content preprocessing**: Modify or analyze the HTML content in the `before_return_html` hook before it's returned. + +By using these hooks, you can customize the behavior of the AsyncWebCrawler to suit your specific needs, including handling authentication, modifying requests, and preprocessing content. \ No newline at end of file diff --git a/docs/md_v2/advanced/hooks.md b/docs/md_v2/advanced/hooks.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/md_v2/advanced/magic-mode.md b/docs/md_v2/advanced/magic-mode.md new file mode 100644 index 00000000..16c7229e --- /dev/null +++ b/docs/md_v2/advanced/magic-mode.md @@ -0,0 +1,52 @@ +# Magic Mode & Anti-Bot Protection + +Crawl4AI provides powerful anti-detection capabilities, with Magic Mode being the simplest and most comprehensive solution. + +## Magic Mode + +The easiest way to bypass anti-bot protections: + +```python +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com", + magic=True # Enables all anti-detection features + ) +``` + +Magic Mode automatically: +- Masks browser automation signals +- Simulates human-like behavior +- Overrides navigator properties +- Handles cookie consent popups +- Manages browser fingerprinting +- Randomizes timing patterns + +## Manual Anti-Bot Options + +While Magic Mode is recommended, you can also configure individual anti-detection features: + +```python +result = await crawler.arun( + url="https://example.com", + simulate_user=True, # Simulate human behavior + override_navigator=True # Mask automation signals +) +``` + +Note: When `magic=True` is used, you don't need to set these individual options. + +## Example: Handling Protected Sites + +```python +async def crawl_protected_site(url: str): + async with AsyncWebCrawler(headless=True) as crawler: + result = await crawler.arun( + url=url, + magic=True, + remove_overlay_elements=True, # Remove popups/modals + page_timeout=60000 # Increased timeout for protection checks + ) + + return result.markdown if result.success else None +``` diff --git a/docs/md_v2/advanced/proxy-security.md b/docs/md_v2/advanced/proxy-security.md new file mode 100644 index 00000000..c7602531 --- /dev/null +++ b/docs/md_v2/advanced/proxy-security.md @@ -0,0 +1,84 @@ +# Proxy & Security + +Configure proxy settings and enhance security features in Crawl4AI for reliable data extraction. + +## Basic Proxy Setup + +Simple proxy configuration: + +```python +# Using proxy URL +async with AsyncWebCrawler( + proxy="http://proxy.example.com:8080" +) as crawler: + result = await crawler.arun(url="https://example.com") + +# Using SOCKS proxy +async with AsyncWebCrawler( + proxy="socks5://proxy.example.com:1080" +) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Authenticated Proxy + +Use proxy with authentication: + +```python +proxy_config = { + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" +} + +async with AsyncWebCrawler(proxy_config=proxy_config) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Rotating Proxies + +Example using a proxy rotation service: + +```python +async def get_next_proxy(): + # Your proxy rotation logic here + return {"server": "http://next.proxy.com:8080"} + +async with AsyncWebCrawler() as crawler: + # Update proxy for each request + for url in urls: + proxy = await get_next_proxy() + crawler.update_proxy(proxy) + result = await crawler.arun(url=url) +``` + +## Custom Headers + +Add security-related headers: + +```python +headers = { + "X-Forwarded-For": "203.0.113.195", + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "no-cache", + "Pragma": "no-cache" +} + +async with AsyncWebCrawler(headers=headers) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Combining with Magic Mode + +For maximum protection, combine proxy with Magic Mode: + +```python +async with AsyncWebCrawler( + proxy="http://proxy.example.com:8080", + headers={"Accept-Language": "en-US"} +) as crawler: + result = await crawler.arun( + url="https://example.com", + magic=True # Enable all anti-detection features + ) +``` \ No newline at end of file diff --git a/docs/md_v2/advanced/session-management-advanced.md b/docs/md_v2/advanced/session-management-advanced.md new file mode 100644 index 00000000..f8c81da2 --- /dev/null +++ b/docs/md_v2/advanced/session-management-advanced.md @@ -0,0 +1,276 @@ +# Session-Based Crawling for Dynamic Content + +In modern web applications, content is often loaded dynamically without changing the URL. Examples include "Load More" buttons, infinite scrolling, or paginated content that updates via JavaScript. To effectively crawl such websites, Crawl4AI provides powerful session-based crawling capabilities. + +This guide will explore advanced techniques for crawling dynamic content using Crawl4AI's session management features. + +## Understanding Session-Based Crawling + +Session-based crawling allows you to maintain a persistent browser session across multiple requests. This is crucial when: + +1. The content changes dynamically without URL changes +2. You need to interact with the page (e.g., clicking buttons) between requests +3. The site requires authentication or maintains state across pages + +Crawl4AI's `AsyncWebCrawler` class supports session-based crawling through the `session_id` parameter and related methods. + +## Basic Concepts + +Before diving into examples, let's review some key concepts: + +- **Session ID**: A unique identifier for a browsing session. Use the same `session_id` across multiple `arun` calls to maintain state. +- **JavaScript Execution**: Use the `js_code` parameter to execute JavaScript on the page, such as clicking a "Load More" button. +- **CSS Selectors**: Use these to target specific elements for extraction or interaction. +- **Extraction Strategy**: Define how to extract structured data from the page. +- **Wait Conditions**: Specify conditions to wait for before considering the page loaded. + +## Example 1: Basic Session-Based Crawling + +Let's start with a basic example of session-based crawling: + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def basic_session_crawl(): + async with AsyncWebCrawler(verbose=True) as crawler: + session_id = "my_session" + url = "https://example.com/dynamic-content" + + for page in range(3): + result = await crawler.arun( + url=url, + session_id=session_id, + js_code="document.querySelector('.load-more-button').click();" if page > 0 else None, + css_selector=".content-item", + bypass_cache=True + ) + + print(f"Page {page + 1}: Found {result.extracted_content.count('.content-item')} items") + + await crawler.crawler_strategy.kill_session(session_id) + +asyncio.run(basic_session_crawl()) +``` + +This example demonstrates: +1. Using a consistent `session_id` across multiple `arun` calls +2. Executing JavaScript to load more content after the first page +3. Using a CSS selector to extract specific content +4. Properly closing the session after crawling + +## Advanced Technique 1: Custom Execution Hooks + +Crawl4AI allows you to set custom hooks that execute at different stages of the crawling process. This is particularly useful for handling complex loading scenarios. + +Here's an example that waits for new content to appear before proceeding: + +```python +async def advanced_session_crawl_with_hooks(): + first_commit = "" + + async def on_execution_started(page): + nonlocal first_commit + try: + while True: + await page.wait_for_selector("li.commit-item h4") + commit = await page.query_selector("li.commit-item h4") + commit = await commit.evaluate("(element) => element.textContent") + commit = commit.strip() + if commit and commit != first_commit: + first_commit = commit + break + await asyncio.sleep(0.5) + except Exception as e: + print(f"Warning: New content didn't appear after JavaScript execution: {e}") + + async with AsyncWebCrawler(verbose=True) as crawler: + crawler.crawler_strategy.set_hook("on_execution_started", on_execution_started) + + url = "https://github.com/example/repo/commits/main" + session_id = "commit_session" + all_commits = [] + + js_next_page = """ + const button = document.querySelector('a.pagination-next'); + if (button) button.click(); + """ + + for page in range(3): + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.commit-item", + js_code=js_next_page if page > 0 else None, + bypass_cache=True, + js_only=page > 0 + ) + + commits = result.extracted_content.select("li.commit-item") + all_commits.extend(commits) + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +asyncio.run(advanced_session_crawl_with_hooks()) +``` + +This technique uses a custom `on_execution_started` hook to ensure new content has loaded before proceeding to the next step. + +## Advanced Technique 2: Integrated JavaScript Execution and Waiting + +Instead of using separate hooks, you can integrate the waiting logic directly into your JavaScript execution. This approach can be more concise and easier to manage for some scenarios. + +Here's an example: + +```python +async def integrated_js_and_wait_crawl(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://github.com/example/repo/commits/main" + session_id = "integrated_session" + all_commits = [] + + js_next_page_and_wait = """ + (async () => { + const getCurrentCommit = () => { + const commits = document.querySelectorAll('li.commit-item h4'); + return commits.length > 0 ? commits[0].textContent.trim() : null; + }; + + const initialCommit = getCurrentCommit(); + const button = document.querySelector('a.pagination-next'); + if (button) button.click(); + + while (true) { + await new Promise(resolve => setTimeout(resolve, 100)); + const newCommit = getCurrentCommit(); + if (newCommit && newCommit !== initialCommit) { + break; + } + } + })(); + """ + + schema = { + "name": "Commit Extractor", + "baseSelector": "li.commit-item", + "fields": [ + { + "name": "title", + "selector": "h4.commit-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + for page in range(3): + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.commit-item", + extraction_strategy=extraction_strategy, + js_code=js_next_page_and_wait if page > 0 else None, + js_only=page > 0, + bypass_cache=True + ) + + commits = json.loads(result.extracted_content) + all_commits.extend(commits) + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +asyncio.run(integrated_js_and_wait_crawl()) +``` + +This approach combines the JavaScript for clicking the "next" button and waiting for new content to load into a single script. + +## Advanced Technique 3: Using the `wait_for` Parameter + +Crawl4AI provides a `wait_for` parameter that allows you to specify a condition to wait for before considering the page fully loaded. This can be particularly useful for dynamic content. + +Here's an example: + +```python +async def wait_for_parameter_crawl(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://github.com/example/repo/commits/main" + session_id = "wait_for_session" + all_commits = [] + + js_next_page = """ + const commits = document.querySelectorAll('li.commit-item h4'); + if (commits.length > 0) { + window.lastCommit = commits[0].textContent.trim(); + } + const button = document.querySelector('a.pagination-next'); + if (button) button.click(); + """ + + wait_for = """() => { + const commits = document.querySelectorAll('li.commit-item h4'); + if (commits.length === 0) return false; + const firstCommit = commits[0].textContent.trim(); + return firstCommit !== window.lastCommit; + }""" + + schema = { + "name": "Commit Extractor", + "baseSelector": "li.commit-item", + "fields": [ + { + "name": "title", + "selector": "h4.commit-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + for page in range(3): + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.commit-item", + extraction_strategy=extraction_strategy, + js_code=js_next_page if page > 0 else None, + wait_for=wait_for if page > 0 else None, + js_only=page > 0, + bypass_cache=True + ) + + commits = json.loads(result.extracted_content) + all_commits.extend(commits) + print(f"Page {page + 1}: Found {len(commits)} commits") + + await crawler.crawler_strategy.kill_session(session_id) + print(f"Successfully crawled {len(all_commits)} commits across 3 pages") + +asyncio.run(wait_for_parameter_crawl()) +``` + +This technique separates the JavaScript execution (clicking the "next" button) from the waiting condition, providing more flexibility and clarity in some scenarios. + +## Best Practices for Session-Based Crawling + +1. **Use Unique Session IDs**: Ensure each crawling session has a unique `session_id` to prevent conflicts. +2. **Close Sessions**: Always close sessions using `kill_session` when you're done to free up resources. +3. **Handle Errors**: Implement proper error handling to deal with unexpected situations during crawling. +4. **Respect Website Terms**: Ensure your crawling adheres to the website's terms of service and robots.txt file. +5. **Implement Delays**: Add appropriate delays between requests to avoid overwhelming the target server. +6. **Use Extraction Strategies**: Leverage `JsonCssExtractionStrategy` or other extraction strategies for structured data extraction. +7. **Optimize JavaScript**: Keep your JavaScript execution concise and efficient to improve crawling speed. +8. **Monitor Performance**: Keep an eye on memory usage and crawling speed, especially for long-running sessions. + +## Conclusion + +Session-based crawling with Crawl4AI provides powerful capabilities for handling dynamic content and complex web applications. By leveraging session management, JavaScript execution, and waiting strategies, you can effectively crawl and extract data from a wide range of modern websites. + +Remember to use these techniques responsibly and in compliance with website policies and ethical web scraping practices. + +For more advanced usage and API details, refer to the Crawl4AI API documentation. \ No newline at end of file diff --git a/docs/md_v2/advanced/session-management.md b/docs/md_v2/advanced/session-management.md new file mode 100644 index 00000000..c38ed852 --- /dev/null +++ b/docs/md_v2/advanced/session-management.md @@ -0,0 +1,133 @@ +# Session Management + +Session management in Crawl4AI allows you to maintain state across multiple requests and handle complex multi-page crawling tasks, particularly useful for dynamic websites. + +## Basic Session Usage + +Use `session_id` to maintain state between requests: + +```python +async with AsyncWebCrawler() as crawler: + session_id = "my_session" + + # First request + result1 = await crawler.arun( + url="https://example.com/page1", + session_id=session_id + ) + + # Subsequent request using same session + result2 = await crawler.arun( + url="https://example.com/page2", + session_id=session_id + ) + + # Clean up when done + await crawler.crawler_strategy.kill_session(session_id) +``` + +## Dynamic Content with Sessions + +Here's a real-world example of crawling GitHub commits across multiple pages: + +```python +async def crawl_dynamic_content(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + all_commits = [] + + # Define navigation JavaScript + js_next_page = """ + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + # Define wait condition + wait_for = """() => { + const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); + if (commits.length === 0) return false; + const firstCommit = commits[0].textContent.trim(); + return firstCommit !== window.firstCommit; + }""" + + # Define extraction schema + schema = { + "name": "Commit Extractor", + "baseSelector": "li.Box-sc-g0xbh4-0", + "fields": [ + { + "name": "title", + "selector": "h4.markdown-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema) + + # Crawl multiple pages + for page in range(3): + result = await crawler.arun( + url=url, + session_id=session_id, + extraction_strategy=extraction_strategy, + js_code=js_next_page if page > 0 else None, + wait_for=wait_for if page > 0 else None, + js_only=page > 0, + bypass_cache=True + ) + + if result.success: + commits = json.loads(result.extracted_content) + all_commits.extend(commits) + print(f"Page {page + 1}: Found {len(commits)} commits") + + # Clean up session + await crawler.crawler_strategy.kill_session(session_id) + return all_commits +``` + +## Session Best Practices + +1. **Session Naming**: +```python +# Use descriptive session IDs +session_id = "login_flow_session" +session_id = "product_catalog_session" +``` + +2. **Resource Management**: +```python +try: + # Your crawling code + pass +finally: + # Always clean up sessions + await crawler.crawler_strategy.kill_session(session_id) +``` + +3. **State Management**: +```python +# First page: login +result = await crawler.arun( + url="https://example.com/login", + session_id=session_id, + js_code="document.querySelector('form').submit();" +) + +# Second page: verify login success +result = await crawler.arun( + url="https://example.com/dashboard", + session_id=session_id, + wait_for="css:.user-profile" # Wait for authenticated content +) +``` + +## Common Use Cases + +1. **Authentication Flows** +2. **Pagination Handling** +3. **Form Submissions** +4. **Multi-step Processes** +5. **Dynamic Content Navigation** diff --git a/docs/md_v2/api/arun.md b/docs/md_v2/api/arun.md new file mode 100644 index 00000000..9ef73aef --- /dev/null +++ b/docs/md_v2/api/arun.md @@ -0,0 +1,226 @@ +# Complete Parameter Guide for arun() + +The following parameters can be passed to the `arun()` method. They are organized by their primary usage context and functionality. + +## Core Parameters + +```python +await crawler.arun( + url="https://example.com", # Required: URL to crawl + verbose=True, # Enable detailed logging + bypass_cache=False, # Skip cache for this request + warmup=True # Whether to run warmup check +) +``` + +## Content Processing Parameters + +### Text Processing +```python +await crawler.arun( + word_count_threshold=10, # Minimum words per content block + image_description_min_word_threshold=5, # Minimum words for image descriptions + only_text=False, # Extract only text content + excluded_tags=['form', 'nav'], # HTML tags to exclude + keep_data_attributes=False, # Preserve data-* attributes +) +``` + +### Content Selection +```python +await crawler.arun( + css_selector=".main-content", # CSS selector for content extraction + remove_forms=True, # Remove all form elements + remove_overlay_elements=True, # Remove popups/modals/overlays +) +``` + +### Link Handling +```python +await crawler.arun( + exclude_external_links=True, # Remove external links + exclude_social_media_links=True, # Remove social media links + exclude_external_images=True, # Remove external images + exclude_domains=["ads.example.com"], # Specific domains to exclude + social_media_domains=[ # Additional social media domains + "facebook.com", + "twitter.com", + "instagram.com" + ] +) +``` + +## Browser Control Parameters + +### Basic Browser Settings +```python +await crawler.arun( + headless=True, # Run browser in headless mode + browser_type="chromium", # Browser engine: "chromium", "firefox", "webkit" + page_timeout=60000, # Page load timeout in milliseconds + user_agent="custom-agent", # Custom user agent +) +``` + +### Navigation and Waiting +```python +await crawler.arun( + wait_for="css:.dynamic-content", # Wait for element/condition + delay_before_return_html=2.0, # Wait before returning HTML (seconds) +) +``` + +### JavaScript Execution +```python +await crawler.arun( + js_code=[ # JavaScript to execute (string or list) + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more').click();" + ], + js_only=False, # Only execute JavaScript without reloading page +) +``` + +### Anti-Bot Features +```python +await crawler.arun( + magic=True, # Enable all anti-detection features + simulate_user=True, # Simulate human behavior + override_navigator=True # Override navigator properties +) +``` + +### Session Management +```python +await crawler.arun( + session_id="my_session", # Session identifier for persistent browsing +) +``` + +### Screenshot Options +```python +await crawler.arun( + screenshot=True, # Take page screenshot + screenshot_wait_for=2.0, # Wait before screenshot (seconds) +) +``` + +### Proxy Configuration +```python +await crawler.arun( + proxy="http://proxy.example.com:8080", # Simple proxy URL + proxy_config={ # Advanced proxy settings + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" + } +) +``` + +## Content Extraction Parameters + +### Extraction Strategy +```python +await crawler.arun( + extraction_strategy=LLMExtractionStrategy( + provider="ollama/llama2", + schema=MySchema.schema(), + instruction="Extract specific data" + ) +) +``` + +### Chunking Strategy +```python +await crawler.arun( + chunking_strategy=RegexChunking( + patterns=[r'\n\n', r'\.\s+'] + ) +) +``` + +### HTML to Text Options +```python +await crawler.arun( + html2text={ + "ignore_links": False, + "ignore_images": False, + "escape_dot": False, + "body_width": 0, + "protect_links": True, + "unicode_snob": True + } +) +``` + +## Debug Options +```python +await crawler.arun( + log_console=True, # Log browser console messages +) +``` + +## Parameter Interactions and Notes + +1. **Magic Mode Combinations** + ```python + # Full anti-detection setup + await crawler.arun( + magic=True, + headless=False, + simulate_user=True, + override_navigator=True + ) + ``` + +2. **Dynamic Content Handling** + ```python + # Handle lazy-loaded content + await crawler.arun( + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="css:.lazy-content", + delay_before_return_html=2.0 + ) + ``` + +3. **Content Extraction Pipeline** + ```python + # Complete extraction setup + await crawler.arun( + css_selector=".main-content", + word_count_threshold=20, + extraction_strategy=my_strategy, + chunking_strategy=my_chunking, + process_iframes=True, + remove_overlay_elements=True + ) + ``` + +## Best Practices + +1. **Performance Optimization** + ```python + await crawler.arun( + bypass_cache=False, # Use cache when possible + word_count_threshold=10, # Filter out noise + process_iframes=False # Skip iframes if not needed + ) + ``` + +2. **Reliable Scraping** + ```python + await crawler.arun( + magic=True, # Enable anti-detection + delay_before_return_html=1.0, # Wait for dynamic content + page_timeout=60000 # Longer timeout for slow pages + ) + ``` + +3. **Clean Content** + ```python + await crawler.arun( + remove_overlay_elements=True, # Remove popups + excluded_tags=['nav', 'aside'],# Remove unnecessary elements + keep_data_attributes=False # Remove data attributes + ) + ``` \ No newline at end of file diff --git a/docs/md_v2/api/async-webcrawler.md b/docs/md_v2/api/async-webcrawler.md new file mode 100644 index 00000000..25164f6c --- /dev/null +++ b/docs/md_v2/api/async-webcrawler.md @@ -0,0 +1,320 @@ +# AsyncWebCrawler + +The `AsyncWebCrawler` class is the main interface for web crawling operations. It provides asynchronous web crawling capabilities with extensive configuration options. + +## Constructor + +```python +AsyncWebCrawler( + # Browser Settings + browser_type: str = "chromium", # Options: "chromium", "firefox", "webkit" + headless: bool = True, # Run browser in headless mode + verbose: bool = False, # Enable verbose logging + + # Cache Settings + always_by_pass_cache: bool = False, # Always bypass cache + base_directory: str = str(Path.home()), # Base directory for cache + + # Network Settings + proxy: str = None, # Simple proxy URL + proxy_config: Dict = None, # Advanced proxy configuration + + # Browser Behavior + sleep_on_close: bool = False, # Wait before closing browser + + # Custom Settings + user_agent: str = None, # Custom user agent + headers: Dict[str, str] = {}, # Custom HTTP headers + js_code: Union[str, List[str]] = None, # Default JavaScript to execute +) +``` + +### Parameters in Detail + +#### Browser Settings + +- **browser_type** (str, optional) + - Default: `"chromium"` + - Options: `"chromium"`, `"firefox"`, `"webkit"` + - Controls which browser engine to use + ```python + # Example: Using Firefox + crawler = AsyncWebCrawler(browser_type="firefox") + ``` + +- **headless** (bool, optional) + - Default: `True` + - When `True`, browser runs without GUI + - Set to `False` for debugging + ```python + # Visible browser for debugging + crawler = AsyncWebCrawler(headless=False) + ``` + +- **verbose** (bool, optional) + - Default: `False` + - Enables detailed logging + ```python + # Enable detailed logging + crawler = AsyncWebCrawler(verbose=True) + ``` + +#### Cache Settings + +- **always_by_pass_cache** (bool, optional) + - Default: `False` + - When `True`, always fetches fresh content + ```python + # Always fetch fresh content + crawler = AsyncWebCrawler(always_by_pass_cache=True) + ``` + +- **base_directory** (str, optional) + - Default: User's home directory + - Base path for cache storage + ```python + # Custom cache directory + crawler = AsyncWebCrawler(base_directory="/path/to/cache") + ``` + +#### Network Settings + +- **proxy** (str, optional) + - Simple proxy URL + ```python + # Using simple proxy + crawler = AsyncWebCrawler(proxy="http://proxy.example.com:8080") + ``` + +- **proxy_config** (Dict, optional) + - Advanced proxy configuration with authentication + ```python + # Advanced proxy with auth + crawler = AsyncWebCrawler(proxy_config={ + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" + }) + ``` + +#### Browser Behavior + +- **sleep_on_close** (bool, optional) + - Default: `False` + - Adds delay before closing browser + ```python + # Wait before closing + crawler = AsyncWebCrawler(sleep_on_close=True) + ``` + +#### Custom Settings + +- **user_agent** (str, optional) + - Custom user agent string + ```python + # Custom user agent + crawler = AsyncWebCrawler( + user_agent="Mozilla/5.0 (Custom Agent) Chrome/90.0" + ) + ``` + +- **headers** (Dict[str, str], optional) + - Custom HTTP headers + ```python + # Custom headers + crawler = AsyncWebCrawler( + headers={ + "Accept-Language": "en-US", + "Custom-Header": "Value" + } + ) + ``` + +- **js_code** (Union[str, List[str]], optional) + - Default JavaScript to execute on each page + ```python + # Default JavaScript + crawler = AsyncWebCrawler( + js_code=[ + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more').click();" + ] + ) + ``` + +## Methods + +### arun() + +The primary method for crawling web pages. + +```python +async def arun( + # Required + url: str, # URL to crawl + + # Content Selection + css_selector: str = None, # CSS selector for content + word_count_threshold: int = 10, # Minimum words per block + + # Cache Control + bypass_cache: bool = False, # Bypass cache for this request + + # Session Management + session_id: str = None, # Session identifier + + # Screenshot Options + screenshot: bool = False, # Take screenshot + screenshot_wait_for: float = None, # Wait before screenshot + + # Content Processing + process_iframes: bool = False, # Process iframe content + remove_overlay_elements: bool = False, # Remove popups/modals + + # Anti-Bot Settings + simulate_user: bool = False, # Simulate human behavior + override_navigator: bool = False, # Override navigator properties + magic: bool = False, # Enable all anti-detection + + # Content Filtering + excluded_tags: List[str] = None, # HTML tags to exclude + exclude_external_links: bool = False, # Remove external links + exclude_social_media_links: bool = False, # Remove social media links + + # JavaScript Handling + js_code: Union[str, List[str]] = None, # JavaScript to execute + wait_for: str = None, # Wait condition + + # Page Loading + page_timeout: int = 60000, # Page load timeout (ms) + delay_before_return_html: float = None, # Wait before return + + # Extraction + extraction_strategy: ExtractionStrategy = None # Extraction strategy +) -> CrawlResult: +``` + +### Usage Examples + +#### Basic Crawling +```python +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") +``` + +#### Advanced Crawling +```python +async with AsyncWebCrawler( + browser_type="firefox", + verbose=True, + headers={"Custom-Header": "Value"} +) as crawler: + result = await crawler.arun( + url="https://example.com", + css_selector=".main-content", + word_count_threshold=20, + process_iframes=True, + magic=True, + wait_for="css:.dynamic-content", + screenshot=True + ) +``` + +#### Session Management +```python +async with AsyncWebCrawler() as crawler: + # First request + result1 = await crawler.arun( + url="https://example.com/login", + session_id="my_session" + ) + + # Subsequent request using same session + result2 = await crawler.arun( + url="https://example.com/protected", + session_id="my_session" + ) +``` + +## Context Manager + +AsyncWebCrawler implements the async context manager protocol: + +```python +async def __aenter__(self) -> 'AsyncWebCrawler': + # Initialize browser and resources + return self + +async def __aexit__(self, *args): + # Cleanup resources + pass +``` + +Always use AsyncWebCrawler with async context manager: +```python +async with AsyncWebCrawler() as crawler: + # Your crawling code here + pass +``` + +## Best Practices + +1. **Resource Management** +```python +# Always use context manager +async with AsyncWebCrawler() as crawler: + # Crawler will be properly cleaned up + pass +``` + +2. **Error Handling** +```python +try: + async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + if not result.success: + print(f"Crawl failed: {result.error_message}") +except Exception as e: + print(f"Error: {str(e)}") +``` + +3. **Performance Optimization** +```python +# Enable caching for better performance +crawler = AsyncWebCrawler( + always_by_pass_cache=False, + verbose=True +) +``` + +4. **Anti-Detection** +```python +# Maximum stealth +crawler = AsyncWebCrawler( + headless=True, + user_agent="Mozilla/5.0...", + headers={"Accept-Language": "en-US"} +) +result = await crawler.arun( + url="https://example.com", + magic=True, + simulate_user=True +) +``` + +## Note on Browser Types + +Each browser type has its characteristics: + +- **chromium**: Best overall compatibility +- **firefox**: Good for specific use cases +- **webkit**: Lighter weight, good for basic crawling + +Choose based on your specific needs: +```python +# High compatibility +crawler = AsyncWebCrawler(browser_type="chromium") + +# Memory efficient +crawler = AsyncWebCrawler(browser_type="webkit") +``` \ No newline at end of file diff --git a/docs/md_v2/api/crawl-result.md b/docs/md_v2/api/crawl-result.md new file mode 100644 index 00000000..06998af3 --- /dev/null +++ b/docs/md_v2/api/crawl-result.md @@ -0,0 +1,301 @@ +# CrawlResult + +The `CrawlResult` class represents the result of a web crawling operation. It provides access to various forms of extracted content and metadata from the crawled webpage. + +## Class Definition + +```python +class CrawlResult(BaseModel): + """Result of a web crawling operation.""" + + # Basic Information + url: str # Crawled URL + success: bool # Whether crawl succeeded + status_code: Optional[int] = None # HTTP status code + error_message: Optional[str] = None # Error message if failed + + # Content + html: str # Raw HTML content + cleaned_html: Optional[str] = None # Cleaned HTML + fit_html: Optional[str] = None # Most relevant HTML content + markdown: Optional[str] = None # HTML converted to markdown + fit_markdown: Optional[str] = None # Most relevant markdown content + + # Extracted Data + extracted_content: Optional[str] = None # Content from extraction strategy + media: Dict[str, List[Dict]] = {} # Extracted media information + links: Dict[str, List[Dict]] = {} # Extracted links + metadata: Optional[dict] = None # Page metadata + + # Additional Data + screenshot: Optional[str] = None # Base64 encoded screenshot + session_id: Optional[str] = None # Session identifier + response_headers: Optional[dict] = None # HTTP response headers +``` + +## Properties and Their Data Structures + +### Basic Information + +```python +# Access basic information +result = await crawler.arun(url="https://example.com") + +print(result.url) # "https://example.com" +print(result.success) # True/False +print(result.status_code) # 200, 404, etc. +print(result.error_message) # Error details if failed +``` + +### Content Properties + +#### HTML Content +```python +# Raw HTML +html_content = result.html + +# Cleaned HTML (removed ads, popups, etc.) +clean_content = result.cleaned_html + +# Most relevant HTML content +main_content = result.fit_html +``` + +#### Markdown Content +```python +# Full markdown version +markdown_content = result.markdown + +# Most relevant markdown content +main_content = result.fit_markdown +``` + +### Media Content + +The media dictionary contains organized media elements: + +```python +# Structure +media = { + "images": [ + { + "src": str, # Image URL + "alt": str, # Alt text + "desc": str, # Contextual description + "score": float, # Relevance score (0-10) + "type": str, # "image" + "width": int, # Image width (if available) + "height": int, # Image height (if available) + "context": str, # Surrounding text + "lazy": bool # Whether image was lazy-loaded + } + ], + "videos": [ + { + "src": str, # Video URL + "type": str, # "video" + "title": str, # Video title + "poster": str, # Thumbnail URL + "duration": str, # Video duration + "description": str # Video description + } + ], + "audios": [ + { + "src": str, # Audio URL + "type": str, # "audio" + "title": str, # Audio title + "duration": str, # Audio duration + "description": str # Audio description + } + ] +} + +# Example usage +for image in result.media["images"]: + if image["score"] > 5: # High-relevance images + print(f"High-quality image: {image['src']}") + print(f"Context: {image['context']}") +``` + +### Link Analysis + +The links dictionary organizes discovered links: + +```python +# Structure +links = { + "internal": [ + { + "href": str, # URL + "text": str, # Link text + "title": str, # Title attribute + "type": str, # Link type (nav, content, etc.) + "context": str, # Surrounding text + "score": float # Relevance score + } + ], + "external": [ + { + "href": str, # External URL + "text": str, # Link text + "title": str, # Title attribute + "domain": str, # Domain name + "type": str, # Link type + "context": str # Surrounding text + } + ] +} + +# Example usage +for link in result.links["internal"]: + print(f"Internal link: {link['href']}") + print(f"Context: {link['context']}") +``` + +### Metadata + +The metadata dictionary contains page information: + +```python +# Structure +metadata = { + "title": str, # Page title + "description": str, # Meta description + "keywords": List[str], # Meta keywords + "author": str, # Author information + "published_date": str, # Publication date + "modified_date": str, # Last modified date + "language": str, # Page language + "canonical_url": str, # Canonical URL + "og_data": Dict, # Open Graph data + "twitter_data": Dict # Twitter card data +} + +# Example usage +if result.metadata: + print(f"Title: {result.metadata['title']}") + print(f"Author: {result.metadata.get('author', 'Unknown')}") +``` + +### Extracted Content + +Content from extraction strategies: + +```python +# For LLM or CSS extraction strategies +if result.extracted_content: + structured_data = json.loads(result.extracted_content) + print(structured_data) +``` + +### Screenshot + +Base64 encoded screenshot: + +```python +# Save screenshot if available +if result.screenshot: + import base64 + + # Decode and save + with open("screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) +``` + +## Usage Examples + +### Basic Content Access +```python +async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + + if result.success: + # Get clean content + print(result.fit_markdown) + + # Process images + for image in result.media["images"]: + if image["score"] > 7: + print(f"High-quality image: {image['src']}") +``` + +### Complete Data Processing +```python +async def process_webpage(url: str) -> Dict: + async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url=url) + + if not result.success: + raise Exception(f"Crawl failed: {result.error_message}") + + return { + "content": result.fit_markdown, + "images": [ + img for img in result.media["images"] + if img["score"] > 5 + ], + "internal_links": [ + link["href"] for link in result.links["internal"] + ], + "metadata": result.metadata, + "status": result.status_code + } +``` + +### Error Handling +```python +async def safe_crawl(url: str) -> Dict: + async with AsyncWebCrawler() as crawler: + try: + result = await crawler.arun(url=url) + + if not result.success: + return { + "success": False, + "error": result.error_message, + "status": result.status_code + } + + return { + "success": True, + "content": result.fit_markdown, + "status": result.status_code + } + + except Exception as e: + return { + "success": False, + "error": str(e), + "status": None + } +``` + +## Best Practices + +1. **Always Check Success** +```python +if not result.success: + print(f"Error: {result.error_message}") + return +``` + +2. **Use fit_markdown for Articles** +```python +# Better for article content +content = result.fit_markdown if result.fit_markdown else result.markdown +``` + +3. **Filter Media by Score** +```python +relevant_images = [ + img for img in result.media["images"] + if img["score"] > 5 +] +``` + +4. **Handle Missing Data** +```python +metadata = result.metadata or {} +title = metadata.get('title', 'Unknown Title') +``` \ No newline at end of file diff --git a/docs/md_v2/api/strategies.md b/docs/md_v2/api/strategies.md new file mode 100644 index 00000000..f0f8f57c --- /dev/null +++ b/docs/md_v2/api/strategies.md @@ -0,0 +1,255 @@ +# Extraction & Chunking Strategies API + +This documentation covers the API reference for extraction and chunking strategies in Crawl4AI. + +## Extraction Strategies + +All extraction strategies inherit from the base `ExtractionStrategy` class and implement two key methods: +- `extract(url: str, html: str) -> List[Dict[str, Any]]` +- `run(url: str, sections: List[str]) -> List[Dict[str, Any]]` + +### LLMExtractionStrategy + +Used for extracting structured data using Language Models. + +```python +LLMExtractionStrategy( + # Required Parameters + provider: str = DEFAULT_PROVIDER, # LLM provider (e.g., "ollama/llama2") + api_token: Optional[str] = None, # API token + + # Extraction Configuration + instruction: str = None, # Custom extraction instruction + schema: Dict = None, # Pydantic model schema for structured data + extraction_type: str = "block", # "block" or "schema" + + # Chunking Parameters + chunk_token_threshold: int = 4000, # Maximum tokens per chunk + overlap_rate: float = 0.1, # Overlap between chunks + word_token_rate: float = 0.75, # Word to token conversion rate + apply_chunking: bool = True, # Enable/disable chunking + + # API Configuration + base_url: str = None, # Base URL for API + extra_args: Dict = {}, # Additional provider arguments + verbose: bool = False # Enable verbose logging +) +``` + +### CosineStrategy + +Used for content similarity-based extraction and clustering. + +```python +CosineStrategy( + # Content Filtering + semantic_filter: str = None, # Topic/keyword filter + word_count_threshold: int = 10, # Minimum words per cluster + sim_threshold: float = 0.3, # Similarity threshold + + # Clustering Parameters + max_dist: float = 0.2, # Maximum cluster distance + linkage_method: str = 'ward', # Clustering method + top_k: int = 3, # Top clusters to return + + # Model Configuration + model_name: str = 'sentence-transformers/all-MiniLM-L6-v2', # Embedding model + + verbose: bool = False # Enable verbose logging +) +``` + +### JsonCssExtractionStrategy + +Used for CSS selector-based structured data extraction. + +```python +JsonCssExtractionStrategy( + schema: Dict[str, Any], # Extraction schema + verbose: bool = False # Enable verbose logging +) + +# Schema Structure +schema = { + "name": str, # Schema name + "baseSelector": str, # Base CSS selector + "fields": [ # List of fields to extract + { + "name": str, # Field name + "selector": str, # CSS selector + "type": str, # Field type: "text", "attribute", "html", "regex" + "attribute": str, # For type="attribute" + "pattern": str, # For type="regex" + "transform": str, # Optional: "lowercase", "uppercase", "strip" + "default": Any # Default value if extraction fails + } + ] +} +``` + +## Chunking Strategies + +All chunking strategies inherit from `ChunkingStrategy` and implement the `chunk(text: str) -> list` method. + +### RegexChunking + +Splits text based on regex patterns. + +```python +RegexChunking( + patterns: List[str] = None # Regex patterns for splitting + # Default: [r'\n\n'] +) +``` + +### SlidingWindowChunking + +Creates overlapping chunks with a sliding window approach. + +```python +SlidingWindowChunking( + window_size: int = 100, # Window size in words + step: int = 50 # Step size between windows +) +``` + +### OverlappingWindowChunking + +Creates chunks with specified overlap. + +```python +OverlappingWindowChunking( + window_size: int = 1000, # Chunk size in words + overlap: int = 100 # Overlap size in words +) +``` + +## Usage Examples + +### LLM Extraction + +```python +from pydantic import BaseModel +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +# Define schema +class Article(BaseModel): + title: str + content: str + author: str + +# Create strategy +strategy = LLMExtractionStrategy( + provider="ollama/llama2", + schema=Article.schema(), + instruction="Extract article details" +) + +# Use with crawler +result = await crawler.arun( + url="https://example.com/article", + extraction_strategy=strategy +) + +# Access extracted data +data = json.loads(result.extracted_content) +``` + +### CSS Extraction + +```python +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +# Define schema +schema = { + "name": "Product List", + "baseSelector": ".product-card", + "fields": [ + { + "name": "title", + "selector": "h2.title", + "type": "text" + }, + { + "name": "price", + "selector": ".price", + "type": "text", + "transform": "strip" + }, + { + "name": "image", + "selector": "img", + "type": "attribute", + "attribute": "src" + } + ] +} + +# Create and use strategy +strategy = JsonCssExtractionStrategy(schema) +result = await crawler.arun( + url="https://example.com/products", + extraction_strategy=strategy +) +``` + +### Content Chunking + +```python +from crawl4ai.chunking_strategy import OverlappingWindowChunking + +# Create chunking strategy +chunker = OverlappingWindowChunking( + window_size=500, # 500 words per chunk + overlap=50 # 50 words overlap +) + +# Use with extraction strategy +strategy = LLMExtractionStrategy( + provider="ollama/llama2", + chunking_strategy=chunker +) + +result = await crawler.arun( + url="https://example.com/long-article", + extraction_strategy=strategy +) +``` + +## Best Practices + +1. **Choose the Right Strategy** + - Use `LLMExtractionStrategy` for complex, unstructured content + - Use `JsonCssExtractionStrategy` for well-structured HTML + - Use `CosineStrategy` for content similarity and clustering + +2. **Optimize Chunking** + ```python + # For long documents + strategy = LLMExtractionStrategy( + chunk_token_threshold=2000, # Smaller chunks + overlap_rate=0.1 # 10% overlap + ) + ``` + +3. **Handle Errors** + ```python + try: + result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy + ) + if result.success: + content = json.loads(result.extracted_content) + except Exception as e: + print(f"Extraction failed: {e}") + ``` + +4. **Monitor Performance** + ```python + strategy = CosineStrategy( + verbose=True, # Enable logging + word_count_threshold=20, # Filter short content + top_k=5 # Limit results + ) + ``` \ No newline at end of file diff --git a/docs/md_v2/assets/DankMono-Bold.woff2 b/docs/md_v2/assets/DankMono-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3072fd8567c7f38769e8fa161b92417f2630f902 GIT binary patch literal 33480 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 MK{-$E|6vLM0LSWEPXGV_ literal 0 HcmV?d00001 diff --git a/docs/md_v2/assets/DankMono-Italic.woff2 b/docs/md_v2/assets/DankMono-Italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1d01ea6d73be14be9ac9014865475ee73c597fb5 GIT binary patch literal 32468 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 j0=7(^{K1R0G5BwXVt^ZBLDyZBYynI literal 0 HcmV?d00001 diff --git a/docs/md_v2/assets/DankMono-Regular.woff2 b/docs/md_v2/assets/DankMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..99c1425ce4f3f324c29c0d4ac465bf73ffa363e9 GIT binary patch literal 30528 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

    31>i z1}HVj-9GVQZ<*GWn_AUH1G;v(dpHd&^NbhZ!dtry4&u{$TvYINum;gjPIa zOt8fi=CVH+s@N@dpFP0>=lsupU50lVEjftn;Kv|Q8^Fm|zUMA%EjVU>c*K^I`ZN)0 z-X}9o&7+2LgFu=6!DfK|+!0%t9f(J9mdee3p)+sBr5UyQupK9w9ff*LXKtkX+*yta zeSx|=do~p)i1}zi)QPl+J;iK66`<9O^yLU<(!*@`z(D6MMeOu#J2xH{)V9oEa2V}a z7P9r&ChPAg@1EN2Bg027e((>Zy?joae(Skj|Y&GkSV;M4gv0TOMAZ$e{8 zv6j=sd+f;ZG8OU7V##~;S|2@!r(_b@V-coXm6Jq5smw;j;f2QC@%mNZuq)=%LoCSj z2h=iVeU_E-UNI8r{A-{AD7+3(&VR$mQT;OI?a3YL2w~)F^(7`nMw*C&(dA`E+v*FV&wL zJzZ2(z*{~o_2)X>^?H+}UN89m$lmC^wD=eH^X19sKxbVm%a=DG7V+vD&L%*_nCp$j zSqFaj*w-d`1Mh&Dv&^_!neScEd%?pK%0^V*5OBv0mje@{b-FqkhyJRKGLU*5nX2P> zD-%&^R?*5G|7{EmLPvGVf-BYSxAwFcUK%Hj$W5 z;zt+Kti5I=(3wBXF*F^TI*}D$T*V86vael(eBnJ*6F>u z#9`hi;lasIc!+)+k%Hx-9zhZ`Fiuz_L z9Ya&T6zgugSxZC>ne=#1G+g`U)LF=cw&||>WBSc|Gk>^@Xvd{gOoR7iWn&bf_3(VE z`yM>I7BH_f>PU0}M;38}O(l22R|8sZEz-sVwzSxW@RK;B5B+f6o3VF?AJQ^h&)*<{rbn_t-Ybs^lIk^m< zG~QTRB0DfkEl8NNXJ=g*QKjaOxk~6Fc-SDGp}JEi?o86*yAc|C;A^35>26OZ5xC|R zH?$%rs!y3;0@nNvNLhLKImNEfor7Wu>ubi&%#hfLjQH#VUg>?2Szl*SSs`OPYt^sA z(lcx>q}@*EftTt}oeq)5Fg`+)wazmpd9wqe8Lb-|Oac9rGfz0%K{aB3?b#TUdj8>r zE$U%FuTkoY5c(ta6i{G3yZi6kbFYx1QwmEI?FJ)!%rMqdK8w#r=+kCVL~Vs3gIvNx zbBBWhHwoSR+$Dv^12i)`96KmrBS$dv^_xBgXlRs+o*ABc09<5P%=XYP|$y%o6zJ zlQ3^kQZL_zw_%E);PQs`K|jEGePPXl{$SOq<@+2~#F7H2^-w50GDY^x7vpI#P*gyp3{@OtF+&v>gNsombA{(-X4cd(si!EuQEc3A z6onf^OEXBBxj{Ap0|gbssUN8}AU`&~_P3;OboOt*R%S^5?z2V8dqCFBySNXD*lb3- zoRZAjT{`#dl`VDA7`%HPyB*`6`6p=C*_~(`mg6YU(AmV0B5y%2{COaO4Ft;n1|~(O zTY5xF?O5X>s!<c%yV~6uwUsIP;or(kq`q`Jrem^r*&pVn&FM(!-}H9WHl=9irR(VIU6+@o~X6m zR^$z@!lRo;;lO`+2Arl+hw(Bf*=XN?Cq>rL1}eb0*96rMVvU9}gTr|cD|<*TXPtjn z8=1WnC^E5JMo|-NQV|iUnE_Ea`z5!iWD+ZD!EZnxQH^BrqE!X{;?xWwlv_-~ zt`}}z-at_wBbgG1ffr9aUV*j%O-#iRN(}x8`7=)@&vIAfxw$_JJml?!2QY4D~sc%2tvcAD5OL5#vdH{+~~^TSkSAadmX8^Qe-0 ze{oxx(>yVQKe=$GblQm9JO~|W7eSdUkn0w8C}nuxq~J1xxx3#6wSUFluIo9dTx})$ zyZUUnt`4CVBfPux1rY$pL#BAjo>m#Hwqp{c4mPnSK%-R6JvtLT$);2+2I<8NGo_eq zK0L*idMd4|s6LBeD!T|;xn7uEb~)Oe+bE{Id0rfF|4DrXABeCX>HmKz0G}VbiEsN3 z3nmvYIz&xK-kY1b*;}w>TeY|L!9Lr%O%cKjOPX$&mhHGgYPw-s zw&QxjkxILsZ>*3?8|%DLtwGG=07RHj#vvF%F`OVN03gDIq8XOs0YsQk#tHuvq4k)$ zK9|};Ou4F?w(CoXDOYvVmaY#mb4pWGV>4C{AwgaaEhk2VoQ^X_hB>kZIZW@eUsk#>=`5<20|< zo9%AD4C6H4=VSQUF1ur6oaSZS_Lcc<4BF_l?FaL#`+3J?etw+S{V=~9&s>}N-GA{a!-@9!79towRJS=CM3^`&y9TB|pjt)eKax@omL-Cn;atNQLAzmRYG!|~R_h6QU` z*P`L^jVxKla#y^{)sHvHl+({T_W*+pGtwC2O)}LCv(2;2Wv^gmtJ4TBaS=etP3u?=B7KI_G{05(JNm4JAd@=N832> z?S^rhmv!4mY3Uo2O{Fi!KbT5qU9!5|Ml&fd&1IpF2 zxd-KRZz}+Zh2aEA(F_Y9h7%-3Gc1Rpy!R<6r#oh%=+LD@r!L)k^y<^Er+=h(9f@K} z_=y`Jc^eLr7yy!->A+STkthI2tC zJ6~K4$zA$oUJM_}xtL|0mNj+R?Z*$QD#Wau?U$>j<}q+JXM>cURgmI75FZmJZ6R>5 zl{@ajSZ0n9xbPC@E)XT>DkWv+7zy)*=9)rMO9(_f0w)8N`=)KowN_`l1tzTaxTT^s zasiILrDJ?hc8RMo7VS-CnlE{oModZXK-ys-RQCH3QqqAYJxUqJk(U%>de(16HR(yu zvP447_(Tp>evWG-C9yuvaT;jPWz5qh+mG`+5+zink8!_>9D>Nel$_%jI4|`cG*5|C z$w}dAi9Bq3xTkEj>(pg~jW(fNOcn}=md(o&%Sf%V#(X=T0~XnvS>~X_LL3_q?(BdF z=QBsT(s3@(iCO>jaOW5#ri;6{B;=u!C~SF z^e~_&0X+}sRX}e8`Vi3PfW83q9iX2A`Zb{M0sRKhpFsZr{SQoIfN2RZ?E%vjFx>#t z2QY&GGZ-)cnB4>BDP>hRZ5NuDa@+N#hM01xt>?ncbl%wW8Kz}BuIHP~7OU<0(xXY2 zVVIWfxZEDE&u^HP?cCArQN(L>r!uoephcGm#*Ij1$W^RT{axK|OwY z$FSvZBxeJ&upV(-3Z_S2Q<+{PpN*JP`Rq*$v*v6>XH7zQ9VuhN67mh>vCW+Goix&@ zKF%GhqAn~ov*bH>bBvM4qoN=R$->A&6}4L;ZkFKEx=B23Dtf0C91Bq% zbs|jy@a9-km91=bN!r@M9)#aGNr@5m3cy~4fG}BlWS@DF8E`FKFLQS3*GQCqG<$51gB(LW{^2VNO1~;i$bJqI(DZn4UEn`_@L{((E?kv(u+kXWTqS z^O8TYJ+Ei5qulf6gKCkA@2h9AvQ!Zx`Q0r6cOF4cNcp8ee^IXmnVV#b0Sani38X>LF9UA_H%Ed2b?&S7VA9=2@&!lT9wS)ouq} z=N5Nr_3(7o=e=s0*UYuZ3ah>2BVTBj$FQedki>T0}QmSg|8kz=%M#d(l&OXX5YP3A_sw;hl5;ZU1 z#7UASOP(T4+QOGdZ>crJI#v|NOm=_(z&t<*APNzW&>&K<+tR3YsYdQSL!M&fq>!8u zTIE|*6^novGn!ad&LNKW(OA{6JGw?@h*Jw|`Fc-QBrP62Ozst}fbNp}&m>?czzR4F zI0-lpxC*!pcnBB=ya9L*@Co26zz^Uz@DHQ_(i|Xd0n!<+lg_*9wui>O;XR-D$`5|? zPyU6&0_oB;r6_A>>}iGbrX!*M*9pKeJ|~QeRjKK#bEv~>AG7&06L-lb8)tX{5r0OHe(x;b?-1rkZOWImPsx;`1Ojg`ZGS)YEg8 z6BiPb1z9A5a(YfU^CU7;poOBvPtO?wt|pmo4w*=*>GQU@+`VRoVDrU@rCkKc4&UKE z@r_?%|KyHX94=YWJCn^6V4e_B;%Oo&m5N6h@)R>NX|h~vu>0NLT#g29u$|RWXI*u- z_w>J_I68U*-t&pC{NT6PKe=NXv@iYu?wHGiui55_ppeqb`LEuMjvui@NCg_LF4+IF z8;mBi#oF6$(>Suj;dHq@Uf2jFw! zjDyD!x4ME=2VBgoCWjwICqsd9CQG&IWwFI>2c2}$O%F4mYU~Ond%`4=WY9zan;chr z+^4H9TEETz>lzwO7we-d<3EQR8OD;Hszy#5n)uT7()F?H73($Ylh&uPyL+i>Sm$dL zY9^sKt*HX5)tG9!1sW`~!WvE1=a*lX!(`~;3aMopjn*{v7FuMvMr$?O;2&BhiE}s+ z@im|FvI*ykD9X1syxaXf4-0y z-Hn;A89&Wry(XJts%fU*#A)`~=J4Y$K%gMOq%G{jf4&74T0|y9s3lg}VW(a8*lQoF z{l*+{&>@E%anv!#op91Ar=9Vq$6zvBtTwyD>2iC#P0cN>eeSmH9i3g>qnB6L6I=am zE+@SHa5R}ORqL%zZ!rBgJzK0chm*5Qx1&UDs2z2nPLxBrlpi_wXfg7N6*peq`6SFg zWvVoV7MCJzi6xhkpv&}#<_$=7tj|vM;Za^3Q?EgjHKvlC4-ZY*m_|@~F`6V+enkQPq3rYFnrl}%_Ah`8v zB9(X!uyxcyY_N?u^eI4wcxG~#prr8#uY@PRma>47xVQ=*Tw?b9C+2{3hyWu1(D8!< zAi-AAFaxw<1prZicpbJaNBm^mAmZ^YQ)Pc9fct?+2acR@QeMSl*-qfXl^b^+JbCdZ zBIc6|PyIx`Ce>kl>=+&)QWUx9o_~0pcnK7g@y=O8OD9p1WGPamNvF41hD=$qV2~lM~ZZGqbj5T(v?5rC#(IN2XG_x78~1 zbAI&*KiKFOzgq69uYBtpH~dI{a?e_pqckgE^gZCG8c&6KFKe>RF143cR=e+kC!Tuf zkrnaud`@`NTVD4D0C>*qv1eZ8PgmRG8do+JzI6@(vV9)iZxgM$RUKMk9j!|nfNf$A zOx9MiB?LHIZohL0RP7eaU;P)}*UC3m!U7T}Z={TFv#J|J53t zENlIALO%q`#Wl(|9`+An#6Vo zT!>W7WcJ8fxwm2_$iLtT-rcfj8Zb>6M>D}lXXgyft{IzSb7t~fnH$Qjk_i$IiRh;X z11?;@%aeKcAUuJ0H+n*YXX}*X-j~weY{I?&gX8Gq|Gyq>4`Fv2_3)YhO@seo8vflZ zYT9pv#&c}pNgtg1U@zx(FIEOmo_T)_@cyhX2k_n(@WEuEeDmegFHgR=R;AqY_Al`E zY*Ihnqs&ETuTkgg$6P=MBq^fk6sbxo1yFi96;}@E+G-+R7S+mD*AD2e1_5nowC(NA z3INW&*lP>-0O(O#s&ekH-V1fs4g%VHJQdihLXvvY@DQlKknh`Cob)D@13 z{*EIoqyI=nqcpkLhT)1pCZ6}@fEFfg(U3A$IayN{-%@UDvO<72))C1A!LJNEN=<$<5 zX&_?Witzvyk4qy-0RnpTs8-rc3ZpL20iQH_9twx7pv0XCLeFow=cW&Xl zNW0>?8*aMgHcNcBB1$STUD`C&5LZIsaZOY|Gw!(;pEIt=QhCR_-t)e%Ydqijt|sw) z?L7v0L#i}A^;V#dfI>yi(^o(J4KUE4!(evsUbhR%y&LjPqsB)8@b;!Be({z=XWr`a z-Shw79z3<<08DG#AizBNalx*Zx}$WA{#@EjkI^MrNP08bu$1UTEgWs>)T3o#>i8K| zE5BMUaF%-7c*72J;t8?Jj%-p>URhYIN*He%BwVo4WrT1Rk)hN!T-mfni^*iY6{g+# z`?`F=YWs(_=RaFg5)~V?eaHl88S8nE{;Lx+MYS_JD;2patN~B7?V)r!$Qg|_DhK? z_H}kHIhwDi(^BA}_;^ikl@B$u&OX!JBy{xouRpQa6yw;>O5g71r<=jLJd)^j>QGrpsBnp6zm38Ex~b5o>eP{Klp~@T50!y zpSeTCWxtBG9$`N=gX)!Tr9%F`m@K85QpHRoONI_)I;YdzGO{S=-VjTPiYz$l?fxTZ zLy_uhnb(Lei$1YadEh*AIiA*z4LIOB4(^2KYKWsfIW?Dy_b^Cf2LLU8nm8d$5vRCW&KVi$)dV5_5C+Fmpy$u^Z&n%gWeN;o-*10 zOQr1R5Bn~Q&Hd%!$Gkx4e*~Rcn$BH!3i}^s87i&+p(c)IrT*maZ!len6SL^ss?(zH zbHD?+YBidflpR|quOqRaM9@I0WkIhAugKCZQJ|UYy2Gt*q!()Rk0mP9DOInEABjjt zjlr32UNsdlFHI&5+|R&Iw0u=6Xp36)5!o+EkrMTc@|m7y*kV?u8C@cfpMYmb29_|( z=Ac66vo1UBdEe3`m}8zmQfhNoN1saL(3YUMc$o{7Y0ssI2^^##4 literal 0 HcmV?d00001 diff --git a/docs/md_v2/assets/Monaco.woff b/docs/md_v2/assets/Monaco.woff new file mode 100644 index 0000000000000000000000000000000000000000..e468c424971227557730c31799863d7cdf9edf7e GIT binary patch literal 20096 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 Negpu1(g`jA006dq^ArF8 literal 0 HcmV?d00001 diff --git a/docs/md_v2/assets/dmvendor.css b/docs/md_v2/assets/dmvendor.css new file mode 100644 index 00000000..0f72703d --- /dev/null +++ b/docs/md_v2/assets/dmvendor.css @@ -0,0 +1,127 @@ +/*! + * @preserve + * Dank Mono (v1.000) + * This font is subject to its EULA. https://dank.sh + * Β© 2018–2020 Phil PlΓΌckthun. All Rights Reserved. + */ + +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUAAAsAAAAAB0AAAAS1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgTGyAcKgZgADQBNgIkAxgEBgWBdAcgG3wGEZWcfyE+EtMN9vKA5Bc0yUoz5//3zdWt7i7MMhk89Jh5YNQyuHYEC0QOBNNZU+uBFf/fX3vNFsIYcE02IK5Pv98Efw+vn0T3zFEboYh4FE8cSTQSMomQ8Ug0TyqZBNtCn6ltmWJarrMX4mswKBIYY7H7bxbmnSgqLFpx8yIZ/cA9xgIYCyNgLJKMxbKRoIjYCNlyHxrXrZvx+abPHxHcKkZSgdCXUgHlHqe3oz//PyeO3o9FFGNhscnzV2zcsf9YxvXRZ1+7WVpaVOU31l5RbOUBkheyMMmlLEZKVK6tsKIwZ+2aLRsVZRNls3HL+pRnzcrVfUmk4lcW376y5PCf7C3gZLvzqq35t/M+nUD5ywykaFPZH9GRJYNONVRVTftcgLGWUIRLYJCwSaGn8CfbxwbYj5JJypDaol6kJ31kizO5PqQxs+bUnM4k56Br0Gkht7C9/jE3vSeO0BJupOuTZ4LoOC298SmP8tF9cfSWHk8L6T4zLaXjfPKCbpwNpiXiyMevCxu5WT2DTtfw9clOp6Y504RNeHjUi8YuUazT0k/jTC76zsgym27QcbGUT545YKQHiSNiCTe5Xk8nN9n4YN9gb19Sn6a9FuFRvaI6Ko0/qhi9pcv0xqB5ibQ4a3Jf7+BgXxrZyJ3+ulgijnDj7IHJC8FiKR0/dYNH/UqVVt1FR+JMrtdjiDg0F2uofsDBPCq2p+4WkcQ4/Rc0CYQ8BEJHaTsf6B/qSenV+i0mVZygWm+cwgtQpJC4kChANpOaHAGRNzFs+v1rfGz+9AbNpvl8qi5YSM0pQrJV3ORaf29oWT1DA/2Ila5ZzAIHJrSkNB8rKfm9uOm9AUf9kDPZ6dI0R1rUF8YxRkHGSZlyab65jFykC/2tt8U39C3Zy7vHxdiD3N3scyc7HF63O6211dvU1OhvqE96WIxzBzb42zglGy8bF8z+Hn93V1Knt7OlzRJwO/wO1v8WY2Xde8n+gPhGfPvWRdJJLxOucWcdzTC2mB9UWrFUcwkjRWoEg6dhL7EO5IEOXyC5v98fCKS1t/s73O/Y4+22lNA4D/tSwcTNDd76pqRGf3Nbi8Ud6Pf2M2uo+5MN48JeTt/Qt5feFrrQHyTX3j4epQ1O/Da0a0hUDgWJWx3B2qDxh49Ezg2FiEJfny9UD/swXA8EAi9OzIuI+LCvvSvQ0f1QROTE4lmpAO8ygwEIQwoLtv/g8TMIgQIAMzMAABD2QotJtUXl3ywvRAIQIlGRnmMB8EkROUL6DnCpsewhrM059FDtivfmBOYMyI9OE1B7WOhuBbs2gbRVjgCgUgBIAGQAs8hgClGgQgUw35zPzEgw+x8lEC2E3C71ywOzS65Hb/83RFaeA4Bfaz/+dHJy/dvvzkxNU+hLyj2ADIkxRf54muAPswIYM39VAEN/eyZ1I0OSc9Y7J0auUtmylYIVwgEE4xIZTAkFUAuzXAYFtVwJEhxcGYCrXIFxOZK49EEsm+9TpFiNUjmyZCunehyqtVZbY3NNM7KRV3UL6lFZbUCOfLVsvkm35SmXrUKhlVR75ANKHNWXEbR3JuEdbaX12qwcQKftHPKwMjmKFFKtsdJqwZQBMWaGrQUAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: normal; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/wAAAIrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIRGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzYDQC4D7AZ5j5pUbZm4E4qID3FTshVsW6OPY3WC/+/H2rnvv11ENDEkTQxJq+WNZLJJVAvFm3aTBlvR0kSNfr+2Z6qhKJBM0ol5Y6ja8JLIZEphKPV9HOFkV4XMJnQRxziiAWnTbbybh5rcbQEVaSD4FH+CudcDRKD1sl994/9u8i2QA733nQ5oNDZoniWWYBj3EluDplDkEWft9AMzuudgMIgSdgg3JGVZ6nd2Bv1B6OwinfywmQofJcRH0ykzg9Iyv2VCRnk43/6X4PXUhMnvZ0DScRl4PMKMu/dGkVLCpfSOfGPKU4mFo9Fou1nWW8n6duj0Rd30ilt5v1vpvG4gDg9rRU4mn5ajexpYLbO6SVMBJFn6yMPnuxP+npv9XR6Oexf1IXJq7ndftzNJD8lJ+eLN4tGnQyi82LheWVxa3UxN+/QqTSCykoeAZEqVU/vW9jG4OMD/A8AQVapZPXFvadqHlofodtvXIiw8pNo9iKFmtUk8TuFR5f/AKby9MHb+CBB2lNQMrABYE0KgkgMgQNB3by5Ifm74PoDHdQc8nr2e/PsjvPM4CBgAgnD1R3CR3f/+5wka5ppslUPoBgEJxDCQJwFDoIFwhsAwxiAAE0VnpRGKc8JoieMZA0MrYy0NTVNWe6wa1KnXOAqs47ZKPVasujW1dOXLrh8VHVNNM31hlVp0dVnrDCe6X0Oolp+rqQoHNO6f6u1jEy0DfVb1atQJDyDxv8QBAA==) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAATcAAsAAAAABxwAAASRAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYdvGyAcKgZgADQBNgIkAxgEBgWBdAcgG1gGEVWcQwE+ElO4G3pA8hc0hxm3NGG6/8up/ZE0IXYD5JQJQigjvFYGThf5VABWk9PS4bI3/ONOAHFwCxNLNbXr539uoRZZW+ctNHr4gnzB5NNGgyUgiUYiIVpoeKSemIV0+UJPp2JbqHYhnb3wk1sQkAQghOQtP1J7bkNdbd2Ed62zY/jbl7IRwEZmARslsNEiK5EsWQLPzxKrPgHLtGmDHo/hs7NS+Xn+USVA+usP7yneyn36YVUB3suHHEIy8suHT5i5YPm6rYc++unqkcbGujbHjdmW+gr7CLfIUzhDQQrTuo/XttSemTplzszcNcs1Oz5nh0pMmTh5MRQqbf/oY/vHXP9DXwMOnc+b3Hz+2LnfThD+zwwgPdH0l+igeGDa0qWNYv8HACGOdMgUgIAA05z1MbwnI0g7uUL+FqYIF3LQo5Iv2a/iQ+zRyAbZYCgzxI1xQxXa+fHrj/kaHEPZFrVvewqux7GHn9KcT1rZn20Eb6kijsRPNDgW19O+3SrbkYpj+JrH1/lxtNO4wRg3lBsMsmyo5se5i7plv6viO3ZK0b+ej61sX8G2pFQyH3N9SwqPp/WReDhSFpGXpCqD4FiOzz1xI67ZBFu1PBKOk0k143po33Kdj+FrKNuxom93Kh+L6zcddktLJNPE62GS9LJRTphiemktn1+bjYTi/ymyysn1LXgjzqexaCLksj8sR8mGSRzehAQhUE4EcgaFUOeSv1QH/ccea54exiE4nPZ3pfKMbELUzfwI1WZhz8mhRCwqbcaDKQ0n2pIUZfIYgfhvojG9KWEoNxhlWV+d84KtI5jCNop4FodrmtCIKldv3eZv8C3qmoO9vOdzard57OV6vdtur3Y63VarxWs2lX3Je6kNT/iLbRLZj2y3xhvyBgNlfrffcbFKseu9et3r8p6m4FLUfcbf8Le39qCKahM39hoGxz+VrWyO5vOc8s1kLwlSCdcQ5qWoeyhVfB6lPBr1Kkr1pUtenwM7Q+5gVQP2Us9fKRWpxuw2WcssXttFR5Vdibqj2gTnP2ju5bpmfINv997mKlc/R+PSCM2R48nfE4sSvDWRwo/6UuU4+9ODOxNpvNYT8aSrGdcyVUVRkuuysq5FQxHF92NWdtI2uqgSgAY0CABkQIUqZPnK9dshDSQAgMFBAADACYw0mrBYzvkjzbVQwrRDrST8TBSgcpCYJbwDsHe15AuYOjthYdfuBVNNaip+PYBgECR9cW4PaCDMFbMa7CYVKrtIirIsEFmSQGuXbKRc08HBypbsnw0cCimurtT2sysP5c7/L02UfgEA+K3z8VP5dNPte4OjB/7f19JCEEAEQbNJfDyAvBmbGb7BLxA6PviHt3+eCEHIgTI7TbdTnrMaHXZaI9gMMgEgFfYSgUjpANAJzAoBCToVAQTQKyIAGEclnkKhjJGn6Dx9mTr1OjQ645TTmml9C1pTTTbF7Cs0oXVJ6yhXaG2uIJ+fN131vD7HnNPstBa1JtJa4jxjyFbehNATsv0Eba1Kx01c2g2ZnfCjmpxRp5bWFBNNTqsiwCCr53WbagEAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: normal; + font-style: italic; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJwAAsAAAAAA/gAAAInAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYIOGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzMDIL4Mb4gcpY8lI6bRFpuvq4/OWEUk/8G00RY3bzZ45QTP/4/7fp977/hhqDRrEowk18nij7AjlZhdyoMv5jaxZE2Bj3goRCo0jjQxTYTacALb/XAD3u4CDzHDKgCagaf4WPsskuLqgQ3PZuX/n3qBs2kmff9/N/kG2b0PAne7CZpHlGAY9xJq09hCASUSoGTuSs0f2zl4eIgSJghGISKxbXaRXsuiPBy17rvw/Wa3uNuCe5R7qd2i2WdRPsGis68g+3ZpyKuftUT4Ml8LiPrqLUz7F5d4lxWHEbH1TwVznkJxQVFRUcPp5HpB22zRiZu6iSW3/FDmps8biI7jmrPj9tdyRFd98BQsNBZBEBFl2zjn+X2+8fxv6Xy4zzkaIU9y/93vGj4f6Sv2n94f9TP6mJ9Hjhzx9Fp4eubk6SNHH1qsnnViFoQdT0KAaDJlk7aOyw1hMAChEACF5cmnyrGLc12BiEREu1GP5QhhGG1RP8C4Wq3coCSGCSj319jF//XtoAt9W6KaQI4DVI22ALkMAAIIJP+TmbHrYDBPAH5p8KtXb689cwMKjQJAQH8K7tVQaL38SkDoDwAARJ3ABkOIABGM44iJAtYQEwTDGigUW6CBnayhJWGkZwXhRfafy2pu4jDqwF25ShQpVrUSDJE05ZqfwVAUZ+7L7O+kXdQXR7maswwqDVrA81Xk1w6+d1Rg+3GHm9q4LECxAkXRaxpCbqELSggA) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+0000-007F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+0080-1AFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+1B00-218F; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,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) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+2190-21FF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAUwAAsAAAAAB1AAAATmAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYgiGyAcKgZgADQBNgIkAxgEBgWBdAcgG4sGAI7TpTZgEnN0/bdZ/WbmEV2iJA6zYsqmkhMnIk5BTAcihkUMkpy4sKwZOZW/86+rntyLICrXkQ0QR08+dWrm0gQ092BRC9DqXtSrFmhMLHCpwL2epzzWACpGhKqRXuEpp/dUeAUCAgeEEGVSsaH2AaPBuPtMK1N7Z4K8GeQtgSBv5eRtvBwusJBAXv0Y9B08uGa1Cq8P9LGwZ+MB/BzxAMI3FN8NuvJ/Wyh+GAJKQvxDYjftPnI66b6Uwkdeeru4ocHYGjOaaq4brORYs5kiqzHz43HWWGZoNlQf2H/8SMsd5Y5Fjq9N5ffv2QcdWSrnbSvN237wz3wHOHNnXr9OX1r74QSeH2ZgOI82/g8dsxCCYs+IPs2/AICQIT8I4IAAB1odPoJ/yf3kNfI7t4Mr5p5UvokvJnrtWBYaZvbgd6pOqUvqipGme1xmdViRfcQxYrfgMKuIct9ip1BD5VM/LmsVqEUx6zZVWvExKy6G4kfesCLcgU4ViqilYY8tJ/4on/VBDTt1y82q0EpdZpOrO7ZL6uqUNKyMWanyTVnL6ry441JomBkvyeUqPMNEuqw9Lp9SsJNM474Lh7CChhXZbQ673e7rmpmedsbMdTk7nWpmwTKqHG+Rf2sl+M6PPL69oOp0OjvnYl2zrrkZDVah5S4307BTVE44vpzkw0TU3plFld9ji85rxnvIW/ilSjJJZilGmu6d7VE7hh0jjhF8gJ2Jct9k3L8UryoYt9V9Nz6AZxx2dKEjenZ6esoZ4zTNdM+qWTomZicht5Wy6wrk/s1epEpWu7RgRRPBj7z8P7JbtZiFgRhFV+72YYEYnZSJmayKSrMzkjPWOTU9O62xOUYddgemYHHUXW4WwKLp0t0+GMCibrlZChYl61cc0c6e3ikpVjKbuk0a5UU5nSAvZ/LYhBtUTWhEDzt34SL7DD9DfcvMr+zLJ+nggG0wtnKwv1JjG7IODsY8w76ing/9W87i5ffkDJXDxzZlm5iMmbJM9Y+pbRY60S/ZuzrK4sC+bJw5jPrH2Gc444IOPXiuiRl/lajyciAfvVs+o3qCGZkHPck6/Iy5mf5x8yH8somOjVvGYp9acDw7YZmxTGnq8UuvdqJHCOivGrIOWgbVnPNwTI9tYGxA3T/hGnH5pWwuxC+fMP/G9M34GbqTLzIP8zyBxiMuquyaW/phXjvPWuYVrGTcp2tO/smKGfO+zGCdtfp5/T0B3rGxsTeXNgYGemZHJ8fGp54KXLe0LTwegE6qgACAP8RRI0l33p8GviAAAKytAXjcqFtsww2m1Bc3GSDKJQ15gfsI40B5DR/I3QaAnOGSp+BAMmChxbPhsfVj6138s6sI+GeJ31mASQAA7gQfCABinAFuoPlAOBcivCyASAsUKa3/tbWaWuqnejdBqM1hrcZr7S0MOvWfLy+8BgDwfceFS/zsno/da2urf/s5hG+AAA+czwr+wiqy+etcSGvLPYAAfJ9wU6ATLdkByYKglFGJEkagAwEA4AM5eCCCHwB0ABkQEKADcMCBBHgAMHcL7ooUYmwhhZ/zExnVadegWqUqTUTPg+iAffY71jDJpONEJdqJdKpU09er6i1DqVpNqjQz2EOUQO+iM9RQoyZQztPlgZahUGYP0qZkBv24RtWMDET77bGvVngAHWsLcAAAAAA=) format('woff2'); +} +@font-face{ + font-family: "dm"; + font-weight: bold; + font-style: normal; + unicode-range: U+F8FF-10FFFF; + src: local('☺'), + url(data:font/woff2;charset=utf-8;base64,d09GMk9UVE8AAAJ0AAsAAAAAA/QAAAIqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAADYILGyAcKgZgAAQBNgIkAwQEBgWBdAcgGzADAC4D7IZjzymg62laLb5im9YqKYd4pp0IH5Q4wfM11t7f3RNPmCSGpIkh4c2yaCYkPItas2reuIZYSNQTM2Ss3ZtaSFrNsPSi0pFIpbFOplQSozQTgtx2D1DYR6JuhAypGLvREA2A7uApPgY1OYgeC8Ayvq0tfgCOy3/OjT9vz2+AA909pwF9/n14lic0TDDgLsCov3cD7HJP9eTYIyKzTeFwCA8+E4xCRLzVnb12vN/reyL9TgUEkfJgglcieJcPbf0Qt29BkrLv8J7zzxfL3bbhyD1wjGfBvFlGfC+BrvfzhBHx1jVXVjwp4PWnrNv+wl6+cBzQyXu6yQ23capTbj9uoPHOagFvx3+WIwca9UT0znIXQESUt2/589nB/3tmaqS0NLrz8cJ7wfRIj9sRh9955/JPstfOQqZk3y3czExOze66PSH71ADLTBIBnGTKJtW1sRzoGIAEAGBYnnxqGre7c9KDHAkko875Ih2j3eoVNMnzyi47CgTI/R/Y5O+BdBB/hz4QRzmYAVQZiQ3kMgAIIPD/xF1r5/lKD3MM+Fw1wOfx8Ya/07h5I9AoAAT0Q/wvR2PC/4+GwJWJmuVBIwSw0URCjAPogY6CoQcVigHUwNDQ4DoL6WWVtc7zaH+wHDXrjYnekaVEkWKlS6TJfCtX11KvsdHs+BudH+X2pDHteZVAhURkrVZjS6jKUF2dVSteBQb3BfvWuNnv6RUrUMSpBo7ENgAAAA==) format('woff2'); +} diff --git a/docs/md_v2/assets/highlight.css b/docs/md_v2/assets/highlight.css new file mode 100644 index 00000000..e69de29b diff --git a/docs/md_v2/assets/highlight.min.js b/docs/md_v2/assets/highlight.min.js new file mode 100644 index 00000000..f43ba9aa --- /dev/null +++ b/docs/md_v2/assets/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ + var hljs=function(){"use strict";function e(n){ + return n instanceof Map?n.clear=n.delete=n.set=()=>{ + throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ + throw Error("set is read-only") + }),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ + const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) + })),n}class n{constructor(e){ + void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} + ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ + return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") + }function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] + ;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope + ;class r{constructor(e,n){ + this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ + this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ + if(e.startsWith("language:"))return e.replace("language:","language-") + ;if(e.includes(".")){const t=e.split(".") + ;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") + }return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} + closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ + this.buffer+=``}}const s=(e={})=>{const n={children:[]} + ;return Object.assign(n,e),n};class o{constructor(){ + this.rootNode=s(),this.stack=[this.rootNode]}get top(){ + return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ + this.top.children.push(e)}openNode(e){const n=s({scope:e}) + ;this.add(n),this.stack.push(n)}closeNode(){ + if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ + for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} + walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ + return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), + n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ + "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ + o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} + addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ + this.closeNode()}__addSublanguage(e,n){const t=e.root + ;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ + return new r(this,this.options).value()}finalize(){ + return this.closeAllNodes(),!0}}function c(e){ + return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} + function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} + function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ + const n=e[e.length-1] + ;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} + })(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} + function p(e){return RegExp(e.toString()+"|").exec("").length-1} + const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ + ;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t + ;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} + i+=a.substring(0,e.index), + a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], + "("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} + const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ + begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", + illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", + contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, + contains:[]},t);i.contains.push({scope:"doctag", + begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) + ;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) + ;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i + },M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ + __proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ + scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, + C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", + begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ + "on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, + MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, + NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, + PHRASAL_WORDS_MODE:{ + begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, + end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, + RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", + SHEBANG:(e={})=>{const n=/^#![ ]*\// + ;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, + end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, + TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, + UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ + "."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ + void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ + n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", + e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, + void 0===e.relevance&&(e.relevance=0))}function I(e,n){ + Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ + if(e.match){ + if(e.begin||e.end)throw Error("begin & end are not supported with match") + ;e.begin=e.match,delete e.match}}function B(e,n){ + void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return + ;if(e.starts)throw Error("beforeMatch cannot be used with starts") + ;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] + })),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ + relevance:0,contains:[Object.assign(t,{endsParent:!0})] + },e.relevance=0,delete t.beforeMatch + },z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" + ;function U(e,n,t=F){const a=Object.create(null) + ;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ + Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ + n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") + ;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ + return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ + console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ + P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) + },G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} + ;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) + ;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ + e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, + delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ + _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope + }),(e=>{if(Array.isArray(e.begin)){ + if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), + G + ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), + G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ + if(Array.isArray(e.end)){ + if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), + G + ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), + G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ + function n(n,t){ + return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) + }class t{constructor(){ + this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} + addRule(e,n){ + n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), + this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) + ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" + }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex + ;const n=this.matcherRe.exec(e);if(!n)return null + ;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] + ;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ + this.rules=[],this.multiRegexes=[], + this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ + if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t + ;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), + n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ + return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ + this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ + const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex + ;let t=n.exec(e) + ;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ + const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} + return t&&(this.regexIndex+=t.position+1, + this.regexIndex===this.count&&this.considerAll()),t}} + if(e.compilerExtensions||(e.compilerExtensions=[]), + e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") + ;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r + ;if(r.isCompiled)return o + ;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), + r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null + ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), + l=r.keywords.$pattern, + delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), + o.keywordPatternRe=n(l,!0), + s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), + r.end&&(o.endRe=n(o.end)), + o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), + r.illegal&&(o.illegalRe=n(r.illegal)), + r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ + variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ + starts:e.starts?a(e.starts):null + }):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) + })),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i + ;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" + }))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" + }),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ + return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ + constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} + const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ + const a=Object.create(null),i=Object.create(null),r=[];let s=!0 + ;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ + disableAutodetect:!0,name:"Plain text",contains:[]};let p={ + ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, + languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", + cssSelector:"pre code",languages:null,__emitter:l};function _(e){ + return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" + ;"object"==typeof n?(a=e, + t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), + q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), + i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) + ;const s=r.result?r.result:f(r.language,r.code,t) + ;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ + const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) + ;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" + ;for(;n;){t+=A.substring(e,n.index) + ;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ + const[e,a]=r + ;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ + const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] + ;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a + ;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ + if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ + if(!a[x.subLanguage])return void S.addText(A) + ;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top + }else e=E(A,x.subLanguage.length?x.subLanguage:null) + ;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) + })():c(),A=""}function g(e,n){ + ""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 + ;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} + const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} + function b(e,n){ + return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), + e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), + A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ + value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) + ;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) + ;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ + for(;e.endsParent&&e.parent;)e=e.parent;return e}} + if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ + return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ + const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x + ;x.endScope&&x.endScope._wrap?(d(), + g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), + u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), + d(),r.excludeEnd&&(A=n));do{ + x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent + }while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} + let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 + ;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ + if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) + ;throw n.languageName=e,n.badRule=y.rule,n}return 1} + if(y=r,"begin"===r.type)return(e=>{ + const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] + ;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) + ;return a.skip?A+=t:(a.excludeBegin&&(A+=t), + d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) + ;if("illegal"===r.type&&!i){ + const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') + ;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} + if("illegal"===r.type&&""===o)return 1 + ;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") + ;return A+=o,o.length}const w=v(e) + ;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') + ;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] + ;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) + ;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ + if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ + R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T + ;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) + ;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, + value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ + if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), + illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, + context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ + language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} + ;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ + const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} + ;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) + ;i.unshift(t);const r=i.sort(((e,n)=>{ + if(e.relevance!==n.relevance)return n.relevance-e.relevance + ;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 + ;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s + ;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ + let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" + ;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) + ;return n||(H(o.replace("{}",t[1])), + H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} + return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return + ;if(x("before:highlightElement",{el:e,language:t + }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) + ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), + console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), + console.warn("The element with unescaped HTML:"), + console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) + ;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) + ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t + ;e.classList.add("hljs"),e.classList.add("language-"+a) + })(e,t,r.language),e.result={language:r.language,re:r.relevance, + relevance:r.relevance},r.secondBest&&(e.secondBest={ + language:r.secondBest.language,relevance:r.secondBest.relevance + }),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ + "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 + }function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} + function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ + i[e.toLowerCase()]=n}))}function k(e){const n=v(e) + ;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ + e[t]&&e[t](n)}))} + "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ + N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, + highlightElement:y, + highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), + q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, + initHighlighting:()=>{ + w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, + initHighlightingOnLoad:()=>{ + w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") + },registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ + if(K("Language definition for '{}' could not be registered.".replace("{}",e)), + !s)throw n;K(n),i=c} + i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ + languageName:e})},unregisterLanguage:e=>{delete a[e] + ;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, + listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, + autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ + e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ + e["before:highlightBlock"](Object.assign({block:n.el},n)) + }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ + e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, + removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ + s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, + lookahead:d,either:m,optional:u,anyNumberOfTimes:g} + ;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t + },te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ + scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ + scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, + FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, + ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", + contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ + scope:"number", + begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", + relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} + }),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) + ;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ + className:"number",variants:[{ + begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ + begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ + begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ + begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ + begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ + begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], + relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} + const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) + ;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, + end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ + const t=e[0].length+e.index,a=e.input[t] + ;if("<"===a||","===a)return void n.ignoreMatch();let i + ;">"===a&&(((e,{after:n})=>{const t="",M={ + match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], + keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} + ;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ + PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, + contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ + label:"use_strict",className:"meta",relevance:10, + begin:/^\s*['"]use (strict|asm)['"]/ + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ + className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ + begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", + keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ + className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ + className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ + className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, + excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, + relevance:0},{variants:[{begin:"<>",end:""},{ + match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, + "on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ + begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ + beginKeywords:"while if switch catch for"},{ + begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", + returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, + className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, + relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, + contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},E,k,{match:/\$[(.]/}]}} + const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] + ;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ + begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} + ;Object.assign(t,{className:"variable",variants:[{ + begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ + className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ + begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, + end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, + end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] + },l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 + }),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, + contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ + name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, + keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], + literal:["true","false"], + built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] + },contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ + match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, + grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ + match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], + type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], + literal:"true false NULL", + built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" + },b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], + relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, + keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, + end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] + }]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, + disableAutodetect:!0,illegal:"=]/,contains:[{ + beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, + strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ + contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], + keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], + literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], + _type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] + },b={className:"function.dispatch",relevance:0,keywords:{ + _hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] + }, + begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) + },m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ + begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ + className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, + contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, + relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] + },s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", + aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ + match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], + className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ + keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), + built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], + literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ + begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ + begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] + },r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, + keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, + end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ + },e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ + begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, + contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) + ;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], + o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ + illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] + },b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ + begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], + keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, + contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ + begin:"\x3c!--|--\x3e"},{begin:""}]}] + }),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", + end:"$",keywords:{ + keyword:"if else elif endif define undef warning error line region endregion pragma checksum" + }},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, + illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" + },t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", + relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", + begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ + className:"string",begin:/"/,end:/"/}]},{ + beginKeywords:"new return throw await else",relevance:0},{className:"function", + begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ + beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", + relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", + begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, + contains:[g,a,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ + const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ + name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ + keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, + contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ + },t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 + },{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 + },t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ + begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] + },t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ + begin:/:/,end:/[;}{]/, + contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ + begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" + },contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, + excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", + relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ + },{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ + $pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ + begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ + className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ + const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ + className:"meta",relevance:10, + match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) + },{className:"comment",variants:[{ + begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), + end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ + className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, + end:/$/}]}},grmr_go:e=>{const n={ + keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], + type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], + literal:["true","false","iota","nil"], + built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] + };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], + case_insensitive:!0,disableAutodetect:!1,keywords:{ + keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], + literal:["true","false","null"]}, + contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ + scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", + begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, + end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ + scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), + relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ + className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ + begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, + end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ + begin:/\$\{(.*?)\}/}]},r={className:"literal", + begin:/\bon|off|true|false|yes|no\b/},s={className:"string", + contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ + begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] + },o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 + },l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ + name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, + contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ + begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), + className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ + const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ + keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], + literal:["false","true","null"], + type:["char","boolean","long","float","int","byte","short","double"], + built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ + begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} + ;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, + relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ + begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, + className:"string",contains:[e.BACKSLASH_ESCAPE] + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ + match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ + 1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ + begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", + 3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", + 3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"new throw return else",relevance:0},{ + begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ + 2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0, + contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, + grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", + beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ + className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ + match:/[{}[\],:]/,className:"punctuation",relevance:0 + },e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], + illegal:"\\S"}},grmr_kotlin:e=>{const n={ + keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", + built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", + literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" + },a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ + className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", + variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", + illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, + contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ + className:"meta", + begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" + },o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, + end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] + },l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ + variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, + contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], + {name:"Kotlin",aliases:["kt","kts"],keywords:n, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", + begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", + begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", + begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", + returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ + begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, + contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, + keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, + endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, + endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 + },e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ + begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ + 3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, + illegal:"extends implements",contains:[{ + beginKeywords:"public protected internal private constructor" + },e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, + excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, + excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", + end:"$",illegal:"\n"},l]}},grmr_less:e=>{ + const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ + className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, + relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", + attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, + relevance:0} + ;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ + begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", + excludeEnd:!0} + },n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ + className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 + },n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ + begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, + contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", + returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ + },n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", + end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] + },m={className:"keyword", + begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", + starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ + className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a + }],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ + begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, + returnEnd:!0,illegal:"[<='$\"]",relevance:0, + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ + begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" + },n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ + className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ + className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, + end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ + begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} + ;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), + {name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, + grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] + },i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 + })];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, + literal:"true false nil", + keyword:"and break do else elseif end for goto if in local not or repeat return then until while", + built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" + },contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", + contains:[e.inherit(e.TITLE_MODE,{ + begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", + begin:"\\(",endsWithParent:!0,contains:i}].concat(i) + },e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", + begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ + className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ + const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ + variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ + begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, + relevance:2},{ + begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), + relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ + begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ + },{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, + returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", + excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", + end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], + variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] + },i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ + begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] + }),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) + ;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) + })),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ + className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ + begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", + contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", + end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, + end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ + begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ + begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", + contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ + begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ + className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ + className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ + const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, + keyword:["@interface","@class","@protocol","@implementation"]};return{ + name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], + keywords:{"variable.language":["this","super"],$pattern:n, + keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], + literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], + built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], + type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] + },illegal:"/,end:/$/,illegal:"\\n" + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", + begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, + contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, + relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ + $pattern:/[\w.]+/, + keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" + },i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, + end:/\}/},s={variants:[{begin:/\$\d/},{ + begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") + },{begin:/[$%@][^\s\w{]/,relevance:0}] + },o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ + const r="\\1"===i?i:n.concat(i,a) + ;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) + },d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ + endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ + begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", + end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ + begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", + relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", + contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ + begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", + begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", + relevance:0},{ + begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", + keywords:"split return print reverse grep",relevance:0, + contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ + begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ + begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ + className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ + begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 + }),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ + begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", + end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ + begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", + subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] + }];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, + contains:g}},grmr_php:e=>{ + const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ + scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ + begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null + }),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ + illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ + begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, + contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ + n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ + begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ + begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ + begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ + begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" + }],relevance:0 + },g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ + keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ + n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) + })),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ + match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ + 1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ + match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" + }},{match:[/::/,/class/],scope:{2:"variable.language"}},{ + match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", + 3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], + scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", + 3:"variable.language"}}]},E={scope:"attr", + match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, + begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] + },N={relevance:0, + match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], + scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) + ;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, + keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, + endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ + begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, + contains:["self",...w]},...w,{scope:"meta",match:i}] + },e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ + scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, + keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, + contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ + begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ + begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ + match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ + scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, + excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" + },e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", + begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, + contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ + beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", + illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ + beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ + beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, + contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ + beginKeywords:"use",relevance:0,end:";",contains:[{ + match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} + },grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ + begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", + end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 + },e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, + skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, + contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", + aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ + const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ + $pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, + built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], + literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], + type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] + },r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, + end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ + className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, + end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, + relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ + begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, + end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, + contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ + className:"number",relevance:0,variants:[{ + begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ + begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ + begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` + },{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` + }]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, + contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ + className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, + end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, + contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ + name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, + illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", + relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ + 1:"keyword",3:"title.function"},contains:[m]},{variants:[{ + match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], + scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ + className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, + grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", + starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ + begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ + const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) + ;return{name:"R",keywords:{$pattern:t, + keyword:"function if in break next repeat else for while", + literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", + built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" + },contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, + starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), + endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ + scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 + }]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] + }),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], + variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', + relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ + 1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, + match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ + 2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, + match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ + match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", + contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ + const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ + "variable.constant":["__FILE__","__LINE__","__ENCODING__"], + "variable.language":["self","super"], + keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], + built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], + literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ + begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] + }),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 + }),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, + end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ + begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ + begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, + end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ + begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ + begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ + begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ + begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ + begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), + contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, + contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", + relevance:0,variants:[{ + begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ + begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" + },{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ + begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ + begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ + className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, + keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ + match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", + 4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ + 2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ + 1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ + match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ + begin:e.IDENT_RE+"::"},{className:"symbol", + begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", + begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", + begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ + className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, + relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", + keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], + illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ + begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", + end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) + ;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} + },{className:"meta.prompt", + begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", + starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", + aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, + contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, + grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, + begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) + },a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] + ;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, + keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], + literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, + grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", + begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", + case_insensitive:!0,illegal:"[=/|']", + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ + className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ + className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 + },n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", + begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", + begin:":("+a.join("|")+")"},{className:"selector-pseudo", + begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, + contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", + begin:"\\b("+ce.join("|")+")\\b"},{ + begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" + },{begin:/:/,end:/[;}{]/,relevance:0, + contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] + },{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ + begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, + keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, + className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" + },r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] + },n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", + aliases:["console","shellsession"],contains:[{className:"meta.prompt", + begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, + subLanguage:"bash"}}]}),grmr_sql:e=>{ + const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ + begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} + ;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ + $pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t + ;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) + })(l,{when:e=>e.length<3}),literal:a,type:i, + built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] + },contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, + keyword:l.concat(s),literal:a,type:i}},{className:"type", + begin:n.either("double precision","large object","with timezone","without timezone") + },c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", + variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, + contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ + className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, + relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 + },t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ + match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), + relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ + className:"keyword", + match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ + $pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ + match:b(/\./,m(...De)),relevance:0},{className:"built_in", + match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ + className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] + }],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, + variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ + match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ + },{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ + match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] + }),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) + }),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ + }),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] + }),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ + className:"string", + variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] + },k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, + contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, + contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, + contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ + scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) + },C=[A,{className:"variable",match:/\$\d+/},{className:"variable", + match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ + contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ + scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ + match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", + match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") + },{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ + match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ + begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) + ;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ + match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 + },...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, + keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, + contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, + relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", + match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ + match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", + 3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ + match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, + contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ + 1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ + 1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} + ;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) + ;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, + end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, + contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", + end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ + className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] + },F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 + },S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ + const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ + beginKeywords:"namespace",end:/\{/,excludeEnd:!0, + contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, + excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, + contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, + keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), + literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", + begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) + ;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} + ;return Object.assign(n.keywords,s), + n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), + l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, + begin:/^\s*['"]use strict['"]/ + }),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ + name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ + const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ + className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ + begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ + begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] + },o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] + }),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) + ;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, + classNameAliases:{label:"symbol"},keywords:{ + keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", + built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", + type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", + literal:"true false nothing"}, + illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ + className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, + end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, + variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ + },{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ + begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ + className:"label",begin:/^\w+:/},o,l,{className:"meta", + begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, + end:/$/,keywords:{ + keyword:"const disable else elseif enable end externalsource if region then"}, + contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) + ;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, + keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] + },contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], + className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ + match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ + begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", + 3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, + className:"type"},{className:"keyword", + match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ + },{className:"number",relevance:0, + match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ + }]}},grmr_xml:e=>{ + const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ + className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, + contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] + },r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ + className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ + endsWithParent:!0,illegal:/`]+/}]}]}]};return{ + name:"HTML, XML", + aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], + case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ + className:"meta",begin://,contains:[i,r,o,s]}]}] + },e.COMMENT(//,{relevance:10}),{begin://, + relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, + relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ + end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ + end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ + className:"tag",begin:/<>|<\/>/},{className:"tag", + begin:n.concat(//,/>/,/\s/)))), + end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ + className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ + className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} + },grmr_yaml:e=>{ + const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ + className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ + },{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", + variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ + end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, + end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", + contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ + begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ + begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", + relevance:10},{className:"string", + begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ + begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, + relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", + begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t + },{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", + begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", + relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ + className:"number", + begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" + },{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] + ;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, + aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ + const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} + return He}() + ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/docs/md_v2/assets/highlight_init.js b/docs/md_v2/assets/highlight_init.js new file mode 100644 index 00000000..e2379278 --- /dev/null +++ b/docs/md_v2/assets/highlight_init.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', (event) => { + document.querySelectorAll('pre code').forEach((block) => { + hljs.highlightBlock(block); + }); + }); + \ No newline at end of file diff --git a/docs/md_v2/assets/styles.css b/docs/md_v2/assets/styles.css new file mode 100644 index 00000000..f103474f --- /dev/null +++ b/docs/md_v2/assets/styles.css @@ -0,0 +1,153 @@ +@font-face { + font-family: "Monaco"; + font-style: normal; + font-weight: normal; + src: local("Monaco"), url("Monaco.woff") format("woff"); +} + +:root { + --global-font-size: 16px; + --global-line-height: 1.5em; + --global-space: 10px; + --font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, + Courier New, monospace, serif; + --font-stack: dm, Monaco, Courier New, monospace, serif; + --mono-font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, + Courier New, monospace, serif; + + --background-color: #151515; /* Dark background */ + --font-color: #eaeaea; /* Light font color for contrast */ + --invert-font-color: #151515; /* Dark color for inverted elements */ + --primary-color: #1a95e0; /* Primary color can remain the same or be adjusted for better contrast */ + --secondary-color: #727578; /* Secondary color for less important text */ + --error-color: #ff5555; /* Bright color for errors */ + --progress-bar-background: #444; /* Darker background for progress bar */ + --progress-bar-fill: #1a95e0; /* Bright color for progress bar fill */ + --code-bg-color: #1e1e1e; /* Darker background for code blocks */ + --input-style: solid; /* Keeping input style solid */ + --block-background-color: #202020; /* Darker background for block elements */ + --global-font-color: #eaeaea; /* Light font color for global elements */ + + --background-color: #222225; + + --background-color: #070708; + --page-width: 70em; + --font-color: #e8e9ed; + --invert-font-color: #222225; + --secondary-color: #a3abba; + --secondary-color: #d5cec0; + --tertiary-color: #a3abba; + --primary-color: #09b5a5; /* Updated to the brand color */ + --primary-color: #50ffff; /* Updated to the brand color */ + --error-color: #ff3c74; + --progress-bar-background: #3f3f44; + --progress-bar-fill: #09b5a5; /* Updated to the brand color */ + --code-bg-color: #3f3f44; + --input-style: solid; + --display-h1-decoration: none; + + --display-h1-decoration: none; +} + +/* body { + background-color: var(--background-color); + color: var(--font-color); +} + +a { + color: var(--primary-color); +} + +a:hover { + background-color: var(--primary-color); + color: var(--invert-font-color); +} + +blockquote::after { + color: #444; +} + +pre, code { + background-color: var(--code-bg-color); + color: var(--font-color); +} + +.terminal-nav:first-child { + border-bottom: 1px dashed var(--secondary-color); +} */ + +.terminal-mkdocs-main-content { + line-height: var(--global-line-height); +} + +strong, +.highlight { + /* background: url(//s2.svgbox.net/pen-brushes.svg?ic=brush-1&color=50ffff); */ + background-color: #50ffff33; +} + +.terminal-card > header { + color: var(--font-color); + text-align: center; + background-color: var(--progress-bar-background); + padding: 0.3em 0.5em; +} +.btn.btn-sm { + color: var(--font-color); + padding: 0.2em 0.5em; + font-size: 0.8em; +} + +.loading-message { + display: none; + margin-top: 20px; +} + +.response-section { + display: none; + padding-top: 20px; +} + +.tabs { + display: flex; + flex-direction: column; +} +.tab-list { + display: flex; + padding: 0; + margin: 0; + list-style-type: none; + border-bottom: 1px solid var(--font-color); +} +.tab-item { + cursor: pointer; + padding: 10px; + border: 1px solid var(--font-color); + margin-right: -1px; + border-bottom: none; +} +.tab-item:hover, +.tab-item:focus, +.tab-item:active { + background-color: var(--progress-bar-background); +} +.tab-content { + display: none; + border: 1px solid var(--font-color); + border-top: none; +} +.tab-content:first-of-type { + display: block; +} + +.tab-content header { + padding: 0.5em; + display: flex; + justify-content: end; + align-items: center; + background-color: var(--progress-bar-background); +} +.tab-content pre { + margin: 0; + max-height: 300px; overflow: auto; border:none; +} \ No newline at end of file diff --git a/docs/md_v2/basic/browser-config.md b/docs/md_v2/basic/browser-config.md new file mode 100644 index 00000000..7df4a97b --- /dev/null +++ b/docs/md_v2/basic/browser-config.md @@ -0,0 +1,208 @@ +# Browser Configuration + +Crawl4AI supports multiple browser engines and offers extensive configuration options for browser behavior. + +## Browser Types + +Choose from three browser engines: + +```python +# Chromium (default) +async with AsyncWebCrawler(browser_type="chromium") as crawler: + result = await crawler.arun(url="https://example.com") + +# Firefox +async with AsyncWebCrawler(browser_type="firefox") as crawler: + result = await crawler.arun(url="https://example.com") + +# WebKit +async with AsyncWebCrawler(browser_type="webkit") as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Basic Configuration + +Common browser settings: + +```python +async with AsyncWebCrawler( + headless=True, # Run in headless mode (no GUI) + verbose=True, # Enable detailed logging + sleep_on_close=False # No delay when closing browser +) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Identity Management + +Control how your crawler appears to websites: + +```python +# Custom user agent +async with AsyncWebCrawler( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" +) as crawler: + result = await crawler.arun(url="https://example.com") + +# Custom headers +headers = { + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "no-cache" +} +async with AsyncWebCrawler(headers=headers) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Screenshot Capabilities + +Capture page screenshots with enhanced error handling: + +```python +result = await crawler.arun( + url="https://example.com", + screenshot=True, # Enable screenshot + screenshot_wait_for=2.0 # Wait 2 seconds before capture +) + +if result.screenshot: # Base64 encoded image + import base64 + with open("screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) +``` + +## Timeouts and Waiting + +Control page loading behavior: + +```python +result = await crawler.arun( + url="https://example.com", + page_timeout=60000, # Page load timeout (ms) + delay_before_return_html=2.0, # Wait before content capture + wait_for="css:.dynamic-content" # Wait for specific element +) +``` + +## JavaScript Execution + +Execute custom JavaScript before crawling: + +```python +# Single JavaScript command +result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);" +) + +# Multiple commands +js_commands = [ + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more').click();" +] +result = await crawler.arun( + url="https://example.com", + js_code=js_commands +) +``` + +## Proxy Configuration + +Use proxies for enhanced access: + +```python +# Simple proxy +async with AsyncWebCrawler( + proxy="http://proxy.example.com:8080" +) as crawler: + result = await crawler.arun(url="https://example.com") + +# Proxy with authentication +proxy_config = { + "server": "http://proxy.example.com:8080", + "username": "user", + "password": "pass" +} +async with AsyncWebCrawler(proxy_config=proxy_config) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Anti-Detection Features + +Enable stealth features to avoid bot detection: + +```python +result = await crawler.arun( + url="https://example.com", + simulate_user=True, # Simulate human behavior + override_navigator=True, # Mask automation signals + magic=True # Enable all anti-detection features +) +``` + +## Handling Dynamic Content + +Configure browser to handle dynamic content: + +```python +# Wait for dynamic content +result = await crawler.arun( + url="https://example.com", + wait_for="js:() => document.querySelector('.content').children.length > 10", + process_iframes=True # Process iframe content +) + +# Handle lazy-loaded images +result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);", + delay_before_return_html=2.0 # Wait for images to load +) +``` + +## Comprehensive Example + +Here's how to combine various browser configurations: + +```python +async def crawl_with_advanced_config(url: str): + async with AsyncWebCrawler( + # Browser setup + browser_type="chromium", + headless=True, + verbose=True, + + # Identity + user_agent="Custom User Agent", + headers={"Accept-Language": "en-US"}, + + # Proxy setup + proxy="http://proxy.example.com:8080" + ) as crawler: + result = await crawler.arun( + url=url, + # Content handling + process_iframes=True, + screenshot=True, + + # Timing + page_timeout=60000, + delay_before_return_html=2.0, + + # Anti-detection + magic=True, + simulate_user=True, + + # Dynamic content + js_code=[ + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more')?.click();" + ], + wait_for="css:.dynamic-content" + ) + + return { + "content": result.markdown, + "screenshot": result.screenshot, + "success": result.success + } +``` \ No newline at end of file diff --git a/docs/md_v2/basic/content-selection.md b/docs/md_v2/basic/content-selection.md new file mode 100644 index 00000000..f5f7397b --- /dev/null +++ b/docs/md_v2/basic/content-selection.md @@ -0,0 +1,199 @@ +# Content Selection + +Crawl4AI provides multiple ways to select and filter specific content from webpages. Learn how to precisely target the content you need. + +## CSS Selectors + +The simplest way to extract specific content: + +```python +# Extract specific content using CSS selector +result = await crawler.arun( + url="https://example.com", + css_selector=".main-article" # Target main article content +) + +# Multiple selectors +result = await crawler.arun( + url="https://example.com", + css_selector="article h1, article .content" # Target heading and content +) +``` + +## Content Filtering + +Control what content is included or excluded: + +```python +result = await crawler.arun( + url="https://example.com", + # Content thresholds + word_count_threshold=10, # Minimum words per block + + # Tag exclusions + excluded_tags=['form', 'header', 'footer', 'nav'], + + # Link filtering + exclude_external_links=True, # Remove external links + exclude_social_media_links=True, # Remove social media links + + # Media filtering + exclude_external_images=True # Remove external images +) +``` + +## Iframe Content + +Process content inside iframes: + +```python +result = await crawler.arun( + url="https://example.com", + process_iframes=True, # Extract iframe content + remove_overlay_elements=True # Remove popups/modals that might block iframes +) +``` + +## Structured Content Selection + +### Using LLMs for Smart Selection + +Use LLMs to intelligently extract specific types of content: + +```python +from pydantic import BaseModel +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +class ArticleContent(BaseModel): + title: str + main_points: List[str] + conclusion: str + +strategy = LLMExtractionStrategy( + provider="ollama/nemotron", # Works with any supported LLM + schema=ArticleContent.schema(), + instruction="Extract the main article title, key points, and conclusion" +) + +result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy +) +article = json.loads(result.extracted_content) +``` + +### Pattern-Based Selection + +For repeated content patterns (like product listings, news feeds): + +```python +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +schema = { + "name": "News Articles", + "baseSelector": "article.news-item", # Repeated element + "fields": [ + {"name": "headline", "selector": "h2", "type": "text"}, + {"name": "summary", "selector": ".summary", "type": "text"}, + {"name": "category", "selector": ".category", "type": "text"}, + { + "name": "metadata", + "type": "nested", + "fields": [ + {"name": "author", "selector": ".author", "type": "text"}, + {"name": "date", "selector": ".date", "type": "text"} + ] + } + ] +} + +strategy = JsonCssExtractionStrategy(schema) +result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy +) +articles = json.loads(result.extracted_content) +``` + +## Domain-Based Filtering + +Control content based on domains: + +```python +result = await crawler.arun( + url="https://example.com", + exclude_domains=["ads.com", "tracker.com"], + exclude_social_media_domains=["facebook.com", "twitter.com"], # Custom social media domains to exclude + exclude_social_media_links=True +) +``` + +## Media Selection + +Select specific types of media: + +```python +result = await crawler.arun(url="https://example.com") + +# Access different media types +images = result.media["images"] # List of image details +videos = result.media["videos"] # List of video details +audios = result.media["audios"] # List of audio details + +# Image with metadata +for image in images: + print(f"URL: {image['src']}") + print(f"Alt text: {image['alt']}") + print(f"Description: {image['desc']}") + print(f"Relevance score: {image['score']}") +``` + +## Comprehensive Example + +Here's how to combine different selection methods: + +```python +async def extract_article_content(url: str): + # Define structured extraction + article_schema = { + "name": "Article", + "baseSelector": "article.main", + "fields": [ + {"name": "title", "selector": "h1", "type": "text"}, + {"name": "content", "selector": ".content", "type": "text"} + ] + } + + # Define LLM extraction + class ArticleAnalysis(BaseModel): + key_points: List[str] + sentiment: str + category: str + + async with AsyncWebCrawler() as crawler: + # Get structured content + pattern_result = await crawler.arun( + url=url, + extraction_strategy=JsonCssExtractionStrategy(article_schema), + word_count_threshold=10, + excluded_tags=['nav', 'footer'], + exclude_external_links=True + ) + + # Get semantic analysis + analysis_result = await crawler.arun( + url=url, + extraction_strategy=LLMExtractionStrategy( + provider="ollama/nemotron", + schema=ArticleAnalysis.schema(), + instruction="Analyze the article content" + ) + ) + + # Combine results + return { + "article": json.loads(pattern_result.extracted_content), + "analysis": json.loads(analysis_result.extracted_content), + "media": pattern_result.media + } +``` \ No newline at end of file diff --git a/docs/md_v2/basic/installation.md b/docs/md_v2/basic/installation.md new file mode 100644 index 00000000..a4a60857 --- /dev/null +++ b/docs/md_v2/basic/installation.md @@ -0,0 +1,92 @@ +# Installation πŸ’» + +Crawl4AI offers flexible installation options to suit various use cases. You can install it as a Python package, use it with Docker, or run it as a local server. + +## Option 1: Python Package Installation (Recommended) + +Crawl4AI is now available on PyPI, making installation easier than ever. Choose the option that best fits your needs: + +### Basic Installation + +For basic web crawling and scraping tasks: + +```bash +pip install crawl4ai +playwright install # Install Playwright dependencies +``` + +### Installation with PyTorch + +For advanced text clustering (includes CosineSimilarity cluster strategy): + +```bash +pip install crawl4ai[torch] +``` + +### Installation with Transformers + +For text summarization and Hugging Face models: + +```bash +pip install crawl4ai[transformer] +``` + +### Full Installation + +For all features: + +```bash +pip install crawl4ai[all] +``` + +### Development Installation + +For contributors who plan to modify the source code: + +```bash +git clone https://github.com/unclecode/crawl4ai.git +cd crawl4ai +pip install -e ".[all]" +playwright install # Install Playwright dependencies +``` + +πŸ’‘ After installation with "torch", "transformer", or "all" options, it's recommended to run the following CLI command to load the required models: + +```bash +crawl4ai-download-models +``` + +This is optional but will boost the performance and speed of the crawler. You only need to do this once after installation. + +## Option 2: Using Docker (Coming Soon) + +Docker support for Crawl4AI is currently in progress and will be available soon. This will allow you to run Crawl4AI in a containerized environment, ensuring consistency across different systems. + +## Option 3: Local Server Installation + +For those who prefer to run Crawl4AI as a local server, instructions will be provided once the Docker implementation is complete. + +## Verifying Your Installation + +After installation, you can verify that Crawl4AI is working correctly by running a simple Python script: + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun(url="https://www.example.com") + print(result.markdown[:500]) # Print first 500 characters + +if __name__ == "__main__": + asyncio.run(main()) +``` + +This script should successfully crawl the example website and print the first 500 characters of the extracted content. + +## Getting Help + +If you encounter any issues during installation or usage, please check the [documentation](https://crawl4ai.com/mkdocs/) or raise an issue on the [GitHub repository](https://github.com/unclecode/crawl4ai/issues). + +Happy crawling! πŸ•·οΈπŸ€– \ No newline at end of file diff --git a/docs/md_v2/basic/output-formats.md b/docs/md_v2/basic/output-formats.md new file mode 100644 index 00000000..0d25e884 --- /dev/null +++ b/docs/md_v2/basic/output-formats.md @@ -0,0 +1,195 @@ +# Output Formats + +Crawl4AI provides multiple output formats to suit different needs, from raw HTML to structured data using LLM or pattern-based extraction. + +## Basic Formats + +```python +result = await crawler.arun(url="https://example.com") + +# Access different formats +raw_html = result.html # Original HTML +clean_html = result.cleaned_html # Sanitized HTML +markdown = result.markdown # Standard markdown +fit_md = result.fit_markdown # Most relevant content in markdown +``` + +## Raw HTML + +Original, unmodified HTML from the webpage. Useful when you need to: +- Preserve the exact page structure +- Process HTML with your own tools +- Debug page issues + +```python +result = await crawler.arun(url="https://example.com") +print(result.html) # Complete HTML including headers, scripts, etc. +``` + +## Cleaned HTML + +Sanitized HTML with unnecessary elements removed. Automatically: +- Removes scripts and styles +- Cleans up formatting +- Preserves semantic structure + +```python +result = await crawler.arun( + url="https://example.com", + excluded_tags=['form', 'header', 'footer'], # Additional tags to remove + keep_data_attributes=False # Remove data-* attributes +) +print(result.cleaned_html) +``` + +## Standard Markdown + +HTML converted to clean markdown format. Great for: +- Content analysis +- Documentation +- Readability + +```python +result = await crawler.arun( + url="https://example.com", + include_links_on_markdown=True # Include links in markdown +) +print(result.markdown) +``` + +## Fit Markdown + +Most relevant content extracted and converted to markdown. Ideal for: +- Article extraction +- Main content focus +- Removing boilerplate + +```python +result = await crawler.arun(url="https://example.com") +print(result.fit_markdown) # Only the main content +``` + +## Structured Data Extraction + +Crawl4AI offers two powerful approaches for structured data extraction: + +### 1. LLM-Based Extraction + +Use any LLM (OpenAI, HuggingFace, Ollama, etc.) to extract structured data with high accuracy: + +```python +from pydantic import BaseModel +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +class KnowledgeGraph(BaseModel): + entities: List[dict] + relationships: List[dict] + +strategy = LLMExtractionStrategy( + provider="ollama/nemotron", # or "huggingface/...", "ollama/..." + api_token="your-token", # not needed for Ollama + schema=KnowledgeGraph.schema(), + instruction="Extract entities and relationships from the content" +) + +result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy +) +knowledge_graph = json.loads(result.extracted_content) +``` + +### 2. Pattern-Based Extraction + +For pages with repetitive patterns (e.g., product listings, article feeds), use JsonCssExtractionStrategy: + +```python +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +schema = { + "name": "Product Listing", + "baseSelector": ".product-card", # Repeated element + "fields": [ + {"name": "title", "selector": "h2", "type": "text"}, + {"name": "price", "selector": ".price", "type": "text"}, + {"name": "description", "selector": ".desc", "type": "text"} + ] +} + +strategy = JsonCssExtractionStrategy(schema) +result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy +) +products = json.loads(result.extracted_content) +``` + +## Content Customization + +### HTML to Text Options + +Configure markdown conversion: + +```python +result = await crawler.arun( + url="https://example.com", + html2text={ + "escape_dot": False, + "body_width": 0, + "protect_links": True, + "unicode_snob": True + } +) +``` + +### Content Filters + +Control what content is included: + +```python +result = await crawler.arun( + url="https://example.com", + word_count_threshold=10, # Minimum words per block + exclude_external_links=True, # Remove external links + exclude_external_images=True, # Remove external images + excluded_tags=['form', 'nav'] # Remove specific HTML tags +) +``` + +## Comprehensive Example + +Here's how to use multiple output formats together: + +```python +async def crawl_content(url: str): + async with AsyncWebCrawler() as crawler: + # Extract main content with fit markdown + result = await crawler.arun( + url=url, + word_count_threshold=10, + exclude_external_links=True + ) + + # Get structured data using LLM + llm_result = await crawler.arun( + url=url, + extraction_strategy=LLMExtractionStrategy( + provider="ollama/nemotron", + schema=YourSchema.schema(), + instruction="Extract key information" + ) + ) + + # Get repeated patterns (if any) + pattern_result = await crawler.arun( + url=url, + extraction_strategy=JsonCssExtractionStrategy(your_schema) + ) + + return { + "main_content": result.fit_markdown, + "structured_data": json.loads(llm_result.extracted_content), + "pattern_data": json.loads(pattern_result.extracted_content), + "media": result.media + } +``` \ No newline at end of file diff --git a/docs/md_v2/basic/page-interaction.md b/docs/md_v2/basic/page-interaction.md new file mode 100644 index 00000000..7555f225 --- /dev/null +++ b/docs/md_v2/basic/page-interaction.md @@ -0,0 +1,207 @@ +# Page Interaction + +Crawl4AI provides powerful features for interacting with dynamic webpages, handling JavaScript execution, and managing page events. + +## JavaScript Execution + +### Basic Execution + +```python +# Single JavaScript command +result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);" +) + +# Multiple commands +js_commands = [ + "window.scrollTo(0, document.body.scrollHeight);", + "document.querySelector('.load-more').click();", + "document.querySelector('#consent-button').click();" +] +result = await crawler.arun( + url="https://example.com", + js_code=js_commands +) +``` + +## Wait Conditions + +### CSS-Based Waiting + +Wait for elements to appear: + +```python +result = await crawler.arun( + url="https://example.com", + wait_for="css:.dynamic-content" # Wait for element with class 'dynamic-content' +) +``` + +### JavaScript-Based Waiting + +Wait for custom conditions: + +```python +# Wait for number of elements +wait_condition = """() => { + return document.querySelectorAll('.item').length > 10; +}""" + +result = await crawler.arun( + url="https://example.com", + wait_for=f"js:{wait_condition}" +) + +# Wait for dynamic content to load +wait_for_content = """() => { + const content = document.querySelector('.content'); + return content && content.innerText.length > 100; +}""" + +result = await crawler.arun( + url="https://example.com", + wait_for=f"js:{wait_for_content}" +) +``` + +## Handling Dynamic Content + +### Load More Content + +Handle infinite scroll or load more buttons: + +```python +# Scroll and wait pattern +result = await crawler.arun( + url="https://example.com", + js_code=[ + # Scroll to bottom + "window.scrollTo(0, document.body.scrollHeight);", + # Click load more if exists + "const loadMore = document.querySelector('.load-more'); if(loadMore) loadMore.click();" + ], + # Wait for new content + wait_for="js:() => document.querySelectorAll('.item').length > previousCount" +) +``` + +### Form Interaction + +Handle forms and inputs: + +```python +js_form_interaction = """ + // Fill form fields + document.querySelector('#search').value = 'search term'; + // Submit form + document.querySelector('form').submit(); +""" + +result = await crawler.arun( + url="https://example.com", + js_code=js_form_interaction, + wait_for="css:.results" # Wait for results to load +) +``` + +## Timing Control + +### Delays and Timeouts + +Control timing of interactions: + +```python +result = await crawler.arun( + url="https://example.com", + page_timeout=60000, # Page load timeout (ms) + delay_before_return_html=2.0, # Wait before capturing content +) +``` + +## Complex Interactions Example + +Here's an example of handling a dynamic page with multiple interactions: + +```python +async def crawl_dynamic_content(): + async with AsyncWebCrawler() as crawler: + # Initial page load + result = await crawler.arun( + url="https://example.com", + # Handle cookie consent + js_code="document.querySelector('.cookie-accept')?.click();", + wait_for="css:.main-content" + ) + + # Load more content + session_id = "dynamic_session" # Keep session for multiple interactions + + for page in range(3): # Load 3 pages of content + result = await crawler.arun( + url="https://example.com", + session_id=session_id, + js_code=[ + # Scroll to bottom + "window.scrollTo(0, document.body.scrollHeight);", + # Store current item count + "window.previousCount = document.querySelectorAll('.item').length;", + # Click load more + "document.querySelector('.load-more')?.click();" + ], + # Wait for new items + wait_for="""() => { + const currentCount = document.querySelectorAll('.item').length; + return currentCount > window.previousCount; + }""", + # Only execute JS without reloading page + js_only=True if page > 0 else False + ) + + # Process content after each load + print(f"Page {page + 1} items:", len(result.cleaned_html)) + + # Clean up session + await crawler.crawler_strategy.kill_session(session_id) +``` + +## Using with Extraction Strategies + +Combine page interaction with structured extraction: + +```python +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy, LLMExtractionStrategy + +# Pattern-based extraction after interaction +schema = { + "name": "Dynamic Items", + "baseSelector": ".item", + "fields": [ + {"name": "title", "selector": "h2", "type": "text"}, + {"name": "description", "selector": ".desc", "type": "text"} + ] +} + +result = await crawler.arun( + url="https://example.com", + js_code="window.scrollTo(0, document.body.scrollHeight);", + wait_for="css:.item:nth-child(10)", # Wait for 10 items + extraction_strategy=JsonCssExtractionStrategy(schema) +) + +# Or use LLM to analyze dynamic content +class ContentAnalysis(BaseModel): + topics: List[str] + summary: str + +result = await crawler.arun( + url="https://example.com", + js_code="document.querySelector('.show-more').click();", + wait_for="css:.full-content", + extraction_strategy=LLMExtractionStrategy( + provider="ollama/nemotron", + schema=ContentAnalysis.schema(), + instruction="Analyze the full content" + ) +) +``` \ No newline at end of file diff --git a/docs/md_v2/basic/quickstart.md b/docs/md_v2/basic/quickstart.md new file mode 100644 index 00000000..f4904915 --- /dev/null +++ b/docs/md_v2/basic/quickstart.md @@ -0,0 +1,297 @@ +# Quick Start Guide πŸš€ + +Welcome to the Crawl4AI Quickstart Guide! In this tutorial, we'll walk you through the basic usage of Crawl4AI with a friendly and humorous tone. We'll cover everything from basic usage to advanced features like chunking and extraction strategies, all with the power of asynchronous programming. Let's dive in! 🌟 + +## Getting Started πŸ› οΈ + +First, let's import the necessary modules and create an instance of `AsyncWebCrawler`. We'll use an async context manager, which handles the setup and teardown of the crawler for us. + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # We'll add our crawling code here + pass + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### Basic Usage + +Simply provide a URL and let Crawl4AI do the magic! + +```python +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun(url="https://www.nbcnews.com/business") + print(f"Basic crawl result: {result.markdown[:500]}") # Print first 500 characters + +asyncio.run(main()) +``` + +### Taking Screenshots πŸ“Έ + +Capture screenshots of web pages easily: + +```python +async def capture_and_save_screenshot(url: str, output_path: str): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url=url, + screenshot=True, + bypass_cache=True + ) + + if result.success and result.screenshot: + import base64 + screenshot_data = base64.b64decode(result.screenshot) + with open(output_path, 'wb') as f: + f.write(screenshot_data) + print(f"Screenshot saved successfully to {output_path}") + else: + print("Failed to capture screenshot") +``` + +### Browser Selection 🌐 + +Crawl4AI supports multiple browser engines. Here's how to use different browsers: + +```python +# Use Firefox +async with AsyncWebCrawler(browser_type="firefox", verbose=True, headless=True) as crawler: + result = await crawler.arun(url="https://www.example.com", bypass_cache=True) + +# Use WebKit +async with AsyncWebCrawler(browser_type="webkit", verbose=True, headless=True) as crawler: + result = await crawler.arun(url="https://www.example.com", bypass_cache=True) + +# Use Chromium (default) +async with AsyncWebCrawler(verbose=True, headless=True) as crawler: + result = await crawler.arun(url="https://www.example.com", bypass_cache=True) +``` + +### User Simulation 🎭 + +Simulate real user behavior to avoid detection: + +```python +async with AsyncWebCrawler(verbose=True, headless=True) as crawler: + result = await crawler.arun( + url="YOUR-URL-HERE", + bypass_cache=True, + simulate_user=True, # Causes random mouse movements and clicks + override_navigator=True # Makes the browser appear more like a real user + ) +``` + +### Understanding Parameters 🧠 + +By default, Crawl4AI caches the results of your crawls. This means that subsequent crawls of the same URL will be much faster! Let's see this in action. + +```python +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # First crawl (caches the result) + result1 = await crawler.arun(url="https://www.nbcnews.com/business") + print(f"First crawl result: {result1.markdown[:100]}...") + + # Force to crawl again + result2 = await crawler.arun(url="https://www.nbcnews.com/business", bypass_cache=True) + print(f"Second crawl result: {result2.markdown[:100]}...") + +asyncio.run(main()) +``` + +### Adding a Chunking Strategy 🧩 + +Let's add a chunking strategy: `RegexChunking`! This strategy splits the text based on a given regex pattern. + +```python +from crawl4ai.chunking_strategy import RegexChunking + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + chunking_strategy=RegexChunking(patterns=["\n\n"]) + ) + print(f"RegexChunking result: {result.extracted_content[:200]}...") + +asyncio.run(main()) +``` + +### Using LLMExtractionStrategy with Different Providers πŸ€– + +Crawl4AI supports multiple LLM providers for extraction: + +```python +from crawl4ai.extraction_strategy import LLMExtractionStrategy +from pydantic import BaseModel, Field + +class OpenAIModelFee(BaseModel): + model_name: str = Field(..., description="Name of the OpenAI model.") + input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") + +# OpenAI +await extract_structured_data_using_llm("openai/gpt-4o", os.getenv("OPENAI_API_KEY")) + +# Hugging Face +await extract_structured_data_using_llm( + "huggingface/meta-llama/Meta-Llama-3.1-8B-Instruct", + os.getenv("HUGGINGFACE_API_KEY") +) + +# Ollama +await extract_structured_data_using_llm("ollama/llama3.2") + +# With custom headers +custom_headers = { + "Authorization": "Bearer your-custom-token", + "X-Custom-Header": "Some-Value" +} +await extract_structured_data_using_llm(extra_headers=custom_headers) +``` + +### Knowledge Graph Generation πŸ•ΈοΈ + +Generate knowledge graphs from web content: + +```python +from pydantic import BaseModel +from typing import List + +class Entity(BaseModel): + name: str + description: str + +class Relationship(BaseModel): + entity1: Entity + entity2: Entity + description: str + relation_type: str + +class KnowledgeGraph(BaseModel): + entities: List[Entity] + relationships: List[Relationship] + +extraction_strategy = LLMExtractionStrategy( + provider='openai/gpt-4o-mini', + api_token=os.getenv('OPENAI_API_KEY'), + schema=KnowledgeGraph.model_json_schema(), + extraction_type="schema", + instruction="Extract entities and relationships from the given text." +) + +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://paulgraham.com/love.html", + bypass_cache=True, + extraction_strategy=extraction_strategy + ) +``` + +### Advanced Session-Based Crawling with Dynamic Content πŸ”„ + +For modern web applications with dynamic content loading, here's how to handle pagination and content updates: + +```python +async def crawl_dynamic_content(): + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://github.com/microsoft/TypeScript/commits/main" + session_id = "typescript_commits_session" + + js_next_page = """ + const button = document.querySelector('a[data-testid="pagination-next-button"]'); + if (button) button.click(); + """ + + wait_for = """() => { + const commits = document.querySelectorAll('li.Box-sc-g0xbh4-0 h4'); + if (commits.length === 0) return false; + const firstCommit = commits[0].textContent.trim(); + return firstCommit !== window.firstCommit; + }""" + + schema = { + "name": "Commit Extractor", + "baseSelector": "li.Box-sc-g0xbh4-0", + "fields": [ + { + "name": "title", + "selector": "h4.markdown-title", + "type": "text", + "transform": "strip", + }, + ], + } + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + for page in range(3): # Crawl 3 pages + result = await crawler.arun( + url=url, + session_id=session_id, + css_selector="li.Box-sc-g0xbh4-0", + extraction_strategy=extraction_strategy, + js_code=js_next_page if page > 0 else None, + wait_for=wait_for if page > 0 else None, + js_only=page > 0, + bypass_cache=True, + headless=False, + ) + + await crawler.crawler_strategy.kill_session(session_id) +``` + +### Handling Overlays and Fitting Content πŸ“ + +Remove overlay elements and fit content appropriately: + +```python +async with AsyncWebCrawler(headless=False) as crawler: + result = await crawler.arun( + url="your-url-here", + bypass_cache=True, + word_count_threshold=10, + remove_overlay_elements=True, + screenshot=True + ) +``` + +## Performance Comparison 🏎️ + +Crawl4AI offers impressive performance compared to other solutions: + +```python +# Firecrawl comparison +from firecrawl import FirecrawlApp +app = FirecrawlApp(api_key=os.environ['FIRECRAWL_API_KEY']) +start = time.time() +scrape_status = app.scrape_url( + 'https://www.nbcnews.com/business', + params={'formats': ['markdown', 'html']} +) +end = time.time() + +# Crawl4AI comparison +async with AsyncWebCrawler() as crawler: + start = time.time() + result = await crawler.arun( + url="https://www.nbcnews.com/business", + word_count_threshold=0, + bypass_cache=True, + verbose=False, + ) + end = time.time() +``` + +Note: Performance comparisons should be conducted in environments with stable and fast internet connections for accurate results. + +## Congratulations! πŸŽ‰ + +You've made it through the updated Crawl4AI Quickstart Guide! Now you're equipped with even more powerful features to crawl the web asynchronously like a pro! πŸ•ΈοΈ + +Happy crawling! πŸš€ \ No newline at end of file diff --git a/docs/md_v2/basic/simple-crawling.md b/docs/md_v2/basic/simple-crawling.md new file mode 100644 index 00000000..097d5e61 --- /dev/null +++ b/docs/md_v2/basic/simple-crawling.md @@ -0,0 +1,120 @@ +# Simple Crawling + +This guide covers the basics of web crawling with Crawl4AI. You'll learn how to set up a crawler, make your first request, and understand the response. + +## Basic Usage + +Here's the simplest way to crawl a webpage: + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler() as crawler: + result = await crawler.arun(url="https://example.com") + print(result.markdown) # Print clean markdown content + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Understanding the Response + +The `arun()` method returns a `CrawlResult` object with several useful properties. Here's a quick overview (see [CrawlResult](../api/crawl-result.md) for complete details): + +```python +result = await crawler.arun(url="https://example.com") + +# Different content formats +print(result.html) # Raw HTML +print(result.cleaned_html) # Cleaned HTML +print(result.markdown) # Markdown version +print(result.fit_markdown) # Most relevant content in markdown + +# Check success status +print(result.success) # True if crawl succeeded +print(result.status_code) # HTTP status code (e.g., 200, 404) + +# Access extracted media and links +print(result.media) # Dictionary of found media (images, videos, audio) +print(result.links) # Dictionary of internal and external links +``` + +## Adding Basic Options + +Customize your crawl with these common options: + +```python +result = await crawler.arun( + url="https://example.com", + word_count_threshold=10, # Minimum words per content block + exclude_external_links=True, # Remove external links + remove_overlay_elements=True, # Remove popups/modals + process_iframes=True # Process iframe content +) +``` + +## Handling Errors + +Always check if the crawl was successful: + +```python +result = await crawler.arun(url="https://example.com") +if not result.success: + print(f"Crawl failed: {result.error_message}") + print(f"Status code: {result.status_code}") +``` + +## Logging and Debugging + +Enable verbose mode for detailed logging: + +```python +async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun(url="https://example.com") +``` + +## Complete Example + +Here's a more comprehensive example showing common usage patterns: + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://example.com", + # Content filtering + word_count_threshold=10, + excluded_tags=['form', 'header'], + exclude_external_links=True, + + # Content processing + process_iframes=True, + remove_overlay_elements=True, + + # Cache control + bypass_cache=False # Use cache if available + ) + + if result.success: + # Print clean content + print("Content:", result.markdown[:500]) # First 500 chars + + # Process images + for image in result.media["images"]: + print(f"Found image: {image['src']}") + + # Process links + for link in result.links["internal"]: + print(f"Internal link: {link['href']}") + + else: + print(f"Crawl failed: {result.error_message}") + +if __name__ == "__main__": + asyncio.run(main()) +``` diff --git a/docs/md_v2/extraction/chunking.md b/docs/md_v2/extraction/chunking.md new file mode 100644 index 00000000..f429310f --- /dev/null +++ b/docs/md_v2/extraction/chunking.md @@ -0,0 +1,133 @@ +## Chunking Strategies πŸ“š + +Crawl4AI provides several powerful chunking strategies to divide text into manageable parts for further processing. Each strategy has unique characteristics and is suitable for different scenarios. Let's explore them one by one. + +### RegexChunking + +`RegexChunking` splits text using regular expressions. This is ideal for creating chunks based on specific patterns like paragraphs or sentences. + +#### When to Use +- Great for structured text with consistent delimiters. +- Suitable for documents where specific patterns (e.g., double newlines, periods) indicate logical chunks. + +#### Parameters +- `patterns` (list, optional): Regular expressions used to split the text. Default is to split by double newlines (`['\n\n']`). + +#### Example +```python +from crawl4ai.chunking_strategy import RegexChunking + +# Define patterns for splitting text +patterns = [r'\n\n', r'\. '] +chunker = RegexChunking(patterns=patterns) + +# Sample text +text = "This is a sample text. It will be split into chunks.\n\nThis is another paragraph." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### NlpSentenceChunking + +`NlpSentenceChunking` uses NLP models to split text into sentences, ensuring accurate sentence boundaries. + +#### When to Use +- Ideal for texts where sentence boundaries are crucial. +- Useful for creating chunks that preserve grammatical structures. + +#### Parameters +- None. + +#### Example +```python +from crawl4ai.chunking_strategy import NlpSentenceChunking + +chunker = NlpSentenceChunking() + +# Sample text +text = "This is a sample text. It will be split into sentences. Here's another sentence." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### TopicSegmentationChunking + +`TopicSegmentationChunking` employs the TextTiling algorithm to segment text into topic-based chunks. This method identifies thematic boundaries. + +#### When to Use +- Perfect for long documents with distinct topics. +- Useful when preserving topic continuity is more important than maintaining text order. + +#### Parameters +- `num_keywords` (int, optional): Number of keywords for each topic segment. Default is `3`. + +#### Example +```python +from crawl4ai.chunking_strategy import TopicSegmentationChunking + +chunker = TopicSegmentationChunking(num_keywords=3) + +# Sample text +text = "This document contains several topics. Topic one discusses AI. Topic two covers machine learning." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### FixedLengthWordChunking + +`FixedLengthWordChunking` splits text into chunks based on a fixed number of words. This ensures each chunk has approximately the same length. + +#### When to Use +- Suitable for processing large texts where uniform chunk size is important. +- Useful when the number of words per chunk needs to be controlled. + +#### Parameters +- `chunk_size` (int, optional): Number of words per chunk. Default is `100`. + +#### Example +```python +from crawl4ai.chunking_strategy import FixedLengthWordChunking + +chunker = FixedLengthWordChunking(chunk_size=10) + +# Sample text +text = "This is a sample text. It will be split into chunks of fixed length." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +### SlidingWindowChunking + +`SlidingWindowChunking` uses a sliding window approach to create overlapping chunks. Each chunk has a fixed length, and the window slides by a specified step size. + +#### When to Use +- Ideal for creating overlapping chunks to preserve context. +- Useful for tasks where context from adjacent chunks is needed. + +#### Parameters +- `window_size` (int, optional): Number of words in each chunk. Default is `100`. +- `step` (int, optional): Number of words to slide the window. Default is `50`. + +#### Example +```python +from crawl4ai.chunking_strategy import SlidingWindowChunking + +chunker = SlidingWindowChunking(window_size=10, step=5) + +# Sample text +text = "This is a sample text. It will be split using a sliding window approach to preserve context." + +# Chunk the text +chunks = chunker.chunk(text) +print(chunks) +``` + +With these chunking strategies, you can choose the best method to divide your text based on your specific needs. Whether you need precise sentence boundaries, topic-based segmentation, or uniform chunk sizes, Crawl4AI has you covered. Happy chunking! πŸ“βœ¨ diff --git a/docs/md_v2/extraction/cosine.md b/docs/md_v2/extraction/cosine.md new file mode 100644 index 00000000..9ce49e40 --- /dev/null +++ b/docs/md_v2/extraction/cosine.md @@ -0,0 +1,222 @@ +# Cosine Strategy + +The Cosine Strategy in Crawl4AI uses similarity-based clustering to identify and extract relevant content sections from web pages. This strategy is particularly useful when you need to find and extract content based on semantic similarity rather than structural patterns. + +## How It Works + +The Cosine Strategy: +1. Breaks down page content into meaningful chunks +2. Converts text into vector representations +3. Calculates similarity between chunks +4. Clusters similar content together +5. Ranks and filters content based on relevance + +## Basic Usage + +```python +from crawl4ai.extraction_strategy import CosineStrategy + +strategy = CosineStrategy( + semantic_filter="product reviews", # Target content type + word_count_threshold=10, # Minimum words per cluster + sim_threshold=0.3 # Similarity threshold +) + +async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url="https://example.com/reviews", + extraction_strategy=strategy + ) + + content = result.extracted_content +``` + +## Configuration Options + +### Core Parameters + +```python +CosineStrategy( + # Content Filtering + semantic_filter: str = None, # Keywords/topic for content filtering + word_count_threshold: int = 10, # Minimum words per cluster + sim_threshold: float = 0.3, # Similarity threshold (0.0 to 1.0) + + # Clustering Parameters + max_dist: float = 0.2, # Maximum distance for clustering + linkage_method: str = 'ward', # Clustering linkage method + top_k: int = 3, # Number of top categories to extract + + # Model Configuration + model_name: str = 'sentence-transformers/all-MiniLM-L6-v2', # Embedding model + + verbose: bool = False # Enable logging +) +``` + +### Parameter Details + +1. **semantic_filter** + - Sets the target topic or content type + - Use keywords relevant to your desired content + - Example: "technical specifications", "user reviews", "pricing information" + +2. **sim_threshold** + - Controls how similar content must be to be grouped together + - Higher values (e.g., 0.8) mean stricter matching + - Lower values (e.g., 0.3) allow more variation + ```python + # Strict matching + strategy = CosineStrategy(sim_threshold=0.8) + + # Loose matching + strategy = CosineStrategy(sim_threshold=0.3) + ``` + +3. **word_count_threshold** + - Filters out short content blocks + - Helps eliminate noise and irrelevant content + ```python + # Only consider substantial paragraphs + strategy = CosineStrategy(word_count_threshold=50) + ``` + +4. **top_k** + - Number of top content clusters to return + - Higher values return more diverse content + ```python + # Get top 5 most relevant content clusters + strategy = CosineStrategy(top_k=5) + ``` + +## Use Cases + +### 1. Article Content Extraction +```python +strategy = CosineStrategy( + semantic_filter="main article content", + word_count_threshold=100, # Longer blocks for articles + top_k=1 # Usually want single main content +) + +result = await crawler.arun( + url="https://example.com/blog/post", + extraction_strategy=strategy +) +``` + +### 2. Product Review Analysis +```python +strategy = CosineStrategy( + semantic_filter="customer reviews and ratings", + word_count_threshold=20, # Reviews can be shorter + top_k=10, # Get multiple reviews + sim_threshold=0.4 # Allow variety in review content +) +``` + +### 3. Technical Documentation +```python +strategy = CosineStrategy( + semantic_filter="technical specifications documentation", + word_count_threshold=30, + sim_threshold=0.6, # Stricter matching for technical content + max_dist=0.3 # Allow related technical sections +) +``` + +## Advanced Features + +### Custom Clustering +```python +strategy = CosineStrategy( + linkage_method='complete', # Alternative clustering method + max_dist=0.4, # Larger clusters + model_name='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' # Multilingual support +) +``` + +### Content Filtering Pipeline +```python +strategy = CosineStrategy( + semantic_filter="pricing plans features", + word_count_threshold=15, + sim_threshold=0.5, + top_k=3 +) + +async def extract_pricing_features(url: str): + async with AsyncWebCrawler() as crawler: + result = await crawler.arun( + url=url, + extraction_strategy=strategy + ) + + if result.success: + content = json.loads(result.extracted_content) + return { + 'pricing_features': content, + 'clusters': len(content), + 'similarity_scores': [item['score'] for item in content] + } +``` + +## Best Practices + +1. **Adjust Thresholds Iteratively** + - Start with default values + - Adjust based on results + - Monitor clustering quality + +2. **Choose Appropriate Word Count Thresholds** + - Higher for articles (100+) + - Lower for reviews/comments (20+) + - Medium for product descriptions (50+) + +3. **Optimize Performance** + ```python + strategy = CosineStrategy( + word_count_threshold=10, # Filter early + top_k=5, # Limit results + verbose=True # Monitor performance + ) + ``` + +4. **Handle Different Content Types** + ```python + # For mixed content pages + strategy = CosineStrategy( + semantic_filter="product features", + sim_threshold=0.4, # More flexible matching + max_dist=0.3, # Larger clusters + top_k=3 # Multiple relevant sections + ) + ``` + +## Error Handling + +```python +try: + result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy + ) + + if result.success: + content = json.loads(result.extracted_content) + if not content: + print("No relevant content found") + else: + print(f"Extraction failed: {result.error_message}") + +except Exception as e: + print(f"Error during extraction: {str(e)}") +``` + +The Cosine Strategy is particularly effective when: +- Content structure is inconsistent +- You need semantic understanding +- You want to find similar content blocks +- Structure-based extraction (CSS/XPath) isn't reliable + +It works well with other strategies and can be used as a pre-processing step for LLM-based extraction. \ No newline at end of file diff --git a/docs/md_v2/extraction/css-advanced.md b/docs/md_v2/extraction/css-advanced.md new file mode 100644 index 00000000..393b79a5 --- /dev/null +++ b/docs/md_v2/extraction/css-advanced.md @@ -0,0 +1,282 @@ +# Advanced Usage of JsonCssExtractionStrategy + +While the basic usage of JsonCssExtractionStrategy is powerful for simple structures, its true potential shines when dealing with complex, nested HTML structures. This section will explore advanced usage scenarios, demonstrating how to extract nested objects, lists, and nested lists. + +## Hypothetical Website Example + +Let's consider a hypothetical e-commerce website that displays product categories, each containing multiple products. Each product has details, reviews, and related items. This complex structure will allow us to demonstrate various advanced features of JsonCssExtractionStrategy. + +Assume the HTML structure looks something like this: + +```html +

    +

    Electronics

    +
    +

    Smartphone X

    +

    $999

    +
    + TechCorp + X-2000 +
    +
      +
    • 5G capable
    • +
    • 6.5" OLED screen
    • +
    • 128GB storage
    • +
    +
    +
    + John D. + 4.5 +

    Great phone, love the camera!

    +
    +
    + Jane S. + 5 +

    Best smartphone I've ever owned.

    +
    +
    + +
    + +
    +``` + +Now, let's create a schema to extract this complex structure: + +```python +schema = { + "name": "E-commerce Product Catalog", + "baseSelector": "div.category", + "fields": [ + { + "name": "category_name", + "selector": "h2.category-name", + "type": "text" + }, + { + "name": "products", + "selector": "div.product", + "type": "nested_list", + "fields": [ + { + "name": "name", + "selector": "h3.product-name", + "type": "text" + }, + { + "name": "price", + "selector": "p.product-price", + "type": "text" + }, + { + "name": "details", + "selector": "div.product-details", + "type": "nested", + "fields": [ + { + "name": "brand", + "selector": "span.brand", + "type": "text" + }, + { + "name": "model", + "selector": "span.model", + "type": "text" + } + ] + }, + { + "name": "features", + "selector": "ul.product-features li", + "type": "list", + "fields": [ + { + "name": "feature", + "type": "text" + } + ] + }, + { + "name": "reviews", + "selector": "div.review", + "type": "nested_list", + "fields": [ + { + "name": "reviewer", + "selector": "span.reviewer", + "type": "text" + }, + { + "name": "rating", + "selector": "span.rating", + "type": "text" + }, + { + "name": "comment", + "selector": "p.review-text", + "type": "text" + } + ] + }, + { + "name": "related_products", + "selector": "ul.related-products li", + "type": "list", + "fields": [ + { + "name": "name", + "selector": "span.related-name", + "type": "text" + }, + { + "name": "price", + "selector": "span.related-price", + "type": "text" + } + ] + } + ] + } + ] +} +``` + +This schema demonstrates several advanced features: + +1. **Nested Objects**: The `details` field is a nested object within each product. +2. **Simple Lists**: The `features` field is a simple list of text items. +3. **Nested Lists**: The `products` field is a nested list, where each item is a complex object. +4. **Lists of Objects**: The `reviews` and `related_products` fields are lists of objects. + +Let's break down the key concepts: + +### Nested Objects + +To create a nested object, use `"type": "nested"` and provide a `fields` array for the nested structure: + +```python +{ + "name": "details", + "selector": "div.product-details", + "type": "nested", + "fields": [ + { + "name": "brand", + "selector": "span.brand", + "type": "text" + }, + { + "name": "model", + "selector": "span.model", + "type": "text" + } + ] +} +``` + +### Simple Lists + +For a simple list of identical items, use `"type": "list"`: + +```python +{ + "name": "features", + "selector": "ul.product-features li", + "type": "list", + "fields": [ + { + "name": "feature", + "type": "text" + } + ] +} +``` + +### Nested Lists + +For a list of complex objects, use `"type": "nested_list"`: + +```python +{ + "name": "products", + "selector": "div.product", + "type": "nested_list", + "fields": [ + // ... fields for each product + ] +} +``` + +### Lists of Objects + +Similar to nested lists, but typically used for simpler objects within the list: + +```python +{ + "name": "related_products", + "selector": "ul.related-products li", + "type": "list", + "fields": [ + { + "name": "name", + "selector": "span.related-name", + "type": "text" + }, + { + "name": "price", + "selector": "span.related-price", + "type": "text" + } + ] +} +``` + +## Using the Advanced Schema + +To use this advanced schema with AsyncWebCrawler: + +```python +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +async def extract_complex_product_data(): + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://gist.githubusercontent.com/githubusercontent/2d7b8ba3cd8ab6cf3c8da771ddb36878/raw/1ae2f90c6861ce7dd84cc50d3df9920dee5e1fd2/sample_ecommerce.html", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + product_data = json.loads(result.extracted_content) + print(json.dumps(product_data, indent=2)) + +asyncio.run(extract_complex_product_data()) +``` + +This will produce a structured JSON output that captures the complex hierarchy of the product catalog, including nested objects, lists, and nested lists. + +## Tips for Advanced Usage + +1. **Start Simple**: Begin with a basic schema and gradually add complexity. +2. **Test Incrementally**: Test each part of your schema separately before combining them. +3. **Use Chrome DevTools**: The Element Inspector is invaluable for identifying the correct selectors. +4. **Handle Missing Data**: Use the `default` key in your field definitions to handle cases where data might be missing. +5. **Leverage Transforms**: Use the `transform` key to clean or format extracted data (e.g., converting prices to numbers). +6. **Consider Performance**: Very complex schemas might slow down extraction. Balance complexity with performance needs. + +By mastering these advanced techniques, you can use JsonCssExtractionStrategy to extract highly structured data from even the most complex web pages, making it a powerful tool for web scraping and data analysis tasks. \ No newline at end of file diff --git a/docs/md_v2/extraction/css.md b/docs/md_v2/extraction/css.md new file mode 100644 index 00000000..3b5075a6 --- /dev/null +++ b/docs/md_v2/extraction/css.md @@ -0,0 +1,142 @@ +# JSON CSS Extraction Strategy with AsyncWebCrawler + +The `JsonCssExtractionStrategy` is a powerful feature of Crawl4AI that allows you to extract structured data from web pages using CSS selectors. This method is particularly useful when you need to extract specific data points from a consistent HTML structure, such as tables or repeated elements. Here's how to use it with the AsyncWebCrawler. + +## Overview + +The `JsonCssExtractionStrategy` works by defining a schema that specifies: +1. A base CSS selector for the repeating elements +2. Fields to extract from each element, each with its own CSS selector + +This strategy is fast and efficient, as it doesn't rely on external services like LLMs for extraction. + +## Example: Extracting Cryptocurrency Prices from Coinbase + +Let's look at an example that extracts cryptocurrency prices from the Coinbase explore page. + +```python +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +async def extract_structured_data_using_css_extractor(): + print("\n--- Using JsonCssExtractionStrategy for Fast Structured Output ---") + + # Define the extraction schema + schema = { + "name": "Coinbase Crypto Prices", + "baseSelector": ".cds-tableRow-t45thuk", + "fields": [ + { + "name": "crypto", + "selector": "td:nth-child(1) h2", + "type": "text", + }, + { + "name": "symbol", + "selector": "td:nth-child(1) p", + "type": "text", + }, + { + "name": "price", + "selector": "td:nth-child(2)", + "type": "text", + } + ], + } + + # Create the extraction strategy + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + # Use the AsyncWebCrawler with the extraction strategy + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.coinbase.com/explore", + extraction_strategy=extraction_strategy, + bypass_cache=True, + ) + + assert result.success, "Failed to crawl the page" + + # Parse the extracted content + crypto_prices = json.loads(result.extracted_content) + print(f"Successfully extracted {len(crypto_prices)} cryptocurrency prices") + print(json.dumps(crypto_prices[0], indent=2)) + + return crypto_prices + +# Run the async function +asyncio.run(extract_structured_data_using_css_extractor()) +``` + +## Explanation of the Schema + +The schema defines how to extract the data: + +- `name`: A descriptive name for the extraction task. +- `baseSelector`: The CSS selector for the repeating elements (in this case, table rows). +- `fields`: An array of fields to extract from each element: + - `name`: The name to give the extracted data. + - `selector`: The CSS selector to find the specific data within the base element. + - `type`: The type of data to extract (usually "text" for textual content). + +## Advantages of JsonCssExtractionStrategy + +1. **Speed**: CSS selectors are fast to execute, making this method efficient for large datasets. +2. **Precision**: You can target exactly the elements you need. +3. **Structured Output**: The result is already structured as JSON, ready for further processing. +4. **No External Dependencies**: Unlike LLM-based strategies, this doesn't require any API calls to external services. + +## Tips for Using JsonCssExtractionStrategy + +1. **Inspect the Page**: Use browser developer tools to identify the correct CSS selectors. +2. **Test Selectors**: Verify your selectors in the browser console before using them in the script. +3. **Handle Dynamic Content**: If the page uses JavaScript to load content, you may need to combine this with JS execution (see the Advanced Usage section). +4. **Error Handling**: Always check the `result.success` flag and handle potential failures. + +## Advanced Usage: Combining with JavaScript Execution + +For pages that load data dynamically, you can combine the `JsonCssExtractionStrategy` with JavaScript execution: + +```python +async def extract_dynamic_structured_data(): + schema = { + "name": "Dynamic Crypto Prices", + "baseSelector": ".crypto-row", + "fields": [ + {"name": "name", "selector": ".crypto-name", "type": "text"}, + {"name": "price", "selector": ".crypto-price", "type": "text"}, + ] + } + + js_code = """ + window.scrollTo(0, document.body.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for 2 seconds + """ + + extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://example.com/crypto-prices", + extraction_strategy=extraction_strategy, + js_code=js_code, + wait_for=".crypto-row:nth-child(20)", # Wait for 20 rows to load + bypass_cache=True, + ) + + crypto_data = json.loads(result.extracted_content) + print(f"Extracted {len(crypto_data)} cryptocurrency entries") + +asyncio.run(extract_dynamic_structured_data()) +``` + +This advanced example demonstrates how to: +1. Execute JavaScript to trigger dynamic content loading. +2. Wait for a specific condition (20 rows loaded) before extraction. +3. Extract data from the dynamically loaded content. + +By mastering the `JsonCssExtractionStrategy`, you can efficiently extract structured data from a wide variety of web pages, making it a valuable tool in your web scraping toolkit. + +For more details on schema definitions and advanced extraction strategies, check out the[Advanced JsonCssExtraction](./css-advanced.md). \ No newline at end of file diff --git a/docs/md_v2/extraction/extraction_strategies.md b/docs/md_v2/extraction/extraction_strategies.md new file mode 100644 index 00000000..2b29a081 --- /dev/null +++ b/docs/md_v2/extraction/extraction_strategies.md @@ -0,0 +1,185 @@ +## Extraction Strategies 🧠 + +Crawl4AI offers powerful extraction strategies to derive meaningful information from web content. Let's dive into three of the most important strategies: `CosineStrategy`, `LLMExtractionStrategy`, and the new `JsonCssExtractionStrategy`. + +### LLMExtractionStrategy + +`LLMExtractionStrategy` leverages a Language Model (LLM) to extract meaningful content from HTML. This strategy uses an external provider for LLM completions to perform extraction based on instructions. + +#### When to Use +- Suitable for complex extraction tasks requiring nuanced understanding. +- Ideal for scenarios where detailed instructions can guide the extraction process. +- Perfect for extracting specific types of information or content with precise guidelines. + +#### Parameters +- `provider` (str, optional): Provider for language model completions (e.g., openai/gpt-4). Default is `DEFAULT_PROVIDER`. +- `api_token` (str, optional): API token for the provider. If not provided, it will try to load from the environment variable `OPENAI_API_KEY`. +- `instruction` (str, optional): Instructions to guide the LLM on how to perform the extraction. Default is `None`. + +#### Example Without Instructions +```python +import asyncio +import os +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # Define extraction strategy without instructions + strategy = LLMExtractionStrategy( + provider='openai', + api_token=os.getenv('OPENAI_API_KEY') + ) + + # Sample URL + url = "https://www.nbcnews.com/business" + + # Run the crawler with the extraction strategy + result = await crawler.arun(url=url, extraction_strategy=strategy) + print(result.extracted_content) + +asyncio.run(main()) +``` + +#### Example With Instructions +```python +import asyncio +import os +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # Define extraction strategy with instructions + strategy = LLMExtractionStrategy( + provider='openai', + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract only financial news and summarize key points." + ) + + # Sample URL + url = "https://www.nbcnews.com/business" + + # Run the crawler with the extraction strategy + result = await crawler.arun(url=url, extraction_strategy=strategy) + print(result.extracted_content) + +asyncio.run(main()) +``` + +### JsonCssExtractionStrategy + +`JsonCssExtractionStrategy` is a powerful tool for extracting structured data from HTML using CSS selectors. It allows you to define a schema that maps CSS selectors to specific fields, enabling precise and efficient data extraction. + +#### When to Use +- Ideal for extracting structured data from websites with consistent HTML structures. +- Perfect for scenarios where you need to extract specific elements or attributes from a webpage. +- Suitable for creating datasets from web pages with tabular or list-based information. + +#### Parameters +- `schema` (Dict[str, Any]): A dictionary defining the extraction schema, including base selector and field definitions. + +#### Example +```python +import asyncio +import json +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # Define the extraction schema + schema = { + "name": "News Articles", + "baseSelector": "article.tease-card", + "fields": [ + { + "name": "title", + "selector": "h2", + "type": "text", + }, + { + "name": "summary", + "selector": "div.tease-card__info", + "type": "text", + }, + { + "name": "link", + "selector": "a", + "type": "attribute", + "attribute": "href" + } + ], + } + + # Create the extraction strategy + strategy = JsonCssExtractionStrategy(schema, verbose=True) + + # Sample URL + url = "https://www.nbcnews.com/business" + + # Run the crawler with the extraction strategy + result = await crawler.arun(url=url, extraction_strategy=strategy) + + # Parse and print the extracted content + extracted_data = json.loads(result.extracted_content) + print(json.dumps(extracted_data, indent=2)) + +asyncio.run(main()) +``` + +#### Use Cases for JsonCssExtractionStrategy +- Extracting product information from e-commerce websites. +- Gathering news articles and their metadata from news portals. +- Collecting user reviews and ratings from review websites. +- Extracting job listings from job boards. + +By choosing the right extraction strategy, you can effectively extract the most relevant and useful information from web content. Whether you need fast, accurate semantic segmentation with `CosineStrategy`, nuanced, instruction-based extraction with `LLMExtractionStrategy`, or precise structured data extraction with `JsonCssExtractionStrategy`, Crawl4AI has you covered. Happy extracting! πŸ•΅οΈβ€β™‚οΈβœ¨ + +For more details on schema definitions and advanced extraction strategies, check out the[Advanced JsonCssExtraction](./css-advanced.md). + + +### CosineStrategy + +`CosineStrategy` uses hierarchical clustering based on cosine similarity to group text chunks into meaningful clusters. This method converts each chunk into its embedding and then clusters them to form semantical chunks. + +#### When to Use +- Ideal for fast, accurate semantic segmentation of text. +- Perfect for scenarios where LLMs might be overkill or too slow. +- Suitable for narrowing down content based on specific queries or keywords. + +#### Parameters +- `semantic_filter` (str, optional): Keywords for filtering relevant documents before clustering. Documents are filtered based on their cosine similarity to the keyword filter embedding. Default is `None`. +- `word_count_threshold` (int, optional): Minimum number of words per cluster. Default is `20`. +- `max_dist` (float, optional): Maximum cophenetic distance on the dendrogram to form clusters. Default is `0.2`. +- `linkage_method` (str, optional): Linkage method for hierarchical clustering. Default is `'ward'`. +- `top_k` (int, optional): Number of top categories to extract. Default is `3`. +- `model_name` (str, optional): Model name for embedding generation. Default is `'BAAI/bge-small-en-v1.5'`. + +#### Example +```python +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import CosineStrategy + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + # Define extraction strategy + strategy = CosineStrategy( + semantic_filter="finance economy stock market", + word_count_threshold=10, + max_dist=0.2, + linkage_method='ward', + top_k=3, + model_name='BAAI/bge-small-en-v1.5' + ) + + # Sample URL + url = "https://www.nbcnews.com/business" + + # Run the crawler with the extraction strategy + result = await crawler.arun(url=url, extraction_strategy=strategy) + print(result.extracted_content) + +asyncio.run(main()) +``` diff --git a/docs/md_v2/extraction/llm.md b/docs/md_v2/extraction/llm.md new file mode 100644 index 00000000..ca562147 --- /dev/null +++ b/docs/md_v2/extraction/llm.md @@ -0,0 +1,179 @@ +# LLM Extraction with AsyncWebCrawler + +Crawl4AI's AsyncWebCrawler allows you to use Language Models (LLMs) to extract structured data or relevant content from web pages asynchronously. Below are two examples demonstrating how to use `LLMExtractionStrategy` for different purposes with the AsyncWebCrawler. + +## Example 1: Extract Structured Data + +In this example, we use the `LLMExtractionStrategy` to extract structured data (model names and their fees) from the OpenAI pricing page. + +```python +import os +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy +from pydantic import BaseModel, Field + +class OpenAIModelFee(BaseModel): + model_name: str = Field(..., description="Name of the OpenAI model.") + input_fee: str = Field(..., description="Fee for input token for the OpenAI model.") + output_fee: str = Field(..., description="Fee for output token for the OpenAI model.") + +async def extract_openai_fees(): + url = 'https://openai.com/api/pricing/' + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url=url, + word_count_threshold=1, + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", # Or use ollama like provider="ollama/nemotron" + api_token=os.getenv('OPENAI_API_KEY'), + schema=OpenAIModelFee.model_json_schema(), + extraction_type="schema", + instruction="From the crawled content, extract all mentioned model names along with their " + "fees for input and output tokens. Make sure not to miss anything in the entire content. " + 'One extracted model JSON format should look like this: ' + '{ "model_name": "GPT-4", "input_fee": "US$10.00 / 1M tokens", "output_fee": "US$30.00 / 1M tokens" }' + ), + bypass_cache=True, + ) + + model_fees = json.loads(result.extracted_content) + print(f"Number of models extracted: {len(model_fees)}") + + with open(".data/openai_fees.json", "w", encoding="utf-8") as f: + json.dump(model_fees, f, indent=2) + +asyncio.run(extract_openai_fees()) +``` + +## Example 2: Extract Relevant Content + +In this example, we instruct the LLM to extract only content related to technology from the NBC News business page. + +```python +import os +import json +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +async def extract_tech_content(): + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract only content related to technology" + ), + bypass_cache=True, + ) + + tech_content = json.loads(result.extracted_content) + print(f"Number of tech-related items extracted: {len(tech_content)}") + + with open(".data/tech_content.json", "w", encoding="utf-8") as f: + json.dump(tech_content, f, indent=2) + +asyncio.run(extract_tech_content()) +``` + +## Advanced Usage: Combining JS Execution with LLM Extraction + +This example demonstrates how to combine JavaScript execution with LLM extraction to handle dynamic content: + +```python +async def extract_dynamic_content(): + js_code = """ + const loadMoreButton = Array.from(document.querySelectorAll('button')).find(button => button.textContent.includes('Load More')); + if (loadMoreButton) { + loadMoreButton.click(); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + """ + + wait_for = """ + () => { + const articles = document.querySelectorAll('article.tease-card'); + return articles.length > 10; + } + """ + + async with AsyncWebCrawler(verbose=True) as crawler: + result = await crawler.arun( + url="https://www.nbcnews.com/business", + js_code=js_code, + wait_for=wait_for, + css_selector="article.tease-card", + extraction_strategy=LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Summarize each article, focusing on technology-related content" + ), + bypass_cache=True, + ) + + summaries = json.loads(result.extracted_content) + print(f"Number of summarized articles: {len(summaries)}") + + with open(".data/tech_summaries.json", "w", encoding="utf-8") as f: + json.dump(summaries, f, indent=2) + +asyncio.run(extract_dynamic_content()) +``` + +## Customizing LLM Provider + +Crawl4AI uses the `litellm` library under the hood, which allows you to use any LLM provider you want. Just pass the correct model name and API token: + +```python +extraction_strategy=LLMExtractionStrategy( + provider="your_llm_provider/model_name", + api_token="your_api_token", + instruction="Your extraction instruction" +) +``` + +This flexibility allows you to integrate with various LLM providers and tailor the extraction process to your specific needs. + +## Error Handling and Retries + +When working with external LLM APIs, it's important to handle potential errors and implement retry logic. Here's an example of how you might do this: + +```python +import asyncio +from tenacity import retry, stop_after_attempt, wait_exponential + +class LLMExtractionError(Exception): + pass + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) +async def extract_with_retry(crawler, url, extraction_strategy): + try: + result = await crawler.arun(url=url, extraction_strategy=extraction_strategy, bypass_cache=True) + return json.loads(result.extracted_content) + except Exception as e: + raise LLMExtractionError(f"Failed to extract content: {str(e)}") + +async def main(): + async with AsyncWebCrawler(verbose=True) as crawler: + try: + content = await extract_with_retry( + crawler, + "https://www.example.com", + LLMExtractionStrategy( + provider="openai/gpt-4o", + api_token=os.getenv('OPENAI_API_KEY'), + instruction="Extract and summarize main points" + ) + ) + print("Extracted content:", content) + except LLMExtractionError as e: + print(f"Extraction failed after retries: {e}") + +asyncio.run(main()) +``` + +This example uses the `tenacity` library to implement a retry mechanism with exponential backoff, which can help handle temporary failures or rate limiting from the LLM API. \ No newline at end of file diff --git a/docs/md_v2/extraction/overview.md b/docs/md_v2/extraction/overview.md new file mode 100644 index 00000000..53a8b87d --- /dev/null +++ b/docs/md_v2/extraction/overview.md @@ -0,0 +1,197 @@ +# Extraction Strategies Overview + +Crawl4AI provides powerful extraction strategies to help you get structured data from web pages. Each strategy is designed for specific use cases and offers different approaches to data extraction. + +## Available Strategies + +### [LLM-Based Extraction](llm.md) + +`LLMExtractionStrategy` uses Language Models to extract structured data from web content. This approach is highly flexible and can understand content semantically. + +```python +from pydantic import BaseModel +from crawl4ai.extraction_strategy import LLMExtractionStrategy + +class Product(BaseModel): + name: str + price: float + description: str + +strategy = LLMExtractionStrategy( + provider="ollama/llama2", + schema=Product.schema(), + instruction="Extract product details from the page" +) + +result = await crawler.arun( + url="https://example.com/product", + extraction_strategy=strategy +) +``` + +**Best for:** +- Complex data structures +- Content requiring interpretation +- Flexible content formats +- Natural language processing + +### [CSS-Based Extraction](css.md) + +`JsonCssExtractionStrategy` extracts data using CSS selectors. This is fast, reliable, and perfect for consistently structured pages. + +```python +from crawl4ai.extraction_strategy import JsonCssExtractionStrategy + +schema = { + "name": "Product Listing", + "baseSelector": ".product-card", + "fields": [ + {"name": "title", "selector": "h2", "type": "text"}, + {"name": "price", "selector": ".price", "type": "text"}, + {"name": "image", "selector": "img", "type": "attribute", "attribute": "src"} + ] +} + +strategy = JsonCssExtractionStrategy(schema) + +result = await crawler.arun( + url="https://example.com/products", + extraction_strategy=strategy +) +``` + +**Best for:** +- E-commerce product listings +- News article collections +- Structured content pages +- High-performance needs + +### [Cosine Strategy](cosine.md) + +`CosineStrategy` uses similarity-based clustering to identify and extract relevant content sections. + +```python +from crawl4ai.extraction_strategy import CosineStrategy + +strategy = CosineStrategy( + semantic_filter="product reviews", # Content focus + word_count_threshold=10, # Minimum words per cluster + sim_threshold=0.3, # Similarity threshold + max_dist=0.2, # Maximum cluster distance + top_k=3 # Number of top clusters to extract +) + +result = await crawler.arun( + url="https://example.com/reviews", + extraction_strategy=strategy +) +``` + +**Best for:** +- Content similarity analysis +- Topic clustering +- Relevant content extraction +- Pattern recognition in text + +## Strategy Selection Guide + +Choose your strategy based on these factors: + +1. **Content Structure** + - Well-structured HTML β†’ Use CSS Strategy + - Natural language text β†’ Use LLM Strategy + - Mixed/Complex content β†’ Use Cosine Strategy + +2. **Performance Requirements** + - Fastest: CSS Strategy + - Moderate: Cosine Strategy + - Variable: LLM Strategy (depends on provider) + +3. **Accuracy Needs** + - Highest structure accuracy: CSS Strategy + - Best semantic understanding: LLM Strategy + - Best content relevance: Cosine Strategy + +## Combining Strategies + +You can combine strategies for more powerful extraction: + +```python +# First use CSS strategy for initial structure +css_result = await crawler.arun( + url="https://example.com", + extraction_strategy=css_strategy +) + +# Then use LLM for semantic analysis +llm_result = await crawler.arun( + url="https://example.com", + extraction_strategy=llm_strategy +) +``` + +## Common Use Cases + +1. **E-commerce Scraping** + ```python + # CSS Strategy for product listings + schema = { + "name": "Products", + "baseSelector": ".product", + "fields": [ + {"name": "name", "selector": ".title", "type": "text"}, + {"name": "price", "selector": ".price", "type": "text"} + ] + } + ``` + +2. **News Article Extraction** + ```python + # LLM Strategy for article content + class Article(BaseModel): + title: str + content: str + author: str + date: str + + strategy = LLMExtractionStrategy( + provider="ollama/llama2", + schema=Article.schema() + ) + ``` + +3. **Content Analysis** + ```python + # Cosine Strategy for topic analysis + strategy = CosineStrategy( + semantic_filter="technology trends", + top_k=5 + ) + ``` + +## Best Practices + +1. **Choose the Right Strategy** + - Start with CSS for structured data + - Use LLM for complex interpretation + - Try Cosine for content relevance + +2. **Optimize Performance** + - Cache LLM results + - Keep CSS selectors specific + - Tune similarity thresholds + +3. **Handle Errors** + ```python + result = await crawler.arun( + url="https://example.com", + extraction_strategy=strategy + ) + + if not result.success: + print(f"Extraction failed: {result.error_message}") + else: + data = json.loads(result.extracted_content) + ``` + +Each strategy has its strengths and optimal use cases. Explore the detailed documentation for each strategy to learn more about their specific features and configurations. \ No newline at end of file diff --git a/docs/md_v2/index.md b/docs/md_v2/index.md new file mode 100644 index 00000000..7061aea5 --- /dev/null +++ b/docs/md_v2/index.md @@ -0,0 +1,113 @@ +# Crawl4AI + +Welcome to the official documentation for Crawl4AI! πŸ•·οΈπŸ€– Crawl4AI is an open-source Python library designed to simplify web crawling and extract useful information from web pages. This documentation will guide you through the features, usage, and customization of Crawl4AI. + +## Introduction + +Crawl4AI has one clear task: to make crawling and data extraction from web pages easy and efficient, especially for large language models (LLMs) and AI applications. Whether you are using it as a REST API or a Python library, Crawl4AI offers a robust and flexible solution with full asynchronous support. + +## Quick Start + +Here's a quick example to show you how easy it is to use Crawl4AI with its asynchronous capabilities: + +```python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + # Create an instance of AsyncWebCrawler + async with AsyncWebCrawler(verbose=True) as crawler: + # Run the crawler on a URL + result = await crawler.arun(url="https://www.nbcnews.com/business") + + # Print the extracted content + print(result.markdown) + +# Run the async main function +asyncio.run(main()) +``` + +## Key Features ✨ + +- πŸ†“ Completely free and open-source +- πŸš€ Blazing fast performance, outperforming many paid services +- πŸ€– LLM-friendly output formats (JSON, cleaned HTML, markdown) +- πŸ“„ Fit markdown generation for extracting main article content. +- 🌐 Multi-browser support (Chromium, Firefox, WebKit) +- 🌍 Supports crawling multiple URLs simultaneously +- 🎨 Extracts and returns all media tags (Images, Audio, and Video) +- πŸ”— Extracts all external and internal links +- πŸ“š Extracts metadata from the page +- πŸ”„ Custom hooks for authentication, headers, and page modifications +- πŸ•΅οΈ User-agent customization +- πŸ–ΌοΈ Takes screenshots of pages with enhanced error handling +- πŸ“œ Executes multiple custom JavaScripts before crawling +- πŸ“Š Generates structured output without LLM using JsonCssExtractionStrategy +- πŸ“š Various chunking strategies: topic-based, regex, sentence, and more +- 🧠 Advanced extraction strategies: cosine clustering, LLM, and more +- 🎯 CSS selector support for precise data extraction +- πŸ“ Passes instructions/keywords to refine extraction +- πŸ”’ Proxy support with authentication for enhanced access +- πŸ”„ Session management for complex multi-page crawling +- 🌐 Asynchronous architecture for improved performance +- πŸ–ΌοΈ Improved image processing with lazy-loading detection +- πŸ•°οΈ Enhanced handling of delayed content loading +- πŸ”‘ Custom headers support for LLM interactions +- πŸ–ΌοΈ iframe content extraction for comprehensive analysis +- ⏱️ Flexible timeout and delayed content retrieval options + +## Documentation Structure + +Our documentation is organized into several sections: + +### Basic Usage +- [Installation](basic/installation.md) +- [Quick Start](basic/quickstart.md) +- [Simple Crawling](basic/simple-crawling.md) +- [Browser Configuration](basic/browser-config.md) +- [Content Selection](basic/content-selection.md) +- [Output Formats](basic/output-formats.md) +- [Page Interaction](basic/page-interaction.md) + +### Advanced Features +- [Magic Mode](advanced/magic-mode.md) +- [Session Management](advanced/session-management.md) +- [Hooks & Authentication](advanced/hooks.md) +- [Proxy & Security](advanced/proxy-security.md) +- [Content Processing](advanced/content-processing.md) + +### Extraction & Processing +- [Extraction Strategies Overview](extraction/overview.md) +- [LLM Integration](extraction/llm.md) +- [CSS-Based Extraction](extraction/css.md) +- [Cosine Strategy](extraction/cosine.md) +- [Chunking Strategies](extraction/chunking.md) + +### API Reference +- [AsyncWebCrawler](api/async-webcrawler.md) +- [CrawlResult](api/crawl-result.md) +- [Extraction Strategies](api/strategies.md) +- [arun() Method Parameters](api/arun.md) + +### Examples +- Coming soon! + +## Getting Started + +1. Install Crawl4AI: +```bash +pip install crawl4ai +``` + +2. Check out our [Quick Start Guide](basic/quickstart.md) to begin crawling web pages. + +3. Explore our [examples](https://github.com/unclecode/crawl4ai/tree/main/docs/examples) to see Crawl4AI in action. + +## Support + +For questions, suggestions, or issues: +- GitHub Issues: [Report a Bug](https://github.com/unclecode/crawl4ai/issues) +- Twitter: [@unclecode](https://twitter.com/unclecode) +- Website: [crawl4ai.com](https://crawl4ai.com) + +Happy Crawling! πŸ•ΈοΈπŸš€ \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 66bc3473..30136c61 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,36 +1,60 @@ site_name: Crawl4AI Documentation -docs_dir: docs/md +site_description: πŸ”₯πŸ•·οΈ Crawl4AI, Open-source LLM Friendly Web Crawler & Scrapper +site_url: https://docs.crawl4ai.com +repo_url: https://github.com/unclecode/crawl4ai +repo_name: unclecode/crawl4ai +docs_dir: docs/md_v2 + nav: - - Home: index.md - - First Steps: - - Introduction: introduction.md - - Installation: installation.md - - Quick Start: quickstart.md - - Examples: - - Intro: examples/index.md - - Structured Data Extraction: examples/json_css_extraction.md - - LLM Extraction: examples/llm_extraction.md - - JS Execution & CSS Filtering: examples/js_execution_css_filtering.md - - Hooks & Auth: examples/hooks_auth.md - - Summarization: examples/summarization.md - - Research Assistant: examples/research_assistant.md - - Full Details of Using Crawler: - - Crawl Request Parameters: full_details/crawl_request_parameters.md - - Crawl Result Class: full_details/crawl_result_class.md - - Session Based Crawling: full_details/session_based_crawling.md - - Advanced Features: full_details/advanced_features.md - - Advanced JsonCssExtraction: full_details/advanced_jsoncss_extraction.md - - Chunking Strategies: full_details/chunking_strategies.md - - Extraction Strategies: full_details/extraction_strategies.md - - Miscellaneous: - - Change Log: changelog.md - - Contact: contact.md + - Home: 'index.md' + - 'Installation': 'basic/installation.md' + - 'Quick Start': 'basic/quickstart.md' + + - Basic: + - 'Simple Crawling': 'basic/simple-crawling.md' + - 'Output Formats': 'basic/output-formats.md' + - 'Browser Configuration': 'basic/browser-config.md' + - 'Page Interaction': 'basic/page-interaction.md' + - 'Content Selection': 'basic/content-selection.md' + + - Advanced: + - 'Content Processing': 'advanced/content-processing.md' + - 'Magic Mode': 'advanced/magic-mode.md' + - 'Hooks & Auth': 'advanced/hooks-auth.md' + - 'Proxy & Security': 'advanced/proxy-security.md' + - 'Session Management': 'advanced/session-management.md' + - 'Session Management (Advanced)': 'advanced/session-management-advanced.md' + + - Extraction: + - 'Overview': 'extraction/overview.md' + - 'LLM Strategy': 'extraction/llm.md' + - 'Json-CSS Extractor Basic': 'extraction/css.md' + - 'Json-CSS Extractor Advanced': 'extraction/css-advanced.md' + - 'Cosine Strategy': 'extraction/cosine.md' + - 'Chunking': 'extraction/chunking.md' + + - API Reference: + - 'AsyncWebCrawler': 'api/async-webcrawler.md' + - 'AsyncWebCrawler.arun()': 'api/arun.md' + - 'CrawlResult': 'api/crawl-result.md' + - 'Strategies': 'api/strategies.md' + theme: name: terminal palette: dark -# Add the css/extra.css +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - attr_list + - tables + extra_css: - assets/styles.css - assets/highlight.css @@ -38,4 +62,4 @@ extra_css: extra_javascript: - assets/highlight.min.js - - assets/highlight_init.js + - assets/highlight_init.js \ No newline at end of file