Goのbufio.Scannerは入力データの1行の長さが一定以上になるとスキャンをやめてしまう

Goのbufio.Scannerの落とし穴について。

概要

Goのbufio.Scannerはio.Readerを一行ずつ読み込んで行く時に非常に便利なライブラリなのだけど、タイトルの通り、入力データ(io.Reader)の1行の長さがScannerのバッファサイズを超えるとスキャンをやめてしまうという問題がある。バッファサイズはデフォルトでbufio.MaxScanTokenSize(65536)バイトとそれほど大きくないので、例えば大きめのCSVや各行にJSONが書かれているテキストファイルをScannerで処理するとこの問題に当たることがあるかもしれない。

以下は動作確認用コード。Go Playground上で実行してみたい方はこちらをどうぞ。

package main

import (
    "bufio"
    "fmt"
    "strings"
)

func main() {
    // 2行目が65537バイト(改行含む) > bufio.MaxScanTokenSize (65536)
    in := strings.NewReader("1st line\n" + strings.Repeat("X", 65536) + "\n3rd line\n")
    scanner := bufio.NewScanner(in)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        fmt.Printf("Scanner error: %q\n", err)
    }
}

実行結果

1st line
Scanner error: "bufio.Scanner: token too long"

この場合、2行目が大きすぎてScannerのバッファに格納できないので、Scanner.Scan()がfalseを返してしまう。一応スキャン後にScanner.Err()を確認すればbufio.ErrTooLongが返ってくるので問題を検出することはできるが、わざわざErr()を手動で呼ぶ必要があるのが難点(実際Scan()後にErr()を確認しないコードを何度か見たことがある)。

対策

入力データの1行の最大長が事前に分かっている場合は、bufio.Scanner.Buffer()でバッファサイズを変更するとよい。

const (
    // 初期バッファサイズ
    initialBufSize = 10000
    // バッファサイズの最大値。Scannerは必要に応じこのサイズまでバッファを大きくして各行をスキャンする。
    // この値がinitialBufSize以下の場合、Scannerはバッファの拡張を一切行わず与えられた初期バッファのみを使う。
    maxBufSize = 1000000
)

scanner := bufio.NewScanner(in)
buf := make([]byte, initialBufSize)
scanner.Buffer(buf, maxBufSize)

入力行の最大長が事前に分からず、最大長を決め打ちできない場合は、Scannerを使うことはできない。代わりに以下のようにbufio.Reader.ReadBytes()などを使って1行ずつ読み込むという方法がある(Go Playgroundはこちら)。

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)

func main() {
    // 2行目が65537バイト(改行含む) > bufio.MaxScanTokenSize (65536)
    in := strings.NewReader("1st line\n" + strings.Repeat("X", 65536) + "\n3rd line\n")
    reader := bufio.NewReader(in)

    for {
        line, err := reader.ReadBytes('\n')
        if err != nil && err != io.EOF {
            fmt.Printf("Reader error: %q\n", err)
            return
        }

        // ReadBytes()がdelimiter('\n')を見つける前にEOFに到達した場合、
        // それまでに読み込まれた行のバイト列とio.EOFが返される。
        // 従って、入力の最後の行が'\n'で終わらない場合、err == io.EOFだけ確認してループをbreakしてしまうと
        // 最後の行を処理せず捨ててしまうことになるので注意。
        allLinesProcessed := err == io.EOF && len(line) == 0
        if allLinesProcessed {
            break
        }

        // ReadBytesが返したバイト列はdelimiter('\n')を含む
        print(string(line))
    }
}

実行結果

1st line
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (省略)
3rd line

一応目的は達成できるが、最終行が'\n'で終わらない場合を加味する必要があったりとScannerよりもコードが複雑化するので注意。

はてなブログに移行しました

今更ですがはてなダイアリーからはてなブログに移行しました。はてなダイアリーの新規開設受付が終了してこのままはてなダイアリーを使い続けていても近い将来にはてなブログに移行させられそうだったので、余裕があるうちにやっておきました。

旧ダイアリーへのリンクやはてブは全てこちらのブログにリダイレクトされます。一応便利。

