Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

C++でプレースホルダを利用したsql文作成(sqlite3)

先日C++でsqlite3を用いてDBを操作してみました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com

こちらのブログでは実行するsql文を下記の通りべた書きで記載していました。

    char sql2[30] = "select * from huga;";
    sqlite3_prepare_v2(db, sql2, strlen(sql2), &stmt, NULL);

ですが、sql文を作成するにあたり変数を用いたい場合があるかと思います。
今回は、sql文を生成させるにあたり変数をどのように与えればよいかをまとめたいと思います。



プレースホルダについて

sqlite3で引数を渡す場合、プレースホルダとバインドを利用する必要があります。
プレースホルダを用いずに、

snprintf(sqlstr,30,"select * from huga where name=%s",huga);
ret = sqlite3_exec(db, sqlstr, NULL, NULL, &err);

とした場合、hugaにSQLインジェクションが可能になってしまいます。
プレースホルダを用いることにより与える変数がSQL文と認識・実行されなることがなくなります。
このため、プレースホルダを用い、プレースホルダとして指定された位置にバインドにより変数を与えることが一般的になります。

プレースホルダとは?】
正式な値が入るまで一時的に場所を確保しておく方法のことを指します。
今回の場合、sql文で引数を与えたい箇所を仮置きで値を挿入しておくことを指します。

■条件

今回も前回と同様にsqlite3を用いかつ、テーブルは下記を用いることにします。
f:id:Elsammit:20210529115441p:plain

プレースホルダを用いた実装

では各sql文の実装をまとめていきたいと思います!!
まずはselect文から。
コードはこちら。

int SelectDB(){
    sqlite3 *db = NULL;
    char* err = NULL;
    sqlite3_stmt *stmt;
    int rc;
    
    int ret = sqlite3_open("data.db", &db);
    if(ret != SQLITE_OK){
        printf("ERROR(%d) %s\n",rc,sqlite3_errmsg(db));
        return -1;
    }
    unsigned int id = 5;
    rc = sqlite3_prepare_v2(db,"select * from tableA where id=?", -1, &stmt, 0);
    if(rc != SQLITE_OK){
        printf("ERROR(%d) %s\n",rc, sqlite3_errmsg(db));
        sqlite3_close(db);
        return -1;
    }
    sqlite3_bind_int(stmt, 1, id);
    rc = sqlite3_step(stmt);
    while(rc == SQLITE_ROW){
        printf("%d %s %s %lld %lld %lld \n",
            sqlite3_column_int(stmt,0),
            sqlite3_column_text(stmt,1),
            sqlite3_column_text(stmt,2),
            sqlite3_column_int64(stmt,3),
            sqlite3_column_int64(stmt,4),
            sqlite3_column_int64(stmt,5)
        );
        rc = sqlite3_step(stmt);
    }

    rc = sqlite3_finalize(stmt);
    if(rc != SQLITE_OK){
        printf("ERROR(%d) %s\n",rc,sqlite3_errmsg(db));
    }
    sqlite3_close(db);
    return 0;
}

プレースホルダを用いるにあたり、sql文をこちらのように定義しております。

 rc = sqlite3_prepare_v2(db,"select * from tableA where id=?", -1, &stmt, 0);

引数として与えたい位置を"?"とすることでプレースホルダとすることが出来ます。

こちらのプレースホルダに値を入れる際には、

sqlite3_bind_int(stmt, 1, id);

というようにバインドさせればOKです。

次にinsert文です。

int InsertDB(){
    sqlite3 *db = NULL;
    char* err = NULL;
    sqlite3_stmt *stmt;
    int rc;
    
    int ret = sqlite3_open("data.db", &db);
    if(ret != SQLITE_OK){
        printf("[%s] FILE Open Error \n", __func__);
        return -1;
    }
    unsigned int id = 1;
    rc = sqlite3_prepare_v2(db,"insert into tableA (id,name,type,cost,size,weight) values "
                                "(?,?,?,?,?,?)", -1, &stmt, 0);
    if(rc != SQLITE_OK){
        printf("ERROR(%d) %s\n",rc, sqlite3_errmsg(db));
        sqlite3_close(db);
        return -1;
    }
    sqlite3_bind_int(stmt, 1, id);
    sqlite3_bind_text(stmt, 2, "nameA", -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 3, "typeB", -1, SQLITE_STATIC);
    sqlite3_bind_int64(stmt, 4, "100000");
    sqlite3_bind_int(stmt, 5, "30");
    sqlite3_bind_int(stmt, 6, "15");
    rc = sqlite3_step(stmt);
    if(rc != SQLITE_DONE){
        printf("ERROR(%d) %s\n",rc,sqlite3_errmsg(db));
    }
    
    rc = sqlite3_finalize(stmt);
    if(rc != SQLITE_OK){
        printf("ERROR(%d) %s\n",rc,sqlite3_errmsg(db));
    }
    sqlite3_close(db);
    return 0;
}

プレースホルダは先ほどのselect文と同様に、

    rc = sqlite3_prepare_v2(db,"insert into tableA (id,name,type,cost,size,weight) values "
                                "(?,?,?,?,?,?)", -1, &stmt, 0);

といった形で変数を与えたい位置を"?"という形でプレースホルダを定義すればOKです。

プレースホルダに値を与える処理は、

    sqlite3_bind_int(stmt, 1, id);
    sqlite3_bind_text(stmt, 2, "nameA", -1, SQLITE_STATIC);
    sqlite3_bind_text(stmt, 3, "typeB", -1, SQLITE_STATIC);
    sqlite3_bind_int64(stmt, 4, "100000");
    sqlite3_bind_int(stmt, 5, "30");
    sqlite3_bind_int(stmt, 6, "15");

