RDF 和 SPARQL 初探:以維基資料為例

阮一峰發表於2020-02-23

維基百科有一個姐妹專案,叫做"維基資料"(Wikidata)。你可以從維基百科左側邊欄點進去。

"維基資料"將維基百科的所有資料,整理成一個可以機器處理的資料庫,方便查詢。比如,山西省人口最多的地區是哪一個?

這種問題在維基百科查詢,非常費時,必須人工從一個個條目提取資訊。但是,維基資料可以只執行一條命令,就返回答案(詳見後文)。因為它提供結構化資料,可以機器查詢。

但是,維基資料不是關係型資料庫,而是 RDF 資料庫;查詢語言不是 SQL,而是 SPARQL。我粗淺地學了一點 RDF 和 SPARQL,本文就是學習筆記,演示如何使用維基資料查詢資訊。

一、RDF 的含義

大家都知道,關係型資料庫是目前使用最廣泛的資料庫,將資料抽象成行和列的表格關係。

但是,現實世界不像表格,更像網路。各種事物通過錯綜複雜的關係,連線在一起,組成一張網。

網路在數學裡面稱為圖(graph),每樣事物就是圖的一個節點,節點之間的關係就是將它們連在一起的那條邊。如果資料庫以圖的方式儲存資料,就稱為圖資料庫。

RDF 就是圖資料庫的一種描述方式,或者說是一種使用協議。它以"三元組"( triple)的方式,描述事物與事物之間的直接關係。

"三元組"是 RDF 的核心概念,指的是兩個事物和它們之間的關係,在語法上呈現為"主語 + 謂語 + 賓語"。

天空是藍色的。

上面這句話,就是一個 RDF 三元組。"天空"(主語)和"藍色"(賓語)是兩種事物,它們通過顏色關係(謂語)連線在一起。

RDF 要求,謂語(即事物之間的關係)必須有明確定義。大家這樣想,如果謂語是給定的,就可以用主語去查詢賓語,或者用賓語去查詢主語。比如,顏色關係是給定的,那麼就可以向資料庫進行下面的查詢。

查詢一:天空 + 顏色 = ?

查詢二:? + 顏色 = 藍色

任何組織和個人,都可以定義自己的謂語。RDF 要求每套謂語必須有一個明確的 URL,通過 URL 區分不同的謂語。RDF 官方定義了一套常用的謂語,URL 如下。

https://www.w3.org/1999/02/22-rdf-syntax-ns

使用的時候,只要引用這個 URL,別人就知道用的是哪一套謂語。

URL 比較冗長,引用不方便。RDF 允許指定一個字首,代表 URL 地址,比如上面那個官方謂語的 URL,通常用字首rdf表示。


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns>

每個 URL 裡面可以包含多種謂語,通過"字首 : 謂語"的形式來區分。比如,官方定義了一個"type"謂語,說明主語的型別,就可以用rdf:type表示。

小明是學生。

上面這句話,寫成 RDF 三元組,就是下面的形式。


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns>

小明 rdf:type 學生.

由於rdf:type是一個常用謂語,RDF 允許把它簡寫成a,因此"小明是學生"又可以表示成小明 a 學生


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns>

小明 a 學生 .

注意,每個 RDF 三元組的結尾是一個英文的句號,用來區分多個三元組。

二、 RDF 的語法示例

下面通過一個例子,演示 RDF 如何定義事物之間的關係。

甲殼蟲是一個樂隊,成員有 John Lennon、Paul McCartney、Ringo Starr 和George Harrison。他們都是藝術家,1963年出版過一張專輯《Please Please Me》,裡面包含《Love Me Do》這首單曲,長度125秒。

上面這段話,是自然語言的文字。我們先畫出網路關係圖。

然後,轉成 RDF 三元組。首先,給出謂語的 URL,及其對應的字首。


PREFIX : <http://foo.com/tutorial/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns>

上面例子中,有兩個 URL,表示使用兩套謂語。其中一套是官方謂語,使用字首rdf表示;另一套是自己定義的,字首為空,表示這是預設的字首。

"甲殼蟲是一個樂隊,成員有 John Lennon、Paul McCartney、Ringo Starr 和George Harrison。"這句話對應的三元組如下。


甲殼蟲 rdf:type Band .
甲殼蟲 :name "甲殼蟲" .
甲殼蟲 :member John_Lennon .
甲殼蟲 :member Paul_McCartney .
甲殼蟲 :member Ringo_Starr .
甲殼蟲 :member George_Harrison .

上面例子中,rdf:type:name:member都是謂語。由於這些三元組的主語相同,RDF 允許將它們合併。


甲殼蟲 a 樂隊 ;
      :name "甲殼蟲" ;
      :member John_Lennon, Paul_McCartney, George_Harrison, Ringo_Starr .

上面的程式碼中,主語相同的三元組採用合併寫法時,每個三元組之間使用分號隔開,最後一個三元組採用句號結尾。

其餘部分對應的 RDF 三元組如下。


