- 投稿日:2021-12-05T21:18:47+09:00
[読書備忘録]RubyとSinatraではじめる Webアプリケーション開発の教科書
はじめに Ruby 関連本って Rails を除いてなかなか出版されない中、sinatraですからちょっとびっくり。 RubyとSinatraではじめる Webアプリケーション開発の教科書 RubyとSinatraではじめる Webアプリケーション開発の教科書 を図書館で借りて読みました。 いわゆる?安定の森北出版 目次 第1章 Webアプリが動くしくみ 第2章 PC上に仮想環境を構築しよう ― Webアプリ作成前の準備 第3章 Rubyのコーディングに慣れよう 第4章 Webカレンダーを作ろう ― いちばん簡単なWebアプリ 第5章 シンプルな掲示板Webアプリを作ろう ― データベースと連携する 第6章 パスワードの管理のしかた ― ログイン機能をつけるための基礎知識 第7章 Cookieの使い方 ― セッションを継続させるための基礎知識 第8章 掲示板Webアプリを完成させよう ― より実用的なWebアプリ JavaScriptを使用した派手な部分はないものの、パスワードの暗号化やセッション管理などに章を割いており好印象。 検索すれば得られる知識ではあるものの、手っ取り早く紙媒体で学習したい人におすすめ。 メモ がんばれRuby がんばれ森北出版
- 投稿日:2021-12-05T16:43:57+09:00
Rubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念- に挑戦しました
この記事はRubyプログラミング問題にチャレンジ! -改訂版・チェリー本発売記念- Advent Calendar 2021 14日目の記事です。 バックエンドエンジニアをしておりますyukoonoと申します。 書いたコードを外部に公開したことがないので、力試しをしてみたいという動機で参加させていただきました。 よろしくお願いします。 プルリクエストのリンク、または自分が書いたコード Github: 点字メーカーチャレンジ 12/14 #20 JunichiIto/tenji-maker-challenge ロジックの解説 入力をひらがな一文字分ずつ分割する 例:"KI RI N" -> "KI", "RI", "NN" このとき、"N" または "NN" が入力されていた場合は、「ん」を点字にした "--\n-o\noo" に直接変換し、次のひらがな一文字分の処理に移る。 ひらがな一文字分のローマ字をさらに英字一文字に分割する 例:"KI" -> "K", "I" 英字に "Y", "W" が含まれている場合、母音の位置を下に下げるフラグ (vowel_tenji_needs_shift_downward_flag) を立てる 英字を、母音・子音ごとにクラスで定義した点字対応表を用いて、それぞれ相当する点字に置き換える 例: "K", "I" -> "--\n--\n-o", "o-\no-\n--" 母音の位置を下に下げるフラグがfalseであれば、5 に進む フラグがtrueである場合、母音の o の位置を一段下げ、上に"--"を挿入する操作を行う 例: "A" -> "o-\n--\n--" -> "--\no-\n--" 一番下の段に o が移動するまで 4.1 の操作を繰り返す 母音と子音を組み合わせて一文字の点字にする 例: "--\n--\n-o", "o-\no-\n--" -> "o-\no-\n-o" 得られた点字を横並びで表示できるよう、ひと続きの文字列に置き換える 例: "o-\no-\n-o", "--\n-o\n--", "--\n-o\noo"\ -> "o- -- --\no- -o --\n-- -o oo" 結果を出力すると、以下のように表示されます。 tenji = @tenji_maker.to_tenji('KI RI N') assert_equal <<~TENJI.chomp, tenji o- o- -- o- oo -o -o -- oo TENJI コードのアピールポイント 一番のポイントは、音と点字の対応表を作成し、それを利用してテキストの変換を行なったことです。例えば、母音では以下です。 VOWELS = { A: <<~A.chomp, o- -- -- A I: <<~I.chomp, o- o- -- I U: <<~U.chomp, oo -- -- U E: <<~E.chomp, oo o- -- E O: <<~O.chomp, -o o- -- O } 点字にしたときに、どの音がどのような形に対応するのかを視覚的に示したかったため、対応表を文字列で用意し、それを組み合わせたり、順番を入れ替えることでテキストの変換を進めました。コードを見る人に、処理するための材料を示すことが狙いです。 工夫したところ 母音を移動させる行について、母音を o が下の行に入るまでシフトさせる仕様にしたこと 1.4.1, 1.4.2の手順です。 この方法で、元々の変換対象ではなかった「を」について、変換できるようになりました。 実は、最初は「や・ゆ・わ」の変換はできていたのですが、母音由来の o が二段目にもある違いから「よ」の変換がうまくできず、試行していたら思いがけず「を」が変換できてしまった、という感じです。 自分で工夫した点なのか、と聞かれると微妙ですが、お題の範囲外の変換ができたので記しました。 「ん」について、"N" と "NN" からの点字の変換ができるようにした 手順1に記述していますが、「ん」についてだけは、他の文字の変換処理と切り離して、"N" または "NN" の入力があれば変換する形式にしました。test.rb には "N" から変換するケースのみが書かれていましたが、個人的によく "NN" で入力することから、これも「ん」に変換したいというこだわりがありました。他の変換方法として、/N+ $/が入力されていたら「ん」に変換したらどうか、とも考えましたが、"NNN" も「ん」に変換されそうだったのでやめました。 苦労したところ とにかく大変だったところは、変数の名付けです。 一文字分の点字 ( ex: "o-\n--\n--" ) 点字が配列に複数入っている状態 ( ex: ["o-\n--\n--", "o-\no-\noo"] ) 点字の点と線を1文字ずつに分解したものが配列に入っている状態 ( ex: ["o", "-", "\n", "-", "-", "\n", "-", "-"] ) 点字の点と線を1文字ずつ取り出した状態 ( ex: "o", "-" ) などが処理の中で登場しました。 コードを書き始めた当初は、「これどうやって名付ければ状態伝わるの?」と、かなり困ることになりました。数日後に見返して理解できそうな表現か確かめる、図を描いて自分で状態遷移を整理するなどして、ようやくこれだ、と思う名付けができた、さあ Qiita に書こう!と書き始めたあとで、また違和感を覚えて書き直しました。ようやく自分の中では整理されて、わかりやすい表現になってきたのではないかと思っています。 おかげで、変数名を変えただけのコミットが積み上がってしまいましたが。苦笑 伊藤さんにメッセージ(もしあればw) 面白い企画を立ち上げて頂きありがとうございます。会社の方以外に自分のコードを見ていただく機会などなかなか無いので、怖くもありますが、わくわくもしています。 また、他の参加者の方のコードなどは見ていませんが、Qiitaのカレンダーからタイトルだけは見ています。読むのをとても楽しみにしています。
- 投稿日:2021-12-05T16:11:57+09:00
Rubyやってた人間がGo始めて思ったこと
この記事は、フラー株式会社 Advent Calendar 2021の6日目の記事です。 5日目の記事は @m-coder さんによる DroidKaigi2021に登壇した話 でした。 私は元々Ruby on Railsで開発をしていたのですが、最近転職しまして、Goに入門しました。 Goやり始めて「へぇ〜」と思ったことを書いてみます。 型があるとうれしい 私は静的型付けの言語をこれまであまり触ったことがなかったのですが、型がちゃんと決まってるとありがたいですね。 エディタ(VSCodeを使っています)の補完機能がバリバリ効いて、 この変数にどんな値が入っているのか この変数はどんなメソッドが使えるか このメソッドにどんな値を入れればいいか などを教えてもらえるので、サクサクコードを書き進められます。 Rubyだと「最近入力した単語」くらいしかエディタの候補に出してもらえないので、ファイルを行き来してメソッドの中身とかを確認する必要があったりしたので、そこが地味に手間だったな〜と思います。 クラスは無い Goではクラスを定義する仕組みが無いんですね。 他の言語で言うクラス的なことをやるには、struct(構造体)を使って任意の型のフィールドを持つ型を定義して、その型に対するメソッド定義を外側で(型定義とは別に)やる、みたいな流れになります。 例えばfooというstring型のフィールドと、barというint型のフィールドを持ったFooBarという型を定義したいな〜というときはこういう風に書きます。 type FooBar struct { foo string bar int } で、FooBar型のインスタンスに紐づくメソッド(Rubyで言うところのインスタンスメソッド)を定義したいな〜というときは、型定義とは別に、以下のようなメソッド定義を書きます。 func (f FooBar) SayFoo() { fmt.Sprint(f.foo) } funcキーワードの後に(f FooBar)とあるのが、「この関数はFooBar型の変数fがレシーバーになるよ」という意味で、これによりFooBar型のインスタンスでSayFoo()というメソッドを使用できるようになります。 結果として、以下のようにSayFoo()メソッドを備えたFooBar型が使用できるようになります。 func main() { f := FooBar{foo: "Woooo!", bar: 1} // FooBar型のインスタンスを生成して、`f`に代入 f.SayFoo() // `f`のメソッド`SayFoo()`を実行 } 全体のコードとしてはこうなります。 package main import "fmt" type FooBar struct { foo string bar int } func (f FooBar) SayFoo() { fmt.Print(f.foo) } func main() { f := FooBar{foo: "Woooo!", bar: 1} f.SayFoo() } ちなみに、同じようなことをRubyでやるとしたらこんな感じでしょうか。 class FooBar def initialize(attr = {}) @foo = attr[:foo].to_s @bar = attr[:bar].to_i end def say_foo() puts @foo end end foo_bar = FooBar.new(foo: "Woooo!", bar: 1) foo_bar.say_foo Goの方が型定義とメソッド定義が分離されてスッキリしてて良いね〜ということなんでしょうかね。 変数名は短くする Go界隈だと、割と短い変数名をつける風潮があるように感じます。1文字だったり略語にしたり。 Rubyだと2単語をアンダーバーでつなげた変数名とかを良く書いていた気がします。 型が明確になっているし、説明的な変数名をつけないといけないような見通しの悪いメソッドは書くもんじゃねえぞ、というノリなんでしょうかね。 まあGoに入ればGoに従えということで。 略語は全部大文字にする Goでは複数単語をつなげた名称をつけるときはキャメルケースにする風習です。 その際にIDやURLといった、略語はすべて大文字のままにしておくのが良しとされているようです。 例えばuserIDとかbaseURLみたいな感じです。 JavaScriptなんかだとuserIdとかbaseUrlみたいにしちゃいそうな気がしますが、まあGoに入ればGoに従えということで。 構造体のタグというものがある Goの構造体のフィールド定義では、型に加えて「タグ」と呼ばれる情報を付与することができます。フィールドのオプションみたいなもののようです。 例えばGoの標準パッケージであるjsonパッケージを使うときに、読み込むJSONのキーと構造体のフィールド名を関連付けるのに使われます。 こんな感じで書き、json:"id"という部分がタグです。 type User struct { ID int `json:"id"` Name string `json:"name"` } こうすることで、「JSONの"id"というキーが、User型のIDフィールドに対応しているよ〜」ということがjsonパッケージに伝えられて良い感じにやってくれるようになります。へぇ〜。 ディレクトリごとにパッケージとしてまとめられる Goではプロジェクト内にディレクトリを作成し、その中に.goファイルを配置すると、別パッケージとして扱えるようになります。(同じディレクトリに複数パッケージのソースコードを配置することはできない) また、同じパッケージのソースコード同士では、importを書かなくても別ファイルの内容を参照できます。 例えばこんな感じのディレクトリ構成になっていたとして、 . ├── main.go └── nice ├── bar.go └── foo.go nice/foo.goとnice/bar.goの一番上にはpackage niceと書いておきます。 そうするとmain.go内では、niceパッケージをimportすることで、niceパッケージで定義した型や関数をnice.HogeHogeといった形で参照できるようになります。 また、nice/foo.goとnice/bar.goの中では、同じパッケージ内なので、内容を相互に参照可能です。 割と分かりやすいので、コードの整理をしやすそうだな〜という雰囲気を感じます。 以上です。
- 投稿日:2021-12-05T15:46:53+09:00
Rubyのメモリフットプリント肥大化の歴史
Rubyプロセスを起動した直後のメモリフットプリント(メモリ消費量)を、Ruby 1.8.7〜3.1.0の各バージョンごとに調べました。バージョンが上がるにつれフットプリントが肥大化する様子がよく分かります。 各バージョンごとのメモリフットプリント Rubyプロセス起動直後のメモリフットプリントを、次のようにして調べました。 $ ruby -e '$stdin.read' & $ ps aux | awk 'NR==1||/rub[y]/' USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND ubuntu 158555 2.8 2.2 78716 22324 pts/0 T 04:04 0:00 ruby -e $stdin.read psコマンドの出力のうち、RSSの値をプロセスのフットプリントとしました。 また比較のために、ruby --disable=gems や ruby --enable=jit をしたときの値も調べました。 以下が調べた結果です(単位:KB、環境:Ubuntu 20.04 LTS x86_64)。 ruby version `ruby` `ruby --disable=gems` `ruby --enable=jit` ------------------------------------------------------------------------ 1.8.7-p358 3,368 - - 1.9.3-p551 6,900 - - 2.0.0-p648 8,672 6,164 - 2.1.10 17,116 14,572 - 2.2.10 17,808 14,524 - 2.3.8 19,084 14,272 - 2.4.10 17,856 14,252 - 2.5.9 17,124 14,224 - 2.6.9 21,728 18,792 22,180 2.7.5 21,512 18,772 21,584 3.0.3 21,916 19,260 22,008 3.1.0-preview1 22,420 19,624 22,452 Ruby 1.8.7 の頃はたった 3.3MB ぐらいだったのに、3.1.0 だと 22MB を超えています。起動するだけで 22MB・・・。ちょっといただけないですね。 メモリフットプリント肥大化の理由 メモリフットプリントが肥大化した理由を、Rubyのバージョンごとに推察してみます。 Ruby 1.8: 3,368(kb) 今となっては信じられないくらいフットプリントが小さいです。このときは構文木をたどって実行するシンプルな方式だったので、そのおかげでしょう。 Ruby 1.9: 6,900(kb) 1.8 と比べてフットプリントが倍増しました。1.9 では高速化のために実行エンジンが仮想マシン方式になったので、その影響でしょう。 Ruby 2.0: 8,672(kb) 1.9 よりフットプリントが少し増えています。ruby --disable=gems だと増えてないので、増えたのは rubygems を標準で読み込むようになったことが理由でしょう。 Ruby 2.1: 17,116(kb) フットプリントがまた倍増しました。Ruby プロセスのメモリ大食らいが決定的になったバージョンです。2.1 では世代別GCが導入されたので、そのせいだと思います。 Ruby 2.2: 17,808(kb) 特には変化なし。2.2でオブジェクトサイズが増えたりインクリメンタルGCになりましたが、その影響は起動直後のフットプリントには見られませんでした。 Ruby 2.3: 19,084(kb) 少し増えました。ruby --disable=did_you_min とするとフットプリントが少し減るので、例外時に「Did you mean?」とヒントを出してくれるDid you mean機能が入った影響だと思います。 Ruby 2.4: 17,856(kb) 少しですがフットプリントが減りました!ruby --disable=did_you_mean をつけてもフットプリントが減らなかったので、例外が発生するまでDid you mean機能を読み込まなくなったのだろうと思われます。 Ruby 2.5: 17,124(kb) 若干減っている気がするけど、大きな変化はなし。 Ruby 2.6: 21,728(kb) またフットプリントが大きく増えました。このバージョンからJITが導入されたので、そのせいでしょう。ruby --disable=jit をつけてもフットプリントは変わらないので、JITを無効化してもJIT機能はエンジンに組み込まれたままなのでしょう。 Ruby 2.7: 21,512(kb) 特には変化なし。 Ruby 3.0: 21,916(kb) 特には変化なし。 Ruby 3.1: 22,420(kb) 少しだけ増えています。2つ目のJIT機能であるYJITが導入されたので、その影響でしょう。 Rubyプロセスのメモリフットプリントを減らす方法 起動直後のRubyプロセスのフットプリントを少しでも減らすにはどうしたらいいでしょうか。 --disable=gems オプションをつける。 Gemsパッケージに頼らずに済むなら、この方法でメモリサイズが少し減らせます。 rubygems.rb を改造し、「パッケージの読み込み機能」と「パッケージの作成機能」とに分ける。 起動時に必要なのは前者だけであり、後者は必要ありません。両者を分離して起動時には前者だけを読み込むようにすれば、フットプリントを減らして起動時間も短くできるはずです。ただし現在のrubygems.rbと関連ファイルは約3万7千行もあるので、全体を理解して改造するのは大きな手間がかかります。 JIT機能を有効化して起動した場合だけ実行エンジンにJIT機能を読み込むよう、Rubyを改修する。 この方法は技術的な難易度が高く、Ruby内部に詳しい人でないとできません。またRuby内部に詳しい人は、高速化には興味があってもフットプリントの減少にはあまり興味なさそうです。 2番目はなんとか実現したいですね。 また起動直後ではなく、実行中のRubyプロセスのメモリ消費量を減らすにはどうしたらいいでしょうか。 大量のデータを処理するときは、子プロセスを起動してそっちで処理をする。 処理が終わったら子プロセスを終了させれば、もとのプロセスには影響がありません。 コードサイズの大きいライブラリを使わず、コードサイズが小さくてコンパクトな設計のライブラリを使う。 本質的な解決策とは言えませんが、結局はこれがいちばん効きそうです。たとえばO/Rマッパーを使わずSQLで頑張るとか。RailsやめてSinatra使うとか。RSpecやめてOktest.rb使うとか(Oktest.rb の紹介記事)。 mrubyに乗り換える。 Rubyからmrubyへの乗り換えは、今なら十分検討対象でしょう。全体を乗り換えなくても、処理を切り出して部分的にmrubyに任せるのはアリよりのアリです。 参考:他言語のメモリフットプリント 起動直後のメモリフットプリントを、他の言語でも調べてみました。当然ですがスクリプト言語だけが対象です(例外としてJVMは対象とします)。 language version RSS (kb) ------------------------------------ perl 5.30.0 5,104 python 3.8.10 9,004 php 7.4.3 15,748 nodejs 10.19.0 34,364 openjdk 17 32,036 erlang 22.2.7 20,544 guile 3.0.1 9,420 gauche 0.6.9 9,296 gforth 0.7.3 2,988 tcl 8.6.9 4,172 ## シェル系 bash 5.0.17 3,096 zsh 5.8 3,544 dash 0.5.10 608 tcsh 6.21.00 672 yash 2.49 1,188 https://yash.osdn.jp/ gawk 5.0.1 3,292 mawk 1.3.4 1,064 ## 組み込み向け lua 5.3.3 1,276 squirrel 3.1 1,828 mruby 2.0.0 2,296 mruby 3.0.0 2,640 micropython 1.12 1,424 lily 2.0.0 1,124 http://lily-lang.org/ wren 0.4.0 3,172 https://wren.io/ gravity 0.8.5 2,216 http://gravity-lang.org/ pocketlang 0.1.0 864 https://thakeenathees.github.io/pocketlang/ 所感です。 メモリ喰いは、Node.js と JVM (OpenJDK) がツートップ。第2グループは Erlang と PHP(と Ruby)。 最近の Python は起動直後のフットプリントが徐々に減っており、Python 3.8 では 9MB で済んでいる(以前は 15MB ぐらいあった)。よい傾向なので今後もキープしてほしい。 Perl が 5MB しか消費していないのは称賛されるべき。 組み込み向け言語はどれもフットプリントが小さい(Bash や Zsh よりも小さい)。どの言語も VM 方式なので、フットプリントが大きいのを VM のせいにしてはいけないことがわかる。 dash と tcsh と pocketlang が極端に小さい。たぶん libreadline など余計なものを使ってないのだろう。 GC の方式による違いは分からなかった。たとえば世代別GCを採用するとフットプリントが増えるとか、そういうのがあるなら知りたい。 上の一覧にはないけど、将来は WebAssembly 用 VM がインタプリタ言語界を席巻するでしょう。Server-side Kotlin は JVM よりも WASM VM 上で動かすことが多くなり、JVM と CLR のシェアはぐっと下がるでしょう。 まとめ Railsプロセスなら仕方ないにしても、そうではないRubyプロセスが起動直後で 20MB 以上も消費するのは勘弁してほしいです(最近のPythonを見習おう)。AWS EC2 t3.nano のメモリ量が 512MB であり、OSや他プロセスによる使用分などを除いて実質的に使えるのが 300MB だとすると、20MB は 7% になります。Rubyを起動するだけで使用可能メモリ量の 7% も消費すると考えれば、あまり歓迎したくない数字です。今はまだJVMやV8よりましとはいえ、Rubyは「軽量」な言語であってほしいと思います。 おまけ:Rubyプロセスの起動時間 Ruby 3.1.0-preview1 における起動〜終了までの時間を、rubygems が有効なときと無効にしたときとで比較しました。前者が 98ms、後者が 18ms で、5倍以上の違いがあることが分かります。メモリフットプリントだけでなく起動速度の観点からも、rubygems の軽量化が望まれます。 $ time ruby -e 'nil' # rubygems 有効 real 0m0.098s user 0m0.078s sys 0m0.020s $ time ruby -e 'nil' --disable=gems # rubygems 無効 real 0m0.018s user 0m0.005s sys 0m0.013s おまけ:プロセスデーモン化ツール「reraise」 もともと本記事の発端は、Python 製の Supervisor というプロセスデーモン化ツールが起動するだけで23MBもメモリを消費したり、Ruby 製の God が33MBも消費することに嫌気が差し、この手のツールをもし書き直すならどの言語がいいだろうかと検討したことがきっかけです。最終的にはシェルスクリプトを使って「reraise」というツールを作ったので、よければ使ってみてください。メモリフットプリントは2MB未満で済んでおり1、また root 権限なしで利用できます。 reraise ・・・ a service process supervisor tool, like runit, dameontools, Supervisor (python), or God (ruby). https://github.com/kwatch/reraise/ メモリフットプリントに関してはC言語製のツールがいちばん優れており、daemontoolsやrunitはメモリフットプリントが1MB未満です。ただどちらもCLIでの操作がいまいちであり、ドキュメントもinitとして使う方法に多くを割いているのであまり分かりやすくはないと思っています。 ↩
- 投稿日:2021-12-05T14:46:28+09:00
【ruby】条件演算子を用いたif文
条件演算子とは? 今回のトピックは、rubyの基礎である条件演算子を用いた プログラミングを解説していこうと思います。 if文を使用しなくても、短く書けるので、 リーダブルコードになるためとてもいいと思いますよ^^ ぜひ、覚えていってくださいね! 条件文 まず、初めによく見る条件文というのはif文だと思います。 例えばこんな感じのやつ↓ example def country(language) if language == "Japan" "Japanese" elsif language == "the us" "English" elseif language == "Italy" "Italy" else "WHAT???" end end language = Japan puts country(language) ifの後に続けて書くのが、一番初めに習う if文の条件式かと思います。 しかしもっと、短く書くこともできるのが 条件演算子なんですね^^ 短いif文とかであれば、条件演算子で書くといいですよ。 次にて紹介いたしますね! 条件演算子 それでは、条件演算子についてまずコードを 見ていきましょうか! 条件演算子 def enzanshi(number) number > 10 ? "true" : "false" end number = 5 puts enzanshi(number) 上だけで処理が完成してしまうんですね。 短くていいですよね^^ 見方としても簡単です。 number > 10の条件を見て 正であればtrueの左側が実行されます。 falseであれば右側が実行されます。 注意としては、trueの位置にfalseと書いても 左側が原則としてtrueの位置になるため 左側を正そして、右側を誤として覚えてくださいね! まとめ いかがでしたでしょうか? こんな簡単にできてしまうなんてびっくりですね^^ それでは、また次のruby講座でおあいしましょう! ちゃお!
- 投稿日:2021-12-05T14:11:08+09:00
英語ディクテーション用にyoutube-dlでダウンロードしたvtt形式の字幕ファイルをPlain Textに変換するrubyスクリプト
vtt形式は字幕表示に特化したフォーマットなので、人間が目で見るにはなかなか厳しいものがあります。 そこで、人間が見るのに適したPlain Textに変換するrubyスクリプトを作りました。 vtt2txt.rb #!ruby valid_texts = [] while line = gets line.strip! if line =~ /-->/ valid_texts << gets.strip end end lastline = nil valid_texts.each do |line| if line != lastline puts line lastline = line end end YouTubeのCNN 10の自動生成字幕で動作確認済みです。
- 投稿日:2021-12-05T09:54:31+09:00
Rubyの新しいデバッガを試してみる
はじめに Ruby 3.1.0 の目玉機能の1つに新しいデバッガがあります。 私は、このデバッガの2つの特徴に注目しました。 1つは、リモートデバッガ。もう1つは、DAPに対応していることです。 新しいデバッガのインストール デバッガ自体は、Ruby 3.0 でも使うことができます。 $ ruby -v ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux] デバッガをインストールしてみましょう。 $ gem install debug デバッガのバージョンは、1.3.4です。 $ rdbg -v rdbg 1.3.4 リモートデバッガ リモートデバッガは、別のサーバーで動いているデバッガに接続してデバッグできる機能です。 以下のような簡単なプログラムで少し試してみましょう。 target.rb a = 1 b = 2 c = 3 d = 4 p [a, b, c, d] リモートデバッガを試してみる コンソールからデバッガを起動し、別のコンソールからTCP/IPで接続してデバッグの操作をしてみます。 まずは、コンソールからデバッガを起動します。デバッガのコマンドは、 rdbg です。デバッガを実行するときに、リモートでデバッグできるように -O オプションを指定します。また、 --port でポート番号を指定します。 コンソール1 $ rdbg -O --port 12345 target.rb DEBUGGER: Debugger can attach via TCP/IP (127.0.0.1:12345) DEBUGGER: wait for debugger connection... メッセージから、デバッガの接続を待ち受けていることがわかります。 では、別のコンソールからTCP/IPで接続してみましょう。 接続する場合も rdbg コマンドを使います。 -A オプションを指定し、--port オプションで接続先ポートを指定します。 コンソール2 $ rdbg -A --port 12345 [1, 5] in target.rb => 1| a = 1 2| b = 2 3| c = 3 4| d = 4 5| p [a, b, c, d] =>#0 <main> at target.rb:1 (rdbg:remote) (rdbg:remote) とプロンプトが表示され、リモートでデバッガが動作されたのがわかります。 また、1つ目のコンソールには、リモートから接続があったメッセージが出力されます。 コンソール1 DEBUGGER: Connected. いくつか、2つ目のコンソールからdebugの操作をしてみます。 n で1行だけ実行します。 コンソール2 (rdbg:remote) n # next command [1, 5] in target.rb 1| a = 1 => 2| b = 2 3| c = 3 4| d = 4 5| p [a, b, c, d] =>#0 <main> at target.rb:2 (rdbg:remote) 変数 a の値を参照してみます。 コンソール2 (rdbg:remote) a 1 (rdbg:remote) info コマンドを実行してみましょう コンソール2 (rdbg:remote) info # command %self = main a = 1 b = nil c = nil d = nil (rdbg:remote) c コマンドで最後まで実行します。 コンソール2 (rdbg:remote) c # continue command $ 最初のコンソールに接続が切れたことが出力され、デバッガが終了します。 shell:コンソール1 [1, 2, 3, 4] DEBUGGER: Disconnected. 何が嬉しいのか 例えば、別のサーバーでデバッガを起動して、自分の端末からリモートで接続してデバッグするとか、docker のコンテナでデバッガを起動しておいて、別コンソールから接続するとか、そういったことに利用できそうです。 DAP と NeoVim DAP は、Debug Adapter Protocol の略です。DAPに対応したエディタ(DAPに対応したクライアントなら何でも良い)であれば、エディタ経由でRubyのデバッガを操作できます。 Visual Studio Code には、VSCode rdbg Ruby Debugger があります。 このプラグインをインストールすれば、 Visual Studio Code で、Rubyのデバッグできるようになるようです。 が、私は NeoVim を使っています。NeoVim から使えないかと思って調べました。 nvim-dap を見つけました。 このプラグイン単体だと使いづらいので、自分でRubyのデバッガ用の設定を追加するプラグイン nvim-dap-ruby を作って公開しました。 nvim-dap と nvim-dap-ruby の2つのプラグインを NeoVim に追加すれば、NeoVim が Ruby のデバッガと連携して動作します。 2つのプラグインを追加したあとで、 target.rb を NeoVim で開きます。 nvim target.rb では、Rubyのデバッガを起動してみましょう。 :lua require('dap').continue() と入力します。 続けて Enter キーを押します。選択肢が表示されるので、ここでは、 1 と入力してEnterキーを押します。 1行目がハイライトされます。 続けて、 :lua require('dap').repl.open() と入力して、 Enter キーを押します。 エディタのカーソルを下のバッファに移動してから i キーを押して入力モードにすると dap > というプロンプトが表示されます。 このプロンプトでデバッガを操作することができるようになります。 以下の画像は、 .n と入力して次の行まで処理を進めてから、 a と入力して、変数 a の値を参照しているところです。 まとめ Rubyの新しいデバッガの機能のうち、リモートデバッガとDAPを試してみました。 あなたも新しいRubyのデバッガを試してみてはいかがでしょうか?
- 投稿日:2021-12-05T09:47:40+09:00
【Rails】ユーザーの状態に応じた対話機能をLINEbotで実現する【LINE Messaging API】
概要 本記事は RUNTEQアドベントカレンダー 2021 の7日目の記事となります! 個人開発でゴミ捨て日を通知してくれるLINEアプリを開発したのですが、 その中で実装するのに苦労した「ユーザーの状態を把握する」方法を記事にします。 一般的なRailを使用したWebアプリケーションでは、Cookieを利用したユーザー識別やフォームを利用してユーザーの送信する内容を一挙に受け取るなどができますが、LINE Messaging APIではこれらが使えません... そこで私が実装した、Railsのみでユーザーの状態を保持する方法を紹介します。 これからRails x LINE Messaging API を試してみたい方の参考になれば幸いです。 ※ LINE Developperへの登録や設定、基本的な実装についての概要などは他に多数の素晴らしい記事があるため、割愛させていただきます。(こちらの記事など) ※ 私はいわゆる未経験エンジニアですので、あまり綺麗なコードではないかもしれませんし、他にもっといい方法があるかもしれません。マサカリ歓迎です。 どんな実装にしたかったか? まず、どんな実装にしたかったかを説明します。 ユーザーがゴミ捨ての予定を登録するという作業で、 「ゴミ捨ての予定を次々と質問され、それに答えていくと登録できる。」 というシナリオで実装したいと思いました。 具体的には、 下図のようにゴミの名前・収集する曜日・周期を答えていき 最後に通知してほしい時間を答えれば、ゴミ捨ての登録完了! という流れです。 しかし、LINEユーザーから送られてくる情報は主に ユーザーの識別情報 送信したテキスト の2つのため、ユーザーの状態の把握(セッション管理)まではできません。 例えば、「1」というテキストが送られてきただけでは「毎週」なのか「月曜日」なのか分からないということです。 実装 結論から言うと、Userモデルに対話状態を表すstatusカラムを持たせ、対話内容を記録するMessageモデルを作成しました。 コードで説明していきます。 Model Userモデルに状態を管理するstatusカラムを追加しました。 user.rb class User < ApplicationRecord ... has_many :messages, dependent: :destroy enum status: { top: 0, registration: 1, show_next: 2, add_day_of_week: 3, add_cycle: 4, add_notification: 5 } end Messageモデルを作成し、Userモデルに対し1対多の関連付けをしました。 message.rb class Message < ApplicationRecord belongs_to :user end ※ 他のモデルについては省略します Controller コントローラーの内容は少し複雑かと思いますので、4段階に分けて説明していきます。 1. 基本形 まずは基本的なオウム返しの実装です。 main_actionメソッドの中でユーザーの返信内容などによって条件分岐させることによって、 レスポンスメッセージを書き換えることができます。 これ以降はmain_actionメソッドに注目し、書き加えていきます。 linebot_controller.rb # オウム返しする LINEbot class LinebotController < ApplicationController require 'line/bot' def client @client ||= Line::Bot::Client.new { |config| config.channel_secret = ENV["LINE_CHANNEL_SECRET"] config.channel_token = ENV["LINE_CHANNEL_TOKEN"] } end def callback body = request.body.read signature = request.env['HTTP_X_LINE_SIGNATURE'] head :bad_request unless client.validate_signature(body, signature) events = client.parse_events_from(body) events.each { |event| case event when Line::Bot::Event::Message case event.type when Line::Bot::Event::MessageType::Text # 条件分岐 main_action(event) message = { type: 'text', text: @response } client.reply_message(event['replyToken'], message) end end } head :ok end # この中身だけ書き換える def main_action(event) # ユーザーの返信したメッセージ text = event.message['text'] @response = text end end 2. user の 状態によって返信内容を変える いちいちupdateが走ってしまいますが、やり取りの度にstatusカラムの値を更新して状態を保持します。 これによって、「ゴミの名前は?」「曜日は?」と言ったように、レスポンスを投げる順序を制御できます。 linebot_controller.rb class LinebotController < ApplicationController ... def main_action(event) # ユーザーの識別 @user = User.find_or_create_by(line_id: event['source']['userId']) text = event.message['text'] # statusカラムの値で分岐 case @user.status when 'top' case text # 1 が返信されたら、登録モードへ when '1' @response = "登録モードへ移行します。\nゴミの名前を入力してください。" @user.registration! end ### 登録モード ### when 'registration' @response = '曜日を入力してください。' @user.add_day_of_week! when 'add_day_of_week' @response = '周期を入力してください。' @user.add_cycle! when 'add_cycle' @response = '通知する時間を入力してください。' @user.add_notification! when 'add_notification' @response = '登録完了です。' # ゴミの登録が完了したら、topに戻す @user.top! end end end 3. ユーザーの返信内容を保存する ユーザーのいくつかの返信を組み合わせてゴミ捨て日のレコードを生成したいため、以前のユーザーの返信内容を保存しておく必要があります。 ここでMessageモデルを使用します。 以下のコードのように、ユーザーが返答した内容(ゴミの名前など)をDBに保存しておきます。 そして、登録完了メッセージと同時にゴミを確定し、ゴミのレコードを生成します。 linebot_controller.rb class LinebotController < ApplicationController ... def main_action(event) @user = User.find_or_create_by(line_id: event['source']['userId']) text = event.message['text'] case @user.status when 'top' case text when '1' @response = "登録モードへ移行します。\nゴミの名前を入力してください。" @user.registration! end ### 登録モード ### when 'registration' @user.messages.create!(text: text) # ゴミの名前を覚えておく @response = '曜日を入力してください。' @user.add_day_of_week! when 'add_day_of_week' @user.messages.create!(text: text) # ゴミの曜日を覚えておく @response = '周期を入力してください。' @user.add_cycle! when 'add_cycle' @user.messages.create!(text: text) # ゴミの周期を覚えておく @response = '通知する時間を入力してください。' @user.add_notification! when 'add_notification' # ここでゴミレコードを生成する @response = '登録完了です。' @user.top! end end end 4. 完成! ここまでの知識とその他諸々を組み合わせ、完成したコードです。 これで最初の写真のようなやり取りができるようになります! # main_action 完成形 class LinebotController < ApplicationController ... def main_action(event) @user = User.find_or_create_by(line_id: event['source']['userId']) text = event.message['text'] .tr(" \r\n\t", '') # 空白の除去 .tr('0-9', '0-9') # 全角数字を半角に case @user.status when 'top' case text # 1 が返信されたら、登録モードへ when '1' @response = <<~TEXT ゴミの名前を何にする?一つだけ答えてね! (例)燃えるゴミ TEXT @user.registration! end ### 登録モード ### when 'registration' # ゴミの名前を保存! @user.messages.create!(text: text) @response = <<~TEXT 収集日はいつにする? 1: 月曜日 2: 火曜日 3: 水曜日 4: 木曜日 5: 金曜日 6: 土曜日 7: 日曜日 0: ゴミの登録をやめる\n TEXT @user.add_day_of_week! when 'add_day_of_week' # 収集する曜日を保存! @user.messages.create!(text: text) @response = <<~TEXT 周期はどうする? 1: 毎週 2: 今週から隔週 3: 来週から隔週 4: 第1・3 5: 第2・4 0: やめる TEXT @user.add_cycle! when 'add_cycle' # 収集する周期を保存! @user.messages.create!(text: text) @response = <<~TEXT 何時に通知する? 10分単位で設定できるよ! (例1)6:40 (例2)7時20分 (例3)8時半 0: ゴミの登録をやめる TEXT @user.add_notification! when 'add_notification' text.gsub!(/時|分|半/, '時' => ':', '分' => '', '半' => '30') # 00:00-23:50のフォーマットに則っているかどうかの判定 if text.match(/^([01]?[0-9]|2[0-3]):[0-5]0$/) # やり取りしたゴミの情報を引き出す trash_name = @user.messages[-3].text day_of_weeks = @user.messages[-2].text cycle_num = @user.messages[-1].text collection_days = CollectionDay.find(day_of_weeks) cycle = Cycle.find(cycle_num) # ゴミと通知時間を作成する trash = @user.trashes.create!( name: trash_name, cycle: cycle, collection_days: [collection_days].flatten ) Notification.create!(trash: trash, notify_at: text) @response = <<~TEXT 登録したよ! 「#{trash.name}」の収集日は 「#{trash.cycle.name_i18n}」の「#{trash.collection_days_list}」 「#{l trash.notification.notify_at}」に通知するからね! TEXT @user.top! else @response = '正しく入力してね!' end end end end 最後に ここまで読んでいただき、ありがとうございます。 以上が私の実装したコードの一部です。 段々長くなっていくコードで読みづらいとは思いますが、、、みなさまの参考になれば幸いです。 参考 今更ながらRails5+line-bot-sdk-ruby+HerokuでLineBot作成してみたら、色々詰まったのでまとめました。 【LINE×Rails】Rails初学者も作れるLINE Botアプリケーション
- 投稿日:2021-12-05T09:46:13+09:00
gemを使ってスクレイピングしてみた
はじめに 本記事は、2021 RUNTEQアドベントカレンダー5日目の記事になります! 現在ポートフォリオを作成しており、その作成過程でスクレイピングを用いて必要な情報を取得しDBに格納しました。今回はそこで使用したgem Mechanizeについて書いていこうと思います! この記事でわかること gem Mechanizeについて できること 提供しているメソッド PFで用いたDBに格納する方法 できること まず概要を公式より見てみます Mechanizeライブラリは、ウェブサイトとの対話を自動化するために使用されます。Mechanizeは、自動的にクッキーを保存、送信し、リダイレクトを行い、リンクをたどり、フォームを送信することができます。フォームフィールドは入力して送信することができます。また、Mechanizeは、あなたが訪問したサイトを履歴として記録します。 ざっくりとした解釈ですが、自動的にwebサイトに訪問して、フォームがあれば入力もできちゃうよ〜ってことですね。 webサイトを自動で訪問できるので、そこでスクレイピングしたいページにとび、取得したいデータのみ抽出するという処理も書けちゃうわけです 提供しているメソッド 下準備 メソッドの説明に入っていくのですが、gemをinstallすると、mechanizeクラスが使えるようになります。 公式では下準備としてインスタンス化してくださいとあるので、下記のように設定します # 公式には下記の記載がありますが、Ruby1.9以降では必要ないのでコメントアウトします # require 'rubygems' require 'mechanize' class Scraping agent = Mechanize.new end get スクレイピングしたいwebサイトのHTMLを取得してくれます。ここでは試しにマクドナルドのサイト情報を取得してみましょう require 'mechanize' class Scraping def self.mcd agent = Mechanize.new page = agent.get("https://www.mcdonalds.co.jp/") end end コンソールで実行してみます irb(main):001:0> Scraping.mcd => #<Mechanize::Page {url #<URI::HTTPS https://www.mcdonalds.co.jp/>} {meta_refresh} {title "マクドナルド公式サイト | McDonald's Japan"} {iframes #<Mechanize::Page::Frame nil ... #<Mechanize::Page::Link "ホーム" "/"> #<Mechanize::Page::Link "\n" + "メニュー\n" + "\n" + "\n" "/menu/"> #<Mechanize::Page::Link "おすすめ" "/menu/"> #<Mechanize::Page::Link "バーガー" "/menu/burger/"> #<Mechanize::Page::Link "セット" "/menu/set/"> #<Mechanize::Page::Link "サイドメニュー" "/menu/side/"> #<Mechanize::Page::Link "ドリンク" "/menu/drink/"> #<Mechanize::Page::Link "ハッピーセット" "/menu/happyset/"> #<Mechanize::Page::Link "朝マック" "/menu/morning/"> #<Mechanize::Page::Link "夜マック" "/menu/dinner/"> #<Mechanize::Page::Link "スイーツ" "/menu/dessert/"> #<Mechanize::Page::Link "マックカフェ" "/menu/barista/"> #<Mechanize::Page::Link "キャンペーン" "/campaign/"> #<Mechanize::Page::Link "ファミリー" "/family/"> #<Mechanize::Page::Link "私たちの責任" "/scale_for_good/"> #<Mechanize::Page::Link "お店・サービス" "/shop/"> ... これ以上は長くなってしまうので省きますが、これだけでもざっくりページの情報を得られているのがわかると思います〜〜 search cssを指定するのと同じ要領で要素を指定できます。タグはそのまま指定。 classを指定する際は、「.(ドット) class名」。idを指定する際は、「#(シャープ) id名」といった感じですね。返り値は配列の形式で返ってきます。 試しにli a を指定し、格納した配列を出力してみます class Scraping def self.mcd agent = Mechanize.new page = agent.get("https://www.mcdonalds.co.jp/") elements = page.search('li a') puts elements end end コンソール irb(main):001:0> Scraping.mcd <a href="/" class="no-underline block h-xs line-height-md p-2:lg my-0 font-semibold active">ホーム</a> <a href="/menu/" class="no-underline block h-xs line-height-md p-2:lg my-0 font-semibold flex justify-between items-center"> <span>メニュー</span> <span class="header-submenu-toggle-btn"></span> </a> <a href="/menu/" class="no-underline">おすすめ</a> <a href="/menu/burger/" class="no-underline">バーガー</a> <a href="/menu/set/" class="no-underline">セット</a> <a href="/menu/side/" class="no-underline">サイドメニュー</a> <a href="/menu/drink/" class="no-underline">ドリンク</a> <a href="/menu/happyset/" class="no-underline">ハッピーセット</a> ... 先程とは違い、HTMLの情報を取得できています! at searchメソッドと同じ要領で要素を指定し、該当する1件のみ取得することができます 先程の文法のまま、searchの部分をatメソッドに変更すると、コンソール上では1件のみ取得できるのがわかります! irb(main):001:0> Scraping.mcd <a href="/" class="no-underline block h-xs line-height-md p-2:lg my-0 font-semibold active">ホーム</a> get_attribute タグの属性値を取得することができます。属性値とは例えばaタグの中の、href属性に指定されてるURLのことを指します。先程searchメソッドで取得した配列のうち、get_attributeメソッドを使用して、URLの情報を取得してみます class Scraping def self.mcd agent = Mechanize.new page = agent.get("https://www.mcdonalds.co.jp/") elements = page.search('li a') urls = [] elements.each do |element| urls << element.get_attribute(:href) end puts urls end end コンソール irb(main):001:0> Scraping.mcd / /menu/ /menu/ /menu/burger/ /menu/set/ /menu/side/ /menu/drink/ /menu/happyset/ /menu/morning/ /menu/dinner/ /menu/dessert/ /menu/barista/ /campaign/ /family/ /scale_for_good/ /shop/ /order /recruit/crew_recruiting/ /shop/mobile/ inner_text searchメソッドやatメソッドではタグも含めて全て取り出していましたが、inner_textメソッドではタグの中身のテキストだけを取り出すことができます。 searchで取得した情報にinner_textメソッドを使用してみましょう class Scraping def self.mcd agent = Mechanize.new page = agent.get("https://www.mcdonalds.co.jp/") elements = page.search('li a').inner_text puts elements end end コンソール irb(main):001:0> Hoge.mcd ホーム メニュー おすすめバーガーセットサイドメニュードリンクハッピーセット朝マック夜マックスイーツマックカフェキャンペーンファミリー私たちの責任お店・サービス モバイルオーダー 会員登録なし! スマホでスムーズにご注文・お支払い 採用情報公式アプリ 2020.04.07 「マックシェイク®」と「プッチンプリン」が史上初のコラボ 江崎グリコのプッチンプリン担当者お墨付きの味の再現性! マックシェイク® “プッチン”できないけど プッチンプリン 4月13日(月)から期間限定販売! 自分で“プッチン”出来ちゃう?!「ちょい足しカラメルソース付」を特別限定で実施 ... テキストだけ取得することができました。 このような形で、サイトに応じてメソッドを使用すればDBに必要な情報を好きなように取得し、格納することもできそうです PFで用いたDBに格納する方法 上記のメソッドを活用して、実際にDBにスクレイピングしたデータを格納していきます。 執筆時のマクドナルドのハンバーガー一覧ページから、簡易的ですが商品名(name)、価格(price)、説明(description)のカラムを持ったfoodsテーブルを作成し、こちらに格納していきます。テーブル作成は割愛します require 'mechanize' class Scraping def self.mcd agent = Mechanize.new links = [] page = agent.get("https://www.mcdonalds.co.jp/menu/burger/") # elementsには商品詳細へのHTMLが格納されています elements = page.search('.product-list-card a') # each doメソッドで各HTMLに対し処理を実行。get_attributeメソッドを活用してリンクを取得しlinksに格納しています elements.each do |ele| unless (ele.get_attribute(:href) == '/shop/mobileorder/') links << ele.get_attribute(:href) end end # 取得した各リンクを引数としget_foodメソッドを実行します links.each do |link| get_food("https://www.mcdonalds.co.jp/#{link}") end end まず第一段階で一覧ページから、各商品詳細情報のリンクを取得し、linksの配列に格納していきます require 'mechanize' class Scraping def self.mcd ... end def self.get_food(link) agent = Mechanize.new page = agent.get(link) name = page.at('.pdp__product-info h1').inner_text price = page.at('.items-baseline span:nth-child(3)').inner_text description = page.at('.pdp__product-info p').inner_text Food.create(name: name, price: price, description: description) end end 第二段階で、取得した各商品詳細情報から、必要な情報を指定して、取得し、最後にcreateします コンソール上で実行すると以下のようにDBに格納できていることがわかります! 終わりに 最後までご一読いただきありがとうございました! 同じように、ポートフォリオ作成を考えていらっしゃる方の参考になれば幸いです! また、もし間違っている点等あれば、ご指摘いただけると嬉しいです。ありがとうございました! 参考URL Github 公式 Rails スクレイピング手法 Mechanizeの使い方 スクレイピングでサイト情報を取得し、データベースにストアするまでの流れをまとめてみた[Rails]
- 投稿日:2021-12-05T03:01:20+09:00
国民の祝日.csvを使いやすい形式にパースしてみた
概要 国民の祝日.csvを、使いやすい形式にパースする課題にチャレンジしました。 置換前と置換後は、以下となります。 置換前 app/public_holiday.csv 平成28年(2016年),,平成29年(2017年),,平成30年(2018年), 名称,月日,名称,月日,名称,月日 元日,2016/01/01,元日,2017/01/01,元日,2018/01/01 成人の日,2016/01/11,成人の日,2017/01/09,成人の日,2018/01/08 建国記念の日,2016/02/11,建国記念の日,2017/02/11,建国記念の日,2018/02/11 春分の日,2016/03/20,春分の日,2017/03/20,春分の日,2018/03/21 昭和の日,2016/04/29,昭和の日,2017/04/29,昭和の日,2018/04/29 憲法記念日,2016/05/03,憲法記念日,2017/05/03,憲法記念日,2018/05/03 みどりの日,2016/05/04,みどりの日,2017/05/04,みどりの日,2018/05/04 こどもの日,2016/05/05,こどもの日,2017/05/05,こどもの日,2018/05/05 海の日,2016/07/18,海の日,2017/07/17,海の日,2018/07/16 山の日,2016/08/11,山の日,2017/08/11,山の日,2018/08/11 敬老の日,2016/09/19,敬老の日,2017/09/18,敬老の日,2018/09/17 秋分の日,2016/09/22,秋分の日,2017/09/23,秋分の日,2018/09/23 体育の日,2016/10/10,体育の日,2017/10/09,体育の日,2018/10/08 文化の日,2016/11/03,文化の日,2017/11/03,文化の日,2018/11/03 勤労感謝の日,2016/11/23,勤労感謝の日,2017/11/23,勤労感謝の日,2018/11/23 天皇誕生日,2016/12/23,天皇誕生日,2017/12/23,天皇誕生日,2018/12/23 置換後 { 2016 => { #<Date: 2016-01-01 ((2457389j,0s,0n),+0s,2299161j)> => '元日', #<Date: 2016-01-11 ((2457399j,0s,0n),+0s,2299161j)> => '成人の日', # ... #<Date: 2016-11-23 ((2457399j,0s,0n),+0s,2299161j)> => '勤労感謝の日', #<Date: 2016-12-23 ((2457399j,0s,0n),+0s,2299161j)> => '天皇誕生日', }, 2017 => { #<Date: 2017-01-01 ((2457399j,0s,0n),+0s,2299161j)> => '元日', #<Date: 2017-01-09 ((2457399j,0s,0n),+0s,2299161j)> => '成人の日', # ... #<Date: 2017-11-23 ((2457399j,0s,0n),+0s,2299161j)> => '勤労感謝の日', #<Date: 2017-12-23 ((2457399j,0s,0n),+0s,2299161j)> => '天皇誕生日', }, 2018 => { #<Date: 2018-01-01 ((2457399j,0s,0n),+0s,2299161j)> => '元日', #<Date: 2018-01-08 ((2457399j,0s,0n),+0s,2299161j)> => '成人の日', # ... #<Date: 2018-11-23 ((2457399j,0s,0n),+0s,2299161j)> => '勤労感謝の日', #<Date: 2018-12-23 ((2457399j,0s,0n),+0s,2299161j)> => '天皇誕生日', }, } このような形式のハッシュに置換するために自分が書いたコードと、回答例のコードを比較して、ポイントをまとめていきたいと思います。 自分が書いたソースコード app/public_holiday_converter.rb require 'csv' require 'date' class PublicHolidayConverter def initialize @data_list = CSV.read('app/public_holiday.csv') end def call pair_array = @data_list.flatten.each_slice(2).map { |a| a }.drop(6) pair_hash = pair_array.map do |a, b| Hash[Date.parse(b), a] end name_hash = pair_hash.each_slice(3).map { |a| a } transform_hash(name_hash) end private def transform_hash(name_hash) year_hash1 = { 2016 => name_hash.map { |a| a[0].flatten }.to_h } year_hash2 = { 2017 => name_hash.map { |a| a[1].flatten }.to_h } year_hash3 = { 2018 => name_hash.map { |a| a[2].flatten }.to_h } hash = {} hash.merge(year_hash1).merge(year_hash2).merge(year_hash3) end end 自分で解いてみた結果です。 次に出てくる回答例とは程遠いですね。。 置換後の形式にパースされてはいますが、全体的に可読性が低いと感じます。 良くなさそうな箇所をリストアップしてみます。 全体的に汎用性がない作りになっている。 年が増えた場合に、この実装では対応出来ていない。 変数名が適切でない。 arrayが入っているのに、_hashという変数名が付いている。 aやbという変数名では、中身が想像できない。(mapのブロック引数) 冗長な記述や、無駄な記述がある。 year_hash1等、同じような処理を書いている。 hash = {}は不要。 public_holiday.csvの最初の行を活用できていない。 上手く使用できずdrop(6)で捨ててしまっている。 回答例のソースコード ※ 先輩にいただいた回答例です。 app/public_holiday_converter.rb require 'csv' require 'date' class PublicHolidayConverter HOLIDAY_ROW_START_POINT = 2 def initialize @data_list = CSV.read('app/public_holiday.csv') end def call holiday_infos_array.each_with_object({}) do |holiday_info_of_year, hash| year = year(holiday_info_of_year[0][0]) holiday_info_of_year[HOLIDAY_ROW_START_POINT..].each do |holiday_info_of_day| hash[year] ||= {} hash[year][Date.parse(holiday_info_of_day[1])] = holiday_info_of_day[0] end end end private def holiday_infos_array @data_list.transpose.each_slice(2).map(&:transpose) end def year(date_text) date_text[/\d{4}/].to_i end end 私が気付けなかったことや困っていたところが解決されているので、良いと思ったポイントをリストアップしたいと思います。 固定値が定数化されている。(HOLIDAY_ROW_START_POINT) each_with_objectを使用している。 ハッシュで値が格納されるというイメージがつきやすい。 変数名が分かりやすい。 メソッドがそれぞれの役割に分かれているので、返ってくる値のイメージがつきやすい。 public_holiday.csvの最初の行が使用されている。 正規表現で4文字の数字を取得している。 まとめ 最初に課題を見たときは、それほど難しくなさそうに思いましたが、いざやってみるとなかなか思うように行かず、苦戦してしまいました。。 繰り返しの処理が多かったので、Enumerableのメソッドともっと仲良くならないといけないなと感じました。 今回学んだポイントを生かして、可読性の高いコードが書けるようになりたいと思います。
- 投稿日:2021-12-05T00:37:31+09:00
現役ミュージシャンが実務未経験から実務案件に挑戦。コードを書く事だけがエンジニアの仕事ではないと気付かされる!
自己紹介 初めまして!現在音楽講師をしているhirokiと申します。 未経験からエンジニアへの転職を目指し日々学習をしております。 楽器歴はもう17年と日々演奏技術を磨きながら最新の音楽情報をキャッチアップしております。 そんな私がプログラミングを始めたのは、別のミュージシャンの方が自分のホームページを持っていたので「僕も作ってみたいな」という気持ちからでした。 そこからプログラミングについて調べみるとそのページにはログイン機能などがあり、これはなんだと?と思い調べてみました。 調べていくと、Webアプリケーションが作れ、雛形が揃っていて尚且つ学びやすいというフレームワークがあるという記事を見つけました。そこで出会ったのがRuby on Railsでした。まずは始めないと正解・不正解も分からないので、何事も触れてみようと思い学習をスタートしました。 独学で勉強し少しずつ理解が深まるごとにどんどんプログラミングにハマっていきました。 ある出来事で転職したいと思うようになりました。それは、目の前でWebサービスを使っている生徒さんの困っている姿でした。 生徒さんから『バンドを組んでライブをしたいのにメンバーが見つからないです』っという相談を頂きました。私自身バンドメンバー募集サイトを利用し、新たなメンバーと出会えた経験があることから、そのサービスを勧めました。 しかし生徒様から、『自分で探さなくても自動で紹介してくれる機能が欲しい、探しにくいな』と感想を頂きました。 『そのような機能をがあるのかな?』と思い調べていくとある機能に注目しました。 それはプロフィールや求める人物を自動でマッチングしてくれるようなシステムです。きっかけは恋愛マッチングアプリのシステムからです。 しかし、構想があってもスキルがなければ実現できません。 『私がサービスを改善する事で問題解決し、役に立ちたいな』と思うようになりました。 目の前に困っている生徒さんがいるのに、改善出来ないことへの悔しさが込み上げてきました。 悔しい経験から、技術探求し、IT技術を通し、『人の役に立てようになりたい』と思うようなりました。 この経験から私自身エンジニアとして成長し、IT技術を通じ、人々の役に立ちたいと思う様になりエンジニアへの転職を決意しました。 案件の概要 実務案件を頂けたのですが、今回クライアントから依頼があった内容は、医院紹介サイト(Rails)のSEOの改善とCore Web Vitalsの改善をしてほしいという案件内容でした。 チームメンバーでタスクを分けしたのですが、私が担当させて頂いたのはCore Web Vitalsの改善でした。(Core Web Vitalsの詳しい内容は後述) メンバー構成と実務期間は、リーダーであるY氏と、私含め3名の未経験者が2ヶ月間実務に取り組みました。 実務案件を申し込んだ理由と経緯 応募したきっかけは、入社前後のギャップを埋めたいと思い応募しました。実務を体験をすることによって独学でプログラミングに触れている時と実務とのギャップを少しでも埋めることができ、入社後の成長、心構えを知る事が出来るのではないかと考えたからです。 もう一つは書いてあることしかできない(言われたことしかできない)人間ではなく、自走して課題を解決できる人間であることを少しでも証明したいと思ったため、今回の実務案件に挑戦させていただきました。 実務経験を積みたいと思い検索する中で、実務の募集を見つけました。どうしても実務経験を積みたいと思い2度も応募フォームを送信し気持ちが届いたのか実務案件に参画させて頂くことになりました。 1. 実装準備 実装を始める前に、初めて聞く用語の理解や、計測ツールを使っての問題の洗い出し方法を理解してから着手しました。まずは実装準備をどの様にしてきたのかを説明していきたいと思います。 Core Web Vitalsとは? Core Web Vitalsの知識が無いと何も始まりませんので実務ではしっかり用語の意味を理解してから着手しました。 ここでは閲覧して頂いている皆様に簡潔にお伝えさせて頂きますm(_ _)m Core Web Vitalsとは ユーザー体験を向上させるためのGoogleが定めたガイダンス 一言で表すとユーザーが感じる使い勝手を評価する指標 Core Web Vitals参考URL https://webtan.impress.co.jp/e/2020/06/05/36210 内容を深ぼると指針が分かりやすく定められていました。 Core Web Vitalsの指針 LCP(Largest Contentful Paint) 読み込みパフォーマンスを測定して数値化したもの。ユーザーがそのWebページのなかで最も有意義なコンテンツをどれだけ早く見ることができるかを表す指標。一言で言うと読み込み時間の事。ページの読み込み開始から2.5秒以内にそのページが描画されることが望ましいとされる。 FID(First Input Delay) ユーザーがWebページを閲覧する際に ページのレスポンスが良いかどうかを測定した数値。 ユーザー体験(UX)の最適化にはこのFIDの数値が 100ミリ/秒未満であることが望ましいとされる。 CLS(Cumulative Layout Shift) ユーザーがページを閲覧する際、 テキストや画像などのコンテンツが快適に表示されているか否かを示す指標。 ユーザー体験(UX)の最適化には、このCLSの値を 0.1未満に維持しなければならない。 参考URL https://mtame.jp/seo/corewebvitals/ 何を改善するのか? Core Web Vitalsの3つの指針を理解したのですが、今回どこの指針の部分を改善するのかを決めないといけません。 Core Web Vitalsの改善方法を調べていくと、ウェブページの読み込み速度をスコア測定してくれるツールである『PageSpeed Insights』というツールをがある事を知りました。ツール内で今回依頼を頂いたサイトURLを入力すると計測してくれます。計測結果で表示された内容で、適切な画像サイズの画像、使用していないJavaScriptの削減、使用していないCSSの削減の3項目が改善の余地があると指摘を受けました。 今回私が施策提案させて頂いたのは、使用していないCSSの削減と、適切な画像サイズの変更の2項目です。 前もって説明させて頂きますが、適切な画像サイズの画像の施策については、実装する事が出来ませんでした、、、 理由は、 指摘のあった画像部分だけを圧縮すればいいのだと施策を勘違いしてしまったこと 使用していないCSSの削減*の施策に時間が掛かってしまったこと の2点です。 こちらに関しては力不足を痛感しました。とても悔しい気持ちでした、、、 しかしこの悔しい気持ちをバネに今後エンジニアとして更に『成長していきたい』と気持ちが強くなりました!! PageSpeed Insights URL https://pagespeed.web.dev/?utm_source=psi&utm_medium=redirect&hl=ja 指摘を受けた内容 適切なサイズの画像 使用していないJavaScriptの削減 使用していないCSSの削減 原因特定 CSS削減の余地があると指摘を受けたのですが、まずは現状どのようにCSSが読み込まれている状況なのか確認しました。すると、assets/stylesheet/apllication.cssに全ファイル読み込まれている記述がありました。 assets/stylesheet/apllication.css @import 'bases/_variable'; @import 'bulma/bulma'; @import 'bases/_base'; @import 'partials/*'; @import 'pages/*'; 更に全ページの共通のレイアウトファイルであるviews/layout/application.html.slimにstylesheet_link_tag 'application'、 media: 'all'で全てのCSSファイルが読み込まれている現状でした。 views/layout/application.html.slim = stylesheet_link_tag 'application' media: 'all' このコードを見てわかったことは、全ページで共通のCSSを読み込んでしまっている事が原因で、ページ表示速度が遅くなっている、ということです。 原因を解決する為の学習 全ページで共通のCSSを読み込んでしまっている事が原因だったのですが、CSS設計の基礎を知らないと解決策の模索が出来ないなと思いました。そこでCSSの事をもっと詳しくなりたいと思い書籍を購入しました。 書籍を読んだ内容を元に今回のCSS設計時に重要となる観点をまとめ、その観点に基づいて最適な設計案を作成しました。目を通して特に重要であると思われる内容を絞り出しました。 参考書籍URL https://www.amazon.co.jp/dp/B0856YMH7L CSSの問題点と原因 CSSの問題点は、ページ数が増えるとCSSもどんどん複雑になり、管理しきれなくなってくる事です。数ページだけのシンプルなサイトであれば大した問題ではありませんが、世の中で使われているWebサイトは、決して小規模なものだけではありません。1ページのみでもWebサイトと呼べますが、上限は限りなく、100ページ、1,000ページ、10,000ページ超のWebサイトも珍しくありません。 そんな状況で何も考えずトップページではp要素を「大きくしたいから、fontsizeを18pxから20pxに変更しよう」とするとどうなるかというと、もちろんトップページだけ見れば意図した通りにp要素は20pxになります。しかしCSSの「スタイリングの内容がCSSファイルを読み込んでいるすべてのページに反映される」という仕様上、他のページのすべてのp要素も20pxになってしまいます。きちんとスタイリングの規則を決めた上でCSSが作成されているのであれば、まだよいですが、規則をきちんと決めていないと、「ページ数が増えると、CSSもどんどん複雑になり、管理しきれなくなってくる」という問題が発生し始めます。 規則のないCSSは、ページ数が10ページを超えただけでもすべてを把握するのは難しいです。そしてWebサイトは、制作して、公開し、終わり、ではありません。その次には情報を更新したり、新たなページを作成したり運用し続けていかないといけません。数ヶ月後の自分が、あるいは他の人が読み解いて、想定外の影響がないように運用していくのは、とても骨が折れる作業です。そしてそんな状態を避けようと規則を定めたとしても、すぐに破綻することがほとんどです。なぜこのようなことが起こってしまうのでしょうか? その原因は「CSSはすべてがグローバルスコープ」です。グローバルスコープとは読み込んだCSSファイル全てが同じスコープで管理されてしまう事。 原因を回避する為のCSS設計とは ではどの様な設計によってCSSを管理し壊れにくくすることができるのかと言うと、具体的には、「予測できる」、「再利用できる」、「保守できる」、「拡張できる」の4つ設計指針がある様です。これはGoogleのエンジニアであるPhilip Walton氏が「CSS Architecture」という記事で提唱した考え方です。 4つのCSS設計指針とは 予測できる スタイリングが期待通りに振る舞うかどうか スタイリングの影響範囲が予測できるか 再利用できる 既存のパーツを別の箇所でも使用したいとき、コードをいちいち書き直したり上書きする手間がない状態 保守できる 新しいモジュールや機能を追加・更新、あるいは配置換えしたとき、既存のCSSをリファクタリングする必要がない状態 拡張できる CSSに携わる人が1人であっても複数からなるチームであっても、問題なく管理できる状態 自分とその他の開発者にとってメンテナンス・管理がしやすくする事 施策提案の設計 上記の4つの設計指針と問題点を元に、現状のクライアントの開発人数が少ないこと、また今後実際に運用するのはクライアントである為、最終的にこのような設計方針で検討する事にしました。 ルールが複雑でない 拡張しやすい 影響範囲がみだりに広すぎない 現状からの修正コストが大きすぎない 何故このように考えたのか ルールが複雑でない 実際に運用し続け、複雑なルールを設定してしまうと今後変更したい時に修正しにくいのでシンプルな設計が必要だと思いました。 拡張しやすい Webサイトは「公開して終わり」ではなく、公開後も運用が続き、その中で既存のページやモジュールに対する変更が発生することも珍しくありません。今後もサービスが大きくなる可能性があると考えたからです。 影響範囲がみだりに広すぎない 影響範囲の広いCSSを修正しようとしても、影響範囲が広いので、どこでレイアウト崩れなどが発生するかわかりません。一度影響範囲の広いコードを書くと、修正と確認の作業コストにずっと悩まされ続けてしまうと考えたのでこの様な方針にしました。 現状からの修正コストが大きすぎない あまりに大きな修正ををしてしまうと依頼の納期に間に合わなくなってご迷惑おかけしてしまうと考え、大きな修正コストは掛けられないと判断しました。 実装方法を模索 設計指針に基づいて、分かりやすく複雑でなく、現状からの修正が大きすぎないというテーマを元にCSSの設計について調べてみました。調べてみるとファイル分割に焦点が当てられている記事を見つけました。 参考記事の内容 ViewファイルとCSSファイルを1対1になるようにファイルパスを揃えて、スコープ用にdata属性を書きます。修正したいViewファイルがあった場合、対となるCSSファイルを修正するだけで他のページに影響を与えず修正出来るのでとても管理しやすくなります。 次の手順で、各ページで必要なCSSファイルだけを呼び出す Viewファイルごとのルート要素に、自身の app/views/ 以降のファイルパス(拡張子抜き)を data-scope-path というデータ属性で設定。 対となる CSS 側では [data-scope-path="..."] という属性セレクタで絞って参照。 ファイルはapp/assets/stylesheets/エントリー名/scopesに格納。 参考記事URL https://tech.basicinc.jp/articles/166 何故この案にしようとしたのか この記事を読んでいくと、 設計を理解しているメンバーがプロジェクトから離れるとCSSの影響範囲を把握するのが大変です。CSS設計の場合、グローバルスコープなので全ページ確認しないといけないという大変な作業が待っています。新しいプロパティを追加する事によって、他のページのスタイルを崩してしまわないかと、複数のページを確認しなければなりません。どれだけCSSが得意な人がプロジェクトに入ったところで、解決は大変です。 とありました。 今後設計を理解しているメンバーが離れる事もあると思います。前任の方がプロジェクトから離れ新しいメンバーがプロジェクトに参加し、新規にプロパティを追加した事によってどこのページに影響しているのか把握出来ず、複数のページを確認しないといけない確認作業が大変になってしまいます。 そこでViewファイルとCSSファイルを1対1になる様に設計しておけば『このページはこのCSSファイルを変更するだけでOK 』と分かりやすいので、メンバーの入れ替わりがあっても管理しやすいのではと考えました。 この方法を使用し、全ページで同時に読み込まれているCSSファイル分割してページ毎にCSSファイルを読み込ませたら、分かりやすく、管理しやすいのではと思いこの案で実装していこうと思いました。 実装方針の提案書 今回の案件に対しクライアントに以下の様に提案書を提出しました。 CSSの設計方針 viewsの各ファイルに対して、以下方針で対応するCSSファイルを作成します ページファイル: CSSと1:1対応させる パーシャルファイル CSSと1:多対応させる 施策提案書を提出し確認を取る 実装方法を確認したところ、大まかな流れですが承諾頂けました。 一つクライアントから、『ページ単位での分割は実装/移行コストが少し高いように感じるのでコントローラー単位での分割に切り替えて欲しい』という依頼を頂きました。 私が提案した内容は、コントローラ単位での分割にも応用出来る内容でした。そこで開発方針を「ページ単位での分割」から「コントローラー単位での分割」に切り替えました。実装方針を固める事が出来たので本格的に実装開始する事にしました。 まず最初にユーザーがアクセスし、表示されるトップページで使用されるコントローラーから施策をスタートしました。順次流入数が多いページで使われるコントローラーの順番で進めていく方向で決まりました。施策内容と施策順序の許可を頂けましたので、ここから実際に手を動かし実装開始となります。 2. 実装開始 まずは全ページ読み込まれてしまっているのでコントローラー毎に必要なCSSファイルを読み込めるようにする為の基盤作りから進めていきました。 全ページの共通のレイアウトファイルであるviews/layout/application.html.slimに対し 1. if文の条件分岐で値が入っていたら個別のCSSを読み込み、なかったら既存のapplication.cssを読み込む stylesheet_link_tag yield(:stylesheet_scope)でビューテンプレートに対応するCSSファイルを呼び出す views/layout/application.html.slim - if content_for?(:stylesheet_scope) = stylesheet_link_tag yield(:stylesheet_scope), media: 'all' - else = stylesheet_link_tag 'application', media: 'all' 2. 設定したいコントローラで扱われるビューファイル全部にvirtual_pathを指定 各ページテンプレートの最上位の要素に記載 app/views/hoge/hoge.html.slim - stylesheet_scope(@virtual_path) 3. メソッドを定義しスコープを受け取れるようにする app/helpers/application_helper.rb STYLESHEET_PATH_PATTERN = /^(?<controller>[a-z_]+?)\/(?<method>[a-z_]*)$/ def stylesheet_scope(path) controller_name = path.match(STYLESHEET_PATH_PATTERN)&.[](:controller) content_for :stylesheet_scope, controller_name end 4. app/assets/stylesheets/直下にCSSファイル作成 コントローラー名の CSSファイルを作成し、コントローラで使われているビューファイルのCSSを全て読み込ませる app/assets/stylesheets/hoge.css @import 'bases/_variable'; @import 'bulma/bulma'; @import 'bases/_base'; @import 'partials/_footer'; @import 'partials/_header'; @import 'pages/_hoge'; @import 'partials/_fuga'; 3. 実装結果 数値を確認した結果、ページの読み込みスピード改善ができていました。 下記の検証手順で確認してみると、現在読み込まれているCSSファイルが確認できます。 今後サービスが大きくなり別の方がアサインされた場合でも拡張しやすく、新しいモジュールや機能を追加し、配置換えしたときでも保守性も保たれると思います。特にコントローラーとCSSファイルを1:1にする事によってどこのCSSファイルを読み込んでいるか分かりやすくなったと思い、今後の運用しやすくする為の基盤が作られたと思います。 個別読み込みと読み込みスピード改善の検証方法 Coverage機能による検証手順 Google Chromeの検証ツールからCoverage機能を使用 a. 「Coverage」という機能を使用するのですが、標準では表示されていないので初回は追加設定します。 b. 「開発者ツール」右上に表示されている「…」が縦に並んだ設定ボタンをクリックします。 c. 「More tools」から「Coverage」を選択し追加します。 d. 「Coverage」が追加されると、開発者ツールの下に表示されます。 Coverageで表示されるURLを確認 a. 対象ページ:設定したCSSファイルが読み込まれている b. 対象外のページ:application.cssが読み込まれている 読み込みスピードの改善数字 a. Before: LCP 1.5秒 b. After : LCP 0.8秒 計測結果と確認一覧 実装後にPageSpeed Insightsでページ読み込みスピードが改善されているか数値確認し、Coverage機能を使い現在読み込まれているCSSファイルの確認をしました。 確認項目 読み込み箇所の確認は以下の通りです。 個別読み込みを設定し必要なCSSが読み込まれている 個別で設定したCSSファイルだけがしっかり読み込まれています。 個別読み込みを設定していないので全ファイルのCSSが読み込まれている @import 'pages/*':のようにディレクトリ内の全ファイルが読み込まれています。 計測結果 計測結果は以下の通りです。 コントローラ毎に個別のCSSが読み込まれている状態の計測数値 LCPの計測値が0.8秒と基準値より上回っています。 個別読み込みを設定していない状態の計測数値 LCPの計測値が1.5秒と基準値より下回っています。 実務を始める前と終えた後の変化 実務経験前と経験後でプログラミングに対しての考え方や取り組み方が大きく変わりました。 具体的に3つです。 1. エンジニアとしての心構え 実務を経験する前は書いてある事をそのままやる事しかしてこなかったと思います。教材があり、そこに書いてある事をそのままやるだけで自分で考えて取り組んでいませんでした。 今回実務体験をさせていただき、課題に対してどうやって解決出来るか思考し、それに対しての解決策を導き出す大切さを知る事ができました。書いてある事をそのままやるのではなく自分で思考して取り組む大切さを知る事ができました。 2. コードに対しての品質の意識 これは本当に反省です。最も意識が変わった事だと思います。実務経験前の私は動けば大丈夫くらいな気持ちでコードを書いていました。 コード品質の良し悪しがサービスの良し悪しにも直結します。しっかり理解したコードを書き、他の人が見ても分かる様なコードを書き品質を落とさない大切さを知る事ができました。 3. 相手が言っていることを理解する力と伝え方 仕事は基本相手がいて成立するものなので、自分から提案する力、正確に伝える文章力、相手が本当に必要としていることを汲み取る力が大切だと気づけました。 今回の実務を終えて一つ毎日続けようかなと思うことが一つあります。 それは読書です。 元々読書はしていたのですが今後はアウトプット前提の読書をしていきます。 アウトプットとは読書をしたら感想文を書くという事です。感想文を書くという事は、何となく読書してたら感想は書けないので集中して読む+作者の意図を正確に理解する力がついていくと思います。今後エンジニアとして働くときに相手の言っている事の理解と背景を汲み取る力をつける事によって、一番良い結果をクライアントやお願いしてくれたメンバーにお返しが出来ると思いました。ビジネスマンとして成長する為に、今後『読解力』と、『ロジカルシンキング』の二つの力を付けていきます。 実務を終えての感想とプログラミングに対しての思い 今回の経験を踏まえて、自分に改めて足りていないもの、学習しなければならないこと、エンジニアという職業がコードを書くだけの職業ではないこと、技術以外に必要となるスキル、エンジニアになるってどんなことなのか等、本当に多くのことを学ばせていただきました。 実務を通し、改めてエンジニアという職業の魅力を感じました。エンジニアの技術と現職の楽器の演奏技術は似ているなと改めて感じました。 僕は音楽講師を目指した時も技術面に関して何度もつまずき悩まされた事がありました。 そんな僕でも自分の弱点をしっかり分析し一つずつ解消する事によって講師テストに合格する事ができました。 今回も状況がよく似ていると思っています。 エンジニアという立場で仕事をするとは、音楽に置き換えるとライブ出演したような感覚でした。 最初は周りに迷惑をかけてしまう事があると思いますが、誰よりも情熱を持って取り組んでたらその差は埋まると思っています。 エンジニア転職した後も、一つ一つ毎日技術や知識を積み上げて成長し、会社で必要とされ、将来リーダー的なポジションを目指し、メンバーを引っ張っていける様なエンジニアを目指して行きたいと思います。 今回リーダーのY氏に褒められた部分が明るく、ポジティブなのでチームに居るとチームが明るくなると言っていただけました。とても嬉しかったです。技術面では未熟ですが、エンジニアではチーム開発する時の雰囲気作りも大切なんだなと学べました。将来的にチーム開発に携わる時は雰囲気作りのサポートもしていきたいです。それと僕は教える事は仕事柄得意な方だと思うので、新人教育などにも携わっていきたいなと思い、段階的に各ポジションに就いてみたいと思っています。 学習が大変という意見もありますが、好きな事を「学習する」という考えは、違和感があります。 僕は楽器の練習を練習と思ってなく、好きな事だから自然と追求するものだと思っています。 プログラミングに対しても同じ思いでいます。まだ分からない事だらけで自然に追求していけるレベルまで到底達していませんが、そのレベルまでいったら僕は凝り性なのでどんどん追求できる自信があります。 そして何より感謝したいのは、リーダであるY氏と、クライアントです。 私に何度もコードレビューや、施策提案の進め方を丁寧に指導していただく事によって、今回の実務をやり遂げる事ができました。まだまだ自分の力だけでは出来ていないことばかりですが、今後しっかり成長し、あの時育ててよかったなと思ってもらえる様なエンジニアに成長していきます。 同じミスをしない対策としては、読解力を鍛え、何をして欲しいか理解し、技術+知識+理解力を付けて同じミスをしない様にしていきます。 一緒に実務に挑戦した仲間たちにもとても感謝しています。大変な時に励まし合える仲間がいる事は本当に素晴らしいなと実感しました。 エンジニアとしてまだまだ至らない部分が多くありますが、学習を継続するとともに、今後の転職活動に力を入れたいと思います。ご覧いただきありがとうございました。
- 投稿日:2021-12-05T00:00:05+09:00
Lambda サブスクリプションフィルター + AWS WAF で実現する「フルリモートワーク時代のお手軽社内サイト」
本記事は AWS LambdaとServerless Advent Calendar 2021 の4日目です。 たまたま空きがあることに気付いたため、せっかくでしたらと急遽参加させていただきます! よろしくお願いいたします ? こんにちは。Togetter を運営しているトゥギャッター株式会社でエンジニアをしている @MintoAoyama です。 Togetter はツイートを始めとした様々な情報を組み合わせてコンテンツを作り出すキュレーションサービスです。 2009年に誕生してから今年で13年目に突入し、現在も月間PV約1億、月間UU約1500万という規模感で成長を続けています。 そんなトゥギャッター社もコロナ禍に入り、全従業員がフルリモートワーク体制に移行しました。 もっとも、以前からリモートワークは実施されていました。オフィスは東京ですが地方からフルリモートで出勤されているメンバーも多く、エンジニアは基本週に一度リモートワークを実施する試みが行われていました。 コロナ禍でもトゥギャッター社のリモートワーク、うまくいってます|Togetter(トゥギャッター )|note(2020年5月の記事) コロナ禍以前から実施してきた弊社の"リモートワーク遍歴"を振り返る|Togetter(トゥギャッター )|note(2020年11月の記事) しかし、全ての従業員・全ての職種で完全なフルリモートワークに移行し始めたところ(2020年2月 - )、想定が及んでいない課題が出てきました。社内サイト・ドキュメントへのアクセス権限です。 意外にも色々あった "社内限定リソース" 弊社ではグループウェアとして Google Workspace (旧 G Suite) を活用しています。また、ソースコードやチケット管理ツールとして GitHub 、コミュニケーションツールとして Slack も活用しています。 これらについては以前から2FA(2段階認証)設定も義務化しているくらいで、フルリモートワークにあたって特別障壁になるものはありませんでした。 ただし、それ以外の手段で社内ネットワーク向けに限定公開しているリソースも存在していました。 社内向けドキュメント(Webサイト) 静的サイトジェネレーターで生成される 開発者向けとして別サイトもある "CloudFront" すごい便利な社内ツール(Webアプリケーション) "ALB" 検証環境・ステージング環境(Webアプリケーション) "ALB" 開発用データベースのためのデータ(MySQL の Docker DataVolume / sqldump) "CloudFront" 各種環境へのssh踏み台サーバ "EC2" (ざっと思い出した限りこのあたりです。他にもあるかもしれません。) 上記に関してはすべてIPアドレス制限を行っていました。 まず「オフィスネットワークのIPアドレスを許可」し、「社外からのアクセスを必要とする際は都度申請してもらい、私の方で手動で許可」していました。 ただ、「申請の際には自身でIPアドレスを調べて貰う」必要がありましたし、「プロバイダなど通信環境によってはIPアドレスが頻繁に変わる」可能性もあり、お互いの負担になっていました。また、「退社などを想定した定期的な棚卸し」も課題になります。 …それらが全従業員のフルリモートワーク移行により、ほぼキャパオーバーになったというわけです ? 普通に考えてこれらを個別申請・手動で管理すること自体が非合理的ですよね…。 何らかの形で仕組みを見直す必要がありました。 様々な従業員の環境からスムーズにアクセスしてもらうために 皆さんならどうするでしょうか…。 ドキュメントであれば Google Drive や GitHub Wiki に移行するという手もありそうです。 しかし、それだけでは賄えないリソースもあります。Webサイト(Webアプリ)でないと満たせない要件もありました。IPアドレス認証が必要なリソースもあります。 Digest認証も一瞬検討しましたし、GoogleアカウントやIAMなどを利用した認証手段がないかなども考えました。しかしいずれにせよ課題があり(全ての従業員・関係者が対象のアカウントを持っているわけではなかったり)、あまり明解ではありませんでした。 ※ SSO / OIDC / VPN など、よりベターな認証手段があることは把握していますが、今回は従来のIPアドレス認証を "拡張" しようという "お手軽" な手段を取っています ? と、ここで、そもそも 本番プロダクト(Togetter)のための管理画面 があることを思い出しました。記事情報の閲覧やフラグ管理、本番バッチの実行状況の確認など、本番データの参照・操作に利用されている仕組みです。当然Webアプリ内で認証しており、全従業員が日々ログインしています。 つまり、「認証後であれば正常にアクセスできるパスがある」ということです。 Lambda サブスクリプションフィルター で実現できるIPアドレスによるアクセス制御 Webサーバ(nginx)のアクセスログは json 形式で CloudWatch Logs に送っています。 特定のパス(認証を必要とするパス)に status=200 でアクセスしているIPアドレスが分かれば、それをホワイトリストとして扱えそうです。 CloudWatch Logs には「サブスクリプションフィルター」という機能があり、るログデータを Kinesis / Lambda / Kinesis Data Firehose に送信できます。 CloudWatch Logs サブスクリプションフィルターの使用 - Amazon CloudWatch Logs Lambda であれば、対象のIPアドレスをWAFに登録する処理を実装できそうです。 なお、サブスクリプションフィルタにはフィルターパターンを設定して、特定の条件に当てはまるログのみを送信対象として絞り込めます。 Lambda を実行する場合、その 実行回数 = 利用料金 にも影響するため、極力条件を付けることをオススメします。 フィルターとパターンの構文 - Amazon CloudWatch Logs 以下の例は、GETリクエストで status=200 で 管理画面配下(例: /adm/*) にアクセスできたリクエスト、みたいな感じです。 { $.request_method = "GET" && $.status = "200" && ( $.request_uri = "/adm/*" } WAFを定期更新できれば CloudFront や ALB に関連付けしてアクセス制御できますし、セキュリティグループを更新すれば ssh など他のポートも制御の対象になります。 今回、個人的にとっつきやすいRubyで実装してみました ✋ 以下、受け取ったログデータを取り出す簡単な実装例です。 require 'base64' require 'json' require 'zlib' def lambda_handler(event:, context:) # base64エンコードされているデータをデコードする decoded_data = Base64.decode64(event['awslogs']['data']) # バイナリ圧縮されているログデータを展開する json_data = Zlib::Inflate.new(Zlib::MAX_WBITS + 16).inflate(decoded_data) data = JSON.parse(json_data) # ログデータを1件ずつループ処理 # ここではとりあえずIPアドレスとリクエストパスを出力している for log in data['logEvents'] do access_log = JSON.parse(log['message']) remote_addr = access_log['remote_addr'] p "remote_addr = #{remote_addr}" request_uri = access_log['request_uri'] p "request_uri = #{request_uri}" end end CloudWatch Logs から送信されてくるログデータは base64エンコード + gzip圧縮 されているためそれらに対する処理が最初に必要になりますが、後は配列上になっているので1件ずつ処理します。 WAFのIPアドレスリスト(IP addresses)の更新には AWS SDK 、Ruby であれば Aws::WAF::Client クラス を使います。なお、 WAFv2 であればこれで良いですが、 旧バージョンの WAF(AWS WAF Classic)を対象としたい場合は Aws::WAFRegional::Client クラスを使う必要があります。地味な罠になっていますのでご注意ください…。 Class: Aws::WAF::Client — AWS SDK for Ruby V3 Class: Aws::WAFRegional::Client — AWS SDK for Ruby V3 以下、複数の対象IPアドレスを追加する簡単な実装例です。 require 'aws-sdk-waf' def update_ip_sets(ip_set_id, ipaddresses) waf_client = Aws::WAF::Client.new( region: region_name ) # 対象IPアドレスのリストから更新リクエスト内容を生成 # (今回は "IPv4" の "追加" としています) updates = [] ipaddresses.each do |ipaddress| updates.push({ action: 'INSERT', ip_set_descriptor: { type: 'IPV4', value: "#{ipaddress}/32" } }) end # 更新処理に必要なトークンを取得する waf_responce = waf_client.get_change_token({}) change_token = waf_responce.change_token # 更新 response = waf_client.update_ip_set({ change_token: change_token, ip_set_id: ip_set_id, updates: updates }) end なお、実装にあたっては考慮すべきポイントが色々出てきます。 WAFのIPアドレスリスト(IP addresses)にはサイズ制限がありますし、先ほど書いたように登録されたIPアドレスはその性質上永続的に保存するものではなく、定期的な棚卸しが必要になります。 そこで、「それぞれのIPアドレスの登録日時を記録し、一定期間が経過したものを削除する」などの処理を入れたりします。 記録用のデータストアとしては通常 DynamoDB などを使うかと思いますが、今回は規模感なども踏まえ、お手軽に Parameter Store (AWS Systems Manager) を利用してみます。 AWS Systems Manager Parameter Store - AWS Systems Manager Parameter Store は 階層型のパラメータに対して値を格納できるKVSのようなサービスです。「スタンダード」の条件を満たせば無料(大体の場合はこれで必要十分なはずです)。 バージョニングにも対応している上、Lambdaからの読み書きが可能。1秒間に最大1,000件のアクセスにも耐えられるスケーラビリティを備えているため、簡易的なデータストアとして重宝する仕組みでもあります。用途次第ですが、使わないと損?かもしれません。 Class: Aws::SSM::Client — AWS SDK for Ruby V3 以下は Parameter Store の読み書きをする簡単な実装例です。 require 'aws-sdk-ssm' def ssm_sample() ssm_client = Aws::SSM::Client.new # 追加(更新) res = ssm_client.put_parameter({ name: 'test', value: 'aaa', type: 'String', overwrite: true }) # 取得 res = ssm_client.get_parameter({ name: 'test', with_decryption: false }) p res.parameter.value end 詳細は割愛しますが、例えば json で「"IPアドレス" と "登録日時" の組み合わせ配列」を定義・保存するよう実装してみたりします。 スタンダードの場合は保存できるデータサイズの上限が 4,096 byte なので、超えないように注意する必要もあります。 ということで、ひとまず合理的な形で制御できるようになりました。 今回は少し特殊なケースかも知れませんが、サブスクリプションフィルターはログ1行1行に対してほぼリアルタイムで処理できるため、様々な用途が考えられると思います。 ご存じなかった方は是非ご活用ください ???