2023,江端さんの技術メモ

type testJSON struct {
	Id      string `json:"@id"`
	Type    string `json:"@type"`
	Date    string `json:"dc:date"`
	Context string `json:"@context"`
	Title   string `json:"dc:title"`
	Note    string `json:"odpt:note"`
	Regions struct {
		Type        string `json:"type"`
		Coordinates []interface{}
	} `json:"ug:region"`
	Owl              string `json:"owl:sameAS"`
	Pattern          string `json:"odpt:pattern"`
	Busroute         string `json:"odpt:busroute"`
	Operator         string `json:"odpt:operator"`
	Direction        string `json:"odpt:direction"`
	BusstopPoleOrder []struct {
		Note        string `json:"odpt:note"`
		Index       int    `json:"odpt:index"`
		BusstopPole string `json:"odpt:busstopPole"`
	} `json:"odpt:busstopPoleOrder"`
}

の、

Coordinates []interface{}

の部分をsrting型→分割 → float64型に戻して処理しようしているのですが、

for _, e := range data {
		fmt.Println(e) // 全情報表情
		coors := e.Regions.Coordinates
		fmt.Println("len_mm", len(coors))
		for _, coor := range coors {
			fmt.Println(coor)
			str_coor := coor.(string)
			arr := strings.Split(str_coor, " ")
			lng := arr[0]
			lat := arr[1]
			fmt.Println(lng, lat)
		}
	}

[139.6249873 35.4648941]
panic: interface conversion: interface {} is []interface {}, not string

goroutine 1 [running]:
main.main()
C:/Users/ebata/yoko_bus_route/route_json.go:92 +0x694
exit status 2

というエラーが出てきて停止します ―― キャストできない。

多分対応方法はあると思うのですが、調べるのが面倒なので、C/C++で定番の、あの美しくない方法 "sprintf"を使いました

for _, coor := range coors {

			//	str_coor := coor.(string)   
			str_coor := fmt.Sprintf("%v", coor) // この段階では" [139.575019, 35.439622]"という文字列 

			arr := strings.Split(str_coor, " ") // スペース" " で arr[0], arr[1]という文字列に分離する

			str_lng := strings.Replace(arr[0], "[", "", -1) // arr[0] ("[139.575019")から、"["を消す
			str_lat := strings.Replace(arr[1], "]", "", -1) // arr[1] ("35.439622]")から、"]"を消す

			lng64, _ := strconv.ParseFloat(str_lng, 64) // float64に強制的に型変換する
			lat64, _ := strconv.ParseFloat(str_lat, 64) // float64に強制的に型変換する

			fmt.Println("lng:", lng64)
			fmt.Println("lat:", lat64)

		}

動けばいいんですよ、動けば。

2023,江端さんの技術メモ

https://ckan.odpt.org/dataset/b_busstop-yokohamamunicipal/resource/da256719-0c39-48a7-a1f2-20354e18d529

// stop_json.go
// 横浜市交通局 バス停情報 / Bus stop information of Transportation Bureau, City of Yokohama
// https://ckan.odpt.org/dataset/b_busstop-yokohamamunicipal

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
)

type stopJSON struct {
	Id        string `json:"@id"`
	Type      string `json:"@type"`
	Lng_Title struct {
		En      string `json:"en"`
		Ja      string `json:"ja"`
		Ko      string `json:"ko"`
		Zh      string `json:"zh"`
		Ja_Hrkt string `json:"ja-Hrkt"`
	} `json:"title"`
	Date              string        `json:"dc:date"`
	Lat               float64       `json:"geo:lat"`
	Long              float64       `json:"geo:long"`
	Context           string        `json:"@context"`
	Title             string        `json:"dc:title"`
	Kana              string        `json:"odpt:kana"`
	SameAS            string        `json:"owl:sameAS"`
	Operator          []interface{} `json:"odpt:operator"`
	BusroutePattern   []interface{} `json:"odpt:busroutePattern"`
	BusstopPoleNumber string        `json:"odpt:busstopPoleNumber"`
	// BusstopPoleTimetable
}

func main() {

	// JSONファイルから読み取る場合
	//file, err := ioutil.ReadFile("route.json")
	//file, err := ioutil.ReadFile("odpt_BusroutePattern.json")
	file, err := ioutil.ReadFile("odpt_BusstopPole.json")
	if err != nil {
		// エラー処理
	}

	/*
		// Webアクセスで取得する場合

		client := &http.Client{}
		req, err := http.NewRequest("GET", "https://api.odpt.org/api/v4/odpt:BusstopPole?odpt:operator=odpt.Operator:YokohamaMunicipal&acl:consumerKey=f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4", nil)
		if err != nil {
			log.Fatal(err)
		}

		resp, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		}
		defer resp.Body.Close()
		if err != nil {
			log.Fatal(err)
		}

		file, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			log.Fatal(err)
		}
	*/

	var data []stopJSON

	err = json.Unmarshal(file, &data)
	if err != nil {
		fmt.Println("Unmarshal")
		log.Fatal(err)
	}

	//fmt.Println(string(file))

	// ループによる取得
	for _, e := range data {

		fmt.Println(e) // 全情報表情

		stops := e.BusroutePattern
		fmt.Println("len_stop", len(stops))

		for _, stop := range stops {
			fmt.Println(stop)
		}

	}
}

