DuckDBをただShapefileからの変換ツールとして使う

 先日参加してきた FOSS4G Hokkaido 2025 にて、DuckDBの話を聞いて、表記のことをやってみたくなりました。
実は2月のイベントで DuckDBの機能を伺ったときにも、やってみたいと考えていたのですが、すっかり先延ばしになっていたので、今回改めて刺激を注入していただいて、手を動かしてみた次第。

目的・環境

  • とにかく手元に Shapefileが来た時に、他のフォーマットにさくっと変換したい
  • ゆくゆくは Linux上で作業することになろうが、さしあたって手元の Windows上でできるようになっておきたい
  • 話に聞いたので、(色々方法はあろうが)DuckDBを使ってみたい

DuckDBのインストール

 以下のURLにアクセスして、自分の環境をポチポチ。
duckdb.org

ZIPファイルを取得して展開する方法とwingetコマンドを使用する方法があり、今回は、自分に馴染みが薄かったという理由で(体験として)wingetコマンドを使ってみることにしました。

winget install DuckDB.cli

インストールはこれだけ。お手軽。

DuckDB操作の基本

 コマンドライン(いわゆるDOSプロンプト)から duckdb を起動する。

C:\Users\myname>duckdb
DuckDB v1.4.0 (Andium) b8a06e4a22
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D help
ツキ

 Dというプロンプトが表示され、そこにコマンドを打ち込む。上記は(誤って)helpと入力したところ。
2行目の入力を促すプロンプトが表示される。「次」を要求しているのか、プロンプトが「ツキ」というのが個性的なツールだと感じた。
(たぶんそうではなく、化けている)

duckdbの(データ操作ではない)コマンドは基本的に、先頭に「.」をつける。とりあえず覚えておくのは以下の2つ。

  • .help :使えるコマンド一覧が表示される
  • .exit : 対話型duckdbを終了する。.quitでも可。ctrl-cでも抜けることができる(というか抜けちゃう)のは昔のWindowsmysqlっぽい

spatial extensionのインストール

 GISデータを扱うための拡張をインストールし、使える状態にする。Dはプロンプト。INSTALLは1回だけやれば良い。LOADはduckdbを起動する都度行う必要がある(設定ファイルに書くこともできるが今回はサクサクと先に進む)。

D INSTALL spatial;
D load spatial;

シェープファイルを見る

 シェープファイル(.shp)をFROMに指定して、あたかもテーブルであるかのように読むことができる。Windowsの場合のパスの区切りが \ のままで使えるのに好印象( / に書き換えたり \\ にしたりしないといけないものも多いので)。

