Domača naloga: Gutenberg

V domači nalogi delamo z enakimi podatki kot na predavanju in vajah, vendar je projekt Guttenberg na dan ali dva po predavanjih spremenil obliko svojih strani. To je odlično, saj boste morali svoje znanje pokazati na podobni, vendar malenkost drugačni nalogi.

Podatki projekta Gutenberg so dostopni tudi v lažje berljivi obliki. Tole delamo za vajo.

Ocena 6

Za oceno 6 bo potrebno napisati le dve funkciji, ki ne bi smeli imeti več kot 10 vrstic.

prenesi_podatke()

Rešitev

Komentirati ni kaj dosti, saj vse piše že v nasvetih o koristnih funkcijah: preverimo, ali direktorij obstaja in ga naredimo, če ga ni, nato pa gremo čez vse začetnice in poberemo, česar ni. Z urlopen "odpremo" stran, z read() preberemo vsebino in jo z decode("utf-8") pretvorimo iz bajtov v niz.

import os
import string
from urllib.request import urlopen

def prenesi_podatke():
    if not os.path.exists("authors"):
        os.mkdir("authors")
    for crka in string.ascii_lowercase:
        if not os.path.exists(f"authors/{crka}.html"):
            open(f"authors/{crka}.html", "w").write(
                urlopen(f'https://ucilnica.fri.uni-lj.si/pluginfile.php/217381/mod_folder/content/0/{crka}.html'
                       ).read().decode("utf-8"))

avtorji(priimek)

Rešitev

Najprej preberemo html, v katerem bomo našli iskanega avtorja. Nekateri študenti so delali zanko prek vseh črk; to nima smisla, saj vemo, da je iskani avtor v f"authors/{priimek[0]}.html".

Preberemo torej to datoteko. Z lepo župo gremo čez vse h2 in strings poberemo vse, kar je napisano znotraj. Razdelimo glede na vejice in če je prvi element enak iskanemu priimku, dodamo celotno ime avtorja v seznam, ki ga na koncu vrnemo.

def avtorji(priimek):
    html = open(f"authors/{priimek[0]}.html").read()
    soup = BeautifulSoup(html, "html.parser")
    avtorji = []
    for h2 in soup.find_all("h2"):
        avtor = "".join(h2.strings)
        if avtor and avtor.split(", ")[0] == priimek:
            avtorji.append(avtor)
    return avtorji

Ocena 7

razberi_avtorja

Rešitev

Tule se začne najbolj zoprni del naloge - ne toliko zaradi zahtevnega dela temveč zato, ker je bilo potrebno (ne povsem predvideno) upoštevati toliko posebnosti.

def razberi_avtorja(s):
    deli = s.split(", ")
    mo = re.match(r"(\d*)\??( BC)?-(\d*)\??( BC)?", deli[-1])
    if not mo:
        return None
    rojen, rbc, umrl, ubc = mo.groups()
    rojen = int(rojen) if rojen else None
    umrl = int(umrl) if umrl else None
    if rbc:
        rojen = -rojen
    if ubc:
        umrl = -umrl
    return deli[0], deli[1:-1], rojen, umrl

Regularni izraz smo na srečo napisali že na predavanju, le še ( BC)? (ki pomeni da mora pride presledek in BC) dodamo.

Ker naloga pravi, da mora letnica slediti zadnji vejici in presledku, niz razbijemo glede na to ločilo in potem gledamo zadnji kos (deli[-1]). Če ta ne ustreza regularnemu izrazu, vrnemo None. Sicer shranimo njegove dele v spremenljivke - spet tako kot na predavanjih, le da imamo še dva dodatna rbc in ubc, ki sta morda prazna, morda pa sta enaka " BC". Letnici pretvorimo v števili in če sta BC, ju še negiramo. Nato vrnemo, kar zahteva naloga: priimek (prvi element, deli[0]), ostala imena (vsi elementi razen prvega in zadnjega, deli[1:-1]) in letnici rojstva in smrti.

zberi_podatke(crke)

Rešitev

