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

もっぱら壁打ち

CloudWatch Logsの調査にuternを使ってみた

アプリケーションのエラー調査のため、CloudWatchログの中身を漁ることを行なっていました。

調査をする際にuternというGo製のツールを使っていたので、所感をまとめます。

  • ロググループとログストリームを取得するときはaws cliを使う
aws logs describe-log-groups --query "logGroups[].[logGroupName]" --output text
aws logs describe-log-streams --log-group-name /ecs/users-stg --query "logStreams[].[logStreamName]" --output text/ecs/users-stg
  • 対象のログストリームをフィルタリングして対象のログを取得する
utern --no-log-group --no-log-stream --max-length 100 --filter "error" -n ecs/datadog -s 1h /ecs/hogehoge-stg

基本的に作者のQiitaとGitHubのREADMEが充実していたので、使い方に特に困ることはありませんでした。

qiita.com

github.com

そんなに詳しく調べたわけではないのですが、個人的にありがたかったのはログがたくさん出てしまうと探しにくいので、 --max-length である程度文字数を絞って選択範囲の一覧を俯瞰し、そこからログストリームを指定したりさらにフィルターをかけたりするような使い方をしていました。

自動でtail状態になるので、そこはオプションで選択できるようになるといいなと思いました。

1日で作られたそうですごい。使い易くて良いツールでした。

tarコマンドの挙動の確認

2014-04-10/不特定多数のzipファイルがある構成

# アーカイブ前
~/test1106$ ls -lah 2014-04-10 | head -n 1
total 532K
# アーカイブ
~/test1106$ tar cvf 2014-04-10.tar 2014-04-10
~/test1106$ ls -lah
total 568K
# 展開
~/test1106/2014-04-10_tar$ ls -lah 2014-04-10/ | head -n 3
total 532K

30Kほど増えた。

#1 含む
~/test1106$ zip -r 2014-04-10.zip 2014-04-10
#2 含まない
~/test1106/2014-04-10$ zip -r 2014-04-10_2.zip ./

#1の解凍
~/test1106$ unzip 2014-04-10.zip -d ../2014-04-10_1
~/test1106/2014-04-10_1$ tree -L 2 | head -n 5
.
└── 2014-04-10
    ├── 0021_0_log.gz
    ├── 0042_0_log.gz
    ├── 0051_0_log.gz
#2の解凍
~/test1106/2014-04-10$ unzip 2014-04-10_2.zip -d ../2014-04-10_2
~/test1106/2014-04-10_2$ tree -L 2 | head -n 3
.
├── 0021_0_log.gz
├── 0042_0_log.gz

最初親ディレクトリを含んでしまっていることを分かっておらず、codebuild等で実行するymlファイルのzip化を親ディレクトリ含んで行ってしまっていて、実行ファイルがありませんとエラーが出て結構な時間悩んだことがあった(今思えばむしろなんで気づかなかったと言いたいところだけれど)

ちなみに容量に関しては、親ディレクトリを含む方が2Kほど増えた

  • (余談)lsとduについて ファイルの容量を調べたい時、lsとduコマンドどちらを使えばいいかで迷った。 二つのコマンドを実行してフォルダやファイルのデータ容量を見ると、表示される数字に大きく差分が生じた。
$ ls -lah
-rw-r--r--@  1 user  staff   3.0K 11  4 22:53 test.txt

$ du -h ./test.txt
4.0K   ./test.txt

lsは「ファイルそのものの容量」

一方duは「ファイルが使用するディスク量」

を出力する。

どちらのコマンドもよく使っていたけれど、この辺りちゃんと区別できていなかったから目から鱗だった。

今回の場合はファイルそのものの容量を調査したかったから ls でよかった。

SRE Lounge #10 行ってきました

はじめに

sre-lounge.connpass.com

今日仕事終わりに行ってきました。

今回で3回目の参加になるのですが、一番楽しみにしているTechイベントです。

資料は一旦、現時点で上がっているものだけ貼ります。

タイムテーブル

  • SREcon19 Asia/Pacific Recap #1
    メルカリ @dtan4さん
  • SREcon19 Asia/Pacific Recap #2
    Quipper @chaspyさん
  • Make It Visible 〜株式会社ビズリーチ HRMOS SREチームのObservability戦略〜
    BizReach @saitotakさん
  • ヤフー様スポンサートーク SRE部の取り組み
  • スペシャル企画】モニタリングパネルディスカッション
    ヤフー @ykawamot さん、メルカリ @spesnovaさん、ソラコム 五十嵐さん、サイバーエージェント 袴田類さん

