>>> bool([]) False
> Boolean([]) true
どっちが良いかと言われると難しいなあ。
どちらにせよ、配列そのものを真偽値チェックに使うのはバグの温床になるから止めたほうがいいね。lengthをチェックするとか変数定義をチェックするとかにした方がいい。
>>> bool([]) False
> Boolean([]) true
どっちが良いかと言われると難しいなあ。
どちらにせよ、配列そのものを真偽値チェックに使うのはバグの温床になるから止めたほうがいいね。lengthをチェックするとか変数定義をチェックするとかにした方がいい。
axios(+ axios-retry)だとタイムアウトとリトライの処理があるけど、fetchには何もないらしい。まじか。
手続き型言語なら普通にタイマーでキャンセル処理を登録してリトライはforループとかで、、
for(1 .. retry_num){ if (timer(fetch, timeout) == 0){ break } }
とこんな感じのコードで済むんだけど、javascriptは何かと面倒だな。
色々調べてこんな感じになった。
function fetch_retry(url, params={}) { const {retry=5, timeout_ms=1000, options={}} = params; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout_ms); options["signal"] = controller.signal; return fetch(url, options) .catch((error) => { if (retry === 1) throw error; return fetch_retry(url, {retry : retry-1, timeout_ms : timeout_ms, options : options}); }) .finally(() => clearTimeout(timer)); }
こんな感じで使いたい。
fetch_retry(url) fetch_retry(url, {timeout_ms:10000})
fetchを中断させるには AbortController().abort() を使うらしい。
おそらくこれを実行するとfetchのoptionに指定したsignalが送られてコネクションを閉じるんだと思う。
Promise.race()を使って、指定した時間でrejectを実行するPromiseと同時に動かすという方法もあるようだけど、これだと処理的にはタイムアウトするけどコネクションはそのままになってしまう。
まあ放っておけばkeep-aliveでcloseするだろうから(keepalive有効になってないことなんてないよね?)大きな問題ではなさそうだけど
fetch自体に備わってるabort処理を使う方が素直な実装でしょう。Promise.race()も使わずにすむし。
で、前のエントリーに合わせてStreams対応させれば。。もうちょっとだ。
参考:
var ts = function(){ dt = new Date(); year = dt.getFullYear(); month = (dt.getMonth()+1).toString().padStart(2, '0'); day = dt.getDate().toString().padStart(2, '0'); hour = dt.getHours().toString().padStart(2, '0'); min = dt.getMinutes().toString().padStart(2, '0'); sec = dt.getSeconds().toString().padStart(2, '0'); ms = dt.getMilliseconds(); str = year + "-" + month + "-" + day + " " + hour + ":" + min + ":" + sec + "." + ms return str } var url = "<big data URL>" fetch(url) .then((response) => response.body.getReader()) .then((reader) => { var charsReceived = 0; function processText({ done, value }) { if (done) { console.log("Stream complete"); return; } charsReceived += value.length; console.log( ts() + " " + value.length + " bytes received. total size=" + charsReceived + " bytes"); return reader.read().then(processText); } return reader.read().then(processText); });
実行結果
2023-09-09 11:34:19.427 16384 bytes received. total size=16384 bytes 2023-09-09 11:34:19.519 32768 bytes received. total size=49152 bytes 2023-09-09 11:34:19.522 32274 bytes received. total size=81426 bytes 2023-09-09 11:34:19.614 65536 bytes received. total size=146962 bytes 2023-09-09 11:34:19.614 32491 bytes received. total size=179453 bytes 2023-09-09 11:34:19.618 16098 bytes received. total size=195551 bytes 2023-09-09 11:34:19.708 65536 bytes received. total size=261087 bytes 2023-09-09 11:34:19.709 36951 bytes received. total size=298038 bytes Stream complete
とりあえず動いた。
参考:
javascript慣れてないからお作法というか文化的なものが分からんなあ。。
async版。こっちの方が分かりやすいな。Promise分かんなすぎ。
(async () => { var charsReceived = 0; const response = await fetch(url); const reader = response.body.getReader(); while (true) { const {value, done} = await reader.read(); if (done) break; charsReceived += value.length; console.log( ts() + " " + value.length + " bytes received. total size=" + charsReceived + " bytes"); } console.log('Stream complete'); })()
あとちなみに、valueはバイト列(Uint8Array)として渡されるので、テキストとして扱う場合はTextDecoder()でデコードするか
TextDecoderStream()をTransformStreamとして指定すればよいらしい。
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
わぁーおしゃれ!
最初は一番下のreturn reader.read().then(processText)の中にprocessTextの定義を埋め込んでたんだけど、そうすると途中でPromiseが切れる(という言い方が適切なのかは分からない)というか
.thenで繋いだときに読み込みが終わる前に実行されてしまうので分けて定義しないといけないみたい。
こちらの記事の続き。
IMAPに比べてSMTPでOAuth 2.0の認証をするサンプルコードについてはほとんど情報がありませんでしたが、唯一こちらが参考になりました。
Gmail用のAuthen::SASLオブジェクトをAuthen::SASL::Perl::XOAUTH2として定義していて、これをそのまま使えば良さそう。
package Authen::SASL::Perl::XOAUTH2 ; use strict ; use warnings ; our $VERSION = "0.01c" ; our @ISA = qw( Authen::SASL::Perl ) ; my %secflags = ( ) ; sub _order { 1 } sub _secflags { shift ; scalar grep { $secflags{$_} } @_ ; } sub mechanism { # SMTP->auth may call mechanism again with arg $mechanisms # but that means something is not right if ( defined $_[1] ) { die "XOAUTH2 not supported by host\n" } ; return 'XOAUTH2' ; } ; my @tokens = qw( user auth access_token ) ; sub client_start { # Create authorization string: # "user=" {User} "^Aauth=Bearer " {Access Token} "^A^A" my $self = shift ; $self->{ error } = undef ; $self->{ need_step } = 0 ; return 'user=' . $self->_call( $tokens[0] ) . "\001auth=" . $self->_call( $tokens[1] ) . " " . $self->_call( $tokens[2] ) . "\001\001" ; } 1 ;
他に使う機会もないし、スクリプトにそのまま埋め込めばいいかと思っていたけど
これを使うNet::SMTPS(Net::SMTP)内部でreuireする処理があるので
実ファイルとして@INCのパスの通ったディレクトリに保存する必要がありました。
とりあえずシンプルな例としてNet::SMTPSから直接叩いてメールを送るサンプルです。
最もシンプルな例としてはAuthen::SASLオブジェクト経由ではなく
直接 Net::SMTPS->command()で認証コマンドを実行することになりますが、
勉強も兼ねて公開されているAuthen::SASL::Perl::XOAUTH2を活かす方向で行きます。
use strict; use warnings; use utf8; use Encode qw /encode/; use Net::SMTPS; use Authen::SASL qw/Perl/; use Email::MIME; my $USER_MAIL = 'kobayashi01234@gmail.com'; my $access_token = '[my access token]'; my $email = Email::MIME->create( header => [ From => $USER_MAIL, To => $USER_MAIL, Subject => 'test mail', ], attributes => { content_type => 'text/plain', charset => 'UTF-8', encoding => '8bit', }, body => encode('utf8', "テストメール"), ); my $msg_string = $email->as_string; my $sasl = Authen::SASL->new( mechanism => 'XOAUTH2', callback => { user => $USER_MAIL, auth => 'Bearer', access_token => $access_token, } ); my $smtp = Net::SMTPS->new( 'smtp.gmail.com', Port => 587, doSSL => 'starttls', Debug => 1 ); $smtp->auth($sasl) or die "Can't authenticate:" . $smtp->message(); $smtp->mail($USER_MAIL); $smtp->recipient($USER_MAIL); $smtp->data(); $smtp->datasend($msg_string); $smtp->dataend();
実行結果
Net::SMTPS=GLOB(0x80009c5f0)<<< 220 smtp.gmail.com ESMTP Net::SMTPS=GLOB(0x80009c5f0)>>> EHLO localhost.localdomain Net::SMTPS=GLOB(0x80009c5f0)<<< 250-smtp.gmail.com at your service, [39.111.129.226] Net::SMTPS=GLOB(0x80009c5f0)<<< 250-SIZE 35882577 Net::SMTPS=GLOB(0x80009c5f0)<<< 250-8BITMIME Net::SMTPS=GLOB(0x80009c5f0)<<< 250-STARTTLS Net::SMTPS=GLOB(0x80009c5f0)<<< 250-ENHANCEDSTATUSCODES Net::SMTPS=GLOB(0x80009c5f0)<<< 250-PIPELINING Net::SMTPS=GLOB(0x80009c5f0)<<< 250-CHUNKING Net::SMTPS=GLOB(0x80009c5f0)<<< 250 SMTPUTF8 Net::SMTPS=GLOB(0x80009c5f0)>>> STARTTLS Net::SMTPS=GLOB(0x80009c5f0)<<< 220 2.0.0 Ready to start TLS Net::SMTPS=GLOB(0x80009c5f0)>>> EHLO localhost.localdomain Net::SMTPS=GLOB(0x80009c5f0)<<< 250-smtp.gmail.com at your service, [39.111.129.226] Net::SMTPS=GLOB(0x80009c5f0)<<< 250-SIZE 35882577 Net::SMTPS=GLOB(0x80009c5f0)<<< 250-8BITMIME Net::SMTPS=GLOB(0x80009c5f0)<<< 250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH Net::SMTPS=GLOB(0x80009c5f0)<<< 250-ENHANCEDSTATUSCODES Net::SMTPS=GLOB(0x80009c5f0)<<< 250-PIPELINING Net::SMTPS=GLOB(0x80009c5f0)<<< 250-CHUNKING Net::SMTPS=GLOB(0x80009c5f0)<<< 250 SMTPUTF8 Net::SMTPS=GLOB(0x80009c5f0)>>> AUTH XOAUTH2 XXXXXXXXXX Net::SMTPS=GLOB(0x80009c5f0)<<< 235 2.7.0 Accepted Net::SMTPS=GLOB(0x80009c5f0)>>> MAIL FROM:<kobayashi01234@gmail.com> Net::SMTPS=GLOB(0x80009c5f0)<<< 250 2.1.0 OK Net::SMTPS=GLOB(0x80009c5f0)>>> RCPT TO:<kobayashi01234@gmail.com> Net::SMTPS=GLOB(0x80009c5f0)<<< 250 2.1.5 OK Net::SMTPS=GLOB(0x80009c5f0)>>> DATA Net::SMTPS=GLOB(0x80009c5f0)<<< 354 Go ahead Net::SMTPS=GLOB(0x80009c5f0)>>> From: kobayashi01234@gmail.com Net::SMTPS=GLOB(0x80009c5f0)>>> To: kobayashi01234@gmail.com Net::SMTPS=GLOB(0x80009c5f0)>>> Subject: test mail Net::SMTPS=GLOB(0x80009c5f0)>>> Date: Wed, 15 Jun 2022 16:35:50 +0900 Net::SMTPS=GLOB(0x80009c5f0)>>> MIME-Version: 1.0 Net::SMTPS=GLOB(0x80009c5f0)>>> Content-Type: text/plain; charset=UTF-8 Net::SMTPS=GLOB(0x80009c5f0)>>> Content-Transfer-Encoding: 8bit Net::SMTPS=GLOB(0x80009c5f0)>>> Net::SMTPS=GLOB(0x80009c5f0)>>> テストメール Net::SMTPS=GLOB(0x80009c5f0)>>> . Net::SMTPS=GLOB(0x80009c5f0)<<< 250 2.0.0 OK 1655278555 Net::SMTPS=GLOB(0x80009c5f0)>>> QUIT Net::SMTPS=GLOB(0x80009c5f0)<<< 221 2.0.0 closing connection
いい感じですね!
ようやく最終目標であるEmail::Senderから送る方法を考えます。
Net::SMTP(S)をそのまま使ってもいいですが、Email::Senderがいい感じにラップしてくれるので
モダンなPerlコードはこれを使うみたいです。
Email::Senderを使うにはGmailの認証に対応したEmail::Sender::Transportが必要になりますが
うまい具合に指定する方法が見つからなかったので、強引にEmail::Sender::Transport::SMTPを上書き(継承)した
Email::Sender::Transport::SMTP::Gmailクラスを作成します。
sendmail()の処理では_smtp_client()関数からsmtpオブジェクトの生成や認証を行うのですが、
コンストラクタで認証済みのNet::SMTPオブジェクトをセットし、それをそのまま返すようにしています。
package Email::Sender::Transport::SMTP::Gmail; use strict; use warnings; use base qw(Email::Sender::Transport::SMTP); sub new{ my $this = shift; my $class = ref $this || $this; return bless {_smtps_client => $_[0]}, $class; } sub _smtp_client{ return $_[0]->{_smtps_client}; } 1; package main; use strict; use warnings; use utf8; use Encode qw /encode/; use Net::SMTPS; use Authen::SASL qw/Perl/; use Email::MIME; use Email::Sender::Simple qw(sendmail); my $USER_MAIL = 'kobayashi01234@gmail.com'; my $access_token = '[my access token]'; my $email = Email::MIME->create( header => [ From => $USER_MAIL, To => $USER_MAIL, Subject => 'test mail', ], attributes => { content_type => 'text/plain', charset => 'UTF-8', encoding => '8bit', }, body => encode('utf8', "テストメール"), ); my $msg_string = $email->as_string; my $sasl = Authen::SASL->new( mechanism => 'XOAUTH2', callback => { user => $USER_MAIL, auth => 'Bearer', access_token => $access_token, } ); my $smtp = Net::SMTPS->new( 'smtp.gmail.com', Port => 587, doSSL => 'starttls', Debug => 1 ); $smtp->auth($sasl) or die "Can't authenticate: " . $smtp->message(); my $sender = Email::Sender::Transport::SMTP::Gmail->new($smtp); sendmail($email, {transport => $sender});
GmailのIMAPログインが失敗するようになってしまった。
Google アカウントが5月30日にセキュリティ強化、Gmailの外部メールアプリ利用などが使えなくなる可能性 - ケータイ Watch
これのことらしい。困りますね。
use strict; use warnings; use utf8; use IO::Socket::SSL; use Net::IMAP::Client; my $imap = Net::IMAP::Client->new( server => 'imap.gmail.com', port => 993, ssl => 1, user => '[user gmail address]', pass => '[password]', ); die "Could not connect to IMAP server" unless $imap; $imap->login or die('Login failed: ' . $imap->last_error);
$ perl gmail_test.pl Login failed: [AUTHENTICATIONFAILED] Invalid credentials (Failure) at gmail_test.pl line 17.
ユーザーID+パスワードではなく、OAuth2.0を使ってログインする必要があるそうです。
しかしOAuthとかすっかり忘れてしまった。
自分の過去記事でもまとめてるけど、何しろ10年くらい前の話(!)なので勉強し直しですな。。
大まかな手順としては以下のような感じ。
とりあえず公式ドキュメントのこの辺を読んでおけばよさそう。
動くPerlのコードサンプルとしてはこちらが参考になりました。
しかしやっぱりWeb周りに関してはPerlよりPythonの方が簡単だなあ。Googleが提供してるライブラリもPerlはないし干されている。
Google API Consoleを開いてプロジェクトを作成する。
色々と項目があって混乱するけど、実際やることはほとんどない。
Gmail APIを有効にする必要があるかな?と思ったけど特にAPIの有効化やscopeの設定は必要ない。
今回はAPIを使うわけではなく認証するだけ(実際の処理はIMAP/SMTPで行う)だからなのかな?
また、ScopeにGmail API(https://mail.google.com/)を指定するとpublishing statusをproductionにする際verificationが必要になる。verificationには色々手順が必要らしく面倒そう。
support.google.com
しかしpublishing statusがtestのままだと7日ごとにrefresh tokenの取り直し(OAuth2.0の再認証?)が必要になるのでやっぱり面倒そう。
ということで、scopeを空にしてpublishing statusをproductionにする。これだとverificationが不要なので楽だと思う。
ダウンロードしたclient_secret.jsonはこういう感じのデータになってるので、これをそのまま使います。
redirect_urisはlocalhostになっていますが、"urn:ietf:wg:oauth:2.0:oob"に書き換えておきます。
{ "installed": { "client_id": "XXXXXXXX", "project_id": "YYYYYYYY", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "ZZZZZZZZ", "redirect_uris": [ "localhost" ] } }
redirect URIの詳細な説明はこちらのドキュメントに載ってます。
というわけで、参考サイトのドキュメントやサンプルコードを眺めつつOAuth2.0の認証とアクセストークンのrefreshを実装してみました。
use strict; use warnings; use utf8; use Getopt::Std; use JSON::XS; use Path::Class qw/file/; use URI; use LWP::UserAgent; my $opt = {}; getopts("r:", $opt); my $client_secret = JSON::XS->new->utf8->decode(scalar file('client_secret.json')->slurp)->{installed}; if($opt->{r}){ refresh($client_secret, $opt->{r}); } else{ auth($client_secret); } sub auth{ my $client_secret = shift; my $auth_uri = URI->new($client_secret->{auth_uri}); $auth_uri->query_form( response_type => 'code', client_id => $client_secret->{client_id}, redirect_uri => $client_secret->{redirect_uris}->[0], scope => 'https://mail.google.com/', ); print "To authorize token, visit this url and follow the directions:\n$auth_uri\n"; print "Enter verification code: "; my $authorization_code = <>; $authorization_code =~ tr/\x0A\x0D//d; my $ua = LWP::UserAgent->new(); my $token_uri = URI->new($client_secret->{token_uri}); my $res = $ua->post($token_uri, { client_id => $client_secret->{client_id}, client_secret => $client_secret->{client_secret}, code => $authorization_code, redirect_uri => $client_secret->{redirect_uris}[0], grant_type => 'authorization_code', }); my $res_text = $res->decoded_content; my $res_json = JSON::XS->new->utf8->decode($res_text); print "\n"; print "Refresh Token: $res_json->{refresh_token}\n"; print "Access Token: $res_json->{access_token}\n"; print "Access Token Expiration Seconds: $res_json->{expires_in}\n"; } sub refresh{ my ($client_secret, $refresh_token) = @_; my $ua = LWP::UserAgent->new(); my $token_uri = URI->new($client_secret->{token_uri}); my $res = $ua->post($token_uri, { client_id => $client_secret->{client_id}, client_secret => $client_secret->{client_secret}, refresh_token => $refresh_token, grant_type => 'refresh_token', }); my $res_text = $res->decoded_content; my $res_json = JSON::XS->new->utf8->decode($res_text); print "\n"; print "Access Token: $res_json->{access_token}\n"; print "Access Token Expiration Seconds: $res_json->{expires_in}\n"; }
use strict; use warnings; use utf8; use MIME::Base64; use IO::Socket::SSL; use Net::IMAP::Client; use Data::Dumper; use Time::Out qw /timeout/; my $imap = Net::IMAP::Client->new( server => 'imap.gmail.com', port => 993, ssl => 1, timeout => 10, ); die "Could not connect to IMAP server" unless $imap; my $access_token = '[access token]'; my $user = '[user mail]'; my $auth_string = sprintf("user=%s\001auth=Bearer %s\001\001", $user, $access_token); my $capability = $imap->capability; my $ok; timeout 5 => sub { ($ok) = $imap->_tell_imap('AUTHENTICATE' => 'XOAUTH2 ' . encode_base64($auth_string, '')); }; if ($@){ die("AUTHENTICATE timed out"); } unless($ok){ die('Login failed: ' . $imap->last_error); } $imap->select('INBOX');
Net::IMAP::Client->_tell_imap()というのが生のIMAPコマンドを投げる内部関数らしいので、これを使ってOAuthの認証情報を送ります。
ちなみにオリジナルのNet::IMAP::Client->loginはこんな感じ。
sub login { my ($self, $user, $pass) = @_; $user ||= $self->{user}; $pass ||= $self->{pass}; $self->{user} = $user; $self->{pass} = $pass; _string_quote($user); _string_quote($pass); my ($ok) = $self->_tell_imap(LOGIN => "$user $pass"); return $ok; }
このAUTHENTICATEコマンドで間違ったアクセストークンを投げるとエラーを返すと思っていたけど、
実際は応答がなくなってしまう(ハングする)のでTime::Outを使って強制的にタイムアウトさせてる。
コンストラクタにtimeoutオプションがあるけど、これはTCP connectのときのタイムアウトっぽくてコマンドのタイムアウトはしなかった。
PycURL – A Python Interface To The cURL library — PycURL 7.45.1 documentation
PythonでHTTP関係の処理をするならRequestsなのですが、PycURLはlibcurlベースで高速だったり細かい処理ができたりして便利なこともあるので使ってみます。
残念ながらWindows用のバイナリは公式に存在しないそうです。かといってソースからbuildするのもなあ。
検索してみるとこちらのサイトでバイナリを配布してるようなので使ってみます。
Python Extension Packages for Windows - Christoph Gohlke
自分の環境はこれなので "pycurl‑7.45.1‑cp38‑cp38‑win_amd64.whl" をダウンロードします。
$ python -VV Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)]
$ pip install pycurl-7.45.1-cp38-cp38-win_amd64.whl
とりあえず動くサンプル。
from datetime import datetime, timedelta import pycurl from io import BytesIO def report(curl): print("Performance report:") print("-----------------------------------------------------------------------") print("EFFECTIVE_URL : {}".format(curl.getinfo(pycurl.EFFECTIVE_URL))) print("RESPONSE_CODE : {}".format(curl.getinfo(pycurl.RESPONSE_CODE))) print("SIZE_DOWNLOAD : {}".format(curl.getinfo(pycurl.SIZE_DOWNLOAD))) print("NAMELOOKUP_TIME : {}".format(curl.getinfo(pycurl.NAMELOOKUP_TIME))) print("CONNECT_TIME : {} {}".format(curl.getinfo(pycurl.CONNECT_TIME), curl.getinfo(pycurl.CONNECT_TIME)-curl.getinfo(pycurl.NAMELOOKUP_TIME))) # APPCONNECT : ssl_handshake_done print("APPCONNECT_TIME : {} {}".format(curl.getinfo(pycurl.APPCONNECT_TIME), curl.getinfo(pycurl.APPCONNECT_TIME)-curl.getinfo(pycurl.CONNECT_TIME))) # Time to HTTP GET done # https://curl.se/libcurl/c/CURLINFO_PRETRANSFER_TIME.html print("PRETRANSFER_TIME : {} {}".format(curl.getinfo(pycurl.PRETRANSFER_TIME), curl.getinfo(pycurl.PRETRANSFER_TIME)-curl.getinfo(pycurl.APPCONNECT_TIME))) # STARTTRANSFER : TTFB(time to first byte) print("STARTTRANSFER_TIME : {} {}".format(curl.getinfo(pycurl.STARTTRANSFER_TIME), curl.getinfo(pycurl.STARTTRANSFER_TIME)-curl.getinfo(pycurl.PRETRANSFER_TIME))) print("TOTAL_TIME : {} {}".format(curl.getinfo(pycurl.TOTAL_TIME), curl.getinfo(pycurl.TOTAL_TIME)-curl.getinfo(pycurl.STARTTRANSFER_TIME))) print("REDIRECT_TIME : {}".format(curl.getinfo(pycurl.REDIRECT_TIME))) print() def _curl_debug(type, data): # CURLINFO_TEXT = 0, # CURLINFO_HEADER_IN, /* 1 */ # CURLINFO_HEADER_OUT, /* 2 */ # CURLINFO_DATA_IN, /* 3 */ # CURLINFO_DATA_OUT, /* 4 */ # CURLINFO_SSL_DATA_IN, /* 5 */ # CURLINFO_SSL_DATA_OUT, /* 6 */ type_str = ('*', '<', '>', '{', '}', '<<', '>>') msg = None if type == 3 or type == 4: msg = "[{} bytes data]".format(len(data)) else: msg = data.decode('utf-8').strip() print("{} {} {}".format(datetime.now(), type_str[type], msg)) buffer = BytesIO() curl = pycurl.Curl() curl.setopt(pycurl.URL, 'http://pycurl.io/docs/latest/index.html') curl.setopt(pycurl.WRITEDATA, buffer) curl.setopt(pycurl.FOLLOWLOCATION, True) curl.setopt(pycurl.VERBOSE, True) curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug) curl.perform() print() report(curl) curl.close()
$ python a.py 2022-05-06 09:09:49.740667 * Trying 192.30.252.154:80... 2022-05-06 09:09:49.914677 * Connected to pycurl.io (192.30.252.154) port 80 (#0) 2022-05-06 09:09:49.914677 > GET /docs/latest/index.html HTTP/1.1 Host: pycurl.io User-Agent: PycURL/7.45.1 libcurl/7.80.0 Schannel zlib/1.2.11 zstd/1.5.2 c-ares/1.18.1 libssh2/1.10.0 Accept: */* 2022-05-06 09:09:50.093687 * Mark bundle as not supporting multiuse 2022-05-06 09:09:50.094688 < HTTP/1.1 200 OK 2022-05-06 09:09:50.094688 < Server: GitHub.com 2022-05-06 09:09:50.094688 < Date: Fri, 06 May 2022 00:09:49 GMT 2022-05-06 09:09:50.094688 < Content-Type: text/html; charset=utf-8 2022-05-06 09:09:50.094688 < Content-Length: 22758 2022-05-06 09:09:50.094688 < Vary: Accept-Encoding 2022-05-06 09:09:50.094688 < Last-Modified: Sun, 13 Mar 2022 07:25:32 GMT 2022-05-06 09:09:50.094688 < Vary: Accept-Encoding 2022-05-06 09:09:50.094688 < Access-Control-Allow-Origin: * 2022-05-06 09:09:50.094688 < ETag: "622d9c6c-58e6" 2022-05-06 09:09:50.094688 < expires: Fri, 06 May 2022 00:19:49 GMT 2022-05-06 09:09:50.094688 < Cache-Control: max-age=600 2022-05-06 09:09:50.094688 < Accept-Ranges: bytes 2022-05-06 09:09:50.094688 < x-proxy-cache: MISS 2022-05-06 09:09:50.094688 < X-GitHub-Request-Id: E3E7:3DA7:4CD618:744035:6274674D 2022-05-06 09:09:50.094688 < 2022-05-06 09:09:50.094688 { [984 bytes data] 2022-05-06 09:09:50.094688 { [12924 bytes data] 2022-05-06 09:09:50.268697 { [5744 bytes data] 2022-05-06 09:09:50.268697 { [3106 bytes data] 2022-05-06 09:09:50.268697 * Connection #0 to host pycurl.io left intact Performance report: ----------------------------------------------------------------------- EFFECTIVE_URL : http://pycurl.io/docs/latest/index.html RESPONSE_CODE : 200 SIZE_DOWNLOAD : 22758.0 NAMELOOKUP_TIME : 0.003169 CONNECT_TIME : 0.177756 0.174587 APPCONNECT_TIME : 0.0 -0.177756 PRETRANSFER_TIME : 0.17809 0.17809 STARTTRANSFER_TIME : 0.356996 0.17890599999999998 TOTAL_TIME : 0.531489 0.174493 REDIRECT_TIME : 0.0
いいね。
シンプルなkey=valueの形式なら何を使っても同じ結果になるけど、ネストしたデータ構造だと結果が違う。
元々はjQueryのparam()とPythonのurlencodeで結果が違うなーと思って(jQueryはPHPと同じ結果になる)調べてたけど、どっちでもサーバー側では問題なく解釈されるみたい。まあサーバーの実装にもよるだろうけど。。
import requests import urllib def flatten(dictionary, parent_key=None): items = [] for key, value in dictionary.items(): new_key = "{}[{}]".format(str(parent_key), key) if parent_key else key if isinstance(value, dict): items.extend(flatten(value, new_key).items()) elif isinstance(value, list) or isinstance(value, tuple): for k, v in enumerate(value): items.extend(flatten({str(k): v}, new_key).items()) else: items.append((new_key, value)) return dict(items) form_data = { "k1" : "v1", "k2" : { "k2_1" : "v2_1", "k2_2" : "v2_2", }, "k3" : ["v3_1", "v3_2", "v3_3", "v3_4"] } # urlencode print(urllib.parse.urlencode(form_data)) # http_build_query compatible print(urllib.parse.urlencode(flatten(form_data)))
$ python a.py | nkf --url-input k1=v1&k2={'k2_1':+'v2_1',+'k2_2':+'v2_2'}&k3=['v3_1',+'v3_2',+'v3_3',+'v3_4'] k1=v1&k2[k2_1]=v2_1&k2[k2_2]=v2_2&k3[0]=v3_1&k3[1]=v3_2&k3[2]=v3_3&k3[3]=v3_4
jQueryでは
decodeURI($.param(form_data)) "k1=v1&k2[k2_1]=v2_1&k2[k2_2]=v2_2&k3[]=v3_1&k3[]=v3_2&k3[]=v3_3&k3[]=v3_4"
jQueryのparam()は配列のインデックスを入れないの?
PHP環境がないので生のhttp_build_query()がどうなってるのか分からないけど、
とりあえず今回は配列データを使わないので気にしないことにした。