正論なんて諭んないで

cordx56のブログです

Tweet generatorについて

こんにちは。

Tweet generetorというサービスを運用して1年とちょっとが経ちました。

2020/12/09更新

上のリンクは旧バージョンへのリンクです。是非新バージョンのTweet generatorをご利用ください。 新バージョンについて詳しくはこちらの記事で紹介しています。 cordx56.hatenablog.com

これまでに少なくとも36万アカウントに利用していただき1、自分が作ったWebサービスとしては一番アクセス数が多いものになりました。

簡単にサービスについて紹介させていただくと、Twitter認証をすることで直近のツイートからマルコフ連鎖モデルを学習し、学習済みモデルを利用してテキスト生成を自由に行うことができる、というサービスです。

今回の記事では、Tweet generatorについて少しまとめてみようかと思います。

リクエストの急増

このTweet generatorがいわゆる「バズった」状態になったのは2020年2月頭のことでした。 この頃のテキスト生成数を横軸を日にち、縦軸を生成数としてグラフにしました。

f:id:cordx56:20201015014254p:plain
2月中のテキスト生成数グラフ(日別)

2月中のテキスト総生成数は16,475,342件でした。だいぶ大きな数ですね。

グラフから、1日2日の生成数が少ないのに対し、3日から急にテキスト生成数が増加している様子がわかります。 これが2月頭にバズったという状況です。

この時期に何故急にリクエストが増えたのか、その理由に心当たりがあります。

github.com

このコミットを見ていただければわかる通り、リクエストが急増する直前の1月30日にTwitter cardの設定をしています。 Twitter cardの設定が本当にリクエストの急増と関係しているのかはわかりませんが、まぁ設定しておくに越したことはないでしょう。

利用時間帯

2020年9月中のテキスト生成数を横軸を時間、縦軸を生成数としてグラフにしました。

f:id:cordx56:20201015012720p:plain
9月中のテキスト生成数グラフ(時間別)

グラフから、Tweet generatorがよく利用されている時間帯が23時から0時の深夜帯であることがわかります。 Tweet generatorのユーザーは夜ふかしさんが多いのでしょうか?

モデル生成1回あたりのテキスト生成数

2020年9月の統計では、1回のモデル生成に対してテキスト生成は平均117.82回行われていました。 1回モデルを生成したら、100回以上テキスト生成をする方が多かったということでしょうか。

最後に

趣味で運用していたシステムをこんなに多くの方に使っていただけるとは思ってもいなかったので、正直なところ驚いています。

ご利用いただいた皆様、ありがとうございます。 今後ともTweet generatorをよろしくお願いします。


  1. ログがうまく取れてなくてちゃんとした統計が取れませんでした。サービス開始からのログをみる限り約37万アカウントが利用していました。2020年4月以降から2020年11月25日現在までの総計では21万アカウントがこのサービスを利用していたことになります。この間に生成されたテキストの数は38,326,571件になります。

LUKSで暗号化されたボリュームを起動時に自動でマウントする

こんにちは。

今回は外付けHDDを暗号化して、起動時に自動でマウントする設定を行ったので、そのことについて書いていきたいと思います。

暗号化されたボリュームを用意する

まずは暗号化ボリュームの準備です。

$ sudo cryptsetup -y -v luksFormat /dev/sda1
$ sudo cryptsetup open /dev/sda1 localhdd

/dev/sda1localhddは必要に応じて書き換えてください。

これで/dev/mapper以下にlocalhddが現れます。

$ sudo mkfs.ext4 /dev/mapper/localhdd
$ sudo mount /dev/mapper/localhdd /mnt

これで暗号化ボリュームのフォーマット及びマウントができます。

/etc/crypttabを書く

暗号化ボリュームを自動でopenするために、/etc/crypttabを書いていきます。

まずは暗号化ボリュームのUUIDを調べます。

$ sudo cryptsetup luksDump /dev/sda1 | grep "UUID"

でてきたUUIDを拾って、次のような行を/etc/crypttabに記述します。

localhdd UUID=[上で調べたUUID] /etc/luks-keys/password luks

/etc/luks-keys/passwordのパスは任意です。パスワードを記述したファイルを置く場所に応じて変更してください。

次にパスワードを/etc/luks-keys/passwordに書き込みます。

$ echo -n "password" > /etc/luks-keys/password

改行文字が入らないようにecho -nにしています。

/etc/fstabを書く

最後に、/etc/fstabを書いていきます。

次のような行を記述します。

/dev/mapper/localhdd /mnt ext4 defaults 0 2

/dev/mapper/localhdd/mntは環境に応じて書き換えてください。

以上で設定は終わりです。 ほとんど次の記事の和訳みたいな感じになってしまいました。 必要に応じて次の記事も参考にしてください。

blog.tinned-software.net

はてなサマーインターン2020に参加してきました

こんにちは。今回ははてなサマーインターンに参加したので、そのことについて書いていきたいと思います。

はてなサマーインターンについて

はてなサマーインターンは今年の夏に開催された株式会社はてなさんのインターンになります。

