Parsování textu z Wikipedie pomocí Pythonu

Dnešní článek se bude zajisté hodit každému, kdo pracuje v oblasti machine learningu a potřebuje zpracovat velké množství textu, které se nachází v současné Wikipedii. Ukáži pár kousků kódu, které vám usnadní začátek při získávání čistého textu z MediaWiki stránek.

Článek původně vyšel na autorově blogu.

Kde vzít data?

Wikipedia publikuje veškerá data v ní obsažená v mnoha formátech, pro detailnější počtení vás odkáži na stránku Wikipedia:Database download. Z ní se lze snadno proklikat na stránku http://dumps.wikimedia.org/enwiki/latest/ (pro českou Wikipedii http://dumps.wikimedia.org/cswiki/latest/).

Odtud je možné již stáhnout dump v XML formátu v mnoha úrovních detailu (nadpisy, texty článků, texty+historie…). Nám se budou hodit soubory ve formátu enwiki-latest-pages-articles*.xml-*.bz2 popř. obdobné pro českou Wikipedii s prefixem cswiki-*. Soubor(y) postahujeme a nyní můžeme přistoupit k jejich zpracování.

Co bude třeba?

Pro zpracování souborů budeme potřebovat především Python a dále pak knihovnu mwlib. Pokud máte ve vaší distribuci Pythonu příkaz pip, pak není nic jednoduššího než:

pip install mwlib (--user)

kde přepínač --user použijte, pokud chcete nainstalovat pouze pro aktuálního uživatele.

Importy

Jako obvykle začneme importy a definicí konstant, které budeme dále potřebovat:

import xml.etree.ElementTree as ET
import bz2

from mwlib.parser import nodes 
from mwlib.refine.compat import parse_txt

MW_NS = "{http://www.mediawiki.org/xml/export-0.9/}"

Parsování XML

Jak jste si již povšimli, tak stažený dump je XML zabalené pomocí bzip2. Chytře využijeme možnosti v Pythonu parsovat pomocí ElementTree otevřený soubor, tudíž si nejprve otevřeme bz2 soubor (v tomto článku budu pracovat s dumpem české Wikipedie cswiki-latest-pages-articles.xml.bz2) a ten následně předhodíme ElementTree parseru. Pozor, pro udržení celého stromu elementů v paměti je potřeba dostatek paměti (u mě řádově 20GB). Ti, kteří nechtějí obětovat tolik paměti jistě použijí jiný přístup pro parsování (viz závěrečná část článku Iterativní parsování XML).

Následně získáme kořenový element a na něm pomocí volání metody find() jednotlivé elementy page odpovídající Wikipedia stránkám. Pozor, celý XML soubor má XML namespace a proto i jména tagů a atributů jsou tímto XML namespace prefixovány. Jméno namespace je uloženo v proměnné MW_NS. Pro ElementTree se pak URI XML namespace uzavře do složených závorek a vloží se před název tagu.

Po získání XML elementu stránky se najdou potomci obsahující titulek a text poslední revize a vše se vrátí yieldem k dalšímu zpracování. Výsledný generátor pak vypadá následovně:

def parse_dump(xml_fn):
    with bz2.BZ2File(xml_fn, 'r') as fr:
        tree = ET.parse(fr)
        root = tree.getroot()

    for page in root.findall('./{0}page'.format(MW_NS)):
        text = page.find('{0}revision/{0}text'.format(MW_NS))
    
        if text is None:
            self.info("Skipped page: %s, no text", id)
            continue
        else:
            text = text.text

        title = page.find('{0}title'.format(MW_NS)).text

        yield title, text

Odstranění MediaWiki značkování

Výše zmíněný generátor nám vrátí název stránky a text její poslední revize, nicméně tento text stále obsahuje MediaWiki značkování, proto použijeme knihovnu mwlib pro odstranění tohoto značkování. Navíc přeskočíme takové značkování, které odkazuje na obrázky a tabulky — chceme přece čistý text. Kromě použití mwlibu pak použijeme i regulární výrazy pro jistý preprocessing textu. Tento preprocessing spočívá v odstranění řídících bloků z MediaWiki stejně jako úprava hypertextových odkazů (jinými slovy odstranění bloků {{...}} a náhrada [[foo]] za [[foo|foo]]):

IGNORE = (nodes.ImageLink, nodes.Table, nodes.CategoryLink)

def get_text(p, buffer=None, depth=0):
    if buffer is None:
        buffer = []
        
    for ch in p.children:
        descend = True
        if isinstance(ch, IGNORE):
            continue
        elif isinstance(ch, nodes.Section):
            for ch in ch.children[1:]:
                get_text(ch, buffer, depth+1)
            descend = False
        elif isinstance(ch, nodes.Text):
            text = ch.asText()
            text = ' '.join(text.splitlines())
            buffer.append(text)

        if descend:
            get_text(ch, buffer, depth+1)
    
    return buffer