どれくらいの頻度で更新するか分かりませんが、今後ともよろしくお願いします。最近はGoやTensorFlowにハマっているので、それらの記事が書けたらいいかなーと思っています。

雨予報bot「mickey24_bot」の使い方 ver 3.0

mickey24_botは主に以下の機能を備えているTwitter botです。

どんなbot?

短時間雨予報用bot。「○○の雨」もしくは「○○の天気」から始まるツイートを@mickey24_botに送ると、○○の地域について向こう5時間の雨予報をしてくれます。基本的に日本全国の雨予報に対応しています。

また、あらかじめ登録しておいた地域で雨が降りそうな場合にbotがreplyで知らせてくれる雨予報自動配信機能も備えています。

短時間雨予報&天気予報

以下の形式でmickey24_botにreplyすると、指定した地域について向こう5時間の雨予報および天気予報を教えてくれます。

@mickey24_bot ○○の雨
または
@mickey24_bot ○○の天気

また「明日の○○の天気」「明後日の○○の天気」と話しかけると、明日or明後日の天気予報を教えてくれます。ただし雨予報は向こう5時間の予報を返します。

@mickey24_bot 明日の○○の天気
または
@mickey24_bot 明後日の○○の天気

週間天気予報

以下の形式でmickey24_botにreplyすると、指定した地域の週間天気を教えてくれます。

@mickey24_bot ○○の週間天気

雨予報自動配信



指定した地域の雨予報を定期的に確認し、以下の条件を満たした時に配信専用bot(@rainforecast0, @rainforecast1, ...)が知らせてくれる機能です。

  • その地域で向こう5時間以内に雨が降りそうな場合
  • その地域で向こう3時間以内に雨が止みそうな場合

雨予報を配信してもらいたい時は、mickey24_botに以下のようにreplyすればOKです。

@mickey24_bot ○○の雨予報の配信

雨予報の自動配信を停止したい場合は、mickey24_botに以下のようにreplyすればOKです。

@mickey24_bot 雨予報の配信の停止

注意点は以下の通りです。

  • 配信登録できる地域の数は1ユーザにつきひとつまでとなっています。既に配信登録している地域がある状態で別の地域の配信登録をしようとすると、以前登録した地域の自動配信は解除されます。
  • 一度自動配信をした地域は以降5時間は自動配信をしない仕様になっています。
  • 配信専用botに話しかけても基本的に返事しません。配信登録・停止や天気予報などはmickey24_bot本体に話しかけてください。

漢字天気予報


1日4回(7:02, 13:02, 18:02, 23:02)、漢字や絵文字を使って天気予報をpostします。

人間画像製造

f:id:mickey24:20170708221722p:plain:w360

以下の形式でmickey24_botにreplyすると、mickey24_botがいらすとや風の人間画像を製造してくれます。

@mickey24_bot 人間画像作って
または
@mickey24_bot 人間画像ください

人間画像製造モデルは完璧ではないので、ほとんどの場合すり潰された人間のような画像が出てきますがご了承ください。詳しくは以下の記事をご覧ください。
うちのbotが「いらすとや風の人間画像」を製造できるようになった - ぬいぐるみライフ?

臓物占い

f:id:mickey24:20170712000841p:plain:w360

以下の形式でmickey24_botにreplyすると、今日のあなたにピッタリのラッキー臓物を3Dモデル画像付きで提示してくれます。「動物占い」ではありません。占いの結果は毎日0時に変わります。

@mickey24_bot 臓物占いやって

リンク先の臓物画像は以下のような感じです(「胆嚢」の場合)。ラッキー臓物をより具体的な形で把握する上で役立ちます。

f:id:mickey24:20170712000435p:plain:w240

3Dモデル画像はライフサイエンス統合データベースセンター(DBCLS)のサービス「BodyParts3D」を使って生成されています。画像のライセンスは「クリエイティブ・コモンズ 表示 - 継承 2.1 日本」が適用されます。

Brainf*ckインタプリタ

f:id:mickey24:20170713011300p:plain:w360