最初の二つの発表は6月にシンガポールで行われていたSRECon19に参加されたお二方のレポ&LTの再演&面白かった発表の紹介でした。意外なことに銀行系など固めな企業もこの領域に手を伸ばしていて、SRECon19に登壇されていたとか。

▼SREcon19 Asia/Pacific Recap #1 @dtan4さん発表資料

SREcon19 Asia/Pacific Recap - Speaker Deck

Our Practices of Delegating Ownership in Microservices World - Speaker Deck

@dtan4さんのLT再演は個人的に刺さる内容でこのタイミングで聞けてよかったなと。社内に数多くあるMSのTerraformコードを一つのリポジトリで一括管理されている話なんかも、Terraformのディレクトリ構成やmodule化って他社はどうしているのだろうと気になっていたので参考になりました。(Terraformは自由度高いからこういう部分が悩みどころで、せめて公式でBestPracticeみたいなのを一つ用意していて欲しい、、それらしきもの見かけたけどどうして廃止されてしまったのか)。メルカリさんは基本的には開発が運用も行う方針になっているらしく、SREチームが監視や運用部分の共通基盤を提供しているとのことでした。その共通基盤を作る取り組みの一環としてサービスごとにレベルを設定して、そのレベルごとにReadness Checklist(AutoScalingするか、キャパシティが設定されているかなど)を提供することで運用ルールを厳格化している話も興味深かったです(正直見たいと思ったのは私だけでないはず)。

▼SREcon19 Asia/Pacific Recap #2 @chaspyさん発表資料

SRECon19 AsiaPacific Recap - Speaker Deck

@chaspyさんの発表は現地の写真も多かったです。600人でこじんまりしていたと最初の発表で@dtan4さんがおっしゃっていたけど、全然こじんまりしているように見えなかった...。Linkdin社とInstagrem社の発表内容が印象に残ったと紹介してくれました。特にInstagram社さんの話は大陸をまたいでデータセンターを構築した時の話で正直スケールが大きくて付いていけなかったです。

ビズリーチの@saitotakさんの発表は、ビズリーチ社におけるSREの取り組みについて、とりわけミッションの可視化、日々の業務の可視化計測が中心の話でした。 優先順位(やらなければやばい順)に並び替えても全部優先度《高》になってしまったタスクについて、メンバーが納得のいく優先順位をつけるために「可用性+トイル+セキュリティ指標 / 概算ストーリーポイント」で計算してissue scoreを付けた話など、普通だとこの辺まで考えるだろうなのさらに一歩先まで深く考えられていることに感心しました。可視化の話は前の発表でも出てきており、改めて大事さを思い知らされました。(弊社の場合だと可視化まではできていてもカテゴリー分けや集計みたいなところが疎かになりがちだったので)。jiraとかあると集計が楽でいいですよね。

ヤフーSRE部さんの取り組みの話、ヤフーさんの会社自体は規模が大きくてエンジニアも数多くいてどちらかといえば結構特殊だと思っていたのですが、ツールを広めるためのお話はどんなチームの規模でも共通して活かせそうな体験談になっていて聞けて良かったです。機会があれば参考にしようとメモに残しました。

パネルディスカッションは今回初の取り組みでした。司会を主催の@katsuhisa__ が行い、ヤフー@ykawamot さん、メルカリ@spesnovaさん、ソラコム五十嵐さん、サイバーエージェント袴田類さんがテーマに対して自社のお話をしていく形式で、今回は監視ツールの選定やモニタリングやアラート、オンコールがテーマでした。パネルディスカッションのいいなと思ったところが、発表ではまとめようとしたらこぼれ落ちるような些細だけど他社にとっては意外と貴重だったりする情報が聞けるところでした。個人的に印象に残っているところが、CAさんはstg環境にも監視システムを導入しているという話で、最初聞いたときは検証だったらテスト用のチャンネルやDashboardなどに向ければいいじゃないかと思っていたのですが、意図としてはfalseアラートをできる限り無くして監視担当者の余計なストレスを減らすことを気をつけているそうでした。狼少年問題はよくある話でそれに対してパネルディスカッションでも色々な意見がありましたが、CAさんのこの姿勢は見習いたいと思うものでした。またアラートの飛ばし方に関しては多くの会社が主にwarningとcriticalの2種類あって、warningはslackへ通知、criticalは合わせてPargerDutyで電話もという組み合わせが多かったです。(弊社も最近PargerDutyが導入されてそんな感じになっている)

