MAP Violation Detection: How to Build a Scraper in Python

Share:

MAP violation detection

MAP violation detection requires continuously monitoring retailer pricing and comparing advertised prices against approved MAP thresholds. While manual checks may work for a handful of products, they quickly become impractical across multiple retailers and large product catalogs.

This tutorial shows you how to build a basic MAP violation detection scraper in Python that extracts retailer pricing, normalizes the data, compares it against MAP, and generates automated violation alerts and reports.

How Does a MAP Monitoring Scraper Work?

At a high level, an automated MAP violation detection tool follows a simple five-step data workflow:

  1. Reference Database: Maintain a record of your SKUs, their official MAP prices, and the product URLs of the retail sites you want to monitor.
  2. Web Scraping Engine: Programmatically request each retailer’s product page.
  3. Price Parser: Parse the raw HTML page, locate the price element, and clean up currency formatting.
  4. MAP Comparison Engine: Compare the extracted price with the allowed MAP. If the scraped price is lower than the MAP, calculate the violation margin.
  5. Alerting System: Log the violation to a CSV report and instantly send an alert (like a Slack notification or email) to your brand protection team.

Let’s walk through how to build a lightweight script to detect MAP violations across marketplaces.

A flowchart showing steps to detect MAP violation programmatically

How to Detect Map Violation: Step-by-Step Python Tutorial

1. Set Up the Product Directory

Every MAP monitoring run starts with a list of products to check. Create a CSV file named products_map.csv containing the product SKU, its approved MAP price, the retailer to monitor, and the corresponding product URL.

A single SKU can appear multiple times because each retailer has its own product page. During the scan, the scraper processes each row independently, compares the advertised price against the configured MAP, and flags any violations.

sku,name,map_price,retailer,url
SKU-SH-101,Pro-Level Noise Cancelling Headphones,199.99,SoundWholesale,https://www.soundwholesale.com/item/pro-headphones-101
SKU-SH-101,Pro-Level Noise Cancelling Headphones,199.99,MegaRetailer,https://www.megaretailer.com/products/pro-headphones-sh101
SKU-SH-102,UltraFit Sport Smartwatch,149.99,GizmoGalaxy,https://www.gizmogalaxy.com/products/ultrafit-sport-watch
SKU-SH-102,UltraFit Sport Smartwatch,149.99,DiscountTech,https://www.discounttech.com/sports/ultrafit-smartwatch
SKU-SH-103,EcoShield Stainless Steel Water Bottle,29.99,EcoWilderness,https://www.ecowilderness.com/gear/ecoshield-bottle
SKU-SH-103,EcoShield Stainless Steel Water Bottle,29.99,QuickShop,https://www.quickshop.com/ecoshield-water-bottle-silver

The product directory tells the scraper what to monitor.  Next, configure simulation mode, which provides sample retailer pages for testing before scraping live websites. 

2. Configure Simulation Mode

Before scraping live retailer websites, test the scraper against a controlled set of HTML pages. This script uses simulation mode, which loads mock retailer pages instead of making HTTP requests. It lets you verify that the parsing logic works as expected without relying on network connectivity or changing website layouts.

Begin by defining a dictionary of mock HTML pages. Each entry represents a retailer product page and contains the HTML the scraper will parse during simulation mode.

# Mock HTML data for simulation mode
# Enables running the script immediately without network dependency.
MOCK_HTML_PAGES = {
    "https://www.soundwholesale.com/item/pro-headphones-101": """
        <html>
            <body>
                <div class="product-container">
                    <h1 class="product-title">Pro-Level Noise Cancelling Headphones</h1>
                    <div class="price-container">
                        <span class="wholesale-price">$189.99</span>
                    </div>
                </div>
            </body>
        </html>

    """,
    "https://www.megaretailer.com/products/pro-headphones-sh101": """
        <html>
            <body>
                <main>
                    <h1>Pro-Level Noise Cancelling Headphones</h1>
                    <div id="pricing-element">
                        <span class="price-val">$205.00</span>
                    </div>
                </main>
            </body>
        </html>

    """,
    "https://www.gizmogalaxy.com/products/ultrafit-sport-watch": """
        <html>
            <body>
                <section class="details">
                    <h2 class="title">UltraFit Sport Smartwatch</h2>
                    <span class="current-price">$135.00</span>
                </section>
            </body>
        </html>

    """,
    "https://www.discounttech.com/sports/ultrafit-smartwatch": """
        <html>
            <body>
                <div class="content">
                    <div class="product-name">UltraFit Sport Smartwatch</div>
                    <div class="discount-price">$149.99</div>
                </div>
            </body>
        </html>

    """,
    "https://www.ecowilderness.com/gear/ecoshield-bottle": """
        <html>
            <body>
                <span class="price">$29.99</span>
            </body>
        </html>

    """,
    "https://www.quickshop.com/ecoshield-water-bottle-silver": """
        <html>
            <body>
                <div class="product-card">
                    <div class="item-price">$24.99</div>
                </div>
            </body>
        </html>
    """
}