mickey24_botに難解プログラミング言語「Brainf*ck」のコードをreplyすると実行してくれます。詳しくは以下の記事をご覧ください。

mickey24_bot de Brainf*ck - ぬいぐるみライフ?

おまけ

  • 挨拶するとちゃんと(?)返してくれます。
  • お礼を言うと喜びます。
  • うさみみ(「「)に反応します。
  • 「殴って」と話しかけると殴ります。
  • 「励まして」と話しかけると励ましてくれます。
  • 他にもいろいろ!


以前のバージョン(2.0)からの変更点

  • 話しかけると1〜2秒で返事をくれるようになりました(Twitter Streaming APIに対応)。
  • 雨予報自動配信が配信専用bot(@rainforecast0, @rainforecast1, ...)によって行われるようになりました。
  • 一部の機能が廃止されました(飲食店検索、デレモード、エアコン操作、非公式RTボクシング)。

どうやって動かしてるの?

Goでソースコードを書き、Dockerのコンテナを作ってさくらのVPSにデプロイすることで動かしています。

何で雨予報?

局所的かつ短期的な雨の予報をすぐに配信してくれるようなサービスがあると便利なんじゃないかなーと思ったので。

雨予報が当たらないんだけど

あくまでも予報なのでその辺は勘弁してください><実際は雨が降らないのに「降りそう」「降ってる」と通知してくることが多いようです。

以上

かわいがってやってください。

更新履歴

  • 2017-06-14 記事作成
  • 2017-07-08 人間画像製造についての記述を追加
  • 2017-07-12 臓物占いについての記述を追加
  • 2017-07-13 Brainf*ckインタプリタについての記述を追加

mickey24_botの雨予報自動配信の仕様が変わりました

mickey24_botの雨予報自動配信が配信専用bot(@rainforecast0, @rainforecast1, ...)によって行われるようになりました。この変更によって、mickey24_bot本体のツイート数が少なくなり、rate limit exceededエラーによってbotがしばらく会話できなくなるといったことを防ぐことが期待できます。

配信専用botに話しかけても基本的に反応しませんので、配信登録・停止や天気予報などは今まで通りmickey24_bot本体に話しかけてください。

おーばーびゅー


配信されている様子


Cucumber+Mockで悩んでいる

CucumberでMockに対するmethod呼び出しのexpectation(should_receive)をどのように書くか悩んでいる.

動くけど好きじゃない例

CucumberのexamplesにMockを使う例がある.
https://github.com/cucumber/cucumber/tree/master/examples/rspec_doubles

この例ではMockの作成とmethod呼び出しのexpectationの設定をGiven節に書いている.Then節には何も書いていない.

mocking.feature

Feature: Mocking
  In order to test external stuff
  I want to mock

  Scenario: Mock a transmogrifier
    Given I have a cardboard box
    When I poke it all is good

calvin_steps.rb

class CardboardBox
  def initialize(transmogrifier)
    @transmogrifier = transmogrifier
  end
  
  def poke
    @transmogrifier.transmogrify
  end
end

Given /^I have a cardboard box$/ do
  transmogrifier = double('transmogrifier')
  transmogrifier.should_receive(:transmogrify)
  @box = CardboardBox.new(transmogrifier)
end

When /^I poke it all is good$/ do
  @box.poke
end

これでテスト自体は問題なく動くのだけど,いくつか不満点がある.

不満点
  • "Given I have a cardboard box"で実行されるコードが"I have a cardbox"以上のことをやっている
    • transmogrifierに関するGiven節を追加してコードを分割すればいいのかもしれないけど…
  • method呼び出しのexpectationがGiven節に,methodの戻り値のassertionがThen節に分散してしまう

こんなふうに書きたい

method呼び出しのexpectationもmethodの戻り値のassertionもThen節に書きたい.

mocking.features

Feature: Mocking
  In order to test external stuff
  I want to mock

  Scenario: Mock a transmogrifier
    Given I have a cardboard box
    When I poke it
    Then all is ok

calvin_steps.rb

class CardboardBox
  def initialize(transmogrifier)
    @transmogrifier = transmogrifier
  end
  
  def poke
    @transmogrifier.transmogrify
  end
