維基百科有一個姐妹項目,叫做"維基數據"(Wikidata)。你可以從維基百科左側邊欄點進去。

"維基數據"將維基百科的所有數據,整理成一個可以機器處理的數據庫,方便查詢。比如,山西省人口最多的地區是哪一個?
這種問題在維基百科查詢,非常費時,必須人工從一個個條目提取信息。但是,維基數據可以只執行一條命令,就返回答案(詳見後文)。因為它提供結構化數據,可以機器查詢。
但是,維基數據不是關係型數據庫,而是 RDF 數據庫;查詢語言不是 SQL,而是 SPARQL。我粗淺地學了一點 RDF 和 SPARQL,本文就是學習筆記,演示如何使用維基數據查詢信息。

一、RDF 的含義
大家都知道,關係型數據庫是目前使用最廣泛的數據庫,將數據抽象成行和列的表格關係。

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

網絡在數學裡面稱為圖(graph),每樣事物就是圖的一個節點,節點之間的關係就是將它們連在一起的那條邊。如果數據庫以圖的方式儲存數據,就稱為圖數據庫。
RDF 就是圖數據庫的一種描述方式,或者說是一種使用協議。它以"三元組"( triple)的方式,描述事物與事物之間的直接關係。
"三元組"是 RDF 的核心概念,指的是兩個事物和它們之間的關係,在語法上呈現為"主語 + 謂語 + 賓語"。
天空是藍色的。
上面這句話,就是一個 RDF 三元組。"天空"(主語)和"藍色"(賓語)是兩種事物,它們通過顏色關係(謂語)連接在一起。

RDF 要求,謂語(即事物之間的關係)必須有明確定義。大家這樣想,如果謂語是給定的,就可以用主語去查詢賓語,或者用賓語去查詢主語。比如,顏色關係是給定的,那麼就可以向數據庫進行下面的查詢。
查詢一:天空 + 顏色 = ?
查詢二:? + 顏色 = 藍色
任何組織和個人,都可以定義自己的謂語。RDF 要求每套謂語必須有一個明確的 URL,通過 URL 區分不同的謂語。RDF 官方定義了一套常用的謂語,URL 如下。
使用的時候,只要引用這個 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")) }
運行這個查詢,就可以看到程序員名單了。

注意,這裡只返回有中文名的程序員。如果數據庫裡面沒有收入程序員的中文名,這裡就不會返回。
然後,查詢每個程序員的主要成就。
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按鈕,看看官方提供的示例。

六、參考鏈接
- RDF, Wikipedia
- RDF Graph Data Model, Stardog
- Learn SPARQL, Stardog
- SPARQL Nuts & Bolts, Cambridge Semantics
- How to Extract Knowledge from Wikipedia, Data Science Style, Michael Li
(完)