2023,江端さんの技術メモ

https://ckan.odpt.org/dataset/b_busroute-yokohamamunicipal


■動かなくて悩んでいたコード

// xml_parse.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type testXML struct {
	Date     string `json:"dc:date"`
	Busroute string `json:"odpt:BusroutePattern"`
}

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://api.odpt.org/api/v4/odpt:BusroutePattern?odpt:operator=odpt.Operator:YokohamaMunicipal&acl:consumerKey=f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4", nil)
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if err != nil {
		log.Fatal(err)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	//fmt.Println(body)

	var data testXML

	err = json.Unmarshal(body, &data)
	if err != nil {
		fmt.Println("Unmarshal")
		log.Fatal(err)

	}

	fmt.Println(data)

}

出力結果

> go run xml_parse.go
Unmarshal
2023/05/01 18:05:31 json: cannot unmarshal array into Go value of type main.testXML
exit status 1

悩むこと1時間、("type testXML"スキーマを色々いじったりしていた)

『そういえば、このjsonのデータは、違う路線データも入っているはずだよな』
→ 『ということは、複数のデータが入っていることになるよな』
→ 『 For文で取らなければ不味くないか?』

で、ググってみたら、どうやらループになるように仕込んでおかないといけないらしいことが分かりました(XMLの先頭の情報が取れるだけだと思っていた)

■動いたコード

// xml_parse.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type testXML struct {
	Date     string `json:"dc:date"`
	Busroute string `json:"odpt:BusroutePattern"`
}

func main() {
	client := &http.Client{}
	req, err := http.NewRequest("GET", "https://api.odpt.org/api/v4/odpt:BusroutePattern?odpt:operator=odpt.Operator:YokohamaMunicipal&acl:consumerKey=f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4", nil)
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if err != nil {
		log.Fatal(err)
		x
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	//fmt.Println(body)

	var data []testXML

	err = json.Unmarshal(body, &data)
	if err != nil {
		fmt.Println("Unmarshal")
		log.Fatal(err)

	}

	for _, e := range data {
		fmt.Println(e.Date)
	}

}

go run xml_parse.go
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00
2023-04-03T00:00:00+09:00"当たり"だったようです。


バス路線情報のJSONをブラウザで読みとってみました。


ここから、酷いハマり方をします(10時間くらい)。

上記のルートの位置情報の表示のコーディングがどうしても分からない。

以下は、上記のJSONを簡略化したものです(route.json)。

[
	{
		"dc:date":"2023-04-03T00:00:00+09:00",
		"dc:title":"007",
		"ug:region":
				{
					"type":"Triangle",
					"coordinates":[[139.6249873,35.4648941],[139.6237514,35.4648862],[139.623672,35.4650137]]
				},
		"owl:sameAs":"test1"
	},
	{
		"dc:date":"2023-04-03T00:00:00+09:00",
		"dc:title":"008",
		"ug:region":
				{
					"type":"LineString",
					"coordinates":[[139.667765,35.416456],[139.668006,35.416708],[139.668116,35.416788],[139.668276,35.416841]]
				},
		"owl:sameAs":"test2"
	}
]

問題は、ネストの中に入っていて、タグの名前のついていない、

"coordinates":[[139.6249873,35.4648941],[139.6237514,35.4648862],[139.623672,35.4650137]]

の部分です。

10時間くらいは戦ったかなぁ ―― もう疲れ果てて、質問サイトに投稿する為に、(上記の)JSONファイルと、このパーサープログラムを成形していました。

で、そのプログラムをテストランさせたら ―― これが動いてしまったのです。

// route_json.go

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
)

type testXML struct {
	Date    string `json:"dc:date"`
	Title   string `json:"dc:title"`
	Note    string `json:"odpt:note"`
	Owl     string `json:"owl:sameAS"`
	Regions struct {
		Type        string `json:"type"`
		Coordinates []interface{}
		//Coordinates []struct {
		//	Longitude float64 `json:"longitudes>longitude"`
		//	Latitude  float64 `json:"latitudes>latitude "`
		//} `json:"coodtinates"`
	} `json:"ug:region"`
}

func main() {
	file, err := ioutil.ReadFile("route.json")
	//file, err := ioutil.ReadFile("odpt_BusroutePattern.json")

	if err != nil {
		// エラー処理
	}

	var data []testXML

	err = json.Unmarshal(file, &data)
	if err != nil {
		fmt.Println("Unmarshal")
		log.Fatal(err)
	}

	fmt.Println(string(file))

	// ループによる取得
	for _, e := range data {

		fmt.Println(e) //
		mm := e.Regions.Coordinates

		fmt.Println(len(mm))

		for _, m := range mm {
			fmt.Println(m)
		}
	}
}

