Skip to content

Instantly share code, notes, and snippets.

@typebrook
Last active July 30, 2022 23:58
Show Gist options
  • Save typebrook/2fc690e826d1af510196d7605d4d5045 to your computer and use it in GitHub Desktop.
Save typebrook/2fc690e826d1af510196d7605d4d5045 to your computer and use it in GitHub Desktop.
Update OSM villages with wikidata #osm #wikidata #village #script
---
tags: wikidata, script, import
---
# 新增Wikidata欄位
Wikidata中不少項目指涉的物件實體,和OSM中的物件是相同的。
當我們想在OSM新增[`wikidata`](https://wiki.openstreetmap.org/wiki/Key:wikidata)的tag,而對應的`wikidata`項目中又沒有座標([P625](https://www.wikidata.org/wiki/Property:P625))或OSM-ID([P402](https://www.wikidata.org/wiki/Property:P402))時,則可以透過有關聯的標識符(identifier)來尋找對應的項目。
以下以台灣村里單位的行政代號為例,利用Shell指令來更新`wikidata`識別碼至OSM資料中:
## 台灣村里界標記
### OSM
台灣OSM地圖的村里界目前以`relation`型式存在,標記為:
```
admin_level=9
type=boundary
boundary=administrative
nat_ref=
```
其中,`nat_ref`即為「行政代碼」(戶役政資訊系統資料代碼)。
### Wikidata
台灣「村里單位」在Wikidata中,相關項目為[`Q7930614`](https://www.wikidata.org/wiki/Q7930614)。故一項目若為台灣的村里,且假設其識別碼為`QXXXXXX`,可表示為:
```bash
# QXXXXXX 屬於 台灣的村里
QXXXXXX P31 Q7930614
```
而「戶役政資訊系統資料代碼」在Wikidata中,相關屬性為[`P5020`](https://www.wikidata.org/wiki/Property:P5020),故可表示為:
```bash
# QXXXXXX 的行政代碼為 [CODE]
QXXXXXX P5020 [CODE]
```
`[CODE]`就是實際上的行政代碼。
## 使用指令自動匯入
指令使用`Makefile`撰寫,方便閱讀每一步驟以及除錯。
可將每一步驟分別輸入,例如:
```bash
make wd_villages.list # 取得wikidata中的相關資料
make villages.osm # 取得OSM中的相關資料
make matched.list # 取得比對清單
```
也可直接自動匯入
```bash
make changeset # 完成所有必需步驟,送出Changeset
```
Makefile撰寫於gist上,可使用以下指令取得
```bash
curl -O https://gist.githubusercontent.com/typebrook/2fc690e826d1af510196d7605d4d5045/raw/291531e947a444b4df5de15fdd182cf08edee7cc/Makefile
```
以下為Makefile內容:
```bash=
clean:
rm *.list *.osm *.osc
# P31=屬於 Q7930614=中華民國村里 P5020=中華民國戶政資料代碼
define quest
SELECT DISTINCT ?village ?ref
WHERE {
?village wdt:P31 wd:Q7930614.
?village wdt:P5020 ?ref.
}
ORDER BY ?ref
endef
export quest
# 取得有戶政代碼的Wikidata村里清單
# 第一欄為Q identifier, 第二欄為戶政資料代碼, 以空白分格
wd_villages.list:
curl -G 'https://query.wikidata.org/sparql' \
--header "Accept: text/csv" \
--data-urlencode query="$$quest" | \
sed -Ee '1d; s#.+/##; s/,/ /; s/\r$$//' >$@
OVERPASS_API := https://overpass.nchc.org.tw/api/interpreter
TAIWAN_BBOX := 20.72799,118.1036,26.60305,122.9312
# 使用NCHC Overpass Server
# 取得臺灣內, 有戶政代碼, 但沒有wikidata tag的"村里資料"(OSM格式)
villages.osm:
echo "[out:xml]; relation[admin_level=9][nat_ref][!wikidata]($(TAIWAN_BBOX));out meta;" | \
curl -d @- -X POST $(OVERPASS_API) >$@
# 簡化"村里資料"為"OSM村里清單", 去除外層標籤, 每一筆資料縮為一行
villages_oneline.osm: villages.osm
xq -x --xml-root=relation '.osm.relation[] | {"@id": .["@id"], "@version": .["@version"], tag: .tag, member: .member}' $< | \
tr -d '\n' | \
sed -E 's/(<\/relation>)/\1\n/g' >$@
# 依"OSM村里清單",取得"戶政代碼清單"(一行一個)
osm_nat_ref.list: villages.osm
xq -r '.osm.relation[] | .tag[] | select(.["@k"]=="nat_ref") | .["@v"]' $< >$@
# 依"戶政代碼清單", 取得相對應的Q identifier
matched.list: wd_villages.list osm_nat_ref.list
awk 'NR==FNR {a[$$2]=$$1; next} {print a[$$1]}' $^ >$@
# 將對應的Q identifier加入"OSM村里清單"中
# 若無對應的Q identifier, 則刪去該村里relation
# 將結果包裏成osmChange file (.osc file)
.ONESHELL:
final.osc: villages_oneline.osm matched.list
paste $^ | \
sed -E '/<\/relation>\t$$/ d; s/(<\/relation>)\t(.+)$$/<tag k="wikidata" v="\2"><\/tag>\1/' | \
sed -r '1 i <osmChange version="0.6" generator="bash script">
1 i <modify>
$$ a </modify>
$$ a </osmChange>' >$@
# 新建Changeset, 上傳osmChange file, 關閉該Changeset
changeset: final.osc
curl -fsS https://raw.githubusercontent.com/typebrook/settings/dev/tools/osm/osm.api.changeset.commit | \
bash /dev/stdin $<
```
### 步驟
1. (L4-L21) **取得有戶政代碼的Wikidata村里清單:**
- 使用`SPARQL`語法查詢清單。
- 要注意返回的資料格式是逗號分隔(`CSV`),而且用`CRLF`斷行,故使用`sed`手動處理成下列格式:
```bash
# [wikidata識別碼] [行政代碼]
Q64451271 09007010001
Q64451385 09007010002
Q21449461 09007010003
```
得到檔案`wd_villages.list`
1. (L26-L30) **取得臺灣村里資料:**
- 使用NCHC Overpass API
- 使用`Overpass QL`語法查詢
- 資料為`OSM`格式
- 查詢有戶政代碼, 但沒有`wikidata` tag的村里
- 資料格式如下:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6"...>
<meta osm_base="2020-08-07T13:33:03Z"/>
<relation id="3339719" version="12"...>
<member type="way" ref="9213418" role="outer"/>
<tag k="admin_level" v="9"/>
<tag k="boundary" v="administrative"/>
<tag k="name" v="大坪村"/>
<tag k="nat_ref" v="09007030005"/>
<tag k="type" v="boundary"/>
<relation>
```
得到檔案`villages.osm`
1. (L32-L36) **條列村里資料:**
為了編輯方便,加工前一步驟結果,將每項村里資料表示為一行,格式如下:
```xml
<relation id="3339719" version="12"> <tag k="admin_level" v="9"></tag> <tag k="boundary" v="administrative"></tag> <tag k="is_in:city" v="連江縣"></tag> <tag k="is_in:country" v="TW"></tag> <tag k="is_in:county" v="連江縣"></tag> <tag k="is_in:district" v="莒光鄉"></tag> <tag k="is_in:town" v="莒光鄉"></tag> <tag k="name" v="大坪村"></tag> <tag k="name:en" v="Daping Village"></tag> <tag k="name:zh" v="大坪村"></tag> <tag k="nat_ref" v="09007030005"></tag> <tag k="type" v="boundary"></tag> <member type="way" ref="9213418" role="outer"></member> <member type="way" ref="369463474" role="outer"></member> <member type="way" ref="9209434" role="outer"></member></relation>
<relation id="3713913" version="6"> <tag k="admin_level" v="9"></tag> <tag k="boundary" v="administrative"></tag> <tag k="is_in" v="臺南市中西區"></tag> <tag k="is_in:county" v="臺南市"></tag> <tag k="is_in:town" v="中西區"></tag> <tag k="name" v="兌悅里"></tag> <tag k="name:en" v="Duiyue Village"></tag> <tag k="name:zh" v="兌悅里"></tag> <tag k="nat_ref" v="67000370044"></tag> <tag k="type" v="boundary"></tag> <member type="way" ref="279300276" role="outer"></member> <member type="way" ref="282949509" role="outer"></member> <member type="way" ref="279300251" role="outer"></member> <member type="way" ref="279300248" role="outer"></member> <member type="way" ref="563074676" role="outer"></member> <member type="way" ref="279300272" role="outer"></member> <member type="way" ref="279300262" role="outer"></member></relation>
<relation id="3713915" version="5"> <tag k="admin_level" v="9"></tag> <tag k="boundary" v="administrative"></tag> <tag k="is_in" v="臺南市中西區"></tag> <tag k="is_in:county" v="臺南市"></tag> <tag k="is_in:town" v="中西區"></tag> <tag k="name" v="淺草里"></tag> <tag k="name:en" v="Qiancao Village"></tag> <tag k="name:zh" v="淺草里"></tag> <tag k="nat_ref" v="67000370045"></tag> <tag k="type" v="boundary"></tag> <member type="way" ref="279300287" role="outer"></member> <member type="way" ref="279300256" role="outer"></member> <member type="way" ref="279300297" role="outer"></member> <member type="way" ref="279300247" role="outer"></member> <member type="way" ref="279300260" role="outer"></member> <member type="way" ref="279300324" role="outer"></member> <member type="way" ref="279300295" role="outer"></member> <member type="way" ref="279300254" role="outer"></member> <member type="way" ref="279300308" role="outer"></member></relation>
```
得到檔案`villages_oneline.osm`
4. (L38-L40) **取得戶政代碼清單:**
- 處理前一步驟結果,只留下戶政代碼以供比對
- 格式如下:
```
09007030005
67000370044
67000370045
```
得到檔案`osm_nat_ref.list`
5. (L42-L44) **取得相對應的`Wikidata`識別碼:**
- 將前一步驟結果依序和步驟1結果比對,得到`Wikidata`識別碼清單
- 格式如下:
```bash
# 空行表示沒有比對到
Q64451271
Q64451272
Q64451385
```
得到檔案`matched.list`
6. (L46-L56) 取得`osmChange`格式檔案:
- 將上一步驟的比對結果加入到步驟3的清單中,使得每項村里資料都多了一個新的`wikidata`tag
- 刪去沒有比對到的村里
- 將最後結果以`osmChange`的標籤包起來,做成可用於Changeset的`.osc`檔案
- 格式如下:
```xml
<osmChange version="0.6"...>
<modify>
<relation id="9256118" ...> <member .../>... <tag k="wikidata".../>...
<relation id="9256119" ...> <member .../>... <tag k="wikidata".../>...
</modify>
</osmChange>
```
得到檔案`final.osc`
7. (L58-L61) **上傳結果至OSM:**
- 使用既有的Shell Script,將上一步驟的結果進行上傳
- 需手動輸入Changeset 留言以及使用者帳密。
## Reference
- [Makefile命令教程](http://www.ruanyifeng.com/blog/2015/02/make.html)
- [Wikidata SPARQL 語法](https://www.wikidata.org/wiki/Wikidata:SPARQL_tutorial)
- [Overpass Query Language 語法](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL)
all: final.osc
clean:
rm *.list *.osm *.osc
# P31=屬於 Q7930614=中華民國村里 P5020=中華民國戶政資料代碼
define quest
SELECT DISTINCT ?village ?ref
WHERE {
?village wdt:P31 wd:Q7930614.
?village wdt:P5020 ?ref.
}
ORDER BY ?ref
endef
export quest
# 取得有戶政代碼的Wikidata村里清單
# 第一欄為Q identifier, 第二欄為戶政資料代碼, 以空白分格
wd_villages.list:
curl -G 'https://query.wikidata.org/sparql' \
--header "Accept: text/csv" \
--data-urlencode query="$$quest" | \
sed -Ee '1d; s#.+/##; s/,/ /; s/\r$$//' >$@
OVERPASS_API := https://overpass.nchc.org.tw/api/interpreter
TAIWAN_BBOX := 20.72799,118.1036,26.60305,122.9312
# 使用NCHC OverPass Server
# 取得臺灣內, 有戶政代碼, 但沒有wikidata tag的"村里資料"(OSM格式)
villages.osm:
echo "[out:xml]; relation[admin_level=9][nat_ref][!wikidata]($(TAIWAN_BBOX));out meta;" | \
curl -d @- -X POST $(OVERPASS_API) >$@
# 簡化"村里資料"為"OSM村里清單", 去除外層標籤, 每一筆資料縮為一行
villages_oneline.osm: villages.osm
xq -x --xml-root=relation '.osm.relation[] | {"@id": .["@id"], "@version": .["@version"], tag: .tag, member: .member}' $< | \
tr -d '\n' | \
sed -Ee 's/(<\/relation>)/\1\n/g' >$@
# 依"OSM村里清單",取得"戶政代碼清單"(一行一個)
osm_nat_ref.list: villages.osm
xq -r '.osm.relation[] | .tag[] | select(.["@k"]=="nat_ref") | .["@v"]' $< >$@
# 依"戶政代碼清單", 取得相對應的Q identifier
matched.list: wd_villages.list osm_nat_ref.list
awk 'NR==FNR {a[$$2]=$$1; next} {print a[$$1]}' $^ >$@
# 將對應的Q identifier加入"OSM村里清單"中
# 若無對應的Q identifier, 則刪去該村里relation
# 將結果包裏成osmChange file (.osc file)
.ONESHELL:
final.osc: villages_oneline.osm matched.list
paste $^ | \
sed -Ee '/<\/relation>\t$$/ d; s/(<\/relation>)\t(.+)$$/<tag k="wikidata" v="\2"><\/tag>\1/' | \
sed -e '1 i <osmChange version="0.6" generator="bash script">
1 i <modify>
$$ a </modify>
$$ a </osmChange>' >$@
# 新建Changeset, 上傳osmChange file, 關閉該Changeset
changeset: final.osc
curl -fsS https://raw.githubusercontent.com/typebrook/helper/dev/tools/osm/osm.api.changeset.commit | \
bash /dev/stdin $<
Q7930614 village
P5020 nat_ref
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment