SPARQL (от англ. SPARQL Protocol and RDF Query Language) – язык запросов к данным, представленным по модели RDF.

Официальное описание SPARQL

Неофициальный перевод

SPARQL in 11 minutes

Сервисы, предоставляющие API для доступа к базам знаний

SPARQLWrapper – библиотека-обертка для работы с SPARQL на языке Python (на других языках: RDF-Query для Perl, SPARQL/Grammar для Ruby и ARQ для Java).

Документация для SPARQLWrapper

Далее приведены примеры автоматизации запросов к базе знаний: https://dbpedia.org/sparql

Следующий запрос возвращает ответ в формате JSON: Содержимое переменной results будет иметь вид:

{ 'head':    {'vars': ['label'], 'link': []}, 
  'results': {'distinct': False, 'bindings': 
                                [{'label': {'value': 'Asturias', 'xml:lang': 'en', 'type': 'literal'}}, 
                                 {'label': {'value': 'منطقة أستورياس', 'xml:lang': 'ar', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturien', 'xml:lang': 'de', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturias', 'xml:lang': 'es', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturies', 'xml:lang': 'fr', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturie', 'xml:lang': 'it', 'type': 'literal'}}, 
                                 {'label': {'value': 'アストゥリアス州', 'xml:lang': 'ja', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturië (regio)', 'xml:lang': 'nl', 'type': 'literal'}}, 
                                 {'label': {'value': 'Asturia', 'xml:lang': 'pl', 'type': 'literal'}}, 
                                 {'label': {'value': 'Astúrias', 'xml:lang': 'pt', 'type': 'literal'}}, 
                                 {'label': {'value': 'Астурия', 'xml:lang': 'ru', 'type': 'literal'}}, 
                                 {'label': {'value': '阿斯图里亚斯', 'xml:lang': 'zh', 'type': 'literal'}}],  
  'ordered': True}
}
In [1]:
# Пример с официального сайта: https://rdflib.github.io/sparqlwrapper/

from SPARQLWrapper import SPARQLWrapper, JSON

sparql = SPARQLWrapper("http://dbpedia.org/sparql")
queryString = """
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
    SELECT ?label
    WHERE { <http://dbpedia.org/resource/Asturias> rdfs:label ?label }
"""
sparql.setQuery(queryString)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()

#print(results, "\n")

if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")       
else:
    for result in results["results"]["bindings"]:
        print(result["label"]["value"])    
Asturias
منطقة أستورياس
Asturien
Asturias
Asturies
Asturie
アストゥリアス州
Asturië (regio)
Asturia
Astúrias
Астурия
阿斯图里亚斯

Результат возвращается в формате JSON: Формат ответа, т.е. содержимое переменной results:

{'head': {'vars': ['author_name', 'title', 'pages'], 'link': []}, 
 'results': {'distinct': False, 
  'bindings': [
   {'title': {'value': 'David Copperfield', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Charles Dickens', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '624', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'Fifty Shades of Grey', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'E. L. James', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '514', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'Roma (novel)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Steven Saylor', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '592', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}},
   {'title': {'value': 'The Alexandria Quartet', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Lawrence Durrell', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '884', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'Helter Skelter (book)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Vincent Bugliosi', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '502', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'Inferno (Brown novel)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Dan Brown', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '609', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'Terra Nostra (novel)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Carlos Fuentes', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '783', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'The Dark Is Rising Sequence', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Susan Cooper', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '786', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'The Kindly Ones (Littell novel)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Jonathan Littell', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '902', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}, 
   {'title': {'value': 'The Kindly Ones (Littell novel)', 'xml:lang': 'en', 'type': 'literal'}, 
    'author_name': {'value': 'Jonathan Littell', 'xml:lang': 'en', 'type': 'literal'}, 
    'pages': {'value': '992', 'datatype': 'http://www.w3.org/2001/XMLSchema#positiveInteger', 'type': 'typed-literal'}}], 'ordered': True}}
In [2]:
from SPARQLWrapper import SPARQLWrapper, JSON

sparql = SPARQLWrapper("http://dbpedia.org/sparql")
queryString = """
    PREFIX : <http://dbpedia.org/resource/> 
    PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> 
    PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> 
    PREFIX dbo: <http://dbpedia.org/ontology/> 
    SELECT  ?author_name ?title ?pages     
    WHERE { 
        ?author rdf:type dbo:Writer . 
        ?author rdfs:label ?author_name 
        FILTER (LANG(?author_name)="en"). 
        ?author dbo:notableWork ?work . 
        ?work dbo:numberOfPages ?pages 
        FILTER (?pages > 500) . 
        ?work rdfs:label ?title . 
        FILTER (LANG(?title)="en"). 
    } 
    LIMIT 10
"""
sparql.setQuery(queryString)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()

#print(results)

# проверяем количество записей, которое вернул сервис:
if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")       
else:
    for result in results["results"]["bindings"]:
        print (result["author_name"]["value"], result["title"]["value"], result["pages"]["value"], sep=" : ")
Charles Dickens : David Copperfield : 624
E. L. James : Fifty Shades of Grey : 514
Steven Saylor : Roma (novel) : 592
Lawrence Durrell : The Alexandria Quartet : 884
Vincent Bugliosi : Helter Skelter (book) : 502
Dan Brown : Inferno (Brown novel) : 609
Carlos Fuentes : Terra Nostra (novel) : 783
Susan Cooper : The Dark Is Rising Sequence : 786
Jonathan Littell : The Kindly Ones (Littell novel) : 902
Jonathan Littell : The Kindly Ones (Littell novel) : 992

Следующий скрипт из книги Learning SPARQL отправляет запрос к базе знаний LinkedMDB об актерах, которые хотя бы один раз снимались в фильмах Стивена Спилберга и Стенли Кубрика.

Результат возвращается в формате JSON:

{ 'results': {
            'bindings': 
                    [
                        {'actorName': {'type': 'literal', 'value': 'Wolf Kahler'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Slim Pickens'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Tom Cruise'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Arliss Howard'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Ben Johnson'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Scatman Crothers'}}, 
                        {'actorName': {'type': 'literal', 'value': 'Philip Stone'}}
                    ]
            }, 
  'head': {'vars': ['actorName']}
}
In [3]:
from SPARQLWrapper import SPARQLWrapper, JSON
# SPARQL endpoint
sparql = SPARQLWrapper("http://data.linkedmdb.org/sparql")
queryString = """
PREFIX m: <http://data.linkedmdb.org/resource/movie/>
SELECT DISTINCT ?actorName WHERE {
?dir1 m:director_name "Steven Spielberg" .
?dir2 m:director_name "Stanley Kubrick" .
?dir1film m:director ?dir1 ;
m:actor ?actor .
?dir2film m:director ?dir2 ;
m:actor ?actor .
?actor m:actor_name ?actorName .
}
"""
sparql.setQuery(queryString)
# по умолчанию результат возвращается в XML формате: https://www.w3.org/TR/rdf-sparql-XMLres/
# преобразуем в JSON: https://www.w3.org/TR/rdf-sparql-json-res/
sparql.setReturnFormat(JSON)
# вернется словарь (dict), основанный на JSON
results = sparql.query().convert()

# проверяем количество записей, которое вернул сервис:
if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")       
else:
    for result in results["results"]["bindings"]:
        print (result["actorName"]["value"])
         
Wolf Kahler
Slim Pickens
Tom Cruise
Arliss Howard
Ben Johnson
Scatman Crothers
Philip Stone
In [4]:
from SPARQLWrapper import SPARQLWrapper, JSON

# Имена режиссеров поместили в отдельные переменные
director1 = "Steven Spielberg"
director2 = "Stanley Kubrick"
sparql = SPARQLWrapper("http://data.linkedmdb.org/sparql")
queryString = """
PREFIX m: <http://data.linkedmdb.org/resource/movie/>
SELECT DISTINCT ?actorName WHERE {
?dir1 m:director_name "DIR1-NAME".
?dir2 m:director_name "DIR2-NAME".
?dir1film m:director ?dir1 ;
m:actor ?actor .
?dir2film m:director ?dir2 ;
m:actor ?actor .
?actor m:actor_name ?actorName .
}
"""
queryString = queryString.replace("DIR1-NAME",director1)
queryString = queryString.replace("DIR2-NAME",director2)
sparql.setQuery(queryString)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()

if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")
else:
    for result in results["results"]["bindings"]:
        print (result["actorName"]["value"])
Wolf Kahler
Slim Pickens
Tom Cruise
Arliss Howard
Ben Johnson
Scatman Crothers
Philip Stone

Добавим программе графический интерфейс на основе tkinter. Список виджетов tkinter представлен по ссылке.

Сначала рассмотрим пример простой графической программы:

In [5]:
from tkinter import *

master = Tk()

var = StringVar()
var2 = StringVar()

var.set("one") # initial value

label = Label (master, text = "Укажите режиссера:")
label.pack()

# http://effbot.org/tkinterbook/optionmenu.htm
option = OptionMenu(master, var, "Steven Spielberg", "Stanley Kubrick")
option.pack()

def ok():
    var2.set(var.get())
    
button = Button(master, text="Готово!", command=ok)
button.pack()

label = Label (master, textvariable=var2)
label.pack()

mainloop()

В следующем примере подключаем возможности SPARQLWrapper:

In [6]:
from SPARQLWrapper import SPARQLWrapper, JSON
from tkinter import *

master = Tk()

var = StringVar()
var2 = StringVar()

var.set("Укажите режиссера:") # initial value

option = OptionMenu(master, var, "Steven Spielberg", "Stanley Kubrick")
option.pack()

def ok():    
    sparql = SPARQLWrapper("http://data.linkedmdb.org/sparql")
    queryString = """
    PREFIX m: <http://data.linkedmdb.org/resource/movie/>
    SELECT DISTINCT ?actorName WHERE {
    ?dir1 m:director_name "DIR1-NAME".
    ?dir1film m:director ?dir1 ;
    m:actor ?actor .
    ?actor m:actor_name ?actorName .
    }
    """
    queryString = queryString.replace("DIR1-NAME",var.get())
    sparql.setQuery(queryString)
    sparql.setReturnFormat(JSON)
    results = sparql.query().convert()

    if (len(results["results"]["bindings"]) == 0):
        var2.set ("No results found.")
    else:
        res = ""
        for result in results["results"]["bindings"]:            
            res = res + result["actorName"]["value"] + '\n'                      
        var2.set (res)
    
button = Button(master, text="Готово!", command=ok)
button.pack()

# Получился длинный список актеров, поэтому желательно использовать:
# scrollbar = Scrollbar(master)
label = Label (master, textvariable=var2)
label.pack()

mainloop()

Результат работы программы:

Для осуществления запросов к собственному RDF-документу потребуется настроить локальный SPARQL-сервис. Например, на основе Fuseki. Getting Started With Fuseki

Архитектура Apache Jena

Потребуется версия Java 8. Запуск сервиса из командной строки:

c:\apache-jena-fuseki-2.4.0>fuseki-server --update --mem /ds

После запуска откройте в браузере: localhost:3030

Пример book.ttl, на котором проверялась работа сервиса

Далее представлен исходный текст программы для обращения к локальному SPARQL endpoint:

In [7]:
from SPARQLWrapper import SPARQLWrapper, JSON
# Локальный SPARQL endpoint
sparql = SPARQLWrapper("http://localhost:3030/book-pers/query")
queryString = """
SELECT ?subject ?predicate ?object
WHERE {
  ?subject ?predicate ?object
}
LIMIT 25
"""

sparql.setQuery(queryString)
# по умолчанию результат возвращается в XML формате: https://www.w3.org/TR/rdf-sparql-XMLres/
# преобразуем в JSON: https://www.w3.org/TR/rdf-sparql-json-res/
sparql.setReturnFormat(JSON)
# вернется словарь (dict), основанный на JSON
results = sparql.query().convert()

#print(results)

# проверяем количество записей, которое вернул сервис:
if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")       
else:
    for result in results["results"]["bindings"]:
        print (result["object"]["value"])
         
Harry Potter and the Philosopher's Stone
J.K. Rowling
Harry Potter and the Chamber of Secrets
b0
J.K. Rowling
b1
Harry Potter and the Prisoner Of Azkaban
b0
Harry Potter and the Goblet of Fire
Harry Potter and the Order of the Phoenix
J.K. Rowling
Harry Potter and the Half-Blood Prince
J.K. Rowling
Harry Potter and the Deathly Hallows
J.K. Rowling
Rowling
Joanna

Примечание: онтология для Fuseki загружается в формате RDF/XML ("Сохранить как" в Protege).

Пример автоматизации запроса: "Вы вегетарианец? Да."

In [1]:
from SPARQLWrapper import SPARQLWrapper, JSON
# SPARQL endpoint
sparql = SPARQLWrapper("http://localhost:3030/new-last-pizza/query")
queryString = """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX : <http://www.pizza.com/ontologies/pizza.owl#>

SELECT ?Pizza 
WHERE {
?Pizza rdfs:subClassOf :VegetarianPizza.}

"""
sparql.setQuery(queryString)
# по умолчанию результат возвращается в XML формате: https://www.w3.org/TR/rdf-sparql-XMLres/
# преобразуем в JSON: https://www.w3.org/TR/rdf-sparql-json-res/
sparql.setReturnFormat(JSON)
# вернется словарь (dict), основанный на JSON
results = sparql.query().convert()

#print(results)

# проверяем количество записей, которое вернул сервис:
if (len(results["results"]["bindings"]) == 0):
    print ("No results found.")       
else:
    for result in results["results"]["bindings"]:
        print (result["Pizza"]["value"])
http://www.pizza.com/ontologies/pizza.owl#MargheritaPizza
http://www.pizza.com/ontologies/pizza.owl#MushroomHotPizza
http://www.pizza.com/ontologies/pizza.owl#MushroomPizza
http://www.pizza.com/ontologies/pizza.owl#SpicyVegetablePizza

Пример ЭС с графичесим интерфейсом (на основе таблицы решений):

In [2]:
from SPARQLWrapper import SPARQLWrapper, JSON
from tkinter import *

# возвращает SPARQL-запрос в зависимости от выбора пользователя (a - (не)с мясом, b - (не)острая, c - (не)традиционная)
def queryResult (a, b, c):
    prefix = '''
            PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
            PREFIX owl: <http://www.w3.org/2002/07/owl#>
            PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
            PREFIX : <http://www.pizza.com/ontologies/pizza.owl#>
            '''
    # любая без мяса
    if a == "Да" and b == "Не знаю" and c == "Не знаю":
        queryString = prefix + """
                    SELECT ?Pizza
                    WHERE {
                        ?Pizza rdfs:subClassOf :VegetarianPizza.
                    }
                    """
        return queryString
    # любая с мясом
    elif a == "Нет" and b == "Не знаю" and c == "Не знаю":
        queryString = prefix + """
                    SELECT ?Pizza
                    WHERE {
                        ?Pizza rdfs:subClassOf :NamedPizza
                        MINUS {?Pizza rdfs:subClassOf :VegetarianPizza.}
                    }
                    """
        return queryString
    # любая острая без мяса
    elif a == "Да" and b == "Да" and c == "Не знаю":
        queryString = prefix + """
                    SELECT ?Pizza
                    WHERE {
                        ?Pizza rdfs:subClassOf :VegetarianPizza.
                        ?Pizza rdfs:subClassOf :SpicyPizza.}
                    """
        return queryString
    # Предпочитает пиццу с мясом, неострую и традиционную
    elif a == "Нет" and b == "Нет" and c == "Да":
        queryString = prefix + """
                    SELECT ?Pizza
                    WHERE {
                        ?Pizza rdfs:subClassOf :ExoticPizza.
                        Minus {?Pizza rdfs:subClassOf :VegetarianPizza.}
                        Minus {?Pizza rdfs:subClassOf :SpicyPizza.}
                    }
                    """
        return queryString
    # С мясом, неострая и нетрадиционная
    elif a == "Нет" and b == "Нет" and c == "Нет":
        queryString = prefix + """
                    SELECT ?Pizza
                    WHERE {
                        ?Pizza rdfs:subClassOf :NamedPizza.
                        Minus {?Pizza rdfs:subClassOf :ExoticPizza}.
                        Minus {?Pizza rdfs:subClassOf :VegetarianPizza.}
                        Minus {?Pizza rdfs:subClassOf :SpicyPizza.}
                    }
                    """
        return queryString
    return "Неизвестный запрос"

master = Tk()

var = StringVar()
var1 = StringVar()
var2 = StringVar()
var3 = StringVar()

label = Label(master, text="Для выбора подходящей пиццы необходимо ответить на вопросы:")
label.pack()

var1.set("Не знаю")
var2.set("Не знаю")
var3.set("Не знаю")

label = Label(master, text="Вы вегетарианец?")
label.pack()
option = OptionMenu(master, var1, "Да", "Нет", "Не знаю")
option.pack()

label = Label(master, text="Вы любите острое?")
label.pack()
option = OptionMenu(master, var2, "Да", "Нет", "Не знаю")
option.pack()

label = Label(master, text="Предпочитаете традиционную пиццу?")
label.pack()
option = OptionMenu(master, var3, "Да", "Нет", "Не знаю")
option.pack()


def ok():
    sparql = SPARQLWrapper("http://localhost:3030/new-last-pizza/query")
    queryString = queryResult(var1.get(), var2.get(), var3.get())
    if queryString != "Неизвестный запрос":
        sparql.setQuery(queryString)
        sparql.setReturnFormat(JSON)
        results = sparql.query().convert()

        if (len(results["results"]["bindings"]) == 0):
            var.set("No results found.")
        else:
            res = ""
            for result in results["results"]["bindings"]:
                # split('#')[1] позволяет вывести только имя пиццы (см. п.11.5 эл. учебника)
                res = res + result["Pizza"]["value"].split('#')[1] + '\n'
            var.set(res)
    else:
        var.set("Что-то пошло не так, повторите выбор :-(")


button = Button(master, text="Готово!", command=ok)
button.pack()

label = Label(master, textvariable=var)
label.pack()

mainloop()

Результат работы программы: