日々のあれこれφ(..)

もっぱら壁打ち

『データ分析人材になる』を読んだ

1週間くらいで読むつもりだったのですが、GWを挟んだのと、旅先で別の本に浮気したのもあって結局2週間後読み終わりました😇

読書ログもブログに付けていくことを検討しているのでお試しで。

データ分析に限らず、システム開発や人とやり取りする他の仕事にも応用できそうなことがたくさん書いてありました。

この本に書いてあること

  • 7割がデータ分析を成功させるための思考法のフレームワーク
    • 教科書的な方法論ではなく、筆者が今まで体験した失敗例や成功例も合わせて載っており、実践に取り込みやすそうだった
  • 残り3割が上記を応用したデータ分析人材育成論
    • これも筆者の業務経験ベースで、教育フローだけでなくツールや環境を整備したり、能力を身につけ成果を出した人は市場に沿った給料を与えるようにする、と言ったことまで触れられていた
  • データ分析の具体的な手法に付いては書かれていない
  • 想定読者:データサイエンスの領域に踏み込まざるを得ないビジネスマン(文理問わず)

筆者について

  • 三井住友海上火災保険会社のデジタル戦略部の社員4人が共著したもの
  • 内二人は元々営業で、データサイエンスは独学で勉強して30歳くらいにデータサイエンティストとして働くようになったような人もいる

読んだきっかけ

とある技術ブログで紹介されているのを見て、なんとなく興味を持ったからなのですが、もう少し言語化するとしたら以下なのだと思います。

  • 私自身ひょんな事からデータ分析基盤の開発運用チームで仕事をするようになり、業務を通して必要なことは都度キャッチアップしていったのですが一方で、そもそもデータ分析基盤について体系的な知識*1を持っていないという課題感があった
  • それとは別にワークマンのエクセル経営みたいなものに興味があって、タスク管理面など普段の自分の仕事にも考え方を取り入れて実践できないか悩んでいて、本に書いてある内容に興味を持った

読書ログ