John_Lennon      a 藝術家 .
Paul_McCartney   a 藝術家 .
Ringo_Starr      a 藝術家 .
George_Harrison  a 藝術家 .
Please_Please_Me a 專輯 ;
                 :name "Please Please Me" ;
                 :date "1963" ;
                 :artist "甲殼蟲" ;
                 :track Love_Me_Do .
Love_Me_Do       a Song ;
                 :name "Love Me Do" ;
                 :length 125 .

三、SPARQL 查詢語言

SPARQL 是 RDF 資料庫的查詢語言,跟 SQL 的語法很像。它的核心思想是,根據給定的謂語動詞,從三元組提取符合條件的主語或賓語。

SPARQL 查詢的語法如下。


SELECT <variables>
WHERE {
   <graph pattern>
}

上面程式碼中,<variables>是所要提取主語或賓語,<graph pattern>是所要查詢的三元組模式。

比如,查詢資料庫裡面的所有專輯。


SELECT ?album
WHERE {
   ?album rdf:type :Album .
}

上面程式碼中,?album是一個變數,名字可以隨便起,第一個字元必須是問號?。查詢的條件是,?album這個變數是主語,根據rdf:type這個謂語,可以得到:Album這個賓語。這個賓語也有字首,表示這是當前資料庫定義的。

如果返回的是符合條件的所有記錄,變數可以用星號*代替,並且WHERE這個關鍵詞在SELECT查詢裡面可以省略,最後一個三元組的結尾句號也可以省略,所以上面的查詢也可以寫成下面的樣子。


SELECT * { ?album a :Album }

除了專輯名稱,如果還要返回專輯的演唱者,可以增加一個變數?artist


SELECT ?album ?artist
{
   ?album a :Album .
   ?album :artist ?artist .
}

上面程式碼中,?artist這個變數必須是?album(主語)和:artist(謂語)的賓語。

四、維基資料查詢示例:山西省人口最多的地區

下面通過維基資料查詢"山西省人口最多的是哪一個地區",進一步學習 SPARQL 語法。

首先,進入維基資料網站,在頁面頂部的搜尋欄,搜尋"山西"。或者,維基百科的"山西省"頁面,左邊欄也有跳轉到維基資料的連結。

然後,進入山西省的頁面

這時,留意一下這個頁面的 URL。

https://www.wikidata.org/wiki/Q46913

上面 URL 最後結尾的Q46913,就是山西省這個條目在維基資料的編號(即主語),後面要用到。

接著,頁面向下滾動,找到"contains administrative territorial entity"(所包含的行政實體)這個部分,它列出了山西省下轄的各個地區。

點選"contains administrative territorial entity"這個標題,進入它的頁面,也留意一下 URL。

https://www.wikidata.org/wiki/Property:P150

上面 URL 的最後部分P150,就是"所包含的行政實體"這個謂語動詞的編號。

現在,就可以開始查詢了。進入維基資料的線上查詢頁面 query.wikidata.org

在查詢框裡面,輸入下面的 SPARQL 語句。


SELECT ?area
WHERE {
   wd:Q46913  wdt:P150 ?area .
}

上面程式碼要求返回變數?area,該變數必須滿足主語"山西省"(wd:Q46913)和謂語"所包含的行政實體"(wdt:P150)。字首wd表示這是維基資料的條目,而字首wdt表示這是維基資料定義的謂語關係。

點選左側邊欄的三角形執行按鈕,就可以在頁面下方得到查詢的結果。

從上圖可以看到,返回的都是條目的編號。修改一下查詢語句,增加一欄文字標籤。


SELECT 
  ?area
  ?areaLabel
WHERE {
   wd:Q46913  wdt:P150 ?area .
   ?area rdfs:label ?areaLabel .
   FILTER(LANGMATCHES(LANG(?areaLabel), "zh-CN")) 
}

上面程式碼中,增加了一個返回的變數?areaLabel,該變數是前一個變數?area的文字標籤(滿足謂語rdfs:label),同時增加了一個過濾語句FILTER,要求只返回中文標籤。

執行這段查詢,就可以看到每個地區的中文名字了。

接著,再增加一個人口變數?popTotal,返回每個地區的人口總數。


SELECT 
  ?area 
  ?areaLabel 
  ?popTotal
WHERE {
   wd:Q46913  wdt:P150 ?area .
   ?area rdfs:label ?areaLabel .
   FILTER(LANGMATCHES(LANG(?areaLabel), "zh-CN")) 

   ?area wdt:P1082 ?popTotal .
}

執行這段程式碼,就可以看到人口總數了。

然後,增加一個排序子句order by,按照人口的倒序排序。


SELECT 
  ?area 
  ?areaLabel 
  ?popTotal
WHERE {
   wd:Q46913  wdt:P150 ?area .
   ?area rdfs:label ?areaLabel .
   FILTER(LANGMATCHES(LANG(?areaLabel), "zh-CN")) 

   ?area wdt:P1082 ?popTotal .
}
ORDER BY desc(?popTotal)

執行結果如下。

最後,加上一個limit 1子句,只返回第一條資料。


SELECT 
  ?area 
  ?areaLabel 
  ?popTotal
