Perl入学式 オンライン 2020 第2回お疲れ様でした
受講された方、サポーターの方、お疲れ様でした。 講師をやったジャージの人です。
講義に利用したスライド・動画類は以下 Perl 入学式の公式サイトで公開しています。参加された方も、参加できなかった方も、ぜひ復習に使ってください。
また、復習問題を用意しています。
四則演算、文字列連結の問題にチャレンジしてみてください。私の回答例もこちらおいておきます。
問題の意味がわからない、とか、このような解答例はどうだろう?という方は Discord のPerl入学式チャンネル(招待コード)や、twitterでハッシュタグ #Perl入学式 をつけて聞いてみてください。
Perl入学式 オンライン 2020 第2回
今回は「四則演算」「文字列連結」「コマンドライン引数」を学習しました。次回は条件分岐、IF 文からとなります。
IF 文は変数に続くプログラミング独特、プログラミングらしい学習内容です。おたのしみに!
もちろん、待ち切れない方は資料やスライドツールから先に学習を進めていただいて大丈夫です。その上で、不明な点があれば Discord や twitter で聞いてください。
コマンドライン引数
昨年までの Perl 入学式 ではコマンドライン引数ではなく、標準入力を学習していました。
標準入力はコマンドライン引数と同様に、プログラムの外部から入力を与え、プログラムの挙動や表示を変える方法の一つです。
「コマンドライン引数」と「標準入力」、何が違うかというと、入力を行うタイミングです。
コマンドライン引数:実行前にあらかじめ引数という形で入力を与えておく。
標準入力:プログラムの実行中に、入力する。対話型のプログラムを作ることができる。
標準入力に興味のある方は昨年度のカリキュラムを見てみてください。
なお、Wandbox で標準入力の問題を実行してみる場合、対話型のプログラムとはならず、コマンドライン引数と同じような形であらかじめ入力しておく、という形になります。
標準入力の入力欄はコマンドライン引数のものとは異なります。
コードを書いているエディタ欄の下に Stdin というリンクがあります。このリンクをクリックして開くところにあらかじめ入力しておきます
・・・あらかじめ?
となると、Wandbox 上ではコマンドライン引数と同じ様な感じだよなぁ、ってことで、Wandbox を使った学習ではコマンドライン引数に変更しました。
以下に Wandbox で標準入力を使った場合のサンプルを置いておきます。
コマンドライン引数の実例
自作のツールでコマンドライン引数を使っています。
最初の引数に銀行名、次の引数に支店名を入力すると、その条件に該当する銀行コードと支店コードを表示してくれます。
もし、コマンドライン引数がないと、検索する都度、コードの一部を書き換えてから実行という手順になります。
それは面倒ですよね。
こういう時、コマンドライン引数でちゃちゃっと条件を変えて実行結果を変えていけると、とても楽です。
第3回の開催について
次回は12月を予定しています。年末にかからないところ、初旬から中旬での開催を予定しています。
次回の参加、お待ちしております!
Perl で Google Drive API をつかってフォルダーを作成する
はい、慣れてきました。
API の URL と mimeType 以外はそのまんまです。
my $GOOGLE_DRIVE_API = "https://www.googleapis.com/upload/drive/v3/files?create"; # (中略) `mimeType => 'application/vnd.google-apps.folder',`
To create a folder, use the files.create method with the application/vnd.google-apps.folder MIME type and a title.
(DeepL翻訳)フォルダを作成するには、files.create メソッドを使用して、application/vnd.google-apps.folder MIME タイプとタイトルを指定します。
#!/usr/bin/env perl use strict; use warnings; binmode STDOUT, ":utf8"; use HTTP::Request::Common; use JSON qw/encode_json/; use LWP::UserAgent; my $GOOGLE_DRIVE_API = "https://www.googleapis.com/upload/drive/v3/files?create"; my $ACCESS_TOKEN = ""; my $bearer = join ' ', ('Bearer', $ACCESS_TOKEN); my $ua = LWP::UserAgent->new; my $res = $ua->request( POST $GOOGLE_DRIVE_API, 'Content-Type' => 'multipart/form-data', Authorization => $bearer, Content => [ metadata => [ undef, undef, 'Content-Type' => 'application/json;charset=UTF-8', 'Content' => encode_json( { name => 'create_folder_test', mimeType => 'application/vnd.google-apps.folder', parents => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'], }, ), ], ], ); print $res->code . "\n"; print $res->content . "\n"; # 200 # { # "kind": "drive#file", # "id": "1cJ1jQOQ9KAqxVPJwIWNhGuXH1BBA9k2M", # "name": "create_folder_test", # "mimeType": "application/vnd.google-apps.folder" # }
Perl で Google Drive API をつかって特定のファイルを更新する
特定のファイルを更新する
ちょっと前、ファイルを Google Drive にアップロードした時は、同名のファイルがある場合でもアップロードが可能でした。
・・・可能でした、っていうか完全に不意打ちでしたが・・・
しかし、ファイルを更新したい時はどうすれば良いのでしょう?ってわけで、やりました。人生で初めて PATCH メソッド使いました。
URI のパラメータにファイル ID を利用していますが、だいたいはアップロードの時と同じ感じですね。
$target_fileid
で指定したファイルが更新されます。metadata の値を変えることで、ファイル名やmimeType を更新することも可能です。
#!/usr/bin/env perl use strict; use warnings; use HTTP::Request::Common; use JSON qw/encode_json/; use LWP::UserAgent; use URI::QueryParam; use URI; my $target_fileid = '1dZU7-4d52U2pRWsmMLhMEuh8Cpa0m8sI'; my $GOOGLE_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3/files/"; my $ACCESS_TOKEN = ""; my $bearer = join ' ', ( 'Bearer', $ACCESS_TOKEN ); my $URI = URI->new( $GOOGLE_DRIVE_UPLOAD_API . $target_fileid ); $URI->query_param( uploadType => 'multipart' ); my $ua = LWP::UserAgent->new; my $res = $ua->request( PATCH $URI, 'Content-Type' => 'multipart/form-data', Authorization => $bearer, Content => [ metadata => [ undef, undef, 'Content-Type' => 'application/json;charset=UTF-8', 'Content' => encode_json( { # name => 'hogefuga.txt', # mimeType => 'plain/text', # parents => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'], # id => $target_fileid, }, ), ], file => ["./hoge.txt"], ], ); print $res->code . "\n"; print $res->content . "\n"; # 200 # { # "kind": "drive#file", # "id": "19f2RrocH4I3Mdig0LkmNPghJDZnmq35f", # "name": "hoge.txt", # "mimeType": "plain/text" # }
Perl で Google Drive API をつかって特定フォルダ配下のファイル一覧を取得する
特定のフォルダ配下にあるファイル一覧を表示する
こちらは GET アクセスなので簡単だろう〜・・・とナメてかかって、割と引っかかりました。
url でパラメータを組み立てる時に 2 回 URL エンコードしてしまったというもの。
Google Drive API v3 で特定のフォルダ配下にあるファイルを引っ掛けるってのやってて、フォルダの中に一つしかファイルがないのに複数引っかかって???ってなってた。$uri->query_param( 'q' => "'$folder_id' in parents" );
— sironekotoro (@sironekotoro) 2020年11月11日ほんで、ググって見つけた C# のコード参考にしてみたら検索結果が1つだけと望んだ結果になった。$uri->query_param( 'q' => "'$folder_id' in parents and trashed = false" );
— sironekotoro (@sironekotoro) 2020年11月11日
ゴミ箱の中にあるファイルも、元どこのフォルダにあったかを記録しており、検索結果で明示的に除外する必要があると
つまり、「特定のフォルダの中にある」って条件だけじゃなくて「ゴミ箱の中にはない」とかちゃんと指定しようねというお話。まぁ、確かにね。
$folder_id
の中にあるファイル・フォルダの ID が表示されます。
#!/usr/bin/env perl use strict; use warnings; use HTTP::Tiny; use URI::QueryParam; use URI; my $ACCESS_TOKEN = ''; my $folder_id = '10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'; my $uri = URI->new('https://www.googleapis.com/drive/v3/files'); $uri->query_param( 'q' => "'$folder_id' in parents and trashed = false" ); my $bearer = join ' ', ( 'Bearer', $ACCESS_TOKEN ); my $ht = HTTP::Tiny->new( default_headers => { Authorization => $bearer } ); my $res = $ht->get($uri); print $res->{content}; # { # "kind": "drive#fileList", # "incompleteSearch": false, # "files": [ # { # "kind": "drive#file", # "id": "1dZU7-4d52U2pRWsmMLhMEuh8Cpa0m8sI", # "name": "hogefuga.txt", # "mimeType": "text/plain" # } # ] # }
query_param で組み立てている検索条件を名前での検索にする時はこんな感じ。クォーテーションの位置とかでわりと試行錯誤しました。
$uri->query_param( 'q' => "name = 'hogefuga.txt'" );
他の検索条件を使いたい人はこちら
Perl で Google Drive API のアクセストークンを更新する
今日も Perl から Google Drive API v3 をやっていきます。
リフレッシュトークンを使ってアクセストークンを更新する
Google の発行したアクセストークンは 3600 秒、つまり 1 時間で失効します。
失効する都度、アクセストークンを発行しても良いのですが(うちもそうしてた)、さすがに面倒・・・になってきました。
そこで、アクセストークンと一緒に発行されるリフレッシュトークンを利用してアクセストークンを更新します。
その際には以下のものが必要です。
それらを POST で Google のAPI になげると、新しいアクセストークンが返ってきます。
これでまだ戦えますね!
注: ACCESS_TOKEN なくても更新できたので、コード更新しております
#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use HTTP::Tiny; use JSON; use URI; my $CLIENT_ID = ""; my $CLIENT_SECRET = ""; my $REFRESH_TOKEN = ""; my $URI = URI->new('https://oauth2.googleapis.com/token'); my $ht = HTTP::Tiny->new(); my $response = $ht->request( 'POST', $URI, { content => encode_json( { client_id => $CLIENT_ID, client_secret => $CLIENT_SECRET, grant_type => 'refresh_token', refresh_token => $REFRESH_TOKEN, } ) } ); my $json = decode_json($response->{content}); print Dumper $json; # "access_token": "hogehogefoofoobarbar", # "expires_in": 3599, # "scope": "https://www.googleapis.com/auth/drive", # "token_type": "Bearer"
Perl で Google Drive API を使ってファイルをアップロードする
Google Drive API を使ってファイルをアップロードしたい!
これが超苦労しましたん・・・
先に blog で書いたとおり、自分の環境(と技能)では、Google Drive を操作するモジュールを使うことができませんでした。
しかし!
Google Drive API は REST API なので、特別モジュールを使わずともいけるはずです。
We have a REST API. You can use any language to implement your own client, you don't have to use one of the client libraries we support.
(DeepLで翻訳)私たちはREST APIを持っています。独自のクライアントを実装するために任意の言語を使用することができます。
実際、一つ前のエントリに書いたファイルリストの取得であれば、https://www.googleapis.com/drive/v3/files?access_token=アクセストークン
をブラウザの URL 欄に貼り付けてアクセスすれば表示が可能です。
ってことで、API を生で扱うツールとして curl に目をつけ、利用事例を探しました。そこから Perl の http クライアントに実装していこうという狙いです。
curl を使って Google Drive にファイルをアップロードする
curl というのは、macos や linux 、そして Windows10 にも入っている HTTP クライアントです。
Windows10 でも使えるのはついさっき知りました・・・
ターミナルやコマンドプロンプトから curl https://www.yahoo.co.jp
とかやると応答が返ってくるはずです。
curl での実装を探して見つけたサイトが以下です。割と自然な日本語のタイトルなんですが、日本語に自動翻訳されたサイトみたいです。
ここのサンプルを用いて Google Drive の任意のフォルダにファイルをアップロードすることができました。
curl -X POST \ -H "Authorization: Bearer アクセストークン" \ -F "metadata={ \ name : 'hoge.txt', \ mimeType : 'plain/text', \ parents: ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'] \ };type=application/json;charset=UTF-8" \ -F "file=@hoge.txt;type=plain/text" \ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
ここでハマったのは、parents: ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'] \
の [ ]
は削除するものと思い、結果、いつまでも Google Drive の指定したフォルダの中に入らずに困ったという。
いや、先のサンプルで -H "Authorization: Bearer [ACCESS-TOKEN]" \
とあるんだけど、ここは実際には [ ]
なしで入力するので、parents のところも [ ]
いらないと思ったんですよね・・・
結局、公式のリファレンスでここにはリストを渡す、って記載を発見してやっと気づいたという。公式はいつも大事(1回目)
parents[] list
Perl を使って Google Drive にファイルをアップロードする
curl でできるなら、Perl でもできる!あとは移植していくだけや!と意気揚々といつもの use HTTP::Tiny
をタイプしますが、ここで手が止まります。
Perl を使ってファイルアップロード?
HTTP::Tiny
でファイルのアップロードどうやるんだ?
GET, POST 共に使ったことありますが、はて、ファイルアップロードはしたことない・・・
ググります。
Not easily. HTTP::Tiny contains no support for the multipart/form-data content type required by file uploads. (That's one of the reasons it's called "Tiny".)
(DeepLで翻訳)簡単にはできません。HTTP::Tiny は、ファイルのアップロードに必要な multipart/form-data コンテンツタイプをサポートしていません。(これが "Tiny" と呼ばれる理由の 1 つです)。
いや、curl でアクセスするときに multipart
とか書いてあるんやけど、それ無いのん・・・この時点で心が折れかかるんですが、大丈夫、我々には Furl
, LWP::Simple
, LWP::UserAgent
がある!
そして、どうやら POST するときは HTTP::Request::Common
でリクエストメッセージを作るのが良い、という事を知ります。
ただし "multipart/form-data" を送る場合は、post() メソッドを使ってリクエストを行うよりも、HTTP::Request::Common オブジェクトを直接扱って LWP::UserAgent へ request() させた方がいいかも知れない。boundary だのファイル名だの若干複雑なので……。
これは多分、先の Stack overflow で回答者が最後に書いていた
Correctly populating $multipart_form_data is left as an exercise for the reader. :-)
(DeepLで翻訳)multipart_form_dataを正しく入力することは、読者のための練習として残されています。)
ここを解決するのが、この HTTP::Request::Common
なのかなぁ?と思っています。
煮詰まって助けてもらう
なんとか、LWP::UserAgent
での実装にたどり着いたものの、動かない。
煮詰まったので、 Perl入学式の公開チャンネルに質問したのでした。
そこに上げた、動かないコードがこちら。
use HTTP::Request::Common; use LWP::UserAgent; my $ua = LWP::UserAgent->new; my $res = $ua->request( POST 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', Authorization => 'Bearer アクセストークン', Content_Type => 'multipart/form-data', Content => [ metadata => encode_json( { name => 'hoge.txt', mimeType => 'plain/text', parents => ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'], } ), file => ["./hoge.txt"], ] ); say $res->code; # 400 say $res->content; # { # "error": { # "errors": [ # { # "domain": "global", # "reason": "badContent", # "message": "Unsupported content with type: application/octet-stream" # } # ], # "code": 400, # "message": "Unsupported content with type: application/octet-stream" # } # }
ベテランの Perl Monger 達に助けてもらいました。本当にありがとうございます。
id:anatofuz , id:karupanerura, id:shoichikaji
動かなかった原因
これは curl と LWP::UserAgent の Dump をとって分かったのですが、metadata 部のリクエストがうまく組み立てられていませんでした。あと typo も。
ちなみに、LWP の Dumper はこんな感じで入れられます。(slack で教えてもらった)
$ua->add_handler("request_send", sub { shift->dump; return }); $ua->add_handler("response_done", sub { shift->dump; return });
以下は LWP::UserAgent の Dump の抜粋です。
--xYzZY\r Content-Disposition: form-data; name="metadata"\r \r {"mimeType":"plain/text","name":"hoge.txt","parents":["10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr"]}\r --xYzZY\r Content-Disposition: form-data; name="file"; filename="plain/text"\r Content-Type: text/plain\r \r hoge \r
metadata のところに Content-Type:
がありません。json のデータを送る旨明示しなければならないのですが、それがありません。
対して、file のところには Content-Type: text/plain\r
があります。
つまり、metadata のところのデータ構造の構築に失敗していました。
で、どうやってこの metadata のところに curl と同じ application/json;charset=UTF-8
を入れるんだ?となりました。
どう書けば良いかはちゃんと HTTP::Request::Common 公式にありました。公式大事(2回目)
複数の値を持つフォームフィールドは、フィールド名を繰り返すか、 配列リファレンスを渡すことで指定できます。
POST メソッドはRFC1867 で示された Form-based File Upload のために使われる multipart/form-data コンテントもサポートします。 リクエストヘッダの一つとして 'form-data' のコンテントタイプを 指定することにより、このコンテントフォーマットを利用することが出来ます。 もし $form_ref の中の値の1つが配列リファレンスであれば、それは以下の解釈で ファイル部分の指定であるように扱われます:
[ $file, $filename, Header => Value... ]
[ undef, $filename, Header => Value,..., Content => $content ]
配列での先頭の値 ($file) はオープンするファイルの名前です。 このファイルは読みこまれ、その内容がリクエストに入れられます。 もしファイルをオープンできなければルーチンは croak します。 コンテントを直接 Content ヘッダで指定したければ $file の値を undef に してください。 $filename はリクエストで報告されるファイル名です。 この値が未定義であれば、$file の基本名が使われます。 $file の値を提供したとき、ファイル名の送信をよくせいしたいなら、 $filename に空文字列を指定することができます。
なるほど。
動かないコードだと metadata に対する値は一つだけでした。
[ $file, $filename, Header => Value... ]
の $file
しか入っていない状態です。
しかも、モジュールは file
が来ることを想定してるのに、渡しているのは(JSON化した)文字列です。なるほど動かん。
metadata => encode_json( { name => 'hoge.txt', mimeType => 'plain/text', parents => ['1PSb3xH000llDtXSWLaAtKEXJy5DY_Wic'], }
これを直すとこうです。
[ undef, $filename, Header => Value,..., Content => $content ]
の形式にするコンテントを直接 Content ヘッダで指定したければ $file の値を undef に してください
に従うJSONの文字列は Content キーの値に移動
適用した配列リファレンスを metadata の値に渡しています。
metadata => [ undef, # ファイル使わないので undef undef, # ファイルからデータを取得しないので undef 'Content-Type' => 'application/json;charset=UTF-8', 'Content' => encode_json( # jsonのデータは Content キーの下に { name => 'hoge.txt', mimeType => 'plain/text', parents => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'], }, ), ],
Perl で Google Drive API を使ってファイルをアップロードする(タイトル回収)
ということで、やっと Perl で Google Drive にファイルをあげることができました。
#!/usr/bin/env perl use strict; use warnings; binmode STDOUT, ":utf8"; use HTTP::Request::Common; use JSON qw/encode_json/; use LWP::UserAgent; my $GOOGLE_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"; my $ua = LWP::UserAgent->new; my $res = $ua->request( POST $GOOGLE_DRIVE_UPLOAD_API, 'Content-Type' => 'multipart/form-data', Authorization => 'Bearer アクセストークン', Content => [ metadata => [ undef, undef, 'Content-Type' => 'application/json;charset=UTF-8', 'Content' => encode_json( { name => 'hoge.txt', mimeType => 'plain/text', parents => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'], }, ), ], file => ["./hoge.txt"], ], ); print $res->code . "\n"; print $res->content . "\n"; # 200 # { # "kind": "drive#file", # "id": "19f2RrocH4I3Mdig0LkmNPghJDZnmq35f", # "name": "hoge.txt", # "mimeType": "plain/text" # }
おまけ HTTP::Tiny::Multipart
むやみやたらとググる中、コアモジュール民(自分)が大好きな HTTP::Tiny の拡張がありました。HTTP::Tiny::Multipart です。
これを使って Google Drive へのアップロードを書いてみました。こちらも動きました。
#!/usr/bin/env perl use strict; use warnings; binmode STDOUT, ":utf8"; use File::Slurp qw/read_file/; use HTTP::Tiny; use HTTP::Tiny::Multipart; use JSON; my $GOOGLE_DRIVE_UPLOAD_API = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"; my $ACCESS_TOKEN = ""; my $bearer = join " ", ( 'Bearer', $ACCESS_TOKEN ); my $content = read_file('./hoge.txt'); my $http = HTTP::Tiny->new( default_headers => { Authorization => $bearer } ); my $response = $http->post_multipart( $GOOGLE_DRIVE_UPLOAD_API, { metadata => { content => encode_json( { name => 'hoge.txt', mimeType => 'plain/text', parents => ['10kCqEUmWsWlqMdP_vF9pDGrQXFVZ-Lvr'], }, ), content_type => 'application/json; charset=utf-8', }, file => { filename => './hoge.txt', content => $content, content_type => 'plain/text', }, } ); print $response->{content} . "\n"; # { # "kind": "drive#file", # "id": "1UInkENiv0GApyfhJWyxgNaplw512Mmw2", # "name": "hoge.txt", # "mimeType": "plain/text" # }
後始末
Perl で Google Drive API を使ってファイル一覧を取得する
さて、Google Cloud Platform で利用する ACCESS TOKEN と REFRESH TOKEN を手にしたからには、次の段階へ向かわねばなりません。
そもそも、なんか OAuth いじるの楽しくなって色々やってしまいましたが、これは目的と手段が逆転するいつものパターンです。
楽しいんですよねー
Google Drive に上がっているファイル一覧を取得する
GET でパラメータを並べていくだけなので、簡単にできました。
注意するところとしては、一回の応答で取得できるのは100件のみ。100件以上ファイルがある場合には応答に nextPageToken
が含まれるので、次の API 組み立ての際に pageToken
を加えてアクセスって感じです。
これで連続取得ができます。
大量にファイルがあると返ってこないので、5 回取得したら(つまり 500件)ループを抜けるようにしてます。
#!/usr/bin/env perl use strict; use warnings; binmode STDOUT, ":utf8"; use HTTP::Tiny; use JSON; use URI::Escape; use URI; my $GOOGLE_DRIVE_API = "https://www.googleapis.com/drive/v3/files"; my $ACCESS_TOKEN = ""; my $count_limit = 5; # 全てのファイルを取得する my $uri = URI->new($GOOGLE_DRIVE_API); $uri->query_form( access_token => $ACCESS_TOKEN ); files($uri); sub files { my $uri = shift; my $count = 0; my $ht = HTTP::Tiny->new(); while ( $count < $count_limit ) { my $contents = decode_json( $ht->get($uri)->{content} ); $uri->query_form( access_token => $ACCESS_TOKEN, pageToken => $contents->{nextPageToken}, ); for my $content ( @{ $contents->{files} } ) { print "=" x 20 . "\n"; printf( "%-8s: %s\n", "id", $content->{id} ); printf( "%-8s: %s\n", "name", $content->{name} ); printf( "%-8s: %s\n", "mimeType", $content->{mimeType} ); printf( "%-8s: %s\n", "kind", $content->{kind} ); print "=" x 20 . "\n"; } $count++; last if !$contents->{nextPageToken}; # 最終ページには nextPageToken キーが無い } }
==================== id : 13uf_8fJph3J3raPee0Sg5rsgb-w5MIUBLVdhDhD8xSE name : チェックリスト mimeType: application/vnd.google-apps.document kind : drive#file ==================== ==================== id : 1f9ZuDCOSGUujhSSXvv_kruzI7qbkKq9FQXf2YuNn_Io name : メモ mimeType: application/vnd.google-apps.document kind : drive#file ==================== ...