Mercurial > hg > lxmldump
annotate lxmldump.py @ 23:3ef1c5463b8f
Add license.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Sat, 22 May 2021 21:57:38 +0300 |
parents | 76856fc4e5fa |
children | 8c8e8e4504bb |
rev | line source |
---|---|
0 | 1 #!/usr/bin/python3 -B |
2 # coding=utf-8 | |
3 ### | |
4 | 4 ### lxmldump - Dump ISO/FDIS 1951 XML file data |
5 ### Programmed and designed by Matti 'ccr' Hämäläinen <ccr@tnsp.org> | |
6 ### (C) Copyright 2021 Tecnic Software productions (TNSP) | |
7 ### | |
23 | 8 ### Released / distributed under 3-clause BSD license |
9 ### (see file "COPYING" for more information) | |
10 ### | |
4 | 11 ### Python 3.7+ required! |
0 | 12 ### |
13 import sys | |
14 import signal | |
15 import re | |
16 from pathlib import Path | |
17 import xml.etree.ElementTree as xmlET | |
5
274b2091137c
Some more work on cleaning this up.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
18 import unicodedata |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
19 import argparse |
0 | 20 |
21 assert sys.version_info >= (3, 7) | |
22 | |
23 | |
24 ### | |
25 ### Default settings | |
26 ### | |
7 | 27 pkk_str_fmap = { |
28 "Fragment" : ["<", ">"], | |
29 } | |
30 | |
31 | |
32 pkk_debug_list = [ | |
33 "ahas", | |
34 "ahavakkaine", | |
35 "ahavakala", | |
36 "ahavakoittuo", | |
37 "ahvaliha", | |
9 | 38 "aloilleh", |
18 | 39 "hanjahtoakseh", |
19 | 40 "akkalisto", |
7 | 41 ] |
42 | |
43 | |
21 | 44 # Operation modes |
45 PKK_MODE_NORMAL = 0 | |
46 PKK_MODE_DUMP = 1 | |
47 PKK_MODE_XML = 2 | |
48 | |
49 | |
0 | 50 ### |
51 ### Misc. helper functions, etc | |
52 ### | |
53 def pkk_cleanup(): | |
54 return 0 | |
55 | |
56 | |
13 | 57 ## Print string to stdout using normalized Unicode if enabled |
5
274b2091137c
Some more work on cleaning this up.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
58 def pkk_print(smsg): |
12 | 59 try: |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
60 if pkk_cfg.normalize: |
12 | 61 sys.stdout.write(unicodedata.normalize("NFC", smsg)) |
62 else: | |
63 sys.stdout.write(smsg) | |
64 | |
65 except (BrokenPipeError, IOError) as e: | |
66 sys.stderr.close() | |
67 | |
0 | 68 |
13 | 69 ## Print string with indentation |
7 | 70 def pkk_printi(indent, smsg): |
22 | 71 pkk_print((" " * pkk_cfg.indent * indent) + smsg) |
7 | 72 |
0 | 73 |
13 | 74 ## Check value against current verbosity level |
8 | 75 def pkk_verbosity(lvl): |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
76 return pkk_cfg.verbosity >= lvl |
8 | 77 |
78 | |
0 | 79 ## Fatal error handler |
80 def pkk_fatal(smsg): | |
81 print(u"ERROR: "+ smsg) | |
82 sys.exit(1) | |
83 | |
84 | |
85 ## Handler for SIGINT signals | |
86 def pkk_signal_handler(signal, frame): | |
87 pkk_cleanup() | |
88 print(u"\nQuitting due to SIGINT / Ctrl+C!") | |
89 sys.exit(1) | |
90 | |
91 | |
13 | 92 ## Clean string by removing tabs and newlines |
10 | 93 def pkk_str_clean(mstr): |
94 return re.sub(r'[\n\r\t]', '', mstr) | |
95 | |
96 | |
13 | 97 ## Format "Ptr" node as text |
9 | 98 def pkk_ptr_to_text(pnode): |
16 | 99 return "<PTR:{}>{}</PTR>".format( |
10 | 100 pnode.attrib["{http://www.w3.org/TR/xlink}href"], |
101 ("".join(pnode.itertext())).strip()) | |
9 | 102 |
103 | |
13 | 104 ## Get text inside a given node |
16 | 105 def pkk_node_to_text(lnode): |
7 | 106 stmp = "" |
107 for pnode in lnode.iter(): | |
9 | 108 if pnode.tag == "Ptr": |
109 stmp += pkk_ptr_to_text(pnode) | |
110 else: | |
111 if isinstance(pnode.text, str): | |
10 | 112 ptext = pkk_str_clean(pnode.text).strip() |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
113 if pkk_cfg.annotate and isinstance(pnode.tag, str) and pnode.tag in pkk_str_fmap: |
9 | 114 stmp += pkk_str_fmap[pnode.tag][0] + ptext + pkk_str_fmap[pnode.tag][1] |
115 else: | |
116 stmp += ptext | |
7 | 117 |
9 | 118 if isinstance(pnode.tail, str): |
10 | 119 stmp += pkk_str_clean(pnode.tail) |
7 | 120 |
121 return stmp.strip() | |
122 | |
123 | |
13 | 124 ## Simple recursive dump starting at given node |
7 | 125 def pkk_dump_recursive(indent, lnode): |
126 if lnode.tag in ["Example"]: | |
16 | 127 stmp = pkk_node_to_text(lnode) |
9 | 128 pkk_printi(indent, "{} \"{}\"\n".format(lnode.tag, stmp)) |
6 | 129 else: |
7 | 130 if isinstance(lnode.text, str): |
10 | 131 stmp = pkk_str_clean(lnode.text).strip() |
7 | 132 if stmp != "": |
133 stmp = " \""+ stmp +"\"" | |
134 else: | |
135 stmp = "" | |
6 | 136 |
137 if len(lnode.attrib) > 0: | |
10 | 138 atmp = " "+ str(lnode.attrib) |
6 | 139 else: |
140 atmp = "" | |
141 | |
7 | 142 pkk_printi(indent, "{}{}{}\n".format(lnode.tag, atmp, stmp)) |
6 | 143 for qnode in lnode.findall("./*"): |
7 | 144 pkk_dump_recursive(indent + 1, qnode) |
6 | 145 |
146 | |
17 | 147 ## Output item(s) under given node with given format string |
13 | 148 def pkk_output_subs_fmt(indent, dnode, dsub, dname, dfmt): |
7 | 149 for qnode in dnode.findall(dsub): |
16 | 150 pkk_printi(indent, dfmt.format(dname, pkk_node_to_text(qnode))) |
7 | 151 |
17 | 152 ## Output item(s) under given node with a prefixed name string |
13 | 153 def pkk_output_subs_prefix(indent, dnode, dsub, dname): |
154 pkk_output_subs_fmt(indent, dnode, dsub, dname, "{0} \"{1}\"\n") | |
7 | 155 |
156 | |
17 | 157 ## Output a main "Headword" or "Sense" node under it |
7 | 158 def pkk_output_sense(indent, dnode): |
17 | 159 # Search form and definition |
13 | 160 pkk_output_subs_prefix(indent, dnode, "./SearchForm", "srch") |
161 pkk_output_subs_prefix(indent, dnode, "./Definition", "defn") | |
7 | 162 |
17 | 163 # Examples |
7 | 164 for wnode in dnode.findall("./ExampleBlock/ExampleCtn"): |
16 | 165 sstr = pkk_node_to_text(wnode.find("./Example")) |
8 | 166 lstr = "" |
6 | 167 |
8 | 168 if pkk_verbosity(1): |
169 ltmp = [] | |
170 for qnode in wnode.findall("./FreeTopic[@type='levikki']/GeographicalUsage"): | |
16 | 171 ltmp.append("{} [{}]".format(pkk_node_to_text(qnode), qnode.attrib["class"])) |
8 | 172 |
173 if len(ltmp) > 0: | |
174 lstr = " ({})".format(", ".join(ltmp)) | |
7 | 175 |
176 pkk_printi(indent + 1, "{} \"{}\"{}\n".format("exmp", sstr, lstr)) | |
177 | |
178 | |
17 | 179 ## Output one "DictionaryEntry" node |
7 | 180 def pkk_output_node(indent, dnode): |
6 | 181 |
7 | 182 for wnode in dnode.findall("./HeadwordCtn"): |
18 | 183 # Create list with grammatical attributes (noun, verb, etc.) |
184 tmpl = [] | |
185 for pnode in wnode.findall("./PartOfSpeechCtn/PartOfSpeech"): | |
186 tmpl.append(pnode.attrib["freeValue"]) | |
187 | |
188 for pnode in wnode.findall("./GrammaticalNote"): | |
19 | 189 tmpl.append(pkk_node_to_text(pnode)) |
18 | 190 |
191 # Remove duplicates and sort the list | |
192 tmpl = list(set(tmpl)) | |
193 tmpl.sort(reverse=False, key=lambda attr: (attr, len(attr))) | |
194 | |
195 # Print the headword and attributes if any | |
196 pkk_output_subs_fmt(indent, wnode, "./Headword", "", "\"{1}\"") | |
197 if len(tmpl) > 0: | |
198 pkk_print(" ({})\n".format(" ; ".join(tmpl))) | |
199 else: | |
200 pkk_print("\n") | |
201 | |
202 # Print main "sense" | |
7 | 203 pkk_output_sense(indent + 1, wnode) |
6 | 204 |
18 | 205 # Print any other "senses" |
7 | 206 index = 1 |
207 for wnode in dnode.findall("./SenseGrp"): | |
8 | 208 pkk_printi(indent + 1, "sense #{}\n".format(index)) |
7 | 209 pkk_output_sense(indent + 2, wnode) |
210 index += 1 | |
6 | 211 |
212 | |
0 | 213 ### |
214 ### Main program starts | |
215 ### | |
216 signal.signal(signal.SIGINT, pkk_signal_handler) | |
217 | |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
218 optparser = argparse.ArgumentParser( |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
219 description="lxmldump - Dump ISO/FDIS 1951 XML file data", |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
220 usage="%(prog)s [options] <input xml file(s)>", |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
221 epilog="\n\n" |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
222 ) |
0 | 223 |
22 | 224 optparser.add_argument("filenames", |
225 type=str, action="extend", nargs="*", | |
226 metavar="filename", | |
227 help="XML filename(s)") | |
0 | 228 |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
229 optparser.add_argument("-d", "--dump", |
22 | 230 dest="mode", |
21 | 231 action="store_const", const=PKK_MODE_DUMP, default=PKK_MODE_NORMAL, |
22 | 232 help="output as simple dump") |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
233 |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
234 optparser.add_argument("-x", "--xml", |
22 | 235 dest="mode", |
21 | 236 action="store_const", const=PKK_MODE_XML, |
22 | 237 help="output as XML") |
0 | 238 |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
239 optparser.add_argument("-n", "--normalize", |
22 | 240 dest="normalize", |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
241 action="store_const", const=True, default=False, |
22 | 242 help="output NFC normalized Unicode") |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
243 |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
244 optparser.add_argument("-a", "--annotate", |
22 | 245 dest="annotate", |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
246 action="store_const", const=True, default=False, |
22 | 247 help="annotate strings") |
0 | 248 |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
249 optparser.add_argument("-v", "--verbosity", |
22 | 250 dest="verbosity", |
251 type=int, choices=range(0, 4), default=3, | |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
252 metavar="n", |
22 | 253 help='set verbosity level (0-3, default: %(default)s)') |
0 | 254 |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
255 optparser.add_argument("-p", "--debug", |
22 | 256 dest="debug", action="store_const", const=True, default=False, |
257 help=argparse.SUPPRESS) | |
258 | |
259 optparser.add_argument("-i", "--indent", | |
260 dest="indent", | |
261 type=int, choices=range(0, 32), default=4, | |
262 metavar="n", | |
263 help='indent output by <n> characters (default: %(default)s)') | |
0 | 264 |
265 | |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
266 ### Show help if needed |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
267 pkk_cfg = optparser.parse_args() |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
268 if len(pkk_cfg.filenames) == 0: |
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
269 optparser.print_help() |
0 | 270 sys.exit(0) |
271 | |
272 | |
6 | 273 ### Handle each input file |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
274 for filename in pkk_cfg.filenames: |
0 | 275 # Parse XML file into element tree |
276 try: | |
277 uxml = xmlET.parse(filename) | |
278 except Exception as e: | |
279 pkk_fatal(u"SVG/XML parsing failed: {0}".format(str(e))) | |
280 | |
281 # Dump output | |
282 try: | |
283 xroot = uxml.getroot() | |
284 for dnode in xroot.findall("./DictionaryEntry"): | |
7 | 285 |
20
f274504eafd0
Use Python argparse module instead of custom self-rolled argument parser.
Matti Hamalainen <ccr@tnsp.org>
parents:
19
diff
changeset
|
286 if pkk_cfg.debug and dnode.attrib["identifier"] not in pkk_debug_list: |
7 | 287 continue |
288 | |
21 | 289 if pkk_cfg.mode == PKK_MODE_NORMAL: |
19 | 290 try: |
291 pkk_output_node(0, dnode) | |
292 except Exception as e: | |
293 pkk_dump_recursive(0, dnode) | |
294 print(str(e)) | |
295 sys.exit(0) | |
21 | 296 elif pkk_cfg.mode == PKK_MODE_DUMP: |
7 | 297 pkk_dump_recursive(0, dnode) |
21 | 298 elif pkk_cfg.mode == PKK_MODE_XML: |
10 | 299 pkk_print(str(xmlET.tostring(dnode, encoding="utf8")) + "\n") |
0 | 300 else: |
10 | 301 pkk_fatal("Invalid operation mode?") |
7 | 302 |
303 print("\n") | |
0 | 304 |
305 except (BrokenPipeError, IOError) as e: | |
306 sys.stderr.close() | |
307 sys.exit(1) | |
308 | |
309 pkk_cleanup() | |
310 sys.exit(0) |