В этой заметке я хотел бы дополнить эту статью и рассказать, как можно гибче использовать экстрактор Википедии WikiExtractor, фильтруя статьи по категориям.


Началось все с того, что мне нужны были определения для различных терминов. Термины и их определения, как правило, являются первым предложением на каждой странице Википедии. Пойдя по самому простому пути, я извлек все статьи и регулярками быстро выцепил все, что было нужно. Проблема в том, что объем определений перевалил за 500 Мб, причем, было слишком много лишнего, например, именованные сущности, города, годы и т.д. которые мне не нужны.


Я верно предположил, что у инструмента WikiExtractor (я буду использовать другую версию, ссылка будет ниже) есть какой-то фильтр и это оказался фильтр по категориям. Категории являются тегами для статей, которые имеют иерархическую структуру для организации страниц. Я на радостях выставил категорию "Точные науки", очень наивно полагая, что все статьи, которые относятся к точным наукам будут включены в список, но чуда не случилось — у каждой страницы свой, крошечный, набор категорий и на отдельно взятой странице нет никакой информации о том, как эти категории соотносятся. Значит, если мне нужны страницы по точным наукам, я должен указать все категории, которые являются потомками для "Точных наук".


Ну не беда, сейчас найду сервис, подумал я, который запросто мне отгрузит все категории от заданного начала. К сожалению, я нашел только это, где можно просто посмотреть, как эти категории взаимосвязаны. Попытка в ручную перебрать категории тоже не увенчалась успехом, зато я "обрадовался" тому, что эти категории имеют структуру не дерева, как я думал все это время, а просто направленного графа, с циклами. Причем, сама иерархия очень сильно плывет — скажу наперед, что задав начальную точку "Математика", легко можно дойти до Александра I. В итоге, мне оставалось только восстановить этот граф локально и как-то получить список интересующих меня категорий.


Итак, задача ставится следующим образом: начиная с какой-то вершины, получить список всех категорий, которые связаны с это вершиной, имея возможность как-то ограничивать их.


Работа проводилась на машине с Ubuntu 16.04, но, полагаю, что для 18.04 следующие инструкции не вызовут проблем.


Скачиваем и развертываем данные


Первым делом, нам необходимо скачать все необходимые данные вот отсюда, а именно


  • ruwiki-latest-pages-articles.xml.bz2
  • ruwiki-latest-categorylinks.sql.gz
  • ruwiki-latest-category.sql.gz
  • ruwiki-latest-page.sql.gz

Таблица categorylinks содержит связи между страницей, в смысле Википедии, и ссылкой на категорию вида [[Category:Title]] в любом месте этой страницы, информация. Нас интересуют столбцы cl_from, которая содержит id страницы, и cl_to, которая содержит название категории. Для того, чтобы связать id страницы, нам нужна таблица page (информация) со столбцами page_id и page_title. Но нам не нужно знать взаимосвязь всех страниц, мы хотим только категории. Все категории, или их большинство, как я понял, имеют свою страницу, значит нам нужен перечень всех категорий, чтобы фильтровать названия страниц. Эта информацию содержится в таблице category([информация](category table)) в столбце cat_title. Файл pages-articles.xml содержит текст самих статей.


Для работы с базами данных нам необходим mysql. Установить его можно, выполнив команду


sudo apt-get install mysql-server  mysql-client

После этого, необходимо зайти в mysql и создать там базы данных, для того чтобы импортировать базы данных Википедии.


$ mysql -u username -p
mysql> create database category;
mysql> create database categorylinks;
mysql> create database page;

Создав базы данных, приступим к импорту. Он может занять весьма продолжительное время.


$  mysql -u username -p category < ruwiki-latest-category.sql
$  mysql -u username -p categorylinks < ruwiki-latest-categorylinks.sql
$  mysql -u username -p page < ruwiki-latest-page.sql

Формируем таблицу взаимосвязи категорий и восстанавливаем граф


Теперь нам нужно получить таблицу, в которой будет отражено как между собой связаны категории и для дальнейшей работы выгрузить таблицу в csv. Сделать это можно следующим запросом


mysql> select page_title, cl_to from categorylinks.categorylinks join page.page
on cl_from = page_id  where page_title in (select cat_title from category) INTO outfile '/var/lib/mysql-files/category.csv' FIELDS terminated by ';' enclosed by '"' lines terminated by '\n';

Результат будет выглядеть следующим образом. Не забудьте вручную добавить название столбцов.



Стоит заметить, что слева у нас потомок, а справа — его предки, поэтому восстанавливать граф будем от потомков к предкам. Кроме того, есть еще очень много разных служебных категорий, которые мне лично не нужны, поэтому я их отфилтровал, сократив количество строк с примерно 1,6 миллионов до 1,1. Сделать все это можно при помощи следующего кода.


import pandas as pd
import networkx as nx
from tqdm.auto import tqdm, trange

