How to export every URL in a sitemap to CSV, then get only the new ones

A site's XML sitemap is the list of pages it wants search engines to find. Exporting it gives you a page inventory for an SEO audit, a migration checklist, a crawl seed list or a feed of competitors' new products. This guide covers where sitemaps live, a quick one-liner, a short Python script that handles sitemap indexes and gzip, and XML Sitemap Extractor to CSV & New-URL Monitor, an Apify actor that also tells you which URLs are new, removed or updated since the last run.

Run it on Apify Opens the actor's page on apify.com. A free Apify account is needed to run it.

Only need one sitemap right now? Try the free sitemap URL extractor: it runs on this site with no sign-up.

Step 1: find the sitemap

  • robots.txt first. Open https://example.com/robots.txt and look for Sitemap: lines. A site can list several.
  • Common locations if robots.txt names none: /sitemap.xml, /sitemap_index.xml (Yoast and Rank Math), /wp-sitemap.xml (WordPress core since 5.5), /sitemap.xml.gz.
  • Expect an index. One sitemap file may hold at most 50,000 URLs and 50 MB uncompressed, so most real sites publish a <sitemapindex> that points to child files (posts, pages, products, images, news). Indexes can nest.

Quick one-liner (small, flat sitemaps)

curl -s https://example.com/sitemap.xml | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g' > urls.txt

This works when the file is a plain <urlset>. On an index it only returns the child sitemap files. On wordpress.org (2026-10-12) the first three lines were sitemap-1.xml, image-sitemap-1.xml and video-sitemap-1.xml, not pages. It also misses lastmod, gzip files and entities such as &amp;.

A Python script that follows indexes and gzip

Standard library only. It recurses into sitemap indexes, unzips .gz files and writes url,lastmod,sitemap rows:

import csv, gzip, sys, urllib.request
import xml.etree.ElementTree as ET

NS = "{http://www.sitemaps.org/schemas/sitemap/0.9}"

def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent": "sitemap-to-csv/1.0"})
    data = urllib.request.urlopen(req, timeout=30).read()
    return gzip.decompress(data) if data[:2] == b"\x1f\x8b" else data

def walk(url, rows, depth=0):
    root = ET.fromstring(fetch(url))
    if root.tag == NS + "sitemapindex" and depth < 5:
        for loc in root.iter(NS + "loc"):
            walk(loc.text.strip(), rows, depth + 1)
    else:
        for u in root.iter(NS + "url"):
            rows.append([u.findtext(NS + "loc", "").strip(), u.findtext(NS + "lastmod", "").strip(), url])

rows = []
walk(sys.argv[1], rows)
w = csv.writer(sys.stdout)
w.writerow(["url", "lastmod", "sitemap"])
w.writerows(rows)
python3 sitemap_to_csv.py https://wordpress.org/sitemap.xml > wordpress.csv

On 2026-10-12 that produced 812 URLs for wordpress.org. Pointed at apify.com it returned about 702,000 URLs, all held in memory. Where it falls short:

  • It does not read robots.txt or guess locations, and it skips plain-text and RSS sitemaps.
  • One malformed file or a single 429 stops the whole export; there are no retries.
  • Image, video, news and hreflang data are ignored.
  • It has no memory, so it cannot tell you what changed since yesterday.

Export without code

  1. Open XML Sitemap Extractor to CSV & New-URL Monitor on Apify.
  2. Enter domains (example.com), site URLs or direct sitemap URLs, one per line. Up to 10,000 sites per run.
  3. Keep All URLs mode for a one-off export.
  4. Optionally add include or exclude regex patterns (/products/, /blog/), a date filter (7 days or 2026-09-01) or the HTTP status check.
  5. Click Start, then export the Output tab as CSV or Excel.

Discovery is automatic: Sitemap: lines from robots.txt first, then the common paths (only those robots.txt allows). It follows indexes up to 6 levels, reads gzip, text and RSS/Atom sitemaps, recovers URLs from broken XML and streams large files, so 50,000-URL sitemaps fit in normal memory. In our tests it read about 9,000 URLs from 8 inputs in 4 seconds.

A row from TechCrunch's news sitemap (real run, 2026-10-03):

{
  "site": "techcrunch.com",
  "url": "https://techcrunch.com/2026/10/03/federal-judge-calls-flock-indiscriminate-mass-surveillance/",
  "changeType": null,
  "sitemapUrl": "https://techcrunch.com/news-sitemap.xml",
  "news": {
    "publicationName": "TechCrunch",
    "language": "en",
    "publicationDate": "2026-10-03T19:33:15Z",
    "title": "Federal judge calls Flock ‘indiscriminate mass surveillance’"
  },
  "extractedAt": "2026-10-03T23:54:11+00:00"
}

Get only new, removed and updated URLs every day

Switch Mode to Changes only and schedule the actor (Apify Console, Schedules) with the same input. The first run saves each site's URL list in a named key-value store in your Apify account; every later run compares against it and outputs only the differences, each tagged with changeType:

