apply_review.py
Übersetzungsprüfung: wendet die Prüfungsmodule r<JJJJ>_*.py auf translations/<JJJJ>.json an (Ersetzungen bzw. Neufassungen, danach Glossar-Regeln auf jede geprüfte Meldung, Dubletten automatisch) und schreibt die Änderungsliste (uebersetzungspruefung_3301, uebersetzungspruefung_3302, uebersetzungspruefung_3303, uebersetzungspruefung_3304, uebersetzungspruefung_3305, uebersetzungspruefung_3309, uebersetzungspruefung_3310). Dazu dump.py (Original und deutsche Fassung nebeneinander; ohne englischen Text wird ES/FR/PT-BR/RU gezeigt, Dubletten nach Originaltext), cov.py (Vollständigkeit, Jahr über Umgebungsvariable Y), filt.py (Vorfilterung), wkchk.py (Fraktionslisten der Wochenberichte gegen das Original) und mkpkg.py (Korrekturpaket).
Aufruf: python3 translations/review/apply_review.py galnet_raw <JJJJ> (Eingabe ist der Stand vor der Prüfung).
Herunterladen: auf den Dateinamen über dem Code klicken.
apply_review.py
Ablage beim Arbeiten: translations/review/apply_review.py.
- apply_review.py
"""apply_review.py RAW JAHR -> wendet die Durchsicht (translations/review/r<JAHR>_*.py) auf translations/<JAHR>.json an und schreibt die Aenderungsliste translations/review/aenderungen_<JAHR>.txt (Wiki-Syntax). R[fid] = (Titel, Text, [Aenderungen]) ersetzt die Fassung; None = geprueft, keine inhaltliche Aenderung.""" import json, csv, sys, re, glob, importlib, collections from pathlib import Path sys.path.insert(0, 'translations/review') R_ = Path(sys.argv[1]); y = int(sys.argv[2]) idx = [r for r in csv.DictReader(open(R_ / 'index.csv', encoding='utf-8'), delimiter=';') if int(r['date'][:4]) == y] byday = collections.defaultdict(list) for r in idx: byday[r['date'][:10]].append(r['frontier_id']) PID = {} for d, fids in byday.items(): for n, f in enumerate(sorted(fids), 1): PID[f] = f"news:galnet:{d[:4]}:{d[5:7]}:{d[8:10]}_{n:02d}" DATE = {r['frontier_id']: r['date'][:10] for r in idx} P = f'translations/{y}.json' T = json.load(open(P, encoding='utf-8')) R = {} for f in sorted(glob.glob(f'translations/review/r{y}_*.py')): R.update(importlib.import_module(Path(f).stem).R) orig = {k: v['body'] for k, v in T.items()} MISS = [] # Glossar (ki:galnet:glossar): einheitliche Begriffe nach Frontier-DE ab 3306, auf jede geprüfte Meldung angewandt ART = {'der': 'der Föderalen Flotte', 'die': 'die Föderale Flotte', 'Der': 'Der Föderalen Flotte', 'Die': 'Die Föderale Flotte'} def _cmdr(m): # Singular vor Namen oder nach der/ein/jeder, sonst Plural (deutsch „die Commander“) if m[1] or m[2]: return (m[1] or '') + 'Kommandant' return 'Kommandanten' GLOSS = [ # Behörden wie Frontier-DE: IISS = Imperialer Interner Sicherheitsdienst, FIA = Föderaler Nachrichtendienst, FSS = Föderaler Sicherheitsdienst (r'\b(der|Der) Imperial Internal Security Service\b', r'\1 Imperiale Interne Sicherheitsdienst'), (r'\bdes Imperial Internal Security Service\b', 'des Imperialen Internen Sicherheitsdienstes'), (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Imperial Internal Security Service\b', r'\1 Imperialen Internen Sicherheitsdienst'), (r'\bImperial Internal Security Service\b', 'Imperialer Interner Sicherheitsdienst'), (r'\b(?:des|der) Internen Imperialen Sicherheitsdienst(?:es|s)?\b', 'des Imperialen Internen Sicherheitsdienstes'), (r'\bImperiale(\w*) Innere(\w*) Sicherheitsdienst', r'Imperiale\1 Interne\2 Sicherheitsdienst'), (r'\b(der|Der) Geheimdienst der Föderation\b', r'\1 Föderale Nachrichtendienst'), (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Geheimdienst der Föderation\b', r'\1 Föderalen Nachrichtendienst'), (r'\bdes [Ff]öderalen Geheimdienst(?:es|s)?\b', 'des Föderalen Nachrichtendienstes'), (r'\b[Ff]öderale(n|r)? Geheimdienst(es|s)?\b', lambda m: 'Föderale' + (m[1] or '') + ' Nachrichtendienst' + (m[2] or '')), (r'\bProfessor (Katrien )?Rook\b', lambda m: 'Professorin ' + (m[1] or '') + 'Rook'), (r'\bFabrikatoren\b', 'Autofabrikatoren'), (r'\bCobalt\b', 'Kobalt'), (r'\b(der|Der) Federal Security Service\b', r'\1 Föderale Sicherheitsdienst'), (r'\b(dem|Dem|vom|Vom|zum|beim|den|Den) Federal Security Service\b', r'\1 Föderalen Sicherheitsdienst'), (r"\b(?:Imperial Dawn|Imperatorendämmerung|(?:Imperialen?|Imperatorischen?) Morgen(?:röte|dämmerung))\b", "Emperor's Dawn"), (r"\b(gegen|über|für|an) die Emperor's Dawn\b", r"\1 Emperor's Dawn"), (r'^(?:Galaktische Nachrichten|Galactic News):', 'Galaktische News:'), (r'^(?:Freelancer Report|Freelance Report|Freiberuflicher Bericht|Freelancer-Bericht):', 'Freelancer-Report:'), (r'^(?:Community Goal|Gemeinschaftsziel):', 'Community-Ziel:'), (r'^(?:Powerplay|Machtspiele): (?:Wöchentlicher Bericht|Wochenbericht)$', 'Machtspiele: Wochenbericht'), (r'^Powerplay:', 'Machtspiele:'), (r'\bKumo-Besatzung\b', 'Kumo Crew'), (r'\bThargoid Raumpocken-', 'Thargoiden-Barnacle-'), (r'\bRaumpocken-', 'Barnacle-'), (r'\bRaumpocken\b', 'Barnacles'), (r'\bRaumpocke\b', 'Barnacle'), (r'\bWitch Head Nebel', 'Witch-Head-Nebel'), (r'\bInterstellaren? Landwirtschafts-Gesellschaft\b', 'Interstellar Association for Agriculture'), (r'\bILG\b', 'IAA'), (r'\bIISD\b', 'IISS'), (r'\bProfessor (Alba )?Tesreau\b', lambda m: 'Professorin ' + (m[1] or '') + 'Tesreau'), (r'\bOcellus-Sphäre-Sternenhafen\b', 'Ocellus-Sternenhäfen'), (r'\bBarnaklen-', 'Barnacle-'), (r'\b(?:Barnaklen|Baranklen)\b', 'Barnacles'), (r'\bSeepocken\b', 'Barnacles'), (r'\bSeepocke\b', 'Barnacle'), (r'\bPiraten-Lords?\b', lambda m: 'Piratenfürst' + ('en' if m[0].endswith('s') else '')), (r'\bImperatorin(?= Arissa)', 'Imperator'), (r'\bPiratenlords\b', 'Piratenfürsten'), (r'\bPiratenlord\b', 'Piratenfürst'), (r"\b(?:Entdecker|Forscher)vereinigung\b", "Explorers' Association"), (r"\bExplorer's Association\b", "Explorers' Association"), (r'\bSterne?nflughafen(s)?\b', r'Sternenhafen\1'), (r'\bSterne?nflughäfen\b', 'Sternenhäfen'), (r'\bKumo-Crew\b', 'Kumo Crew'), (r'\bSchild(?:es)? der Gerechtigkeit\b', 'Shield of Justice'), (r'\bKorrigendum\b', 'Corrigendum'), (r'\bLibertas-Kooperative\b', 'Libertas Cooperative'), (r'\bDefence Force of Amitrite\b', 'Defence Force of Amitrite'), (r'\bCerberus-(?:Seuche|Pest)\b', 'Cerberus-Plage'), (r'\bZerberus-?[Pp]est\b', 'Cerberus-Plage'), (r'\bRaumschiff Eins\b', 'Starship One'), (r'\bRaumhafen(s)?\b', r'Sternenhafen\1'), (r'\bRaumhäfen\b', 'Sternenhäfen'), (r'\bAdmiral(s)? der Flotte\b', lambda m: 'Flottenadmiral' + (m[1] or '')), (r'\bSternhäfen\b', 'Sternenhäfen'), (r'\b(der|die|Der|Die) (?:Föderationsmarine|Bundesmarine|[Ff]öderalen? Marine|Federal Navy|Föderalen? Navy|Föderal Navy)\b', lambda m: f"{m[1]} {'Föderalen' if m[1].lower() == 'der' else 'Föderale'} Flotte"), (r'\b(?:Föderationsmarine|Bundesmarine|[Ff]öderale Marine|Föderal Navy)\b', 'Föderale Flotte'), (r'\b(der|die|Der|Die) (?:[Ii]mperialen? Marine)\b', lambda m: f"{m[1]} {'Imperialen' if m[1].lower() == 'der' else 'Imperiale'} Flotte"), (r'\b[Ii]mperiale Marine\b', 'Imperiale Flotte'), (r'\bKommandeur(?:s)?\b', 'Kommandant'), (r'\bKommandeure(?:n)?\b', 'Kommandanten'), (r'(?<!Post )(?<!Post-)\bCommanders\b', 'Kommandanten'), (r'(?<!Post )(?<!Post-)(?<!Wing )\b(der |Der |ein |Ein |jeder |Jeder |dieser |Dieser )?Commander\b(?=( [A-Z0-9]| ?[\'"„“]?[A-Z0-9])?)', _cmdr), (r'\bKaiserpalast(?:es|s)?\b', lambda m: 'imperialen Palast' + ('es' if m[0].endswith(('es', 's')) else '')), (r'\bKaiserhochzeit\b', 'imperiale Hochzeit'), (r'\bKaiserreich(?:s|es)?\b', 'Imperium'), (r'\bkaiserlich(e|en|er|es|em)?\b', lambda m: 'imperial' + (m[1] or '')), (r'\bKaiserlich(e|en|er|es|em)?\b', lambda m: 'Imperial' + (m[1] or '')), (r'\bKaisers\b', 'Imperators'), (r'\bKaiser\b', 'Imperator'), (r'\bSternhafen', 'Sternenhafen'), (r'\bGalaxis\b', 'Galaxie'), (r'\bbesetzten (Welt)?[Rr]aum(s|es)?\b', lambda m: 'besiedelten Raum' + (m[2] or '')), (r'\bExplorationsdaten\b', 'Erkundungsdaten'), (r'\bFöderationsraum(s|es)?\b', lambda m: 'föderalen Raum' + (m[1] or '')), (r'\b(im|ins|ans|vom|zum|dem|das|des|ein|einem) ((?:[A-Z][\w\'’.+-]*|\d[\w.+-]*)(?: (?:[A-Z][\w\'’.+-]*|\d[\w.+-]*|[a-z]\d[\w-]*))*)-Systems?\b', lambda m: f"{m[1]} System{'s' if m[0].endswith('Systems') else ''} {m[2]}"), ] def gloss(t): for a, b in GLOSS: t = re.sub(a, b, t) return t.replace('imperialen Palastes', 'imperialen Palastes') log = [] for fid, v in R.items(): twins = [k for k in T if k != fid and orig[k] == orig[fid]] for k in [fid] + twins: T[k]['reviewed'] = True if v is not None and v[0] == 'sub': for a, b in v[1]: if a not in T[k]['body'] and a not in T[k]['title']: MISS.append(f'{k} {a!r}'); continue T[k]['body'] = T[k]['body'].replace(a, b); T[k]['title'] = T[k]['title'].replace(a, b) elif v is not None: T[k]['title'], T[k]['body'] = v[0], v[1].strip() T[k]['title'], T[k]['body'] = gloss(T[k]['title']), gloss(T[k]['body']) if v: log.append((DATE[fid], fid, T[fid]['title'], v[2], twins)) if MISS: raise SystemExit('Ersetzungen passen nicht:\n' + '\n'.join(MISS)) json.dump(T, open(P, 'w', encoding='utf-8'), ensure_ascii=False, indent=1) # Aenderungsliste out = [f"====== Übersetzungsprüfung {y + 1286} ======", "", f"Vollständige Durchsicht aller Übersetzungen ohne Frontier-DE gegen das Original (Englisch; wo Frontier keinen englischen Text hat, Spanisch, Französisch oder Russisch; [[ki:galnet:glossar|Glossar]]). " f"Geprüft: {sum(1 for t in T.values() if t.get('reviewed'))} von {len(T)} Meldungen, davon {len(log)} geändert. " "Fehlerarten: **S** Sinnfehler, **F** Fakt/Name/Zahl, **B** Begriff (Glossar), **Sp** Sprache. " "Meldungen, die nur durch das Glossar angeglichen wurden (z. B. Kaiser → Imperator), sind nicht einzeln aufgeführt.", ""] mon = None for d, fid, title, notes, twins in sorted(log): if d[:7] != mon: mon = d[:7]; out += ["", f"===== {mon[5:7]}/{y + 1286} =====", "^ Datum ^ Meldung ^ Änderungen ^"] n = ' \\\\ '.join(('**' + x + '**') if x.startswith('S:') else x for x in notes).replace('|', '/') extra = f" (+ Dublette {', '.join('[[' + PID[t] + ']]' for t in twins)})" if twins else '' out.append(f"| {d[8:10]}.{d[5:7]}. | [[{PID[fid]}|{title}]]{extra} | {n} |") open(f'translations/review/aenderungen_{y}.txt', 'w', encoding='utf-8').write('\n'.join(out) + '\n') print(len(R), 'geprüft,', len(log), 'geändert,', sum(1 for _, _, _, n, _ in log for x in n if x.startswith('S:')), 'Sinnfehler-Einträge')
dump.py
Ablage beim Arbeiten: translations/review/dump.py.
- dump.py
"""dump.py RAW JAHR MONAT -> Englisch und aktuelle deutsche Fassung nebeneinander (zur Durchsicht)""" import json, csv, sys from pathlib import Path R = Path(sys.argv[1]); y = int(sys.argv[2]); mon = sys.argv[3] idx = [r for r in csv.DictReader(open(R / 'index.csv', encoding='utf-8'), delimiter=';') if r['date'][:7] == f'{y}-{mon}'] EN = {} for f in (R / 'EN').glob(f'{y - 1286 + 1286 - 1286 + 3301 - 2015 + y - y}*.json' if False else f'{y + 1286}*.json'): for a in json.load(open(f, encoding='utf-8'))['articles']: EN[a['id']] = a T = json.load(open(f'translations/{y}.json', encoding='utf-8')) OTHER = {} # Fallback, wenn Frontier keinen englischen Text hat (z. B. nur ES) for L in ('ES', 'FR', 'PT-BR', 'RU', 'EN-US'): for f in (R / L).glob(f'{y + 1286}*.json'): for a in json.load(open(f, encoding='utf-8'))['articles']: OTHER.setdefault(a['id'], (L, a)) seen = {} for r in idx: k = r['frontier_id'] if k not in T: continue e = EN.get(k); lab = 'EN' if not e and k in OTHER: lab, e = OTHER[k] key = (e['body'] if e else T[k]['body'])[:400] # Dublette nach Originaltext, nicht nach Übersetzung if key in seen: print(f"=== {k} {r['date'][:10]} DUBLETTE von {seen[key]}\n"); continue seen[key] = k print(f"=== {k} {r['date'][:10]} [{T[k]['source']}]") print(f'{lab}:', e['title'] if e else '-'); print(e['body'].strip() if e else '') print('DE:', T[k]['title']); print(T[k]['body']); print()
cov.py
Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/cov.py.
- cov.py
import sys, os, importlib; sys.path[:0]=['translations/review','translations/src']; Y=os.environ.get('Y','2015') m=sys.argv[1]; S=sys.argv[2]; R={} for p in sys.argv[3:]+[f"r{Y}_weekly"]: R.update(importlib.import_module(p).R) ids=[l.split()[1] for l in open(f'{S}/rv_{Y}_{m}.txt') if l.startswith('=== ') and 'DUBLETTE' not in l] print('fehlt:',[i for i in ids if i not in R]); W=importlib.import_module(f'r{Y}_weekly'); print("extra:",[i for i in R if i not in ids and i not in W.R])
filt.py
Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/filt.py.
- filt.py
import sys, os, re; sys.path[:0]=['translations/review','translations/src']; Y=os.environ.get('Y','2015') import importlib, weekly; w=importlib.import_module(f'r{Y}_weekly') m=sys.argv[1]; S=sys.argv[2] txt=open(f'{S}/rv_{Y}_{m}.txt').read() blocks=re.split(r'(?m)^(?==== )', txt) out=[] for b in blocks: if not b.strip(): continue fid=b.split()[1] if 'DUBLETTE' in b.split('\n')[0] or fid in w.R: continue t=b.split('\n')[1][4:].strip() if '[KI]' in b.split('\n')[0] and t in weekly.TITLES: out.append(f"=== {fid} KI-WEEKLY\n\n"); continue out.append(b) open(f'{S}/rf_{Y}_{m}.txt','w').write(''.join(out)) print(m, len(out), sum(len(x) for x in out))
wkchk.py
Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/wkchk.py.
- wkchk.py
import re, sys S='/tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad' Y, ms = sys.argv[1], sys.argv[2:] for m in ms: out=[] for b in re.split(r'(?m)^(?==== )', open(f'{S}/rf_{Y}_{m}.txt').read()): l=b.rstrip('\n').split('\n') if len(l)<3 or 'KI-WEEKLY' in l[0]: out.append(b); continue i=[n for n,x in enumerate(l) if x.startswith('DE: ')][0] de=l[i:] if ('Wöchentlicher' in de[0] and 'report' in de[0].lower()) or 'Status-Update' in de[0]: bul=[x[2:].strip() for x in de if x.startswith('• ')] src=[x.replace('<br />','').strip() for x in l[2:i] if x.replace('<br />','').strip() and not re.search(r'[.:!?]\s*$', x.replace('<br />','').strip()) and len(x.split())<=7] src=[re.sub(r'\t',', ',x).strip() for x in src] norm=lambda x: re.sub(r'[\s,]+',' ',x).strip() ok = [norm(x) for x in bul]==[norm(x) for x in src] print(m, l[0].split()[1], 'WEEKLY', 'OK' if ok else f'DIFF {set(src)^set(bul)}') continue out.append(b) open(f'{S}/rg_{Y}_{m}.txt','w').write(''.join(out))
mkpkg.py
Ablage beim Arbeiten: /tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad/mkpkg.py.
- mkpkg.py
"""mkpkg.py OUT YEAR:OLD:NEW [...] -- Korrekturpaket: geänderte Meldungsseiten, Korrektur-CSV, Prüfungs-/Werkzeugseiten""" import os, sys, shutil, json, glob, filecmp, csv S='/tmp/claude-0/-home-claude/412360f8-9c0b-55d1-a24e-f4646e8683bb/scratchpad' OUT=sys.argv[1]; K=f'{S}/{OUT}'; W=f'{K}/pages/ki/galnet/werkzeuge' shutil.rmtree(K, ignore_errors=True); os.makedirs(W) years=[] for a in sys.argv[2:]: y,old,new=a.split(':'); years.append(y); n=0 for root,_,fs in os.walk(f'{S}/{new}/pages/news/galnet/{y}'): for f in fs: nf=os.path.join(root,f); of=nf.replace(f'/{new}/',f'/{old}/') if not os.path.exists(of) or not filecmp.cmp(of,nf,shallow=False): d=nf.replace(f'{S}/{new}/',f'{K}/'); os.makedirs(os.path.dirname(d),exist_ok=True); shutil.copy(nf,d); n+=1 oc=f'{S}/{old}/galnet_{y}.csv' if os.path.exists(oc): old_r={r['pid']:r for r in csv.DictReader(open(oc))} rows=list(csv.DictReader(open(f'{S}/{new}/galnet_{y}.csv'))); ch=[r for r in rows if old_r.get(r['pid'])!=r] if ch: with open(f'{K}/galnet_{y}_korrektur.csv','w',newline='') as f: w=csv.DictWriter(f,list(rows[0].keys())); w.writeheader(); w.writerows(ch) else: ch=[] print(y,'Seiten',n,'CSV',len(ch)) shutil.copy('translations/review/glossar.md', f'{K}/pages/ki/galnet/glossar.txt') def page(name, title, intro, files): out=[f'====== {title} ======','',intro,'',"Herunterladen: auf den Dateinamen über dem Code klicken.",''] for path, lang in files: fn=os.path.basename(path) out += [f"===== {fn} =====", f"Ablage beim Arbeiten: ''{path}''.", '', f'<code {lang} {fn}>', open(path, encoding='utf-8').read().rstrip('\n'), '
', ]
open(f'{W}/{name}.txt','w',encoding='utf-8').write('\n'.join(out))
done=sorted(int(f[-8:-4]) for f in glob.glob('translations/review/aenderungen_*.txt'))
links=', '.join(f'uebersetzungspruefung_{y+1286}' for y in done)
page('apply_review','apply_review.py',f„Übersetzungsprüfung: wendet die Prüfungsmodule ''r<JJJJ>_*.py auf translations/<JJJJ>.json an (Ersetzungen bzw. Neufassungen, danach Glossar-Regeln auf jede geprüfte Meldung, Dubletten automatisch) und schreibt die Änderungsliste ({links}). Dazu dump.py (Original und deutsche Fassung nebeneinander; ohne englischen Text wird ES/FR/PT-BR/RU gezeigt, Dubletten nach Originaltext), cov.py (Vollständigkeit, Jahr über Umgebungsvariable Y), filt.py (Vorfilterung), wkchk.py (Fraktionslisten der Wochenberichte gegen das Original) und mkpkg.py (Korrekturpaket).\n\nAufruf: python3 translations/review/apply_review.py galnet_raw <JJJJ> (Eingabe ist der Stand vor der Prüfung).“,
[('translations/review/apply_review.py','python'),('translations/review/dump.py','python'),(f'{S}/cov.py','python'),(f'{S}/filt.py','python'),(f'{S}/wkchk.py','python'),(f'{S}/mkpkg.py','python')])
for y in years:
j=int(y)+1286
shutil.copy(f'translations/review/aenderungen_{y}.txt', f'{K}/pages/ki/galnet/uebersetzungspruefung_{j}.txt')
mods=sorted(glob.glob(f'translations/review/r{y}_*.py'))
page(f'review_{y}',f'Prüfungsmodule {j}',f"Ergebnis der vollständigen Durchsicht {j}: ''R[frontier_id] = None'' (geprüft, in Ordnung), ''('sub', [(alt, neu), …], Notizen)'' (gezielte Ersetzungen) oder ''(Titel, Text, Notizen)'' (Neufassung). Notizen: **S** Sinnfehler, **F** Fakt/Name/Zahl, **B** Begriff, **Sp** Sprache. ''r{y}_weekly.py'' setzt die alten Wochenberichte mit ''weekly.py'' neu und wird zuletzt geladen.",
[(m,'python') for m in mods])
T=json.load(open(f'translations/{y}.json')); src={}
for v in T.values(): src[v['source']]=src.get(v['source'],0)+1
open(f'{W}/translations_{y}.txt','w',encoding='utf-8').write('\n'.join([f'====== {y}.json ======','',
f"Übersetzungen {j} (keine Frontier-DE): {src.get('eigene',0)} × Oliver, {src.get('KI',0)} × Claude. **Stand nach der vollständigen Übersetzungsprüfung** ([[ki:galnet:uebersetzungspruefung_{j}]]): alle Meldungen gegen das Original gelesen, nach [[ki:galnet:glossar]] vereinheitlicht.",
'',f"Ablage beim Arbeiten: ''translations/{y}.json''. Herunterladen: auf den Dateinamen über dem Code klicken.",'',f'<code json {y}.json>', open(f'translations/{y}.json',encoding='utf-8').read().rstrip('\n'), '</code>','']))
</code>
