Introduction
Keeping track of changes on websites—whether it's competitor pricing, job listings, government policy updates, or product release notes—is a common need for developers and businesses alike. But writing a full-blown web scraper from scratch for every site you want to monitor? That's brittle, time-consuming, and often breaks the moment the target site tweaks its layout.
Enter the Web to Markdown/JSON API: a simple, powerful API that converts any webpage into clean, structured Markdown or JSON in a single request. It handles all the messy HTML parsing, boilerplate stripping, and formatting so you can focus on what actually matters—detecting changes and acting on them.
In this tutorial, you'll build a website change monitor in Python that:
- Fetches any web page as clean Markdown via the API
- Compares snapshots over time to detect meaningful changes
- Sends email alerts when something important shifts
- Runs on a cron schedule for fully automated monitoring
Let's dive in.
The API at a Glance
Endpoint: POST https://web2md-api-production-d822.up.railway.app/extract
Request body:
{
"url": "https://example.com",
"format": "markdown",
"max_length": 50000
}
Formats supported: markdown, json, text
Free tier: 50 requests per day
RapidAPI page: web-to-markdown-json-api
This API does one thing and does it well: give it a URL, and it returns the page's meaningful content stripped of navigation, ads, and scripting cruft. Exactly what you need for change detection—no BeautifulSoup or regex wrestling required.
Step 1: Fetching a Page Snapshot
Create a file called monitor.py. We'll start with a function that takes a URL and returns the clean Markdown content:
import requests
API_ENDPOINT = "https://web2md-api-production-d822.up.railway.app/extract"
def fetch_snapshot(url: str, format: str = "markdown", max_length: int = 50000) -> str:
resp = requests.post(
API_ENDPOINT,
json={"url": url, "format": format, "max_length": max_length},
timeout=30
)
resp.raise_for_status()
data = resp.json()
return data.get("markdown") or data.get("content", "")
# Quick test
if __name__ == "__main__":
content = fetch_snapshot("https://example.com")
print(content[:500])
print(f"\n--- Total length: {len(content)} characters")
Run it and you'll see clean, readable Markdown instead of raw HTML—a huge win before we even start the monitoring logic.
Step 2: Storing and Comparing Snapshots
To detect changes, we need to store previous snapshots and compare them against new ones. We'll use a simple JSON file as our snapshot database:
import hashlib
import json
import os
from datetime import datetime
SNAPSHOT_FILE = "snapshots.json"
def load_snapshots() -> dict:
if os.path.exists(SNAPSHOT_FILE):
with open(SNAPSHOT_FILE) as f:
return json.load(f)
return {}
def save_snapshots(snapshots: dict):
with open(SNAPSHOT_FILE, "w") as f:
json.dump(snapshots, f, indent=2, default=str)
def compute_hash(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
def compare_snapshot(url: str, new_content: str):
snapshots = load_snapshots()
new_hash = compute_hash(new_content)
if url not in snapshots:
snapshots[url] = {
"hash": new_hash,
"timestamp": datetime.now().isoformat(),
"length": len(new_content)
}
save_snapshots(snapshots)
return None # Baseline established
old = snapshots[url]
if old["hash"] == new_hash:
return None # No change
length_delta = len(new_content) - old.get("length", 0)
snapshots[url] = {
"hash": new_hash,
"timestamp": datetime.now().isoformat(),
"length": len(new_content)
}
save_snapshots(snapshots)
return {
"url": url,
"previous_checked": old["timestamp"],
"length_delta": length_delta,
"length_delta_pct": round(
(length_delta / old["length"]) * 100, 1
) if old["length"] > 0 else 0
}
This gives us hash-based equality checks (fast and reliable) plus a quick size-delta metric.
Step 3: Detecting Meaningful Changes
Not all changes matter. A page might change its timestamp, or a random ad insertion could shift content slightly. Let's add smart thresholds:
CHANGE_THRESHOLD = 0.02 # 2% content change before we alert
def detect_meaningful_change(url: str, new_content: str):
diff = compare_snapshot(url, new_content)
if diff is None:
return None
abs_pct = abs(diff["length_delta_pct"])
if abs_pct < CHANGE_THRESHOLD * 100:
print(f"Change too small ({abs_pct}%), skipping alert for {url}")
return None
return diff
Tune CHANGE_THRESHOLD per URL—higher for noisy pages, lower for pages where every word counts.
Step 4: Sending Email Alerts
When a meaningful change is detected, fire off an email using Python's built-in smtplib:
import smtplib
from email.mime.text import MIMEText
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
SMTP_USER = "alerts@yourdomain.com"
SMTP_PASS = "your-app-password"
ALERT_EMAIL = "you@yourdomain.com"
def send_alert(change: dict):
subject = f"Web Change Detected: {change['url']}"
body = (
f"A change was detected on a monitored page.\n\n"
f"URL: {change['url']}\n"
f"Last checked: {change['previous_checked']}\n"
f"Size change: {change['length_delta']:+d} chars "
f"({change['length_delta_pct']:+}%)\n\n"
f"See: {change['url']}\n"
)
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = SMTP_USER
msg["To"] = ALERT_EMAIL
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.send_message(msg)
print(f"Alert sent for {change['url']}")
Step 5: Putting It All Together
The main monitoring loop—feed it a list of URLs and it checks them all:
import time
MONITORED_URLS = [
"https://example.com/pricing",
"https://example.com/changelog",
"https://example.com/jobs",
]
def run_monitor(urls: list[str]):
print(
f"Starting monitor for {len(urls)} URLs "
f"at {datetime.now().isoformat()}"
)
for url in urls:
try:
print(f"Checking: {url}")
content = fetch_snapshot(url)
change = detect_meaningful_change(url, content)
if change:
print(
f" -> CHANGE DETECTED! "
f"Size delta: {change['length_delta']:+d} chars"
)
# send_alert(change) # Uncomment when SMTP configured
else:
print(f" -> No meaningful change")
time.sleep(1) # Respect free tier limits
except Exception as e:
print(f" -> ERROR for {url}: {e}")
print("Monitor run complete.")
if __name__ == "__main__":
run_monitor(MONITORED_URLS)
Step 6: Scheduling with Cron
Once working, schedule it to run daily:
# Every day at 8 AM
0 8 * * * cd /path/to/project && python3 monitor.py >> monitor.log 2>&1
# Every 6 hours for fast-moving pages
0 */6 * * * cd /path/to/project && python3 monitor.py >> monitor.log 2>&1
With the free tier (50 requests/day), monitor 40-45 URLs daily with a safety buffer.
Advanced Ideas
1. Diff Highlighting
Store full Markdown content and use difflib for human-readable diffs:
import difflib
def generate_diff(old: str, new: str) -> str:
return "\n".join(difflib.unified_diff(
old.splitlines(), new.splitlines(), lineterm=""
))
2. Per-URL Thresholds
Some pages change frequently, others rarely:
URL_CONFIG = {
"https://example.com/news": {"threshold": 0.10},
"https://example.com/terms": {"threshold": 0.01},
}
3. JSON Format for Structured Data
Switch format to "json" when monitoring structured fields:
resp = requests.post(
API_ENDPOINT,
json={
"url": "https://example.com/product/123",
"format": "json",
"max_length": 20000
}
)
# resp.json() returns structured data with title, content, metadata
4. LLM-Powered Summaries
Pipe the Markdown diff into an LLM and ask for a one-paragraph summary of what changed. Turns raw deltas into actionable intelligence.
Why This API Over DIY Scraping?
| Aspect | Web to Markdown/JSON API | DIY Scraping |
|---|---|---|
| Setup time | 1 API call | Hours of BeautifulSoup + custom parsing |
| Maintenance | Handled by the API | You fix it every site redesign |
| Content quality | Clean Markdown, boilerplate stripped | Raw HTML to parse yourself |
| Multiple formats | Markdown, JSON, plain text | You implement each |
| Rate limiting | Free tier included | You write retry logic |
Wrapping Up
With under 100 lines of Python and the Web to Markdown/JSON API, you have a fully automated website change monitor that:
- Fetches clean, readable content from any URL
- Compares snapshots and detects meaningful changes
- Alerts by email when something shifts
- Runs on autopilot via cron
This pattern is production-ready for monitoring competitor pricing, government notices, job boards, documentation updates, and anything else on the web that matters.
Try It Now
Grab a key on RapidAPI or hit the endpoint directly:
curl -X POST https://web2md-api-production-d822.up.railway.app/extract \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "format": "markdown", "max_length": 50000}'
The first 50 requests each day are free. Go build something useful!
Top comments (0)