Reading Government Open Data in XML With a USGS Feed
Read a real public QuakeML feed from the USGS: two namespaces, nested value elements, depth in meters, magnitude types, and checks that catch a wrong unit.

Public agencies publish data as XML more often than the web's fashion suggests: weather, water, earthquakes, statistics, legislation, procurement. The files are real data with real documentation, and they teach the same habits as any other XML source. This article reads one, live: the earthquake feed from the United States Geological Survey, in QuakeML.
Fetching it
curl -sS -m 30 -o quake20.xml \
"https://earthquake.usgs.gov/fdsnws/event/1/query?format=quakeml&limit=20&orderby=time&minmagnitude=4.5"The service follows the FDSN standard for event web services: a base URL, query parameters for time window, magnitude and region, a format for the output. The same parameters can request CSV, GeoJSON or text. I chose QuakeML because it carries everything the agency has (uncertainties, magnitude types, quality figures) and because it is a good XML specimen. The response was 48,811 bytes for 20 events, about 2.4 KB each, which shows how much detail sits behind a one-line "magnitude 4.6".
The structure
The top of the document:
<q:quakeml xmlns="http://quakeml.org/xmlns/bed/1.2" xmlns:anss="http://anss.org/xmlns/event/0.1" xmlns:catalog="http://anss.org/xmlns/catalog/0.1" xmlns:q="http://quakeml.org/xmlns/quakeml/1.2">
<eventParameters publicID="quakeml:earthquake.usgs.gov/fdsnws/event/1/query?...">
<event catalog:datasource="us" catalog:eventid="7000ttfx" publicID="quakeml:earthquake.usgs.gov/...">
<description><type>earthquake name</type><text>5 km ENE of Tierra y Libertad, Mexico</text></description>
<origin ...>
<time><value>2026-10-27T12:01:11.952Z</value></time>
<longitude><value>-93.817</value></longitude>
<latitude><value>16.3977</value></latitude>
<depth><value>140342</value><uncertainty>2559</uncertainty></depth>Four namespaces appear. The root q:quakeml is in the QuakeML namespace, and its child eventParameters, event, origin and the rest are in the default namespace http://quakeml.org/xmlns/bed/1.2 (BED, the basic event description). Two extension namespaces, anss and catalog, add agency attributes to events and origins. The measurements follow a pattern the format uses throughout: a quantity is an element with a value child and optional uncertainty, lowerUncertainty and similar. longitude is not the number; longitude/value is.
Reading it
NS = {"b": "http://quakeml.org/xmlns/bed/1.2", "q": "http://quakeml.org/xmlns/quakeml/1.2"}
r = etree.parse("quake20.xml").getroot()
evs = r.findall("b:eventParameters/b:event", NS)
for e in evs:
o = e.find("b:origin", NS); m = e.find("b:magnitude", NS)
rows.append((o.findtext("b:time/b:value", namespaces=NS),
float(o.findtext("b:latitude/b:value", namespaces=NS)),
float(o.findtext("b:longitude/b:value", namespaces=NS)),
float(o.findtext("b:depth/b:value", namespaces=NS)) / 1000,
float(m.findtext("b:mag/b:value", namespaces=NS)),
m.findtext("b:type", namespaces=NS),
e.findtext("b:description/b:text", namespaces=NS)))root: quakeml | default ns: http://quakeml.org/xmlns/bed/1.2
20 events
('2026-10-27T12:01:11.952Z', 16.3977, -93.817, 140.342, 4.6, 'mb', '5 km ENE of Tierra y Libertad, Mexico')
('2026-10-27T11:41:22.141Z', -23.1819, -66.6616, 206.259, 4.6, 'mb', '100 km W of El Aguilar, Argentina')
('2026-10-27T10:50:35.418Z', -14.0067, 165.6479, 10.0, 5.2, 'mb', '191 km NW of Port-Olry, Vanuatu')
no ns: 0Twenty events became twenty rows. The last line is the usual one: the unprefixed path eventParameters/event found 0 events, since every element but the root is in the BED namespace. The prefix b is mine. The q prefix is declared but I did not need it, because the root is only a wrapper.
Three details came out of reading the numbers, and each is the sort of fact the parser cannot tell you.
Units. The depth value for the first event is 140342. The QuakeML specification defines depths in meters, so this is 140.342 km, a deep event, and the code divides by 1000. Had I treated it as kilometers, the events would sit a thousand times too deep. The check that catches such a mistake is a range test on the output (no earthquake is 140,000 km deep; the Earth's radius is about 6,371 km), which is cheap and should live in the loader. The same unit question applies to every quantity: the file says value, and the schema or the documentation says the unit.
Magnitude types. The 20 events carried three types: mb, mwr and mww. They measure the earthquake differently, a body-wave magnitude and two moment magnitudes, and are not directly comparable. A report that ranks by magnitude without the type mixes scales. Keep the type in the table.
Uncertainty. All 20 events had an uncertainty on the depth. The file stores the measurement and its error bars together. If you reduce an event to a point (a pin on a map), you drop the information on how well it is known, which for the first event is 2.5 km on a 140 km depth, but for a shallow event can be much larger.
I also checked the time span: the first and last events were 24.4 hours apart, newest first, which tells you the feed's order and that 20 events at this threshold cover about a day. Times are ISO 8601 in UTC with a trailing Z, to the millisecond; datetime.fromisoformat reads them after replacing Z on older Pythons.
The attributes of the first event, datasource, eventsource, eventid and publicID, come from the catalog namespace and the standard. In lxml their names are in Clark notation, so a lookup needs {http://anss.org/xmlns/catalog/0.1}eventid; I stripped the namespace for display. Use eventid as the key if you store events. Events are revised: a magnitude changes as more data arrive, and an event can be deleted, so a table of events needs an update path, not just inserts (parsing an Atom feed into a database shows the pattern).
Habits for public data
Prefer the simplest format that has what you need. If you only want time, place and magnitude, the CSV or GeoJSON output is simpler than QuakeML. Use the XML when you need its extra structure.
Read the documentation for units and meaning, and write them in your table's column names (depth_km, mag_type).
Respect the service. Use query windows and limit, cache responses, send a descriptive User-Agent and do not poll faster than the data changes. The agency's guidance on usage is on its site.
Keep the raw response with its timestamp, so a figure can be reproduced; the live endpoint will not return the same data tomorrow.
Check your assumptions with ranges and counts. Latitude between -90 and 90, longitude between -180 and 180, depth between 0 and 700 km for earthquakes, magnitude plausible, and the count of events equal to what you asked for. All 20 events passed these checks.
Related reading
For an earlier public-data example with two namespaces and values in attributes, see reading financial and government XML datasets. The cluster overview is working with XML feeds and APIs. QuakeML is described at the QuakeML site, and the USGS service in its FDSN event web service documentation. Test a namespaced path in the XPath tester.
When the agency offers a simpler format
You only need a table. CSV or GeoJSON from the same endpoint has no namespaces and no value wrappers.
You need the whole catalog. Bulk downloads or archives exist for large histories; paging an API for years of events is slow and impolite.
The format is a standard with a library. For seismology there are libraries that read QuakeML into objects (ObsPy is the usual one; I did not test it here), and they handle the unit and uncertainty conventions.
You need real-time updates. Some services offer feeds or streaming for new events. Polling a query endpoint is the fallback.