WHERE {
   wd:Q46913  wdt:P150 ?area .
   ?area rdfs:label ?areaLabel .
   FILTER(LANGMATCHES(LANG(?areaLabel), "zh-CN")) 

   ?area wdt:P1082 ?popTotal .
}
ORDER BY desc(?popTotal)
limit 1

這樣就得到了山西省人口最多的地區。

五、維基資料查詢示例:程式設計師名錄

下面再看一個例子,找出維基百科收入的所有程式設計師。


SELECT 
  ?programmer 
  ?programmerLabel
WHERE {
  ?programmer wdt:P106 wd:Q5482740 .
  ?programmer rdfs:label ?programmerLabel .  
  FILTER (LANGMATCHES(LANG(?programmerLabel), "zh-CN"))
}

上面程式碼中,Q5482740 是程式設計師,P106 是職業。

執行這個查詢,就可以看到程式設計師名單了。

注意,這裡只返回有中文名的程式設計師。如果資料庫裡面沒有收入程式設計師的中文名,這裡就不會返回。

然後,查詢每個程式設計師的主要成就。


SELECT 
  ?programmer 
  ?programmerLabel 
  ?notableworkLabel
WHERE {
  ?programmer wdt:P106 wd:Q5482740 .
  ?programmer rdfs:label ?programmerLabel .  
  FILTER (LANGMATCHES(LANG(?programmerLabel), "zh-CN"))

  ?programmer wdt:P800 ?notablework .  
  ?notablework rdfs:label ?notableworkLabel .  
  FILTER(LANGMATCHES(LANG(?notableworkLabel), "zh-CN"))
}

執行結果如下。

有的程式設計師有多項成就,比如,約翰·卡馬克有"毀滅戰士"和"雷神之錘"兩項成就。這時可以用GROUP BY子句將它們合併在一起。


SELECT 
  ?programmer 
  ?programmerLabel 
  (GROUP_CONCAT(?notableworkLabel; separator="; ") AS ?works)
WHERE {
  ?programmer wdt:P106 wd:Q5482740 .
  ?programmer rdfs:label ?programmerLabel .  
  FILTER(LANGMATCHES(LANG(?programmerLabel), "zh-CN"))

  ?programmer wdt:P800 ?notablework .  
  ?notablework rdfs:label ?notableworkLabel .  
  FILTER (LANGMATCHES(LANG(?notableworkLabel), "zh-CN"))
}
GROUP BY ?programmer ?programmerLabel

上面程式碼中,GROUP_CONCAT函式用來把多個?notableworkLabel變數合併成新的一欄works

執行結果如下。

上面圖片中,"毀滅戰士"和"雷神之錘"已經合併成一個單元格了。

接著,為每個人增加一個頭像照片。


SELECT 
  ?programmer
  ?programmerLabel 
  (GROUP_CONCAT(?notableworkLabel; separator="; ") AS ?works) 
  ?image
WHERE {
  ?programmer wdt:P106 wd:Q5482740 .
  ?programmer rdfs:label ?programmerLabel .  
  FILTER(LANGMATCHES ( LANG ( ?programmerLabel ), "zh-CN"))

  ?programmer wdt:P800 ?notablework .  
  ?notablework rdfs:label ?notableworkLabel .  
  FILTER (LANGMATCHES ( LANG ( ?notableworkLabel ), "zh-CN"))

  OPTIONAL {?programmer wdt:P18 ?image}
}
GROUP BY ?programmer ?programmerLabel ?image

上面程式碼中,返回值增加了一個照片變數?image。由於不是每個人都有照片,所以把照片要求放在OPTIONAL條件中,表示這一項是可選的。

得到查詢結果後,把結果的表格檢視(table)切換成影象檢視(image grid)。

這時,照片就可以顯示出來了。

最後,我們想知道他們是哪個地方的人,維基資料提供他們的出生地。


SELECT ?programmer 
  ?programmerLabel 
  (GROUP_CONCAT(?notableworkLabel; separator="; ") AS ?works) 
  ?image
  ?cood
WHERE {
  ?programmer wdt:P106 wd:Q5482740 .
  ?programmer rdfs:label ?programmerLabel .  
  FILTER(LANGMATCHES ( LANG ( ?programmerLabel ), "zh-CN"))

  ?programmer wdt:P800 ?notablework .  
  ?notablework rdfs:label ?notableworkLabel .  
  FILTER (LANGMATCHES ( LANG ( ?notableworkLabel ), "zh-CN"))

  OPTIONAL {?programmer wdt:P18 ?image}

  OPTIONAL {
    ?programmer wdt:P19 ?birthplace .
    ?birthplace wdt:P625 ?cood .
  }
}
GROUP BY ?programmer ?programmerLabel ?image ?cood

上面程式碼中,返回值增加了座標變數cood,先查詢程式設計師的出生地,然後查詢出生地的地理座標。

執行查詢之後,預設的表格檢視就會出現座標。

把檢視切換成地圖(map)。

這時就能看到這些程式設計師在世界地圖上的位置。

這篇教程就到這裡為止,維基資料的查詢方法還有很多,繼續學習可以點選查詢頁頭部的Examples按鈕,看看官方提供的示例。

六、參考連結

(完)

相關文章