2023年6月10日土曜日

rqではワーカーに渡す変数情報はクラスのオブジェクトでも良い

rqではワーカーに渡す変数情報はクラスのオブジェクトでも良い

概要

エンキューする際にシリアライザを内部的にコールしておりデフォルトのシリアライザが pikle を使っているためオブジェクトでもワーカーに渡すことができます

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

エンキュー

  • vim app.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from lib.util import say, Message

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

scheduler.cron(
    "*/1 * * * *",
    func=say,
    args=[Message("Hello!")],
)


scheduler.run()

上記の args=[Message("Hello!")], のポイントです
string や integer の値ではなく Message クラスのオブジェクトを渡すことができます

ワーカー

  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message


def say(msg: Message):
    print(msg.message)
    return f"{msg.message}: {datetime.now()}"
  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

動作確認

  • pipenv run python app.py
  • pipenv run python worker.py

最後に

デフォルトのシリアライザを拡張して独自のシリアライザを指定することも可能です

2023年6月9日金曜日

Flask-RQ2を試す

Flask-RQ2を試す

概要

前回 rq-scheduler を使って見ました
今回は Flask の拡張である Flask-RQ2 を使って rq-scheduler と組み合わせてみました

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1
    • flask 2.3.2
    • flask-rq2 18.3

流れ

  1. Flask アプリでリクエストを受け取りジョブを登録
  2. スケジューラの実行
  3. ワーカーの実行

という流れになります
つまり起動するプロセスも 3 つ必要になります

Flask アプリ: ジョブを登録する

  • vim app.py
from flask import Flask, request
from flask_rq2 import RQ
from lib.util import Message

app = Flask(__name__)
app.config['RQ_REDIS_URL'] = 'redis://localhost:6379/0'

rq = RQ(app)


@app.route("/")
def register_job():
    username = request.args.get('username', "default_user")
    # get_scheduler は rq-scheduler のオブジェクトが変えるので cron が使える
    scheduler = rq.get_scheduler(queue='bar')
    job = scheduler.cron(
        "*/1 * * * *",
        func=Message(f"{username}").say,
        args=[],
    )
    print(job)
    return f"<p>{job.get_id()}</p>"
  • vim lib/util.py
from datetime import datetime

class Message():
    def __init__(self, message: str):
        self.message = message

    def say(self):
        msg = f"{self.message}: {datetime.now()}"
        print(msg)
        return msg

以下で起動しておきます

  • pipenv run flask run

スケジューラの起動

スケジューラは登録されたジョブの内容に応じて実際にキューにジョブを登録しワーカーにジョブを渡す役割になります

今回は特にスケジューラはカスタムしないのでコマンドラインで起動します (カスタムする場合は python スクリプトを作成してそれを起動します)

  • pipenv run flask rq scheduler

flask-rq2 をインストールすると flask 経由でスケジューラを起動できるようになります

ワーカーの起動

最後にワーカーを起動しましょう
これもコマンドで起動できますがせっかくなのでスクリプトにしました

  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)

起動は

  • pipenv run python worker.py

で OK です

動作確認

  • curl localhost:5000

と実行するとジョブが登録されます
そしてスケジューラが登録されたジョブのルールーに沿ってキューにジョブを登録します

あとはワーカーのログにジョブが実行されているログが流れれば OK です

最後に

Flask-RQ2 を使ってみました

組み合わせて使わなくても OK ですが組み合わせることで特殊なデコレータが使えたり Flask の設定ファイルの管理と同じにしたりすることができます
また Queue や Worker などのモジュールも Flask 経由で取得できるようになります

参考サイト

2023年6月8日木曜日

Pythonのrq-schedulerを試す

Pythonのrq-schedulerを試す

概要

rq-scheduler は Python で使えるスケジューラツールです
特定の時間や条件に応じて何か処理を実行させることができます
バックエンドには redis を使います
今回はインストールから簡単なサンプルコードを動かしてみます

環境

  • macOS 11.7.6
  • Python 3.10.2
    • rq 1.15.0
    • rq-scheduler 0.13.1

インストール

  • pipenv install rq-scheduler

redis 起動

  • brew services run redis

サンプルコード

流れとしては

  1. ジョブを redis に登録
  2. 登録したジョブをワーカーが受け取り実行

という感じになります