実行結果は以下の通りです。

> go run route_json.go
[
{
"dc:date":"2023-04-03T00:00:00+09:00",
"dc:title":"007",
"ug:region":
{
"type":"Triangle",
"coordinates":[[139.6249873,35.4648941],[139.6237514,35.4648862],[139.623672,35.4650137]]
},
"owl:sameAs":"test1"
},
{
"dc:date":"2023-04-03T00:00:00+09:00",
"dc:title":"008",
"ug:region":
{
"type":"LineString",
"coordinates":[[139.667765,35.416456],[139.668006,35.416708],[139.668116,35.416788],[139.668276,35.416841]]
},
"owl:sameAs":"test2"
}
]

{2023-04-03T00:00:00+09:00 007 test1 {Triangle [[139.6249873 35.4648941] [139.6237514 35.4648862] [139.623672 35.4650137]]}}
3
[139.6249873 35.4648941]
[139.6237514 35.4648862]
[139.623672 35.4650137]
{2023-04-03T00:00:00+09:00 008 test2 {LineString [[139.667765 35.416456] [139.668006 35.416708] [139.668116 35.416788] [139.668276 35.416841]]}}
4
[139.667765 35.416456]
[139.668006 35.416708]
[139.668116 35.416788]
[139.668276 35.416841]

ずっとエラーが出ていたんですが、突然表示されるようになりました。

        Coordinates []interface{}

JSONタグが付いていない問題は、これで解消できるようです。

で、(簡易版ではなく)実体のJSONファイルを使ってみても動きました。

こういうことがあるから、本当にコーディングというのは厄介なんですよね。


パースしたデータを表示してみました。

通路が(多分)順番に表示されることが確認できました。


だいたいこれで完成です。

// route_json.go
// 横浜市交通局 バス路線情報 / Bus route information of Transportation Bureau, City of Yokohama
// https://ckan.odpt.org/dataset/b_busroute-yokohamamunicipal

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
)

type testJSON struct {
	Id      string `json:"@id"`
	Type    string `json:"@type"`
	Date    string `json:"dc:date"`
	Context string `json:"@context"`
	Title   string `json:"dc:title"`
	Note    string `json:"odpt:note"`
	Regions struct {
		Type        string `json:"type"`
		Coordinates []interface{}
	} `json:"ug:region"`
	Owl              string `json:"owl:sameAS"`
	Pattern          string `json:"odpt:pattern"`
	Busroute         string `json:"odpt:busroute"`
	Operator         string `json:"odpt:operator"`
	Direction        string `json:"odpt:direction"`
	BusstopPoleOrder []struct {
		Note        string `json:"odpt:note"`
		Index       int    `json:"odpt:index"`
		BusstopPole string `json:"odpt:busstopPole"`
	} `json:"odpt:busstopPoleOrder"`
}

func main() {

	// JSONファイルから読み取る場合
	//file, err := ioutil.ReadFile("route.json")
	file, err := ioutil.ReadFile("odpt_BusroutePattern.json")
	if err != nil {
		// エラー処理
	}

	/*
		// Webアクセスで取得する場合

		client := &http.Client{}
		req, err := http.NewRequest("GET", "https://api.odpt.org/api/v4/odpt:BusroutePattern?odpt:operator=odpt.Operator:YokohamaMunicipal&acl:consumerKey=f4954c3814b207512d8fe4bf10f79f0dc44050f1654f5781dc94c4991a574bf4", nil)
		if err != nil {
			log.Fatal(err)
		}

		resp, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		}
		defer resp.Body.Close()
		if err != nil {
			log.Fatal(err)
		}

		file, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			log.Fatal(err)
		}
	*/

	var data []testJSON

	err = json.Unmarshal(file, &data)
	if err != nil {
		fmt.Println("Unmarshal")
		log.Fatal(err)
	}

	//fmt.Println(string(file))

	// ループによる取得
	for _, e := range data {

		fmt.Println(e) // 全情報表情
		coors := e.Regions.Coordinates

		fmt.Println("len_mm", len(coors))

		for _, coor := range coors {
			fmt.Println(coor)
		}

		stops := e.BusstopPoleOrder

		fmt.Println("len_ss", len(stops))

		for _, stop := range stops {
			fmt.Println(stop.Note)
			fmt.Println(stop.Index)
			fmt.Println(stop.BusstopPole)
		}

	}
}

2023,江端さんの技術メモ

1 目的

すぐに環境やらコード(の場所)やらを忘れるので、自分の為にメモを残す

2 簡単なfastapiの環境とコード

私の環境では、c:\users\ebata\fastapi1,2,3,4,5あたりにある。

2.1. 最新のPIPをアップグレードする

