Skip to content
Snippets Groups Projects
parser.py 8.48 KiB
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