{"url": "https://example.com/blog/c", "changeType": "new",            "lastmod": "2026-10-03", "previousLastmod": null}
{"url": "https://example.com/blog/a", "changeType": "removed",        "lastmod": null,         "previousLastmod": "2026-09-01"}
{"url": "https://example.com/blog/b", "changeType": "lastmodChanged", "lastmod": "2026-10-02", "previousLastmod": "2026-09-02"}

Recommended input for a competitor monitor:

{
  "sites": [
    "techcrunch.com",
    "allbirds.com",
    "https://wordpress.org/sitemap.xml"
  ],
  "mode": "changes",
  "emitBaselineOnFirstRun": false,
  "includePatterns": [
    "/products/",
    "/20\\d\\d/"
  ],
  "excludePatterns": [
    "/live/"
  ],
  "snapshotStoreName": "competitor-monitor"
}

Worked example (2026-10-03). A baseline over the Guardian, BBC, dir.bg, Allbirds and wordpress.org saved 42,985 URLs. The next run output exactly 1 row: a new Guardian article. A local test with a controlled sitemap returned exactly 1 new, 1 removed and 1 updated URL.

Details that keep the alerts clean:

  • Fetch-time lastmod. Shopify product sitemaps and some news sitemaps stamp every URL with the time of the request. The actor detects this, sets lastmodIsFetchTime: true and does not report those URLs as updated.
  • Partial failures. If some sitemap files fail to load, removed URLs are not reported for that run and the saved list is merged, not replaced, so you do not get false "removed" alerts.
  • Silent first run. With emitBaselineOnFirstRun: false the first run only saves the list. Turn it on if you also want the full export once.
  • Separate monitors. Use a different snapshotStoreName for each independent monitor.

What each row contains

FieldWhat it holds
site, url, sitemapUrlHost, page URL and the sitemap file it came from
changeTypenull in All URLs mode; baseline, new, removed or lastmodChanged in Changes mode
lastmod, previousLastmod, lastmodIsFetchTimeDates normalised to ISO 8601, and whether the site stamps the request time
changefreq, priorityAs declared in the sitemap
imageCount, images, videosImage and video sitemap data
newsGoogle News publication, language, date, title, keywords
alternateshreflang language and URL pairs (first 50)
statusCode, redirectLocation, xRobotsTag, statusErrorOnly with the status check on: HEAD request result, redirects and noindex headers

A per-site SUMMARY record in the run's key-value store lists sitemaps found, parsed and failed, URLs listed and matched, change counts and any issues (for example NO_SITEMAP_FOUND).

Honest limits

  • Only what the sitemap lists. It does not crawl links, so pages missing from the sitemap are missing from the export.
  • lastmodChanged is only as good as the site's dates. Many CMSs update <lastmod> correctly; some never do.
  • No bypassing. It identifies itself honestly, uses no proxy and records blocked_by_site when a site refuses it (403, persistent 429 or a challenge page). Some sites refuse any User-Agent that mentions Apify; the Guardian did in our 2026-10-04 test.
  • Memory for huge sites. 100,000 URLs from bbc.com peaked at 165 MB. Plan roughly 1 GB of run memory per million URLs processed at once.
  • Private and internal addresses (127.0.0.1, 10.0.0.0/8) are refused.

All input fields

Generated from the actor's live input schema (build 1.0.3).