> pip install --upgrade pip
> python -m pip install --upgrade pip
> python3 -m pip install --upgrade pip

のいずれかでできる。

2.2. uvicorn, fastapi, pydantic, pandas, requests, jsonのモジュールをインストールする

> pip3 install fastapi
> pip3 install uvicorn[standard]

は必須だが、後はプログラムを動かせば、プログラムの方から、インストールを指示されると思うので大丈夫(だろう)。

2.3. uvicornを起動する

> uvicorn index:app --reload

これで、自動的に"index.py"が動き出す。

2.4. クライアントを起動する

> python request.py

で、起動を確認する。

以上

3. ソースコード

3.1. index.py

# https://miseruit.com/2022/07/18/post-2939/

from fastapi import FastAPI
from pydantic import BaseModel
import pandas

class Item(BaseModel):
    ID: str
    Name: str
    Class: str

app = FastAPI() #インスタンスを作成

User_list =[
    {"ID":"M001","Name":"Takashi","Class":"A"},
    {"ID":"M002","Name":"Hanako","Class":"A"},
    {"ID":"M003","Name":"Hiroshi","Class":"B"},
    {"ID":"M004","Name":"Kyoko","Class":"B"},
    ]

# joson_normalize関数を用いてJsonデータをDataFrame型に変換します。
df = pandas.json_normalize(User_list)

# Get /Users/ : 全件取得
# Get /Users/{ID} :特定のIDのみ取得
# POST /Users/ :ユーザの登録
# DELETE /Users/{ID} :ユーザの削除

# curl http://localhost:8000/Users/
@app.get("/Users/")
async def users():
    return User_list

# curl http://localhost:8000/Users/M003
@app.get("/Users/{u_id}")
async def users(u_id:str):
    return list(filter(lambda item : item['ID']==u_id, User_list))

# Windowsの場合 
# {"ID":"M005","Name":"Aya","Class":"C"}]を追加する
# curl -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"ID\":\"M005\", \"Name\":\"Aya\", \"Class\":\"C\"}" http://localhost:8000/Users/

@app.post("/Users/")
async def users(user: Item):
    User_list.append({"ID": user.ID,"Name":user.Name,"Class":user.Class})
    return User_list

# curl -X DELETE -H "accept: application/json" http://localhost:8000/Users/M003
@app.delete("/Users/{u_id}")
async def users(u_id:str):
    global df
    df = df.drop(df.index[df["ID"]==u_id])
    return df.to_json(orient='records')

3.2. request.py

import requests
import json

# テスト
#url = 'https://wp.kobore.net/'
#response = requests.get(url)
#print(response.text)

#print()

print("-----start of get(1)")

# curl http://localhost:8000/Users/ と同じ機能を実施
r = requests.get('http://localhost:8000/Users/')

print(r.url)
print(r.status_code)
print(r.text)
print(r.json())

print("-----end of get(1)")

print()

# curl http://localhost:8000/Users/M003 と同じ機能を実施

print("-----start of get(2)")

r = requests.get('http://localhost:8000/Users/M003')

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())
print("-----end of get(2)")

print()
print("-----start of delete")
# curl -X DELETE -H "accept: application/json" http://localhost:8000/Users/M003と同じ機能を実施
r = requests.delete('http://localhost:8000/Users/M003')

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())

print("-----end of delete")
print()

# curl -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"ID\":\"M005\", \"Name\":\"Aya\", \"Class\":\"C\"}" http://localhost:8000/Users/ と同じ機能を実現
# {"ID":"M005","Name":"Aya","Class":"C"}]を追加する

headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
json_payload = '{"ID":"M005","Name":"Aya","Class":"C"}'  # ここで100回くらいのパターンを試したぞ

print("headers:",headers)
print("payload:",json_payload)

#r = requests.post('http://localhost:8000/Users/',headers=headers, params=payload)
#r = requests.post('http://localhost:8000/Users/', headers=headers,params=json.loads(payload))
r = requests.post('http://localhost:8000/Users/', headers=headers, data=json_payload)

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())

2023,江端さんの技術メモ

結論から先に言うと

json_payload ='{"ID":"M005","Name":"Aya","Class":"C"}'
'"が付いているかどうか、だったというオチでした。

pythonでFastAPIを試す

のように、pythonでFastAPIを使って、外部と内部のインターフェースのテンプレートを作るというのが、目下の課題でした。
多くのサンプルプログラムがあったのですが、私はhttpのGET/POSTができれば十分だったので、他のことは忘れて、そのポイントのみに絞って、ネットを探し回りました。
まずは、FastAPIを受ける側(サーバと言えるのかな)のプログラムは、

Python | FastAPIでAPI作成 ~その6:DELETEでデータ削除 & Pandas活用

を参考(というかコピペ)させて頂きました。

以下のコードを、私の場合はC:\Users\ebata\fastapi4に、index.pyという名前で置きました。
# https://miseruit.com/2022/07/18/post-2939/