といった処理となります。
ここで、int、文字列、long型はそれぞれ、

【int】
sqlite3_bind_int()関数

【文字列】
sqlite3_bind_text()関数

【long型】
sqlite3_bind_int64()関数

を使い分けてbind関数を用いる必要があります。

■最後に

今回はプレースホルダを用いたsql文の作成方法をまとめてみました。
あまり意識することが出来ず、使いこなせていなかった処理なので今後はしっかりと使っていきたいと思います!!



緯度・経度情報からMap上に位置をプロットしてみる

先日住所から緯度・経度情報を収集する方法をまとめました。
elsammit-beginnerblg.hatenablog.com

緯度・経度を取得出来ても数値だけだと、どこなのかピンときませんよね。。。
googleMapで緯度・経度を入力すれば地図上にピンが立って分かりやすいですよね!!

今回は取得した緯度・経度情報から自動でピンを立てて、地図上で分かりやすく表示させてみたいと思います!!
※今回も前回と同様にGoogle Mapは利用しない方法で実現させてみたいと思います。


■環境

コードはpython3(Python 3.7.3)を利用します。
また、住所から緯度・経度を取得するためのライブラリは前回と同様に、

https://msearch.gsi.go.jp/address-search/AddressSearch?q=" 

を用います。

■前提条件

今回のメインとなる緯度・経度からMap上にピンを配置する方法として、
folium
を用います。

【foliumtoとは】
leaflet.jsというjavascriptで使用することのできるマップをPythonライブラリ化したものです。
これにより、Pythonで簡易的にマップ上にピンを立てることが出来ます。
ライセンスはMITです。
下記に詳細がまとめられておりますので詳しくはこちらをご覧ください。
 ・documentation:https://python-visualization.github.io/folium/
 ・GitHubhttps://github.com/python-visualization/folium

本ライブラリですが、

pip3 install folium

でインストール可能です。

■foliumを用いたMap上へのプロット

緯度・経度が分かっている場合、foliumを用いることでたった数行でマップ上にプロット可能です。

import folium

map = folium.Map(location=[Latitude Number, Longitude Number], zoom_start=18)
folium.Marker(location=[Latitude Number, Longitude Number], popup=Name).add_to(map)
map.save("result.html")

folium.Map関数により引数として渡した緯度・経度を中心とした地図情報を取得します。
zoom_startは地図を描画する際の倍率になります。
今回は18倍をセットしてみました。

次に、

folium.Marker(location=[Latitude Number, Longitude Number], popup=Name).add_to(map)

により、先ほど取得した地図情報上にピンを立てます。
popupにはピンをクリックした際に表示される情報を指します。

例えば、東京駅(緯度:35.681561、経度:139.767197)の場合はこちらのようなコードになります。

import folium

map = folium.Map(location=[35.681561, 139.767197], zoom_start=18)
folium.Marker(location=[35.681561, 139.767197], popup="東京駅").add_to(map)
map.save("result.html")

こちらを実行するとresult.htmlファイルが生成されるかと思います。
こちらを開くと
f:id:Elsammit:20210723163350p:plain

といった画像が得られるかと思います。

数行で位置をプロット出来るのは便利ですね。

■住所リストからMap上にプロットをしてみる

では前回の住所リストから緯度・経度リストを取得するコードからMap上にプロットするコードはこちら。

# -*- coding: utf-8 -*-

import csv
import sys
import urllib.parse # pip3 install urllib3
import json         # https://note.com/masato1230/n/nba86746179ca
import requests     # pip3 install requests
import time
import os
import sqlite3
import folium