#Filtering
df = pd.read_csv("category.csv", sep=";", error_bad_lines=False)
df = df.dropna()
df_filtered = df[df.parant.str.contains("[А-Яа-я]+:") != True] 
df_filtered = df_filtered[df_filtered.parant.str.contains("Страницы,_") != True]
df_filtered = df_filtered[df_filtered.parant.str.contains("Статьи_проекта_") != True] 
df_filtered = df_filtered[df_filtered.parant.str.contains("Хорошие_статьи") != True] 
df_filtered = df_filtered[df_filtered.parant.str.contains("Перенаправления,_") != True] 
df_filtered = df_filtered[df_filtered.parant.str.contains("Избранные_списки_") != True]
df_filtered = df_filtered[df_filtered.parant.str.contains("Избранные_статьи_") != True]
df_filtered = df_filtered[df_filtered.parant.str.contains("Списки_проекта") != True] 
df_filtered = df_filtered[df_filtered.parant.str.contains("Добротные_статьи_") != True]
df_filtered = df_filtered[df_filtered.parant.str.contains("Статьи") != True] 

# Graph recovering
G = nx.DiGraph()
c = 0
for i, gr in tqdm(df_filtered.groupby('child')):

    vertex = set()
    edges = []
    for i, r in gr.iterrows():
        G.add_node(r.parant, color="white")
        G.add_node(r.child, color="white")
        G.add_edge(r.parant, r.child)

Работаем с графом и извлекаем фильтрованные статьи


Для того, чтобы пользоваться этим графом и решить поставленную в начале задачу, воспользуемся алгоритмом поиска в глубину с модификациями для обнаружения циклов, для чего мы пометили каждый узел белым цветом, и ограничения глубины поиска.


counter = 0
nodes = []

def dfs(G, node, max_depth):
    global nodes, counter
    G.nodes[node]['color'] = 'gray'
    nodes.append(node)
    counter += 1
    if counter == max_depth:
        counter -= 1
        return
    for v in G.successors(node):
        if G.nodes[v]['color'] == 'white':
            dfs(G, v, max_depth)
        elif G.nodes[v]['color'] == 'gray':
            continue
    counter -= 1

В результате, в листе nodes у нас содержатся все категории начиная от указаной и до желаемой глубины от начала. Ниже представлен пример для начальной точки "Точные науки" с ограничением на глубину в 5 вершин. Всего их получилось около 2500 тысяч. Конечно, там содержатся категории, которые не относятся к точным наукам и, возможно, каких-то категорий, которые должны быть, там не окажутся, но с этим способом лучше не выйдет — либо больше покрытие и больше ненужных категорий, либо наоборот. Однако, это гораздо лучше, чем вручную отбирать эти категории.


Результат нужно сохранить построчно в файл, он нам понадобится для фильтарции.


Подкатегории с вершины Точные науки
Точные_науки
Информатика
CAM
Авторы_учебников_информатики
Архивное_дело
Археографические_комиссии
Археографические_комиссии_Украины
Виленская_археографическая_комиссия
Архивисты
Архивариусы
Архивисты_по_алфавиту
Архивисты_по_векам
Архивисты_по_странам
Архивное_дело_на_Украине
Архивисты_Украины

...

Терминология_телевидения
Терминология_японских_боевых_искусств
Термины_для_знаменитостей
Термины_и_понятия_аниме_и_манги
Технические_термины
Транспортная_терминология
Фантастические_термины_по_их_изобретателям
Филателистические_термины
Философские_термины
Цирковые_термины
Экономические_термины
Японские_исторические_термины
Экономика_знаний
Инкапсуляция_(программирование)

...

Бесконечность
Бесконечные_графы
Единое
Философы_математики
Прокл_Диадох
Функции
Арифметические_функции
Мультипликативные_функции
Большие_числа
Кусочно-линейные_функции
Преобразования
Дискретные_преобразования
Интегральные_преобразования
Преобразования_пространства
Теория_потенциала
Типы_функций
Числа

Для того, чтобы применить эти категории для фильтрации для русского языка, однако, нужно кое-что подправить в исходниках. Я использовал эту версию. Сейчас там что-то новое, возможно, исправления ниже уже не актуальны. В файле WikiExtractor.py нужно заменить "Category" на "Категория" в двух местах. Области с уже исправленным вариантом представлены ниже:



tagRE = re.compile(r'(.*?)<(/?\w+)[^>]*?>(?:([^<]*)(<.*?>)?)?')
#                    1     2               3      4
keyRE = re.compile(r'key="(\d*)"')
catRE = re.compile(r'\[\[Категория:([^\|]+).*\]\].*')  # capture the category name [[Category:Category name|Sortkey]]"

def load_templates(file, output_file=None):
...

if inText:
    page.append(line)
    # extract categories
    if line.lstrip().startswith('[[Категория:'):
        mCat = catRE.search(line)
        if mCat:
            catSet.add(mCat.group(1))

После этого нужно запустить команду


python WikiExtractor.py --filter_category categories --output wiki_filtered ruwiki-latest-pages-articles.xml

где categories — это файл с категориями. Отфильтрованные статьи будут лежать в wiki_filtered.
На этом все. Спасибо за внимание.