from fastapi import FastAPI
from pydantic import BaseModel
import pandas

class Item(BaseModel):
    ID: str
    Name: str
    Class: str

app = FastAPI() #インスタンスを作成

User_list =[
    {"ID":"M001","Name":"Takashi","Class":"A"},
    {"ID":"M002","Name":"Hanako","Class":"A"},
    {"ID":"M003","Name":"Hiroshi","Class":"B"},
    {"ID":"M004","Name":"Kyoko","Class":"B"},
    ]

# joson_normalize関数を用いてJsonデータをDataFrame型に変換します。
df = pandas.json_normalize(User_list)

# Get /Users/ : 全件取得
# Get /Users/{ID} :特定のIDのみ取得
# POST /Users/ :ユーザの登録
# DELETE /Users/{ID} :ユーザの削除

# curl http://localhost:8000/Users/
@app.get("/Users/")
async def users():
    return User_list


# curl http://localhost:8000/Users/M003
@app.get("/Users/{u_id}")
async def users(u_id:str):
    return list(filter(lambda item : item['ID']==u_id, User_list))


# Windowsの場合 
# {"ID":"M005","Name":"Aya","Class":"C"}]を追加する
# curl -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"ID\":\"M005\", \"Name\":\"Aya\", \"Class\":\"C\"}" http://localhost:8000/Users/

@app.post("/Users/")
async def users(user: Item):
    User_list.append({"ID": user.ID,"Name":user.Name,"Class":user.Class})
    return User_list

# curl -X DELETE -H "accept: application/json" http://localhost:8000/Users/M003
@app.delete("/Users/{u_id}")
async def users(u_id:str):
    global df
    df = df.drop(df.index[df["ID"]==u_id])
    return df.to_json(orient='records')
このプログラムは、メモリ上の名簿を検索(GET)、追加(POST)、削除(DELETE)するものです。

C:\Users\ebata\fastapi4> uvicorn index:app --reload

で起動します。
多分、プログラムの方から、「pipでxxxxを入れろ」と文句を言ってきますので、大人しく従います。
プログラム中に記載されている、curlのコマンドを使うとFastAPIの動作を確認できます。
さて、このプログラムで基本動作は確認できますが、基本的にクライアントのプログラムの叩き台がなければ、使えません。モジュールプログラムの中から、curlのコマンドを発行しろというのも乱暴な話です。
ですので、サクッとクライアント(というかFastAPIのアクセス用のテストプログラム)を作成してしまうと思ったのですが、ここで嵌りました。
先ずは、以下のコードをrequest.pyという名前で、C:\Users\ebata\fastapi4に置きました。
(以下のファイルは、動きたてのコードを修正せずに、汚いままで展開しています)
import requests
import json

# テスト
#url = 'https://wp.kobore.net/'
#response = requests.get(url)
#print(response.text)

#print()

print("-----start of get(1)")

# curl http://localhost:8000/Users/ と同じ機能を実施
r = requests.get('http://localhost:8000/Users/')

print(r.url)
print(r.status_code)
print(r.text)
print(r.json())


print("-----end of get(1)")

print()

# curl http://localhost:8000/Users/M003 と同じ機能を実施

print("-----start of get(2)")

r = requests.get('http://localhost:8000/Users/M003')

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())
print("-----end of get(2)")


print()
print("-----start of delete")
# curl -X DELETE -H "accept: application/json" http://localhost:8000/Users/M003と同じ機能を実施
r = requests.delete('http://localhost:8000/Users/M003')

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())

print("-----end of delete")
print()



# curl -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"ID\":\"M005\", \"Name\":\"Aya\", \"Class\":\"C\"}" http://localhost:8000/Users/ と同じ機能を実現
# {"ID":"M005","Name":"Aya","Class":"C"}]を追加する

headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
json_payload = '{"ID":"M005","Name":"Aya","Class":"C"}'  # ここで100回くらいのパターンを試したぞ

print("headers:",headers)
print("payload:",json_payload)

#r = requests.post('http://localhost:8000/Users/',headers=headers, params=payload)
#r = requests.post('http://localhost:8000/Users/', headers=headers,params=json.loads(payload))
r = requests.post('http://localhost:8000/Users/', headers=headers, data=json_payload)

print(r.url)

print(r.status_code)
print(r.text)
print(r.json())
で、
C:\Users\ebata\fastapi4> python request.py
にて、GETとDELETEは問題なくサクっと動いたのですが、POSTだけが思うように動かない
{'detail': [{'loc': ['body'], 'msg': 'value is not a valid dict', 'type': 'type_error.dict'}]}
{"detail":[{"loc":["body"],"msg":"field required","type":"value_error.missing"}]}
エラー番号422
やらが、ワラワラと出てきて、頭を抱えました。
色々検索して調べると『こうやればいい』というアドバイスだけして、サンプルコード(1行で足る)を残さない奴が、世界中には山ほどいます(アドバイスではなく、コードを残して欲しい)。
のコードを真似て、ようやく動かすことができました(本当に助かりました)。
とりあえず、これでサーバとクライアントの対(つい)が完成し、実プログラムに展開できる準備ができたと思います。