def main():
    args = sys.argv     # 実行時の引数取得
    if len(args) < 2:   # 実行時にcsvファイルを指定していなかった場合はエラーとする
        print("[Error] csvファイルを引数に与えてください")
        return

    if os.path.exists(args[1]) == False:        # 指定したファイルが存在しない場合にはエラーとする
        print("[Error] 指定したファイルがみつかりませんでした")
        return        

    fileName = os.path.splitext(args[1])        # 指定したファイルがcsvファイルでなければエラーとする
    if fileName[1] != ".csv":
        print("拡張子はcsvファイルにしてください")
        return

    i = 0               # ヘッダーとbody切り替え用
    AddressNum = -1     # 住所 or Addressが書かれた番号
    locationNum = -1
    locationName = ""
    MapLists = []       # csvから読み出しかつ緯度・経度を追加する用変数

    # 国土地理院URL
    # APIの使用方法等は下記を参考のこと
    # https://libraries.io/github/gsi-cyberjapan/internal-search
    makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="    
    map = folium.Map(location=[35.681561, 139.767197], zoom_start=8)
    with open(args[1],encoding='utf-8') as f:   # 引数に与えたcsvファイル読み込み
        reader = csv.reader(f)
        for line in reader:                     # 各行読み取り
            buf = []
            if i == 0:                          # ヘッダーの場合
                j = 0                           # 列番号カウント用変数
                for item in line:               # 列読み取り
                    if item == "住所" or item == "Address" or item == "address":
                        AddressNum = j          # 住所を見つけたらその時の番号を変数に格納
                    elif item == "店名" or item == "場所名":
                        locationNum = j
                    j+=1                        # 列番号カウンタをインクリメント
                    buf.append(item)            # 取得した列をbufに格納
                if AddressNum < 0:              # 住所やアドレスがなかったらエラーとして修了
                    print("[Error] 住所もしくはAddressが項目にありません")
                    return

                buf.append("latitude")          # ヘッダーの列にlatitudeを追加
                buf.append("longitude")         # ヘッダーの列にlongitudeを追加
                MapLists.append(buf)            # ヘッダー行をMapListsに格納
            else:                               # body側の各行情報
                for item in line:               # 各行ごとのカラムを取得
                    buf.append(item)            # 取得したカラムを変数に格納
                print(line[AddressNum])
                s_quote = urllib.parse.quote(line[AddressNum])          # 住所の文字列をURLエンコード
                response = response = requests.get(makeUrl + s_quote)   # エンコードした文字列を国土地理院APIの引数として与えてget request
                if response.json() == []:                               # レスポンスされたjsonデータの中身を確認し空だったら
                    print("[Error] 住所がよくわかりませんでした")          # 判定できなかった旨を出力し緯度・経度は空文字を格納
                    buf.append("")                                      
                    buf.append("")                    
                elif len(response.json()) >1:                           # 候補が複数あった場合、判定出来ないためスキップ
                    print("[Error] 住所の絞り込みが出来ず複数候補が出ました \n 住所の絞り込みをおこなってください ")
                    buf.append("")                                      
                    buf.append("") 
                else:                                                   # レスポンスされたjsonデータが空でなかった場合
                    print(response.json()[0]["geometry"]["coordinates"]) 
                    buf.append(response.json()[0]["geometry"]["coordinates"][0])    # 緯度情報をbufに格納
                    buf.append(response.json()[0]["geometry"]["coordinates"][1])    # 経度情報をbufに格納
                    if locationNum < 0:
                        locationName = ""
                    else:
                        locationName = line[locationNum]
                    
                    folium.Marker(location=[response.json()[0]["geometry"]["coordinates"][1], response.json()[0]["geometry"]["coordinates"][0]], popup=locationName).add_to(map)
                    
                MapLists.append(buf)            # 各行ごとの情報をMapListsに格納                                    
                time.sleep(1)                   # 国土地理院APIに負荷を掛けないように
            i+=1
    map.save("result.html")

    with open("output.csv", mode='w',newline='',encoding='shift_jis') as f: # データをcsvファイルへ格納
        writer = csv.writer(f)
        for maplist in MapLists:                # 各行の情報を格納した変数を読み出す
            writer.writerow(maplist)            # 読み出したデータをcsvへ書き込み
    print("Write Finish !!")
            
if __name__ == "__main__":
    main()

細かいところは割愛しますが、、、
前回から追加した点は、ファイル読み出し前に

map = folium.Map(location=[35.681561, 139.767197], zoom_start=8)

でmapを定義し、
map上に取得した緯度・経度のプロットを

folium.Marker(location=[response.json()[0]["geometry"]["coordinates"][1], response.json()[0]["geometry"]["coordinates"][0]], popup=locationName).add_to(map)

にてセット。
最後に、

map.save("result.html")

で保存しております。
住所から緯度・経度を取得するコードはこちらにまとめておりますのでこちらをご確認下さい。
elsammit-beginnerblg.hatenablog.com

例えばこちらのようなcsvを作成し、本コードを実行してみます。

id,店名,住所
1,札幌駅,北海道札幌市北区北6条西4丁目
2,東京駅,東京都千代田区丸の内1丁目
3,蒲郡駅,愛知県蒲郡市元町1

結果、result.htmlを開くとこちらのような画像が得られるかと思います。
f:id:Elsammit:20210723164438p:plain

■最後に

今回は緯度・経度からMap上に位置をプロットしてみました。
緯度・経度さえわかれば簡単にプロットが出来ますね!!
foliumはピンをあってるだけでなく、領域を塗ることが出来たり線が書けたりするみたいです。
まだまだ機能使いこなせていないので、もう少し調べて使ってみたいと思います!!



C++でjsonデータを取り扱う

今回はC++jsonデータを取り扱ってみたいと思います。
簡単なcsvデータ(カンマ区切りのみ)であれば文字列をparseすればよいだけなので簡単なのですが、
jsonデータってどうやればよいのだろう??
と気になったので調べてみました!!

今回は調べて動かした内容の備忘録をまとめたいと思います。



jsonデータを扱うためには?

調べてみたところ、nlohmannより公開されているライブラリを用いるのが手っ取り早いようでした。
ライセンスはMITでした。
github.com

こちらのgithub
「single_include/nlohmann/」
配下の”json.hpp”を作成しているソースコードと同階層に格納すればライブラリが使用できます。
Linuxの場合にはinclude配下に格納してもよいです。

今回はこちらのnlohmann json.hppを用いてjsonデータやファイルを取り扱っていきたいと思います!!

json型の文字列をjsonオブジェクトに変換

ではまずは文字列をjson型のオブジェクトに変換してみます。
コードはこちら。

#include <iostream>
#include "json.hpp"

void main(){
    std::string jsonstr = R"({
        "str":"こんにちは",
        "boolian":false,
        "person":[
            {
                "name":"Taro",
                "age":20
            },
            {
                "name":"Tom",
                "age":30
            }
        ]
    })";
    auto jobj = json::parse(jsonstr);
    std::cout <<"抽出: " << jobj["person"] << endl;
}

実際に文字列をオブジェクトに変換するのはこちらの1行のみ!!

auto jobj = json::parse(jsonstr);

これで変換OK!!とても簡単でした。