(斜体) になっているのは個人の感想です。

  • 5Dフレームワーク:データ分析を成功させるための思考法のフレームワーク

    • Demand:依頼元の要求を聞く
    • Design:設計。期間や予算を踏まえ、やることやアウトプットを定める
    • Data:データを集める
    • Develop:開発。集めたデータを分析する
    • Deploy:結果を依頼元に共有し、次の行動に繋げてもらう
  • p26 多くの企業が直面している課題(8):データ分析に対する組織の認識や文化の差

    • (ちょうどここに書いてあるのと同じ問題を前職で見たことあったのを思い出した。(人事系の部署が「簡単な集計をデータ分析」と考えていて「データ分析はこっちでやっています」と言っていて、データサイエンティストが「違うんだけど、どうしたら伝わるんだろう...」と頭を抱えているような))
  • Demand:分析の価値は意思決定の背中を押す

    • (同じことを以前私も指摘されたことがあった。レポート画面の設計をしていて、レビューした先輩から「そのレポートを見て、見た人は次にどんなアクションに繋げられるの?」と。)
  • Design:目的、仮説、データ、手段を整理する

  • p92 有意差:誤差を超えた、意味のある差のこと

  • p92 モデリング:構造化すること。予測するロジックを作ること

    • 例:タピオカ屋の場合、性別・年代別の来店期間を予測するロジックを作ること
  • p101 適切なサンプリングをしていれば少ないデータ量でも意味のある結果を得ることは可能

    • (こういうのもっと知りたい。統計学とかになるんだろうけど、果たして手を伸ばせるか...?)
  • p133 AIが役に立つケース:要因が分からなくても予測できればいいというケース。例えば、体の動きだけで不審者を特定する、離脱しそうな会員を特定する、など

  • p135 データ分析会社に依頼する際の注意事項

    • 課題が不明点であることなど、多くの場合は発注側に問題がある
    • いきなり外部に依頼せず、まずは自分で5Dフレームワークを一通り実践してみて、できない部分のみを依頼するようにする
    • 課題感を明確に伝え、渡せる情報は共有し、具体的な提案を求める
    • スケジュールと役割分担を明確にする
  • p148 データ分析の方向性には二つパターンがある

    • マイニング:金脈の意味。意思決定に繋がる重要な要素を見つける
    • モデリング:さまざまなパターンから将来を予測すること。AIはこちらに使われる
  • p158 より良い予測アルゴリズムを作る工程

  • p189 ひとくくりにデータサイエンティストと言っても本人の希望分野がAIアルゴリズムの開発であるのに対して、企業側がデータマーケター的要素を求めていた場合ミスマッチが発生し得る

    • (ちょうど弊社でもデータサイエンティストの採用が始まったので、そうならないといいな...と思ったり)
  • p190 リカレント教育の限界

    • 「流行っているから勉強して来い」というぼやっとした理由で外部講習に行かされ、「業務命令が出たから」となんとなく参加し、知見が職場で生かされない
    • また生かそうにもデータにアクセスできない、PCのスペックが足りないなどの物理的な制約がある場合もある
    • (前者の講座に関してはありそうだなぁと思ったり)
  • p199 データ分析人材の様々なタイプ

    • データエンジニア
    • データアナリスト
    • データサイエンティスト
    • AIエンジニア
    • データマーケター
    • マーケティングテクノロジスト
  • p199 データサイエンティスト協会が定義している、データサイエンティストに求められる力三つ

    • ビジネス力:とりわけドメイン知識や人間関係が効いてくる部分
    • データサイエンス力
    • データエンジニアリング力
    • この三つ全てを兼ね備えている人材はなかなかおらず、人によって得意不得意領域が存在する
  • p209 色々なデータ分析ツールの紹介が載っていた

    • BIツール:Tableauなど
    • 加工・集計・モデリングIBM SPSSなど
    • 自動分析(AutoML):DataRobotなど
  • p213 文系からデータ分析人材を育成するフロー

    • ツールを触りながら集計を行いつつ、座学(統計学の基礎など)を並行する形
    • (自分がいつか勉強したくなった時の参考にしよう...)
  • p236 データサイエンティストと現場の間に、ビジネストランスレーターがいないと結局うまくいきにくい

    • ビジネストランスレーター:橋渡し。データ分析者ほどではなくてもデータ分析の基礎やツールの使い方を理解しており、業務の経験があって企画が得意な人
    • 一度文系の社内の人材にデータ分析を学ばせ、「やっぱ向いていないや」となった人がいるケースがあり得るが、そういう人が現場に戻ったときにビジネストランスレーターとして振る舞ってくれたりするので、教育コストもあながち無駄にはならない
  • p239 三井住友海上のデジタライゼーション

    • 営業現場の課長がExcel VBAを使って手作業していたものを自動化したら楽だろうなと独自で実装した「1クリックツール」なるものが便利だったため全社プロジェクトに普及した話とか
    • 他にも執筆メンバーが所属しているデジタル戦略部の取り組みとして、火災保険のデータを使ってどのような場所で自然災害の被害が多くなるかの予測精度を上げる取り組みなどが紹介されていた
    • (会社の事例の話が面白くてこれってもしかして採用本だったのと一瞬疑ってしまった。(そういう効果も狙ってはいるだろうが、きっかけは講演の依頼だったのでおそらく違う))

おわりに

ビジネスとシステムどちらにも知見がある人の書く本はやっぱり面白いです。

ちなみにこういう本を読んでいるからといってデータサイエンティストになりたい訳ではなく、考え方を知って自分の仕事や日常生活に応用したいと言った心持ち。

*1:この本で紹介されていた、データサイエンティスト協会が定義している「データサイエンティストにとって必要な3つの能力」でいうところの「ビジネス力」「データエンジニアリング力」「データサイエンス力」の内、ビジネスとデータサイエンスの部分がよく分かっていない状態でした。

redis入門+トランザクション周りの覚書

サーバサイドエンジニア3年目にして先日初めてredisとやりとりするような処理を書きました。

せっかくなので調べたことを残しておこうと思います。