Next, define a CSS selector for each retailer that identifies the HTML element containing the advertised price. During scraping, the parser uses these selectors to extract the price from the corresponding HTML page. 

import os
import re
import csv
import sys
import time
import argparse
from datetime import datetime
from bs4 import BeautifulSoup
import requests

# Define selector mapping for different retailers
# Demonstrates the selector maintenance challenge in multi-retailer tracking.
RETAILER_SELECTORS = {
    "SoundWholesale": "span.wholesale-price",
    "MegaRetailer": "span.price-val",
    "GizmoGalaxy": "span.current-price",
    "DiscountTech": "div.discount-price",
    "EcoWilderness": "span.price",
    "QuickShop": "div.item-price"
}

In production, maintaining these selectors becomes one of the biggest challenges. Retailers frequently redesign product pages or rename HTML classes, so even small layout changes can prevent the scraper from locating the correct price until the selector is updated.

Pro-Tip: How to Find HTML Selectors Using the “Inspect” Tool

Before you can tell a web scraper what to click or extract, you need to find the element’s exact identifier hidden in the website’s code. Here is the standard workflow using Google Chrome:

  1. Open Developer Tools: Navigate to the competitor’s product page. Right-click anywhere on the page and select Inspect (or press F12 / Cmd+Option+I on Mac). A panel showing the site’s underlying HTML will open.
  2. Activate the Selector Tool: In the top-left corner of the Developer Tools panel, click the small icon that looks like a cursor clicking a square (or press Ctrl+Shift+C).
  3. Hover and Click: Move your mouse over the actual web page and click the exact element you want to scrape (e.g., the ZIP code input box or the delivery date text).
  4. Extract the Locator: The Developer Tools will immediately highlight the corresponding HTML code for that element.
    • Identify the attributes: Look closely at the highlighted line for attributes like id=”GLUXZipUpdateInput” or class=”a-color-success”.
    • The Shortcut: Right-click the highlighted line of code, hover over Copy, and select Copy selector (for CSS) or Copy XPath. You can paste this value directly into your Selenium script!

3. Fetch and Parse Product Prices

The scraper now has everything it needs to begin monitoring prices from retailer pages. 

The next step is to extract prices. This step retrieves the HTML for each retailer, locates the price using the configured CSS selector, and converts the extracted text into a numeric value that it can compare against the product’s MAP price.

Before comparing prices, the scraper first normalizes the extracted text into a consistent numeric format. This is the role of the clean_price() function. 

def clean_price(raw_price_str):
    """
    Cleans raw price strings extracted from HTML (e.g. '$199.99 USD' or '  $99 ')
    and converts them to float.
    """
    if not raw_price_str:
        return None
    clean_str = re.sub(r'[^\d.]', '', raw_price_str)
    try:
        return float(clean_str)
    except ValueError:
        return None

The clean_price() function removes currency symbols, whitespace, and other non-numeric characters before converting the value to a floating-point number. This ensures prices from different retailers can be compared consistently. 

Next, write a fetch_price() function that combines the rest of the workflow. 

The fetch_price() function can run in simulation and live modes. In simulation mode, the scraper loads HTML from the MOCK_HTML_PAGES dictionary. When you run the script with the –live flag, it instead sends an HTTP request to the retailer’s website using a browser-like user agent.

Note: HTTP requests work well for websites that return their product data in the initial HTML response. Sites that render prices with JavaScript typically require a headless browser such as Playwright or Selenium, which we’ll discuss later.

def fetch_price(url, retailer, simulate=True):
    """
    Fetches the price from the given URL.
    Uses simulated HTML pages if simulate is True, otherwise makes live HTTP requests.
    """
    selector = RETAILER_SELECTORS.get(retailer)
    if not selector:
        print(f"[-] No selector configured for retailer: {retailer}")
        return None

    if simulate:
        # Simulation Mode: Returns mock HTML to ensure code runs out-of-the-box
        time.sleep(0.5)
        html_content = MOCK_HTML_PAGES.get(url)
        if not html_content:
            print(f"[-] Simulation URL not found: {url}")
            return None
        soup = BeautifulSoup(html_content, 'html.parser')
    else:
        # Live Scraping Mode (Requires network and target server availability)
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        }
        try:
            response = requests.get(url, headers=headers, timeout=10)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
        except Exception as e:
            print(f"[-] HTTP error occurred while scraping {url}: {e}")
            return None
    price_element = soup.select_one(selector)
    if price_element:
        raw_price = price_element.get_text()
        return clean_price(raw_price)
    return None