以上

2023,江端さんの技術メモ

一回纏めて再学習

FastAPI入門https://zenn.dev/sh0nk/books/537bb028709ab9/viewer/f1b6fc

パラメタの違い

  • パスパラメタ   http://127.0.0.1:8000/items/3
  • クエリパラメタ http://127.0.0.1:8000/items/?skip=0&limit=10
  • リクエストボディ ???

環境構築

 pip install fastapi

pip install sqlalchemy uvicorn

を実施。途中でpythonのバージョンアップしろと言われたので、素直に従いました。

run.py (サーバ立ち上げ用)
urls.py (URLのルーティング用)
controllers.py (レスポンス処理用)
を作りましたが、以下のエラーが出てきました。

> python run.py
Traceback (most recent call last):
  File "run.py", line 1, in <module>
    from urls import app
  File "C:\Users\ebata\fastapi\urls.py", line 1, in <module>
    from controllers import *
  File "C:\Users\ebata\fastapi\controllers.py", line 3, in <module>
    app = FastAPI(
NameError: name 'FastAPI' is not defined

さて、python環境構築の経験もなく、python使うの数年ぶり、ということで、まあ、「ご挨拶」でしょう。

試しに、 https://wp.kobore.net/江端さんの技術メモ/post-7450/ を入れて実行してみたとこ、ちゃんと動くようなので、環境側の問題 と特定しました。

ーーーー

PS C:\Users\ebata\fastapi> uvicorn main:app --reload
ERROR: Error loading ASGI app. Attribute "app" not found in module "m

が出てきたので、いろいろ調べたら、なんとプログラムがセーブされていなかったという間抜けなオチでした。

実施した(履修した)ページ

現時点までの実施事項(続き)

https://fastapi.tiangolo.com/ja/tutorial/body/ の、「クエリパラメータ」まで履修完了。續きは、「リクエストボディ」から。

現在、履修に使っているホルダは、~/fastapi, ~/fasapi2

所感

外部インタフェースについては、Webや他の手段でAPIを叩くことができると思うが、モジュール間の関数にfastapiを使うことは可能だろうか。

原理的には、curl等で送り込めばば、何でも可能と思うが、相互通信を考えるとwebsocketを剥き出しにした方がよいのではないだろうか?

ちなみに、FastAPIでPostgresqlのDBアクセスをやろうと思ったけど、

$ pip install psycopg2

がどうしてもインストールできずに、断念(くやしい)

以上

2023,江端さんの技術メモ

package main

import (
	"fmt"
)

// (1)
var m = make(map[int]int)  // (2)をコメントアウトするならこっちを使う

func main() {

	// (2)
	//m := map[int]int{} // (1)をコメントアウトするならこっちを使う

	m[3124] = 9
	m[1992] = 2
	m[2020] = 3

	// キーのみ取り出す
	for key := range m {
		fmt.Println(key)
	}

	//3124
	//1992
	//2020

	fmt.Println()

	// キーと値
	for key, value := range m {
		fmt.Println(key, value)
	}
	fmt.Println()

	//1992 2
	//2020 3
	//3124 9

	// 値のみ必要な場合
	for _, value := range m {
		fmt.Println(value)
	}

	//9
	//2
	//3

	fmt.Println()

	// ループの回数を数える
	i := 0
	for key, value := range m {
		fmt.Println(key, value)
		i++
	}

	//3124 9
	//1992 2
	//2020 3

	fmt.Println()
	fmt.Println("delete(m, 1992)")
	delete(m, 1992)
	for key, value := range m {
		fmt.Println(key, value)
	}

	//delete(m, 1992)
	//2020 3
	//3124 9


	fmt.Println()
	fmt.Println("add as m[2999] = 2")
	m[2999] = 2
	for key, value := range m {
		fmt.Println(key, value)
	}

	//add as m[2999] = 2
	//3124 9
	//2999 2
	//2020 3

	_, ok := m[100]
	if ok {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

	// NG

	_, ok = m[2999]
	if ok {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

	// OK

	fmt.Println(m)
	// map[2020:3 2999:2 3124:9]

	m[2020]++

	fmt.Println(m)
	// map[2020:4 2999:2 3124:9]



}

2023,江端さんの技術メモ

問題

現在、192.168.0.23/postgresにアクセスを確認

で、A5:SQL Mk-2は接続成功している様子

だが、publicの下の「テーブル」「ビュー」などを叩いても何も出てこない。

念のためpgAdAdmin4 で試してみたが、こちらも問題なし

解決(T研究所のKさんに教えて貰いました)

『データベースを登録する際に、データベース名に「agent_db」 を指定していただく必要があります』

 

参考文献

https://izit-infobox.net/blog/2021/07/15/a5sql-mk-2/

2023,江端さんの技術メモ

研究室の学生さんたちに負荷テストに協力してもらっています。

今、"cannot parse invalid wire-format data" のエラーでGTFS_HUBのダウンを確認しました。

PrumeMobileの方は、そのままにして、GTFS_HUBの再起動をかけました。

(以下、後日対応)

2023,江端さんの技術メモ

"github.com/gorilla/websocket"の有名なバグ(らしい)

■PruneMobile側(sever.go)

■gtfs_hub側(gtfs_hub.go)

■対策

[Go言語/JavaScript]WebSocketsでチャットアプリを実装!~ユーザーリスト表示編~


PruneMobile側(sever.go)の方が先に落ちる(確認) → で書き込みできなくなってgtfs_hub側(gtfs_hub.go)も落ちる、と、多分、そんな感じ。

"repeated read on failed websocket connection"を普通に読めば、「間違ったWebsocket接続で、*何度*も読み込みを繰り返している」なので、read errorが発生したら、即コネクションクローズして、クライアントも落してしまえばいいのかな、と考えました。→ ダメでした。サーバが落ちて処理が止まります。

で、以前、

Golangのサーバなんか大嫌い

で、

"http.HandleFunc()"は、クライアントがやってくるごにに一つづつ立ち上があるfork()のようなものである

てなことを書いていたので、read errorqを検知したら、forループから直ちにbreakして、クライアントをクラッシュさせてしまうこと(ができるものと)しました。→ ダメでした。サーバが落ちて処理が止まります。

 

for (){
      (色々)
           err = webConn.ReadJSON(&locMsg2) // ここでPanic serving  ... repeated read on failed websocket connectionが発生している可能性あり
           fmt.Printf("c6: ")
           if err != nil{
              fmt.Printf("Before webConn.close(), After c6:")
               webConn.Close()
               break → ダメ
           }
}

javascript (onload.js) の方にも、終了理由がでるように、以下のコードを追加しました。

// サーバを止めると、ここに飛んでくる
  socket.onclose = function (event) {
      console.log("socket.onclose");
  
      let obj = JSON.parse(event.data);
  
      console.log("socket.onclose: obj.id:", obj.id);
      console.log("socket.onclose: obj.lat:", obj.lat);
      console.log("socket.onclose: obj.lng:", obj.lng);
      console.log("socket.onclose: obj.type:", obj.type);
      console.log("socket.onclose: obj.popup:", obj.popup);
  
      socket = null;
  
      // 切断が完全に完了したかどうか
      if(event.wasClean){
          var closed = "完了";
      } else {
          var closed = "未完了";
      }
      info.innerHTML += "切断処理:" + closed + "<br>";
      info.innerHTML += "コード:" + event.code + "<br>";
      info.innerHTML += "理由:" + event.reason + "<br>";
  
  }
  
  window.onunload = function(event){
      // 切断
      ws.close(4500,"切断理由");
  }

上記の「ダメ」の対策中

https://ja.stackoverflow.com/questions/12389/golang%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E5%86%8D%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%99%E3%82%8B%E3%81%A8websocket-server%E3%81%8C%E8%90%BD%E3%81%A1%E3%82%8B


■問題解決編

色々問題があったのですが、順番に説明していきます。


	var addr = flag.String("addr", "192.168.0.8:8080", "http service address") // テスト
	....
	.....
	http.Handle("/", http.FileServer(http.Dir(".")))


	http.HandleFunc("/echo", echo)                                           // echo関数を登録 (サーバとして必要)
	log.Fatal(http.ListenAndServeTLS(*addr, "./cert.pem", "./key.pem", nil)) // localhost:8080で起動をセット
}

まず第一に、http.HandleFunc()の誤解がありました。私は、これを、fork()のようにプロセスかスレッドを発生さるものと思っていましたが、これは、一言で言えば、単なるコールバック関数でした。

乱暴に言えば、Webからアクセスがあると、echo()というファンクションに吹っ飛ばされる、という現象を発生させる"だけ"で、それ意外のことは何にもしてくれないのです。

では、echo()の方はどうなっているかというと、

func echo(w http.ResponseWriter, r *http.Request) { // JavaScriptとの通信
	webConn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("websocket connection err:", err)
		return
	}
	defer webConn.Close()

とすることで、webSocket通信の準備ができて、その通信路が"webConn"にできる、ということです。
で、それ意外のことは何もしてくれないのです。

つまり、2つのWebブラウザからアクセスがくると、その度、echo()に飛んできて、その度に、異なるwebConnを生成する、ということです。

ここまでの説明で分かると思いますが、つまり、Webブラウザがくる度に、それをどっかに格納しておかないと、通信路の情報が上書きされてしまいます

なので、基本的には、以下のwebConnを、配列に格納しておきます。

var clients = make(map[*websocket.Conn]bool) // 接続されるクライアント
という動的な配列を準備しておき、アクセスがある度に、
// クライアントを新しく登録(だけ)
    m1Mutex.Lock() // 同時書き込みを回避するため、ミューテックスロックで囲っておく
    clients[webConn] = true  // これで、Webからのアクセスがある度に、通信路情報が動的に追加される
    m1Mutex.Unlock()

というように、Webブラウザとの通信路を別々に管理しておきます。

もし、fork()みたいなことがしたいのであれば、goroutineを起動して、そのgoroutineにWebブラウザの通信路を明け渡す必要があります。それでも、通信路の全体管理は、echo()が握っているので、webConnを消滅されたい場合は、echo()の方でやって貰う必要があります。

この方法を実現する方法として、GO言語のサンプルプログラムで良く登場するのが、以下のような方法です。

// 江端のPCでは、c:\users\ebata\test20230412\main.go

package main

(中略)

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // 送られてきたGETリクエストをwebsocketにアップグレード
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    // 関数が終わった際に必ずwebsocketnのコネクションを閉じる
    defer ws.Close()

    // クライアントを新しく登録
    clients[ws] = true

    for {
        var msg Message
        // 新しいメッセージをJSONとして読み込みMessageオブジェクトにマッピングする
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("error: %v", err)
            delete(clients, ws)
            break
        }
        // 新しく受信されたメッセージをブロードキャストチャネルに送る
        broadcast <- msg
    }
}

