# **Part 6: Advanced Web Scraping Techniques – JavaScript Rendering, Fingerprinting, and Large-Scale Data Processing** **Duration:** ~60 minutes **Hashtags:** #AdvancedScraping #JavaScriptRendering #BrowserFingerprinting #DataPipelines #LegalCompliance #ScrapingOptimization #EnterpriseScraping #WebScraping #DataEngineering #TechInnovation --- ## **Table of Contents** 1. [Advanced JavaScript Rendering Techniques](#javascript-rendering) 2. [Browser Fingerprinting Countermeasures](#fingerprinting-countermeasures) 3. [Large-Scale Data Processing Pipelines](#data-processing-pipelines) 4. [Legal Risk Assessment Frameworks](#legal-risk-assessment) 5. [Scraping Performance Optimization](#performance-optimization) 6. [Case Study: Enterprise-Scale Scraping Platform](#enterprise-case-study) 7. [Quiz: Advanced Scraping Mastery](#advanced-quiz) 8. [Conclusion and Future of Web Scraping](#part6-conclusion) --- ## **1. Advanced JavaScript Rendering Techniques** 🖥️ Modern websites increasingly rely on JavaScript frameworks that require sophisticated rendering approaches beyond basic headless browsers. ### **JavaScript Framework Detection** #### **1. Framework Detection System** ```python # framework_detection.py import re import requests from bs4 import BeautifulSoup import tldextract class FrameworkDetector: def __init__(self): self.framework_signatures = self._load_framework_signatures() self.cache = {} def _load_framework_signatures(self): """Load signatures for detecting JavaScript frameworks""" return { 'react': { 'html_patterns': [ r'data-reactroot', r'data-reactid', r'__reactFiber\$', r'__reactProps\$' ], 'js_patterns': [ r'webpackJsonp.*react', r'facebook\.com[/\\]react', r'ReactDOM\.render' ], 'headers': [ ('x-powered-by', 'react') ] }, 'angular': { 'html_patterns': [ r'ng-', r'ng_', r'data-ng-', r'x-ng-', r'ng:', r'ng_', r'ng\$' ], 'js_patterns': [ r'angular\.module', r'ngRoute', r'angular\.version' ], 'headers': [ ('x-powered-by', 'angular') ] }, 'vue': { 'html_patterns': [ r'vue-', r'data-v-', r'__vue__', r'vuejs' ], 'js_patterns': [ r'Vue\.prototype', r'Vue\.extend', r'vue\.runtime' ], 'headers': [ ('x-powered-by', 'vue') ] }, 'svelte': { 'html_patterns': [ r'svelte-', r'data-svelte' ], 'js_patterns': [ r'__sveltets_', r'svelte\.internal' ], 'headers': [] }, 'nextjs': { 'html_patterns': [ r'__NEXT_DATA__', r'next-script', r'__next' ], 'js_patterns': [ r'next\.js', r'__NEXT_RELAY_ENV__' ], 'headers': [ ('server', 'next') ] }, 'nuxt': { 'html_patterns': [ r'__NUXT__', r'nuxt-' ], 'js_patterns': [ r'nuxt\.js', r'NUXT\.config' ], 'headers': [] } } def detect_frameworks(self, url, timeout=10): """ Detect JavaScript frameworks used by a website Returns: List of detected frameworks with confidence scores """ # Check cache first if url in self.cache: return self.cache[url] try: # Get domain for subresource analysis extracted = tldextract.extract(url) domain = f"{extracted.domain}.{extracted.suffix}" # Fetch page response = requests.get(url, timeout=timeout) response.raise_for_status() # Analyze content detected = self._analyze_content( response.text, response.headers, domain ) # Store in cache self.cache[url] = detected return detected except Exception as e: print(f"Error detecting frameworks for {url}: {str(e)}") return [] def _analyze_content(self, html, headers, domain): """Analyze HTML and headers for framework signatures""" detected = [] # Check HTML content for framework, patterns in self.framework_signatures.items(): confidence = 0 # Check HTML patterns for pattern in patterns['html_patterns']: if re.search(pattern, html, re.IGNORECASE): confidence += 0.2 # Check JS patterns soup = BeautifulSoup(html, 'html.parser') scripts = [script.string for script in soup.find_all('script') if script.string] for script in scripts: for pattern in patterns['js_patterns']: if re.search(pattern, script, re.IGNORECASE): confidence += 0.3 break # Check headers for header, value in patterns['headers']: if header in headers and value in headers[header].lower(): confidence += 0.5 # Add to results if confidence is sufficient if confidence > 0.3: detected.append({ 'framework': framework, 'confidence': min(1.0, confidence), 'evidence': self._get_evidence(framework, html, headers) }) # Sort by confidence detected.sort(key=lambda x: x['confidence'], reverse=True) return detected def _get_evidence(self, framework, html, headers): """Get specific evidence for detected framework""" evidence = [] patterns = self.framework_signatures[framework] # Check HTML patterns for pattern in patterns['html_patterns']: if re.search(pattern, html, re.IGNORECASE): evidence.append(f"HTML pattern match: {pattern}") # Check JS patterns soup = BeautifulSoup(html, 'html.parser') scripts = [script.string for script in soup.find_all('script') if script.string] for script in scripts: for pattern in patterns['js_patterns']: if re.search(pattern, script, re.IGNORECASE): evidence.append(f"JS pattern match: {pattern}") break # Check headers for header, value in patterns['headers']: if header in headers and value in headers[header].lower(): evidence.append(f"Header match: {header}: {headers[header]}") return evidence def get_rendering_strategy(self, url): """ Determine optimal rendering strategy based on detected frameworks Returns: Dictionary with rendering strategy recommendations """ frameworks = self.detect_frameworks(url) if not frameworks: return { 'strategy': 'requests', 'reason': 'No JavaScript frameworks detected', 'confidence': 1.0 } # Determine best strategy primary_framework = frameworks[0]['framework'] confidence = frameworks[0]['confidence'] if primary_framework in ['react', 'angular', 'vue', 'nextjs', 'nuxt']: return { 'strategy': 'headless_browser', 'framework': primary_framework, 'reason': f'Detected {primary_framework} framework requiring JS execution', 'confidence': confidence } if primary_framework == 'svelte': return { 'strategy': 'static_rendering', 'framework': primary_framework, 'reason': 'Svelte can often be statically rendered with proper configuration', 'confidence': confidence * 0.8 } return { 'strategy': 'headless_browser', 'framework': primary_framework, 'reason': 'Unknown framework requiring JS execution', 'confidence': confidence * 0.9 } ``` ### **Advanced Rendering Techniques** #### **1. Custom Browser Profiles for JavaScript Rendering** ```python # browser_profiles.py from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import os import json import time import random class BrowserProfileManager: def __init__(self, profile_dir='browser_profiles'): self.profile_dir = profile_dir os.makedirs(profile_dir, exist_ok=True) self.profiles = self._load_profiles() def _load_profiles(self): """Load browser profiles from disk""" profiles = {} for filename in os.listdir(self.profile_dir): if filename.endswith('.json'): with open(os.path.join(self.profile_dir, filename), 'r') as f: profile_id = filename[:-5] profiles[profile_id] = json.load(f) return profiles def save_profile(self, profile_id, profile_data): """Save browser profile to disk""" with open(os.path.join(self.profile_dir, f"{profile_id}.json"), 'w') as f: json.dump(profile_data, f, indent=2) self.profiles[profile_id] = profile_data def get_profile(self, profile_id=None): """Get browser profile by ID or randomly""" if profile_id and profile_id in self.profiles: return self.profiles[profile_id] # Return random profile if ID not specified if self.profiles: return random.choice(list(self.profiles.values())) # Create default profile if none exist return self.create_default_profile() def create_default_profile(self): """Create a default browser profile""" profile = { 'user_agent': self._generate_user_agent(), 'screen_resolution': self._generate_screen_resolution(), 'timezone': self._generate_timezone(), 'language': self._generate_language(), 'webgl_vendor': self._generate_webgl_vendor(), 'webgl_renderer': self._generate_webgl_renderer(), 'audio_context': self._generate_audio_context(), 'font_list': self._generate_font_list(), 'plugins': self._generate_plugins(), 'touch_support': self._generate_touch_support(), 'hardware_concurrency': self._generate_hardware_concurrency(), 'device_memory': self._generate_device_memory(), 'webdriver': False, 'chrome': { 'app': {'version': '1.0.0'}, 'runtime': {'platform': 'Win32'}, 'webstore': {} } } # Save as default profile self.save_profile('default', profile) return profile def _generate_user_agent(self): """Generate realistic user agent string""" browsers = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version} Safari/537.36' ] versions = [ '90.0.4430.212', '91.0.4472.114', '91.0.4472.124', '91.0.4472.164', '92.0.4515.107', '92.0.4515.131' ] browser_template = random.choice(browsers) version = random.choice(versions) return browser_template.format(version=version) def _generate_screen_resolution(self): """Generate realistic screen resolution""" resolutions = [ '1920x1080', '1366x768', '1536x864', '1440x900', '2560x1440', '3840x2160' ] return random.choice(resolutions) def _generate_timezone(self): """Generate realistic timezone""" timezones = [ 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney' ] return random.choice(timezones) def _generate_language(self): """Generate realistic language setting""" languages = [ 'en-US,en;q=0.9', 'en-GB,en;q=0.8', 'es-ES,es;q=0.9', 'fr-FR,fr;q=0.9', 'de-DE,de;q=0.9', 'ja-JP,ja;q=0.9' ] return random.choice(languages) def _generate_webgl_vendor(self): """Generate realistic WebGL vendor""" vendors = [ 'Google Inc.', 'Intel Inc.', 'NVIDIA Corporation', 'AMD Corporation', 'ARM Limited', 'Imagination Technologies' ] return random.choice(vendors) def _generate_webgl_renderer(self): """Generate realistic WebGL renderer""" renderers = [ 'ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0)', 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti Direct3D11 vs_5_0 ps_5_0)', 'WebKit WebGL', 'Mozilla Labs - WebGL' ] return random.choice(renderers) def _generate_audio_context(self): """Generate realistic AudioContext parameters""" return { 'sampleRate': random.choice([44100, 48000]), 'channelCount': 2, 'maxChannelCount': 2, 'contextType': random.choice(['webaudio', 'audioworklet']) } def _generate_font_list(self): """Generate realistic font list""" fonts = [ 'Arial', 'Times New Roman', 'Helvetica', 'Courier New', 'Verdana', 'Georgia', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black', 'Impact', 'Lucida Console', 'Tahoma' ] return random.sample(fonts, k=random.randint(5, 10)) def _generate_plugins(self): """Generate realistic plugin list""" plugins = [ {'name': 'Chrome PDF Viewer', 'description': 'Portable Document Format'}, {'name': 'Widevine Content Decryption Module', 'description': 'Enables Widevine licenses'}, {'name': 'Native Client', 'description': 'Runs native code inside Chrome'} ] return plugins if random.random() > 0.3 else [] def _generate_touch_support(self): """Generate realistic touch support parameters""" return { 'maxTouchPoints': random.choice([0, 5, 10]), 'touchEvent': random.random() > 0.7, 'touchStart': random.random() > 0.7 } def _generate_hardware_concurrency(self): """Generate realistic hardware concurrency value""" return random.choice([2, 4, 8, 16]) def _generate_device_memory(self): """Generate realistic device memory value""" return random.choice([2, 4, 8]) def create_chrome_options(self, profile=None): """Create Chrome options with profile settings""" if not profile: profile = self.get_profile() chrome_options = Options() # Basic options chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--window-size=' + profile['screen_resolution']) chrome_options.add_argument(f'--user-agent={profile["user_agent"]}') # Language and timezone chrome_options.add_argument(f'--lang={profile["language"].split(",")[0]}') chrome_options.add_argument(f'--timezone={profile["timezone"]}') # Prevent detection chrome_options.add_argument('--disable-blink-features=AutomationControlled') chrome_options.add_experimental_option('excludeSwitches', ['enable-automation']) chrome_options.add_experimental_option('useAutomationExtension', False) # Add prefs to prevent detection prefs = { 'profile.default_content_setting_values': { 'images': 2, # Block images to speed up rendering 'javascript': 1 # Enable JavaScript }, 'intl.accept_languages': profile['language'], 'credentials_enable_service': False, 'profile.password_manager_enabled': False } chrome_options.add_experimental_option('prefs', prefs) return chrome_options def create_stealth_driver(self, profile=None): """Create a stealthy WebDriver with evasion techniques""" if not profile: profile = self.get_profile() chrome_options = self.create_chrome_options(profile) # Create driver driver = webdriver.Chrome( service=Service(ChromeDriverManager().install()), options=chrome_options ) # Execute CDP commands to hide WebDriver driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) ''' }) # Additional evasion techniques self._apply_stealth_techniques(driver, profile) return driver def _apply_stealth_techniques(self, driver, profile): """Apply advanced stealth techniques to WebDriver""" # Override navigator properties driver.execute_script(f''' Object.defineProperty(navigator, 'plugins', {{ get: () => [ {{ name: 'Chrome PDF Viewer', description: 'Portable Document Format' }}, {{ name: 'Widevine Content Decryption Module', description: 'Enables Widevine licenses' }} ] }}); Object.defineProperty(navigator, 'languages', {{ get: () => ['{profile['language'].split(",")[0]}', '{profile['language'].split(",")[1].split(";")[0]}'] }}); Object.defineProperty(navigator, 'hardwareConcurrency', {{ get: () => {profile['hardware_concurrency']} }}); Object.defineProperty(navigator, 'deviceMemory', {{ get: () => {profile['device_memory']} }}); Object.defineProperty(navigator, 'platform', {{ get: () => 'Win32' }}); Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', {{ value: function() {{ return 'data:image/png;base64,UNRECOGNIZED'; }} }}); ''') # Set WebGL parameters driver.execute_script(f''' const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) {{ if (parameter === 37445) return '{profile['webgl_vendor']}'; if (parameter === 37446) return '{profile['webgl_renderer']}'; return getParameter.apply(this, [parameter]); }}; ''') # Spoof audio context driver.execute_script(f''' const AudioContext = window.AudioContext || window.webkitAudioContext; const originalAudioContext = AudioContext; class CustomAudioContext extends originalAudioContext {{ constructor() {{ super(); this.sampleRate = {profile['audio_context']['sampleRate']}; }} }} window.AudioContext = CustomAudioContext; window.webkitAudioContext = CustomAudioContext; ''') ``` #### **2. Headless Browser Optimization** ```python # browser_optimization.py import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BrowserOptimizer: def __init__(self, driver): self.driver = driver self.load_times = [] self.resource_usage = [] def optimize_page_load(self, url, max_wait=30, resource_threshold=80): """ Optimize page loading by blocking unnecessary resources Returns: Boolean indicating success """ start_time = time.time() # Block unnecessary resources self._block_resources() # Navigate to URL self.driver.get(url) # Wait for critical elements if not self._wait_for_critical_elements(max_wait): return False # Measure load time load_time = time.time() - start_time self.load_times.append(load_time) # Check resource usage usage = self._get_resource_usage() self.resource_usage.append(usage) # Apply additional optimizations if needed if usage['cpu'] > resource_threshold or usage['memory'] > resource_threshold: self._apply_aggressive_optimizations() return True def _block_resources(self): """Block unnecessary resources to speed up rendering""" # Block images self.driver.execute_cdp_cmd('Network.setBlockedURLs', { 'urls': ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.svg'] }) # Block trackers trackers = [ '*doubleclick.net*', '*googleadservices.com*', '*analytics.js*', '*facebook.net*', '*taboola.com*' ] self.driver.execute_cdp_cmd('Network.setBlockedURLs', { 'urls': trackers }) # Enable network conditions throttling self.driver.execute_cdp_cmd('Network.emulateNetworkConditions', { 'offline': False, 'latency': 50, # ms 'downloadThroughput': 150 * 1024, # 150 KB/s 'uploadThroughput': 50 * 1024 # 50 KB/s }) def _wait_for_critical_elements(self, max_wait): """Wait for critical page elements to load""" try: # Wait for body to be present WebDriverWait(self.driver, max_wait).until( EC.presence_of_element_located((By.TAG_NAME, 'body')) ) # Wait for main content container (customizable per site) try: WebDriverWait(self.driver, max_wait).until( EC.presence_of_element_located((By.ID, 'main-content')) ) except: try: WebDriverWait(self.driver, max_wait).until( EC.presence_of_element_located((By.CLASS_NAME, 'content')) ) except: pass # Wait for JavaScript to stabilize self._wait_for_js_stabilization(max_wait) return True except Exception as e: print(f"Error waiting for critical elements: {str(e)}") return False def _wait_for_js_stabilization(self, max_wait): """Wait for JavaScript execution to stabilize""" start_time = time.time() last_height = 0 stable_count = 0 while time.time() - start_time < max_wait: # Check scroll height current_height = self.driver.execute_script( "return document.body.scrollHeight" ) # Check if height has stabilized if abs(current_height - last_height) < 10: stable_count += 1 if stable_count >= 3: # Stable for 3 consecutive checks return else: stable_count = 0 last_height = current_height time.sleep(0.5) def _get_resource_usage(self): """Get current resource usage metrics""" # Get CPU and memory usage from performance metrics metrics = self.driver.execute_cdp_cmd('Performance.getMetrics', {}) cpu_usage = next( (m['value'] for m in metrics['metrics'] if m['name'] == 'CPUUtilization'), 0 ) * 100 memory_usage = next( (m['value'] for m in metrics['metrics'] if m['name'] == 'JSHeapUsedSize'), 0 ) # Convert to percentage of typical limits memory_percent = min(100, (memory_usage / (256 * 1024 * 1024)) * 100) return { 'cpu': cpu_usage, 'memory': memory_percent, 'network_requests': self._get_network_request_count() } def _get_network_request_count(self): """Get count of network requests made""" # Implementation would track network requests return 0 # Simplified def _apply_aggressive_optimizations(self): """Apply aggressive optimizations when resources are constrained""" # Disable JavaScript for non-critical pages self.driver.execute_cdp_cmd('Emulation.setScriptExecutionDisabled', { 'value': True }) # Set lower resource thresholds self.driver.execute_cdp_cmd('Network.emulateNetworkConditions', { 'offline': False, 'latency': 100, # ms 'downloadThroughput': 50 * 1024, # 50 KB/s 'uploadThroughput': 20 * 1024 # 20 KB/s }) # Block more resources self.driver.execute_cdp_cmd('Network.setBlockedURLs', { 'urls': ['*.css', '*.js', '*.woff', '*.woff2'] }) def analyze_performance(self): """Analyze browser performance metrics""" if not self.load_times: return None return { 'avg_load_time': sum(self.load_times) / len(self.load_times), 'max_load_time': max(self.load_times), 'resource_usage': { 'avg_cpu': sum(u['cpu'] for u in self.resource_usage) / len(self.resource_usage), 'avg_memory': sum(u['memory'] for u in self.resource_usage) / len(self.resource_usage) }, 'recommendations': self._generate_optimization_recommendations() } def _generate_optimization_recommendations(self): """Generate optimization recommendations based on performance data""" recommendations = [] # Load time recommendations avg_load_time = sum(self.load_times) / len(self.load_times) if avg_load_time > 10: recommendations.append( "Page load time is high (>10s). Consider blocking more resources " "or using a simpler rendering strategy." ) # Resource usage recommendations avg_cpu = sum(u['cpu'] for u in self.resource_usage) / len(self.resource_usage) if avg_cpu > 70: recommendations.append( "High CPU usage detected. Consider reducing JavaScript execution " "or using resource throttling." ) avg_memory = sum(u['memory'] for u in self.resource_usage) / len(self.resource_usage) if avg_memory > 80: recommendations.append( "High memory usage detected. Consider closing browser after use " "or using lighter browser profiles." ) return recommendations def cleanup(self): """Clean up resources after browser usage""" # Reset network conditions self.driver.execute_cdp_cmd('Network.clearBrowserCaches', {}) self.driver.execute_cdp_cmd('Network.setBlockedURLs', {'urls': []}) self.driver.execute_cdp_cmd('Network.emulateNetworkConditions', { 'offline': False, 'latency': 0, 'downloadThroughput': -1, 'uploadThroughput': -1 }) # Close browser self.driver.quit() ``` --- ## **2. Browser Fingerprinting Countermeasures** 🖌️ Modern websites use sophisticated browser fingerprinting techniques that require advanced countermeasures. ### **Fingerprinting Detection System** #### **1. Fingerprinting Detection Engine** ```python # fingerprint_detection.py import re from selenium import webdriver from selenium.webdriver.common.by import By import time class FingerprintingDetector: def __init__(self, driver): self.driver = driver self.detected_techniques = [] self.suspicious_patterns = self._load_suspicious_patterns() def _load_suspicious_patterns(self): """Load patterns that indicate fingerprinting attempts""" return [ # Canvas fingerprinting { 'name': 'canvas_fingerprinting', 'pattern': r'CanvasRenderingContext2D|toDataURL|getImageData', 'weight': 0.8, 'description': 'Canvas-based fingerprinting technique' }, # WebGL fingerprinting { 'name': 'webgl_fingerprinting', 'pattern': r'WebGLRenderingContext|getParameter|getSupportedExtensions', 'weight': 0.9, 'description': 'WebGL-based fingerprinting technique' }, # AudioContext fingerprinting { 'name': 'audio_fingerprinting', 'pattern': r'AudioContext|createOscillator|analyserNode', 'weight': 0.7, 'description': 'AudioContext-based fingerprinting technique' }, # Font detection { 'name': 'font_detection', 'pattern': r'div\.style\.fontFamily|document\.fonts', 'weight': 0.6, 'description': 'Font enumeration fingerprinting' }, # Plugin detection { 'name': 'plugin_detection', 'pattern': r'navigator\.plugins|navigator\.mimeTypes', 'weight': 0.5, 'description': 'Browser plugin enumeration' }, # Touch support detection { 'name': 'touch_detection', 'pattern': r'\'ontouchstart\' in document|window\.TouchEvent', 'weight': 0.4, 'description': 'Touch capability detection' }, # Hardware concurrency { 'name': 'hardware_concurrency', 'pattern': r'navigator\.hardwareConcurrency', 'weight': 0.7, 'description': 'Hardware concurrency detection' }, # Device memory { 'name': 'device_memory', 'pattern': r'navigator\.deviceMemory', 'weight': 0.6, 'description': 'Device memory detection' }, # Battery status { 'name': 'battery_status', 'pattern': r'navigator\.getBattery', 'weight': 0.3, 'description': 'Battery status API detection' }, # WebRTC IP leakage { 'name': 'webrtc_ip_leak', 'pattern': r'RTCPeerConnection|createDataChannel', 'weight': 0.9, 'description': 'WebRTC IP address leakage' } ] def detect_fingerprinting(self, url, timeout=15): """ Detect fingerprinting techniques on a website Returns: List of detected fingerprinting techniques """ # Navigate to URL self.driver.get(url) # Wait for page to load time.sleep(2) # Get page source page_source = self.driver.page_source # Analyze for suspicious patterns detected = [] for pattern in self.suspicious_patterns: if re.search(pattern['pattern'], page_source): detected.append({ 'technique': pattern['name'], 'confidence': pattern['weight'], 'description': pattern['description'], 'evidence': self._get_evidence(pattern['pattern'], page_source) }) # Check for dynamic fingerprinting (requires executing JS) dynamic_techniques = self._detect_dynamic_fingerprinting() detected.extend(dynamic_techniques) # Store results self.detected_techniques = detected return detected def _get_evidence(self, pattern, page_source): """Get specific evidence for detected pattern""" matches = re.findall(pattern, page_source) return matches[:3] # Return up to 3 examples def _detect_dynamic_fingerprinting(self): """Detect dynamically executed fingerprinting techniques""" detected = [] # Check for canvas fingerprinting canvas_result = self.driver.execute_script(''' try { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textAlign = "none"; ctx.fillStyle = "#f60"; ctx.fillText("BrowserLeaks", 2, 15); ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText("com", 4, 17); return canvas.toDataURL(); } catch (e) { return null; } ''') if canvas_result and 'data:image/png;base64' in canvas_result: detected.append({ 'technique': 'canvas_fingerprinting', 'confidence': 0.9, 'description': 'Active canvas fingerprinting detected', 'evidence': [canvas_result[:50] + '...'] }) # Check for WebGL fingerprinting webgl_result = self.driver.execute_script(''' try { var canvas = document.createElement('canvas'); var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) return null; var info = { vendor: gl.getParameter(gl.VENDOR), renderer: gl.getParameter(gl.RENDERER), version: gl.getParameter(gl.VERSION), shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION) }; return JSON.stringify(info); } catch (e) { return null; } ''') if webgl_result: detected.append({ 'technique': 'webgl_fingerprinting', 'confidence': 0.95, 'description': 'Active WebGL fingerprinting detected', 'evidence': [webgl_result] }) # Check for AudioContext fingerprinting audio_result = self.driver.execute_script(''' try { var audioCtx = new (window.AudioContext || window.webkitAudioContext)(); var oscillator = audioCtx.createOscillator(); var analyser = audioCtx.createAnalyser(); oscillator.connect(analyser); analyser.connect(audioCtx.destination); oscillator.start(); var bufferLength = analyser.frequencyBinCount; var dataArray = new Uint8Array(bufferLength); analyser.getByteFrequencyData(dataArray); return JSON.stringify({ sampleRate: audioCtx.sampleRate, frequencyData: Array.from(dataArray).slice(0, 10) }); } catch (e) { return null; } ''') if audio_result: detected.append({ 'technique': 'audio_fingerprinting', 'confidence': 0.85, 'description': 'Active AudioContext fingerprinting detected', 'evidence': [audio_result] }) return detected def get_countermeasures(self): """Get recommended countermeasures for detected techniques""" countermeasures = [] for technique in self.detected_techniques: if technique['technique'] == 'canvas_fingerprinting': countermeasures.append({ 'technique': 'canvas_fingerprinting', 'countermeasure': 'Spoof canvas rendering results', 'implementation': self._spoof_canvas }) elif technique['technique'] == 'webgl_fingerprinting': countermeasures.append({ 'technique': 'webgl_fingerprinting', 'countermeasure': 'Spoof WebGL parameters', 'implementation': self._spoof_webgl }) elif technique['technique'] == 'audio_fingerprinting': countermeasures.append({ 'technique': 'audio_fingerprinting', 'countermeasure': 'Spoof AudioContext results', 'implementation': self._spoof_audio }) # Add more countermeasures as needed return countermeasures def _spoof_canvas(self): """Spoof canvas fingerprinting results""" self.driver.execute_script(''' const toDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function() { return 'data:image/png;base64,spoofed'; }; ''') def _spoof_webgl(self): """Spoof WebGL fingerprinting results""" self.driver.execute_script(''' const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) return 'Spoofed Vendor'; if (parameter === 37446) return 'Spoofed Renderer'; return getParameter.apply(this, [parameter]); }; ''') def _spoof_audio(self): """Spoof AudioContext fingerprinting results""" self.driver.execute_script(''' const AudioContext = window.AudioContext || window.webkitAudioContext; const originalAudioContext = AudioContext; class CustomAudioContext extends originalAudioContext { constructor() { super(); this.sampleRate = 44100; } createAnalyser() { const analyser = super.createAnalyser(); const originalGetByteFrequencyData = analyser.getByteFrequencyData; analyser.getByteFrequencyData = function(array) { // Fill with consistent values for (let i = 0; i < array.length; i++) { array[i] = i % 256; } return originalGetByteFrequencyData.call(this, array); }; return analyser; } } window.AudioContext = CustomAudioContext; window.webkitAudioContext = CustomAudioContext; ''') def apply_countermeasures(self): """Apply all recommended countermeasures""" countermeasures = self.get_countermeasures() for cm in countermeasures: cm['implementation']() return len(countermeasures) ``` ### **Advanced Fingerprinting Countermeasures** #### **1. Comprehensive Fingerprint Spoofing System** ```python # fingerprint_spoofing.py import random import string from selenium import webdriver class FingerprintSpoofingSystem: def __init__(self, driver): self.driver = driver self.spoofed_values = {} def spoof_all(self): """Apply all spoofing techniques""" self.spoof_timezone() self.spoof_language() self.spoof_resolution() self.spoof_user_agent() self.spoof_webgl() self.spoof_canvas() self.spoof_audio() self.spoof_fonts() self.spoof_plugins() self.spoof_touch() self.spoof_hardware() self.spoof_webdriver() self.spoof_webrtc() def spoof_timezone(self, timezone=None): """Spoof timezone information""" timezones = [ 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney' ] if not timezone: timezone = random.choice(timezones) self.driver.execute_cdp_cmd('Emulation.setTimezoneOverride', { 'timezoneId': timezone }) self.spoofed_values['timezone'] = timezone def spoof_language(self, language=None): """Spoof language preferences""" languages = [ 'en-US,en;q=0.9', 'en-GB,en;q=0.8', 'es-ES,es;q=0.9', 'fr-FR,fr;q=0.9', 'de-DE,de;q=0.9', 'ja-JP,ja;q=0.9' ] if not language: language = random.choice(languages) self.driver.execute_cdp_cmd('Emulation.setLocaleOverride', { 'locale': language.split(',')[0] }) self.spoofed_values['language'] = language def spoof_resolution(self, width=None, height=None): """Spoof screen resolution""" resolutions = [ (1920, 1080), (1366, 768), (1536, 864), (1440, 900), (2560, 1440), (3840, 2160) ] if width is None or height is None: width, height = random.choice(resolutions) self.driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { 'width': width, 'height': height, 'deviceScaleFactor': 1, 'mobile': False }) self.spoofed_values['resolution'] = f"{width}x{height}" def spoof_user_agent(self, user_agent=None): """Spoof user agent string""" user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1' ] if not user_agent: user_agent = random.choice(user_agents) self.driver.execute_cdp_cmd('Network.setUserAgentOverride', { 'userAgent': user_agent }) self.spoofed_values['user_agent'] = user_agent def spoof_webgl(self, vendor=None, renderer=None): """Spoof WebGL fingerprinting parameters""" vendors = [ 'Google Inc.', 'Intel Inc.', 'NVIDIA Corporation', 'AMD Corporation', 'ARM Limited', 'Imagination Technologies' ] renderers = [ 'ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0)', 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 Ti Direct3D11 vs_5_0 ps_5_0)', 'WebKit WebGL', 'Mozilla Labs - WebGL' ] if not vendor: vendor = random.choice(vendors) if not renderer: renderer = random.choice(renderers) self.driver.execute_script(f''' const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) {{ if (parameter === 37445) return '{vendor}'; if (parameter === 37446) return '{renderer}'; return getParameter.apply(this, [parameter]); }}; ''') self.spoofed_values['webgl'] = {'vendor': vendor, 'renderer': renderer} def spoof_canvas(self): """Spoof canvas fingerprinting results""" self.driver.execute_script(''' const toDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function() { return 'data:image/png;base64,spoofed'; }; const getImageData = CanvasRenderingContext2D.prototype.getImageData; CanvasRenderingContext2D.prototype.getImageData = function() { const data = getImageData.apply(this, arguments); // Modify pixel data to be consistent for (let i = 0; i < data.data.length; i += 4) { data.data[i] = 255; // R data.data[i+1] = 0; // G data.data[i+2] = 0; // B data.data[i+3] = 255; // A } return data; }; ''') self.spoofed_values['canvas'] = 'spoofed' def spoof_audio(self): """Spoof AudioContext fingerprinting results""" self.driver.execute_script(''' const AudioContext = window.AudioContext || window.webkitAudioContext; const originalAudioContext = AudioContext; class CustomAudioContext extends originalAudioContext { constructor() { super(); this.sampleRate = 44100; } createAnalyser() { const analyser = super.createAnalyser(); const originalGetByteFrequencyData = analyser.getByteFrequencyData; analyser.getByteFrequencyData = function(array) { // Fill with consistent values for (let i = 0; i < array.length; i++) { array[i] = i % 256; } return originalGetByteFrequencyData.call(this, array); }; return analyser; } } window.AudioContext = CustomAudioContext; window.webkitAudioContext = CustomAudioContext; ''') self.spoofed_values['audio'] = 'spoofed' def spoof_fonts(self, font_list=None): """Spoof available font list""" default_fonts = [ 'Arial', 'Times New Roman', 'Helvetica', 'Courier New', 'Verdana', 'Georgia', 'Comic Sans MS', 'Trebuchet MS' ] if not font_list: font_list = random.sample(default_fonts, k=random.randint(5, 8)) self.driver.execute_script(f''' // Override document.fonts const originalFonts = document.fonts; document.fonts = new Proxy(originalFonts, {{ get: function(target, prop) {{ if (prop === 'check') {{ return function() {{ return true; }}; }} if (prop === 'keys' || prop === 'values' || prop === 'entries') {{ return function* () {{ for (const font of {json.dumps(font_list)}) {{ yield font; }} }}; }} return target[prop]; }}, has: function(target, prop) {{ return prop === 'check' || prop === 'keys' || prop === 'values' || prop === 'entries' || target.hasOwnProperty(prop); }} }}); // Override getComputedStyle for font detection const originalGetComputedStyle = window.getComputedStyle; window.getComputedStyle = function(element, pseudoElt) {{ const style = originalGetComputedStyle(element, pseudoElt); return new Proxy(style, {{ get: function(target, prop) {{ if (prop === 'fontFamily') {{ return '{", ".join(font_list)}'; }} return target[prop]; }} }}); }}; ''') self.spoofed_values['fonts'] = font_list def spoof_plugins(self, plugins=None): """Spoof browser plugin list""" default_plugins = [ {'name': 'Chrome PDF Viewer', 'description': 'Portable Document Format'}, {'name': 'Widevine Content Decryption Module', 'description': 'Enables Widevine licenses'}, {'name': 'Native Client', 'description': 'Runs native code inside Chrome'} ] if plugins is None: plugins = random.sample(default_plugins, k=random.randint(1, 3)) plugins_script = json.dumps(plugins) self.driver.execute_script(f''' // Override navigator.plugins Object.defineProperty(navigator, 'plugins', {{ get: function() {{ return {plugins_script}.map(plugin => ({{ name: plugin.name, description: plugin.description, item: function() {{ return this; }}, namedItem: function() {{ return this; }} })); }} }}); // Override navigator.mimeTypes Object.defineProperty(navigator, 'mimeTypes', {{ get: function() {{ return {plugins_script}.map(plugin => ({{ type: 'application/pdf', description: plugin.description, suffixes: 'pdf', enabledPlugin: {{ name: plugin.name, description: plugin.description }} })); }} }}); ''') self.spoofed_values['plugins'] = plugins def spoof_touch(self, max_touch_points=None, has_touch=False): """Spoof touch capabilities""" if max_touch_points is None: max_touch_points = random.choice([0, 5, 10]) self.driver.execute_script(f''' // Override touch events ['touchstart', 'touchmove', 'touchend', 'touchcancel'].forEach(function(event) {{ document.addEventListener(event, function(e) {{ e.preventDefault(); }}, {{ passive: false }}); }}); // Override navigator.maxTouchPoints Object.defineProperty(navigator, 'maxTouchPoints', {{ get: function() {{ return {max_touch_points}; }} }}); // Override 'ontouchstart' in document Object.defineProperty(document, 'ontouchstart', {{ get: function() {{ return {str(has_touch).lower()}; }}, set: function() {{ }} }}); // Override window.TouchEvent window.TouchEvent = {str(has_touch).lower()}; ''') self.spoofed_values['touch'] = { 'max_touch_points': max_touch_points, 'has_touch': has_touch } def spoof_hardware(self, hardware_concurrency=None, device_memory=None): """Spoof hardware capabilities""" if hardware_concurrency is None: hardware_concurrency = random.choice([2, 4, 8, 16]) if device_memory is None: device_memory = random.choice([2, 4, 8]) self.driver.execute_script(f''' // Override hardwareConcurrency Object.defineProperty(navigator, 'hardwareConcurrency', {{ get: function() {{ return {hardware_concurrency}; }} }}); // Override deviceMemory Object.defineProperty(navigator, 'deviceMemory', {{ get: function() {{ return {device_memory}; }} }}); ''') self.spoofed_values['hardware'] = { 'hardware_concurrency': hardware_concurrency, 'device_memory': device_memory } def spoof_webdriver(self): """Spoof WebDriver detection""" self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); window.navigator.chrome = { app: {isInstalled: false}, webstore: {onInstallStageChanged: {}, onDownloadProgress: {}}, runtime: {PlatformOs: {MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd'}, PlatformArch: {ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64'}, PlatformNaclArch: {ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64'}, RefreshType: {NONE: 0, FORMAL: 1, OPTIONAL: 2}} }; const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); ''' }) self.spoofed_values['webdriver'] = 'spoofed' def spoof_webrtc(self): """Spoof WebRTC to prevent IP leakage""" self.driver.execute_script(''' // Override WebRTC to prevent IP leakage const originalGetUserMedia = navigator.mediaDevices.getUserMedia; navigator.mediaDevices.getUserMedia = function(constraints) { return originalGetUserMedia(constraints) .then(stream => { // Modify the stream to prevent IP leakage return stream; }); }; // Override RTCPeerConnection const originalRTCPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function(...args) { const peerConnection = new originalRTCPeerConnection(...args); const originalCreateOffer = peerConnection.createOffer; peerConnection.createOffer = function(...args) { return originalCreateOffer.apply(this, args) .then(offer => { // Remove local IP addresses offer.sdp = offer.sdp.replace(/(c=IN IP4)(\\s+\\d+\\.\\d+\\.\\d+\\.\\d+)/g, '$1 0.0.0.0'); return offer; }); }; const originalCreateAnswer = peerConnection.createAnswer; peerConnection.createAnswer = function(...args) { return originalCreateAnswer.apply(this, args) .then(answer => { // Remove local IP addresses answer.sdp = answer.sdp.replace(/(c=IN IP4)(\\s+\\d+\\.\\d+\\.\\d+\\.\\d+)/g, '$1 0.0.0.0'); return answer; }); }; return peerConnection; }; ''') self.spoofed_values['webrtc'] = 'spoofed' ``` --- ## **3. Large-Scale Data Processing Pipelines** 📊 Handling massive amounts of scraped data requires robust data processing infrastructure. ### **Distributed Data Processing Architecture** #### **1. Data Pipeline Orchestration System** ```python # data_pipeline.py import time import uuid import json from datetime import datetime from collections import defaultdict from concurrent.futures import ThreadPoolExecutor, as_completed class DataPipelineOrchestrator: def __init__(self, max_workers=10, batch_size=100): self.stages = [] self.max_workers = max_workers self.batch_size = batch_size self.pipeline_id = str(uuid.uuid4()) self.metrics = { 'start_time': None, 'end_time': None, 'total_items': 0, 'processed_items': 0, 'failed_items': 0, 'stage_metrics': defaultdict(dict) } self.error_log = [] def add_stage(self, stage_name, processor, batched=False): """Add a processing stage to the pipeline""" self.stages.append({ 'name': stage_name, 'processor': processor, 'batched': batched, 'start_time': None, 'end_time': None, 'items_processed': 0, 'items_failed': 0 }) def process(self, data_stream): """ Process data through the pipeline Args: data_stream: Iterable of data items Returns: Processed data stream """ self.metrics['start_time'] = datetime.utcnow() self.metrics['total_items'] = 0 # Initialize output queue output = [] # Process data through each stage current_data = data_stream for stage in self.stages: stage['start_time'] = datetime.utcnow() stage['items_processed'] = 0 stage['items_failed'] = 0 print(f"Starting stage: {stage['name']}") # Process data for this stage if stage['batched']: # Process in batches batch = [] for item in current_data: self.metrics['total_items'] += 1 batch.append(item) if len(batch) >= self.batch_size: self._process_batch(stage, batch, output) batch = [] # Process remaining items if batch: self._process_batch(stage, batch, output) else: # Process items individually for item in current_data: self.metrics['total_items'] += 1 self._process_item(stage, item, output) # Update metrics stage['end_time'] = datetime.utcnow() self.metrics['stage_metrics'][stage['name']] = { 'duration': (stage['end_time'] - stage['start_time']).total_seconds(), 'items_processed': stage['items_processed'], 'items_failed': stage['items_failed'] } # Prepare for next stage current_data = output output = [] self.metrics['end_time'] = datetime.utcnow() self.metrics['processed_items'] = self.metrics['total_items'] - self.metrics['failed_items'] print(f"Pipeline completed. Processed {self.metrics['processed_items']}/{self.metrics['total_items']} items.") return current_data def _process_batch(self, stage, batch, output_queue): """Process a batch of items through a stage""" try: # Process batch results = stage['processor'](batch) # Add successful results to output for result in results: output_queue.append(result) stage['items_processed'] += 1 return True except Exception as e: # Log error self._log_error(stage['name'], batch, str(e)) # Add failed items to error log for item in batch: self.metrics['failed_items'] += 1 stage['items_failed'] += 1 return False def _process_item(self, stage, item, output_queue): """Process a single item through a stage""" try: # Process item result = stage['processor'](item) # Add to output if successful if result is not None: output_queue.append(result) stage['items_processed'] += 1 return True except Exception as e: # Log error self._log_error(stage['name'], item, str(e)) # Track failure self.metrics['failed_items'] += 1 stage['items_failed'] += 1 return False def _log_error(self, stage, data, error): """Log processing error""" error_id = str(uuid.uuid4()) timestamp = datetime.utcnow().isoformat() self.error_log.append({ 'error_id': error_id, 'timestamp': timestamp, 'stage': stage, 'error': error, 'data_sample': str(data)[:500] # Truncate large data }) print(f"ERROR in stage {stage}: {error}") print(f" Error ID: {error_id}") print(f" Data sample: {str(data)[:100]}...") def get_metrics(self): """Get pipeline performance metrics""" return { 'pipeline_id': self.pipeline_id, 'total_items': self.metrics['total_items'], 'processed_items': self.metrics['processed_items'], 'failed_items': self.metrics['failed_items'], 'total_duration': (self.metrics['end_time'] - self.metrics['start_time']).total_seconds() if self.metrics['end_time'] else None, 'stage_metrics': dict(self.metrics['stage_metrics']) } def get_error_log(self): """Get error log for troubleshooting""" return self.error_log def save_metrics(self, file_path): """Save metrics to file""" with open(file_path, 'w') as f: json.dump(self.get_metrics(), f, indent=2) def save_error_log(self, file_path): """Save error log to file""" with open(file_path, 'w') as f: json.dump(self.get_error_log(), f, indent=2) def create_distributed_pipeline(self, num_nodes=3): """Create a distributed pipeline across multiple nodes""" pipelines = [] for i in range(num_nodes): pipeline = DataPipelineOrchestrator( max_workers=self.max_workers, batch_size=self.batch_size ) # Copy stages for stage in self.stages: pipeline.add_stage( stage['name'], stage['processor'], stage['batched'] ) pipelines.append(pipeline) return DistributedPipeline(pipelines) class DistributedPipeline: def __init__(self, pipelines): self.pipelines = pipelines def process(self, data_stream): """Process data using distributed pipelines""" # Split data among pipelines data_chunks = self._split_data(data_stream, len(self.pipelines)) # Process in parallel results = [] with ThreadPoolExecutor(max_workers=len(self.pipelines)) as executor: futures = { executor.submit(pipeline.process, chunk): i for i, (pipeline, chunk) in enumerate(zip(self.pipelines, data_chunks)) } for future in as_completed(futures): i = futures[future] try: chunk_results = future.result() results.extend(chunk_results) except Exception as e: print(f"Pipeline {i} failed: {str(e)}") return results def _split_data(self, data_stream, num_chunks): """Split data stream into chunks""" chunks = [[] for _ in range(num_chunks)] for i, item in enumerate(data_stream): chunks[i % num_chunks].append(item) return chunks def get_metrics(self): """Get metrics from all pipelines""" return [pipeline.get_metrics() for pipeline in self.pipelines] def get_error_logs(self): """Get error logs from all pipelines""" return [pipeline.get_error_log() for pipeline in self.pipelines] ``` #### **2. Schema Evolution Manager** ```python # schema_evolution.py import json import re from datetime import datetime class SchemaEvolutionManager: def __init__(self): self.schemas = {} # {schema_id: schema_definition} self.version_history = {} # {schema_id: [versions]} self.compatibility_rules = { 'backward': self._check_backward_compatibility, 'forward': self._check_forward_compatibility, 'full': self._check_full_compatibility } def register_schema(self, schema_id, schema_definition, compatibility='backward'): """ Register a new schema or update an existing one Returns: Schema version number """ # Validate schema if not self._validate_schema(schema_definition): raise ValueError("Invalid schema definition") # Check compatibility if updating existing schema if schema_id in self.schemas: current_schema = self.schemas[schema_id] if not self.check_compatibility(current_schema, schema_definition, compatibility): raise ValueError(f"Schema update is not {compatibility}-compatible") # Create version version = self._create_version(schema_id) # Store schema if schema_id not in self.schemas: self.schemas[schema_id] = schema_definition self.version_history[schema_id] = [] # Update version history self.version_history[schema_id].append({ 'version': version, 'schema': schema_definition, 'timestamp': datetime.utcnow().isoformat(), 'compatibility': compatibility }) # Update current schema self.schemas[schema_id] = schema_definition return version def _validate_schema(self, schema): """Validate schema structure""" # Implementation would validate against schema standard # This is a simplified version required_fields = ['type', 'fields'] return all(field in schema for field in required_fields) def _create_version(self, schema_id): """Create new version number""" if schema_id not in self.version_history or not self.version_history[schema_id]: return '1.0.0' # Get current version current_version = self.version_history[schema_id][-1]['version'] # Increment minor version major, minor, patch = map(int, current_version.split('.')) return f"{major}.{minor+1}.0" def get_schema(self, schema_id, version=None): """Get schema definition""" if schema_id not in self.schemas: raise ValueError(f"Schema {schema_id} not found") if version: # Find specific version for ver_info in reversed(self.version_history[schema_id]): if ver_info['version'] == version: return ver_info['schema'] raise ValueError(f"Version {version} not found for schema {schema_id}") # Return current schema return self.schemas[schema_id] def get_version_history(self, schema_id): """Get version history for a schema""" if schema_id not in self.version_history: return [] return self.version_history[schema_id] def check_compatibility(self, old_schema, new_schema, compatibility_type='backward'): """Check if schema update is compatible""" if compatibility_type not in self.compatibility_rules: raise ValueError(f"Unknown compatibility type: {compatibility_type}") return self.compatibility_rules[compatibility_type](old_schema, new_schema) def _check_backward_compatibility(self, old_schema, new_schema): """ Check if new schema is backward compatible with old schema (new consumers can read old data) """ # Fields can be added but not removed old_fields = {f['name'] for f in old_schema['fields']} new_fields = {f['name'] for f in new_schema['fields']} # Check for removed fields removed_fields = old_fields - new_fields if removed_fields: return False # Check field type compatibility for field in old_schema['fields']: new_field = next((f for f in new_schema['fields'] if f['name'] == field['name']), None) if new_field and not self._is_type_compatible(field['type'], new_field['type']): return False return True def _check_forward_compatibility(self, old_schema, new_schema): """ Check if new schema is forward compatible with old schema (old consumers can read new data) """ # Fields can be removed but not added old_fields = {f['name'] for f in old_schema['fields']} new_fields = {f['name'] for f in new_schema['fields']} # Check for added fields added_fields = new_fields - old_fields if added_fields: return False # Check field type compatibility for field in new_schema['fields']: old_field = next((f for f in old_schema['fields'] if f['name'] == field['name']), None) if old_field and not self._is_type_compatible(field['type'], old_field['type']): return False return True def _check_full_compatibility(self, old_schema, new_schema): """Check if schema update is fully compatible (both backward and forward)""" return ( self._check_backward_compatibility(old_schema, new_schema) and self._check_forward_compatibility(old_schema, new_schema) ) def _is_type_compatible(self, old_type, new_type): """Check if type conversion is compatible""" # Simplified type compatibility compatibility_map = { 'int': ['int', 'long', 'float', 'double'], 'long': ['long', 'float', 'double'], 'float': ['float', 'double'], 'string': ['string', 'bytes'], 'bytes': ['bytes', 'string'], 'boolean': ['boolean'], 'null': ['null', 'string', 'bytes'] } return new_type in compatibility_map.get(old_type, []) def evolve_data(self, data, from_schema_id, to_schema_id, from_version=None, to_version=None): """ Evolve data from one schema version to another Returns: Transformed data """ # Get schemas from_schema = self.get_schema(from_schema_id, from_version) to_schema = self.get_schema(to_schema_id, to_version) # Check compatibility path if not self._can_evolve(from_schema, to_schema): raise ValueError("Cannot evolve data between these schemas") # Transform data return self._transform_data(data, from_schema, to_schema) def _can_evolve(self, from_schema, to_schema): """Check if data can be evolved between schemas""" # Implementation would check compatibility path return True # Simplified def _transform_data(self, data, from_schema, to_schema): """Transform data from one schema to another""" transformed = {} # Map fields for to_field in to_schema['fields']: # Find matching field in source schema from_field = next( (f for f in from_schema['fields'] if f['name'] == to_field['name']), None ) if from_field and from_field['name'] in data: # Convert value if needed value = self._convert_value( data[from_field['name']], from_field['type'], to_field['type'] ) transformed[to_field['name']] = value elif to_field.get('default') is not None: # Use default value transformed[to_field['name']] = to_field['default'] return transformed def _convert_value(self, value, from_type, to_type): """Convert value between types""" if from_type == to_type: return value # Numeric conversions if from_type in ['int', 'long', 'float', 'double'] and to_type in ['int', 'long', 'float', 'double']: return self._convert_numeric(value, from_type, to_type) # String conversions if from_type == 'string' and to_type == 'bytes': return value.encode('utf-8') if from_type == 'bytes' and to_type == 'string': return value.decode('utf-8') # Null handling if value is None: return None raise ValueError(f"Cannot convert from {from_type} to {to_type}") def _convert_numeric(self, value, from_type, to_type): """Convert numeric values between types""" # Convert to float first for consistent handling if from_type == 'int': numeric = int(value) elif from_type == 'long': numeric = int(value) elif from_type in ['float', 'double']: numeric = float(value) else: raise ValueError(f"Unsupported numeric type: {from_type}") # Convert to target type if to_type == 'int': return int(numeric) elif to_type == 'long': return int(numeric) elif to_type in ['float', 'double']: return float(numeric) raise ValueError(f"Unsupported numeric type: {to_type}") def generate_schema_diff(self, schema_id, version1, version2): """Generate diff between two schema versions""" schema1 = self.get_schema(schema_id, version1) schema2 = self.get_schema(schema_id, version2) changes = { 'added_fields': [], 'removed_fields': [], 'modified_fields': [] } # Find added fields fields1 = {f['name'] for f in schema1['fields']} fields2 = {f['name'] for f in schema2['fields']} changes['added_fields'] = list(fields2 - fields1) changes['removed_fields'] = list(fields1 - fields2) # Find modified fields for field in fields1 & fields2: f1 = next(f for f in schema1['fields'] if f['name'] == field) f2 = next(f for f in schema2['fields'] if f['name'] == field) if f1['type'] != f2['type']: changes['modified_fields'].append({ 'field': field, 'from_type': f1['type'], 'to_type': f2['type'] }) return changes ``` --- ## Continued in next section ..