Marketo で動的なメールを書くときの話です。 以下の条件での話です
このとき、変数の値を https://example.com/
とするとトラッキングが効きません(URLは正常に機能します)。
トラッキングを有効にするには、 変数の値は example.com/
としておいて、メールを書いてリンクを設定する際に https://{{ lead.URLのフィールド }}
とする必要があります。
こんにちは、最近 aiohttp をすごく使っています。 Web-DBな処理はDjangoで実装して、非同期処理が必要なサーバーやクライアントをaiohttpで書くという住み分けをしています (今までNode.jsを使っていたところをaiohttpで実装しています)。
平たく言うとaiohttpはかなり最高なので、今日はそのWebSocketクライアントの実装をソースコードリーディングしていきましょう。
(なぜ読むかと言うと、Dockerのexec start APIがWebSocketのようでそうでない変則的な仕様になっていて、それに対応するためにWebSocketクライアントの実装を読んでいました。 exec start wsのようなAPIを用意してくれると良いのですが)
aiohttp
の ClientSession
にある .ws_connect()
メソッドから実装を紐解いていきます。
まずはこのメソッドのドキュメントを読んでおきましょう。
https://aiohttp.readthedocs.io/en/stable/client_quickstart.html#websockets
今回は細かい仕様には焦点を当てずに、大まかな実装がどのようになっているかを見ていきましょう。
ClientSession.ws_connect
の実装はここにあります
(_ws_connect
というメソッドに処理があるのでこれを見ていきます)。
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L662
まずは必要になるヘッダーの処理が書かれています。
必須になる Connection: Upgrade
や Upgrade: Websocket
などが設定されます。
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L686-L711
ヘッダーの値はここにあります。
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/hdrs.py#L89-L90
次に、対象のURLにリクエストを送信する処理があります。
ここで Connection: Upgrade
のリクエストを送って、WebSocket通信を開始します。
# send request resp = await self.request(method, url, headers=real_headers, read_until_eof=False, auth=auth, proxy=proxy, proxy_auth=proxy_auth, ssl=ssl, proxy_headers=proxy_headers)
Upgradeのリクエストは read_until_eof=False
オプションを指定して、レスポンスを読み切るまで待たないようにしています。
レスポンスが返ってきた後は、ステータスコードが 101
かや、ヘッダーがWebSocketであるかどうかを検証します。
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h
その後はWebSocket通信のキーの計算などの処理が入りますが、ここは割愛します。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h
ここで、今後WebSocketで通信できるように先程のレスポンスからコネクションを取り出します。
conn = resp.connection assert conn is not None proto = conn.protocol assert proto is not None transport = conn.transport assert transport is not None
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L714-L721h
細かいことを抜きにしてしまえば、この transport
の .write()
で書きこめばUpgrade後の通信を通してサーバーに書き込みができます。
ですが、書き込み時にヘッダーをつけたり、圧縮したり、読み込んだ通信結果を解釈して WSMessage
クラスでラップしたり、ping/pongをやりとりしたりする処理が必要になります。
そのために reader
と writer
を設定します。
reader = FlowControlDataQueue( proto, limit=2 ** 16, loop=self._loop) # type: FlowControlDataQueue[WSMessage] # noqa proto.set_parser(WebSocketReader(reader, max_msg_size), reader) writer = WebSocketWriter( proto, transport, use_mask=True, compress=compress, notakeover=notakeover)
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L797-L802
FlowControlDataQueue
は読み込んだデータを貯めておくキューで、実際に読み込んだ通信を解釈しているのは WebSocketReader
というクラスです。
また、書き込みを行う WebSocketWriter
も作成されています。
readerとwriterの詳細は後述します。 変則的にWebSocketのプロトコルを少し変えたい場合などはこの reader/writer を差し替えれば良いわけですが、そのフックポイントはありません
あとはこの reader と writer をまとめた ClientWebSocketResponse
というインスタンスで返せば ws_connect
の処理は終了になります
(self._ws_response_class
で返していますが、これは ClientSession
のコンストラクタで受け取る ws_response_class
引数です。デフォルトで ClientWebSocketResponse
です)。
return self._ws_response_class(reader,
writer,
protocol,
resp,
timeout,
autoclose,
autoping,
self._loop,
receive_timeout=receive_timeout,
heartbeat=heartbeat,
compress=compress,
client_notakeover=notakeover)
https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client.py#L807-L818
ClientWebSocketResponse
にまとめることで、ライブラリーのユーザーからはreader/writerを分けて考えずに使えるようになっています。また、コネクションをクローズする処理などまとめられています。
aiohttpのClientWebSocketResponseの実装はこちらです。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/client_ws.py#L28
平たく言うと receive
メソッドでは reader.read()
を、 send_str
メソッドでは writer.send()
を呼び出すためクラスです。他にも例外を処理したり、 ping/pong
を勝手に処理してくれたり使いやすくするためのクラスでもあります。
WebSocketReaderの実装はここにあります。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/http_websocket.py#L241
このreaderは feed_data
と feed_eof
を実装しています。
ws_connect
で proto.set_parser
することで、このメソッドに通信されてきた文字を渡すことができます。
役割としてはWebSocketの通信 (メッセージなのか、ping/pongなのか) を解釈して、 WSMessage
クラスにラップした上でキューに書き込みます。
このキューというのは ClientWebSocketResponse
の _reader
に設定されている FlowControlDataQueue
です。
WebSocketWriterの実装はここにあります。 https://github.com/aio-libs/aiohttp/blob/v3.5.4/aiohttp/http_websocket.py#L544
writerは send
や close
、 ping
, pong
を実装しています。 ClientWebSocketReader
の _writer
に設定されているので、 self._writer.send
のように呼ばれます。
WebSocketWriter
は書き込まれた内容を self.transport.write
を使ってサーバーに送信します。
writerはメッセージの圧縮や、ヘッダーを足す処理をしてtransportに書き込みます。
ざっと処理の流れを確認するとこのようになっています。 細かいことは割愛しましたが、ますUpgradeのリクエストを送って、そのあとはその通信をそのまま利用してWebSocketのやり取りをするというのがよく分かりました。
「食べ物を粗末にしてはいけない」と言われても、それを疑う人はいないと思います。 幼少期から全日本人が教えられていることだと思います。強制力の違いはあれど、誰しも教えられたことだと思います。 ですが、粗末にしてはいけないのは食べ物だけでしょうか?何か、この常識には偏りを感じています。
食べ物を粗末にしてはいけないのは、もちろん僕も同意です。命をいただく気持ちが大事だと思います。 ですが、なぜ食べ物についてだけなんでしょうか。俗に言う「積読」なんかも、すごく「粗末」にしていると言えるのではないでしょうか。 例えばその本を印刷するための木材や、運搬するためのガソリン、人件費、販売にかかるコストや書籍サイトの運営費などを粗末にしてると言えます。
「食べ物は命をいただいているが、他はそうじゃないからだ」と言い切れるでしょうか? 本や衣類も、木材や綿、動物の革という直接的な物質や命をいただいています。何らかのサービスについても他人の人生をいただいています。その他人の人生には、その人の食べているお肉や野菜も含まれていると僕は思います。
ちなみに僕は気にせず積読をしています。でも誰もそれで僕を怒らないのは、それが僕の本だからです。なぜ食べ物についてはとやかく言われたり、言ったりするのでしょう。
考えを広げれば、人の好意を無下にしたり、話をちゃんと聞かなかったり、機会を無駄にしたり、大切な時間を真剣に取り組まなかったり、無駄にしているものはたくさんあります。 それも同じことと考えられます。他人の人生や、その人の食べた命を間接的にいただいているものだと思います。雑に扱われがちですが、サービスを受け取るのも命を受け取ることだと思います。
「食べ物を粗末にしてはいけない」「残してはいけない」、それは確かにそうだと思います。 でも、それと同じようにサービスや親切心、それ以外のものについても大切にしないといけないんじゃないでしょうか。そして十分な対価を払うべきではないでしょうか。 同時に、そこまで強迫的に「食べものを残してはいけない」と思わなくて良いんじゃないでしょうか。 それは単なる呪いだと思います。もちろん大切にいただくべきものですが、それ以外にも粗末にしてるものやサービスを見直しても良いんじゃないでしょうか。
雑談です。
AIは牛乳を注ぐ女の夢を見るのか? 牛乳を注ぐ女というのはフェルメールのあの絵のことです。
今の世の中はAI、ディープラーニングの話題がホットです。 ですが今言われるAIはとどのつまり機械学習で、僕が学生のときは「弱いAI」と呼ばれてたように思います。 「AI」なんて言おうものなら鼻で笑われていたので、あえて「集合知」のような言い方をしていたような覚えもあります。
機械学習は十二分にすごいものですが、結局のところは人間にはなれていないなと思います。 人間は、何かを判断するときにも日々の風景や、過去の習慣や常識、昨日食べたステーキの味も参考にして判断しています。 ステーキの味と株価の予測が関係するかは定かではありませんが、その店の中で見た若者の多さとか、店の値段設定から外食産業への投資判断を行ったりはありえます。 要するに、機械学習には感性というか、全く関係がないことを結びつけて考える能力がないわけです (AIさんがランチに出かける日が来るまでは)。
ですが逆に機械学習が今後ももっと発達すると考えると、人類は「強いAI」を求めなくなると思います。 「弱いAI」的な判断や考え方が中心になって、人間的な判断や会話が二の次と考えられるようになると思います。
そういう時代が来るだろうからこそ僕は人間的な感性や、数値に出ないデータが大事だと思います。 なぜなら、人間というのは結局のとこ人間を相手にするものだからです。 どれだけ機械学習や最適化をしても、それは限られた軸の中での世界の話です。 人間の感性を相手に世界が回っている以上、究極は人間の感性にしかできないことがあると思います (少なくとも人間の感性や時代背景、共感性をすべて学習したAIさんが登場するまでは)。
人間の感性的な部分、RX-8に乗って最高のドライブフィーリングを味わう感覚や、アンドロイドは電気羊の夢を見るか?のワクワクや、PH5の光やタリアセンライトにうっとりする感覚や、牛乳を注ぐ女をみて感じるものや、ジムノペディを聴いたりWelcome To The Jungleを聴いて感嘆することが大事なんだと思います。その喜びを知ってるからこそ全く関係ないように思える日々の仕事が素晴らしいものになり、人類にとっての価値が産まれると僕は思っています (機械学習があったとして当時ジムノペディは絶対に作れないと思います)。
ほんとに、フェルメール展に行くのを忘れてたのを後悔してます。 それが言いたかっただけです。
DjangoでCookieを設定するときは response.set_cookie
を使います。
response = TemplateResponse(...) response.set_cookie("key", "value", max_age=3600)
今まで知らずに expires
も自分で計算して設定していましたが、 max_age
を指定して expires
を指定しない場合は勝手に設定されるので不要みたいです。
DjangoのHttpResponseBaseのset_cookieにこういう実装がありました(Django 2.1で確認)。
if max_age is not None: self.cookies[key]['max-age'] = max_age # IE requires expires, so set it if hasn't been already. if not expires: self.cookies[key]['expires'] = http_date(time.time() + max_age)
django/response.py at 2.1.7 · django/django · GitHub
なので、わざわざこういう関数を作る必要はないです。
from datetime import timedelta from django.utils import timezone def set_cookie(response, key, value, max_age): expires_at = timezone.now() + timedelta(seconds=max_age) expires = datetime.strftime(expires_at, "%a, %d-%b-%Y %H:%M:%S GMT") response.set_cookie(key, value, max_age=max_age, expires=expires)
ちゃんと実装を調べてよかった。
よくプログラミング初心者の方に「PythonとRubyの違いを教えてください」と聞かれるので書いておきます。
あくまで僕がプログラミング初心者さんに説明するときにこの説明を使うよという視点です。読んでる方が思う視点とか別の意見はぜひご自身のブログに書いてください(読みますので)。 違いはたくさんありますが、5分で簡潔に伝わる違いを伝えたいと思っています。 あと私はPythonをずっと10年近く使っていますが、Rubyはちょっとだけです。でもRubyもとても良い言語だと思っています。
(コードサンプルの色味が読みにくくてごめんなさい。気が向いたら直しときます)
プログラミング初心者に簡単にPythonとRubyの違いを説明する場合ベストは「Pythonはより明示します。Rubyはより略せます」だと思っています (あくまでプログラミング初心者に5分以内に伝えると考えたときの視点ですよ)。
細かな違いはあれど、個人的には言語としてPythonとRubyではそれほど大きな違いはないと思います (少なくともHaskelとPythonや、ErlangとPythonのような違いはないですよね)。
もちろん今回は言語の話です。コミュニティの色や存在しているフレームワーク、ライブラリーの違いは大きくあると思います。 機械学習やデータ処理をしたいなぁ、しかもWebでも同じ言語を使いたいなぁというのであればPythonをオススメします。 でもそういう場合は言語仕様関係なく選びますよね。
言語として「PythonとRubyの違いが知りたい」と言うプログラミング初心者の人に分かりやすく違いを説明するのであれば、やはり「明示するか略すか」が良いと思います。
言うよりも見るということで、フィボナッチ数列の計算をキャッシュありで行うプログラムを2つ書きました。
Pythonの例:
fib = {} fib[1] = 1 fib[2] = 1 def fibonacci(num): if num in fib: return fib[num] fib[num] = fibonacci(num-1) + fibonacci(num-2) return fib[num] print(fibonacci(100))
Rubyの例:
@fib = {} @fib[1] = 1 @fib[2] = 1 def fibonacci(num) @fib[num] ||= fibonacci(num-1) + fibonacci(num-2) end p fibonacci(100)
この2つの例はかなりPythonらしさとRubyらしさを表していると思います。 (もちろんRubyでもPythonのような書き方はできますが、Rubyで書くのであればこの書き方のほうが美しいと思います)
この違いを産んでいるのはこの辺の違いからでしょうか:
return
の略nil
の扱いRubyで ||=
でうまく書けると気持ちいい。
Pythonは愚直ですが、何やっているかはパット見で分かりやすいなぁと感じます。
他の例として、Pythonのほうがより明示的に書く必要があるという説明をします。
Rubyでは def メソッド名=(...)
や @属性名
と記号で書けるところを、Pythonでは明示的に self.
などを書く必要があります。
説明用のプログラムでは、PythonとRubyで Account
クラスを作っています。
Account
は fullname
というゲッターとセッターを持っており、 fullname
という属性に値をそのまま保存します。
Pythonの例:
class Account: @property def fullname(self): return self.fullname @fullname.setter def set_fullname(self, fullname): self.fullname = fullname user = Account() user.fullname = "Hiroki Kiyohara" print(user.fullname)
Rubyの例:
class Account def fullname @fullname end def fullname=(fullname) @fullname = fullname end end user = Account.new() user.fullname = "Hiroki Kiyohara" p user.fullname
この例は極端に分かりやすい例なので、「見た目Rubyのほうが良いじゃん」と思われるかもしれません。 ですが、Pythonはより「明示的に」書く必要があり、記号や略記が少ないのが特徴です。
Pythonは美しいと僕も思いますが、結構、言語開発者とか深い仕様レベルでの美しさだなぁと思います。実にパースしやすそうです。 パッと見た感じはRubyのほうが小奇麗だと思います。Rubyの言語の深い部分は知らないです。
Slackにて話してると
shimizukawa [11:41 AM] Rubyは言語仕様が美しくなるように言語設計している Pythonは文法のパースがシンプルになるように言語設計している
わかり哲也。
僕はそういう意味で、Pythonが好きです。
あつおさんがPEP572でフィボナッチ数列を作って遊んでいたので、僕も遊んでみた。
Python 3.8.0a1時点の話です。
はじめてのPEP 572書いた
— Atsuo Ishimoto (@atsuoishimoto) February 5, 2019
>>> [((p1+p2), (p3:=p1), (p1:=p2), (p2:=p3+p2))[0] for x in (range(2, 10), (p1:=1), (p2:=1))[0]]
[2, 3, 5, 8, 13, 21, 34, 55]
ちょっと改善
— Atsuo Ishimoto (@atsuoishimoto) February 5, 2019
>>> [((p3:=p1), (p1 + (p1:=p2)), (p2:=p3+p2))[1] for x in (range(2, 10), (p1:=1), (p2:=1))[0]]
[2, 3, 5, 8, 13, 21, 34, 55]
これでいいのか
— Atsuo Ishimoto (@atsuoishimoto) February 5, 2019
>>> [(((p3:=p1) + (p1:=p2)), (p2:=p3+p2))[0] for x in (range(2, 10), (p1:=1), (p2:=1))[0]]
[2, 3, 5, 8, 13, 21, 34, 55]
#PEP572Fib
— Hiroki KIYOHARA (@hirokiky) February 5, 2019
>>> [(p3 := (p1 + p2)) and (p1 := p2) and (p2 := p3) for _ in (p1 := (p2 := 1)) and range(10)]
[2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
どや [((p3:=p1) + (p1:=p2))+0*(p2:=p3+p2) for x in (range(1+(p1:=1)+1, 10+(p2:=1)))]
— Atsuo Ishimoto (@atsuoishimoto) February 5, 2019
[(p2 := (p1 + (p1:=p2))) for x in (range(1+(p1:=1)+1, 10+(p2:=1)))]
— Hiroki KIYOHARA (@hirokiky) February 5, 2019
我ながら [(p2 := (p1 + (p1:=p2))) for _ in ...]
はよく出来てるなと思った。
あと、PEP572のnamed assignmentは使ってみると意外と制限があるので、思ったよりも良いなと思った。
ただこの動きは直感的にはビックリした。
>>> a = 1 >>> b = 2 >>> a + (a := b) 3 >>> a 2 >>> b 2
あつおさんのブログも見てね