jsonオブジェクトから各種要素を取得する場合には、

 jobj["str"] 
 jobj["person"]

といったように指定すればOKです。

またjsonオブジェクトに値を代入することも可能です。
例えばこんな感じに。

cout << jobj["str"] << endl;   // こんにちは
jobj["str"] = "Hello";
cout << jobj["str"] << endl;   // Hello 

jsonオブジェクトを文字列に変換

今度は逆にjsonオブジェクトを文字列に変換してみます。
コードはこちら。

#include <iostream>
#include "json.hpp"

void main(){
    std::string jsonstr = R"({
        "str":"こんにちは",
        "boolian":false,
        "person":[
            {
                "name":"Taro",
                "age":20
            },
            {
                "name":"Tom",
                "age":30
            }
        ]
    })";
    auto jobj = json::parse(jsonstr);
    std::cout <<"文字列変換: " << jobj.dump() << endl;

文字列には.dump()関数を用いればよいです。
ということで、

    auto jobj = json::parse(jsonstr);
    std::cout <<"文字列変換: " << jobj.dump() << endl;

のコードにて、jsonオブジェクト変換後、オブジェクトを文字列に変換しているコードになります。

jsonファイルからjsonデータを読み出し・取り扱い

こちらのjsonオブジェクト変換コードとファイル読み出しを用いればjsonデータを簡単に取り扱うことが出来ます。
例えばjsonファイル内のデータをこちらとした時に、

{"person":{"name":"brown","age":30,"message":"Hello"}}

こちらのようなコードを作成すればファイル内のjson要素の取得が可能になります。

#include <iostream>
#include <fstream>
#include <string>
#include "json.hpp"

void main(){
    ifstream ifs("./file.json");
    std::string jsonstr;
    if(ifs.fail()){
        cout << "File Open Error" << endl;
        return -1;
    }
    while(getline(ifs, jsonstr)){
        cout<<"[Read result] "<<str<<endl;
    }
    auto jobj = json::parse(jsonstr);
    std::cout <<"抽出: " << jobj["person"] << endl;
}

ライブラリを用いることで簡単にjsonデータの取り扱いが出来ますね!!

■ビルド時のwarning対応

バージョン7以上のgcc(g++)でコンパイルを行うと、

note: parameter passing for argument of type ‘__gnu_cxx::__normal_iterator<const nlohmann::basic_json<>*, std::vector<nlohmann::basic_json<>, std::allocator<nlohmann::basic_json<> > > >’ changed in GCC 7.1

といったwarningが出てしまうかもしれません。
まぁwarningですし出ても実行には支障は出ないのですが、、、
少し気味が悪い。。恐らくGCCのバージョンが原因なんだろうけど。。
ということで理由を調査してみました!!

すでにIssueとして上げられておりました!!
https://github.com/nlohmann/json/issues/1861

やはりバージョンに起因したものだったようです。
どうやら作成されたライブラリがバージョン6以前で作成されたことが要因のようです。
バージョン7.1以降でビルドを実行している限りwarningは無視することが出来るよう。

こちらのオプションを付ければwarningが出力されなくなるので利用sるうことが勧められておりました。

-Wno-psabi -Wall -Wextra -pedantic

確かにこちらのオプションを付ければwarningは出て来なくなりました。
とりあえずこちらの方法を採用することにします。

■最後に

今回はライブラリを用いてC++jsonデータを取り扱ってみました。
C++でも簡単にjsonパースが出来るのは少し意外でした!!



csvファイルにまとめた住所情報から緯度・経度リスト出力(csv出力)

先日、国土地理院APIで住所から緯度・経度を取得する方法をご紹介しました。
elsammit-beginnerblg.hatenablog.com

こちらの記事の最後にcsvファイルからまとめた住所データから緯度・経度を出力するコードを載せたのですが、
今回はこちらのコードについてブログにてご紹介していきたいと思います。
f:id:Elsammit:20210711122900p:plain



■環境

前回の記事と同様に今回も使用するのはpythonです。
pythonのバージョンはPython 3.7.3です。

また今回、csvファイルの読み込み・書き込みを行う関係でcsvライブラリを用います。
未インストールの方はこちらでインストールを行ってください。

pip3 install python-csv

ライブラリは他に前回と同様にurllib、requestsを用いますので、

pip3 install urllib3
pip3 install requests

でインストールしておいてください。

■条件

今回想定するcsvはこちらのように、
1行目をヘッダー、2行目以降にデータが格納された構成にします。
要素は住所が存在すればOKです。

id,店名,住所
1,札幌駅,北海道札幌市北区北6条西4丁目
2,東京駅,東京都千代田区丸の内1丁目
3,蒲郡駅,愛知県蒲郡市元町1

■コード紹介

ではcsvファイルから住所情報を読み出し、緯度・経度をcsv出力するコードを紹介していきます。
全体のコードはこちらです。

# -*- coding: utf-8 -*-

import csv
import sys
import urllib.parse # pip3 install urllib3
import json         # https://note.com/masato1230/n/nba86746179ca
import requests     # pip3 install requests
import time
import os

