1 | #! /usr/bin/python |
---|
2 | # -*- coding: UTF8 -*- |
---|
3 | |
---|
4 | import sys |
---|
5 | try: |
---|
6 | from lxml import etree |
---|
7 | except ImportError: |
---|
8 | from xml.etree import ElementTree |
---|
9 | |
---|
10 | from decimal import Decimal |
---|
11 | |
---|
12 | class Event(object): |
---|
13 | """ |
---|
14 | Seismic event. |
---|
15 | """ |
---|
16 | |
---|
17 | def __str__(self): |
---|
18 | return str(self.__dict__) |
---|
19 | |
---|
20 | class QuakeML(object): |
---|
21 | """ |
---|
22 | Read access for QuakeML file-like objects. |
---|
23 | |
---|
24 | >>> q = QuakeML(open("example.xml"), True) |
---|
25 | >>> q.rawdata[40:92] |
---|
26 | 'quakeml xmlns="http://quakeml.org/xmlns/quakeml/1.0"' |
---|
27 | >>> len(q.events) |
---|
28 | 9 |
---|
29 | >>> q.events[0].origin |
---|
30 | '2010-07-07T04:03:57.7+00:00' |
---|
31 | >>> q.events[3].description |
---|
32 | 'TAJIKISTAN' |
---|
33 | >>> q.events[1].agencyOrigin |
---|
34 | 'smi:smi-registry/organization/NNC' |
---|
35 | >>> q.events[2].agencyMagnitude |
---|
36 | 'smi:smi-registry/organization/GFZ' |
---|
37 | """ |
---|
38 | |
---|
39 | namespace = {"qml": "http://quakeml.org/xmlns/quakeml/1.0"} |
---|
40 | |
---|
41 | def __init__(self, stream, debug=False): |
---|
42 | """ |
---|
43 | Read stream content (raw). |
---|
44 | """ |
---|
45 | |
---|
46 | # There are two debug levels: |
---|
47 | # "1": normal debugging |
---|
48 | # "2": additionally write XML input data to "debug-dump.xml" |
---|
49 | self.debugLevel = debug |
---|
50 | |
---|
51 | try: |
---|
52 | self.rawdata = stream.read() |
---|
53 | self.debug("Data input length %u bytes." % len(self.rawdata)) |
---|
54 | except: |
---|
55 | quit("input not readable.") |
---|
56 | |
---|
57 | if self.debugLevel > 1: |
---|
58 | try: |
---|
59 | open("debug-dump.xml", "w").write(self.rawdata) |
---|
60 | self.debug("Raw XML data written to 'debug-dump.xml'") |
---|
61 | except Exception, e: |
---|
62 | self.debug("Raw XML data dump requested, BUT an error " |
---|
63 | "occurred. %s" % e) |
---|
64 | |
---|
65 | self.events = {} |
---|
66 | self._initXML() |
---|
67 | self._parseEvents() |
---|
68 | |
---|
69 | def debug(self, text, nontext=None, spaces=0): |
---|
70 | """ |
---|
71 | Simple debug function. |
---|
72 | """ |
---|
73 | |
---|
74 | if not self.debugLevel: |
---|
75 | return |
---|
76 | |
---|
77 | print >> sys.stderr, text |
---|
78 | |
---|
79 | if not nontext: |
---|
80 | return |
---|
81 | |
---|
82 | if type(nontext) == dict: |
---|
83 | for i in nontext: |
---|
84 | print >> sys.stderr, " "*spaces + i, nontext[i] |
---|
85 | |
---|
86 | def _initXML(self): |
---|
87 | """ |
---|
88 | Inititialize XML processor(s). |
---|
89 | |
---|
90 | Depending on available modules the method "xpathns" is set to the |
---|
91 | corresponding helper function: |
---|
92 | |
---|
93 | __lxml_xpathns - uses lxml.etree xpath method |
---|
94 | __xml_findallns - uses xml.etree.ElementTree findall method |
---|
95 | |
---|
96 | Both methods add the quakeml namespace to the path automatically. |
---|
97 | """ |
---|
98 | |
---|
99 | if "etree" in globals().keys(): |
---|
100 | # use lxml |
---|
101 | try: |
---|
102 | parser = etree.XMLParser(remove_blank_text=True) |
---|
103 | self.xml = etree.fromstring(self.rawdata, parser) |
---|
104 | self.debug("LXML parser initiated.") |
---|
105 | self.xpathns = self.__lxml_xpathns |
---|
106 | except Exception, e: |
---|
107 | quit("error while parsing XML: " + e) |
---|
108 | else: |
---|
109 | # use built-in xml.etree |
---|
110 | try: |
---|
111 | self.xml = ElementTree.XML(self.rawdata) |
---|
112 | self.debug("Standard XML parser initiated.") |
---|
113 | self.xpathns = self.__xml_findallns |
---|
114 | except Exception, e: |
---|
115 | quit("error while parsing XML: " + e) |
---|
116 | |
---|
117 | def __lxml_xpathns(self, xml, path, select=0): |
---|
118 | """ |
---|
119 | Helper function to handle global quakeml namespace for lxml. |
---|
120 | """ |
---|
121 | # add namespace prefix |
---|
122 | path = "/qml:".join(path.split("/")) |
---|
123 | |
---|
124 | if select == None: |
---|
125 | return xml.xpath(path, namespaces=self.namespace) |
---|
126 | else: |
---|
127 | return xml.xpath(path, namespaces=self.namespace)[select] |
---|
128 | |
---|
129 | def __xml_findallns(self, xml, path, select=0): |
---|
130 | """ |
---|
131 | Helper function for namespace handling (standard xml module). |
---|
132 | """ |
---|
133 | x = "/{%s}" % self.namespace.values()[0] |
---|
134 | path = x.join(path.split("/")) |
---|
135 | |
---|
136 | if select == None: |
---|
137 | return xml.findall(path) |
---|
138 | else: |
---|
139 | return xml.findall(path)[select] |
---|
140 | |
---|
141 | def _parseEvents(self): |
---|
142 | """ |
---|
143 | Loop through events inside XML data schema. |
---|
144 | """ |
---|
145 | |
---|
146 | for event in self.xpathns(self.xml, "./eventParameters/event", select=None): |
---|
147 | # mostly there is more than one origin |
---|
148 | self.debug("Processing event %s" % event.attrib["publicID"]) |
---|
149 | origins = self.xpathns(event, "./origin", select=None) |
---|
150 | for o in origins: |
---|
151 | publicID = o.attrib["publicID"] |
---|
152 | self.debug(" - origin %s" % publicID) |
---|
153 | |
---|
154 | self.events[publicID] = e = Event() |
---|
155 | # there's only a global description of source region |
---|
156 | e.description = self.xpathns(event, "./description/text").text |
---|
157 | # save publicID of associated event |
---|
158 | e.eventID = event.attrib["publicID"] |
---|
159 | e.publicID = publicID |
---|
160 | |
---|
161 | # detailed information of single origins |
---|
162 | e.origin = self.xpathns(o, "./time/value").text |
---|
163 | e.longitude = Decimal(self.xpathns(o, "./longitude/value").text) |
---|
164 | e.latitude = Decimal(self.xpathns(o, "./latitude/value").text) |
---|
165 | e.depth = self.xpathns(o, "./depth/value").text |
---|
166 | e.mode = self.xpathns(o, "./evaluationMode").text |
---|
167 | e.agencyOrigin = self.xpathns(o, "./creationInfo/authorURI").text |
---|
168 | |
---|
169 | # add associated magnitude to origin |
---|
170 | magnitudes = self.xpathns(event, "./magnitude", select=None) |
---|
171 | for m in magnitudes: |
---|
172 | originID = self.xpathns(m, "./originID").text |
---|
173 | e = self.events[originID] |
---|
174 | e.magnitude = Decimal(self.xpathns(m, "./mag/value").text) |
---|
175 | e.magnitudeType = self.xpathns(m, "./type").text |
---|
176 | e.agencyMagnitude = self.xpathns(m, "./creationInfo/authorURI").text |
---|
177 | |
---|
178 | # sort events by origin (results in a list) |
---|
179 | self.events = self.events.values() |
---|
180 | self.events.sort(key=lambda x: x.origin) |
---|
181 | |
---|
182 | if not self.debugLevel: |
---|
183 | return |
---|
184 | |
---|
185 | for e in self.events: |
---|
186 | self.debug("Origin details:", vars(e), 2) |
---|
187 | |
---|
188 | if __name__ == "__main__": |
---|
189 | import doctest |
---|
190 | errors, _ = doctest.testmod(exclude_empty=True) |
---|