def wiki_to_text(raw_text):
    raw_text = re.sub('(?s){{.*?}}', '', raw_text)
    raw_text = re.sub('(?s)\[\[([^|]+?)\]\]', r'[[\1|\1]]', raw_text)

    parsed = parse_txt(raw_text, lang='cs')
    text = get_text(parsed)
    text = ''.join(text)
    return text

Funkce wiki_to_text() nejprve provede preprocessing a následně pomocí funkce parse_txt()mwlib zpracuje výsledný MediaWiki markup s jazykem nastaveným na češtinu. Následně se na výsledný strom objektů zavolá funkce get_text(), která rekurzivně postupuje stromem objektů a nám vrátí seznam řetězců. Přitom ignoruje objekty typů uvedených v tuple IGNORE (obrázky, tabulky, odkazy na kategorie).

Pro korektní zpracování české Wikipedie je nutné pomocí mwlib stáhnout definici českých tagů v rámci MediaWiki markupu (kategorie, obrázky apod.). Na disku najdeme soubor mwlib/siteinfo/fetch_siteinfo.py a zavoláme jej s parametrem cs. Tím se nám vytvoří soubor siteinfo-cs.json obsahující definice českého markupu z Wikipedie a obrázky, tabulky a kategorie budou správně odstraněny. Angličtina (stejně jako spousta dalších jazyků) je již obsažena v základní distribuci mwlibu, čeština bohužel nikoli.

Nyní již můžeme sestavit cvičný kód, který vše slepí dohromady a vytiskne na standardní výstup prvních deset článků:

idx = 0
for title, raw_text in parse_dump('/home/honzas/tmp/cswiki-latest-pages-articles.xml.bz2'):
    print title
    print wiki_to_text(raw_text)
    print
    idx += 1
    if idx >= 10:
        break

Iterativní parsování XML

Na závěr předvedu kousek kódu, který umožní iterativní načítání XML za použití modulu ElementTree, který je nyní již standardní součástí Pythonu.

Proč iterativní parsování?

Třeba proto, že přestože máte hodně paměti, stále je konečná. Například v předchozím článku zmíněná hranice 20GB na XML dump je postačující, pokud komprimovaný dump má velikost kolem stovek MB, nicméně tento dump v anglické Wikipedii má 2,7 GB zabaleného XML. A to už v běžné paměti neuchováme naráz. Proto je potřeba XML parsovat iterativně. Nicméně, aby nebylo třeba úplně měnit kód z předchozího článku, použijeme opět ElementTree a funkci iterparse().

Jak na to

Funkce iterparse() je zajímavý hybrid mezi SAXem a stromově orientovaným ElementTree. Prakticky dokáže reportovat, že začal vytvářet podstrom XML stromu patřící nějakému elementu a že tuto práci skončil. iterparse() vrátí generátor, který postupně vrací páry událost, element. Vrácené elementy je důležité zpracovávat vždy až s událostí end, kdy je podstrom XML dokumentu již kompletně vytvořen (což při startu být nemusí).

XML strom lze přitom během parsování modifikovat, takže již zpracované elementy lze ze stromu odstranit a tím zásadně ušetříme paměť (viz řádek elem.clear(), který vymaže celý podstrom a v tomto případě ponechá pouze prázdný element page).

A nyní již kód:

def parse_dump(xml_fn):
    with bz2.BZ2File(xml_fn, 'r') as fr:
        for event, elem in ET.iterparse(fr):
            if event == 'end' and elem.tag == '{0}page'.format(MW_NS):
                text = elem.find('{0}revision/{0}text'.format(MW_NS))
    
                title = elem.find('{0}title'.format(MW_NS)).text

                yield title, text.text
                elem.clear()

Rozhraní se oproti předchozí verzi parse_dump() nezměnilo, je vidět, že po vytvoření podstromu lze používat i dotazy metodou find(). Dále, kromě události end, lze využít i další jako start, start-ns a end-ns.

Výše uvedený kód využívá určité apriorní znalosti o struktuře výsledného XML, pokud by například v XML byly rekurzivně zanořené elementy page, pak by již nutné bylo pomocí událostí start a end držet cestu (zanoření elementů) pro každý aktuálně zpracovávaný element.

Závěr

Ukázaný kód by měl sloužit ne jako vodítko, jak velké XML soubory (dump z Wikipedie) zpracovávat, ale jako určité počáteční nakopnutí tím správným směrem. Při potřebě nějakého komplexnějšího chování jistě najdete více na webu a fórech.

Samotné texty z Wikipedie použijete například v oblasti umělé inteligence/komputační lingvistiky pro trénování jazykových modelů, transformací pro latent semantic analysis (LSA), slovní statistky, predikce slov a podobně, fantazii se meze nekladou.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Zdroj: https://www.zdrojak.cz/?p=13813