ジョブを登録する

rq-scheduler の cron 機能を使ってジョブを登録してみます

  • vim enqueue.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler
from datetime import timedelta
from lib.util import say

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)


# 指定の時間に実行するジョブを登録
# scheduler.enqueue_at(datetime(2023, 6, 6, 14, 36), say, "hello")

# 指定の間隔で実行するジョブを登録
# scheduler.enqueue_in(timedelta(seconds=5), say, "hello")

# cron 形式でジョブを登録
scheduler.cron(
    "*/1 * * * *",
    func=say,
    args=["hello"],
)

scheduler.run()  # なくてもいい
  • pipenv run app.py

まずこれを実行すると以下のようなキーが redis に登録されます

redis-cli keys '*'
1) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
2) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"
3) "rq:scheduler:scheduled_jobs"

rq:job から始まるキーが登録されたジョブの実態になります
さらにしばらくすると

redis-cli keys '*'
1) "rq:queue:bar"
2) "rq:queues"
3) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
4) "rq:scheduler:scheduled_jobs"
5) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"

となっておりジョブが実際に bar キューに登録された状態になります (rq:queue:bar )

ジョブを実行するワーカー

ワーカーは rq モジュールだけで OK です
実際にキューイングされたジョブを実行します

キューイングした際に指定したモジュールの関数はこのワーカーからも参照できる必要があるので注意しましょう (lib.util.say は自動でワーカーが参照してコールする)

  • vim worker.py
from rq import Worker, Queue
from redis import Redis

redis = Redis()
queue = Queue('bar', connection=redis)


if __name__ == "__main__":
    worker = Worker(queues=[queue], connection=redis)
    worker.work(with_scheduler=True)
  • pipenv run python worker.py

実行すると以下のようなログが流れます

15:50:00 Worker rq:worker:9f0f761294194e79a4bb9b87c0e35436 started with PID 90351, version 1.15.0
15:50:00 Subscribing to channel rq:pubsub:9f0f761294194e79a4bb9b87c0e35436
15:50:00 *** Listening on bar...
15:50:00 Cleaning registries for queue: bar
15:50:00 bar: lib.util.say('hello') (53acc2fb-aa88-4627-9a4c-183eab6ee07a)
hello
15:50:00 bar: Job OK (53acc2fb-aa88-4627-9a4c-183eab6ee07a)
15:50:00 Result will never expire, clean up result key manually

redis 内は以下のようになりました

keys *
 1) "rq:clean_registries:bar"
 2) "rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
 3) "rq:finished:bar"
 4) "rq:scheduler_instance:6f65031c38a2416bb1352d1fca46dfa9"
 5) "rq:worker:9f0f761294194e79a4bb9b87c0e35436"
 6) "rq:scheduler-lock:bar"
 7) "rq:queues"
 8) "rq:workers"
 9) "rq:results:53acc2fb-aa88-4627-9a4c-183eab6ee07a"
10) "rq:workers:bar"
11) "rq:scheduler:scheduled_jobs"

いろいろ増えていますが完了したジョブは rq:finished:bar にあります
結果は rq:results:53acc2fb-aa88-4627-9a4c-183eab6ee07a に格納されており今回の場合はあれば永遠に保存されます

ワーカーは実装しなくても動かせる

今回のように簡単なワーカーであれば以下のコマンドだけでも OK です

  • pipenv run rq worker -q bar

エンキューの際に実行するべきメソッドが指定されているのでワーカーはどのメソッドを知る必要がないためコマンドだけでもワーカーが起動できるようになっています

再度エンキューを実行した場合に新たにジョブが登録される

もし app.py を終了して再度 app.pyを実行すると新たにジョブが登録されます
つまり前に登録した cron ジョブは残ったままになるので app.py を実行するたびに redis には新しいジョブが登録されます

ジョブの登録を解除する

もう前のジョブはいらないということであれば登録を解除してあげましょう

  • vim delete.py
from redis import Redis
from rq import Queue
from rq_scheduler import Scheduler

queue = Queue('bar', connection=Redis())
scheduler = Scheduler(queue=queue, connection=queue.connection)

jobs = scheduler.get_jobs()
for job in jobs:
    if job.get_id() == "53acc2fb-aa88-4627-9a4c-183eab6ee07a":
        print(scheduler.cancel(job))

