2015年2月9日月曜日

Bot開発日記 分割投稿

RubyでTwitterBotをなんやかんや書いているのですが、
新しい知見を得た時やいい感じの処理が書けた時にそれをメモとして残そうと思いました。

今回は140文字を超える投稿をしなければならない場合、どうやって分割投稿するかみたいなことを試行錯誤したメモです。


概要

そもそも140文字を超える場合の分割投稿は適当に実装してありました。
でもちょっとアホっぽい感じだったので書き直そうと思ったのが今回のきっかけです。

140文字を超えた場合だけ他のメソッドに投げる形にしていたので、再帰的に書き直したいなあと。

実現したい処理は

"140文字より大きい文字列"

"140文字以下の文字列 + (続く)"
"140文字以下の文字列 + (続く)"
"140文字以下の文字列"

という感じで140文字以下に分割して "(続く)" という文字列を最後の文字列以外に追加するという流れです。

もちろん140文字以下のものに関してはそのまま投稿されたいです。
とりあえず実験なので分割する文字数は適当です。


その1 失敗したやつ

post1
1
2
3
4
5
6
7
8
9
10
11
12
13
def post1(str)
  if str.size > 8
    post_size = 8
    *over_str, post_str = str.scan(/.{1,#{post_size}}/m)
    post1(over_str.join << "(続く)")
  end
 
  print "[size: #{post_str ? post_str.size : str.size}] "
  puts post_str ? post_str : str
end
 
text = "abcdefghijklmnopqrstuvwxyz"
post1(text)

8文字より大きいものがきたら scanメソッドで指定した文字数で区切って配列にします。

それを多重代入で over_str という配列と post_str という文字列に分けます。

post_str は指定した文字列以下になっているのでそのまま投稿でき、
over_str は joinメソッドでひとつの文字列にまとめて "(続く)" の文字列を追加して次の post1 へ渡します。

こうすれば最後だけ "(続く)" が付かないし完璧だと思いました。
しかし…
実行結果
1
stack level too deep (SystemStackError)
これだと *over_str, post_str に分けた段階で over_str.join は post_size で割り切れるので、次の post1 に渡す際 "(続く)" を追加すると必ず "(続く)" が余りになって post_str になり続けるので SystemStackError が発生します。


その2 後ろから分割していく

post2
1
2
3
4
5
6
7
8
9
10
11
12
13
def post2(str)
  if str.size > 8
    post_size = 8
    post_str, *over_str = str.reverse.scan(/.{1,#{post_size}}/m)
    post2(over_str.join.reverse << "(続く)")
  end
 
  print "[size: #{post_str ? post_str.size : str.size}] "
  puts post_str ? post_str.reverse : str
end
 
text = "abcdefghijklmnopqrstuvwxyz"
post2(text)
実行結果
1
2
3
4
5
6
[size: 6] ab(続く)
[size: 8] cdef(続く)
[size: 8] ghij(続く)
[size: 8] klmn(続く)
[size: 8] opqr(続く)
[size: 8] stuvwxyz

その2は前からではなく後ろから一定の文字数ずつ分割していく方針です。reverseメソッドでひっくり返してるだけなのでその1とやってることは変わりません。 "(続く)" は文字列の末尾に付けられるので、末尾から分割していけば "(続く)" が余ることはなく、ちゃんと分割できますね。

うおおおおできたぞおおおおおおおと思ってさっそくBotの方に実装してみました。
しかし、試していた文字数が少なかったせいかあまり気にならなかったのですが、140文字にすると…

"280文字"

"4文字 + (続く)"
"136文字 + (続く)"
"140文字"

のように最初がやたら小さくなってしまうのです。
どこか気持ち悪い…
たった数文字で "(続く)" とか付けるのなんか気持ち悪い…
やはり一番最後が一番少なくなるのが分割としても自然っぽいし…


その3 いい感じの割合で分割したい

post3
1
2
3
4
5
6
7
8
9
10
11
12
def post3(str)
  if str.size > 8
    post_size = 8
    *over_str, last, post_str = str.scan(/.{1,#{post_size}}/m)
    last.match(/.{1,4}$/)
    post_str = $& << post_str
    post3(over_str.join << last.sub(/#{$&}$/, "") << "(続く)")
  end
 
  print "[size: #{post_str ? post_str.size : str.size}] "
  puts post_str ? post_str : str
end
実行結果
1
2
3
4
[size: 8] abcd(続く)
[size: 12] efghijkl(続く)
[size: 12] mnopqrst(続く)
[size: 6] uvwxyz

多重代入の部分にもうひとつ last という変数を追加しました。これは over_str 配列の最後の要素的な意味合いです。やはり最後に端数をもってくるためには前から分割していくしかないでしょう。しかし、そのままではその1と同じように再帰から抜けだせません。

そこで次の post へ渡す join する前の配列の最後の要素から4文字取り除いて "(続く)" をねじ込むようにしました。取り除かれた4文字はpost_strにくっつけます。これにより分割した際の余りが "(続く)" になり続けることはありません。実際にやっていることは少し違いますが同様のことが実現されています。140文字で分割してみるといい感じになってるのがわかります。

"280文字"

"132文字 + (続く)"
"136文字 + (続く)"
"12文字"

140文字を超えたから分割した感が出てますね。


おわりに

初心者なのでまだうまいこと書けてないですが、これからも精進していきます。

Rubyのことだから自分の知らないおもしろ便利メソッドがあってもっと簡単に実現できる、あるいはこれこれこういうアルゴリズムがある、みたいになるかもしれませんがそのときはまた追記していきます。

0 件のコメント:

コメントを投稿