import json import re import sys import urllib from collections import defaultdict import bs4 from django.utils.text import slugify from shared.utils import strip_all_html_tags from .constants import BENEFITS KNOWN_KEYS = [ "nadpis", "anotace", "problem", "kontext-problemu", "ideal", "navrhovana-opatreni", "casovy-horizont", "co-jsme-uz-udelali", "faq", "souvisejici-body", "zdroje", ] BENEFIT_FOR_ALL = "společnost jako celek" MAIN_BENEFITS = {slugify(old_name): num for num, _, old_name in BENEFITS} def parse_program_html(fp): # Načteme celý dokument. html = bs4.BeautifulSoup(fp, "html5lib") # Vyházíme odkazy na komentáře. for cmnt in html.select('*[id^="cmnt"]'): cmnt.parent.extract() # Bod má svůj pracovní název nazev_bodu = html.select_one("h1, h2, h3").text.strip() # Bod má své pojmenované sekce. bod = {} SEKCE = [ "Nadpis", "Anotace", "Problém", "Kontext problému", "Ideál", "Navrhovaná opatření", "Časový horizont", "FAQ", "Související body", "Zdroje", "Co jsme už udělali", ] # Tabulka benefitů má své pojmenované cílové skupiny. benefity = {} # Chceme očesat HTML na výstupu a nechat jenom tyto atributy. ATRIBUTY = set(["id", "href"]) # Dokument má právě dvě tabulky. První je s bodem a druhá s jeho benefity. bod_html, bene_html = html.select("table") # Z CSS je potřeba vyvodit, které třídy odpovídají tučnému textu a # které superskriptu. Protože Google. strong = [] sup = [] for style in html.select("style"): strong = re.findall(r"(\.c[0-9]+)\{[^{]*font-weight:700", style.text) sup = re.findall(r"(\.c[0-9]+)\{[^{]*vertical-align:super", style.text) assert strong, "Nenašel jsem styl pro tučný text" assert sup, "Nenašel jsem styl pro superskript" def vycisti(sekce): sekce.name = "div" sekce.attrs.clear() # Nahradíme třídy nativními HTML prvky. for tag in sekce.select(", ".join(sup)): tag.name = "sup" for tag in sekce.select(", ".join(strong)): tag.name = "strong" # Zbavíme se <span>ů. for tag in sekce.select("span"): tag.unwrap() # Ořízneme nežádoucí atributy. for tag in sekce.find_all(): for attr in list(tag.attrs): if attr not in ATRIBUTY: del tag.attrs[attr] if tag.name != "a" and attr == "id": del tag.attrs[attr] # Ořízneme prázdné tagy. for tag in sekce.find_all(): if tag.text == "" and tag.name not in ["br", "hr"]: tag.extract() # Opravíme odkazy, které se nakazily Googlem. for tag in sekce.select("*[href]"): _proto, _loc, _path, query, _frag = urllib.parse.urlsplit(tag.attrs["href"]) qs = urllib.parse.parse_qs(query) if "q" in qs: tag.attrs["href"] = qs["q"][0] # Opravíme odkazy, které se nakazily Facebookem. for tag in sekce.select("*[href]"): proto, loc, path, query, frag = urllib.parse.urlsplit(tag.attrs["href"]) qs = urllib.parse.parse_qs(query) if "fbclid" in qs: del qs["fbclid"] query = urllib.parse.urlencode(qs, doseq=True) tag.attrs["href"] = urllib.parse.urlunsplit( (proto, loc, path, query, frag) ) # Spojíme po sobě následující prvky některých typů. for fst in sekce.select("ul, sup, strong"): if fst.parent is None: continue snd = fst.next_sibling while snd is not None: if snd.name == fst.name: snd.extract() for child in snd: fst.append(child) else: break snd = fst.next_sibling # Nejprve zpracujeme bod. radky = list(bod_html.select("tr")) for radek in radky[1:]: nazev, sekce = radek.select("td") nazev = nazev.text nazev = nazev.strip(" \u00a0\t\r\n:") nazev = re.sub("[ \u00a0]+", " ", nazev) if nazev not in SEKCE: print("Přebývá neznámá sekce: {!r}".format(nazev), file=sys.stderr) vycisti(sekce) bod[nazev] = sekce for nazev in SEKCE: if nazev not in bod: print("Chybí povinná sekce {!r}".format(nazev), file=sys.stderr) # Benefity for radek in bene_html.select("tr")[1:]: cilovka, benefit, _info = radek.select("td") cilovka = cilovka.text cilovka = re.sub(r"\(.*\)", "", cilovka) cilovka = cilovka.strip(" \u00a0\t\r\n:") cilovka = re.sub("[ \u00a0]+", " ", cilovka) vycisti(benefit) if benefit.text.strip() != "": benefity[cilovka] = benefit # Pro případnou zběžnou kontrolu: # print("<h1>" + nazev_bodu + "</h1>") # for nazev, sekce in bod.items(): # print("<hr/>") # print("<h2>" + nazev + "</h2>") # print(sekce) # print("<hr/>") # print("<h2>Benefity</h2>") # for cilovka, benefit in benefity.items(): # print("<h3>" + cilovka + "</h3>") # print(benefit) return { "nazev": nazev_bodu, "sekce": {nazev: str(sekce) for nazev, sekce in bod.items()}, "benefity": {cilovka: str(benefit) for cilovka, benefit in benefity.items()}, } def strip_div(value): return value.replace("<div>", "").replace("</div>", "") def replace_tags(value): value = strip_div(value) if not value.startswith("<p>"): value = f"<p>{value}</p>" return value def set_fancy_lists(value): value = value.replace("<ul>", '<ul class="unordered-list unordered-list-checks">') value = value.replace("<li>", '<li class="mb-4">') return value def clean_point(point): out = {} for old_key, val in point.items(): key = slugify(old_key) if key not in KNOWN_KEYS: raise ValueError(f"Unknown key: {old_key}") if key in ["nadpis"]: out[key] = strip_all_html_tags(val) else: out[key] = replace_tags(val) return out def prepare_faq(value): soup = bs4.BeautifulSoup(value, "html.parser") questions = defaultdict(list) for tag in soup.children: if tag.strong: key = tag.strong.string else: questions[key].append(str(tag)) data = [] for key, val in questions.items(): data.append( {"type": "question", "value": {"question": key, "answer": "".join(val)}} ) return json.dumps(data) def prepare_horizon(value): raw = strip_all_html_tags(value) m = re.match(r"^(\d+)\s(\w+)$", raw) if m: return None, m.group(1), m.group(2) return value, None, None def print_preview(point): print("") for key, val in point.items(): print(key, ":", val[:120]) def print_full(point): for key, val in point.items(): print("") print(key) print("-" * 100) print(val) print("-" * 100) def prepare_point(source): point = clean_point(source) # print_full(point) return point def prepare_benefit_for_all(benefits): if BENEFIT_FOR_ALL in benefits: text = benefits[BENEFIT_FOR_ALL] return strip_div(text) return None def prepare_main_benefits(benefits): data = [] for name, text in benefits.items(): if name == BENEFIT_FOR_ALL: continue name_slug = slugify(name) if name_slug in MAIN_BENEFITS: data.append( { "type": "benefit", "value": { "variant": MAIN_BENEFITS[name_slug], "text": strip_div(text), }, } ) return json.dumps(data) if data else None def prepare_benefits(benefits): data = [] for name, text in benefits.items(): if name == BENEFIT_FOR_ALL: continue name_slug = slugify(name) if name_slug not in MAIN_BENEFITS: data.append( { "type": "benefit", "value": {"title": name, "text": strip_div(text)}, } ) return json.dumps(data) if data else None