ターミナルにiTerm2を使っています。
iTerm2ではターミナル起動時の初期ディレクトリを設定できます。よく使うディレクトリがある場合は設定しておくと便利です。
設定方法
Settingsを開きます。

Profilesを開きます。デフォルトはHome directoryになっています。

Directoryを選択してよく使うディレクトリを設定します。これでOK

Macでペアワイズ法サポートツールのPICTを使ってみます。 github.com
PICTはWindows環境用のツールであり、Macで実行できるようにするのは面倒そうという先入観がありました。
が、今日ではbrewで提供されているので簡単にインストールできました。
brew install pict
以下のようなインプット用のファイルを作成します。
input.txt
インド料理: バターチキン, サンバル, マサラドーサ ネパール料理: チョエラ, スクティ, モモ スリランカ料理: パリップ, ポルサンボル, ワタラッパン
使い方的にはこれでpict input.txtを実行すればいいはずなのですが、生成結果が意図しないものになってしまいました。
調べたところ、「日本語が含まれる際は文字コードをEUCにしないと化ける」という情報がありました。
従って、以下のようにUTF-8をEUC-JPに変換して渡すことで解決しました。ChatGPTに教えてもらったのですが、「プロセス置換」という方法でワンライナーで書けるようです。
pict <(iconv -f UTF-8 -t EUC-JP input.txt)
※ 文字コード問題は2台のMacの内の片方でしか発生せず、発生条件が謎なままです。(もう一台はUTF-8のまま問題なく読み込めた)
以下のようにパターンを生成できました!
インド料理 ネパール料理 スリランカ料理 マサラドーサ スクティ ポルサンボル サンバル スクティ パリップ マサラドーサ モモ ワタラッパン サンバル チョエラ ワタラッパン サンバル モモ ポルサンボル バターチキン スクティ ワタラッパン マサラドーサ チョエラ パリップ バターチキン チョエラ ポルサンボル バターチキン モモ パリップ
Ruby3.1からハッシュとキーワード引数の省略記法が使えます。
仕事でコードレビューをしていて「これは何だ?」と思ってしまったので確認しておきたいと思います。
事前にc = 3のようにローカル変数を定義している場合、{ c: }と書くだけで{c: 3}と同じ意味になります。
mapping1 = {
a: 1,
b: 2
}
pp mapping1 # {a: 1, b: 2}
c = 3
mapping2 = {
c:,
d: 4
}
pp mapping2 # {c: 3, d: 4}
事前にname = "田中"のようにキーワード引数と同じ名前のローカル変数を定義している場合、hello(name:)と書くだけでhello(name: name)と同じ意味になります。
def hello(name:) puts "こんにちは#{name}さん!" end hello(name: "山田") # こんにちは山田さん! name = "田中" hello(name:) # こんにちは田中さん!
『WEB+DB PRESS Vol.127』を読むとこの省略記法が導入された背景もわかって理解が深まります。
Rubyの特徴であるオープンクラスの勉強のため、Stringクラスに絵文字変換メソッドを追加してみます。
例えば、"カレー".to_emojiを実行すると🍛がreturnされるイメージです。
以下のように簡単に実装できました。
class String def to_emoji case self when "カレー" "🍛" when "ラーメン" "🍜" when "寿司" "🍣" else self end end end
Stringインスタンス自身を参照するためにはselfが使えます。
# ※ class定義に続けて実行 puts "カレー".to_emoji # 🍛 puts "ラーメン".to_emoji # 🍜 puts "寿司".to_emoji # 🍣 puts "牛丼".to_emoji # 牛丼 ※ 対応する絵文字が定義されていないため、「牛丼」がそのまま返される puts "curry".upcase # CURRY ※ 既存のRubyのStringクラスの挙動が破壊されていないことの確認
Rubyでは、フィールド名+=というメソッド名を定義することで、インスタンス変数への代入のような表現ができます。
以下のサンプルコードでいうと、def name=(name)がそれにあたります。
class Person def initialize(name) @name = name end def name=(name) @name = name end def introduce puts "私の名前は#{@name}です" end end p = Person.new("山田") p.introduce # 私の名前は山田です p.name = "田中" p.introduce # 私の名前は田中です
このような書き方が可能なのは、次のどちらの理由によるものなのか気になったので調べてみました。
答えは2.でした。Rubyのパーサーには、末尾が=で終わるメソッド専用の処理ロジックが組み込まれていました。
最終的にはRubyのパーサーのコードを確認して裏付けを取りましたが、挙動の観察だけでも2.の可能性が高いと推測できます。
p.name=("田中")の場合メソッド名がname=なので、当然ながら次のようにも書けます。
# 省略 p.name=("田中") p.introduce # 私の名前は田中です
p.name= "田中"の場合Rubyではメソッド呼び出しのかっこを省略できるため、次のように書いても動作します。
# 省略 p.name= "田中" p.introduce # 私の名前は田中です
ここまでは、1.と2.のどちらの仮説でも説明がつきます。
p.name = "田中"の場合しかし、p.nameと=の間にスペースが入るこの書き方も動作します。
# 省略 p.name = "田中" p.introduce # 私の名前は田中です
つまり、Rubyはp.name =をp.name=として解釈していることになります。
Rubyには「メソッド名の途中にスペースが入っても自動で補完する」といった仕様は存在しないため、この挙動は通常のメソッド呼び出しルールでは説明できません。
たとえば、name=メソッドの末尾を?に書き換えて同様のことを試してみると、syntax error, unexpected tIDENTIFIER, expecting ':'というエラーになります。
class Person # 省略 def name?(name) @name = name end # 省略 end # 省略 p.name ? "田中" p.introduce # 私の名前は田中です
このことから、Rubyには末尾が=で終わるメソッドには特別な構文ルールが存在すると推測できます。
裏を取るためにChatGPTにナビゲーションしてもらいながら、Rubyのパーサーのソースコードを読んでみました。
ChatGPTによると、Ruby本体のリポジトリに同封されているパーサーよりもRuby3.4からデフォルトになったPrismパーサーのソースコードを読んだほうがいいそうです。
src/prism.c内のparse_write関数を見ると、以下のような実装が確認できます。
parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_node_t *value) { // 省略 switch (PM_NODE_TYPE(target)) { // 省略 case PM_CALL_NODE: { // 省略 if (char_is_identifier_start(parser, call->message_loc.start, parser->end - call->message_loc.start)) { // When we get here, we have a method call, because it was // previously marked as a method call but now we have an =. This // looks like: // // foo.bar = 1 // // When it was parsed in the prefix position, foo.bar was seen as a // method call with no arguments. Now we have an =, so we know it's // a method call with an argument. In this case we will create the // arguments node, parse the argument, and add it to the list. pm_arguments_node_t *arguments = pm_arguments_node_create(parser); call->arguments = arguments; pm_arguments_node_arguments_append(arguments, value); call->base.location.end = arguments->base.location.end; parse_write_name(parser, &call->name); pm_node_flag_set((pm_node_t *) call, PM_CALL_NODE_FLAGS_ATTRIBUTE_WRITE | pm_implicit_array_write_flags(value, PM_CALL_NODE_FLAGS_IMPLICIT_ARRAY)); return (pm_node_t *) call; } } // 省略 } // 省略 } }
私はパーサーのコードを読み慣れてない & C言語に疎いのでロジックの詳細は分かりませんが、コードコメントからこのブロックが末尾が=で終わるメソッド専用のロジックであることが伺えます。
さらに、parse_writeがPM_CALL_NODEのブロックで呼び出しているparse_write_nameを読むと、メソッド名の末尾に=を追加する処理をやっていそうです。
parse_write_name(pm_parser_t *parser, pm_constant_id_t *name_field) { // The method name needs to change. If we previously had // foo, we now need foo=. In this case we'll allocate a new // owned string, copy the previous method name in, and // append an =. pm_constant_t *constant = pm_constant_pool_id_to_constant(&parser->constant_pool, *name_field); size_t length = constant->length; uint8_t *name = xcalloc(length + 1, sizeof(uint8_t)); if (name == NULL) return; memcpy(name, constant->start, length); name[length] = '='; // Now switch the name to the new string. // This silences clang analyzer warning about leak of memory pointed by `name`. // NOLINTNEXTLINE(clang-analyzer-*) *name_field = pm_constant_pool_insert_owned(&parser->constant_pool, name, length + 1); }
以上より、Rubyのパーサーは「foo.bar = 1のような式を見つけたら、bar=というメソッド呼び出しとして扱う」という専用処理を持っていることが確認できました。
django-auditlogというjazzbandの管轄のDjangoのモデル操作の監査ログを作成してくれるライブラリがあります。
基本的な使い方は以下の記事でryu22eさんが紹介してくださっているので割愛します。
モデルを監視してCRUD操作を検知する思想なので、モデル単位でのログ抑制は設定で可能です。
ただ今回はユーザー種別の場合にログを抑制したいというニーズがあり、やり方が分からなかったので技術検証してみます。
前提としてググってもdjango-auditlogの発展的な使い方に関する情報はほとんど見つかりませんでした。
目ぼしいものは8年前のものですが以下のStack Overflowの記事くらいでした。この記事も前提となるDjangoのバージョンが1.8かつdjango-auditlogも2023年に3系にメジャーバージョンアップしているのでそのまま使える情報ではなさそうです。
ChatGPTに聞いてみたところ、以下のような回答でした。
①カスタム Middleware を定義する
from auditlog.middleware import AuditlogMiddleware class CustomAuditlogMiddleware(AuditlogMiddleware): def get_user(self, request): user = super().get_user(request) # 例: User モデルに user_type フィールドがある場合 if getattr(user, 'user_type', None) in ['system', 'batch']: return None # ログに記録しないようにする return user
②settings.pyで Middleware を差し替える
MIDDLEWARE = [
# 'auditlog.middleware.AuditlogMiddleware', ← 削除
'your_app.middleware.CustomAuditlogMiddleware', # ← 追加
# その他のミドルウェア
]
AuditlogMiddlewareの_get_actorのオーバーライドを試すChatGPTの回答の裏取りで最新のAuditlogMiddlewareのソースコードをみてみたところ、get_userというメソッドはありませんでしたが、恐らく_get_actorのことを指しているのだろうと読み替えました。
以下のように_get_actorをオーバーライドして、元の実装に追加でuser.is_staff = Trueの場合はNoneを返すようにしてみました。
from auditlog.middleware import AuditlogMiddleware from django.contrib.auth import get_user_model class CustomAuditlogMiddleware(AuditlogMiddleware): @staticmethod def _get_actor(request): user = getattr(request, "user", None) if not isinstance(user, get_user_model()): return None if not user.is_authenticated: return None if user.is_staff: return None return None
USERがsystemとなっただけでログは作成されてしまい、期待通りの挙動にはなりませんでした。

_get_actorがNoneを返した場合、USERがsystemになるだけでログ自体は作成される仕様のようです。
AuditlogMiddlewareの__call__のオーバーライドを試す①AuditlogMiddlewareをオーバーライドする方針はそのままで、__call__の方を以下のようにオーバーライドしてみました。
from auditlog.middleware import AuditlogMiddleware from auditlog.cid import set_cid from auditlog.context import set_actor class CustomAuditlogMiddleware(AuditlogMiddleware): def __call__(self, request): remote_addr = self._get_remote_addr(request) remote_port = self._get_remote_port(request) user = self._get_actor(request) if user and user.is_staff: # 追加: ユーザーがスタッフの場合は監査ログを抑制する return self.get_response(request) set_cid(request) with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port): return self.get_response(request)
これでもUSERがsystemとなっただけでログは作成されてしまい、期待通りの挙動にはなりませんでした。

set_actorはあくまでログに操作したユーザーの情報を付与するだけの役割であって、with set_actorを実行しないようにするだけではログは抑制できないようです。
https://django-auditlog.readthedocs.io/en/latest/usage.html#set-actor
AuditlogMiddlewareの__call__のオーバーライドを試す②公式ドキュメントをみていたらset_actorと同様にwithブロックで使えるdisable_auditlogという関数を発見しました。
https://django-auditlog.readthedocs.io/en/latest/usage.html#disable-auditlog
__call__の中でこれを実行するようにしてみます。
from auditlog.middleware import AuditlogMiddleware from auditlog.cid import set_cid from auditlog.context import set_actor, disable_auditlog # disable_auditlogのインポートを追加 class CustomAuditlogMiddleware(AuditlogMiddleware): def __call__(self, request): remote_addr = self._get_remote_addr(request) remote_port = self._get_remote_port(request) user = self._get_actor(request) if user and user.is_staff: # ユーザーがスタッフの場合は監査ログを抑制する with disable_auditlog(): return self.get_response(request) set_cid(request) with set_actor(actor=user, remote_addr=remote_addr, remote_port=remote_port): return self.get_response(request)
この方法でuser.is_staff = Trueの場合にログを抑制することに成功しました。
スギ花粉シーズンに花粉が飛ばない旅行先を調べると小笠原諸島、沖縄、北海道など簡単には行けないところばかり出てくる。
例外として草津が紹介されている記事を見かけたので理由を調べたところ、スギ花粉は標高1000m以上ではあまり飛ばないらしい。草津は標高1200m以上なのでスギ花粉が飛ばないという理由だった。
ただ、標高1000m以上の旅行先というのは意外と他に探すのが難しい。例えばパッと思いつく標高が高そうな温泉街を調べたところ
とのことだった。
ちょうどDeep Researchが話題になっているので、草津以外の標高の観点で花粉の避粉地としておすすめの旅行先を探してもらうのがお題としていいのでは?と考えた。
まずは以下を入力してみた。
スギ花粉シーズンに、標高1,200mの草津のように標高1000m以上で花粉が飛ばない旅行先を調べてください。花粉シーズンの旅行先を調べると、草津以外だと北海道や離島や国外を紹介する記事ばかりで、「そこまではいけないな」と困っています。草津以外にもスギ花粉が飛ばないくらい標高が高い旅行先はあるのではないかと思っています。とはいえ、標高が高そうなイメージの温泉地帯を調べると、 - 箱根: 約100m - 下呂温泉: 300m - 野沢温泉: 600m と、意外と1000m以上という条件を満たす旅行先を見つけるのも難しいなと感じています。
すると、以下のようにより詳細な要件を求められた。
草津以外で標高1,000m以上に位置し、スギ花粉が少ない旅行先をお探しですね。 より的確な候補を見つけるために、以下の点を教えてください。 1.エリアの希望:関東圏、甲信越、関西など、特定の地域の希望はありますか? 2.目的:温泉地を中心に探しますか?それとも景観や観光を楽しめる場所ならOKですか? 3.宿泊施設の有無:宿泊できる施設がある場所がよいですか? 4.アクセス:車が必要な場所でも大丈夫ですか?公共交通機関で行けるほうがよいですか? この情報があれば、より適した候補地をリストアップできます!
以下のように回答した。
1. 関東を中心に青森から近畿地方までくらいならOKです。関東に近いほどうれしいです。 2. 温泉地を中心に探します 3. 宿泊したいです 4. 公共交通機関で行けるほうがよいです
体感10分〜20分くらい調べたのち、リストアップしてくれた旅先は以下だった。
たしかに裏をとってみると、全て標高1000m以上という条件を満たした温泉街であり、最後の青森を除くと関東からアクセスしやすい場所を選んでくれている。
「他にもありますか?」と聞くと、追加で以下を挙げてくれた。
全部長野県だったので「長野県以外もありますか?」と聞くと以下を挙げてくれた。
たしかに全部実際に裏をとってみると実在する温泉街で標高も1000m以上だった。すごい。
Deep Researchは一定期間に10回までしか使えないという制約があるようだが、初回の調査結果に対する追加質問の際は回数は減らないっぽい。