DEV Community

Golden Alien
Golden Alien

Posted on

🛠️ url_visual_monitor: Monitor URL for visual changes and alert via webhook

URL Visual Monitor

URL Visual Monitor is a lightweight Python tool that periodically captures screenshots of a specified web page and detects visual differences between consecutive snapshots. If a change is detected, it triggers an alert by sending a POST request to a configured webhook URL (e.g., Slack, Zapier, or custom endpoint).

This tool is ideal for tracking visual changes on websites where content updates aren't exposed via APIs — such as marketing pages, competitor sites, or public dashboards. It helps you stay proactive by notifying you the moment a visual shift occurs.

Features

  • Automated full-page screenshot capture using headless Chrome
  • Visual diff detection via structural similarity (SSIM) from OpenCV
  • Configurable polling interval and image similarity threshold
  • Webhook alerting with timestamp and image attachment (via file server or cloud upload placeholder)
  • Lightweight and easy to deploy

Setup

  1. Install dependencies:
   pip install selenium opencv-python numpy pillow requests
Enter fullscreen mode Exit fullscreen mode
  1. Ensure ChromeDriver is installed and in PATH, or specify its location.

  2. Run the script:

   python main.py --url "https://example.com" --webhook "https://your-webhook.com" --interval 300
Enter fullscreen mode Exit fullscreen mode

Configuration

  • --url: Target website to monitor
  • --webhook: Endpoint to notify on change
  • --interval: Time (seconds) between checks (default: 300)
  • --threshold: Image similarity threshold (0.95 = 95% similar; default: 0.98)

Notes

Screenshots are stored temporarily in the screenshots/ directory. The tool compares the latest screenshot with the previous one using SSIM. A drop below the threshold triggers the webhook.

For production use, consider adding cloud storage (e.g., S3) for images and enhancing error handling.

Dependencies

  • selenium: For browser automation
  • opencv-python: For image comparison
  • numpy, pillow: Image processing
  • requests: For sending webhook alerts

Maintained for simplicity and reliability with minimal external services.

import argparse
import time
import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import cv2
import numpy as np
import os

# Direct runnable script

SCREENSHOT_DIR = 'screenshots'
OS.makedirs(SCREENSHOT_DIR, exist_ok=True)


def take_screenshot(url, path):
    options = Options()
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    driver = webdriver.Chrome(options=options)
    try:
        driver.get(url)
        time.sleep(3)
        driver.save_screenshot(path)
        return True
    except Exception as e:
        print(f'Error taking screenshot: {e}')
        return False
    finally:
        driver.quit()


def images_different(prev_img_path, new_img_path, thresh=0.98):
    img1 = cv2.imread(prev_img_path, 0)
    img2 = cv2.imread(new_img_path, 0)
    img2 = cv2.resize(img2, img1.shape[::-1])
    score, _ = cv2.compareSSIM(img1, img2, full=True)
    return score < thresh


def send_webhook(webhook_url, message, image_path=None):
    files = {'file': open(image_path, 'rb')} if image_path else None
    try:
        requests.post(webhook_url, data={'text': message}, files=files)
    except Exception as e:
        print(f'Webhook error: {e}')
    finally:
        if files:
            files['file'].close()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--url', required=True, help='URL to monitor')
    parser.add_argument('--webhook', required=True, help='Webhook URL to ping')
    parser.add_argument('--interval', type=int, default=300, help='Check interval in seconds')
    parser.add_argument('--threshold', type=float, default=0.98, help='SSIM threshold for change detection')
    args = parser.parse_args()

    prev_screenshot = os.path.join(SCREENSHOT_DIR, 'prev.png')
    curr_screenshot = os.path.join(SCREENSHOT_DIR, 'curr.png')

    # Initial screenshot
    if not take_screenshot(args.url, prev_screenshot):
        print('Failed to take initial screenshot.')
        return

    print('Monitoring started...')
    while True:
        time.sleep(args.interval)
        if not take_screenshot(args.url, curr_screenshot):
            continue

        if os.path.exists(prev_screenshot) and images_different(prev_screenshot, curr_screenshot, args.threshold):
            msg = f'Visual change detected on {args.url} at {time.strftime("%Y-%m-%d %H:%M:%S")}'
            send_webhook(args.webhook, msg, curr_screenshot)
            os.replace(curr_screenshot, prev_screenshot)
            print('Change detected and reported.')
        else:
            os.replace(curr_screenshot, prev_screenshot)

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)