-
Alexa Valentová authoredAlexa Valentová authored
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