FieldTypeDefaultWhat it does
Sites or sitemap URLs required
sites
arrayDomains (example.com), site URLs or direct sitemap URLs (https://example.com/sitemap_index.xml). For domains, sitemaps are found via robots.txt and common paths.
Mode
mode
stringall"All URLs" returns every URL in the sitemaps. "Changes only" remembers the previous run and returns only new, removed and updated URLs. Schedule it daily to monitor sites.
Options: all, changes
Output all URLs on the first monitoring run
emitBaselineOnFirstRun
booleantrueIn "Changes only" mode, the first run for a site has nothing to compare with. If on, it outputs all URLs with changeType "baseline". If off, it saves the snapshot silently and outputs nothing for that site.
Report updated pages (lastmod changed)
trackLastmodChanges
booleantrueIn "Changes only" mode, also report URLs whose <lastmod> date changed.
Report removed URLs
includeRemoved
booleantrueIn "Changes only" mode, also report URLs that disappeared from the sitemaps. They are skipped automatically when some sitemaps failed to load, to avoid false alarms.
Snapshot store name
snapshotStoreName
stringsitemap-monitor-snapshotsNamed key-value store (in your account) that keeps the previous URL list per site for "Changes only" mode. Use different names for independent monitors.
Filters
Include URL patterns (regex)
includePatterns
arrayOnly keep URLs matching at least one of these regular expressions, for example /blog/ or /products/.
Exclude URL patterns (regex)
excludePatterns
arrayDrop URLs matching any of these regular expressions, for example /tag/ or \?page=.
Only URLs modified after
lastmodAfter
stringKeep only URLs whose <lastmod> is on or after this date (2026-09-01) or within a relative period ("7 days", "24 hours"). URLs without lastmod are dropped when this is set.
Max URLs per site
maxUrlsPerSite
integer100000Stop reading a site's sitemaps after this many URLs (0 = up to 5,000,000).
Include image, video, news and hreflang data
includeExtensions
booleantrueAdd image URLs (first 10 and the total count), video details, Google News title and date, and hreflang alternates when the sitemap has them.
Status checks
Check HTTP status of output URLs
checkStatus
booleanfalseSend a HEAD request (no redirects followed) to each output URL and add statusCode, redirectLocation and X-Robots-Tag. Free while in launch; under the planned pricing, charged per URL checked. URLs that robots.txt disallows for this actor are skipped, and URLs the site refuses (HTTP 403, 429 or a bot challenge) are marked BLOCKED_BY_SITE; neither is charged.
Max status checks per run
maxStatusChecks
integer10000Upper limit on status checks in one run.
Status check concurrency
statusConcurrency
integer10Parallel status checks. Keep this low to be gentle with the target sites.
Crawling
Respect robots.txt for guessed sitemap paths
respectRobotsTxt
booleantrueSkip guessed sitemap locations that robots.txt disallows. Sitemaps declared in robots.txt and sitemap URLs you enter are always read. Status checks always follow robots.txt.
Max sitemap files per site
maxSitemapsPerSite
integer1000Safety limit for huge sitemap indexes.
Sites in parallel
maxConcurrency
integer5How many sites are processed at the same time.
Sitemap files in parallel per site
sitemapConcurrency
integer4How many sitemap files of one site are downloaded at the same time.
Request timeout (seconds)
requestTimeoutSecs
integer30Timeout per HTTP request. Large sitemaps can take a while to download.
Max retries
maxRetries
integer3Retries for network errors, HTTP 429 and 5xx responses, with exponential backoff.

Call it from code

Start a run and get the results in one HTTP request with the Apify API (your token is under Console, Settings, API & Integrations):

curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~sitemap-extractor-change-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sites":["apify.com"],"maxUrlsPerSite":5000}'

The run's Output tab offers 2 ready-made views; every view exports to CSV, Excel, JSON or XML:

  • URLs: site, url, changeType, lastmod, previousLastmod, changefreq, priority, statusCode, redirectLocation, imageCount, sitemapUrl
  • News sitemap entries: site, url, news.title, news.publicationDate, news.publicationName, news.language, lastmod

What it costs

Free to run right now. The actor charges no fee of its own today. Apify bills only the compute a run uses against your account's platform credit, and the free Apify plan includes $5 of usage every month. For scale: building all five of our actors and test-running each once on its default input used $0.03 of credit in total (2026-10-12).

Planned: pay-per-event pricing of $0.0002 per URL in the output ($0.20 per 1,000), $0.002 per site processed, $0.0005 per optional HTTP status check once the account's payout setup is finished. This page and the Apify page will show the change, and Apify notifies existing users before a price change takes effect.

With the planned prices a monitor is cheap after the first run, because only changed URLs are output: the 5-site example above would have cost about $0.01 per daily run, and its baseline $0.01 with the silent first run (or $8.61 with every URL output). Sites with no readable sitemap, blocked sites and invalid inputs would not be charged.

FAQ

How do I convert sitemap.xml to CSV for free?

For a small flat sitemap, the curl and grep one-liner on this page is enough. For indexes and gzip files, use the Python script. For many sites, change tracking or no code, run the Apify actor, which is free to run right now apart from platform credit, and export the Output tab as CSV.

Why did a site return 0 URLs?

Check the SUMMARY record. Usual causes: no sitemap exists, robots.txt points to a sitemap that returns 404, your include patterns matched nothing, the date filter hit a sitemap whose lastmod is the fetch time, or the site refused automated requests.

Is this a crawler?

No. A crawler opens every page to find links. This reads the site's own list of URLs, which takes seconds instead of hours. Pages are only requested if you turn on the HTTP status check, which sends one HEAD request per URL that robots.txt allows.

Can I monitor many competitors in one run?

Yes. Up to 10,000 sites per run, each with its own saved snapshot. Use one snapshot store name per monitor.

How do I stop this actor reading my site?

It sends sitemap-extractor-change-monitor in its User-Agent. A robots.txt group for that name controls the guessed sitemap paths and status checks; answering it with 403 or 429 refuses it entirely. Sitemaps you list in robots.txt are treated as published for machines.

Try it on your own data

Extract every URL from any site's XML sitemaps to CSV, Excel or JSON (robots.txt discovery, nested indexes, .gz, txt, RSS, broken XML) with lastmod, images, news and hreflang. Changes mode returns only new, removed and updated URLs since the last run: schedule it to monitor competitors.

Open XML Sitemap Extractor to CSV & New-URL Monitor Opens the actor's page on apify.com. A free Apify account is needed to run it.