今年は全日リモート開催ということで、一週間おうちからはてなさんのインターンに参加させていただきました。 京都行きたかった……

はてなサマーインターン2020の詳細については下記をご参照ください。 hatenacorp.jp

応募

私は学部4年でほぼ院進が決まっている状態だったのでインターンの募集要項から微妙に除外されているケースが多く、まず募集要項に適合してるインターン先を探すところからでした。 はてなさんはその辺の制約が緩く、申し込みしやすかったです。

事前課題は指定されたイメージをdocker runして軽く情報を打ち込むというものでした。 いいのかこんな簡単で……とか思いましたが良かったみたいです。

正直はてなさんは「通らないだろ〜」とか思いながら申し込んだので、面接の話がきたときはびっくりしました。

面接も、提出したESをじっくりみて丁寧に評価してくださって、ありがたかったです。

講義

講義は体系的でわかりやすく説明されていて非常に質の高いものでした。

内容はHTTPの基本から始まりgRPCについて、Dockerについて基本的なこと、Kubernetesについて基本的なこと、という感じでした。 実践問題なども用意されていて、楽しく受講することができました。 実践問題で突如発生したCTF、解けなかった……*1

講義動画は後日配信される*2とのことなので、楽しみに待ちましょう。

2020/10/04更新

配信されました。 hatenacorp.jp

課題

課題としては、Kubernetes上のブログサービスの拡張をすると行った感じです。

ブログ記事のレンダラーサービスの改良と、リンクのタイトル自動取得を行うマイクロサービスの開発が基本課題、大量のリンクがあったときへの対処とrobots.txtの考慮などが発展課題という感じでした。

課題説明ではコミットの粒度に注意する、テストを書く、などの基本的な部分も丁寧に説明していただきました。

実際の課題に使われたソースコードGitHubで公開されているので、気になる方は見てみると良いのではないでしょうか。 github.com

実装

実装したこととしては、次の通りです。

  • レンダラーサービスにMarkdownライブラリgoldmarkを導入する
  • Markdownライブラリの拡張をして独自記法を導入
    • MarkdownライブラリのASTと向き合う、という感じでした
  • リンク記法のタイトルが空欄のときに、リンク先からタイトルを自動で取得する
    • これもMarkdownライブラリに手を突っ込んで、ASTを走査してタイトルを自動補完すると言った感じでした
    • これについてはレンダラーサービスとタイトル取得間でgRPCで通信して……という感じで実装しました
  • タイトル自動取得の際にrobots.txtを参照してクロールして良いページかどうか判定する
    • 簡単にrobots.txtの仕様を調べて、簡易的なrobots.txtパーサを手書きするという感じでした

メンターさんとのやり取り

メンターの id:yigarashi さんには3日間DiscordとScrapbox上でお世話になりました。 基本的に自分のコードを誰かにレビューしてもらう・レビューをもとに今後の実装について相談する機会というのはなかなかなかったので、メンターさんの優しいレビューやプチ講座には感動しました。

特にテストをきれいに書くためにどういう設計をしたらいいかの講座はありがたかったです。

自分は実装速度はとても遅いほうだと思っていたのですが、メンターさんにはとても手が速くて丁寧と褒めていただけたのでめちゃくちゃ嬉しかったです。

発表

インターン最終日は成果発表会がありました。

皆さんの発表がどれも特徴的で、同じ課題でも様々な取り組み方があって面白かったです。 まず言語もバラバラだったり、自分が難しくて手を付けられなかった課題に全く思いつかなかった方法で解決策が提示されたりと、非常に興味深かったです。

発表中はみんなでScrapboxに感想コメントを書き込みながら発表を聞いていたのですが、自分の発表に「実装も発表も丁寧」と書いていただけてこれもまためちゃくちゃ嬉しかったです。

面談

発表が終わったあとに面談をしていただきました。 ここでも丁寧にインターン中のことや今後のことについてお話させていただけてありがたかったです。

最後に

インターンではめちゃくちゃ褒められが発生したので、嬉しいことの連続でした。

はてなの皆さん、メンターの id:yigarashi さん、同じ部屋*3で作業していた id:CNaan さん、その他のインターン生の皆さん、本当にありがとうございました。 この経験を活かして今後とも頑張ってまいります!

今後ともよろしくお願いします。

*1:初めてCTFっぽいことをしたのですが、楽しかったです

*2:https://developer.hatenastaff.com/entry/2020/07/01/185834

*3:Discordのボイスチャットです

Rustでnomを使って計算機を作ってみる

こんにちは。 Rust初心者中の初心者です。 今回はnomというライブラリを使ってRustで簡単な計算機を作ってみたので、そのことについて書いていきたいと思います。

nomはRust製のパーサコンビネータです。 nomを利用することで、関数の組み合わせで簡単に字句解析器構文解析器を作り上げることが可能になります。 nomの詳しいチュートリアルは次のページを参照することをおすすめします。

hazm.at

本記事ではnomを使って簡単な計算機を作っていきたいと思います。

計算式をEBNFで定義する