Tale je precej rutinska: gremo čez podane črke ali, če je seznam črk prazen, čez vse črke. Preberemo ustrezni html, z župo poiščemo elemente h2 in če vsebujejo avtorje (torej, če razberi_avtorja ne vrne None, v slovar dodamo, kar je pripravila razberi_avtorja.

def zberi_podatke(crke):
    avtorji = defaultdict(list)
    for crka in crke or string.ascii_lowercase:
        html = open(f"authors/{crka}.html").read()
        soup = BeautifulSoup(html, "html.parser")
        for h2 in soup.find_all("h2"):
            avtor = "".join(h2.strings)
            podatki = razberi_avtorja(avtor)
            if podatki is not None:
                avtorji[podatki[0]].append(podatki)
    return avtorji

Izraz crke or string.ascii_lowercase bo imel vrednost crke, če je le-ta resnična (torej: če gre za neprazen niz), sicer pa bo imel vrednost ascii_lowercase. Brez tega trika bi bila funkcija pač vrstico ali dve daljša.

Uporabili smo defaultdict(list), da se nam ni potrebno ukvarjati s tem, ali določen ključ (priimek) že obstaja. Če ga še ni, se bo pojavil "sam od sebe", pripadajoča vrednost pa bo slovar.

(nadaljevanje)

Rešitev

Dodati moramo torej

if not os.path.exists("authors.json"):
    open("authors.json", "w").write(json.dumps(zberi_podatke("")))

Ocena 8

Pazi: Ta in nadaljnje funkcije predpostavljajo, da bereš podatke iz jsona. Pri tem se tere spremenijo v sezname, zato bodo testi pričakovali, četverko s podatki o avtorjih kot sezname in ne terke. Pretvarjanje bi bilo možno, vendar nima smisla, da si zapletamo življenje.

Rešitev

Tule si je potrebno malo risati ali pa vsaj mahati po zraku. En razmislek je tak: intervala A in B se sekata, če je začetek A znotraj B ali pa začetek B znotraj A. Se pravi, pisatelj je živel v določenem obdobju, če je bil rojen znotraj njega ali pa se je obdobje začelo, ko je bil pisatelj živ. Razmislite, da to dejansko pokrije situacije, ko je pisatelj rojen prej in živel v obdobje, ali pa rojen med in živel po njem, ali pa je bilo znotraj obdobja celo njegovo življenje ali pa je bilo celo obdobje znotraj njegovega življenja. Če bi sestavili pogoj tako, da bi moral biti pisatelj znotraj obdobja bodisi rojen bodisi umrjen, potem France Prešeren (1800-1849) ne bi živel v letih 1820-1830.

Poleg tega se moramo poigrati še s primeri, ko ne poznamo bodisi letnice rojstva bodisi letnice smrti (vsaj ena pa je vedno znana).

Ena od možnih rešitev je torej

def v_obdobju(rojen, umrl, zacetek, konec):
    if rojen is None:
        return zacetek <= umrl <= konec
    if umrl is None:
        return zacetek <= rojen <= konec
    return zacetek <= rojen <= konec or rojen <= zacetek <= umrl

avtorji_v_obdobju(zacetek, konec)

Rešitev

Tale je precej rutinska: zanka prek vseh vrednosti v slovarju, ki ga naložimo iz authors.json. Te vrednosti so seznamu, torej naredimo še zanko prek seznama. Tako dobimo četvorke (priimek, imena, rojstvo, smrt). Če je pisatelj živel v podanem odbobju, ga dodamo. Na koncu vrnemo urejen seznam.

def avtorji_v_obdobju(zacetek, konec):
    avtorji = json.loads(open("authors.json").read())
    v_obd = []
    for avtorjii in avtorji.values():
        for avtor in avtorjii:
            rojen, umrl = avtor[2:]
            if v_obdobju(rojen, umrl, zacetek, konec):
                v_obd.append(avtor)
    return sorted(v_obd)

Takole pa gre krajše.

def avtorji_v_obdobju(zacetek, konec):
    avtorji = json.loads(open("authors.json").read())
    return sorted(avtor
                  for avtorjii in avtorji.values()
                  for avtor in avtorjii
                  if v_obdobju(*avtor[2:], zacetek, konec))

razpon()

Rešitev

Ta funkcija je bila malo za oddih.

def razpon():
    avtorji = json.loads(open("authors.json").read())
    najm = najv = 0
    for avtorjii in avtorji.values():
        for _, _, rojen, umrl in avtorjii:
            if rojen is not None and rojen < najm:
                najm = rojen
            if umrl is not None and umrl > najv:
                najv = umrl
    return najm, najv

Ali, krajše:

def razpon():
    avtorji = json.loads(open("authors.json").read())
    return (min(rojen for avt in avtorji.values() for *_, rojen, _ in avt if rojen is not None),
            max(umrl for avt in avtorji.values() for *_, _, umrl in avt if umrl is not None))

pokritost(zacetek, konec)

Rešitev

Tudi tole ni znanost.

def pokritost(zacetek, konec):
    avtorjev = [0] * (konec - zacetek + 1)
    avtorji = json.loads(open("authors.json").read())
    for avt in avtorji.values():
        for _, _, rojen, umrl in avt:
            if rojen is not None and umrl is not None:
                for x in range(max(rojen, zacetek), min(umrl, konec) + 1):
                    avtorjev[x - zacetek] += 1
    return avtorjev

V začetku si pripravimo seznam ničel, ki je dolg toliko, kolikor je let med zacetek in konec (vključno s koncem, zato + 1). Potem gremo spet lepo prek avtorjev in če je za nekoga znano kdaj je rojen in kdaj je umrl, povečamo ustrezne letnice za 1. Da ne pademo ven iz intervala, ki nas zanima, začnemo pri max(rojen, zacetek): tako bomo, če je bil rojen pred začetkom intervala, začeli šteti šele na začetku intervala. Podobno je s koncem.

Nikjer nismo preverili, ali je avtor v resnici živel v tem obdobju. Ni treba. Če je bil rojen, recimo, po njem, bo max(rojen, zacetek) enak rojen in to bo več kot min(umrl, konec). Če je "spodnja" meja višja od "zgornje", range pač ne naredi ničesar in zanka se ne izvede.

Pokritost se da izračunati tudi hitreje: namesto, da bi preštevali, tako kot zgoraj, si v neko začasno tabelo beležimo le, koliko avtorjev je bilo v nekem letu rojenih (+ 1) in koliko jih je umrlo (- 1). Z drugimi besedami, izvemo, za koliko se je v določenem letu spremenilo število avtorjev. Potem je potrebno to le sešteti; k temu prištejemo število avtorjev, ki so bili živi v začetku.

def pokritost(zacetek, konec):
    spremembe = defaultdict(int)
    avtorji = json.loads(open("authors.json").read())
    for avt in avtorji.values():
        for *_, rojen, umrl in avt:
            if rojen is not None and umrl is not None:
                spremembe[rojen] += 1
                spremembe[umrl + 1] -= 1
    avtorjev = [sum(spremembe[x] for x in range(min(spremembe), zacetek + 1))]
    for x in range(zacetek + 1, konec + 1):
        avtorjev.append(avtorjev[-1] + spremembe[x])
    return avtorjev

Ocena 9

V tej nalogi se bomo ukvarjali z deli avtorjev. Upoštevajte le pisne knjige, ne avdio zapisov - v spodnjem primeru torej le prvo delo, ne drugega.

<li class="pgdbetext"><a href="/ebooks/16527">1001 задача для умственного счета</a> (Russian) (as Author)</li>
<li class="pgdbaudio"><a href="/ebooks/19681">Детство</a> (Russian) (as Author)</li>

razberi_delo(s)

Rešitev

Tale funkcija je bila resno zoprna zaradi kupa izjem - na koncu koncev pa je bila čisto kratka.

def razberi_delo(s):
    mo = re.match(r"(.*) \(([\w\- ]+)\) \(as Author\)$", s)
    if not mo or "\n" in s:
        return None
    return mo.groups()

dela()

Rešitev

Z juho iščemo vse li, ki predsatvljajo dela (pgdbetext). Z li.string združimo vse, znotraj tega elementa in pokličemo razberi_delo. Če ta ni mnenja, da gre za delo, s continue nadaljujemo z naslednjim korakom zanke.

Sicer poiščemo prejšnji h2 in iz njega razberemo avtorja. Če smo pri tem uspešni, dodamo delo.

def dela():
    vsa_dela = []
    for crka in string.ascii_lowercase:
        html = open(f"authors/{crka}.html").read()
        soup = BeautifulSoup(html, "html.parser")
        for li in soup.find_all("li", class_="pgdbetext"):
            delo = razberi_delo("".join(li.strings))
            if delo is None:
                continue

            avtor = li.parent.find_previous_sibling("h2")
            avtor = razberi_avtorja("".join(avtor.strings))
            if avtor is None:
                continue
            vsa_dela.append(delo + avtor)
    return vsa_dela

(nadaljevanje)

Rešitev

if not os.path.exists("works.json"):
    open("works.json", "w").write(json.dumps(dela()))

dela_po_jezikih()

Rešitev

Tole je bilo malo za počitek. :)

