The Scraper's Journey

Act 1
Eddie stared at his terminal in frustration as his simple script failed once again. What had started as innocent curiosity-just wanting to map out documentation sites without dealing with their bloated JavaScript interfaces-had turned into hours of frustration.
"All I wanted was a damn sitemap yo"
he muttered, watching another 403 Forbidden error flash on his screen. His basic requests.get() approach worked fine on his personal blog but crumbled against any site with even basic protection.
The Stack Overflow answers weren't helping:
Just add a User-Agent header.
Use Beautiful Soup.
None addressed the modern web's complexity. His CSV files sat nearly empty, missing the very data he needed.
Act 2
That night, Eddie discovered a post about headless browsers. The concept was elegant; instead of trying to parse raw HTML, why not let a browser render everything first?
"So that's how they do it," he whispered, reading about Selenium and Playwright. The code examples showed browsers loading pages silently, executing JavaScript, and then extracting the fully rendered content.
He spent days experimenting with API interception too, watching network requests in Chrome DevTools, noticing patterns in how sites loaded data. Sometimes the content wasn't in the HTML at all but fetched separately through JSON endpoints.
"The data was there all along!" he realized. "Just not where I was looking."
Act 3
Weeks passed. Eddie's simple sitemap script evolved into something more sophisticated; a hybrid system that could map a site through its sitemap.xml, then strategically render pages when needed, and even intercept API calls directly.
His code now included proper delays between requests. It rotated between different user agents. It respected robots.txt files religiously. It backed off when sites seemed overwhelmed.
"I'm not taking anything they wouldn't give a normal visitor," he reminded himself. "I'm just being more efficient about it."
The transformation wasn't just in his code but in his understanding. The web wasn't just HTML; it was a complex ecosystem of APIs, rendering engines, and protection systems. By understanding them, he could work with them rather than against them.
Act 4
Eddie sat in the glow of his monitor at 3 AM, watching as his crawler methodically indexed technical documentation across dozens of sites. Each page rendered, its content extracted, its links followed.
With great scrapes comes IP overdosage. Several times, he'd had to throttle back his scraper when he noticed it was hitting sites too hard. Once, he'd accidentally followed a pattern that generated infinite URLs, nearly taking down a small documentation site.
"This is a conversation, not a siege" he reminded himself, adding more safeguards to his code.
The pain of change wasn't technical but ethical. He'd built something powerful; Theoretically capable of scraping vast portions of the web. He chose to use it judiciously. He added filters to only collect what he actually needed. He cached aggressively to minimize repeat requests.
And when he finally looked at his local storage...exabytes of perfectly organized, searchable technical documentation; he felt not triumph over the systems he'd circumvented, but gratitude for the knowledge they shared.
"The internet isn't something to conquer" he thought, closing his laptop as dawn broke. "It's something to commune with."
His journey had come full circle: from a simple curiosity about sitemaps to a deeper understanding of the web itself. The power wasn't in having the internet in his local storage; it was in knowing how to become the AI yourself.
Act 1: This is me and it's not working
Validation
def check_robots_txt(url):
parsed_url = urlparse(url)
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
robots_url = f"{base_url}/robots.txt"
Sitemap
def get_sitemap(url):
if not url.endswith('sitemap.xml'):
if url.endswith('/'):
url = url + 'sitemap.xml'
else:
url = url + '/sitemap.xml'
XML
soup = BeautifulSoup(response.content, 'xml')
# sitemaps of sitemaps
sitemapTags = soup.find_all('sitemap')
if sitemapTags:
# Handle sitemap index
...
else:
# Handle regular sitemap
...
This implements a crucial pattern in sitemap crawling - the ability to handle sitemap index files. Large websites often use a hierarchical structure.
Extraction
urls = []
for url_tag in soup.find_all('url'):
loc = url_tag.find('loc').text
lastmod = url_tag.find('lastmod').text if url_tag.find('lastmod') else None
url_data = {'url': loc, 'lastmod': lastmod}
urls.append(url_data)
The extraction focuses on:
<loc>- The actual URL (required by the protocol)<lastmod>- When the content was last modified (optional)
Potentially useful fields:
<changefreq>- How often the page changes<priority>- Relative importance within the site
Current Limitations
- Depth: Only extracts URLs without visiting them
- Completeness: Relies entirely on the sitemap (which may be incomplete)
- Authentication: Can't access protected content
- JavaScript Rendering: Can't render JavaScript-based content
- Rate Limiting: No built-in throttling or delay mechanism
Blocking Mechanisms
- Request patterns - Too many requests too quickly
- Browser fingerprinting - Canvas fingerprinting, WebGL fingerprinting, etc.
- Mouse movement analysis - Humans move mice in non-linear patterns
- CAPTCHA systems - Especially for login-protected content
- HTTP header inspection - Missing or incorrect headers
Act 2: Is there another way
- Proper delays - Not just between requests but with randomized timing
- Complete header sets - Including cookies, referrers, etc.
- Proxy rotation - Using residential proxies (more expensive but less detectable)
- Browser environment spoofing - Mimicking real browser fingerprints
- CAPTCHA solving services - Either automated or human-powered (ethically questionable)
Content Fetching
def fetch_page_content(url):
response = requests.get(url)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
return soup
return None
Throttling & Politeness
import time
def polite_request(url, delay=1.0):
time.sleep(delay) # Wait between requests
return requests.get(url)
Breadth-First Crawling
def crawl_site(start_url, max_pages=100):
visited = set()
queue = [start_url]
results = []
while queue and len(visited) < max_pages:
url = queue.pop(0)
if url in visited:
continue
visited.add(url)
page = fetch_page_content(url)
if not page:
continue
content = extract_data(page)
results.append(content)
links = page.find_all('a', href=True)
for link in links:
absolute_url = urljoin(url, link['href'])
if is_valid_url(absolute_url) and absolute_url not in visited:
queue.append(absolute_url)
return results
Content Parsing & Storage
def extract_data(soup):
data = {}
title_tag = soup.find('title')
if title_tag:
data['title'] = title_tag.text.strip()
content_div = soup.find('div', {'class': 'main-content'})
if content_div:
data['content'] = content_div.text.strip()
return data
Act 3: I have transformed
The JavaScript Challenge
Single Page Applications (SPAs) or heavy JavaScript applications:
- You get a minimal HTML shell
- JavaScript executes to fetch data and render the actual content
- The DOM is built dynamically in your browser
Common solutions to modern problems:
Headless Browsers
- Tools like Puppeteer, Playwright, or Selenium
- They launch actual Chrome/Firefox instances without a UI
- Let JavaScript fully execute before extracting content
- Can interact with the page (click, scroll, type)
- Resource-intensive, slow, complex to manage at scale
API Interception (Elegant but Requires Analysis)
- Instead of scraping the rendered page, identify and call the same APIs the page uses
- Much faster and lighter than headless browsers
- Requires reverse engineering the site's API structure, which changes frequently
Best of Both
- Use headless browsers to understand API patterns
- Then switch to direct API calls for production scraping
- Efficient once set up
- More complex implementation
Selenium example
from selenium import webdriver
def render_javascript(url):
driver = webdriver.Chrome()
driver.get(url)
time.sleep(3)
html = driver.page_source
driver.quit()
return BeautifulSoup(html, 'html.parser')
Act 4: Can I handle the pain of change
Parallel Processing
from concurrent.futures import ThreadPoolExecutor
def parallel_crawl(urls, max_workers=5):
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_url = {executor.submit(fetch_page_content, url): url for url in urls}
for future in as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
results.append(data)
except Exception as exc:
print(f'{url} generated an exception: {exc}')
return results
Fingerprint Management
To avoid getting blocked:
import random
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/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
]
def get_headers(): # for rotating user agents
return {
'User-Agent': random.choice(user_agents),
'Accept': 'text/html,application/xhtml+xml,application/xml',
'Accept-Language': 'en-US,en;q=0.9',
}
Incremental Crawling
To efficiently update your dataset:
def incremental_crawl(site_url, previous_data):
sitemap_data = get_sitemap(site_url)
urls_to_crawl = []
for item in sitemap_data:
url = item['url']
lastmod = item.get('lastmod')
if url not in previous_data or previous_data[url]['lastmod'] != lastmod:
urls_to_crawl.append(url)
new_data = crawl_pages(urls_to_crawl)
for url, data in new_data.items():
previous_data[url] = data
return previous_data
✌️🔚
What Will Get You Blocked
- Hammering the server - Rapid, repeated requests
- Ignoring response codes - Not backing off when you get 429s or 503s
- Spider traps - Following infinite URL patterns (like calendars that go to year 9999)
- Scraping login-protected content - Especially with shared/stolen credentials
- Scrapers from data center IPs - AWS, Digital Ocean, etc. are easily identified
The Ethical Dimension
- Terms of Service - Many explicitly forbid scraping
- Legal precedents - Cases like hiQ vs LinkedIn have mixed outcomes
- Server load - Small sites can be overwhelmed by aggressive scraping
- Content licensing - Just because it's visible doesn't mean it's free to reuse
The most sophisticated scrapers today blend in by behaving exactly like real users - they request pages at human speeds, move virtual mice in human-like patterns, and spread requests across many IP addresses to avoid detection.