個人的な話

人見知りなので普段は終わったらさっさと帰ってしまう派だったのですが、今日は聞いてみたい話があったのと他にも色々思うところがあって懇親会に残ることにしました。何名かのエンジニアとお話しさせてもらいました。近い分野をやっている人が同僚以外だとどうしても少なくなってしまうので新鮮な気分になりましたし、自分でも知らず知らずの内に凝り固まっているところがあったなってことに気づいたので、やっぱり色んな人と話すことは大切だなと痛感しました。(いい刺激に近い方の痛感です)

またちょっと気になったのが、自分がSREの一環としてやっていると思っていたことが、他の人だと認識の違いを感じた事がありました。組織や規模によって仕組化の部分に集中できるところもあれば、1つのサービスの業務を行いながら仕組化に昇華させなければいけなかったりだとかその辺りは組織によってまちまちなのかもしれません。

SRE Loungeは現職に就いてから同僚と来ています。数ある技術の仕事の中でも、近しい分野に興味がある歳の近い勉強会に行く知り合いがあまりいなかったので、セッション聞いて普段の業務や仕組や組織について思っていることをああだこうだと気軽に言い合える良い機会にもなっています。仕事場でも普通に話はするけれど、仕事場じゃ話せないような不安やつらみ(?)みたいなのもついでに聞いてもらえて同僚には感謝しかないです。今日も同僚との会話の中で初心を思い出したりなんかしました。

おわりに

勉強会レポートなので情報量というはテンション重視で書き上げました。(途中からは深夜テンション。一応朝に推敲してます)

元々SRE的なことがやりたいと思いつつも、日々の業務に忙殺されてやりたいことに技術力が追いつかなくて歯がゆい思いをしたりして心折れそうになることも多々あります。SRE Loungeで色んな会社の発表を聞くと、狭くなっていた視野が広がって技術以外にも得られるものも多くて本当に良い刺激になっています。 いつももらってばかりで申し訳ないので、そのうち何か発表できるようになりたいです。(凄腕のエンジニアばかりで萎縮しがち)

弊社も開発チームが増えてきたので監視運用などの運用基盤の厳格化が必要になってきましたし、元々あるTerraformをCIでもっと良い感じに実行できるようにしたいですし、インフラ環境のコスト削減や自動化など、やりたいことがいっぱいです...月曜日からも仕事頑張ります。

ECSで動かしているバッチのSlackへのアラート設定

はじめに

少し前に業務でやったことの忘備録です。

アジェンダ

  • 設計思想
  • 全体の流れ
  • ECS
  • lambda
  • Datadog経由でSlackに通知
  • その他考えていたこと
  • おわりに

設計思想

とあるECSクラスタで動いているバッチが5つあるのですが、コケた時にすぐに気づける仕組みがありませんでした。 各バッチそれぞれがログは吐いているものの内容は様々なので(DBへのupdateをかけているものもあれば、毎日1回社内向けにメールを飛ばすものなど)、最低限『コケたらすぐに気づける。ログを見に行けるようにする』という要件を満たせるようにアラートの設定を行いました。

本当はアラートと一緒にログを貼り付けたり、ここを確認してこういった手順を取ってくださいみたいなplaybookを貼り付けられたら親切だったのですが、5つもあって重要度もそれほど高くないので、そこまで個別のバッチに時間をかけるのはやめました。

またDatadogを咬ませているのは、社内のルール(というかMSの監視のベストプラクティス)でサービスに関わるアラートは一箇所でまとめて管理するという方針になっているからです。そうすることでプロダクトを構成しているあらゆるサービスの監視を行いやすくします。アラートの通知先を変える必要が出た時なんかも、Datadog上からSlackのチャンネルを変更するといったことが行えるので、Lambdaのコードをいじる必要が無くなります。 Datadogを使っていなければLambdaからそのままSlackにアラートを飛ばしていました。

全体の流れ