def main():
    args = sys.argv     # 実行時の引数取得
    if len(args) < 2:   # 実行時にcsvファイルを指定していなかった場合はエラーとする
        print("[Error] csvファイルを引数に与えてください")
        return

    if os.path.exists(args[1]) == False:        # 指定したファイルが存在しない場合にはエラーとする
        print("[Error] 指定したファイルがみつかりませんでした")
        return        

    fileName = os.path.splitext(args[1])        # 指定したファイルがcsvファイルでなければエラーとする
    if fileName[1] != ".csv":
        print("拡張子はcsvファイルにしてください")
        return

    i = 0               # ヘッダーとbody切り替え用
    AddressNum = -1     # 住所 or Addressが書かれた番号
    MapLists = []       # csvから読み出しかつ緯度・経度を追加する用変数

    # 国土地理院URL
    # APIの使用方法等は下記を参考のこと
    # https://libraries.io/github/gsi-cyberjapan/internal-search
    makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="    
    with open(args[1],encoding='utf-8') as f:   # 引数に与えたcsvファイル読み込み
        reader = csv.reader(f)
        for line in reader:                     # 各行読み取り
            buf = []
            if i == 0:                          # ヘッダーの場合
                j = 0                           # 列番号カウント用変数
                for item in line:               # 列読み取り
                    if item == "住所" or item == "Address" or item == "address":
                        AddressNum = j          # 住所を見つけたらその時の番号を変数に格納
                    j+=1                        # 列番号カウンタをインクリメント
                    buf.append(item)            # 取得した列をbufに格納
                if AddressNum < 0:              # 住所やアドレスがなかったらエラーとして修了
                    print("[Error] 住所もしくはAddressが項目にありません")
                    return

                buf.append("latitude")          # ヘッダーの列にlatitudeを追加
                buf.append("longitude")         # ヘッダーの列にlongitudeを追加
                MapLists.append(buf)            # ヘッダー行をMapListsに格納
            else:                               # body側の各行情報
                for item in line:               # 各行ごとのカラムを取得
                    buf.append(item)            # 取得したカラムを変数に格納
                print(line[AddressNum])
                s_quote = urllib.parse.quote(line[AddressNum])          # 住所の文字列をURLエンコード
                response = response = requests.get(makeUrl + s_quote)   # エンコードした文字列を国土地理院APIの引数として与えてget request
                if response.json() == []:                               # レスポンスされたjsonデータの中身を確認し空だったら
                    print("[Error] 住所がよくわかりませんでした")          # 判定できなかった旨を出力し緯度・経度は空文字を格納
                    buf.append("")                                      
                    buf.append("")                    
                elif len(response.json()) >1:                           # 候補が複数あった場合、判定出来ないためスキップ
                    print("[Error] 住所の絞り込みが出来ず複数候補が出ました \n 住所の絞り込みをおこなってください ")
                    buf.append("")                                      
                    buf.append("") 
                else:                                                   # レスポンスされたjsonデータが空でなかった場合
                    print(response.json()[0]["geometry"]["coordinates"]) 
                    buf.append(response.json()[0]["geometry"]["coordinates"][0])    # 緯度情報をbufに格納
                    buf.append(response.json()[0]["geometry"]["coordinates"][1])    # 経度情報をbufに格納
                MapLists.append(buf)            # 各行ごとの情報をMapListsに格納                                    
                time.sleep(1)                   # 国土地理院APIに負荷を掛けないように
            i+=1

    with open("output.csv", mode='w',newline='',encoding='shift_jis') as f: # データをcsvファイルへ格納
        writer = csv.writer(f)
        for maplist in MapLists:                # 各行の情報を格納した変数を読み出す
            writer.writerow(maplist)            # 読み出したデータをcsvへ書き込み
    print("Write Finish !!")
            
if __name__ == "__main__":
    main()

まず、csvファイルからデータを読み出し、reader変数に格納します。

    with open(args[1],encoding='utf-8') as f:   # 引数に与えたcsvファイル読み込み
        reader = csv.reader(f)

次に、

for line in reader:                     # 各行読み取り
~~~
    for item in line:               # 列読み取り

にて読み出したデータを1行ごとに分割さらに各列でパースしていきます。

最初はヘッダーになるので、ヘッダー名に"住所"要素がある列番号をセットします。

for item in line:               # 列読み取り
    if item == "住所" or item == "Address" or item == "address":
    AddressNum = j          # 住所を見つけたらその時の番号を変数に格納
    j+=1                        # 列番号カウンタをインクリメント
    buf.append(item)            # 取得した列をbufに格納

後は前回と同様に住所データを国土地理院APIに渡して緯度・経度を取得すればOKです。

for item in line:               # 各行ごとのカラムを取得
    buf.append(item)            # 取得したカラムを変数に格納
	s_quote = urllib.parse.quote(line[AddressNum])          # 住所の文字列をURLエンコード
	response = response = requests.get(makeUrl + s_quote)   # エンコードした文字列を国土地理院APIの引数として与えてget request
	
	buf.append(response.json()[0]["geometry"]["coordinates"][0])    # 緯度情報をbufに格納
   	buf.append(response.json()[0]["geometry"]["coordinates"][1])    # 経度情報をbufに格納
 	MapLists.append(buf)            # 各行ごとの情報をMapListsに格納                                    
	time.sleep(1)                   # 国土地理院APIに負荷を掛けないように

AddressNumにはヘッダーで検索した住所列番号が格納されているので、line[AddressNum]には各住所情報が格納されております。
ですので、line[AddressNum]をurllib.parse.quoteによりURLエンコードしてAPIに渡して緯度・経度を取得しています。

そして、MapListsにデータを格納。
MapListsは最後にcsv出力するためのデータ群が格納されます。

データを読み出して緯度・経度を取得⇒MapListsに格納まで出来たので最後にcsvファイル出力です。
コードはこちら。

