Home (All Topics) → Technology → Software → AI-Created Software
The following is AI-generated (aka vibe-coded) software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.
Crawl the sitemaps of one or more sites, store every discovered page URL in SQLite along with a product name guessed from the URL, then fuzzy-match a list of known product names against those URL-derived names.
Useful for answering “which of these 800 products does this retailer actually list, and at what URL?” without scraping an entire site.
robots.txt and follow any Sitemap: lines. If there are none, probe the usual suspects (/sitemap.xml, /sitemap_index.xml, /wp-sitemap.xml, and a few others).product, shop, collections, locale codes) and normalize it: strip accents, lowercase, split on non-alphanumerics, drop filler tokens and long numeric IDs.difflib.SequenceMatcher), Dice coefficient, and token containment. The best hit above --min-score wins; a close runner-up flags the match as ambiguous.<title>, then score that against the product name.Matching is per-site: each root URL gets its own candidate pool and its own block of output.
curl_cffi — the only third-party dependency, used to fetch with a Chrome TLS fingerprint so that WAF-protected sites don’t block the crawlTwo plain-text files, one entry per line. Blank lines and lines starting with # are ignored, and a UTF-8 BOM is tolerated.
urls.txt — one root URL per line. A missing scheme is filled in as https://, and trailing slashes are stripped.
https://example-shop.com
another-shop.co.uk
products.txt — one product name per line.
Acme Widget Pro 3000
Blue Titanium Kettle 1.7L
Re-match against URLs already in the database, fetching nothing:
python sitemap-product-matcher.py --urls-file-path urls.txt --products-file-path products.txt --use-existing
Keep what’s stored and only crawl sites that have no URLs yet:
python sitemap-product-matcher.py --urls-file-path urls.txt --products-file-path products.txt --resume
Restrict the crawl to product pages and confirm each hit against the live page:
python sitemap-product-matcher.py --urls-file-path urls.txt --products-file-path products.txt --include-regex "/product/" --verify
Show the top three candidates per product, plus the products that missed:
python sitemap-product-matcher.py --urls-file-path urls.txt --products-file-path products.txt --top 3 --show-misses --verbose
Write everything to a specific folder:
python sitemap-product-matcher.py --urls-file-path urls.txt --products-file-path products.txt --output-path D:\scans026-09
Everything the script writes goes to --output-path (default: the current folder).
| File | Contents |
|---|---|
<urls>_<products>.db |
SQLite database of URLs, sitemap docs, products, and matches. Deleted and rebuilt on each crawl run. |
<urls>_<products>.cache.db |
HTTP cache, kept across runs. Disable with --no-cache, wipe with --clear-cache. |
Matches are also printed to the console:
[0.94 @tier 0.93] Acme Widget Pro 3000
URL : https://example-shop.com/product/acme-widget-pro-3000
NAME: acme widget pro 3000
PAGE: Acme Widget Pro 3000 [sku AW3000, Acme, 249.00 USD, InStock]
verified 1.00 via json-ld
The score is the fuzzy score; the tier is the strictest confidence band it cleared (0.99, 0.93, 0.87, 0.80, 0.73, 0.66, 0.58).AMBIGUOUS appears when the runner-up is within --ambiguity-margin. With --verify, a <-- page disagrees with the URL note appears when the page’s own name scores below --verify-warn.
A run ends with a summary line: sites, URLs stored, products, matches, ambiguous matches, HTTP requests, cache hits, and elapsed time.
sitemap_urls — root_url, sitemap_url, url, url_slug, product_name (the normalized slug). Unique on (root_url, url).sitemap_docs — one row per sitemap document fetched: source (network, cache-fresh, cache-304), URL count, kept count, fetch time.products — raw_name and normalized_name for each input product.matches — the winning URL and score per product per site, plus tier, ambiguity flag, runner-up, and any verification fields (verified_name, verified_sku, verified_brand, verified_price, verified_availability, verified_score).So a run can be queried afterwards rather than re-read from the console:
SELECT product_raw, url, score FROM matches
WHERE url IS NOT NULL AND ambiguous = 0
ORDER BY score DESC;
| Option | Default | Purpose |
|---|---|---|
--urls-file-path |
required | Text file of root URLs |
--products-file-path |
required | Text file of product names |
--output-path |
. |
Folder for everything written; created if needed |
--db-path |
derived | Database name or path. A folder or trailing slash means “put the default name in here”. |
--use-existing |
off | Match against stored URLs; fetch nothing |
--resume |
off | Keep stored data, skip crawling sites that already have URLs |
Applied both while crawling and while matching, so the same filters can be re-applied to an existing database with --use-existing.
| Option | Purpose |
|---|---|
--include-regex RE |
Keep only URLs matching (repeatable) |
--exclude-regex RE |
Drop URLs matching (repeatable) |
--path-contains TEXT |
Keep only URLs containing (repeatable or comma-separated) |
--path-excludes TEXT |
Drop URLs containing (repeatable or comma-separated) |
Exclusions are evaluated first; regexes are case-insensitive.
| Option | Default | Purpose |
|---|---|---|
--timeout |
20.0 |
HTTP timeout in seconds |
--workers |
8 |
Parallel fetches |
--delay |
0.1 |
Minimum seconds between requests to the same host, enforced across threads |
--retries |
2 |
Retries on timeouts and 408/425/429/5xx |
--backoff |
1.0 |
Base for exponential backoff, with jitter; Retry-After is honoured |
--max-retry-wait |
30.0 |
Cap on any single wait |
--user-agent |
Chrome 124 | Request user agent |
Bodies are gzipped into SQLite alongside their ETag and Last-Modified, so re-crawls send conditional requests and mostly get 304s back.
| Option | Default | Purpose |
|---|---|---|
--cache-db |
<db>.cache.db |
Cache filename or path |
--no-cache |
off | Neither read nor write the cache |
--cache-ttl |
0.0 |
Reuse cached bodies without revalidating for this many seconds (0 = always revalidate) |
--clear-cache |
off | Delete the cache before starting |
| Option | Default | Purpose |
|---|---|---|
--max-sitemaps |
250 |
Cap on sitemap documents per site |
--max-urls |
0 |
Cap on URLs stored per site (0 = unlimited) |
--batch-size |
2000 |
Rows buffered before writing to SQLite |
| Option | Default | Purpose |
|---|---|---|
--min-score |
0.58 |
Loosest acceptable score |
--pool-limit |
400 |
Candidates fully scored per product |
--max-postings |
50000 |
Cap on index entries walked while shortlisting one product |
--full-scan-limit |
5000 |
Score every URL on a site this small when no tokens matched |
--top |
1 |
Candidates shown per product |
--ambiguity-margin |
0.03 |
Flag a match when the runner-up is this close |
--show-misses |
off | List unmatched products and their best below-threshold hit |
Raising --min-score above 0.80 cuts false positives sharply on large catalogues; lowering it below 0.58 mostly produces noise that --show-misses would have surfaced anyway.
| Option | Default | Purpose |
|---|---|---|
--verify |
off | Fetch each matched URL and read JSON-LD / OpenGraph data |
--verify-max-bytes |
400000 |
Bytes read per verified page |
--verify-warn |
0.55 |
Flag a match when the page’s own name scores below this |
Verification fetches bypass the HTTP cache, and one request is made per matched URL, so --verify on a few hundred products is a few hundred extra requests.
--verbose prints per-sitemap crawl progress and any fetch or parse problems to stderr.
Shortlisting is what keeps large sites tractable. Instead of scoring every URL against every product, the script intersects posting lists for the product’s rarest tokens, cheapest list first, backing off whenever a token would empty the set. Intersecting rather than unioning matters on catalogues built from a small vocabulary, where every individual token is common but the combination is not. When a product shares no exact token with anything, a 4-character prefix index catches typos, plurals, and suffixes; if that also fails and the site is under --full-scan-limit URLs, everything gets scored.
Scoring itself short-circuits aggressively: a length-based ceiling, then real_quick_ratio and quick_ratio, before any full SequenceMatcher.ratio() call.
| Code | Meaning |
|---|---|
0 |
Completed |
2 |
Bad arguments: missing input file, empty input, unusable output folder, bad filter regex, or --use-existing with no database |
130 |
Interrupted with Ctrl-C |
sitemap.xml.gz) are only handled when the server sets Content-Encoding: gzip; a body served as raw application/gzip will fail to parse and be reported as “no entries”./p/8827461) matching will mostly miss; --verify on a filtered crawl is the workaround, at the cost of a request per page.--resume or --use-existing to keep what’s there.robots.txt Disallow enforcement — only Sitemap: lines are read. Be considerate with --delay and --workers.

Leave a Reply