(1)ECS → (2)CloudWatch Events → (3)Lambda → (4)Datadog → (5)Slack

(1) ECSがタスクの起動や終了を行うとイベントストリームを吐き出します。イベントストリームで吐き出された情報には、定義や終了コード、クラスタなど実行されたタスクに関する情報が含まれます。

(2) CloudWatch Eventsの設定で、特定のクラスタのイベントストリームをトリガーに、Lambda関数を実行するルールを作成します。

(3) Lambda側ではDatadogにとばすメトリクスを作成します。 飛ばすメトリクスの内容は以下のようにしました。

・バッチが正常に実行されたら0

・バッチが正常に実行されなければ1 (イベントストリームに含まれるexitCodeが0以外の数字であること、またはexitCodeが存在しない。)

exitCodeはコンテナ終了ステータスです。exitCodeが存在しない時というのは、例えば存在しないimageが指定されていてpullに失敗した時などそもそもタスクのstartに失敗した時になります。

(4) Lambdaから送ったメトリクスに対して、Datadogでモニター作成とアラートの設定を行います。 失敗した時のメトリクスが検知された時アラートを飛ばすようにここから設定を行います。 これであらかじめ設定してあるSlackのチャンネルに条件に従ってアラートを飛ばすようにできます。

ECS

イベントストリームの中身は以下のようになっています。 これはAWSブログから持ってきたもので、Lambdaのtestイベントの設定で使っています。

{
  "version": "0",
  "id": "8f07966c-b005-4a0f-9ee9-63d2c41448b3",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "244698725403",
  "time": "2016-10-17T20:29:14Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
  ],
  "detail": {
    "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/eventStreamTestCluster",
    "containerInstanceArn": "arn:aws:ecs:us-east-1:123456789012:container-instance/f813de39-e42c-4a27-be3c-f32ebb79a5dd",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:us-east-1:123456789012:container/4b5f2b75-7d74-4625-8dc8-f14230a6ae7e",
        "exitCode": 1,
        "lastStatus": "STOPPED",
        "name": "web",
        "networkBindings": [
          {
            "bindIP": "0.0.0.0",
            "containerPort": 80,
            "hostPort": 80,
            "protocol": "tcp"
          }
        ],
        "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
      }
    ],
    "createdAt": "2016-10-17T20:28:53.671Z",
    "desiredStatus": "STOPPED",
    "lastStatus": "STOPPED",
    "overrides": {
      "containerOverrides": [
        {
          "name": "web"
        }
      ]
    },
    "startedAt": "2016-10-17T20:29:14.179Z",
    "stoppedAt": "2016-10-17T20:29:14.332Z",
    "stoppedReason": "Essential container in task exited",
    "updatedAt": "2016-10-17T20:29:14.332Z",
    "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328",
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/wpunconfiguredfail:1",
    "version": 3
  }
}

参考 aws.amazon.com

CloudWatch Events

ルール作成時、イベントソースの設定部分で以下のようなイベントパターンを指定しています。

{
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:[ACCOUNT ID]:cluster/CLUSTER_NAME"
    ],
    "lastStatus": [
      "STOPPED"
    ]
  },
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ]
}

ここでの懸念事項が一つありました。

最終的には正常なメトリクスもDatadog側に飛ばすようにしていますが、当初は失敗した時のみアラートを飛ばすことが目的だったので異常な時だけメトリクスを実行できるようにすればいいのではないかと考えていました。Lambdaも実行するたびにお金がかかってしまうので。

ただ上記にも書いた通り、この時点で異常終了か否かを検知することはできなそうでした。exitCodeが存在しないケースもあるためイベントパターンで定義ができなかったです。 また正常に実行されているかどうかもモニターから確認できた方が良さそうだと要件が変わったこともあり、クラスタが実行される度にLambdaを起動させることにしました。 そもそも4/5がdailyバッチなので料金が心配になる程そもそも実行しませんでした。

参考

docs.aws.amazon.com

Lambda

Pythonで書いています。

import urllib.request
from datadog_lambda.wrapper import datadog_lambda_wrapper
from datadog_lambda.metric import lambda_metric

