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()
z 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.