func handleMessages() {
    for {
        // ブロードキャストチャネルから次のメッセージを受け取る
        msg := <-broadcast
        // 現在接続しているクライアント全てにメッセージを送信する
        for client := range clients {
            err := client.WriteJSON(msg)
            if err != nil {
                log.Printf("error: %v", err)
                client.Close()
                delete(clients, client)
            }
        }
    }
}

上記の例では、 http.HandleFunc("/ws", handleConnections) で、handleConnectionsをコールバック関数にしておき、こちらにWebブラウザからのアクセスを全部任せます

でもって、ついでにWebからやってくる通信の全てを受けつけています(ReadJSON())。さらに、webブラウザが突然閉じられた場合などは、通信エラーとして検知して、それをクローズした後に、webConnの配列から取り除いています。これで、このブラウザからの通信路は閉じらて、消されます。

で、この通信の内容を、チャネルを使って、go handleMessagerで作った、fork()みたいなgoroutineに流し込んでいます。

結論として、fork()みたなことがやりたければ、「自力で作れ」ということになります


しかし、WebSocket単位で別々のgoroutine作るのも面倒くさいです。それに、もしそのようなgoroutineを作ったとすれば、ブロードキャストを実現しなければなりませんが、チャネルには、ブロードキャスト機能がありません(どうしても使わなければならない時は、私はredisを使っています)。