C:\>duckdb
(略)
D SELECT * FROM 'D:\work\gis\chibanzu_abiko\ABIKO_AN.SHP';
┌───────┬───────┬───────┬────────┬─────────┬───┬─────────┬─────────┬──────────────────────┬──────────────────────┐
│ ALIVE │ LAYER │ LTYPE │ SUBLAY │ SUBLTYP │ … │   EL    │   STR   │         ATR          │         geom         │
│ int32 │ int32 │ int32 │ int32  │  int32  │   │ varchar │ varchar │       varchar        │       geometry       │
├───────┼───────┼───────┼────────┼─────────┼───┼─────────┼─────────┼──────────────────────┼──────────────────────┤
│     1 │    21 │    11 │      0 │       0 │ … │ 8-16    │ 8-16    │ 65763,8-16,409,004…  │ MULTIPOINT (21033.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-20    │ 8-20    │ 65764,8-20,409,004…  │ MULTIPOINT (21048.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-24    │ 8-24    │ 65765,8-24,409,004…  │ MULTIPOINT (21047.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-6     │ 8-6     │ 65760,8-6,409,0040…  │ MULTIPOINT (21016.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-11    │ 8-11    │ 65761,8-11,409,004…  │ MULTIPOINT (21036.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-18    │ 8-18    │ 65762,8-18,409,004…  │ MULTIPOINT (21032.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 8-8     │ 8-8     │ 65769,8-8,409,0040…  │ MULTIPOINT (21072.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 9-10    │ 9-10    │ 65770,9-10,409,004…  │ MULTIPOINT (21078.…  │
│     1 │    21 │    11 │      0 │       0 │ … │ 9-14    │ 9-14    │ 65771,9-14,409,004…  │ MULTIPOINT (21094.…  │
:(略)

 実はこのシェープファイル、日本語部分の文字がShift_JISで格納されており、バケないで取得するにはもう一工夫が必要。
以下のように ST_Read()関数を用いて、第2引数にオプションとしてShift_JIS(CP932)であることを与えれば良い。

SELECT * FROM ST_Read('D:\work\gis\chibanzu_abiko\ABIKO_POL.SHP',open_options=['ENCODING=CP932']);

フォーマット変換してファイルへの出力

COPY テーブル名 TO '出力ファイル名' WITH (FORMAT GDAL, DRIVER 'ファイル形式');

で指定したファイル形式に出力される。主なものとしては以下のようなファイル形式に変換可能(括弧内は DRIVERに指定する文字列。ParquetのみFORMATに指定可能)。
GeoPackage(GPKG)、1JSONのGeoJSON(GeoJSON)、各行1件のGeoJSON(GeoJSONSeq)、FlatGeobuf(FlatGeobuf)、GeoParquet(FORMAT PARQUET)


 テーブル名としてSELECT文も書くことができるので、実際の使い方はこんな感じ(GeoPackageに変換)。

COPY (SELECT * FROM 'D:\work\gis\chibanzu_abiko\ABIKO_AN_hiki.SHP') TO 'ABIKO_AN_hiki.gpkg' WITH (FORMAT GDAL, DRIVER 'GPKG');

 先ほど解消した文字コード問題に対応した書き方だと、こうなる(こちらはGeoJSONにしてみた)。

COPY (SELECT * FROM ST_Read('D:\work\gis\chibanzu_abiko\ABIKO_OOAZA_POL.SHP',open_options=['ENCODING=CP932'])) TO 'ABIKO_OOAZA_POL.geojson' WITH (FORMAT GDAL, DRIVER 'GeoJSON');"

対話型でなくコマンドとしての実行

 ここまでは、duckdbの対話型モードにSELECT文などを書いて実施してきたが、コマンドから実行できれば、まとめてバッチ処理で変換したりの道が拡がる。
これは、duckdbに -c オプションでSQL文を与えれば良い。前述の通り LOAD spatial を起動のたびに実施する必要があるので、これも指定する。以下のように独立した -c として書いておくと、実施したいSQL文をそのまま2番目の -c に書けば良いので、少しだけ手間が減ったり作業ミスが減らせそう。

duckdb -c "load spatial;" -c "COPY (SELECT * FROM ST_Read('D:\work\gis\chibanzu_abiko\ABIKO_AN.SHP',open_options=['ENCODING=CP932'])) TO 'ABIKO_AN2.gpkg' WITH (FORMAT GDAL, DRIVER 'GPKG');"

参考記事

 以下の記事を参考にさせていただきました。ありがとうございました!
そういえば、2月のFOSS4G Hokkaido 2024の井口さんの講演でduckdbのspatial extensionの事を知り、会場で早速インストールして動かしたら文字化けしたので訊ねると、湯谷さんのブログに解消法がある旨おしえてもらったのでした。「湯谷さん、今日会場に来てますよ」の言葉と共に(笑)。その節はおふたりともありがとうございました。

追記

 この記事を書いたときには、これいいじゃん!!と興奮したのですが、その後、データの詳細を確認して行くにつれ課題も出てきました。どうもこの手順では .projファイルを見ていないようです。 .geojsonに変換したときにも座標系情報が含まれていなかったし、.gpkgへ変換してQGISで確認した際も(そこだけ見れば結構良い感じだったけど)背景地図載せてみたら全然違う場所に表示されていたなど、もう少し工夫が必要、あるいはこの方法では対応できないかも、な感じがします。もうちょっと調査が必要。