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.txtand look forSitemap: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 &.
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
- Open XML Sitemap Extractor to CSV & New-URL Monitor on Apify.
- Enter domains (
example.com), site URLs or direct sitemap URLs, one per line. Up to 10,000 sites per run. - Keep All URLs mode for a one-off export.
- Optionally add include or exclude regex patterns (
/products/,/blog/), a date filter (7 daysor2026-09-01) or the HTTP status check. - 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: trueand 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: falsethe first run only saves the list. Turn it on if you also want the full export once. - Separate monitors. Use a different
snapshotStoreNamefor each independent monitor.
What each row contains
| Field | What it holds |
|---|---|
site, url, sitemapUrl | Host, page URL and the sitemap file it came from |
changeType | null in All URLs mode; baseline, new, removed or lastmodChanged in Changes mode |
lastmod, previousLastmod, lastmodIsFetchTime | Dates normalised to ISO 8601, and whether the site stamps the request time |
changefreq, priority | As declared in the sitemap |
imageCount, images, videos | Image and video sitemap data |
news | Google News publication, language, date, title, keywords |
alternates | hreflang language and URL pairs (first 50) |
statusCode, redirectLocation, xRobotsTag, statusError | Only 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.
lastmodChangedis 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_sitewhen 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).
| Field | Type | Default | What it does |
|---|---|---|---|
Sites or sitemap URLs requiredsites | array | Domains (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. | |
Modemode | string | all | "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 runemitBaselineOnFirstRun | boolean | true | In "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 | boolean | true | In "Changes only" mode, also report URLs whose <lastmod> date changed. |
Report removed URLsincludeRemoved | boolean | true | In "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 namesnapshotStoreName | string | sitemap-monitor-snapshots | Named 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 | array | Only keep URLs matching at least one of these regular expressions, for example /blog/ or /products/. | |
Exclude URL patterns (regex)excludePatterns | array | Drop URLs matching any of these regular expressions, for example /tag/ or \?page=. | |
Only URLs modified afterlastmodAfter | string | Keep 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 sitemaxUrlsPerSite | integer | 100000 | Stop reading a site's sitemaps after this many URLs (0 = up to 5,000,000). |
Include image, video, news and hreflang dataincludeExtensions | boolean | true | Add 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 URLscheckStatus | boolean | false | Send 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 runmaxStatusChecks | integer | 10000 | Upper limit on status checks in one run. |
Status check concurrencystatusConcurrency | integer | 10 | Parallel status checks. Keep this low to be gentle with the target sites. |
| Crawling | |||
Respect robots.txt for guessed sitemap pathsrespectRobotsTxt | boolean | true | Skip 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 sitemaxSitemapsPerSite | integer | 1000 | Safety limit for huge sitemap indexes. |
Sites in parallelmaxConcurrency | integer | 5 | How many sites are processed at the same time. |
Sitemap files in parallel per sitesitemapConcurrency | integer | 4 | How many sitemap files of one site are downloaded at the same time. |
Request timeout (seconds)requestTimeoutSecs | integer | 30 | Timeout per HTTP request. Large sitemaps can take a while to download. |
Max retriesmaxRetries | integer | 3 | Retries 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.