В этой заметке я хотел бы дополнить эту статью и рассказать, как можно гибче использовать экстрактор Википедии 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 -pmysql> 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.pageon 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 pdimport networkx as nxfrom tqdm.auto import tqdm, trange#Filteringdf = 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 recoveringG = nx.DiGraph()c = 0for 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 = 0nodes = []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 4keyRE = 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.
На этом все. Спасибо за внимание.