The fetch_price() function combines all of the components configured so far. It loads the retailer page, selects the appropriate CSS selector, extracts the advertised price, and returns a normalized value ready for comparison. 

Now that the scraper can retrieve prices, compare them against the configured MAP policy and generate a violation report. 

Want to know how to scrape sites in the real world? Read this article on scraping websites using Python.

4. Detect MAP Violations and Generate Reports

After extracting a product’s advertised price, compare it against the configured MAP price. If the advertised price falls below the threshold, calculate the price difference, record the violation, and generate an alert.

#send alerts

def send_alert(sku, product_name, retailer, map_price, scraped_price, url):
 
    deviation = map_price - scraped_price
    percent_below = (deviation / map_price) * 100

    print("\n" + "="*60)
    print("🚨 [ALERT] MAP VIOLATION DETECTED")
    print(f"Product:      {product_name} ({sku})")
    print(f"Retailer:     {retailer}")
    print(f"MAP Policy:   ${map_price:.2f}")
    print(f"Advertised:   ${scraped_price:.2f} (Under by ${deviation:.2f} / {percent_below:.1f}%)")
    print(f"Retailer Link: {url}")
    print("="*60 + "\n")

Finally, the main() function ties the entire workflow together. It reads the product directory, checks each retailer page, compares the extracted price against the MAP policy, and logs any violations to a CSV report. 

In simulation mode, alerts are printed to the console. In a production environment, you could replace these with Slack notifications, emails, or other alerting systems.