まずは四則演算の計算式をEBNF(拡張バッカス・ナウア記法)で定義することから始めましょう。

計算式は次のようなEBNFで定義されます*1

<addsub> ::= <muldiv> (('+' | '-') <muldiv>)*
<muldiv> ::= <fact> (('*' | '/') <fact>)*
<fact> ::= <num> | '(' <addsub> ')'

それぞれ、addsubが足し算引き算、muldivが掛け算割り算、factが数値または括弧でくくられた計算式を表しています。

次に、それぞれを関数化していきます。

EBNFを関数化する

数字列をパースする

数字列をパースする関数です。 map_resでパース結果をf64に変換します。

fn num(s: &str) -> IResult<&str, f64> {
    map_res(
            digit1,
        |int: &str| -> Result<f64, ParseFloatError> {
            int.parse::<f64>()
        }
    )(s)
}

f64に変換しているのは、計算結果で小数に対応するためです。

返り値はIResult<&str, f64>となっており、これはパーサで解析できなかった余りの&strと解析結果のf64から成ります。

<fact>を関数化する

EBNFでの<fact>を関数化していきます。

fn fact(s: &str) -> IResult<&str, f64> {
    alt((
        num,
        delimited(
            char('('),
            delimited(multispace0, addsub, multispace0),
            char(')'),
        ),
    ))(s)
}

altはいずれかにマッチするものを指定でき、delimitedは囲われた部分を取り出すのに使えます。

この場合だと、数字列にマッチする場合或いは括弧で囲まれた計算式にマッチする場合、となります。

<muldiv>を関数化する

ここから少し複雑になります。

fn muldiv(s: &str) -> IResult<&str, f64> {
    map_res(
        permutation((
            fact,
            many0(
                map_res(
                    permutation((
                        multispace0,
                        alt((
                            char('*'),
                            char('/'),
                        )),
                        multispace0,
                        fact,
                    )),
                    |(_, opr, _, rval)| -> Result<f64, &str> {
                        if opr == '*' {
                            Ok(rval)
                        } else {
                            Ok(1.0 / rval)
                        }
                    }
                )
            ),
        )),
        |(lval, rvec)| -> Result<f64, &str> {
            Ok(lval * rvec.iter().fold(1.0, |acc, x| { acc * x }))
        }
    )(s)
}

permutationはパーサの逐次実行を意味します。 上の例の場合、factに続いてmany0以下が実行されます。 many0は0回以上の繰り返しで、EBNFだと(('*' | '/') <fact>)*の部分にあたります。

内側のmap_resオペランドによってVecに格納する値を変更しています。 掛け算であればそのまま、割り算であれば1/値を追加することで、全ての結果を掛け算にすることができます。

外側のmap_resは左側の値(<fact>)に右側の値((('*' | '/') <fact>)*)の積を掛けて答えを出しています。 これがOk(lval * rvec.iter().fold(1.0, |acc, x| { acc * x }))のところです。

<addsub>を関数化する

<addsub><muldiv>と同様に関数化できます。

fn addsub(s: &str) -> IResult<&str, f64> {
    map_res(
        permutation((
            muldiv,
            many0(
                map_res(
                    permutation((
                        multispace0,
                        alt((
                            char('+'),
                            char('-'),
                        )),
                        multispace0,
                        muldiv,
                    )),
                    |(_, opr, _, rval)| -> Result<f64, &str> {
                        if opr == '+' {
                            Ok(rval)
                        } else {
                            Ok(-1.0 * rval)
                        }
                    }
                )
            ),
        )),
        |(lval, rvec)| -> Result<f64, &str> {
            Ok(lval + rvec.iter().fold(0.0, |acc, x| { acc + x }))
        }
    )(s)
}

やってることは<muldiv>の場合とほとんど変わりません。 変わっているのはオペランドの場合分けと最終的な計算部分です。

外側のmap_res(('+' | '-') <muldiv>)*を処理していって最終的に得られたVecを足して結果を返します。

実行結果

上記プログラムを実行してみます。

fn main() {
    let parser = all_consuming(addsub);
    assert_eq!(parser("1 + 2"), Ok(("", 3.)));
    assert_eq!(parser("1 * 2"), Ok(("", 2.)));
}

問題なく実行できるかと思います。

ソースコード

ソースコード全体は以下のようになりました。

calculator written in Rust with parser combinator…

このプログラムを実行すると、標準入力に入力された計算式を計算し続けるプログラムが走ります。計算機の完成です。

所感

今回はRustのパーサコンビネータnomを使って簡単な計算機を実装しました。

nomは関数の組み合わせで字句解析構文解析を行ってくれるため、非常にわかりやすく字句解析器構文解析器を組み立てることができました。 機会があれば他の用途でもnomを使ってみたいと思います。

新しくブログをはじめました

前もはてなでブログをやっていたのですが、はてなIDが気に入らなかったのでアカウントごと作り直しました。 これまでブログを見てくださっていた方々がいれば、今度からはこちらのブログをよろしくお願いいたします。

このブログは技術的なことや日常のちょっとしたことなどを書いていくブログにしようと思っています。