基本的な操作(cli

接続

$ redis-cli 
127.0.0.1:6379> 

-h {HOST}-n {db} など

redisのdbについて

  • 選択可能な名前空間の形式。0~15などのidを指定する形で使う
  • 未指定の場合は0になる
  • redis clusterでは、0のみがサポートされる
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]>

127.0.0.1:6379[1]> SELECT 15
OK
127.0.0.1:6379[15]> SELECT 16
(error) ERR DB index is out of range
127.0.0.1:6379[1]> SELECT 0
OK
127.0.0.1:6379>
  • database_idの数は databasesで確認でき、この設定を変更すれば増減できる
127.0.0.1:6379[15]> CONFIG GET databases
1) "databases"
2) "16"

set/get

127.0.0.1:6379> SET count 1
OK
127.0.0.1:6379> GET count
"1"

key一覧を見る

  • KEYS {pattern} でpatternに一致する全てのキーを返す。
127.0.0.1:6379> KEYS *
1) "key1"

大規模なデータベースで実行するとパフォーマンスに影響が出る可能性があるため注意。

キーに格納されている値を増やす

127.0.0.1:6379> INCR count
(integer) 2
127.0.0.1:6379> GET count
"2"

キーに有効期限を設ける

  • EXPIRE {key} {秒}
    • 有効期限を設定
  • TTL {key}
    • 残り時間を確認
# keyを設定
127.0.0.1:6379> SET count 3
OK

# timeoutを20sに設定
127.0.0.1:6379> EXPIRE count 20
(integer) 1

# 参照できる
127.0.0.1:6379> GET count
"3"

# 残り時間を確認
127.0.0.1:6379> TTL count
(integer) 13
127.0.0.1:6379> TTL count
(integer) 12
...
127.0.0.1:6379> TTL count
(integer) 1
127.0.0.1:6379> TTL count
(integer) 0
127.0.0.1:6379> TTL count
(integer) -2

# keyが存在しなくなっている
127.0.0.1:6379> GET count
(nil)

TTLの返り値に関して、-1タイムアウトが設定されていない値、 -2 はkeyが存在しないもの。

トランザクションを貼る

  • MULTI
    • トランザクションを貼るときのコマンド。multiの後に入力したコマンドはキューに詰められ、EXECコマンドが入力されたタイミングで一括で反映させる事ができる
  • WATCH
    • 楽観ロック。キーをwatchすると、watchされたキーに変更が加えらた場合トランザクション全体を中止することができる(EXECを実行したタイミングでトランザクション処理の各コマンドが実行されなくなり、代わりにnullが返る)

トランザクション成功例

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> 
127.0.0.1:6379(TX)> SET count 1
QUEUED
127.0.0.1:6379(TX)> INCR count
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (integer) 2
127.0.0.1:6379> GET count
"2"

トランザクション失敗例

127.0.0.1:6379> SET count 1
OK
127.0.0.1:6379> WATCH count
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
# このタイミングで別ターミナルで INCR countを実行し、count keyに変更を加える
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
127.0.0.1:6379(TX)> INCR COUNT
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil)
127.0.0.1:6379> GET count
"2"  # TX内のincr結果が反映されていない

基本的な操作(redis-py)

import redis

def main():
    client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    client.set('count', 1)
    client.incr('count', 3)
    print(client.get('count')) # b'4'


if __name__ == "__main__":
    main()

pipelineメソッド

単一のリクエストでサーバーに複数のコマンドを実行させる事ができる。

import redis

def main():
    client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    pipe = client.pipeline()
    pipe.set('count', 1)
    pipe.incr('count', 3)
    pipe.get('count')
    print(pipe.execute())  # [True, 4, b'4']


if __name__ == "__main__":
    main()

pipeline()はtransactionオプションがあり、デフォルトでonになっているが明示的にoffにして使うこともできる。

transactionメソッド

pipelineとは別に、transactionメソッドというものもあり、それを使うこともできる。 transactionメソッドを使うと、watchや監視対象keyに変更が加えられたときのWatchErrorハンドリングを書かなくても済むが、少しわかりにくかった。

import redis