@datadog_lambda_wrapper
def lambda_handler(event, context):
    # success=0, error=1
    batch_exec_status = 0

    if "exitCode" not in event["detail"]["containers"][0]:
        batch_exec_status = 1
    elif event["detail"]["containers"][0]["exitCode"] != 0:
        batch_exec_status = 1

    batch_name = event["detail"]["taskDefinitionArn"].split('/')[1].split(':')[0]
    result_status = lambda_metric("stg_hoge_batch.batch_exec_status", batch_exec_status, tags=['hogehoge', 'category:batch:'+batch_name])

    return batch_name+':exitCode '+str(batch_exec_status)

Lambdaのソースコードに関してはちょうどMRのコメントで修正箇所が大量に出され、それを今直している最中なのですが、処理内容的にはこんな感じです。

LambdaのLayer機能があるのですが、Datadogが公式でDatadog Layerを用意してくれていて、それを使うとメトリクスの送信(lambda_metric())が簡単に行えるので超便利です。いい時代。

exitCode以外でやっていることは、実行されたバッチの名前を取得してメトリクスのtagsに付けています。これは通知の時に表示させるのに使うためです。 バッチの名前はタスク定義のARNから抜き出しています。

参考

docs.datadoghq.com

Datadog

以下のようにメトリクスとアラートの設定をします。

↓ 3 Set alert conditionでメトリクスが1(>0)だった時にアラートが飛ぶように設定しています。

f:id:reiichii:20190630130527p:plain

f:id:reiichii:20190630130230p:plain

2 Define the metricで「Multi Alert」の設定を行なっています。これを行うと個別のアラートを発生させることができます。今回のケースだと category:batch:[[バッチ名]] のようになっているので、アラートのタイトルにバッチ名が表示されるようになります。

f:id:reiichii:20190630131932p:plain

没にした案

CloudWatch Logsのログストリームからメトリクスを作成してアラートを飛ばす方法

当初はCloudWatch Logsで各バッチが吐き出すログから、「Error」文字などでフィルターをかけてカスタムメトリクスを作成し、それを検知したらLambdaからアラートを飛ばす、というようなことを考えていました。ただこれだとバッチが5種類あってエラーログが様々なので個別に設定しなければならないことが多くなってしまうこと、そもそもタスクが起動しなかったらエラーが検知されないからアラート設定として不完全であること、といったような理由から没にしました。

本当はログのパターンが統一されているべきなんでしょうけど。

ともあれECSのタスク実行の確認をするだけなら、Logを見るよりイベントストリームの情報を確認するやり方の方が良かったなという確信はあります。

AWS SNSを挟むか

アラートの飛ばし方を調べる中で、CloudWatch EventsやAlermからSNSに送って、そこからLambdaに送るような設定も見ました。通知先が複数あったり、中身によってフィルタリングした上で通知先を決めるみたいなことをするのであればLambdaで実装するよりSNSを利用した方が良さそうでしたが、今回は通知先は一つしかないのとメトリクスの加工をする必要があったのでSNSは不要と判断しました。

最初はSNSでexitCodeが0か0以外かでSlackに通知を流すことをやろうとしていたのですが、結局それはできなかったな。。

ECSバッチを実行する方のCloduWatch Eventsのアラート

上記のアラート設定は、ECSタスクスケジューリングの失敗はカバーしていません。そもそもバッチが起動しなかったらイベントストリームは発行されません。なのでバッチを実行するCloduWatch Eventsがコケた時もアラートを出す設定を別で用意する必要があります。

ただ今回の私のケースだとDatadogの方でCloudWatch Eventsの失敗が検知されたらアラートが飛ぶように一括で設定してあるので、ルールを作った時点でそちらに含まれる形になります。Datadog良き。

おわりに

ECSのイベントストリームを知らなかったから、exitCodeでアラートを出すやり方を見つけるまでに苦労しました。

今回の設定を通して(実は苦手意識を持っていた) Datadogとも少し仲良くなれた気がします。もっと手足のように使いこなせるようになりたい。

yieldとは

はじめに

先日webスクレイピングを実装していた時に初めてyieldを使う機会がありました。

パッとググると概要は出てくるのですが使用例などがなかなかピンと来なかったので、これを機会にきちんと理解しておこうと思い立った次第です。

yieldを知るまでの道のり

今回調べていて、やっぱり説明するのも理解するのも一筋縄では行かないなという印象でした。

なので今回のブログは以下の流れでお送りします。

yieldの動作

