Python-Skripte: kleine Korrekturen.
[wortliste.git] / skripte / python / edit_tools / prepare_patch.py
blobc80c38cf7f108f9c8c5dec08408cb06c85083261
1 #!/usr/bin/env python3
2 # :Copyright: © 2014 Günter Milde.
3 # Released without warranty under the terms of the
4 # GNU General Public License (v. 2 or later)
5 # :Id: $Id: $
7 # prepare_patch.py: Helfer für kleine Editieraufgaben
8 # ===================================================
9 # ::
11 u"""
12 Erstelle einen Patch für kleinere Korrekturen der Wortliste.
13 Ausgangspunkt sind Dateien mit einer Korrektur pro Zeile.
14 (Zeilen, die mit ``#`` starten, werden ignoriert.)
16 AKTION ist eine von:
17 doppelte: Einträge mit gleichem Schlüssel entfernen,
18 fehleintraege: Einträge entfernen,
19 grossklein: Großschreibung ändern,
20 grossabgleich: Großschreibung der Trennmuster wie erstes Feld,
21 korrektur: Einträge durch alternative Version ersetzen,
22 neu: Einträge hinzufügen,
23 reformschreibung: Eintrag in "nur Reformschreibung" ändern,
24 zusammenfassen: Sprachvarianten zusammenfassen wenn gleich.
25 """
27 # Die ``<AKTION>.todo`` Dateien im Unterverzeichnis "tasks/" beschreiben das
28 # jeweils erforderliche Datenformat im Dateikopf.
30 # ::
32 import optparse, sys, os
33 from copy import copy, deepcopy
36 from wortliste import WordFile, WordEntry, join_word, udiff, sortkey_duden
38 def teste_datei(datei):
39 """Teste, ob Datei geöffnet werden kann."""
41 try:
42 file = open(datei, 'r')
43 file.close()
44 except:
45 sys.stderr.write("Kann '" + datei + u"' nicht öffnen\n" )
46 sys.exit()
49 # Sprachvarianten
50 # ---------------
51 # Sprach-Tag nach [BCP47]_::
53 sprachvariante = 'de-1901' # "traditionell"
54 # sprachvariante = 'de-1996' # Reformschreibung
55 # sprachvariante = 'de-x-versal' # ohne ß (Großbuchstaben und Kapitälchen)
56 # sprachvariante = 'de-1901-x-versal' # ohne ß (Schweiz oder versal)
57 # sprachvariante = 'de-1996-x-versal' # ohne ß (Schweiz oder versal)
58 # sprachvariante = 'de-CH-1901' # ohne ß (Schweiz) ("süssauer")
61 # Allgemeine Korrektur (z.B. Fehltrennung)
63 # Format:
64 # * ein Wort mit Trennstellen (für "Sprachvariante"):
65 # * vollständiger Eintrag (für Wörter mit Sprachvarianten).
67 # ::
69 def korrektur(wordfile, datei='tasks/korrektur.todo'):
70 """Patch aus korrigierten Einträgen"""
72 teste_datei(datei)
74 korrekturen = {}
75 for line in open(datei, 'r'):
76 if line.startswith('#'):
77 continue
78 # Dekodieren, Zeilenende entfernen
79 line = line.strip()
80 if not line:
81 continue
82 # Eintrag ggf. komplettieren
83 if ';' not in line:
84 line = '%s;%s' % (join_word(line), line)
85 entry = WordEntry(line)
86 key = entry[0]
87 entry.regelaenderungen() # teste auf Dinge wie s-t/-st
89 korrekturen[key] = entry
91 wortliste = list(wordfile)
92 wortliste_neu = [] # korrigierte Liste
94 for entry in wortliste:
95 key = entry[0]
96 if key in korrekturen:
97 entry = korrekturen.pop(key)
98 wortliste_neu.append(entry)
100 if korrekturen:
101 print(u"Die folgenden Einträge sind nicht in der Wortliste:")
102 print(korrekturen) # übrige Einträge
104 return (wortliste, wortliste_neu)
107 # Fehleinträge
108 # ------------
111 def fehleintraege(wordfile, datei='tasks/fehleintraege.todo'):
112 """Entfernen der Einträge aus einer Liste von Fehleinträgen """
114 # Fehleinträge aus Datei.
116 # Format:
117 # Ein Eintrag/Zeile, mit oder ohne Trennzeichen
119 # ::
121 teste_datei(datei)
123 # Dekodieren, Zeilenende entfernen, Trennzeichen entfernen
124 korrekturen = set(join_word(
125 line.strip().replace(';', ' ').split()[0])
126 for line in open(datei, 'r')
127 if line.strip() and not line.startswith('#'))
128 wortliste = list(wordfile)
129 wortliste_neu = [] # korrigierte Liste
130 for entry in wortliste:
131 if entry[0] in korrekturen: # nicht kopieren
132 korrekturen.discard(entry[0]) # erledigt
133 else:
134 wortliste_neu.append(entry)
136 if korrekturen:
137 print('nicht gefunden:')
138 for w in korrekturen:
139 print(w)
141 return (wortliste, wortliste_neu)
144 # Groß-/Kleinschreibung ändern
145 # ----------------------------
147 # Umstellen der Groß- oder Kleinschreibung auf die Variante in der Datei
148 # ``grossklein.todo``
150 # Format:
151 # ein Eintrag oder Wort pro Zeile, mit vorhandener Groß-/Kleinschreibung.
153 # ::
155 def grossklein(wordfile, datei='tasks/grossklein.todo'):
156 """Groß-/Kleinschreibung umstellen"""
158 teste_datei(datei)
160 wortliste = list(wordfile)
162 # Dekodieren, Feldtrenner zu Leerzeichen
163 korrekturen = [line.replace(';',' ')
164 for line in open(datei, 'r')
165 if not line.startswith('#')]
167 # erstes Feld, Trennzeichen entfernen
168 korrekturen = [join_word(line.split()[0]) for line in korrekturen
169 if line.strip() and not line.startswith('#')]
170 korrekturen = set(korrekturen)
171 wortliste_neu = deepcopy(wortliste) # korrigierte Liste
173 for entry in wortliste_neu:
174 if entry[0] in korrekturen:
175 korrekturen.discard(entry[0]) # gefunden
176 # Anfangsbuchstabe mit geänderter Großschreibung:
177 if entry[0][0].islower():
178 anfangsbuchstabe = entry[0][0].title()
179 else:
180 anfangsbuchstabe = entry[0][0].lower()
181 # Einträge korrigieren:
182 for i in range(len(entry)):
183 if entry[i].startswith('-'): # -2-, -3-, ...
184 continue
185 entry[i] = anfangsbuchstabe + entry[i][1:]
187 if korrekturen:
188 print(korrekturen) # übrige Einträge
190 return (wortliste, wortliste_neu)
192 # Anpassung der Großschreibung der Trennmuster an das erste Feld
193 # (ungetrenntes Wort). Siehe "wortliste.py" für einen Test auf Differenzen.
194 # (Differenzen sind größtenteils auf unvorsichtiges Ersetzen mit Texteditor
195 # zurückzuführen.)
196 # ::
198 def grossabgleich(wordfile):
199 wortliste = list(wordfile)
200 wortliste_neu = deepcopy(wortliste) # korrigierte Liste
201 for entry in wortliste_neu:
202 # Übertrag des Anfangsbuchstabens
203 for i in range(1,len(entry)):
204 if not entry[i]:
205 continue
206 entry[i] = entry[0][0] + entry[i][1:]
207 return (wortliste, wortliste_neu)
210 # Sprachvariante ändern
211 # ---------------------
213 # Einträge mit allgemeingültiger (oder sonstwie mehrfacher) Sprachvariante
214 # in "nur in Reformschreibung" (allgemein) ändern.
216 # Format:
217 # ein Wort/(Alt-)Eintrag pro Zeile.
219 # ::
221 def reformschreibung(wordfile, datei='tasks/reformschreibung.todo'):
222 """Wörter die nur in (allgemeiner) Reformschreibung existieren"""
224 teste_datei(datei)
225 # Dekodieren, Zeilenende entfernen
226 korrekturen = [line.strip()
227 for line in open(datei, 'r')
228 if not line.startswith('#')
230 # erstes Feld
231 korrekturen = [line.split(';')[0] for line in korrekturen]
232 korrekturen = set(korrekturen)
234 wortliste = list(wordfile)
235 wortliste_neu = [] # korrigierte Liste
237 for entry in wortliste:
238 if entry[0] in korrekturen:
239 key = entry[0]
240 wort = entry.get('de-1996')
241 if 'ss' in wort:
242 entry = WordEntry('%s;-2-;-3-;%s;%s' % (key, wort, wort))
243 else:
244 entry = WordEntry('%s;-2-;-3-;%s' % (key, wort))
245 korrekturen.discard(key) # erledigt
246 wortliste_neu.append(entry)
248 if korrekturen:
249 print(korrekturen) # übrige Einträge
250 return wortliste, wortliste_neu
253 # Getrennte Einträge für Sprachvarianten
254 # --------------------------------------
256 # Korrigiere fehlende Spezifizierung nach Sprachvarianten, z.B.
258 # - System;Sy-stem
259 # + System;-2-;Sy-stem;Sys-tem
261 # ::
263 def sprachvariante_split(wordfile, alt, neu,
264 altsprache='de-1901', neusprache='de-1996'):
266 wortliste = list(wordfile)
267 wortliste_neu = [] # korrigierte Liste
269 for entry in wortliste:
270 if len(entry) == 2: # Allgemeine Schreibung
271 altwort = entry.get(altsprache)
272 neuwort = altwort.replace(alt, neu)
273 if altwort != neuwort:
274 entry = WordEntry('%s;-2-;3;4' % (join_word(altwort)))
275 entry.set(altwort, altsprache)
276 entry.set(neuwort, neusprache)
277 wortliste_neu.append(entry)
278 return (wortliste, wortliste_neu)
282 # Neueinträge prüfen und vorbereiten
283 # ----------------------------------
285 # Die in einer Datei (ein Neueintrag pro Zeile) gesammelten Vorschläge auf
286 # auf Neuwert testen (vorhandene Wörter werden ignoriert, unabhängig von der
287 # Groß-/Kleinschreibung) und einsortieren.
289 # Akzeptierte Formate:
291 # * vollständiger Eintrag (für Wörter mit Sprachvarianten oder Kommentaren).
293 # * ein Wort mit Trennstellen:
295 # - Schlüssel wird generiert und vorangestellt (durch Semikolon getrennt).
296 # - Test auf einfache/häufige Reformänderungen 1996:
297 # s-t/-st, {ck/k-k}/c-k
299 # ::
301 def neu(wordfile, datei='tasks/neu.todo'):
302 """Neueinträge prüfen und vorbereiten."""
304 teste_datei(datei)
305 neue = open(datei, 'r')
306 print_korrekturhinweis = False
308 wortliste = list(wordfile)
309 wortliste_neu = copy(wortliste)
310 # vorhandene Einträge:
311 words = dict((entry.key().lower(), entry) for entry in wortliste)
313 for line in neue:
314 if line.startswith('#'):
315 continue
316 # Dekodieren, Zeilenende entfernen
317 line = line.strip()
318 if not line:
319 continue
320 # Eintrag ggf. komplettieren:
321 if ';' not in line:
322 line = '%s;%s' % (join_word(line), line)
323 entry = WordEntry(line)
324 key = entry.key().lower()
326 # Test auf "Neuwert":
327 old = words.get(key)
328 if old:
329 if options.force:
330 wortliste_neu.remove(old)
331 else:
332 print(key, 'ignored: schon vorhanden',)
333 # TODO: Einträge vergleichen:
334 if entry == old and entry.comment == old.comment:
335 print('(identisch).')
336 else:
337 print('(unterschiedlich)')
338 print(' -', old)
339 print(' +', entry)
340 print_korrekturhinweis = True
341 continue
343 entry.regelaenderungen() # teste auf Dinge wie s-t/-st
344 wortliste_neu.append(entry)
345 words[key] = entry
347 if print_korrekturhinweis:
348 print('Änderungen können mit "--force" erzwungen werden.')
349 # Sortieren
350 wortliste_neu.sort(key=sortkey_duden)
352 return (wortliste, wortliste_neu)
355 def doppelte(wordfile, use_first=False):
356 """Doppeleinträge entfernen (ohne Berücksichtigung der Großschreibung).
358 Boolscher Wert `use_first` bestimmt, ob der erste oder der letzte von
359 Einträgen mit gleichem Schlüssel in der Liste verbleibt.
361 Die neue Liste ist sortiert.
363 wortliste = list(wordfile)
364 worddict = {}
365 for entry in wortliste:
366 key = entry[0].lower()
367 if use_first and key in worddict:
368 continue
369 worddict[key] = entry
371 wortliste_neu = list(worddict.values()) # korrigierte Liste
372 wortliste_neu.sort(key=sortkey_duden)
374 print(len(wortliste) - len(wortliste_neu), u"Einträge entfernt")
375 return (wortliste, wortliste_neu)
378 def prune(wortliste):
380 wortliste = list(wordfile)
381 wortliste_neu = [] # korrigierte Liste
383 for entry in wortliste:
384 if len(entry) > 2:
385 # Felder zusammenfassen:
386 entry = copy(entry)
387 entry.prune()
388 wortliste_neu.append(entry)
390 return (wortliste, wortliste_neu)
393 # Default-Aktion::
395 if __name__ == '__main__':
397 # Pfad zu "../../../wortliste" unabhängig vom Arbeitsverzeichnis::
399 default_wortliste = os.path.relpath(os.path.join(
400 os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
401 os.path.abspath(__file__))))),
402 'wortliste'))
404 # Optionen::
406 usage = '%prog [Optionen] AKTION\n' + __doc__
408 parser = optparse.OptionParser(usage=usage)
409 parser.add_option('-i', '--file', dest='wortliste',
410 help='Eingangsdatei, Vorgabe "%s"'%default_wortliste,
411 default=default_wortliste)
412 parser.add_option('-k', '--todofile', dest='todo',
413 help='Korrekturdatei, Vorgabe "tasks/<AKTION>.todo"')
414 parser.add_option('-o', '--outfile', dest='patchfile',
415 help='Ausgangsdatei, Vorgabe "%s.patch"'
416 % default_wortliste,
417 default=default_wortliste+'.patch')
418 parser.add_option('-f', '--force',
419 help='überschreibe vorhandene Einträge (Aktion "neu").',
420 action="store_true", default=False)
422 (options, args) = parser.parse_args()
424 if args:
425 aktion = args[0]
426 else:
427 print('Nichts zu tun: AKTION Argument fehlt.', '\n')
428 parser.print_help()
429 sys.exit()
431 # Die `Wortliste`::
432 wordfile = WordFile(options.wortliste)
434 # Da der Aufruf von `wortliste = list(wordfile)` lange dauert, wird er
435 # in den Aktionsroutinen nach dem Test auf die Eingabedatei ausgeführt.
437 # Die Aufgabenliste::
439 todofile = options.todo or "tasks/%s.todo" % aktion
441 # Behandeln::
443 if aktion == 'neu':
444 (wortliste, wortliste_neu) = neu(wordfile, todofile)
445 elif aktion == 'doppelte':
446 (wortliste, wortliste_neu) = doppelte(wordfile)
447 elif aktion == 'fehleintraege':
448 (wortliste, wortliste_neu) = fehleintraege(wordfile, todofile)
449 elif aktion == 'grossklein':
450 (wortliste, wortliste_neu) = grossklein(wordfile, todofile)
451 elif aktion == 'grossabgleich':
452 (wortliste, wortliste_neu) = grossabgleich(wordfile)
453 elif aktion == 'korrektur':
454 (wortliste, wortliste_neu) = korrektur(wordfile, todofile)
455 elif aktion == 'reformschreibung':
456 (wortliste, wortliste_neu) = reformschreibung(wordfile, todofile)
457 elif aktion == 'zusammenfassen':
458 (wortliste, wortliste_neu) = prune(wordfile)
459 else:
460 print('Unbekannte AKTION', '\n')
461 parser.print_help()
462 sys.exit()
464 # (wortliste, wortliste_neu) = sprachvariante_split(wordfile,
465 # 'knien', 'kni-en')
467 # Patch erstellen::
469 patch = udiff(wortliste, wortliste_neu, 'wortliste', 'wortliste-neu')
470 if patch:
471 # print(patch)
472 patchfile = open(options.patchfile, 'w')
473 patchfile.write(patch)
474 print('Änderungen nach %s geschrieben' % options.patchfile)
475 else:
476 print('keine Änderungen')