def main():
    redis_client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
    )

    def incr_req_cnt(p):
        p.multi()
        p.incr(key, amount=2)
        p.incr(key, amount=2)

    key = "count"
    tx_result = redis_client.transaction(incr_req_cnt, *key, value_from_callable=True)
    print(tx_result)


if __name__ == "__main__":
    main()

transaction関数の中身は次のようになっている。

https://github.com/redis/redis-py/blob/883fca7199a7ab7adc583b9ee324a9e44d0909eb/redis/client.py#L1074

    def transaction(self, func, *watches, **kwargs):
        """
        Convenience method for executing the callable `func` as a transaction
        while watching all keys specified in `watches`. The 'func' callable
        should expect a single argument which is a Pipeline object.
        """
        shard_hint = kwargs.pop("shard_hint", None)
        value_from_callable = kwargs.pop("value_from_callable", False)
        watch_delay = kwargs.pop("watch_delay", None)
        with self.pipeline(True, shard_hint) as pipe:
            while True:
                try:
                    if watches:
                        pipe.watch(*watches)
                    func_value = func(pipe)
                    exec_value = pipe.execute()
                    return func_value if value_from_callable else exec_value
                except WatchError:
                    if watch_delay is not None and watch_delay > 0:
                        time.sleep(watch_delay)
                    continue

WatchErrorが発生した場合はリトライされる(watch_delayを指定することで再実行までの時間を指定できる。リトライ回数は指定できない模様)

value_from_callable 変数を指定することで、transactionの返り値を設定できる。 例えば上記の incr_req_cnt()return 'success'を入れたとして、 value_from_callable=Trueにするとprintの中身は'success'になり、Falseにすると、 pipe.execute() の返り値(上記コードではcountキーが0だった場合 [2, 4])が出力される。

参考

GARNET CROW filmscope行ってきた

先日GARNET CROW 20th Anniversary企画の締めとなるオフラインイベント(物販、写真展、上映会)に行ってきました。

GARNET CROWは私が子供の頃初めて好きになったバンドです。 当時はバンドを追っかける習慣みたいなものがなかったのでライブ行ったりファンクラブ入ったりといったことはしておらず、テレビでたまたま流れて好きになった何曲かをTSUTAYAでCD借りてよく聞いていたくらいなのですが、数年間が空いてはふとした時にまた聴きたくなり定期的に聞き返していました。

大学生の時ようやく「そういえばGARNET CROWのライブも一回行ってみたいな」と公式サイトにアクセスしてみたところ、そこで初めて解散していたことを知りました。ショックだったけど、曲はそれからも定期的に聞いていました。

20th Anniversary企画のことは、公式YouTubeチャンネルをたまたま見つけて知りました。Twitterアカウントもフォローしました。楽曲モチーフの新作Tシャツ販売など、いくつかのイベントは既に終わってしまっていたのが悔やまれるのですが、サブスクが解禁し、聞いたことなかった他の曲に手が伸び、私のガネ熱が一気に高まりました。特に去年末からはほとんどGARNET CROWしか聞いておらず、ひとりカラオケ行ってはGARNET CROW縛りで歌って過ごし、あまつさえ全楽曲をしっかり聴き込むんだとNotionにデータベース作って曲名と収録されているCD、アルバム、ベストアルバムの情報とともに聞き込み具合のステータスを管理し始める始末でした。

そんな私にとって初のオフラインイベントが大阪で開催されるとなり、こんなご時勢なので結構迷ったのですが、ここで行かないと次はもう二度とないかもしれないから一生後悔するだろうとワクチン3回目摂取を免罪符に数年ぶりに遠征しました。 感想を一言で表すと、想像以上でした。特に上映会が。

イベントは2日間にわたって行われ、1日目と2日目で上映会の内容は異なるものになっていました。1日目は過去ライブのダイジェスト+リハーサル映像などの未公開映像を新録したもの、2日目ファイナルツアー1本まるまる+舞台裏未公開映像を新録したもので、2日間共参加しました。 会場は大阪の中之島エリアから少し離れたところにある堂島リバーフォーラムという施設で、上映会もその中のイベントホールで行われていました。劇場ではないので巨大なスクリーンの前にパイプ椅子が並べられるといった具合だったのですが、幸い前の人の頭が邪魔でスクリーンが見えないなんてこともなく、わくわくしつつも気楽に座って鑑賞していくつもりだったのですが、始まった瞬間タイムトラベルしていました。