with open("output.csv", mode='w',newline='',encoding='shift_jis') as f: # データをcsvファイルへ格納
    writer = csv.writer(f)
    for maplist in MapLists:                # 各行の情報を格納した変数を読み出す
        writer.writerow(maplist)            # 読み出したデータをcsvへ書き込み

MapListsを1行ごとに分割しwriter.writerowで1行ずつcsvファイルに書き込み。

■コードの使い方

本コードは下記の通り実行すれば引数として渡したcsvファイルから読み取り、output.csvが出力されます。

python3 ファイル名.py csvファイル名.csv

APIに負担を掛けないように1秒ごとにwaitを入れ込んでいます。
この関係で複数住所データが格納されていると少し時間がかかってしまいます。
コーヒーを飲んでゆっくり待ってください。

■最後に

今回はcsvファイルにまとめた住所情報から緯度・経度を算出しcsv出力するコードをご紹介しました。
こちらにcsvファイルにまとめた住所データから緯度・経度を算出するコードを格納しておりますので、
よろしければご覧ください。
https://github.com/Elsammit/SearchAddressToMapInfo


■参考
https://pypi.org/project/python-csv/
https://qiita.com/motoki1990/items/0274d8bcf1a97fe4a869

国土地理院APIで住所から緯度・経度を取得(ジオコーディング)

今回は住所から緯度・経度を取得する方法をまとめていきたいと思います。
f:id:Elsammit:20210711122900p:plain



Google Maps APIの有料化

すでに皆さんご存じの通り、Google Maps APIが有料化しました。。
有料化といっても、無料枠も用意されておりAPIコール数が数千程度であれば無料の範囲で利用できます。
また、使用制限を掛けることも出来るみたいなので無料枠の範囲で使い続けることも出来るようです。

だだ、、、
制限が入ってしまったり、無料枠で使っていきたい人には不便になってしまいました泣。

ということで他に方法がないか調べてみたところ、
国土地理院にて無料でAPIが提供されていることを発見!!
こちらを使わせていただくことにしました!!
APIですが、東京大学CSIS シンプルジオコーディング実験サービス及び地名データを格納したデータベースで検索した結果を配信しているようです。

API仕様のチェック

API仕様のついてまとめていきます。

まずAPIのURLですがこちらの通りです。

http://[server]/address-search/AddressSearch?q=https://msearch.gsi.go.jp/address-search/AddressSearch?q=[検索文字列]

こちらの検索文字列ですが、住所をURLエンコードした文字列である必要があります。
例えば札幌駅の住所である、
北海道札幌市北区北6条西4丁目
を検索したい場合、
一度、北海道札幌市北区北6条西4丁目をURLエンコードし、

北海道札幌市北区北6条西4丁目
↓ URLエンコード
%E5%8C%97%E6%B5%B7%E9%81%93%E6%9C%AD%E5%B9%8C%E5%B8%82%E5%8C%97%E5%8C%BA%E5%8C%97%EF%BC%96%E6%9D%A1%E8%A5%BF%EF%BC%94%E4%B8%81%E7%9B%AE

こちらをAPIのURLに与えます。

http://[server]/address-search/AddressSearch?q=%E5%8C%97%E6%B5%B7%E9%81%93%E6%9C%AD%E5%B9%8C%E5%B8%82%E5%8C%97%E5%8C%BA%E5%8C%97%EF%BC%96%E6%9D%A1%E8%A5%BF%EF%BC%94%E4%B8%81%E7%9B%AE

次にresponseですが、json形式でデータ出力され、
構成は、

[{'geometry': {'coordinates': [141.349243, 43.068455], 'type': 'Point'}, 'type': 'Feature', 'properties': {'addressCode': '', 'title': '北海道札幌市北区北六条西四丁目'}}]

といった形になります。
今回緯度、経度を取得したいので、
geometry配下のcoordinates、

[141.349243, 43.068455]

をGet出来ればよいです。

今回の住所の場合にはjsonが1つでしたが、場合によっては、複数返ってくるようです。
その場合にはjson形式のデータが配列で格納されます。

■環境

今回使用する言語はpythonとしました。
pythonのバージョンは、Python 3.7.3です。

さらにライブラリとして、
urllib、requestsを用います。
まだインストールされていない場合には下記でインストールを実行してください。

pip3 install urllib3
pip3 install requests

国土地理院APIで住所から緯度・経度を取得

ではpythonで先ほどのAPIをコールして緯度・経度を取得します。
まずはコードですが、こちらになります。

Address = "北海道札幌市北区北6条西4丁目"
makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
s_quote = urllib.parse.quote(Address)
response = response = requests.get(makeUrl + s_quote)
print(response.json()[0]["geometry"]["coordinates"])

実施していることですが、
Addressに格納された住所を

s_quote = urllib.parse.quote(Address)

によりURLエンコードし、エンコード結果をAPIのURLと結合してget requestを実行。

makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
response = requests.get(makeUrl + s_quote)

requestした結果のresponseですが、先ほど記載しました通りjson形式となります。
また、緯度・経度はjsonはgeometry⇒coordinatesの配下になるため、

response.json()[0]["geometry"]["coordinates"]

で取得可能です。
先ほどご説明しました通り配列でjson形式のデータが格納されているので、
先頭データとしてresponse.json()[0]としております。

最後に緯度・経度を別々に分離する方法です。
response.json()[0]["geometry"]["coordinates"]により得られる結果は、

[141.349243, 43.068455]

といったように配列で得られるため、
緯度:response.json()[0]["geometry"]["coordinates"][0]
経度:response.json()[0]["geometry"]["coordinates"][1]
で分離した形でデータを取得できます。

