SPARQL: Получаем данные из Wikipedia правильно

Jul 12, 2020 20:31 · 2033 words · 10 minute read tutorial sparql

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').}
}