家では再現できない大音量高音質でライブ映像が流れて、曲の合間に聞こえる客席からの歓声が私の左右後方からも聞こえてきました。気づかなかっただけで後ろにもスピーカーがあったのだと思うのですが、その臨場感が半端なく、まるで客席の中に混じってライブに参加しているかのような没入感でした。 1日目のダイジェストは時系列順だったので小さなライブハウスから始まり、学園祭の貴重な映像も挟まれつつ、少しずつ会場が大きくなって、衣装が豪華になっていき、静かに曲を聞かせるスタンスから一緒に盛り上がってメンバーもそれを楽しむといった具合にパフォーマンスが変化していく様など、ガネクロの変遷が見られました。上映後、スタッフのお馴染みのアナウンスの後に会場からは静かな拍手が湧き上がっていました。終わった後も興奮と余韻が冷めず、物販に駆け込み、衣装と舞台装置が素敵だなと思っていた『GARNET CROW livescope 2010+ 〜welcome to the parallel universe!』のDVDと、前々から気になっていたものの購入に踏み切れていなかった『GARNET CROW Special live in 仁和寺』をAmazonで購入しました(こちらは中古で)。

2日目の上映会はファイナルライブの通しです。私は幸いまだ見たことなかったのですが、円盤持っている人は正直見なくてもいいんじゃないかなと参加する前は思っていましたが、やっぱりそんなことはありませんでした。 特に印象に残っているのは、2回目最後のアンコールの後、出演者が舞台前まで出てきてお辞儀するシーンで、流れていたBGM(『Smily Nation』という曲)に合わせて観客がサビの1フレーズを一緒に歌っていて、メンバーも一緒にノっていて、ライブはもう終わったのに尚BGMだけで盛り上がっている様子に、ただひたすら「より良い楽曲を」と続けてきたガネクロらしさを感じて心打たれていました。幕が閉じて出演者が見えなくなっても客席はBGM(『Fall in Life 〜Hallelujah〜』という曲)を歌い続けていて、こんなの見せられたらもう泣いてしまうじゃないか...と涙を堪えるのが大変でした。実際終わった後辺りを見渡したらハンカチで涙抑えている人も何人か見かけました。上映後やはり興奮と余韻が冷めず、物販に駆け込んでファイナルライブのディスクを買おうとしたら、二日目の最初の回だったにも関わらずもうライブDVD全てが完売してしまっていたので、これまた前々から気になってて買えなかったCD(漫画家の安西信行先生が書いたメンバー4人が裏表紙になっているものがある)を買って会場を後にしたのでした。

この大阪旅行期間に買ったガネクログッツは以下です。

帰宅後お布団の上に並べ直したもの。ほくほく

取捨選択しつつも、比較的ミニマリスト思考の自分にしては買った方なのではと思います。

  • ガネクロTシャツ(真ん中上)、ずっと欲しかったので買えて嬉しい...!着るのもったいないなと思いつつ、保存用とか言いながら箪笥の奥に眠らせておくほうが勿体無い、服は消耗品と割り切っているので、今年の夏私服に合わせてたくさん着たいと思います。 Tシャツは他にももう一つOver Driveというシングルのジャケ写デザインのものも買っており、2日目はこれを着て参加していました。1日目は雨がひどく、2日目が快晴だったのもありちょうど良かったです
  • トートバック(右上)、これも普段使いしやすそうで、重宝したいと思います
  • ポストカード2枚(真ん中上)、これは写真展で飾られていたものになっています。12枚ほど種類がある中で、メンバー4人が揃っていて楽しそうなこの2枚を選びました。買って初めて気づいたのですが、草むらに同じ方向向いて座っている写真、ゆりっぺ(作曲、Vo)一人だけカメラ目線でピースしているのめちゃめちゃ可愛い...!