#main function
def main():
    parser = argparse.ArgumentParser(description="MAP Violation Detector Scraper")
    parser.add_argument("--csv", default="products_map.csv", help="Path to input products CSV")
    parser.add_argument("--live", action="store_true", help="Run in live scraping mode")
    args = parser.parse_args()

    simulate = not args.live

    if not os.path.exists(args.csv):
        print(f"[-] Error: Input CSV file not found.")
        sys.exit(1)

    violations = []
    total_checked = 0

    with open(args.csv, mode='r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            sku = row['sku']
            name = row['name']
            map_val = clean_price(row['map_price'])
            retailer = row['retailer']
            url = row['url']

            total_checked += 1
            print(f"[*] Checking {retailer} for {name} ({sku})...")

            scraped_price = fetch_price(url, retailer, simulate=simulate)

            if scraped_price is None:
                print(f"    [-] Failed to extract price.")
                continue

            # Compare Scraped Price to MAP Policy
            if scraped_price < map_val:
                deviation = map_val - scraped_price
                percent_below = (deviation / map_val) * 100

                violation_record = {
                    "sku": sku,
                    "name": name,
                    "retailer": retailer,
                    "map_price": map_val,
                    "scraped_price": scraped_price,
                    "deviation": round(deviation, 2),
                    "percent_below": round(percent_below, 2),
                    "url": url,
                    "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                }
                violations.append(violation_record)
                send_alert(sku, name, retailer, map_val, scraped_price, url)
            else:
                print(f"    [+] Compliant price: ${scraped_price:.2f}")

    # Output to Violations CSV Report
    if violations:
        fieldnames = ["sku", "name", "retailer", "map_price", "scraped_price", "deviation", "percent_below", "url", "timestamp"]
        with open("map_violations_report.csv", mode='w', newline='', encoding='utf-8') as rf:
            writer = csv.DictWriter(rf, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(violations)
        print(f"\n[+] Logged {len(violations)} MAP violations to 'map_violations_report.csv'.")

if __name__ == "__main__":
    main()

The resulting report provides a centralized record of every detected violation, including the product, retailer, advertised price, MAP price, deviation, and timestamp. 

5. Run the Full Script

Run the script in simulation mode to validate the workflow using the mock retailer pages: 

python map_detector.py --csv products_map.csv

Once you’ve replaced the mock pages with real retailer URLs and selectors, add the –live flag to scrape live websites: 

python map_detector.py --csv products_map.csv --live

The script above works well for a small, controlled set of retailers with simple HTML. The next section covers what happens when you try to run this same approach against dozens of retailers with modern anti-bot protection.

Challenges of Scaling a DIY MAP Monitor 

The tutorial demonstrates the core components of a MAP monitoring scraper. Running the same approach at production scale introduces additional engineering challenges. 

1. The Anti-Bot Wall

Most top-tier retailers use sophisticated anti-bot platforms. A standard Python script using basic HTTP requests will typically trigger 403 errors or CAPTCHAs and fail outright. Getting past this in production generally requires:

  • Headless browser automation that can render JavaScript the way a real browser would.
  • Fingerprint and stealth patching to avoid signals that flag automated traffic, such as automation-specific browser flags, unusual WebGL configurations, and inconsistent user-agent strings.
  • Rotating proxy networks, typically residential or mobile IP pools, to avoid getting rate-limited or blocked at the IP level.

Anti-bot systems are updated frequently, which turns an in-house scraper into an ongoing maintenance commitment rather than a one-time build.

2. The Selector Maintenance Problem

The RETAILER_SELECTORS mapping created earlier works well for a small set of retailers. 

In production, however, ecommerce sites frequently redesign their pages. If a retailer changes a class name or restructures its price container, the scraper silently returns None for that product until someone notices and fixes the selector. 

The more retailers you track, the more frequently this happens, and the more ongoing engineering attention it demands.

3. Localization and Geographic Discrepancies

Marketplaces often show different prices based on zip code or delivery location. A product might be compliant when crawled from one location and in violation for shoppers browsing from another. Capturing these discrepancies requires geo-targeted crawling, which adds another layer of infrastructure most in-house scrapers aren’t built to handle.

These operational challenges often determine whether it makes sense to maintain a scraper in-house or use a managed data provider. 

Buy vs. Build: Choosing Automated MAP Monitoring

Maintaining a web crawling framework in-house pulls engineering time away from your core product. The tradeoffs generally look like this:

Cost Factor Build (In-House Scraping) Buy (Managed Data-as-a-Service)
Proxy Infrastructure Ongoing cost for residential/mobile IP pools Included in service
Engineering Time Recurring time spent fixing broken selectors and anti-bot bypasses Minimal to none
Infrastructure Servers to host and scale headless browser clusters Handled by provider
Data Quality Gaps when scrapers silently fail Validated, monitored data delivery

Whether you build or buy depends largely on scale. For a handful of retailers, an in-house scraper may be sufficient. At enterprise scale, maintaining reliable data collection typically becomes the greater challenge, which is why many organizations choose a managed web crawling service instead. 

Scraping publicly available product pages for pricing data is generally permissible, and courts have repeatedly affirmed that collecting publicly accessible information does not violate anti-hacking statutes like the CFAA. 

That said, retailers’ terms of service may restrict automated access, and how you collect, store, and use the data still matters. If you’re monitoring at scale or across many retailers, it’s worth having your legal team review your specific approach, or working with a web crawling service that already builds compliance into its collection process.

Wrapping Up

The Python scraper in this tutorial shows the core mechanics of MAP violation detection and can work well for small-scale projects or internal proof-of-concepts.

As monitoring expands across more retailers, however, the challenge often shifts from detecting violations to maintaining reliable data collection. Anti-bot protections, selector changes, proxy management, and geographic pricing variations can quickly become ongoing operational work.

For organizations that need continuous MAP monitoring without dedicating engineering resources to crawler maintenance, a web scraping service can provide an alternative approach. Instead of operating scraping infrastructure internally, teams receive structured pricing data through APIs while the provider handles maintenance.

ScrapeHero helps brands collect pricing data across retailers for MAP enforcement, competitive intelligence, and brand monitoring initiatives. Data can be delivered in CSV, JSON, database exports, or custom APIs depending on operational requirements. Contact ScrapeHero today to handle the data collection while your team focuses on analysis and action. 

FAQs

Can this scraper monitor JavaScript-heavy ecommerce websites?

Not reliably. The tutorial uses Python’s requests library, which only retrieves the initial HTML returned by the server. Retailers that load prices with JavaScript or protect their sites with anti-bot systems typically require headless browser automation, proxy rotation, or other advanced scraping techniques.

How can I automate MAP monitoring?

You can schedule the script to run automatically using tools such as cron on Linux or Task Scheduler on Windows. In production, many teams also replace console alerts with Slack notifications, email alerts, or integrations with ticketing and monitoring platforms.

When should I build a MAP monitoring scraper instead of using a managed service?

Building an in-house scraper works well for prototypes, internal tools, or monitoring a small number of retailers. As the number of monitored sites grows, ongoing selector maintenance, anti-bot mitigation, and infrastructure management often become the primary challenges, making a managed data service a more practical option.

Table of contents

Scrape any website, any format, no sweat.

ScrapeHero is the real deal for enterprise-grade scraping.

Clients love ScrapeHero on G2

Ready to turn the internet into meaningful and usable data?

Contact us to schedule a brief, introductory call with our experts and learn how we can assist your needs.

Continue Reading

Delivery promise changes across marketplace

The Logistics Lie Detector: Tracking Delivery Promise Changes Across Marketplaces

Track and monitor delivery promise changes across marketplaces in 2026.
Detect stockouts on competitor listings

Gain the Competitive Edge: Detect Stock Outs on Competitor Listings

Use Python web scraping tools to detect real-time stockouts on competitor sites.
Scraping vs native APIs

Best for Pricing Intelligence: Scraping vs. Native APIs

Compare native APIs and web scraping for 2026 pricing intelligence strategies.
ScrapeHero Logo

Can we help you get some data?