ざっくりと説明すると、 returnが値を返してfunctionを終了させるものに対して、 yieldはreturnと同じように値を返した後、そのyieldの続きから処理を続行させることができます。

内部的な動作

↑のような説明は動作の部分を説明しただけなので、"yieldとは"といった目的の部分を説明するのには当てはまりません。(returnの処理を続行させないverという目的で作られたわけではないということ)

yieldはジェネレーターを作っています。 ジェネレータとはリストや辞書などイテレーターの一種です。他の一般的なイテレーターとの違いは「定義された時点で要素数に限りがあるか」だと言えます。

イテレーターは hoge = [1,2,3] のように中身が決まっているもので、このhogeをまとめてfor文に引き渡して回して一つ一つの要素に処理をするのに対して、ジェネレータは定義した時点では要素の中身は決めることができなく、for文で要素に対して処理を行う段階で初めて要素が確定される形です。「遅延評価」などというみたいです。

「定義段階で要素が確定しないリストってなんだよ」と思う人もいるかもしれません。 その一例がスクレイピングしたい対象のwebページだったりします。「そのページのリンクを3階層までクローリングする」といった処理を実行させる時、実行させるまで中身の数は確定しませんよね。

使用例(webスクレイピング実装したときの話)

以下のコードは私がwebスクレイピング入門した時に書いたもので、好きなアーティストのブログをクローリングして、写真があればあるだけローカルPCにDLするものになっています。

class MimorinSpider(scrapy.Spider):
    ''' 略 '''

    def parse(self, response):
        # そのページの画像を取得
        yield self.parse_items(response)

        # 総ページ数を取得
        ''' 略 '''
        # 総ページ分回してurlを生成
        if self.current_page <= int(total_page):
            ''' 略 '''
            next_url = 'https://lineblog.me/mimori_suzuko/?p=' + str(self.current_page)
            # 次のページへのリクエストを実行する
            yield scrapy.Request(next_url, callback=self.parse)


    def parse_items(self, response):
        item = MimorinItem()
        item['image_urls'] = []

        for image in response.css('img.pict::attr(src)').extract():
            item['image_urls'].append(image)

        return item

プログラム的に重要なポイントは二つです。

  • 画像取得: scrapyが実行されたら、そのページのブログの画像を取得して、ImagePipelineに渡す(ImagePipelineの中で渡した画像をローカルに保存したりといった、取得した画像に対する処理を行ってくれるため。超便利だった)
    • → ImagePipelineにitemを渡すために def parse() 内で return item をする必要がある
  • ページ遷移: そのページのスクレイピングが終わったら次のページに進む。進める限り処理を続けるようにする。
    • → 次のページにクローリングを進めるためには、次のページのurlをリクエストするメソッドを入れて def parse() を実行させないといけない。return item で処理を終わらせられない

という要件があります。

ここではyieldは二ヶ所で使われています。

  • yield self.parse_items(response)
    • 一ヶ所はページの画像を取得するメソッドを呼び出すところです。ここでyieldを指定して値を返す処理を入れないと、itemがPipelineに渡されず、画像のローカル保存が行われません。
  • yield scrapy.Request(next_url, callback=self.parse)
    • もう一ヶ所は次のページへのリクエストを実行するscrapy.Request()を実行する部分です。yieldを使うことでスクレイピングを実行する時に呼び出されるdef parseは終わらずに、その中で次のページに対するdef parseが実行され続けています。

以下は完全に余談で、ちなみに私の詰まっていたところ

  • 🤔「(yield self.parse_items(response)に対して)どうしてここにyieldを定義しないといけなかったんだっけ」
    • → yieldがジェネレーターであるというところに意識が行き過ぎてそもそも値をreturnするものであることを忘れていた
  • 🤔「(yield scrapy.Request(next_url, callback=self.parse)に対して、最初yieldを書かないでいて、なんで次のページのスクレイピングが取得されないのかがわからなかった)」
    • → yieldは実行された時点で処理を中断し再開させるもの。yieldを書かなかったコードはscrapy.Request()を実行して処理を正常終了。当然scrapy.Request()で呼ばれた子parse()も親メソッドの終了に合わせて終了する😇

自分の場合は、この部分の意味を理解したことでyieldの使い方や役割についてイメージが湧くようになりました。

その他のユースケースと参考資料