他は全て、当初は買う予定になかったものたちです。

  • GARNET CROW livescope 2006 〜THE TWILIGHT VALLEY〜(左下で光っているDVD)、私が一番最初にガネクロ好きになった時期に発売されていたアルバムのライブ。DVDは買う予定なかったのですが、物販でグッツ買った後並んでいるのを見ていたら欲しくなって衝動買いしたものです
  • GARNET CROW livescope 2010+ 〜welcome to the parallel universe!(↑の右隣)、1日目の上映会の後に勢いで購入したもの。クリスマスの時期のライブで、後ろに巨大なツリーが飾ってあったり、街灯が立っていたりと舞台がオシャレ
  • GARNET CROW Special live in 仁和寺(↑の右隣)、京都の世界遺産仁和寺で行われていたライブのディスク。当初は映像化される予定はなかったものの、ファンからの要望によって販売されたもので、歴代ライブの中でも人気の高いもののようです。前々から気になっていたものの、1日目のダイジェストで映像見た後「これはもう買わなきゃ一生後悔する」とようやくポチれました
  • music freak(左上赤い雑誌)、様々なアーティストの特集を組んだ音楽雑誌の、ガネクロの部分だけを集めた特集版。流石に全活動期間分はなかったのですがそれでもだいぶボリューミーで、よく聞くあの曲やこの曲についてどんなふうに作っていたのか、またそれ以外にもライブの特集や、メンバーのリレー日記形式のコラム、おすすめの映画や影響を受けたアーティストの楽曲まで紹介されていて、正直一番買って良かったグッツの一つになりました。1日目物販で買ったこれをホテルで読んでいて、今まで知らなかったガネクロの楽曲制作の流れだったり想いだったりといった情報に触れ、それによって2日目のファイナルライブがより一層心に沁みてしまいました
  • ステッカーとポストカード(右下)は、雑誌とDVDを買った時の特典です。ステッカーは7周年記念時の販促で使われていたグッツのようで、Macに貼っても良いかもと思ったのですが、やっぱり勿体無いのでこれもポストカードと一緒に大事にケース買ってしまっておこうと思います

GWが地味に忙しく、せっかく買ったDVDを全然見れていないのですが、週末あたりに少しずつ大事に見ていきたいです。

解散してからガネクロのメンバーは一人を除いて表舞台には一切姿を見せておらず、今回のイベントにも登壇はもちろんメッセージが寄せられることさえありませんでした(それもまたガネクロらしさを感じる)。そんなメンバー不在のイベントだった関わらず、もしもタイムマシンで過去に戻れたらガネクロのライブに行きたいと思っていた私の密かな夢が「叶ったな」って思えてしまうくらい、夢のようなひとときでした。

そして満足して飽きるどころか、新しい情報に触れ、まだまだ深められる余地ができてしまいました。これからも飽きるまで楽曲をたくさん聴き込んでいきたいと思います💪

【Python】ランダムに少しずつリクエストを増やしていく

負荷の高いリクエストを検証をするときに書いたスクリプト

既存データ群を引っ張ってきて、それをランダムに参照しつつ、リクエスト数を徐々に増やし、リクエストごとにレスポンスタイムを計測します。

この時は1回目はDBを再起動してメモリにデータが載っていない状態で行い、2回目続けて実行してレスポンスタイムや各種メトリクスを見比べることをしていました。

メモリにデータが乗りすぎても乗らなさすぎても参考値になるか微妙だったので、どうやって検証するのが良いものか悩んでいた時に同僚に教えてもらいました。ありがたや🙏

import csv
import json
import random
import requests
from time import time

AUTH_TOKEN = ''
URL = ''
FILE = ''  # sample.csv

headers = {
    'content-type': 'application/json',
    'Authorization': f'Bearer {AUTH_TOKEN}'
}

with open(FILE) as fp:
    reader = csv.reader(fp)
    IDS = [row[0] for row in reader]
MAX_IDS = 5000