これで再度 redis を確認すると rq:job:53acc2fb-aa88-4627-9a4c-183eab6ee07a というジョブが削除されているのが確認できると思います

最後に

rq-scheduler を試してみました
特定の時間に実行するジョブを登録しておけば定期的にワーカーが実行してくれるシステムを作りたいときに使えるかなと思います

実際はワーカーからのフィードバックやワーカーのタイムアウト、動的なジョブの登録に対応できるようにする必要がありそうです

参考サイト

2023年6月7日水曜日

自分のツイートを分析してみた3

自分のツイートを分析してみた3

概要

前回の続きで 30,000 ツイートに達したのでデータ分析してみました

環境

  • Ruby 3.2.1

全ツイート数

  • 30220

全ツイートをどれくらいの期間で行ったか

  • 2016/03/24 - 2023/05/31

2625 日分のツイートになります

月単位でどれくらいツイートを行ったか

月間最高ツイートは 2019/06 で 846 ツイートでした (前回と変わらず)

日単位でどれくらいツイートを行ったか

日で最高ツイートは 2018/06/06 で 156 ツイートでした (前回と変わらず)

月単位でどれくらいリツイートをされたか

最高でも月で 4 リツイートが一番多かったです (前回から減っているので取り消しされてるっぽい)

月単位でどれくらいファボをされたか

月間最高ファボは 2021/10 で 41 ファボでした (前回から減っているので取り消しされてるっぽい)

ツイートしているデバイスの割合

ほぼブラウザからツイートでした (前回とほぼ同じ)

よくツイートされている単語 Top100

  • t -> 1990
  • co -> 1952
  • https -> 1780
  • ー -> 1223
  • 感じ -> 1146
  • アプリ -> 1002
  • 方法 -> 920
  • 人 -> 779
  • 自分 -> 744
  • ファイル -> 466
  • インストール -> 434
  • あと -> 402
  • docker -> 395
  • ゲーム -> 357
  • コード -> 346
  • 情報 -> 343
  • 久しぶり -> 326
  • gt -> 321
  • コンテナ -> 318
  • データ -> 309
  • サーバ -> 305
  • アカウント -> 286
  • API -> 277
  • 記事 -> 272
  • 自動 -> 260
  • ruby -> 258
  • コマンド -> 258
  • s -> 254
  • mac -> 253
  • 気 -> 234
  • ポイント -> 206
  • 別 -> 206
  • 環境 -> 205
  • D -> 197
  • 動画 -> 195
  • デフォルト -> 189
  • k -> 185
  • 画面 -> 180
  • ログイン -> 179
  • 雨 -> 175
  • ビルド -> 173
  • S -> 172
  • 複数 -> 171
  • ユーザ -> 164
  • ケース -> 161
  • バージョン -> 160
  • ドメイン -> 159
  • 名前 -> 158
  • ツール -> 157
  • ネットワーク -> 155
  • カード -> 153
  • web -> 152
  • 個人 -> 152
  • 他 -> 151
  • アップデート -> 151
  • レベル -> 148
  • サイト -> 147
  • ページ -> 147
  • イベント -> 142
  • 最後 -> 142
  • デプロイ -> 141
  • google -> 139
  • ホスト -> 139
  • 野球 -> 138
  • ブログ -> 137
  • クライアント -> 137
  • 子供 -> 137
  • ウォーク -> 137
  • 月 -> 136
  • iPhone -> 134
  • モード -> 133
  • ライブラリ -> 132
  • d -> 132
  • p -> 132
  • クラス -> 132
  • 状態 -> 131
  • 次 -> 127
  • 無料 -> 126
  • バグ -> 126
  • python -> 126
  • バッテリー -> 125
  • Ruby -> 125
  • パスワード -> 122
  • 文字 -> 122
  • Mac -> 121
  • プロ -> 120
  • ネタ -> 120
  • 娘 -> 118
  • マシン -> 116
  • 変数 -> 116
  • 時代 -> 116
  • どん -> 114
  • ドキュメント -> 113
  • 仕組み -> 113
  • iOS -> 112
  • wifi -> 111
  • 回線 -> 110
  • Google -> 109
  • メソッド -> 108
  • ローカル -> 108

最後に