参考にした書籍や記事では以下のようなユースケースで説明されていることが多かったです。

  • メモリの負担を和らげるために使う。一度に大きなリストを返すとメモリを多く消費してしまうため負担がかかる。yieldで小分けに実装することで負担を抑える。
    • → ジェネレーターを生成し、for文でそれを実行させて要素に対して実行させるといった使い方がやっぱり多いらしい。

webスクレイピングのコードを書いている時、当初自分は「ページは限られているのだから一度urlを全ページ分スクレイピングし切った後、リストに入れて全部まとめてImagePipelineに渡せばいい」と考えていたのですが、これはアンチパターンのど真ん中だったわけです...

参考資料:

yieldのまとめ

  • functionの中の処理を中断し、値を返して、また続きから再実行させる ≠ return
  • 素数が未確定のジェネレーターを定義し、都度処理を実行させる

おわりに

本や記事を読んでいても、webスクレイピングをしなければyieldについて今以上にピンときていなかったかもしれません。

またyield fromの書き方についてや、more_itertoolsのようなモジュールについては今回は手を伸ばしきれませんでした。

if文だけで書いていたvalidationをクラス設計し直した過程

GWはpythonの勉強がてら麻雀の点数計算を行うスクリプトを書いています。

input()のバリデーション部分の実装で、私は最初フローチャートをそのままコードに落とし込んだようなものを書いていたのですが、知り合いからのツッコミでvalidation用のクラスを設計することにしました。その忘備録です。ちなみに言語はPython 3.6.8。

リファクタ前(抜粋)

class Yaku():

    def input_yaku_validate(self, str):
        end_num = self.list.index(self.list[-1])

        if str.count(',') == 0:
            if int(str) > end_num:
                print('有効な数字ではありません')
                sys.exit()
            else:
                hands = [int(str)]
        else:
            if re.search(r'([0-9]{1,2},)*\d', str) == False:
                print('選択肢の数字と「,」以外は含めないでください')
                sys.exit()
            else:
                hands = []
                for n in str.split(','):
                    if int(n) > end_num:
                        print('有効な数字ではありません: ' + n)
                        sys.exit()
                    hands.append(int(n))
        
        return hands

class Mahjang():
    def calculator(self):
        yaku_name_list = [ str(i) + '.' + j for i, j in enumerate(yaku.yaku_name_list()) ]
        h = input('複数ある場合は「,」で区切ってください ex)1,3,5\n' + ' '.join(yaku_name_list) + '\n: ')
        hands = yaku.input_yaku_validate(h)

上記の処理の内容は以下のようになっています。

  • コンソール上に役一覧を数字付きで表示させて、ユーザーが数字で役を入力する

  • 一つの時は「1」二つ以上の時は「1,2」の形を受け入れる

  • その入力値をinput_yaku_validate()でvalidationする

内容がそれほど多くないこともあって、フローチャートのままif文を駆使して処理していました。 ただif文とfor文が入れ子になっていて汚いですし、他のinputでregexの処理をしたいときなどまた改めて re.search()などで定義しなければ行けなくなってしまいます。

リファクタ後

class ValidationStrategy():
    @abstractmethod
    def validate(self, str):
        raise NotImplementedError()

class Validator():
    @abstractmethod
    def validate(self, input):
        raise NotImplementedError()

class YakuValidationStrategy(ValidationStrategy):

    def validate(self, str):
        regex = r'[0-9]{1,2}' if len(str) == 1 else r'([0-9]{1,2},)*\d'
        max_num = len(Yaku.list)
        validators = [
            RegexValidator(regex),
            NumMaxValidator(max_num)
        ]

        for validator in validators:
            result = validator.validate(str)
            if result != True:
                raise ValueError(result)

class RegexValidator(Validator):
    def __init__(self, regex):
        self.regex = regex
    
    def validate(self, str):
        if re.search(self.regex, str) == False:
            return '適切な値を入力してください'

        return True

class NumMaxValidator(Validator):
    def __init__(self, max_num):
        self.max_num = max_num

    def validate(self, str):
        for i in str.split(','):
            if int(i) >= self.max_num:
                return '適切な値を入力してください'
        
        return True