for n in range(0, MAX_IDS+500, 500):
    if n == 0:
        continue

    random_ids = random.sample(IDS, n)
    d = {} # payload
    data = json.dumps(d)
    s = time()
    r = requests.post(URL, headers=headers, data=data)
    # n件リクエストした時のAPIの応答速度を記録
    print(n, time() - s, r.status_code)

【Python】生成したデータを並列で捌いていく

去年くらいに自分が書いたコードが大変読みにくいものだったので、並列処理再々入門しました。

作りたかった処理としては以下のようなものです。

  • n~mまでのデータの処理を並列で行う
    • 元々はHTTPリクエストすることを想定していました。このリポジトリでは汎用的にするためにprint出力に留めています。
  • リクエストボディで必要になるデータをQueueに入れる
  • どこまで処理が完了したかログを出す

最終成果物はmain.pyのものです。後で思い出しやすいように、要素を細分化したサンプルコードを合わせて残しました。

STEP1. multiprocessで並列処理を行う

組み込みモジュールmultiprocessingを使って行うときの書き方です。

main1.py

  • Process()で子プロセスのインスタンスを生成する。その際に引数に処理させる関数とその関数に渡す引数をTupleで渡す
  • ↑のインスタンスをstart()で処理開始
  • join()は子プロセスが終了するまでメインの処理を一時停止させる

STEP2. Queueを使ってプロセス間でデータを共有する

multiprocessingモジュールではプロセス間通信の手段が二つ用意されています。

  • Queue
  • Pipe:ソケットのような双方向通信チャネル

今回は双方向にする必要はないのでQueueを用いて書きました。

main2.py

  • def worker : 子プロセスが行う処理を定義。キューから取り出したアイテムで計算処理を行う
    • def calculate : 子プロセスが計算を行うときに呼び出すメソッド
    • def mul : calculateに渡せる関数の一つ。名前の通り引数で渡された数を掛け算する
  • def main :
    • タスク(今回の場合は「(mul, (i, 2))」形式のもの)を生成し、キューに詰む
    • キューを作成したり、子プロセスを生成したり
    • 最後にqueueに”STOP”をputすることで、子プロセスが処理を終えるようになっている

STEP3. QueueHandlerを使って複数プロセスから単一ファイルへログを記録する

loggingハンドラーのQueueHandlerを使って書きました。

main3.py

  • def listener_configurer: RotatingFileHandlerの設定
  • def listener_process : ログ用キューに積まれたログを取り出し、ファイルに出力していく。Noneを受け取るまで永遠に動く
  • def worker_configurer : workerで使うQueueHandlerの設定
  • def worker_process : 子プロセスの処理。今回はランダムにログメッセージを生成しログを吐いている
  • def main : 全体と同じようにQueueを生成し、 子プロセスを生成し、最後にqueueにNoneを渡して処理を終わらせるようにしている

まとめ. multiprocess, Queue, QueueHandlerを使った並列処理

main.py

  • def main :
    • ログ用のキューとworkerで処理するデータを詰むキューの二つを作成し、workerに渡す。
    • 子プロセスを起動した後にキューに生成したデータを積んでいき(def generate_data)、全て終わったらNoneを渡す
  • def worker_process : lister_processと同じようにキューのタスクを待ち続けるworkerに。
  • def listener_configurerdef listener_processdef worker_configurer: step3のコードを踏襲したもの

github.com

join()の位置を間違えて「処理が終わったのにメインプロセスが終了しない」「キューの中身を捌き切る前に子プロセスが処理する」みたいなところで少しハマってしまいました...。

参考

【MySQL】SELECT結果をファイル出力する

Notionのメモを整理している時に出てきたので供養。

INTO OUTFILE

以下で可能です 。

SELECT * INTO OUTFILE '/tmp/output.txt' FROM {TABLE};

mysql> SELECT user_id, email, birthday INTO OUTFILE '/tmp/output.txt' FROM users\G;
Query OK, 1 row affected (0.00 sec)

$ cat /tmp/output.txt
1   p1@test.com 2021-12-23

また次のようなコマンドでcsv形式にもできます。