40,000 ツイートしたら再度解析してみたいと思います フォロワー数は若干増えています

参考サイト

2023年6月6日火曜日

Typhoeus超入門

Typhoeus超入門

概要

http リクエストの並列実行が行える typhoeus (テュポーン) を試してみました

環境

  • macOS 11.7.6
  • Ruby 3.2.1
    • typhoeus 1.4.0

インストール

  • vim Gemfile
gem 'typhoeus'

サンプルコード POST

# frozen_string_literal: true

require 'json'
require 'typhoeus'

request = Typhoeus::Request.new(
  'https://request-dumper.kakakikikeke.com',
  method: :post,
  body: 'this is a request body',
  params: { field1: 'a field' },
  headers: { Accept: 'text/html' }
)

request.on_complete do |response|
  if response.success?
    puts response.body
    puts JSON.parse(response.body)['method']
  elsif response.timed_out?
    puts 'got a time out'
  elsif response.code.zero?
    puts response.return_message
  else
    puts "HTTP request failed: #{response.code}"
  end
end

request.run

他のメソッドをコールしたい場合は

method: :post の部分を method: :get にすれば OK です

並列実行

Typhoeus::Hydra を使います
使い方としてはこちらがメインになるケースが多いのかもしれません

queue に request オブジェクトをどんどん追加すれば OK です

# frozen_string_literal: true

require 'json'
require 'typhoeus'

request = Typhoeus::Request.new(
  'https://request-dumper.kakakikikeke.com',
  method: :get,
  body: 'this is a request body',
  params: { field1: 'a field' },
  headers: { Accept: 'text/html' }
)

hydra = Typhoeus::Hydra.new
hydra.queue(request)

request.on_complete do |response|
  if response.success?
    puts response.body
    puts JSON.parse(response.body)['method']
  elsif response.timed_out?
    puts 'got a time out'
  elsif response.code.zero?
    puts response.return_message
  else
    puts "HTTP request failed: #{response.code}"
  end
end

request.run

2023年6月5日月曜日

自分のツイートからマルコフ連鎖で文章を自動生成してみる

自分のツイートからマルコフ連鎖で文章を自動生成してみる

概要

3万件ほどある自分の Twitter のアーカイブデータからマルコフ連鎖を使って文章を自動生成してみました

環境

  • Ubuntu 18.04
    • mecab 0.996
  • Python 3.10.2
    • mecab-python3 1.0.6
    • markovify

Twitter アーカイブデータの取得

Twitter の Web 画面から行います
データ量によっては 1日以上かかる場合もあります
ダウンロードできたら解凍しその中にある tweets.js を保存しておきます

ちなみに自分のツイートは執筆時点で 30220 ツイートありました

ツールインストール

  • sudo apt -y install mecab mecab-ipadic-utf8
  • sudo cp /etc/mecabrc /usr/local/etc/mecabrc
  • mecab -D
filename:       /var/lib/mecab/dic/debian/sys.dic
version:        102
charset:        UTF-8
type:   0
size:   392126
left size:      1316
right size:     1316

ライブラリインストール

  • pipenv install mecab-python3 markovify

サンプルコード

とりあえず動かすのが目的なのでクラス化などのリファクタリングはしていません

  • vim app.py
import markovify
import MeCab
import json

# tweets.js を読み込んでツイート情報を配列に格納
tweets = []
with open("./tweets.js", mode="r") as f:
    for tweet in json.loads(f.read().replace('window.YTD.tweets.part0 = ', '')):
        tweets.append(tweet['tweet']['full_text'])

# ツイートの配列情報を1行ごとにしてテキストファイルに保存
print(len(tweets))
with open('./input.txt', mode='w') as f:
    for tweet in tweets:
        f.write(f"{tweet}\n")

# 1行ごとに保存したツイート情報をmecabを使って分かち書きに変換、分かち書きも1ツイート1行にして保存
parsed_text = ""
with open("./input.txt", mode="r") as f:
    for line in f.read().split():
        tagger = MeCab.Tagger("-Owakati")
        parsed_text += f"{tagger.parse(line).strip()}\n"

# 分かち書きを1行ごとに保存したのでNewlineText を使って学習
text_model = markovify.NewlineText(parsed_text, state_size=1)

