Python: Parsování textu z Wikipedie

Dnešní zápisek 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.

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í. UPDATE: Více v příspěvku Python: 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)
    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. Následně se na výsledný strom objektů zavolá funkce get_text(), která rekurzivně postupu 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). 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

Kde použít?

Použití je celá řada, v oblasti umělé inteligence/komputační lingvistiky pak třeba trénování jazykových modelů, transformací pro latent semantic analysis (LSA), slovní statistky, predikce slov a podobně, fantazii se meze nekladou.

Napsat komentář