def dela_po_jezikih():
    dela = json.loads(open("works.json").read())
    jeziki = defaultdict(int)
    for _, jezik, *_ in dela:
        jeziki[jezik] += 1
    return jeziki

Zanimiva je zanka for: _ pobere prvi element (ki na ne zanima), in *_ pobere vse elemente od tretjega naprej (ki nas prav tako ne zanimajo).

Ocena 10

Čestitam, prišli ste do nalog z oceno 10. Za vzpodbudo jih ni veliko in niso težke. Z malo spretnosti bo vsaka vsebovala le eno vrstico in potem (mogoče malo daljši, a ne zapleten) return.

preveri_delo(delo, avtor=None, naslov=None, jezik=None, leto=None)

Rešitev

Nalogi za oceno 10 sta bili res preprosti. Njun namen je bil predvsem, da si pokažemo, kako koristne stvari smo zmožni narediti.

def preveri_delo(delo, avtor=None, naslov=None, jezik=None, leto=None):
    pnaslov, pjezik, ppriimek, pimena, projen, pumrl = delo
    return ((avtor is None or avtor in " ".join(pimena) + " " + ppriimek)
            and (naslov is None or naslov in pnaslov)
            and (jezik is None or jezik == pjezik)
            and (leto is None or projen is not None and pumrl is not None and projen <= leto <= pumrl))

poisci(avtor=None, naslov=None, jezik=None, leto=None)

Rešitev

Tole bomo naredili z izpeljanim seznamom. Karkoli drugega bi bil greh, posebej za oceno 10.

def poisci(avtor=None, naslov=None, jezik=None, leto=None):
    dela = json.loads(open("works.json").read())
    return [opis for opis in dela if preveri_delo(opis, avtor, naslov, jezik, leto)]