ですので、私、チャネルの配列を作って疑似的なブロードキャストを実現しようとしたのですが、このサンプルプログラムが見るからなくて、困っていました。

『echo()関数の中で、全てのWebコネクションを相手にできないかな』と考え始めました。

基本形はこんな形

for {
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           client.WriteJSON(message)
           client.ReadJSON(message2}
     }
}

問題は、Webブラウザは終了処理などなどをせずに、閉じられてしまうことにあります(私たちが普通にやっていることです)。

とすれば、echo()の中でコネクションの切断を検知して、それをWebConnのリスト から取り除く必要があります。

で、こんなことやってみたんですよ。

for {
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           err = client.WriteJSON(message)
           if err != nil{
               client.Close()
               delete(client, clients)
           err =  client.ReadJSON(message2}
           if err != nil{
               client.Close()
               delete(client, clients)
     }
}

これでは、for ルーチンの中で、回している変数を減らすという処理が不味かったようで、この場合、 echo()が終了してしまうようでした。当然、repeated.....も含めて、エラーの嵐になりました。

なので、こんな風に変更しました。


var delete_client *websocket.Comm
for {
     delete_client = nil
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           err = client.WriteJSON(message)
           if err != nil{
               delete_client = client
           }
           err =  client.ReadJSON(message2}
           if err != nil{
               delete_client = client
           }
     }
     if delete_client != nil{
        delete_client.Close()
        delete(clients, delete_client)
}

つまり、ループの外でコネクション処理を行う、ということです。
ただ、この方法では、ループを回っている途中に2つ以上のWebブラウザが落された場合にどうなるか、という問題が残ります。
この場合、「次のループでエラーを検知するのを待つ」という処理で対応する、と腹を括りました。
なぜなら、repeat..... のエラーは、積り積って発生することを経験的に知っていましたので、それまで通信障害したまま走って貰えばいい、と割り来って考えることにしました。


結論としては、

(1)http.HandleFunc()は、Webからの接続要求時に、飛される先の関数を記述するもの(コールバック関数)であり、
(2)そのコールバック関数の中だけで、入出力処理がしたいのであれば、ループを壊さないような工夫をして、上手く運用すれば、上手くいく(ことがある)

という結論になりそうです。