■コード紹介

こちらにcsvファイルにまとめた住所データから緯度・経度を算出するコードを格納しました。
よろしければご覧ください。
GitHub - Elsammit/SearchAddressToMapInfo

※細かい解説は後でまとめたいと思います!!

■最後に

今回は国土地理院APIで住所から緯度・経度を取得するコードをまとめました。
無料枠でしか使わないのであればGoogle Maps APIを用いてもいいかとおもいますが、
API Keyが必要になったり、少し手間になりますので個人的にはこちらの方がおすすめかな?と思っております。
もしGoogle Maps APIを用いたい場合にはこちらをご参考いただくとよいかもしれません。
qiita.com



gtkmmでwindow位置を可変にする方法

今回はgtkmmでwindowの位置を変更する方法についてまとめていきたいと思います!!
位置を指定してwindowをいくつか座標を指定して配置したい時などに使用できるかな?と思います。



■条件

今回gtkmmでwindowの位置を指定するにあたり、windowのレイアウトはgladeというデザイナーを用います。
gladeの導入方法やgtkmmとの連携方法についてはこちらにまとめております。
elsammit-beginnerblg.hatenablog.com

またOSですが、Ubuntu20.04を用い、
gtkmmのバージョンはgtkmm-3.0を用います。

■windowを準備

gladeによるデザインですが、こちらの通りImage Widgetが1つおいてあるだけのシンプルなwindowにします。
f:id:Elsammit:20210707225327p:plain

またwidget ID名ですが、
・widdow:TestWindow
・Image:_Image
としました。

またヘッダファイルとして下記を定義しました。

class MainWin : public Gtk::Window {
    Gtk::Window TestWindow;
    Gtk::Image* _Image;
    Glib::RefPtr<Gtk::Builder> builder;

public:
    MainWin(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade);
};

■windowの位置を指定する

では本題の位置指定をしていきます。
位置指定ですが、