end

Given /^I have a cardboard box$/ do
  @transmogrifier = double('transmogrifier')
  @box = CardboardBox.new(transmogrifier)
end

When /^I poke it all is good$/ do
  @box.poke
end

Then /^all is ok/ do
  @transmogrifier.should_receive(:transmogrify)
end

しかし,このテストは失敗する.Then節はWhen節よりもあとに実行されるため,When節で@box.pokeが呼ばれる時点で@transmogrifierにexpectationがセットされていないことが原因.

以上

どうすればこの手のテストがスマートに書けるのかまだよく分かってないけど,Then節がWhen節よりも後に実行される以上,Given節にexpectationを書く方法しかないんだろうなーと思ってる.

もっといい書き方がありましたらぜひ教えて下さい.

Ruby 2.0.0-p0がリリースされてた

Ruby 2.0.0-p0 リリースノート

わーい.開発・リリースに関わった皆さん,本当におつかれさまでした&ありがとうございました.

非互換

リリースノートにRuby 2.0.0の「特筆すべき非互換」について書いてあったので真っ先に確認してみた.

デフォルトのスクリプトエンコーディングがUTF-8に

これのおかげで各ファイルにUTF-8用のマジックコメントをわざわざ書く必要がなくなった.わーい.早くRuby 1.9をdeprecateしよう!(ムリ
でもこの変更によって一部のプログラムが遅くなるなどの悪影響もある様子.まあ当たり前か.

iconv削除

Ruby 1.9から登場したString#encodeなどを使ってくださいという話.

ABI互換性がなくなっている

「1.9の.so, .bundleファイルをコピーするな」ということだけ気をつけておけばいいらしい.

#lines, #chars, #codepoints, #bytesメソッドがEnumeratorではなくArrayを返すようになった

結構うれしい変更.今まで文字列の各文字のArrayを取ってくるときにsome_string.chars.to_aと書いていたけど,これがsome_string.charsだけでよくなる.Array#to_aはselfを返すので,to_aがついてる古いコードも一応動くので安心.

Object#inspectの結果が#to_sではなく#のような文字列になった

今のところ特に思うところはないかな.

以上

さっそくrvm get head && rvm install 2.0.0 && rvm use 2.0.0 --defaultした.普段使っているRubyGemsが問題なく動いてくれることを祈ろう.
その他新機能については後日調べてみようかな.リリースノートからもリンクされてた以下の記事が分かりやすそう.
http://blog.marc-andre.ca/2013/02/23/ruby-2-by-example/

うちのbotがエアコンの操作方法を覚えた

追記:2017-07-09
現在この機能は利用できません。最新の情報は雨予報bot「mickey24_bot」の使い方 ver 3.0 - ぬいぐるみライフ?をご覧ください。

ぼくの自宅のエアコン限定ですが,mickey24_botがエアコンを操作できるようになりました.

どんな機能なの

うちのbotにreplyで指示を送ると,うちのエアコンに赤外線が発射されます.指示の出し方の例は,

@mickey24_bot 冷房を28℃に設定して
@mickey24_bot 暖房を20℃に設定して

など.これで外出先からエアコンを操作しておけば,帰宅時にエアコンの効いた快適な部屋がぼくを出迎えてくれます.

エアコンの運転モードは冷房と暖房,温度の設定範囲は16〜30℃(華氏温度は後日対応予定)です.

どうやって実現してるの

Arduino Unoに赤外線LEDを繋いで,Arduino Unoでエアコン用赤外線リモコンの信号をエミュレートさせています.

Arduinoと赤外線LEDは下のような感じになっています.ふたつの赤外線LEDがそれぞれ部屋の照明とエアコンの方を向いています(ちなみに部屋の照明もArduinoから赤外線で操作可能になっています).このArduinoはUSBで自宅サーバと繋がっていて,USBシリアル通信で操作することができるようになっています.

注意事項

  • だれでも操作できます.

以上

帰宅した時に既に部屋が冷えているのは思った以上に快適ですね.癖になりそう.

実際に書いたプログラムやサーバの詳細については後日記事を書く予定です.