#!/usr/bin/env python # -*- coding: utf-8 -*- # # Author: Jayant Jain # Copyright (C) 2016 RaRe Technologies """This script using for extracting plain text out of a raw Wikipedia dump. Input is an xml.bz2 file provided by MediaWiki that looks like wiki--pages-articles.xml.bz2 or wiki-latest-pages-articles.xml.bz2 (e.g. 14 GB of https://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles.xml.bz2). It streams through all the XML articles using multiple cores (#cores - 1, by default), decompressing on the fly and extracting plain text from the articles and their sections. For each extracted article, it prints its title, section names and plain text section contents, in json-line format. How to use ---------- #. Process Wikipedia dump with this script :: python -m gensim.scripts.segment_wiki -i -f enwiki-latest-pages-articles.xml.bz2 -o enwiki-latest.json.gz #. Read output in simple way: .. sourcecode:: pycon >>> from gensim import utils >>> import json >>> >>> # iterate over the plain text data we just created >>> with utils.open('enwiki-latest.json.gz', 'rb') as f: >>> for line in f: >>> # decode each JSON line into a Python dictionary object >>> article = json.loads(line) >>> >>> # each article has a "title", a mapping of interlinks and a list of "section_titles" and >>> # "section_texts". >>> print("Article title: %s" % article['title']) >>> print("Interlinks: %s" + article['interlinks']) >>> for section_title, section_text in zip(article['section_titles'], article['section_texts']): >>> print("Section title: %s" % section_title) >>> print("Section text: %s" % section_text) Notes ----- Processing the entire English Wikipedia dump takes 1.7 hours (about 3 million articles per hour, or 10 MB of XML per second) on an 8 core Intel i7-7700 @3.60GHz. Command line arguments ---------------------- .. program-output:: python -m gensim.scripts.segment_wiki --help :ellipsis: 0, -10 """ import argparse import json import logging import multiprocessing import re import sys from xml.etree import ElementTree from functools import partial from gensim.corpora.wikicorpus import IGNORED_NAMESPACES, WikiCorpus, filter_wiki, find_interlinks, get_namespace, utils import gensim.utils logger = logging.getLogger(__name__) def segment_all_articles(file_path, min_article_character=200, workers=None, include_interlinks=False): """Extract article titles and sections from a MediaWiki bz2 database dump. Parameters ---------- file_path : str Path to MediaWiki dump, typical filename is wiki--pages-articles.xml.bz2 or wiki-latest-pages-articles.xml.bz2. min_article_character : int, optional Minimal number of character for article (except titles and leading gaps). workers: int or None Number of parallel workers, max(1, multiprocessing.cpu_count() - 1) if None. include_interlinks: bool Whether or not interlinks should be included in the output Yields ------ (str, list of (str, str), (Optionally) list of (str, str)) Structure contains (title, [(section_heading, section_content), ...], (Optionally) [(interlink_article, interlink_text), ...]). """ with gensim.utils.open(file_path, 'rb') as xml_fileobj: wiki_sections_corpus = _WikiSectionsCorpus( xml_fileobj, min_article_character=min_article_character, processes=workers, include_interlinks=include_interlinks) wiki_sections_corpus.metadata = True wiki_sections_text = wiki_sections_corpus.get_texts_with_sections() for article in wiki_sections_text: yield article def segment_and_write_all_articles(file_path, output_file, min_article_character=200, workers=None, include_interlinks=False): """Write article title and sections to `output_file` (or stdout, if output_file is None). The output format is one article per line, in json-line format with 4 fields:: 'title' - title of article, 'section_titles' - list of titles of sections, 'section_texts' - list of content from sections, (Optional) 'section_interlinks' - list of interlinks in the article. Parameters ---------- file_path : str Path to MediaWiki dump, typical filename is wiki--pages-articles.xml.bz2 or wiki-latest-pages-articles.xml.bz2. output_file : str or None Path to output file in json-lines format, or None for printing to stdout. min_article_character : int, optional Minimal number of character for article (except titles and leading gaps). workers: int or None Number of parallel workers, max(1, multiprocessing.cpu_count() - 1) if None. include_interlinks: bool Whether or not interlinks should be included in the output """ if output_file is None: outfile = getattr(sys.stdout, 'buffer', sys.stdout) # we want write bytes, so for py3 we used 'buffer' else: outfile = gensim.utils.open(output_file, 'wb') try: article_stream = segment_all_articles(file_path, min_article_character, workers=workers, include_interlinks=include_interlinks) for idx, article in enumerate(article_stream): article_title, article_sections = article[0], article[1] if include_interlinks: interlinks = article[2] output_data = { "title": article_title, "section_titles": [], "section_texts": [], } if include_interlinks: output_data["interlinks"] = interlinks for section_heading, section_content in article_sections: output_data["section_titles"].append(section_heading) output_data["section_texts"].append(section_content) if (idx + 1) % 100000 == 0: logger.info("processed #%d articles (at %r now)", idx + 1, article_title) outfile.write((json.dumps(output_data) + "\n").encode('utf-8')) finally: if output_file is not None: outfile.close() def extract_page_xmls(f): """Extract pages from a MediaWiki database dump. Parameters ---------- f : file File descriptor of MediaWiki dump. Yields ------ str XML strings for page tags. """ elems = (elem for _, elem in ElementTree.iterparse(f, events=("end",))) elem = next(elems) namespace = get_namespace(elem.tag) ns_mapping = {"ns": namespace} page_tag = "{%(ns)s}page" % ns_mapping for elem in elems: if elem.tag == page_tag: yield ElementTree.tostring(elem) # Prune the element tree, as per # http://www.ibm.com/developerworks/xml/library/x-hiperfparse/ # except that we don't need to prune backlinks from the parent # because we don't use LXML. # We do this only for s, since we need to inspect the # ./revision/text element. The pages comprise the bulk of the # file, so in practice we prune away enough. elem.clear() def segment(page_xml, include_interlinks=False): """Parse the content inside a page tag Parameters ---------- page_xml : str Content from page tag. include_interlinks : bool Whether or not interlinks should be parsed. Returns ------- (str, list of (str, str), (Optionally) list of (str, str)) Structure contains (title, [(section_heading, section_content), ...], (Optionally) [(interlink_article, interlink_text), ...]). """ elem = ElementTree.fromstring(page_xml) filter_namespaces = ('0',) namespace = get_namespace(elem.tag) ns_mapping = {"ns": namespace} text_path = "./{%(ns)s}revision/{%(ns)s}text" % ns_mapping title_path = "./{%(ns)s}title" % ns_mapping ns_path = "./{%(ns)s}ns" % ns_mapping lead_section_heading = "Introduction" top_level_heading_regex = r"\n==[^=].*[^=]==\n" top_level_heading_regex_capture = r"\n==([^=].*[^=])==\n" title = elem.find(title_path).text text = elem.find(text_path).text ns = elem.find(ns_path).text if ns not in filter_namespaces: text = None if text is not None: if include_interlinks: interlinks = find_interlinks(text) section_contents = re.split(top_level_heading_regex, text) section_headings = [lead_section_heading] + re.findall(top_level_heading_regex_capture, text) section_headings = [heading.strip() for heading in section_headings] assert len(section_contents) == len(section_headings) else: interlinks = [] section_contents = [] section_headings = [] section_contents = [filter_wiki(section_content) for section_content in section_contents] sections = list(zip(section_headings, section_contents)) if include_interlinks: return title, sections, interlinks else: return title, sections class _WikiSectionsCorpus(WikiCorpus): """Treat a wikipedia articles dump (wiki--pages-articles.xml.bz2 or wiki-latest-pages-articles.xml.bz2) as a (read-only) corpus. The documents are extracted on-the-fly, so that the whole (massive) dump can stay compressed on disk. """ def __init__(self, fileobj, min_article_character=200, processes=None, lemmatize=None, filter_namespaces=('0',), include_interlinks=False): """ Parameters ---------- fileobj : file File descriptor of MediaWiki dump. min_article_character : int, optional Minimal number of character for article (except titles and leading gaps). processes : int, optional Number of processes, max(1, multiprocessing.cpu_count() - 1) if None. filter_namespaces : tuple of int, optional Enumeration of namespaces that will be ignored. include_interlinks: bool Whether or not interlinks should be included in the output """ if lemmatize is not None: raise NotImplementedError( 'The lemmatize parameter is no longer supported since Gensim 4.0.0. ' 'If you need to lemmatize, use e.g. https://github.com/clips/pattern ' 'to preprocess your corpus before submitting it to Gensim.' ) self.fileobj = fileobj self.filter_namespaces = filter_namespaces self.metadata = False if processes is None: processes = max(1, multiprocessing.cpu_count() - 1) self.processes = processes self.min_article_character = min_article_character self.include_interlinks = include_interlinks def get_texts_with_sections(self): """Iterate over the dump, returning titles and text versions of all sections of articles. Notes ----- Only articles of sufficient length are returned (short articles & redirects etc are ignored). Note that this iterates over the **texts**; if you want vectors, just use the standard corpus interface instead of this function: .. sourcecode:: pycon >>> for vec in wiki_corpus: >>> print(vec) Yields ------ (str, list of (str, str), list of (str, str)) Structure contains (title, [(section_heading, section_content), ...], (Optionally)[(interlink_article, interlink_text), ...]). """ skipped_namespace, skipped_length, skipped_redirect = 0, 0, 0 total_articles, total_sections = 0, 0 page_xmls = extract_page_xmls(self.fileobj) pool = multiprocessing.Pool(self.processes) # process the corpus in smaller chunks of docs, because multiprocessing.Pool # is dumb and would load the entire input into RAM at once... for group in utils.chunkize(page_xmls, chunksize=10 * self.processes, maxsize=1): for article in pool.imap(partial(segment, include_interlinks=self.include_interlinks), group): # chunksize=10): partial(merge_names, b='Sons') article_title, sections = article[0], article[1] # article redirects are pruned here if any(article_title.startswith(ignore + ':') for ignore in IGNORED_NAMESPACES): # filter non-articles skipped_namespace += 1 continue if not sections or sections[0][1].lstrip().lower().startswith("#redirect"): # filter redirect skipped_redirect += 1 continue if sum(len(body.strip()) for (_, body) in sections) < self.min_article_character: # filter stubs (incomplete, very short articles) skipped_length += 1 continue total_articles += 1 total_sections += len(sections) if self.include_interlinks: interlinks = article[2] yield (article_title, sections, interlinks) else: yield (article_title, sections) logger.info( "finished processing %i articles with %i sections (skipped %i redirects, %i stubs, %i ignored namespaces)", total_articles, total_sections, skipped_redirect, skipped_length, skipped_namespace) pool.terminate() self.length = total_articles # cache corpus length if __name__ == "__main__": logging.basicConfig(format='%(asctime)s - %(module)s - %(levelname)s - %(message)s', level=logging.INFO) parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=__doc__[:-136]) default_workers = max(1, multiprocessing.cpu_count() - 1) parser.add_argument('-f', '--file', help='Path to MediaWiki database dump (read-only).', required=True) parser.add_argument( '-o', '--output', help='Path to output file (stdout if not specified). If ends in .gz or .bz2, ' 'the output file will be automatically compressed (recommended!).') parser.add_argument( '-w', '--workers', help='Number of parallel workers for multi-core systems. Default: %(default)s.', type=int, default=default_workers ) parser.add_argument( '-m', '--min-article-character', help="Ignore articles with fewer characters than this (article stubs). Default: %(default)s.", type=int, default=200 ) parser.add_argument( '-i', '--include-interlinks', help='Include a mapping for interlinks to other articles in the dump. The mappings format is: ' '"interlinks": [("article_title_1", "interlink_text_1"), ("article_title_2", "interlink_text_2"), ...]', action='store_true' ) args = parser.parse_args() logger.info("running %s", " ".join(sys.argv)) segment_and_write_all_articles( args.file, args.output, min_article_character=args.min_article_character, workers=args.workers, include_interlinks=args.include_interlinks ) logger.info("finished running %s", sys.argv[0])