はじめてのwebアプリ④ アップロードファイルの扱い方
ファイルを読み込んで文字数を数える機能を追加する
文字数カウントするwebアプリも入力テキストを処理するだけなら出来上がったので、今度はファイルをアップロードしてカウントする機能を追加することにした。
(画面イメージ)
ファイルアップロードの結果として
これらを画面に表示する。
ここまでは比較的悩むことなく簡単に進めてこられたが、アップロードファイルを扱う必要が生じて、途端に進みが遅くなった。
色々ハマって一応完成したので、学んだことを以下記す。
ファイルアップロードに対応
リクエストメソッドはPOST, enctype="multipart/form-data"を指定
取り敢えず画面からファイルをアップロードしなければならないので、
<form action="/count/file" method="POST" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit"> </form>
このように、リクエストメソッドはPOST、enctypeは"multipart/form-data"にしておく。
http.Request.FormFileからアップロードされたファイルを取得
次に、サーバー側の処理でアップロードされたファイルを取得、利用する方法だが、リクエストがFormFileというメソッドを持っているのでこれを使うことにした。
FormFileはキーを指定するとファイルを返してくれるが、その際、データをhttp.defaultMaxMemoryまで自動的にメモリに読み込んで、残りを一時ファイルに保存する。
FormFileの該当箇所
https://golang.org/src/net/http/request.go?s=34318:34403#L1118
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) { if r.MultipartForm == multipartByReader { return nil, nil, errors.New("http: multipart handled by MultipartReader") } if r.MultipartForm == nil { err := r.ParseMultipartForm(defaultMaxMemory) if err != nil { return nil, nil, err } } (後略)
ファイルの後始末
FormFileの処理がわかったのは良いとして、一時ファイルが作られたときは後で削除する必要があるが、それはどう対処するのだろうか。
これには、RequestにMutipartForm.RemoveAll()というお誂え向きのメソッドがあった。
ソースを見ると、一時ファイルがあれば消す、と賢く処理してくれるようだ。
RemoveAllの該当箇所
https://golang.org/src/mime/multipart/formdata.go?s=2602:2634#L102
func (f *Form) RemoveAll() error { var err error for _, fhs := range f.File { for _, fh := range fhs { if fh.tmpfile != "" { e := os.Remove(fh.tmpfile) if e != nil && err == nil { err = e } } } } return err }
そこで、deferに無名関数を指定し、
- file.Close() → アップロードファイルを閉じる
- r.MultipartForm.RemoveAll() → 一時ファイルが作成されていれば、削除
を呼ぶことにした(以下)。
file, _, err := r.FormFile("file") if err != nil { http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest) } defer func() { file.Close() r.MultipartForm.RemoveAll() }()
ファイルから正常なruneの個数を数えたい
今までは文字列の中の文字数やバイト数を数えれば良いので簡単だったのだが、今度はファイルを見なければならない。
せっかくファイルを処理するのだから、全データを[]byteの変数に取り出してカウントするといった無駄なことはしたくないものだ。
最初、unicode/utf8を使って何とかしようと試みたがうまくいかず、色々調べた結果、実はbufioのReaderがReadRuneというメソッドを持っているのでこれを使えば良いことが判った。
ReadRuneの該当箇所
https://golang.org/src/bufio/bufio.go?s=6027:6084#L250
func (b *Reader) ReadRune() (r rune, size int, err error) { for b.r+utf8.UTFMax > b.w && !utf8.FullRune(b.buf[b.r:b.w]) && b.err == nil && b.w-b.r < len(b.buf) { b.fill() // b.w-b.r < len(buf) => buffer is not full } (後略)
このように、正常なUTF-8の最大バイト数UTFMax(=4)まで読み取って正常なUnicodeコードポイント(rune)であるか調べて文字を返却してくれる。
なお、utf8にはruneを扱う上で必要な定数がちゃんと定義されているので覚えておくと良いかもしれない。
https://golang.org/pkg/unicode/utf8/#pkg-constants
const ( RuneError = '\uFFFD' // the "error" Rune or "Unicode replacement character" RuneSelf = 0x80 // characters below Runeself are represented as themselves in a single byte. MaxRune = '\U0010FFFF' // Maximum valid Unicode code point. UTFMax = 4 // maximum number of bytes of a UTF-8 encoded Unicode character. )
サンプルコード
最終的に、プログラムは以下のようになった。
type Context map[string]interface{} func FileWordCountHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "POSTでアクセスしてください", http.StatusMethodNotAllowed) return } file, _, err := r.FormFile("file") if err != nil { http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest) } defer func() { file.Close() r.MultipartForm.RemoveAll() }() in := bufio.NewReader(file) count := 0 bc := 0 invalid := 0 for { r, size, err := in.ReadRune() if err == io.EOF { break } if r == utf8.RuneError { invalid += size } else { bc += size // 正常な文字だけカウントする。 count++ } } wc, _ := htmlTemplate.New("file_wc").Parse(` <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>文字数カウント結果</title> <style type="text/css"> .err { border: solid 1px red; } </style> </head> <body> <h1>文字数カウント結果</h1> 文字数は: {{.count}}<br> バイト数は: {{.bc}}<br> 不正なバイト数は: {{.invalid}}<br> でした。<br><br> 文字を入力してください。 <form action="/count" method="GET"> <input type="text" name="text" size="32"> <input type="submit"> </form> ファイルを調べたい場合はこちら。 <form action="/count/file" method="POST" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit"> </form> </body> </html>`) data := Context{ "count": count, "bc": bc, "invalid": invalid, } if err := wc.Execute(w, data); err != nil { log.Panic(err) } }
はじめてのwebアプリ③ JSONで結果を出力する
JSONで出力する機能を追加する
はじめてのwebアプリのバグを直したところで、今度はJSONで結果を返す機能も追加してみる。
/countにformat=jsonで問い合わせが来たら、結果をJSONで返却する。
ハンドラ関数を分岐
まずハンドラ関数を分岐する。
func WordCountHandler(w http.ResponseWriter, r *http.Request) { if r.FormValue("format") == "json" { JSONWordCountHandler(w, r) } else { HTMLWordCountHandler(w, r) } } func JSONWordCountHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") // ...以下略 } func HTMLWordCountHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") // ...以下略 }
json.Marshalは渡された構造体の公開フィールドしか出力しないので注意
Goのオブジェクトをjsonのテキストに出力する場合はencoding/jsonモジュールのMarshalを使う。
但し、利用には注意点がある。
小文字で始まるキーでJSONを出力したければタグを指定する
Marshalには構造体を渡すのだが、ここで渡す構造体の中で公開フィールドしか出力してくれない。
つまり、先頭が大文字で始まる名前のフィールドだけが出力対象となる。
最初、わたしは構造体を以下の通り定義したため、結果が"{}"、空のmapで出力されてしまった。
[ダメな例]
type WordCount struct { text string count int } result, err := json.Marshal(WordCount{text, count}) if err != nil { log.Panic(err) } w.Write(result)
[ブラウザで問い合わせると…]
↑この通り、空のmapが出力されて、NG。
[フィールドの名前を大文字で始めると]
type WordCount struct { Text string Count int } result, err := json.Marshal(WordCount{text, count})
[ブラウザで問い合わせると…]
↑今度はちゃんとデータが出力された、、が、キーが大文字で始まるのはちょっと・・
Goには「タグ」という仕組みがあって、構造体のフィールド宣言の後ろに文字列を書いておくとものによってはよきにはからってくれる。
json.Marshalはタグを見てくれるので、小文字のキーで出力するよう指定する。
[タグ付きでMarshal]
type WordCount struct { Text string `json:"text"` Count int `json:"count"` } result, err := json.Marshal(WordCount{text, count})
[ブラウザで問い合わせると…]
↑今度は小文字でキーが出力され、期待通りの結果となった。
文字数とバイト数のふたつを返すよう変更
ついでに文字数の他にバイト数も返却するよう変更する。
さっきのMarshalに渡す構造体をちょっといじるだけ。
text := r.FormValue("text") count := utf8.RuneCountInString(text) bc := len(text) // json.Marshalは構造体の公開フィールドしか出力してくれないので注意。 // 小文字でJSONのキーを出力したければタグを指定する。 type WordCount struct { Text string `json:"text"` Count int `json:"count"` ByteCount int `json:"byte_count"` } result, err := json.Marshal(WordCount{text, count, bc}) if err != nil { log.Panic(err) } w.Write(result)
出力結果にバイト数が追加されるようになった。
はじめてのwebアプリ② unicode/utf8で文字数を数える
バグを直す
文字数カウントのwebアプリは、以下のバグがあるので直す。
- 文字数ではなくバイト数を算出している
- HTMLのエスケープをしていない
このため、
<font color="red">𩸽</font>
上記文字列を入力すると、スクリーンショットの通り、fontタグが適用されて文字が赤字表記になり、なおかつ本当は26文字と数えて欲しいにもかかわらず29文字と数えてしまう。
文字数ではなくバイト数を算出している
これは前に学んだunicode/utf8のRuneCountInStringで数を調べるよう改める。
[before]
text := r.FormValue("text") count := len(text)
[after]
// import "unicode/utf8" text := r.FormValue("text") count := utf8.RuneCountInString(text)
HTMLのエスケープをかける
次に、HTMLのエスケープをかけるため、html/templateのHTMLEscapeStringを使うことにした。
[before]
content := fmt.Sprintf(`...HTML本文`, text, count, css)
[after]
// import htmlTemplate "html/template" content := fmt.Sprintf(`...HTML本文`, htmlTemplate.HTMLEscapeString(text), count, css)
text/templateモジュールでHTMLを出力するよう変更
html/templateをよく見たら、HTMLEscapeStringを使わなくても、テンプレート機能を利用すれば出力の式({{.名前}})で自動エスケープしてくれるようだ。
なので、以下の通り改めた。
// import htmlTemplate "html/template" // import "log" wc, _ := htmlTemplate.New("wc").Parse(` <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>文字数カウント結果</title> <style type="text/css"> .err { border: solid 1px red; } </style> </head> <body> <h1>文字数カウント結果</h1> 入力文字: {{.text}}<br> 文字数は: {{.count}}<br> でした。<br><br> 文字を入力してください。 <form action="/count" method="GET"> <input type="text" name="text" size="32" class="{{.css}}"> <input type="submit"> </form> </body> </html>`) type Context map[string]interface{} data := Context{"text": text, "count": count, "css": css} if err := wc.Execute(w, data); err != nil { log.Panic(err) }
これでHTMLのタグはエスケープされ、文字数も正しく26を数えてくれるようになった。
はじめてのwebアプリ① net/httpのHandleFunc
文字数カウンターを作る
Goも基本的な構文をだいたい覚えたので簡単なwebアプリを作ってみる。
net/httpパッケージの使い方を覚える目的で、テキストを入力して文字数を数えて返す画面を作ることにする。
Routeの追加: http.HandleFunc
httpにはRouteの追加方法としてHandleとHandleFuncが用意されている。
今回はHandleFuncを使ってトップページとワードカウントのページのRouteを追加してみる。
コードのイメージとしては、以下。
func init() { // URLパターンに正規表現は渡せない。 http.HandleFunc("/count", WordCountHandler) // 順番に注意。"/"を先頭に指定すると他のPathがマッチしない。 http.HandleFunc("/", TopPageHandler) }
Handle, HandleFuncの第1引数に正規表現は渡せない
どうもnet/httpパッケージのHandleやHandleFuncは単なる文字列の前方一致で処理する模様。
ドキュメントでは引数の名前が"pattern"と書いてあるので正規表現を受け付けてくれるのかと思ったが、違うようだ。
(ただ、最も基本的なAPIは余計なことをしないに限るので、これで良いと思う)
正規表現で処理したい場合はHandleの引数に"/"など広くマッチするPathを指定し、http.Requestから取得可能なURL.Pathを調べて目的の処理に取り次ぐ。
以下、例
func init() { http.HandleFunc("/", Index) } func Index(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/count" { WordCountHandler(w, r) } else { TopPageHandler(w, r) } }
この辺のコントローラー層を便利にやってくれるフレームワークは色々あるようだが、取り敢えず使わず基本に忠実に行くことにする。
レスポンスボディの書き込み
わたしはHandleFuncの引数に渡されるResponseWriterのWriteを使った(以下)、
func WordCountHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") // 1. テンプレートエンジンを使って処理するよう変更 // 2. JSONで返却する機能を追加 // 3. ファイルを読み取って文字数を数える機能を追加 // 4. 文字数とバイト数を数えるよう変更 // 5. ユニットテストを追加 // 6. 最初のプログラムだと文字数ではなくバイト数を返してしまうので直す // 7. logをファイルに出力するよう変更 text := r.FormValue("text") count := len(text) css := "" if count == 0 { css = "err" } content := fmt.Sprintf(` <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>文字数カウント結果</title> <style type="text/css"> .err { border: solid 1px red; } </style> </head> <body> <h1>文字数カウント結果</h1> 入力文字: %v<br> 文字数は: %v<br> でした。<br><br> 文字を入力してください。 <form action="/count" method="GET"> <input type="text" name="text" size="32" class="%v"> <input type="submit"> </form> </body> </html>`, text, count, css) w.Write([]byte(content)) }
しかし、Goのドキュメントの例を見ると、fmt.Fprintfを使っていたので、簡単な文字列出力だったらこっちの方が確かに手軽だなぁなどと納得。
https://golang.org/pkg/net/http/#pkg-overview
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) })
Pythonでサロゲートペア -- ほっけの逆襲
Pythonでサロゲートペアを処理するとどうなるか
http://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0
既に他の記事で上記記事に登場する「𩸽」(ほっけ)というサロゲートペアの文字をGoで扱う話しを書いたが、Pythonで処理するとどうなるのか試してみた。
Python2は寛容
Python2は3に比べて寛容である。printに渡しても取り敢えずエラーにならず表示される。
>>> print u'\uD867\uDE3D' 𩸽 >>> x = u'\uD867\uDE3D' >>> y = u'𩸽'
但し、表示される文字は同じだが、文字列比較では同値ではないと評価される
>>> x == y False
更に不思議なことに、こちらはordが結果を返すのに
>>> ord(y) 171581 こちらはエラーになってしまう >>> ord(x) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: ord() expected a character, but string of length 2 found
http://docs.python.jp/2/library/functions.html#ord
ord(c)(原文)
長さ 1 の与えられた文字列に対し、その文字列が unicode オブジェクトならば Unicode コードポイントを表す整数を、 8 ビット文字列ならばそのバイトの値を返します。たとえば、 ord('a') は整数 97 を返し、 ord(u'\u2020') は 8224 を返します。この値は 8 ビット文字列に対する chr() の逆であり、 unicode オブジェクトに対する unichr() の逆です。引数が unicode で Python が UCS2 Unicode 対応版ならば、その文字のコードポイントは両端を含めて [0..65535] の範囲に入っていなければなりません。この範囲から外れると文字列の長さが 2 になり、 TypeError が送出されることになります。
サロゲートペアはlenで長さを調べると、「2」が返却される。
※unicode型をlenに渡しているので「文字数」を取得するはずが、「2」を返す点が問題。
str型をlenに渡すとバイト数を返すのでここでの問題対象外。
>>> len(x) 2 >>> len(y) 1
参考:
>>> len(u'あ') # unicode型: 文字数 1 >>> len('あ') # str型: バイト数 3
「Unicode番号から文字を生成する」
当然、chrに渡すと範囲外なのでエラー。
>>> chr(0x29E3D) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: chr() arg not in range(256
unichrは正しく値を返してくれる。
>>> unichr(0x29E3D) u'\U00029e3d' >>> print unichr(0x29E3D) 𩸽
しかも、他の文字を試すとUbuntu16のVMでは化けて表示されるが、Macだとエラーになった。
★Ubuntu16.04のVM
★El CapitalのMac
Python3だと扱いが難しくなる
Python3ではそもそもu'\uD867\uDE3D'をprintに渡すとエラーになる。
Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'utf-8' codec can't encode character '\ud867' in position 0: surrogates not allowed
Python3は、最初からサロゲートペアを許さないポリシーのようだ。
そのため、'surrogatepass', 'suroogateescape'といったエラーオプションを指定して何とか解決する。
具体的には以下stackoverflowの回答の通り、UTF-16でサロゲートペアを許可した状態で一度byteに変換し、strに戻すと大丈夫らしい。
How to work with surrogate pairs in Python? - Stack Overflow
>>> "\ud867\ude3d".encode('utf-16', 'surrogatepass').decode('utf-16') '𩸽' |< ちなみに、Unicodeコードポイントでリテラル表記してあげれば大丈夫である。 >|text| >>> print('\U00029e3d') 𩸽
サロゲートペアの入った文字列を処理する場合の注意点
サロゲートペア
「𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。」
ほっけの ひらきを いざかやで ちゅうもんして、いちじかん たつが まだこない。 ぞくな きゃくが てんいんを ひきとめて なじる からだ。
年末の東京らしい風景を彷彿とさせるこの一文にはサロゲートペアの文字がふたつ含まれている。
ひとつは「𩸽」という文字、
もうひとつは「𠈻」という文字である。
JavaScriptでのサロゲートペア文字列のメモ - Qiita
この記事に詳しく書かれているが、サロゲートペアは通常の文字列処理が通用しない厄介者である。
Goの文字列の扱い
Goでは文字をruneという型で処理する決まりらしい。
いつもの『プログラミング言語Go』の記述を引用すると、
Unicodeは世界のすべての書記体系のすべての文字、アクセントや他の発音区別記号、タブやキャリッジリターンなどの制御コード、多数の難解な文字を集めており、各文字にUnicodeコードポイント(Unicode code point)あるいはGo用語ではルーン(rune)と呼ばれる規格番号を割り当てています。
つまり、文字をruneとして取り出すと、番号で管理するそうだ。
実際、「ほっけ」と「ぞく」の文字をruneのリテラルで表すと、
package main import "fmt" func main() { var hokke, zok rune hokke, zok = '\U00029E3D', '\U0002023B' fmt.Printf("%c, %c", hokke, zok) // 𩸽, 𠈻 }
このように、16進数の番号を書く。
stringをrangeに渡すと[]runeに置き換えてくれる
Goでは文字列をrangeに渡すとちゃんとruneのスライスに置き換えてくれる。
このため、「一文字ずつ」ループを当たり前のようにできる。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" for _, x := range exp { fmt.Printf("%c", x) } fmt.Println()
これで変数expに書いた通りの文章がコンソールに出力される。
もしこれをbyteのスライスにかえてしまうと都合が悪い。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 for _, x := range []byte(exp) { fmt.Printf("%c", x) }
こっちを動かすと、画像のようにめちゃくちゃな出力になってしまう。
utf8.DecodeRuneを使うとrange同様、安全に文字を取り出せる。
// import "unicode/utf8" exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" var xs []byte xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeRune(xs) fmt.Printf("%c", r) xs = xs[size:] } fmt.Println()
lenで文字数を数えたいか、バイト数を数えたいか
冒頭の文は全部で46文字あるが、単純に文字列をlenで調べてしまうとバイト数を返す。
これは、Pythonでlen(unicode)は文字数、len(str)はバイト数を返す話と似ている。
正しく文字数を取得したい場合は[]runeにキャストしてlenに渡すか、utf8.RuneCountInStringを使う。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 fmt.Println(exp) fmt.Println( len(exp), // 138 utf8.RuneCountInString(exp), // 46 len([]rune(exp))) // 46
サンプルコード
package main import ( "fmt" "unicode/utf8" ) func main() { exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 fmt.Println(exp) fmt.Println( len(exp), // 138 utf8.RuneCountInString(exp), // 46 len([]rune(exp))) // 46 for _, x := range exp { fmt.Printf("%c", x) } fmt.Println() // ここで改行を挟まないと何故か1回めのループの「𩸽」の文字が化けて表示される。 var xs []byte xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeRune(xs) fmt.Printf("%c", r) xs = xs[size:] } fmt.Println() // DecodeLastRuneだと逆から反復 xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeLastRune(xs) fmt.Printf("%c", r) xs = xs[:len(xs)-size] } }
go installを使え -- QCon London 2016
2016年版Goのベストプラクティス
日本語訳(POSTD)
6年間におけるGoのベストプラクティス | プログラミング | POSTD
原著
Go best practices, six years in
こんな記事を見つけて読んでいた。
内容は15項目にもわたるプラクティスの紹介なのだが、今回、特に興味を持ったのが以下だった。
1. $GOPATH/binを$PATHに追加しましょう。インストールしたバイナリが扱いやすくなります。
2. リポジトリfooが主にバイナリ用なら、ライブラリコードはサブディレクトリlib/に置き、fooパッケージと名付けましょう。
3. リポジトリが主にライブラリ用なら、バイナリはcmd/配下の個別のサブディレクトリに置きましょう。
15. go buildよりもgo installを使いましょう。
GOPATH/binにPATHを通し、go installでビルドする
まず、15番の「$GOPATH/binを$PATHに追加 & go buildよりもgo installを使いましょう」という話に興味を持った。
今まで「go build」を実行するとバイナリが生成されることは知っていたが、「go install」というコマンドの存在を知らなかった。
「go install」を実行すると$GOPATH/binの下にバイナリが配置されるので、PATHを通しておけばいつでも実行できて便利ですよ、という意図らしい。
リポジトリ構成はgo getに対応を心がける
以下、バイナリとライブラリを作る場合でリポジトリ構成を変えた方が良いらしい。
[引用] バイナリを作る場合
github.com/peterbourgon/foo/ main.go // package main main_test.go // package main lib/ foo.go // package foo foo_test.go // package foo
[引用] ライブラリを作る場合
github.com/peterbourgon/foo foo.go // package foo foo_test.go // package foo cmd/ foo/ main.go // package main main_test.go // package main
この構成案は既に過去の記事で調べていたが、出典はこのカンファレンスだったのかという感想。
go getに対応したリポジトリ構成に、go installを加えることでシンプルに開発環境を構築することができそうだ。
go installを使えば、bin/の下にバイナリが配置されるので、go buildで生成したバイナリを誤ってコミットしてしまうミスも減ると思った。