# 文章を10個生成してみる
for _ in range(10):
    sentence = text_model.make_short_sentence(100, tries=100)
    if sentence is None:
        continue
    print(sentence)

動作確認

  • pipenv run python app.py
プラットフォーム なんて やっ て みる か ー 、 あきらめる か 、 、 、 やっぱり 筋 トレ 始め たら 逆 に
の 調子 が 読み込ま れ て いる みたい に 含ま れ ちゃう だろ う か 、 、
を 要求 さ
もう 忘れ て ほしかっ た ほう が 少ない から コロナ は ステート フル カラー ボックス ない や で ない の か な 。 もはや 顔出し し たい 場合 は
うーん 明日 から 届か ない 気 が 多い の 見る の 圧力 センサー 系 も 夢 は
300 H 1 . co / LRgfICHpN 0 ZVHy
で 使っ て から 完全 に 見 た ほう が 自分 だけ で も 試し た 事 象っ て も 会わ ず 、 、
background - server
とか なる 可能 性 が ある か 、
を 送り 続け ない ん ? これ は タッチバーモデル しか 見 てる の ほう が 、

正直意味不明ですがツイートと言えばツイートっぽい気もします
URL なども学習させているのでこのあたりをクリーニングすればもっと良くなるかもです

トラブルシューティング: UnicodeEncodeError: ‘utf-8’ codec can’t encode characters in position 0-1: surrogates not allowed

mecab はデフォルトだと euc_jp なので python の文字コードと違うがあるため print などでエラーになっています
mecab-ipadic-utf8 をインストールし直しましょう

最後に

パラメータなどを調整したりデータのクリーニングをしたりすればもっと良くなると思います 内部的な使用は不明ですがデフォルトだと分かち書きをしたデータをただランダムに組み合わせているレベルにしかならないのかもしれません

参考サイト

2023年6月2日金曜日

Sinatraのルーティングをクラスとして定義する方法

Sinatraのルーティングをクラスとして定義する方法

概要

Sinatra のルーティングをクラスとして定義する方法を紹介します
Sinatra::Base を使うのは当然なのですがルーティングの定義などもすべてインスタンスメソッドとして定義します

環境

  • macOS 11.7.6
  • Ruby 3.2.1
    • sinatra 3.0.6

サンプルコード

  • vim app.rb
# frozen_string_literal: true

require 'sinatra'

# / を管理するクラス
class MyApp < Sinatra::Base
  def initialize(path)
    super()
    @path = path
    routing
  end

  def routing
    MyApp.get @path do
      'hello'
    end
  end

  # 例えばルーティングに関する情報を使って何かを生成することもできる
  def to_h
    {
      path: @path
    }
  end
end

config.ru

  • vim config.ru
# frozen_string_literal: true

require 'rack'
require './app'

run Rack::URLMap.new(
  {
    '/home' => MyApp.new('/') # ここを /fuga などにすると /home/fuga にアクセスできる
  }
)

動作確認

  • bundle exec rackup config.ru

ツールとして使う

Web アプリとしてではなくルーティング内に定義したインスタンスメソッドを別のツールとして使うこともできます

  • vim export.rb
# frozen_string_literal: true

require './app'

app = MyApp.new!('/')
puts app.to_h

ポイントは new! を使って強制的にインスタンス化して使います
rackup を挟んでいないので通常のクラスとして使えます

メリット

ルーティングをクラスとして定義することで複数のルーティングを定義した場合に共通の処理を親クラスに抽象化することができます
Mix-in なども使えます (そもそも Sinatra にはミドルウェアという機能があるのでそれと同じように使える)

逆に同じようなページを生成するのであればすでにあるルーティング用のクラスを使って同じページを生成できます

またルーティングに必要な情報 (パスやリクエストパラメータ、テンプレートに与える情報、ヘッダ、認証情報) などをクラス内で管理することができるようになるのでコードの可読性も上がります

デメリット

ルーティングを追加するのにわざわざクラスを一つ追加する必要があります

またせっかく DSL で簡単にアプリを定義できるのにクラス化するためのコードを増えるので記述量は多くなります
Rails っぽくなってしまうというのもあるかなと思います

最後に

使用するかどうかは開発するプロダクトの規模の大きさにもよるかもしれません

これにデータベースが絡んで来る場合などはむしろ Sinatra ではなく Rails を使ったほうがいいまであります