-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbrowser.py
More file actions
192 lines (161 loc) · 7.24 KB
/
Copy pathbrowser.py
File metadata and controls
192 lines (161 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""Browser automation with stealth configuration and tab management."""
import random
import time
from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page
from config import Config
from rich.console import Console
console = Console()
class BrowserManager:
"""Manages Playwright browser with stealth configuration."""
def __init__(self, config: Config):
"""Initialize browser manager."""
self.config = config
self.playwright = None
self.browser = None
self.context = None
def launch(self, storage_state: dict = None) -> tuple[Browser, BrowserContext]:
"""
Launch browser with stealth configuration.
Returns (browser, context) tuple.
"""
try:
self.playwright = sync_playwright().start()
# Try Firefox first (more stable on macOS), fallback to Chromium
try:
console.print("[dim]Launching Firefox...[/dim]")
self.browser = self.playwright.firefox.launch(
headless=self.config.headless,
timeout=60000
)
except Exception as firefox_error:
console.print(f"[yellow]Firefox failed: {firefox_error}[/yellow]")
console.print("[dim]Trying Chromium...[/dim]")
# Fallback to Chromium with compatibility flags
launch_args = [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-dev-shm-usage',
]
self.browser = self.playwright.chromium.launch(
headless=self.config.headless,
args=launch_args,
timeout=60000
)
except Exception as e:
console.print(f"[red]Failed to launch browser: {e}[/red]")
console.print("\n[yellow]Try running:[/yellow]")
console.print(" [cyan]playwright install firefox[/cyan]")
console.print(" [cyan]playwright install chromium[/cyan]")
raise
# Random User-Agent
user_agent = random.choice(self.config.user_agents)
# Random viewport
viewport_width = random.randint(1820, 2020)
viewport_height = random.randint(980, 1180)
# Create context with stealth settings
context_options = {
'user_agent': user_agent,
'viewport': {'width': viewport_width, 'height': viewport_height},
'locale': 'en-US',
'timezone_id': 'America/New_York',
}
# Add storage state if provided
if storage_state:
context_options['storage_state'] = storage_state
self.context = self.browser.new_context(**context_options)
# Override navigator.webdriver
self.context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")
console.print(f"[dim]Browser launched (headless={self.config.headless})[/dim]")
return self.browser, self.context
def close(self):
"""Close browser and cleanup."""
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
console.print("[dim]Browser closed[/dim]")
def random_delay(self, min_seconds: int = None, max_seconds: int = None):
"""Sleep for a random duration to mimic human behavior."""
min_s = min_seconds or self.config.min_delay_seconds
max_s = max_seconds or self.config.max_delay_seconds
delay = random.uniform(min_s, max_s)
time.sleep(delay)
def open_profile_tab(self, context: BrowserContext, url: str) -> Page:
"""
Open a candidate profile in a new tab.
Returns the new page object.
"""
profile_page = context.new_page()
profile_page.goto(url, timeout=30000)
profile_page.wait_for_load_state('networkidle', timeout=10000)
self.random_delay(2, 4) # Brief delay after page load
return profile_page
def close_tab(self, page: Page):
"""Close a tab safely."""
try:
page.close()
except Exception as e:
console.print(f"[yellow]Warning: Error closing tab: {e}[/yellow]")
def cleanup_orphaned_tabs(self, context: BrowserContext, feed_page: Page):
"""Close all tabs except the feed page."""
try:
for page in context.pages:
if page != feed_page:
page.close()
except Exception as e:
console.print(f"[yellow]Warning: Error cleaning up tabs: {e}[/yellow]")
class FeedNavigator:
"""Handles navigation and scraping of the cofounder feed."""
def __init__(self, browser_manager: BrowserManager):
"""Initialize feed navigator."""
self.browser_manager = browser_manager
def get_candidate_cards(self, page: Page) -> list[dict]:
"""
Extract candidate card information from the feed.
Returns list of dicts with 'url' and 'candidate_id'.
"""
try:
# Wait for feed to load
page.wait_for_load_state('networkidle', timeout=10000)
# Find all candidate cards (adjust selector based on actual HTML)
# This is a placeholder - will need to be updated based on actual page structure
candidate_links = page.query_selector_all('a[href*="/cofounder-matching/"]')
candidates = []
seen_urls = set()
for link in candidate_links:
href = link.get_attribute('href')
if not href or href in seen_urls:
continue
# Build full URL if relative
if href.startswith('/'):
full_url = f"https://www.startupschool.org{href}"
else:
full_url = href
# Extract candidate ID from URL
# URL format might be: /cofounder-matching/candidate/abc-123
parts = href.strip('/').split('/')
candidate_id = parts[-1] if parts else 'unknown'
candidates.append({
'url': full_url,
'candidate_id': candidate_id
})
seen_urls.add(href)
console.print(f"[dim]Found {len(candidates)} candidate cards in feed[/dim]")
return candidates
except Exception as e:
console.print(f"[red]Error extracting candidate cards: {e}[/red]")
return []
def scroll_feed(self, page: Page, scroll_count: int = 3):
"""Scroll the feed to load more candidates."""
try:
for i in range(scroll_count):
page.evaluate('window.scrollBy(0, window.innerHeight)')
self.browser_manager.random_delay(1, 2)
except Exception as e:
console.print(f"[yellow]Warning: Error scrolling feed: {e}[/yellow]")