# extract.sql
SELECT
  CONCAT('"', user_id, '"') AS user_id,
  CONCAT('"', email, '"') AS email,
  CONCAT('"', birthday, '"') AS birthday
FROM
  users;

# tsvからcsvに置き換える
$ mysql -u user -p {DB} -e "`cat extract.sql`" | sed -e 's/\t/,/g' > result.csv

$ cat result.csv
user_id,email,birthday
"1","p1@test.com","2021-12-23"

前提: secure_file_priv

MySQLsecure_file_priv システム変数はファイルを読み取りおよび書き込みできる場所を制限します。この値がnullなどになっているとそもそも出力ができないので、必要に応じて設定しておく必要があります。

mysql> select @@secure_file_priv;
+--------------------+
| @@secure_file_priv |
+--------------------+
| NULL               |
+--------------------+
1 row in set (0.00 sec)

mysql> SELECT * INTO OUTFILE '/tmp/output.txt' FROM users \G;
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
ERROR:
No query specified
# my.cnf
[mysqld]
secure_file_priv=""


mysql> select @@secure_file_priv;
+--------------------+
| @@secure_file_priv |
+--------------------+
|                    |
+--------------------+
1 row in set (0.00 sec)

余談

RDS on MySQLではできません...。

以下のような権限エラーが出ますが、RDSの使用上変更できないです。尚、Auroraであればs3に置いてあげるようにすることができるそうな。

mysql> SELECT * INTO OUTFILE '/tmp/output.txt' FROM table\G;
ERROR 1045 (28000): Access denied for user 'USER'@'%' (using password: YES)
ERROR:
No query specified

curlでcookieを取り扱う

curl歴は長いのにcookieを取り扱ったことがなく、今日初めて調べて知ったので忘備録がてらメモ。

ファイルを使うやり方と直指定するやり方がある。

ファイルを使う

-ccookieをローカルファイルに出力する。

curl -X POST -c ./cookies.txt \ 
-H "content-type: application/json" \
http://127.0.0.1:3000/api/signin \
-d '{"email": "p1@test.com", "password": "aaa"}'

cookies.txtの中身は以下のようになった

# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_127.0.0.1 FALSE   /   FALSE   0   session eyJ1c2VyX2lkIjoxMH0.YhswoA.H17TpdD5PHlcdUFVzXrUdyWOjGg

-b で指定したファイルのcookieを読み取る

curl -X POST -b ./cookies.txt \
-H "content-type: application/json" \
-H "Authorization: Bearer TOKEN" \
http://127.0.0.1:3000/api/signout

直指定

curl -vでcookieの中身を確認

curl -v -X POST \
-H "content-type: application/json" \
http://127.0.0.1:3000/api/signin \
-d '{"email": "p1@test.com", "password": "aaa"}'

出力内容の一部を抜粋。

...
< HTTP/1.0 200 OK
< Content-Type: text/html; charset=utf-8
< Content-Length: 155
< Vary: Cookie
* Added cookie session="eyJ1c2VyX2lkIjoxMH0.YhsuJQ.zno-TQJjdDOvOzlTnu6fAfoYrq0" for domain 127.0.0.1, path /, expire 0
< Set-Cookie: session=eyJ1c2VyX2lkIjoxMH0.YhsuJQ.zno-TQJjdDOvOzlTnu6fAfoYrq0; HttpOnly; Path=/
...

-H "Cookie: " で中身を指定する

curl -X POST -H 'Cookie: session="eyJ1c2VyX2lkIjoxMH0.YhsuJQ.zno-TQJjdDOvOzlTnu6fAfoYrq0"' \
-H "content-type: application/json" \
-H "Authorization: Bearer TOKEN" \ 
 http://127.0.0.1:3000/api/signout

余談

ファイルの中身、以下のようにすればいけるのではと思ったけどいけなかった🤔

session eyJ1c2VyX2lkIjoxMH0.YhswoA.H17TpdD5PHlcdUFVzXrUdyWOjGg

とか

session="eyJ1c2VyX2lkIjoxMH0.YhswoA.H17TpdD5PHlcdUFVzXrUdyWOjGg"

など