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.