lv-mkn-2024-282 (likumi.lv 351808, konsolidētā redakcija), 484 punkti, XSD derīgs, teksts pārbaudīts; convert.py nolasa publikācijas rekvizītus arī no likumi.lv pases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016igpMJBtapmG6vHE4vSF9q
534 lines
25 KiB
Python
534 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""MK noteikumu HTML → sources/<id>.txt un data/<id>.xml (datu struktūra mk-noteikumi-0.1).
|
|
|
|
python3 tools/convert.py <html> <likumi.lv / vestnesis.lv id> <ielādes datums GGGG-MM-DD> [--org SS-IIII]
|
|
|
|
Avots:
|
|
* vestnesis.lv/ta/id/<id> — oficiālā publikācija (TextVersion basis="original"). Lapā ir divi formāti:
|
|
vecākās publikācijas ar TV klasēm (TV206 galviņa, TV207 nosaukums, TV900 izdošanas pamatojums, TV212 nodaļa,
|
|
TV213 punkts, TV216 paraksti, TV218 pielikuma galviņa, TV444 HTML) un jaunākās ar p.lik_noteik, p.lik_dat, h3,
|
|
p.lik_izd, h4, p, p.lik_paraksts;
|
|
* likumi.lv/ta/id/<id> — konsolidētā redakcija (basis="consolidated"): TV klases, punkti div.TV213[data-num] ar
|
|
norādēm p.labojumu_pamats un tiesību akta pasi.
|
|
HTML repozitorijā netiek glabāts: avots ir teksta datne, XML atbilst tai (tools/validate.py pārbauda).
|
|
"""
|
|
import copy
|
|
import hashlib
|
|
import os
|
|
import re
|
|
import sys
|
|
from xml.sax.saxutils import escape, quoteattr
|
|
|
|
import lxml.html
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SUP = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
|
|
SUPC = "⁰¹²³⁴⁵⁶⁷⁸⁹"
|
|
MONTHS = {"janvār": 1, "februār": 2, "mart": 3, "aprīl": 4, "maij": 5, "jūnij": 6, "jūlij": 7, "august": 8,
|
|
"septembr": 9, "oktobr": 10, "novembr": 11, "decembr": 12}
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------- text helpers
|
|
def norm(s):
|
|
s = re.sub("[]", "", s or "")
|
|
return re.sub(r"[ \t\r\f\v ]+", " ", (s or "").replace("", "")).strip()
|
|
|
|
|
|
def paras_of(el):
|
|
"""Visible text of an element split into paragraphs: <sup> digits → superscript characters; two consecutive <br>
|
|
(or <br> followed by a new number) start a new paragraph; a single <br> is a space."""
|
|
e = copy.deepcopy(el)
|
|
for s in e.iter("sup"):
|
|
s.text = (s.text or "").translate(SUP).strip()
|
|
s.tail = s.tail or ""
|
|
for b in e.iter("br"):
|
|
b.tail = "\ue000" + (b.tail or "")
|
|
for im in e.iter("img"): # checkbox image in forms \u2192 \u2610
|
|
im.tail = ("\u2610 " if "KVADRATS" in (im.get("src") or "").upper() else "") + (im.tail or "")
|
|
raw = e.text_content().replace("", "")
|
|
raw = re.sub(r"\s*\n\s*", " ", raw) # line breaks of the HTML source are spaces
|
|
raw = re.sub(r"\ue000\s*\ue000", "\n", raw)
|
|
out = []
|
|
for part in raw.split("\n"):
|
|
part = norm(part.replace("\ue000", " "))
|
|
if part:
|
|
out.append(part)
|
|
return out
|
|
|
|
|
|
def text_of(el):
|
|
return " ".join(paras_of(el))
|
|
|
|
|
|
def lv_date(s):
|
|
""""2023. gada 7. novembrī" / "2006.gada 5.decembrī" → 2023-11-07"""
|
|
m = re.search(r"(\d{4})\.\s*gada\s*(\d{1,2})\.\s*([a-zāčēģīķļņšūž]+)", s or "")
|
|
if not m:
|
|
return None
|
|
mon = next((v for k, v in MONTHS.items() if m.group(3).startswith(k)), None)
|
|
return f"{m.group(1)}-{mon:02d}-{int(m.group(2)):02d}" if mon else None
|
|
|
|
|
|
# ---------------------------------------------------------------------------------------------------------------- parse
|
|
def events(page):
|
|
"""Ordered events: (kind, text[, extra]). kinds: head, title, basis, chapter, para, note, sig, annex, line, image."""
|
|
root = lxml.html.fromstring(page)
|
|
box = root.xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' docContent ')]")
|
|
if box:
|
|
body = box[0]
|
|
origin = "vestnesis"
|
|
else: # likumi.lv
|
|
t207 = root.xpath("//div[starts-with(@class, 'TV207')]")[0]
|
|
body = t207.getparent()
|
|
origin = "likumi"
|
|
BLOCK = {"p", "h1", "h2", "h3", "h4", "h5", "div", "table"}
|
|
INLINE_P = {"p", "font", "b", "strong", "i", "em", "span", "u", "center"}
|
|
|
|
def blocks(parent):
|
|
"""Block elements in document order; inline wrappers (<b>, <i>, <center>, <div class="block_14">) that hold
|
|
blocks are opened."""
|
|
for e in parent:
|
|
if not isinstance(e.tag, str):
|
|
continue
|
|
tag = e.tag.lower()
|
|
cls = e.get("class") or ""
|
|
wrapper = tag in ("b", "i", "strong", "em", "center", "span", "font", "u") or \
|
|
(tag == "div" and not cls.startswith("TV") and "docContent" not in cls)
|
|
if wrapper and any(isinstance(c.tag, str) and c.tag.lower() in BLOCK for c in e.iter() if c is not e):
|
|
yield from blocks(e)
|
|
elif tag in BLOCK or (wrapper and norm(e.text_content())): # stray inline text (old HTML) = a paragraph
|
|
yield e
|
|
|
|
def plain(el, ev):
|
|
"""Older publications without classes (2006 and earlier): head, date, title, legal basis, EU reference heading,
|
|
signatures and the editorial note are told apart by position and wording. Returns True if the paragraph was used."""
|
|
t = text_of(el)
|
|
if not t:
|
|
return True
|
|
started = any(e[0] in ("para", "chapter") for e in ev)
|
|
if not started:
|
|
if re.match(r"^Ministru kabineta noteikumi Nr", t):
|
|
ev.append(("head", t)); return True
|
|
if re.match(r"^Rīgā \d{4}\.", t) and ev and ev[-1][0] == "head":
|
|
ev[-1] = ("head", ev[-1][1] + " " + t); return True
|
|
if t.startswith("Izdoti saskaņā"):
|
|
ev.append(("basis", t)); return True
|
|
if not any(e[0] == "title" for e in ev) and split_num(t)[0] is None:
|
|
ev.append(("title", t)); return True
|
|
return False
|
|
if re.match(r"^Informatīv[aā] atsauce", t):
|
|
ev.append(("chapter", t)); return True
|
|
if t.startswith("Redakcijas piebilde"):
|
|
ev.append(("editorial", t)); return True
|
|
if (el.get("align") or "").lower() == "right" and el.xpath(".//i") and split_num(t)[0] is None \
|
|
and sig_split(t)[0] != t and not re.search(r"pielikums", t):
|
|
ev.append(("sig", t)); return True
|
|
return False
|
|
|
|
ev, after_sig = [], False
|
|
for el in blocks(body):
|
|
cls = (el.get("class") or "").split()
|
|
c = cls[0] if cls else el.tag.lower()
|
|
tv = re.match(r"TV(\d+)", c)
|
|
code = tv.group(1) if tv else None
|
|
if c == "lik_noteik" or code == "206":
|
|
ev.append(("head", " ".join(paras_of(el))))
|
|
elif c == "lik_dat":
|
|
ev[-1] = ("head", ev[-1][1] + " " + text_of(el)) if ev and ev[-1][0] == "head" else ("head", text_of(el))
|
|
elif el.tag.lower() == "h3" or code == "207":
|
|
ev.append(("title", text_of(el)))
|
|
elif c == "lik_izd" or (code == "900" and not any(e[0] == "chapter" for e in ev)):
|
|
ev.append(("basis", text_of(el)))
|
|
elif c == "lik_paraksts" or code in ("216", "217", "219"):
|
|
for x in ([text_of(el)] if c == "lik_paraksts" else paras_of(el)):
|
|
ev.append(("sig", x))
|
|
after_sig = True
|
|
elif code == "218" or (after_sig and el.tag.lower() in ("p", "h3", "h4") and re.match(r"^\d+\.\s?pielikums", text_of(el))):
|
|
ev.append(("annex", " ".join(paras_of(el))))
|
|
elif after_sig and ev and any(e[0] == "annex" for e in ev):
|
|
imgs = el.xpath(".//img/@src")
|
|
for src in imgs:
|
|
if "KVADRATS" in src.upper(): # checkbox, kept in the text as ☐
|
|
continue
|
|
ev.append(("image", src if src.startswith("http") else "https://www.vestnesis.lv" + re.sub("/+", "/", src)))
|
|
rows = el.xpath(".//tr")
|
|
if rows:
|
|
for tr in rows:
|
|
cells = [text_of(td) for td in tr.xpath("./td|./th")]
|
|
if any(cells):
|
|
ev.append(("line", " | ".join(cells)))
|
|
elif code == "212" and text_of(el):
|
|
ev.append(("line", text_of(el)))
|
|
else:
|
|
for x in paras_of(el):
|
|
ev.append(("line", x))
|
|
elif el.tag.lower() == "h4" or code == "212":
|
|
if text_of(el):
|
|
ev.append(("chapter", text_of(el)))
|
|
elif code == "213" and el.get("data-num"): # likumi.lv consolidated point
|
|
for p in el.xpath("./p[contains(concat(' ', normalize-space(@class), ' '), ' TV213 ')]"):
|
|
for x in paras_of(p):
|
|
ev.append(("para", x))
|
|
for p in el.xpath(".//p[contains(@class,'labojumu_pamats')]"):
|
|
if text_of(p):
|
|
ev.append(("note", text_of(p)))
|
|
elif el.tag.lower() in INLINE_P and not cls and plain(el, ev):
|
|
after_sig = after_sig or bool(ev and ev[-1][0] == "sig")
|
|
elif el.tag.lower() in INLINE_P or code == "213":
|
|
for x in paras_of(el):
|
|
ev.append(("para", x))
|
|
elif el.tag.lower() == "table" and not after_sig:
|
|
for tr in el.xpath(".//tr"):
|
|
cells = [text_of(td) for td in tr.xpath("./td|./th")]
|
|
if any(cells):
|
|
ev.append(("para", " | ".join(cells)))
|
|
return origin, ev
|
|
|
|
|
|
def split_num(text):
|
|
"""Printed point number → (number, rest). "3.1. x" → 3.1; "4.4.¹ x" / "4.4¹. x" → 4.4¹; "24.¹1. x" → 24¹.1;
|
|
"1) x" → ("1)", x)."""
|
|
m = re.match(r"^(\d+)\)\s*", text)
|
|
if m:
|
|
return m.group(1) + ")", text[m.end():]
|
|
m = re.match(r"^(\d+)", text)
|
|
if not m:
|
|
return None, text
|
|
segs, i = [m.group(1)], m.end()
|
|
while True:
|
|
rest = text[i:]
|
|
sup = re.match(r"^\.?([" + SUPC + r"]+)", rest)
|
|
if sup and not segs[-1].endswith(tuple(SUPC)):
|
|
segs[-1] += sup.group(1)
|
|
i += sup.end()
|
|
nxt = re.match(r"^\.?\s?(\d+)(?=[." + SUPC + r"])", text[i:])
|
|
if nxt:
|
|
segs.append(nxt.group(1)); i += nxt.end()
|
|
continue
|
|
nxt = re.match(r"^\.(\d+)(?=[." + SUPC + r"])", rest)
|
|
if nxt:
|
|
segs.append(nxt.group(1)); i += nxt.end()
|
|
continue
|
|
break
|
|
end = re.match(r"^\.\s*", text[i:]) or (re.match(r"^\s+", text[i:]) if segs[-1][-1] in SUPC else None)
|
|
if not end:
|
|
return None, text
|
|
rest = text[i + end.end():]
|
|
if not rest or re.match(r"^(gada|panta|pant|punkt|apakšpunkt|daļ|nodaļ|pielikum|un |vai |–)", rest): # "2006. gada …" is not a point
|
|
return None, text
|
|
return ".".join(segs), rest
|
|
|
|
|
|
def pid(num):
|
|
"""3.1 → p3.1; 4.2¹ → p4.2s1"""
|
|
return "p" + re.sub("[" + SUPC + "]+", lambda m: "s" + m.group(0).translate(str.maketrans(SUPC, "0123456789")), num)
|
|
|
|
|
|
def chapter_kind(title):
|
|
t = (title or "").lower()
|
|
if "pārejas" in t:
|
|
return "transitional"
|
|
if "noslēguma" in t:
|
|
return "final"
|
|
if "vispārīg" in t:
|
|
return "general"
|
|
return "substantive"
|
|
|
|
|
|
def point_kind(text, chapter_k, has_children):
|
|
t = text.lower()
|
|
if t.startswith("(svītrots") or t == "svītrots.":
|
|
return "deleted"
|
|
if re.match(r"^atzīt par spēku zaudējuš", t):
|
|
return "repeal"
|
|
if re.search(r"^(noteikumi|šie noteikumi)( [^.]{0,40})? stājas spēkā", t) or re.match(r"^noteikumu \d", t) and "stājas spēkā" in t:
|
|
return "in_force"
|
|
if re.match(r"^(noteikumi|šie noteikumi) (nosaka|attiecas)", t):
|
|
return "scope"
|
|
if text.rstrip().endswith(":") or has_children:
|
|
return "lead"
|
|
if re.match(r"^[^–—-]{2,80} [–—-] ", text) and chapter_k == "general":
|
|
return "definition"
|
|
if chapter_k == "transitional":
|
|
return "transitional"
|
|
return "provision"
|
|
|
|
|
|
def build_tree(ev, doc):
|
|
chapters, top, eu, sigs, annexes = [], None, None, [], []
|
|
cur_ch, last_point, index = None, None, {}
|
|
ch_seq = [0]
|
|
mode = "body"
|
|
|
|
def new_chapter(title, parent=None):
|
|
nonlocal cur_ch
|
|
m = re.match(r"^([IVXLC]+|\d+(?:\.\d+)*)\.\s*(.*)$", title)
|
|
n, t = (m.group(1), m.group(2)) if m else (None, title)
|
|
if parent is None:
|
|
ch_seq[0] += 1
|
|
ch = {"id": f"{doc}.c{ch_seq[0]}", "n": n, "title": t, "notes": [], "items": [], "sub": 0}
|
|
chapters.append(ch)
|
|
else:
|
|
parent["sub"] += 1
|
|
ch = {"id": f"{parent['id']}.{parent['sub']}", "n": n, "title": t, "notes": [], "items": [], "sub": 0}
|
|
parent["items"].append(("chapter", ch))
|
|
cur_ch = ch
|
|
return ch
|
|
|
|
for e in ev:
|
|
k, x = e[0], e[1]
|
|
if k == "chapter":
|
|
if re.match(r"^informatīv[aā] atsauce", x.lower()):
|
|
eu = {"id": f"{doc}.eu", "title": x, "lead": None, "items": []}
|
|
mode = "eu"
|
|
continue
|
|
mode = "body"
|
|
m = re.match(r"^(\d+)\.(\d+)\.?\s", x) # 2.1. apakšnodaļa within chapter 2
|
|
if m and top is not None and top["n"] == m.group(1):
|
|
new_chapter(x, parent=top)
|
|
else:
|
|
top = new_chapter(x)
|
|
last_point = None
|
|
elif k == "note":
|
|
if last_point is not None:
|
|
last_point["notes"].append(x)
|
|
elif cur_ch is not None:
|
|
cur_ch["notes"].append(x)
|
|
elif k == "para" and mode == "eu":
|
|
num, rest = split_num(x)
|
|
if num and num.endswith(")"):
|
|
eu["items"].append((num, x))
|
|
elif not eu["items"] and eu["lead"] is None and x.rstrip().endswith(":"):
|
|
eu["lead"] = x
|
|
elif not eu["items"] and re.search(r"Direktīv|Regul", x): # one act, written as one sentence
|
|
eu["items"].append((None, x))
|
|
elif not eu["items"] and eu["lead"] is None:
|
|
eu["lead"] = x
|
|
elif eu["items"]:
|
|
n0, t0 = eu["items"][-1]
|
|
eu["items"][-1] = (n0, t0 + "\n" + x)
|
|
elif k == "para":
|
|
if cur_ch is None:
|
|
top = new_chapter("")
|
|
cur_ch["n"], cur_ch["title"] = None, None
|
|
num, rest = split_num(x)
|
|
if num is None:
|
|
if last_point is not None:
|
|
last_point["text"] += "\n" + x
|
|
else:
|
|
cur_ch["notes"].append(x)
|
|
continue
|
|
if num.endswith(")"):
|
|
parent = last_point if last_point is not None and not last_point["n"].endswith(")") else (last_point or {}).get("parent")
|
|
if parent is None:
|
|
raise SystemExit(f"uzskaitījuma elements bez punkta: {x[:80]}")
|
|
p = {"n": num, "id": f"{parent['id']}.i{num[:-1]}", "text": x, "notes": [], "children": [], "parent": parent}
|
|
parent["children"].append(p)
|
|
else:
|
|
ident = f"{doc}.{pid(num)}"
|
|
if ident in index: # "13.7. apakšpunktā minētie …" — a reference opening a paragraph, not a new point
|
|
print(f" brīdinājums: {num} jau ir — rindkopa pievienota iepriekšējam punktam: {x[:60]}", file=sys.stderr)
|
|
if last_point is not None:
|
|
last_point["text"] += "\n" + x
|
|
continue
|
|
pnum = num.rsplit(".", 1)[0] if "." in num else None
|
|
parent = index.get(f"{doc}.{pid(pnum)}") if pnum else None
|
|
p = {"n": num, "id": ident, "text": x, "notes": [], "children": [], "parent": parent}
|
|
index[ident] = p
|
|
(parent["children"] if parent else cur_ch["items"]).append(p if parent else ("point", p))
|
|
p["chk"] = chapter_kind(cur_ch["title"]) if cur_ch["title"] else "none"
|
|
last_point = p
|
|
elif k == "sig":
|
|
if annexes:
|
|
annexes[-1]["sigs"].append(x)
|
|
else:
|
|
sigs.append(x)
|
|
elif k == "annex":
|
|
n = re.match(r"^(\d+)\.\s?pielikums", x)
|
|
annexes.append({"id": f"{doc}.a{len(annexes) + 1}", "n": n.group(1) if n else None, "heading": x, "title": None,
|
|
"lines": [], "images": [], "sigs": []})
|
|
elif k == "line" and annexes:
|
|
a = annexes[-1]
|
|
if a["title"] is None and not a["lines"] and not a["images"]:
|
|
a["title"] = x
|
|
else:
|
|
a["lines"].append(x)
|
|
elif k == "image" and annexes:
|
|
annexes[-1]["images"].append(x)
|
|
return chapters, eu, sigs, annexes
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------------------------------- write
|
|
def a(k, v):
|
|
return f" {k}={quoteattr(str(v))}" if v not in (None, "") else ""
|
|
|
|
|
|
def el(tag, text, **at):
|
|
return f"<{tag}{''.join(a(k, v) for k, v in at.items())}>{escape(text)}</{tag}>" if text else ""
|
|
|
|
|
|
def sig_split(s):
|
|
""""Ministru prezidents A.Kalvītis" / "Klimata un enerģētikas ministrs K. Melnis" → (role, name)."""
|
|
m = re.match(r"^(.*?)\s+((?:[A-ZĀČĒĢĪĶĻŅŠŪŽ]\.\s?)+[A-ZĀČĒĢĪĶĻŅŠŪŽ][\wāčēģīķļņšūž-]+(?:\s[A-ZĀČĒĢĪĶĻŅŠŪŽ][\wāčēģīķļņšūž-]+)?)$", s)
|
|
return (m.group(1), m.group(2)) if m else (s, s)
|
|
|
|
|
|
def ministry_of(sigs):
|
|
for s in sigs:
|
|
role, _ = sig_split(s)
|
|
m = re.match(r"^(.*?)\s+ministra (?:pienākumu izpildītāj|vietā)", role) or re.match(r"^(.*?)\s+ministr(s|e)$", role)
|
|
if m and "prezident" not in role.lower():
|
|
return m.group(1) + " ministrija"
|
|
return None
|
|
|
|
|
|
def point_xml(p, lines, ind):
|
|
text = p["text"]
|
|
if not p["n"].endswith(")"):
|
|
text = split_num(text.split("\n")[0])[1] + ("\n" + "\n".join(text.split("\n")[1:]) if "\n" in text else "")
|
|
kind = "item" if p["n"].endswith(")") or (p["parent"] is not None and p["parent"]["text"].rstrip().endswith(":")) else \
|
|
point_kind(text, p.get("chk", "substantive"), bool(p["children"]))
|
|
if kind == "item" and point_kind(text, "x", False) == "deleted":
|
|
kind = "deleted"
|
|
elif kind == "item" and p["parent"] is not None and re.search(r"termin", p["parent"]["text"].split("\n")[0].lower()):
|
|
kind = "definition"
|
|
lines.append(f"{ind}<Point{a('id', p['id'])}{a('n', p['n'])}{a('kind', kind)}>")
|
|
lines.append(f"{ind} {el('Text', text)}")
|
|
for n in p["notes"]:
|
|
lines.append(f"{ind} {el('Note', n)}")
|
|
for c in p["children"]:
|
|
point_xml(c, lines, ind + " ")
|
|
lines.append(f"{ind}</Point>")
|
|
|
|
|
|
def chapter_xml(ch, lines, ind, kind_parent=None):
|
|
kind = ("none" if not ch["title"] else chapter_kind(ch["title"])) if kind_parent is None else \
|
|
(chapter_kind(ch["title"]) if chapter_kind(ch["title"]) != "substantive" else kind_parent)
|
|
lines.append(f"{ind}<Chapter{a('id', ch['id'])}{a('n', ch['n'])}{a('kind', kind)}>")
|
|
if ch["title"]:
|
|
lines.append(f"{ind} {el('Title', ch['title'])}")
|
|
for n in ch["notes"]:
|
|
lines.append(f"{ind} {el('Note', n)}")
|
|
for t, item in ch["items"]:
|
|
if t == "point":
|
|
point_xml(item, lines, ind + " ")
|
|
else:
|
|
chapter_xml(item, lines, ind + " ", kind)
|
|
lines.append(f"{ind}</Chapter>")
|
|
|
|
|
|
def celex(text):
|
|
m = re.search(r"(Direktīv|Regul)\w*\s+(?:\((ES|EK|EEK|Euratom)\)\s+)?(\d{4})/(\d+)(?:/(ES|EK|EEK))?", text)
|
|
if not m:
|
|
m2 = re.search(r"(Direktīv|Regul)\w*\s+(?:Nr\.\s*)?(\d+)/(\d{4})", text)
|
|
if m2:
|
|
return f"3{m2.group(3)}{'L' if m2.group(1) == 'Direktīv' else 'R'}{int(m2.group(2)):04d}"
|
|
return None
|
|
return f"3{m.group(3)}{'L' if m.group(1) == 'Direktīv' else 'R'}{int(m.group(4)):04d}"
|
|
|
|
|
|
def build(page, src_id, retrieved, org=None):
|
|
origin, ev = events(page)
|
|
head = next((e[1] for e in ev if e[0] == "head"), "")
|
|
title = next(e[1] for e in ev if e[0] == "title")
|
|
basis = next((e[1] for e in ev if e[0] == "basis"), None)
|
|
m = re.search(r"noteikumi Nr\.\s*(\d+)", head)
|
|
number = int(m.group(1))
|
|
adopted = lv_date(head)
|
|
year = adopted[:4]
|
|
doc = f"lv-mkn-{year}-{number}"
|
|
protocol = re.search(r"\((prot\.[^)]*)\)", head)
|
|
pub = re.search(r"Publicēts oficiālajā (?:izdevumā|laikrakstā) "Latvijas Vēstnesis", (\d{1,2})\.(\d{1,2})\.(\d{4})\., Nr\. ([0-9A-Za-z]+)", page) \
|
|
or re.search(r'Publicēts oficiālajā (?:izdevumā|laikrakstā) "Latvijas Vēstnesis", (\d{1,2})\.(\d{1,2})\.(\d{4})\., Nr\. ([0-9A-Za-z]+)', page)
|
|
if not pub: # likumi.lv passport: "Publicēts: <a …>Latvijas Vēstnesis</a>, 89, 09.05.2024."
|
|
m2 = re.search(r"Publicēts: </font><a[^>]*>Latvijas Vēstnesis</a>, ([0-9A-Za-z]+), (\d{1,2})\.(\d{1,2})\.(\d{4})\.", page)
|
|
if m2:
|
|
class _Pub: # same groups as the vestnesis pattern: day, month, year, issue
|
|
def __init__(self, m): self.g = (None, m.group(2), m.group(3), m.group(4), m.group(1))
|
|
def group(self, i): return self.g[i]
|
|
pub = _Pub(m2)
|
|
op = re.search(r"https://www\.vestnesis\.lv/op/(\d{4}/[0-9A-Za-z]+\.\d+)", page)
|
|
pub_date = f"{pub.group(3)}-{int(pub.group(2)):02d}-{int(pub.group(1)):02d}" if pub else adopted
|
|
chapters, eu, sigs, annexes = build_tree(ev, doc)
|
|
|
|
# ------------------------------------------------------------------------------------------ text source (one line each)
|
|
txt = [x for x in ([head, title, basis]) if x]
|
|
for e in ev:
|
|
if e[0] in ("chapter", "para", "note", "sig", "annex", "line", "editorial"):
|
|
txt.append(e[1])
|
|
elif e[0] == "image":
|
|
txt.append("[attēls] " + e[1])
|
|
body = "\n".join(txt) + "\n"
|
|
os.makedirs(f"{ROOT}/sources", exist_ok=True)
|
|
with open(f"{ROOT}/sources/{doc}.txt", "w", encoding="utf-8") as f:
|
|
f.write(body)
|
|
sha = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
|
|
url = f"https://www.vestnesis.lv/ta/id/{src_id}" if origin == "vestnesis" else f"https://likumi.lv/ta/id/{src_id}"
|
|
ministry = ministry_of(sigs)
|
|
L = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
f'<CabinetRegulation xmlns="urn:pppa:vpk:mk-noteikumi:0.1" id="{doc}" schemaVersion="0.1">',
|
|
" <Act>", f" {el('Title', title)}", " <Issuer>Ministru kabinets</Issuer>", " <Type>Noteikumi</Type>",
|
|
f" <Number>{number}</Number>", f" <Adopted>{adopted}</Adopted>"]
|
|
if protocol:
|
|
L.append(f" {el('Protocol', protocol.group(1))}")
|
|
if ministry:
|
|
L.append(f" <ResponsibleInstitution{a('org', org)} basis=\"signatory\">{escape(ministry)}</ResponsibleInstitution>")
|
|
if pub:
|
|
L.append(f" <Published year=\"{pub.group(3)}\" issue=\"{pub.group(4)}\"{a('opId', op.group(1) if op else None)}>"
|
|
f"Latvijas Vēstnesis, {pub.group(3)}, {pub.group(4)}. nr.</Published>")
|
|
# in force: the editorial note ("noteikumi stājas spēkā ar 2006.gada 29.aprīli") or the in-force point of the text
|
|
force = next((lv_date(e[1]) for e in ev if e[0] == "editorial" and "stājas spēkā" in e[1]), None)
|
|
if not force:
|
|
force = next((lv_date(e[1]) for e in ev if e[0] == "para" and re.match(r"^\d+\.\s*(Šie )?[Nn]oteikumi stājas spēkā", e[1])), None)
|
|
if force:
|
|
L.append(f" <InForce>{force}</InForce>")
|
|
L += [f" <LikumiId>{src_id}</LikumiId>", f" {el('Url', url)}", " </Act>",
|
|
f" <TextVersion basis=\"{'original' if origin == 'vestnesis' else 'consolidated'}\" asOf=\"{pub_date if origin == 'vestnesis' else retrieved}\"/>",
|
|
" <Source>", f" <File>sources/{doc}.txt</File>", f" <SHA256>{sha}</SHA256>", f" <Retrieved>{retrieved}</Retrieved>",
|
|
f" {el('Url', url)}", " </Source>"]
|
|
if basis:
|
|
L.append(f" {el('LegalBasis', basis)}")
|
|
for ch in chapters:
|
|
chapter_xml(ch, L, " ")
|
|
if eu:
|
|
L.append(f" <EuReference id=\"{eu['id']}\">")
|
|
L.append(f" {el('Title', eu['title'])}")
|
|
if eu["lead"]:
|
|
L.append(f" {el('Lead', eu['lead'])}")
|
|
for i, (n, t) in enumerate(eu["items"], 1):
|
|
L.append(f" <Item id=\"{eu['id']}.{i}\"{a('celex', celex(t))}>{el('Text', t)}</Item>")
|
|
L.append(" </EuReference>")
|
|
for s in sigs:
|
|
role, name = sig_split(s)
|
|
L.append(f" <Signatory role={quoteattr(role)}>{escape(name)}</Signatory>")
|
|
for an in annexes:
|
|
L.append(f" <Annex{a('id', an['id'])}{a('n', an['n'])}>")
|
|
L.append(f" {el('Heading', an['heading'])}")
|
|
if an["title"]:
|
|
L.append(f" {el('Title', an['title'])}")
|
|
for x in an["lines"]:
|
|
L.append(f" {el('Line', x)}")
|
|
for x in an["images"]:
|
|
L.append(f" {el('Image', x)}")
|
|
for s in an["sigs"]:
|
|
role, name = sig_split(s)
|
|
L.append(f" <Signatory role={quoteattr(role)}>{escape(name)}</Signatory>")
|
|
L.append(" </Annex>")
|
|
L += [f' <DataVersion number="1" date="{retrieved}">',
|
|
f" {el('Change', 'Pirmā versija: ' + ('oficiālā publikācija no vestnesis.lv, vēlākie grozījumi nav iestrādāti' if origin == 'vestnesis' else 'konsolidētā redakcija no likumi.lv'))}",
|
|
" </DataVersion>", "</CabinetRegulation>"]
|
|
os.makedirs(f"{ROOT}/data", exist_ok=True)
|
|
with open(f"{ROOT}/data/{doc}.xml", "w", encoding="utf-8") as f:
|
|
f.write("\n".join(L) + "\n")
|
|
return doc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = [x for x in sys.argv[1:] if not x.startswith("--")]
|
|
org = sys.argv[sys.argv.index("--org") + 1] if "--org" in sys.argv else None
|
|
if org:
|
|
args.remove(org)
|
|
page = open(args[0], encoding="utf-8", errors="replace").read()
|
|
print(build(page, int(args[1]), args[2], org))
|