MainWin::MainWin(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade) :
    Gtk::Window(cobject), builder(refGlade) {
    
    gtk_window_move(GTK_WINDOW(gobj()), input_x, input_y);

    ・・・

というように、gtk_window_moveを用いればOKです。
下記の通り、x軸(input_x)、y軸(input_y)を指定することでwindowの座標位置を指定することが可能です。
この座標ですが、windowの左上端の位置に該当します。

 gtk_window_move(GTK_WINDOW(gobj()), input_x, input_y);

全体のコードはこちらのように記載しました。

MainWin::MainWin(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade) :
    Gtk::Window(cobject), builder(refGlade) {

    gtk_window_move(GTK_WINDOW(gobj()), input_x, input_y);
    this->builder->get_widget("_Image", this->_Image);
    this->_Image->set("画像ファイル名");
}

こちらのinput_x、input_yを指定する際に、ファイルなどの外部ファイルからセットするようにすればwindowを動的(可変)にセットすることが出来ます。
例えば、

void FileRead(){
    std::ifstream ifs(fileName);
    std::string str;
    if (!ifs){
        std::cout << "ファイルが開けませんでした。" << std::endl;
        return;
    }
    int i = 0;
    while(getline(ifs, str)){
        if(i == 0){
            input_x = std::stoi(str);
            i++;
        }else if(i == 1){
            input_y = std::stoi(str);
            i++;
        }
    }
}

というようにテキストファイルを読み出し、input_x、input_yにx座標、y座標を指定するようにします。
こうすることにより、細かい座標位置を外部ファイルで調整できるようになるので調整しやすくなります。

■(おまけ)座標の動的指定について

windowの位置を指定する、

 gtk_window_move(GTK_WINDOW(gobj()), input_x, input_y);

ですが、windowを生成した後でも位置変更が可能です。
このため、ボタンwidgetを設置してボタンクリックするとwindowの位置が移動するコードの作成も可能になります。

■まとめ

今回はgtkmmでwindowの座標位置を指定する方法をまとめておきました。
話しは変わりますが、Flutterがwindowアプリや組み込みに対応しましたね。
どうなんだろう??トヨタのカーナビに採用されたようだけど。
今度使ってみたいと思います!!



pythonでグーグル翻訳を利用し英語字幕を日本語字幕に変換してみる

先日動画に字幕を付ける方法をまとめました。
elsammit-beginnerblg.hatenablog.com

ただ私がやりたかったことは英語字幕を日本語字幕にすること!!
そもそも字幕付与を行おうと思った理由が英語音声で公開されているCourseraを日本語字幕で閲覧したかったからなので。


ということで、今回はグーグル翻訳を利用して英語字幕を日本語字幕に変更してみたいと思います!!



■前提条件

タイトルの通り、pythonを用います。
pythonのバージョンですが、
python:3.7.3
を用いました。

さらに、今回は字幕ファイルとしてvttが用意できる場合を前提にします。
※vttファイルの無い(英語字幕付き動画しかない)場合は、、、vttファイルを生成すればよいのだろうけど、、、どうするのだろう??

pythongoogle翻訳

ではまずはpythongoogle翻訳する環境を整えていきます。

google翻訳を行うにあたりGoole翻訳APIが無料で公開されているのでこちらを用いれば自動で翻訳が出来そうなのですが、、、
事前にGoogle Apps APIサイトにてプロジェクト作成し、そのパスをpythonコード上に載せて、、、
と少し面倒だったので別の方法がないか調べていました。
※3分で構築と書いてあったのですが、出来れば1分でやりたいw

そこで、"googletrans"というライブラリを発見!!
こちらを利用することにしました。
googletransですが、

pip3 install googletrans

でインストールが行えます。

googletransを用いた翻訳コードですがこちらのようになります。

from googletrans import Translator

Translator(service_urls=['translate.googleapis.com'])
translated = translator.translate("Hello", dest="ja")

使い方も結構簡単で、

from googletrans import Translator

Translator(service_urls=['translate.googleapis.com'])

を定義し、

translated = translator.translate("Hello", dest="ja")

で文字列を日本語翻訳。
以上です。
translateの引数ですが、第1引数に翻訳したい英語文字列、第2引数に翻訳したい言語(今回は日本語)になります。
逆に、日本語⇒英語翻訳したい場合には、

translated = translator.translate("こんにちは", dest="en")

とすればOKです。

■英語字幕ファイルを日本語に翻訳

では、googletrans を用いて英語字幕ファイルから日本語へgoogle翻訳していきます。
vttファイルの構成ですが、こちらの通り
最初の数行がHeaderで、
その後、1行空けて、
表示時間⇒文字列⇒空欄⇒表示時間⇒文字列⇒空欄⇒...
といった構成になっております。

WEBVTT

00:00:00.000 --> 00:00:04.000 position:10%,line-left align:left size:35%
Where did he go?

00:00:03.000 --> 00:00:06.500 position:90% align:right size:35%
I think he went down this lane.

00:00:04.000 --> 00:00:06.500 position:45%,line-right align:center size:35%
What are you waiting for?

そこで、表示時間を検出しその次の行を翻訳する文字列として抽出、translate関数の引数として与えることとしました。

コードはこちら。

import os
from googletrans import Translator
import time
import sys
import subprocess

def TranslateFunc(FileName):
    i = 1
    j = 0
    translator = Translator(service_urls=['translate.googleapis.com'])
    with open(FileName) as f:
        sl = f.readlines()
    
    ret = sl[0]
    ret += "\n"
    data = str(j)+"/"+str(len(sl)-1)
    while 1:
        data = str(j)+"/"+str(len(sl)-1)
        print(data)

        if j > len(sl)-1:
            break
        try:
            buf = sl[j].split(':')
            if len(buf) >= 5:
                ret += sl[j]
                result = translator.translate(sl[j+1], dest="ja")
                ret += result.text
                j+=2
                while 1:
                    if len(sl[j]) > 1:
                        result = translator.translate(sl[j], dest="ja")
                        ret += result.text
                        j+=1
                    else:
                        break
            else:
                ret += sl[j]
                j+=1
        except:
            break
                
    f = open('output.vtt','w', encoding='UTF-8')
    f.write(ret)
    f.close()

if __name__ == '__main__':
    args = sys.argv
    if args[2].endswith('.vtt'):
        TranslateFunc(args[2])

    if args[1].endswith('.mp4') and args[2].endswith('.vtt'):
        cmdline = "ffmpeg -i " + args[1] + " -i output.vtt" + " -map 0:v -map 0:a -map 1 -metadata:s:s:0 language=jpn -c:v copy -c:a copy -c:s srt " + "output2.mkv"
        res = subprocess.call(cmdline, shell=True)

ちょっと長いですね。。。

    if args[1].endswith('.mp4') and args[2].endswith('.vtt'):
        cmdline = "ffmpeg -i " + args[1] + " -i output.vtt" + " -map 0:v -map 0:a -map 1 -metadata:s:s:0 language=jpn -c:v copy -c:a copy -c:s srt " + "output2.mkv"
        res = subprocess.call(cmdline, shell=True)

はこちらの記事と同じなので割愛。
elsammit-beginnerblg.hatenablog.com

TranslateFunc関数ですが、
引数として与えたvttを読み出し、

    with open(FileName) as f:
        sl = f.readlines()
    while 1:
        data = str(j)+"/"+str(len(sl)-1)
        print(data)

        if j > len(sl)-1:
            break
        try:
            buf = sl[j].split(':')
            if len(buf) >= 5:
                ret += sl[j]
                result = translator.translate(sl[j+1], dest="ja")
                ret += result.text
                j+=2
                while 1:
                    if len(sl[j]) > 1:
                        result = translator.translate(sl[j], dest="ja")
                        ret += result.text
                        j+=1
                    else:
                        break
            else:
                ret += sl[j]
                j+=1
        except:
            break

にて先ほど記載した通り、表示時間を検出し次行の文字列を翻訳しています。
表示時間の検知方法ですが、

00:00:03.000 --> 00:00:06.500 position:90% align:right size:35%

となっているので、":"でsplitをかけ、5分割以上出来れば表示時間と判定することにしました。

翻訳する文が2行に分かれている可能性もあります。
そこで、次回の文字列が空欄でない場合には翻訳する文言と判断し、翻訳を実行し次の行に移行させる制御としました。

これらの翻訳データをret変数に格納していっております。

data = str(j)+"/"+str(len(sl)-1)
print(data)

ですが、こちらは進捗状況表示用になります。
結構時間がかかり、停止してしまっているのか不安になったので。

■最後に

こちらを用いればCourseraの英語翻訳しかない動画も怖くないですねw
まぁ、Google翻訳にかけているだけなので誤りもあるはずなので英語の理解は必須になりますが。。

Courseraについてはこちらにまとめましたので是非!!
https://elsammit-diarypractice.hatenablog.com/entry/2021/06/30/224351

":"でsplitするのは微妙だったかな??
"-->"が含まれているか判定した方がよかったかな??
まぁ、この辺は使いながらチューニングしていくことにします!!