class Mahjang():
    def calculator(self):

        yaku_name_list = [ str(i) + '.' + j for i, j in enumerate(yaku.yaku_name_list()) ]
        h = input('複数ある場合は「,」で区切ってください ex)1,3,5\n' + ' '.join(yaku_name_list) + '\n: ')
        try:
            YakuValidationStrategy().validate(h)
        except ValueError as e:
            print(e)
            sys.exit()      
        hands = [int(i) for i in h.split(',')]

新たに以下のような役割を持つクラスを作りました。

  • YakuValidationStrategy: 役入力用のStrategyクラス。役以外にもDoraValidationStrategyUserAttrValidationStrategy など入力に応じて〜ValidationStrategyというクラスを作りました。

  • ValidationStrategy: 〜ValidationStrategyの親クラス。必ず validateメソッドを定義させるようにします。

  • RegexValidator: 正規表現で入力された文字列がマッチするかを調べるvalidateを行うクラス。

  • NumMaxValidator: 入力された数字が役一覧の数字であることを確認するvalidateを行うクラス。例えば0.ツモ〜46.国士無双十三面待ちまである中で47などない数字が入力されていないかをチェックします。

  • Validator: 渡された値をチェックして返す系の処理を行う〜Validatorクラスの親クラスです。こちらもvalidateメソッドを必ず実装の強制を定義しています。

クラスは大きく分けると二種類、

  • 実際にvalidationを行う内容を定義した〇〇Validatorクラス

  • inputごとになんのどのvalidationを行わせるか(validatorクラスを呼び出すか)ロジックを管理する〇〇ValidationStrategyクラス

です。

どちらもvalidateメソッドを持っていて最初は紛らわしく見えるかもしれませんが、これらのクラスの扱い方としてはinputごとに〇〇ValidationStrategyクラスを作成し↓、

class YakuValidationStrategy(ValidationStrategy):

    def validate(self, str): 
        regex = r'[0-9]{1,2}' if len(str) == 1 else r'([0-9]{1,2},)*\d' 
        max_num = len(Yaku.list)
        validators = [    # ※1
            RegexValidator(regex), # ※2
            NumMaxValidator(max_num)
        ]

        for validator in validators:
            result = validator.validate(str)
            if result != True:
                raise ValueError(result)

この〇〇ValidationStrategyクラスの中でチェックする項目のvalidationを行うクラスを定義して(※1)、順番にvalidateを実行していきます。

class RegexValidator(Validator):
    def __init__(self, regex):
        self.regex = regex
    
    def validate(self, str):
        if re.search(self.regex, str) == False:
            return '適切な値を入力してください'

        return True

regexのようなものはinputによってre.search()の第一引数に入れる正規表現を変えられるようにすべきですが、その部分は〇〇VlidationStrategyクラスの中でValidatorインスタンスを呼び出すときに渡すことで実現可能です。(※2)

validationを行う時は、〇〇ValidationStrategyクラスのvalidate()を呼び出せば、Strategyクラスの中で定義したvalidationが順次行われ、True以外のもの(今回の場合はエラーメッセージ)が返ってきたらエラーを発生させて処理を終わらせられます。

このようにクラス化することで以下のようなメリットが得られるようになりました。

  • validateを行う処理の使い回しが効く

  • あるinputのvalidationロジックを変えても他の箇所に変更が影響しない

  • mainの処理にvalidateの処理を長々と書かなくて良くなった

  • ValidatorのvalidateメソッドはTrueかエラーメッセージが返ってくると決まっているので改修が必要になっても付け加えやすい

まとめ

オブジェクト指向については本を読んだことがあって概要は知っていましたし、FWを使った開発の経験もあったので既存のソースコードを読んで改修することなどはできます。しかしクラス設計等を自分でやったことはなかったので、デザインパターンを読んでいまいちピンと来なかった実装方法を自分の中に落とし込むことができて良かったです。それにオブジェクト指向を使った開発の便利さをまた別の形で実感することができました。

今回の点数計算スクリプトの全体は以下です。※未完成

github.com

ちなみにこのお遊びスクリプト、一応形はできているもののまだまだ完成には程遠いです。 そもそも府計算を切り捨てている時点で正確な実戦で使える正確な点数は出ないのですが、それ以前にまだ七対子や数え役満に対応させないといけません。実用的なものを作りたいわけじゃないので完璧さは追い求めませんが、最低限上に書いたような改良をしていくにはそろそろテストコードがないと動作確認がめんどくさくなってきている。。