SPARQL: Получаем данные из Wikipedia правильно
Jul 12, 2020 20:31 · 2032 words · 10 minute read
Disclaimer: Текстовая расшифровка лишь частично пересекается с видео. На мастер-классе всё пошло не так :)
Здравствуйте, меня зовут Виктор Тыщенко, и сегодня я бы хотел поговорить как можно удобно получать факты из самой большой базы знаний - Wikipedia. Давным давно я увидел игру Puzzle Mercator, которую сделал один из сотрудников Google. Мне очень понравилась идея, так что я решил её скопировать, расширить и доработать. Например, показывать информацию об объектах в отдельном окошке, который бы напоминал инфобокс Wikipedia. Тогда я пошёл на страницу какой-то страны и начал парсить таблицу с этой информацией, а вот для второй парсер уже не подходил. Тогда я начал задумываться, что не так всё просто - структура и состав полей может отличаться. Т.е. таблица генерируется из данных по какому-то шаблону.
Шаблоны могут быть очень разными - например для площади Екатеринбурга, Санкт-Петербурга и Мадрида они разные. Так что парсить в лоб может быть проблематично. Так я пришёл к хранилищу структурированной информации wikidata.org. И дальше самое интересное. Наверно, мало кто заходил туда, но там представлено море фактов по самым разным объектам: картинки, столица, координаты, население и идентификаторы в других справочниках, например, geonames. Все блоки доступны для редактирования. При подготовке доклада я заметил, что население Краснодара меньше 1 000 000 человек и поправил это. Через пару дней правку аппрувнули, так что редактирование - вполне себе нормальная практика. В итоге, я запретил что-то править у себя в базе GeoPuzzle. Если мне что-то надо, то я правлю в wikidata и заново импортирую эту информацию к себе. Так что не бойтесь править, это действительно легко.
Давайте чуть внимательнее присмотримся к странице. Первое, что бросается в глаза - идентификатор. Дальше идут блоки свойств, причём у одного свойства может быть несколько значений. У каждого - ссылка на источник и один или несколько квалификаторов. Они нам понадобятся. У каждого проперти также есть свой идентификатор. Т.е. в общем случае получается вот такой граф:
Q1 - P17 - Q43
|
P34 - Q33
|
Q11
Собственно, на этом и построена вся wikidata. Такая же схема применяется и на многих других базах. И да, под это есть стандарт, который описывает как должны описываться свойства объекта, сами объекты, позволяет визуализировать зависимости и т.п. Но давайте вернёмся к теории чуть позже. Думаю, многим всё же интересно как отсюда извлекать данные. Во-первых, вы можете парсить сами эти страницы, но это трудоёмкий способ. К тому же не позволяет делать запросы на поиск объектов. Так что отправяемся в сервис запросов.
Простые запросы
Для начала давайте попробуем найти все города Краснодар. Вы можете заменить на свой город:
select ?item where {
?item rdfs:label "Краснодар"@ru.
}
Итак, что же здесь происходит. Что начинается со знака ? - переменные, которые нужно найти. Поиск проводится путём сопоставления троек - субъект-предикат-объект. Т.о. мы выбираем все элементы, у которых свойство rdfs:label
равно Краснодар на русском языке. Хм, их несколько. Но в выборке есть и основной, страницу которого мы уже видели. Посмотрим что же это за другие объекты. Стадион и парк - надо бы их отсеять, уточнив запрос. Но давайте ещё немного поиграемся с текущим. Как я уже сказал, любая часть из тройки может быть объектом поиска. Давайте попробуем найти все свойства wd:Q3646, которые равны Краснодар:
select ?label where {
wd:Q3646 ?label "Краснодар"@ru.
}
Что это у нас. Отлично, пока всё предсказуемо. Возвращаемся к предыдущему и делаем уточнение, что нам нужен объект типа city. Т.е. Wiki Data Type с идентификатором P31 должно быть равно Wiki Data объекту Q7930989.
select ?item where {
?item rdfs:label "Краснодар"@ru.
?item wdt:P31 wd:Q7930989.
}
Вот он наш единственный объект, попадающий под эти 2 условия. А как у вас? Остались лишние или удалились нужные? Хорошо, давайте попробуем найти все города, которые есть в wikidata:
select ?item where {
?item wdt:P31 wd:Q7930989.
}
Как-то маловато, здесь что-то не так. Возьмём какой-нибудь Париж. Инстансом каких объектов он является. Во-первых, он столица, проверим:
select ?item where {
?item wdt:P31 wd:Q5119.
}
ok, есть. И он город Q515. А Краснодар, кстати, инстансом этого типа не является. Т.о. связи в Wikidata не всегда полные. Для одного базового типа может быть несколько потомков, и далеко не все релевантные могут быть перечислены. Так что иногда приходится искать по самому общему признаку, тут он - поселение:
select ?item where {
?item wdt:P31 ?type.
?type wdt:P279* wd:Q486972.
}
или можно сократить:
select ?item where {
?item wdt:P31/wdt:P279* wd:Q486972.
}
Так, 616000 уже больше похоже на правду. Итак, что тут происходит. В первой строке мы выбираем все тройки такие что субьект и объект связаны через свойство P31, причём у объект должен наследоваться от human settlement. Чуть позже я остановлюсь на магии rdfs, сейчас продолжим разбираться с возможностями sparql. Например, отобразим их все на карте (в окне результатов слева есть глазик и в нём есть Map). В таком варианте оно скорее всего упадёт, так что ограничим по стране. Я выбрал Финляндию, но вы можете поэкспериментровать с другими. Желательно, не очень большими:
select ?item ?coord where {
?item wdt:P31 ?type.
?type wdt:P279* wd:Q486972.
?item wdt:P625 ?coord.
?item wdt:P17 wd:Q33.
}
Тааааак, мы нашли засланца. Кто это тут у нас? Форт Ино (Q1972371) на территории России, но в странах у него действительно есть Финляндия.
Кстати, есть ещё один вариант записи:
select ?item ?coord where {
?type wdt:P279* wd:Q486972.
?item wdt:P31 ?type; wdt:P625 ?coord; wdt:P17 wd:Q33.
}
Здесь все условия для ?item собраны в одном месте и объединены через логическое И - ;.
Итак, мы узнали как искать объекты по некоторым свойствам. Давайте в качестве практики попробуем найти:
- все страны:
SELECT ?item WHERE {
?item wdt:P31 wd:Q6256.
}
- все страны в Азии:
SELECT ?item WHERE {
?item wdt:P30 wd:Q48.
?item wdt:P31 wd:Q6256.
}
- столицы всех стран в Азии на карте:
SELECT ?item ?capital ?coord WHERE {
?item wdt:P30 wd:Q48.
?item wdt:P31 wd:Q6256.
?item wdt:P36 ?capital.
?capital wdt:P625 ?coord.
}
Аггрегаты
Ok, давайте тепереь немного поиграемся с аггрегатами и посчитаем кол-во городов в каждой стране в Азии:
select ?country ?label (count(?item) as ?count) where {
?item wdt:P31 wd:Q515.
?item wdt:P17 ?country.
?country rdfs:label ?label filter (lang(?label) = 'ru')
?country wdt:P31 wd:Q6256; wdt:P30 wd:Q48.
} group by ?country ?label
Сортировка также поддерживается:
select ?country ?label (count(?item) as ?count) where {
?item wdt:P31 wd:Q515.
?item wdt:P17 ?country.
?country rdfs:label ?label filter (lang(?label) = 'ru')
values ?country { wd:Q79 wd:Q912 }.
} group by ?country ?label order by ?label
Так что в следующий раз если понадобится найти большой объём информации, может это можно взять из wikidata?
Надеюсь, все успели освоиться с синтаксисом, так что немного теории. Как я уже говорил, вся база данных построена на отношениях-тройках субъект-атрибут-объект. И для описания этих троек есть стандарт RDF. Собственно, SPARQL как раз и расшифровывается как SPARQL Protocol and RDF Query Language. RDF - лишь описывает структуру и связь данных, но ничего не знает о формате сериализации. Самый популярный - XML, но есть ещё JSON и N3 (более компактный). Т.е. по сути файл в формате RDF можно интерпретировать лишь как направленный граф, не больше. Чтобы превратить его в базу знаний нужны дополнительные справочники: словари (определяют список терминов), таксономия (строят из терминов иерархию) и онтологии (определяет семантику, отец-дед). Подключаются эти сущности как префиксы XML. Т.е. запрос, который выбирает все столицы правильнее было бы написать запрос как
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX wd: <http://www.wikidata.org/entity/>
select * where {
?item rdfs:label ?label filter( lang(?label) = "en" ).
?item wdt:P31 wd:Q5119.
}
Но wikidata делает это за нас. Но давайте всё же посмотрим что такое rdfs. К сожалению, в Wikidata определён только rdfs:label. Для того же subClassOf есть wdt:P279, который описан в другом неймспейсе. Сделано это для того, чтобы расширить и наложить некоторые констрейнты. Например, для P31 - список квалификаторов.
Работа со значениями
Получение значения населения:
select * where {
wd:Q3646 wdt:P1082 ?order.
}
Получили значение с максимальным ранком. Если ранк не указан, то мы получим все значения:
select * where {
# wd:Q3646 wdt:P1082 ?order.
wd:Q3646 wdt:P190 ?city.
}
Но ok, как же нам получить наиболее актуальную численность населения? Наверно, надо взять все, отсортировать по квалификатору “момент времени” и взять первое. Каждая такая карточка - тоже объект wikidata. Его можно получить с помощью префикса p, с помощью префикса wdt мы получаем только значение.
select * where {
wd:Q3646 p:P1082 ?order.
?order pq:P585 ?date.
?order ps:P1082 ?value.
} order by desc(?date) limit 1
Давайте ещё немного поработаем с объектами свойств. Думаю, многие смотрели сериал Теория большого взрыва. Давайте сделаем оглавление по сериям. Можете, кстати, и свой подставить. Для начала идём на его страницу в wikidata.
PREFIX wds: <http://www.wikidata.org/entity/statement/>
select * where {
?season wdt:P179 wd:Q8539.
?season p:P179 ?obj.
?obj pq:P1545 ?season_no.
?season p:P527 ?episod.
?episod ps:P527 ?episod_obj.
?episod pq:P1545 ?episod_no.
?episod_obj rdfs:label ?label filter(lang(?label) = 'en')
} order by xsd:integer(?season_no) xsd:integer(?episod_no)
Программисты, получившие награды:
select ?name ?label ?year where {
?programmer wdt:P106 wd:Q5482740.
?programmer rdfs:label ?name filter(lang(?name) = 'en').
?programmer p:P166 ?award.
?award ps:P166 ?value.
?award pq:P585 ?when.
?value rdfs:label ?label filter(lang(?label) = 'en').
BIND(year(?when) as ?year).
}
Интеграция между SPARQL-серверами
Помните я говорил, что существует более одного сервиса для извлечения информации. Ещё один популярный dbpedia.org. Давайте составим простейший запрос: https://dbpedia.org/sparql
SELECT DISTINCT ?film_title ?film_abstract
WHERE {
?film_title rdf:type <http://dbpedia.org/ontology/Film> .
?film_title rdfs:comment ?film_abstract
} LIMIT 1000 OFFSET 0
Суть его в том же самом. В SPARQL заложена возможность искать информацию не только внутри одного сервера, но также и выполнять запросы на сторонних.
PREFIX dbo: <http://dbpedia.org/ontology/>
SELECT DISTINCT ?film ?film_title WHERE {
SERVICE <http://dbpedia.org/sparql> {
SELECT DISTINCT ?film ?film_title WHERE {
?film rdf:type dbo:Film;
rdfs:label ?film_title.
}
LIMIT 1000
}
}
Собственный RDF сервер
На этом пока всё. Стоит отетить, что вы можете поднять свой собственный сервер и загрузить туда свои RDF с онтологией для удобной структуризации информации. Это можно развернуть, например, на Jena.
Задачи для самостоятельного решения
Найти все населённые пункты с названием London в США
select ?item ?coord where {
?item rdfs:label "London"@en.
?item wdt:P625 ?coord.
?item wdt:P17 wd:Q30.
?item wdt:P31/wdt:P279* wd:Q486972.
}
Список столиц европейских государств, отсортированных по дате основания
SELECT distinct ?capital ?coord ?establ ?label WHERE {
VALUES ?types { wd:Q3624078 wd:Q7275 wd:Q6256}.
?country wdt:P30 wd:Q46.
?country wdt:P31 ?types.j
?country wdt:P36 ?capital.
?capital wdt:P571 ?establ.
?capital wdt:P625 ?coord.
?capital rdfs:label ?label filter(lang(?label) = 'ru').
filter(year(?establ) > 1000).
} order by DESC(?establ)
Столицы всех стран Азии с левосторонним движением
SELECT ?capital ?coord WHERE {
?item wdt:P1622 wd:Q13196750.
?item wdt:P31 wd:Q6256.
?item wdt:P36 ?capital.
?item wdt:P30 wd:Q48.
?capital wdt:P625 ?coord.
}
Список всех городов России с телефонными кодами
SELECT ?item ?label ?code WHERE {
?item wdt:P17 wd:Q159.
?item wdt:P31 ?type.
?type wdt:P279* wd:Q486972.
?item wdt:P473 ?code.
?item rdfs:label ?label filter (lang(?label) = 'ru').
}
Yаселённые пункты, основанные в тот же год, что и Краснодар
SELECT ?item ?coord ?year WHERE {
# ?type wdt:P279* wd:Q486972.
?item wdt:P571 ?inception FILTER(YEAR(?inception) = 1793).
?item wdt:P31 Q515;
wdt:P625 ?coord.
}
Типы розеток по странам
select ?type ?label (count(?item) as ?count) where {
?item wdt:P31 wd:Q6256.
?item wdt:P2853 ?type.
?type rdfs:label ?label filter(lang(?label) = 'en').
} group by ?label ?type
Подготовить датасет с информацией о населении стран Азии на каждый год
select ?item ?year ?value where {
?item wdt:P31 wd:Q6256.
?item wdt:P30 wd:Q48.
?item p:P1082 ?population.
?population ps:P1082 ?value.
?population pq:P585 ?date.
BIND(YEAR(?date) as ?year).
}
Все фильмы, получившие Оскар за лучший фильм
select ?label ?year where {
?item wdt:P31 wd:Q11424.
?item rdfs:label ?label filter(lang(?label) = 'ru').
?item p:P166 ?award.
?award ps:P166 wd:Q102427.
?award pq:P585 ?date.
bind(year(?date) as ?year).
} order by ?year
Все выданные премии Оскар
select ?item ?label ?kind_label ?year where {
?item wdt:P31 wd:Q11424.
?item rdfs:label ?label filter(lang(?label) = 'ru').
?item p:P166 ?award.
?award ps:P166 ?kind.
?kind wdt:P31/wdt:P279* wd:Q19020.
?kind rdfs:label ?kind_label filter(lang(?kind_label) = 'ru').
OPTIONAL {
?award pq:P585 ?date.
bind(year(?date) as ?year).
}
} order by ?year
Программисты, работавшие в Google
select ?programmer ?name ?start ?end where {
# ?programmer wdt:P108 wd:Q95.
?programmer rdfs:label ?name filter(lang(?name) = 'en').
?programmer p:P108 ?work
?work ps:P108 wd:Q95.
?work pq:P580 ?start.
OPTIONAL{?work pq:P582 ?end.}
}
Ладно, что это мы всё про города да страны. Попробуйте узнать кого в википедии больше - Александров, Олегов или может Артёмов
select ?label (count(?item) as ?count) where {
?item wdt:P103 wd:Q7737.
?item wdt:P31 wd:Q5.
?item wdt:P735 ?name.
?name rdfs:label ?label filter (lang(?label) = 'ru')
values ?name { wd:Q18523982 wd:Q713322 wd:Q17501806 }.
} group by ?label order by ?label
В каких учебных заведениях учились известные программисты
select ?programmer ?name ?university ?end ?grade where {
?programmer wdt:P106 wd:Q5482740.
?programmer rdfs:label ?name filter(lang(?name) = 'en').
?programmer p:P69 ?education.
?education ps:P69/rdfs:label ?university filter(lang(?university) = 'en').
OPTIONAL{?education pq:P582 ?end.}
OPTIONAL{?education pq:P512/rdfs:label ?grade filter(lang(?grade) = 'en').}
}