sironekotoroの日記

Perl で楽をしたい

Perl入学式 オンライン 2021 第2回お疲れ様でした

Perl入学式 オンライン 2021 第2回

受講された方、サポーターの方、お疲れ様でした。 講師の人です。

今回は条件分岐の if 文に繰り返しの for 文をやりました。そして、それを合わせて利用する fizzbuzz をやりました。

また、複数の値を格納する配列を学びました。

今回のスライドの内容です。

github.com

講義中に解いた練習問題の回答例です。

github.com

講義スライドや動画を見て、不明点がある場合には Discord で質問してみてください。第 0 回の環境構築や第 1 回、前年度の内容などについてでも大丈夫です。

サポーター陣一同お待ちしております

discord.com

fizzbuzzExcel

今回、というか自分が講義を受け持つときは毎回、fizzbuzz と自分の出会いのエピソードを語っていました。

それは、Excel でやってみようとしてできなかった、というものです。

今回もそれを説明しつつ、そろそろこの過去からの宿題を終わらせるべき・・・と思ったのでした。

Excel で剰余

Excel で剰余を求めるときは =mod(数値, 割る数) を利用します。

Excel で if 文

if 文もあるのですが、fizz, buzz, fizzbuzz と条件が複数あるので、 ifs を利用します。

=IFS(論理式1, 値が真の場合1, 論理式2, 値が真の場合2 ... )というものです。

また、いずれの条件にも合致しないときには対象の数字を表示するのですが、それを実現するために =IFERROR(値, エラーの時の値) で囲みます。

Excel で AND

これはそのまま AND(論理式1, 論理式2) を使います。

完成

できたのがこの関数です。

=IFERROR( IFS( AND( MOD($A2,3)=0,( MOD($A2,5)=0) ), "fizzbuzz", MOD($A2,3)=0, "fizz", MOD($A2,5)=0, "buzz"), $A2 )

f:id:sironekotoro:20210620221947p:plain

長く未提出だった宿題を終えることができました。

でもまぁ、あの時「え、オレってこの簡単そうに見える fizzbuzz も解けないの!?」という衝撃あればこそ、 Perl をしっかり学ぶことができたので塞翁が馬ってやつですね。

ほんと fizzbuzz に感謝です。

あと、過去できなかったことや分からなかったことをブログに書いておくと、過去からの宿題という形でネタができるので、ブログ書いておくのおすすめです。

オンライン第 2 回

7 月以降を予定しています。次回は以下を予定してます。

  • 配列を操作する関数
  • ハッシュ
  • ハッシュを操作する関数
  • サブルーチン

開催日は現在 Discord でアンケート中です。

discord.com

KOKUYO のワーキングチェアにヘッドレストを追加した

全国で 4 人くらいにしか刺さらない記事

4 人というのは適当ですが、まぁ、そんなに多くはないだろうけど・・・

リモートワークの頻度が増えたので、昨年末より部屋を快適な環境にすべく色々と買いました。

その中でもワーキングチェアの話になります。

CRS-G2800E6 コクヨ/KOKUYO 肘付ハイバックチェア 301175

購入したのはこの KOKUYO 製の中古のワーキングチェアです。

モデレートタイプという、ヘッドレストのないタイプ。

cata.kokuyo.com

背もたれ倒してフラットにするすことはないよなー(フラグ 1 )、後ろに服をかけるところあると便利かもねー(フラグ 2 )ってことでこの椅子にしました。

あと、当時セール中だったというのも大きい。

https://www.officebusters.com/items/301175?rname=replywww.officebusters.com

商品代金合計:26,600円
送料:3,420円

概ね満足したのですが、ただ一つ・・・背もたれをガッツリと倒したい!(フラグ 1 回収)

KOKUYO さんに聞いてみる

いや、背もたれを倒すことないよなーって思ってたのは本当です。

会社でも倒すことないですし。

なのですが、実際に倒してみると、とても楽なんですよね。

・・・頭の支えがあれば。

ヘッドレストがないので、フルフラットに近い状態にすると頭が浮くわけです。

そこで、メーカーのコクヨヘッドレストのみを注文可能か聞いてみました。

ベゼルチェアのヘッドレストですが、
ファンクショナルタイプには、後付けで取り付けが可能です。
モデレートタイプにも取り付けは可能ですが、
取り付け高さが低くなり頭の位置が合わないため、
ヘッドレストの取り付けは推奨できません。
(中略)
ベゼル用ヘッドレストの品番をご案内します。
品番:CRB-G2800E6

うちが使っているのはモデレートタイプ。推奨されないのか・・・ううーん。

なお、注文は代理店で承るとのこと。

送料無料高級本革ヘッドレスト 6色対応

で、注文すべきかしないべきかを悩んでいたところ、このハンガー部分を活かせないか?と思い至りました(フラグ 2 回収)。

f:id:sironekotoro:20210606213439p:plain

メジャーでサイズを測り、このソファ用のヘッドレストであればうまくハマりそう・・・ということで注文しました。

f:id:sironekotoro:20210819161617j:plain

送料無料高級本革ヘッドレスト 6色対応

当時はセール中で 3300 円でした。

これが見事にハマり、まるで正規のオプション品のような使い心地(個人の感想です)

背もたれを倒しても首が支えられて最高!素晴らしい!これで午後ローを横になりながら見ることができる!となったのでした。

これも結構色々検索して迷ったりしたので、ここに書き残しておきます。

f:id:sironekotoro:20210606230158j:plain

Perl を使って画像にマスクをかける

Perl を使って画像にマスクをかける

Perl で画像を扱うライブラリといえば代表的なのが以下の 3 つですが、どれも使ったことないです。

ということで、今回は Imager でやってみます。

metacpan.org

そして、わからないー!ってなっている時に見つけた Perl.org のチュートリアルです。

blogs.perl.org

このチュートリアルなんですが、記事中の画像が連結されてたりするので、こちらでバラしてついでに png にしてます。

画像保存して名前つけてプログラムを試すことができます。

コードはこちらです。

#!/usr/bin/env perl
use strict;
use warnings;

use Imager;

my $tile = Imager->new(
    xsize    => 450,
    ysize    => 450,
    channels => 4,
);

my $mask      = Imager->new( file => 'mask.png' );
my $craftsman = Imager->new( file => 'craftman.png' );

$tile->compose( src => $craftsman, mask => $mask );

$tile->write( file => 'tile.png' );

で、合成した画像がこちらです。

f:id:sironekotoro:20210606155125p:plain

craftman.pngmask.png を合成し、mask.png の黒枠のところは隠して白い部分だけ craftman.png の画像を出す、隠した部分は透過させる・・・という処理です。

こういうのをマスクをかける、というそうです。

言葉は知ってましたが、実際にやってみるのは初めてでした。

わからないなりになんとか

画像処理のプログラムですが、業務で利用している PHP の画像合成コードを Perl で書き直してみよう!ってことで着手しました。

PHP の方では GD という画像処理ライブラリを利用しており、それと 1 対 1 で翻訳していくような形で Perl に移植しました。

いったんなんとか動く、ってところまで持っていき Perl っぽく書きなおし、 Imager のメソッドをフル活用し、最終的には Perl のモジュールにすることができました。

いやー、大変だった。

何が大変だったかというと、画像を回転させると画像の縦横が少し増えることを意識できず遠回りしまくったことですね。

完成させられたのは Google 翻訳と運のおかげですわ。

以下のメソッドを使いました。

  • compose: マスクをかける

  • rubthrough: 合成(セル画を重ねるようなイメージ?)

  • scale: サイズ変更、サムネイル作るとか

  • crop: 画像の一部を抜き出す

  • rotate: 画像を回転させる。回転させて生じた余白は指定がなければ透過黒

(追記あり)SSD を換装した MacBook Air (13-inch, Mid 2013) で panic(cpu 0 caller 0xffffff8002ac2838)

続きの記事を書きました

sironekotoro.hateblo.jp

panic(cpu 0 caller 0xffffff8002ac2838)

今年(2021 年)の春くらい、5 月頃からこのエラーが出るようになりました。

発生タイミングはスリープ復帰時&おそらく高負荷時。

エラーメッセージは以下。

エラーメッセージ

panic(cpu 0 caller 0xffffff8002ac2838): nvme: "Fatal error occurred. CSTS=0xffffffff US[1]=0x0 US[0]=0x41 VID=0x1987 DID=0x5012
. FW Revision=ECFM22.6\n"@/System/Volumes/Data/SWE/macOS/BuildRoots/2288acc43c/Library/Caches/com.apple.xbs/Sources/IONVMeFamily/IONVMeFamily-557.100.13/Common/IONVMeController.cpp:5499
Backtrace (CPU 0), Frame : Return Address
0xffffffa077583960 : 0xffffff800028e02d 
0xffffffa0775839b0 : 0xffffff80003d48e3 
0xffffffa0775839f0 : 0xffffff80003c4eda 
0xffffffa077583a40 : 0xffffff8000232a2f 
0xffffffa077583a60 : 0xffffff800028d84d 
0xffffffa077583b80 : 0xffffff800028db43 
0xffffffa077583bf0 : 0xffffff8000a9d68a 
0xffffffa077583c60 : 0xffffff8002ac2838 
0xffffffa077583c80 : 0xffffff8002aa7433 
0xffffffa077583de0 : 0xffffff80009f54f5 
0xffffffa077583e50 : 0xffffff80009f53f6 
0xffffffa077583e80 : 0xffffff80002d44b5 
0xffffffa077583ef0 : 0xffffff80002d5424 
0xffffffa077583fa0 : 0xffffff800023213e 
      Kernel Extensions in backtrace:
         com.apple.iokit.IONVMeFamily(2.1)[2A44DC48-B629-386C-985E-3BE03CCA48F2]@0xffffff8002aa0000->0xffffff8002ac9fff
            dependency: com.apple.driver.AppleEFINVRAM(2.1)[C6EE02AA-79D2-3EF8-83A6-9E52549E16D9]@0xffffff80016fe000->0xffffff8001707fff
            dependency: com.apple.driver.AppleMobileFileIntegrity(1.0.5)[2D13AEBE-3C77-3EE0-BC06-BDED7FE19FDE]@0xffffff80018b8000->0xffffff80018cdfff
            dependency: com.apple.iokit.IOPCIFamily(2.9)[29933CED-5D05-36A4-BFA1-6F4B4F349283]@0xffffff8002d5f000->0xffffff8002d87fff
            dependency: com.apple.iokit.IOReportFamily(47)[3F7604AB-EA65-3904-A1F4-AFEB25D288A7]@0xffffff8002d96000->0xffffff8002d98fff
            dependency: com.apple.iokit.IOStorageFamily(2.1)[58EA4506-4E6B-3AC3-A70D-ED35EE2C381D]@0xffffff8002e62000->0xffffff8002e73fff

Process name corresponding to current thread: kernel_task
Boot args: -x

Mac OS version:
20E232

Kernel version:
Darwin Kernel Version 20.4.0: Fri Mar  5 01:14:14 PST 2021; root:xnu-7195.101.1~3/RELEASE_X86_64
Kernel UUID: BB2FFEBA-7D53-301F-A238-9867CA51276F
KernelCache slide: 0x0000000000000000
KernelCache base:  0xffffff8000200000
Kernel slide:      0x0000000000010000
Kernel text base:  0xffffff8000210000
__HIB  text base: 0xffffff8000100000
System model name: MacBookAir6,2 (Mac-7DF21CB3ED6977E5)
System shutdown begun: NO
Panic diags file available: YES (0x0)
Hibernation exit count: 0

System uptime in nanoseconds: 565017200055
Last Sleep:           absolute           base_tsc          base_nano
  Uptime  : 0x000000838da6550d
  Sleep   : 0x0000000000000000 0x0000000000000000 0x0000000000000000
  Wake    : 0x0000000000000000 0x00000009032d2f54 0x0000000000000000
last started kext at 379393286647: @filesystems.afpfs   11.3 (addr 0xffffff7f9a183000, size 327680)
last stopped kext at 302135866140: >!ASMBusPCI  1.0.14d1 (addr 0xffffff7f99490000, size 4096)
loaded kexts:
@filesystems.afpfs  11.3
@nke.asp_tcp    8.2
@filesystems.autofs 3.0
>AGPM   122
>X86PlatformShim    1.0.0
>!AGraphicsDevicePolicy 6.2.9
@Dont_Steal_Mac_OS_X    7.0.0
>!ABacklight    180.3
>!A!IFramebufferAzul    16.0.2
>!ALPC  3.1
>!AMCCSControl  1.14
>!UTopCaseDriver    4040.11
|IO!BUSBDFU 8.0.4d18
|SCSITaskUserClient 436.100.4
>!UCardReader   511.101.1
>!AFileSystemDriver 3.0.1
@filesystems.tmpfs  1
@filesystems.hfs.kext   556.100.11
@BootCache  40
@!AFSCompression.!AFSCompressionTypeZlib    1.0.0
@!AFSCompression.!AFSCompressionTypeDataless    1.0.0d1
>!ATopCaseHIDEventDriver    4040.11
@filesystems.apfs   1677.100.114
>AirPort.BrcmNIC    1400.1.1
@private.KextAudit  1.0
>!ASmartBatteryManager  161.0.0
>!ARTC  2.0
>!AACPIButtons  6.1
>!AHPET 1.8
>!ASMBIOS   2.1
>!AACPIEC   6.1
>!AAPIC 1.7
@!ASystemPolicy 2.0.0
@nke.applicationfirewall    311
|IOKitRegistryCompatibility 1
|EndpointSecurity   1
$SecureRemotePassword   1.0
@kext.triggers  1.0
>!AGraphicsControl  6.2.9
@!AGPUWrangler  6.2.9
>!ABacklightExpert  1.1.0
|IONDRVSupport  585.1
|IOAccelerator!F2   442.9
@!AGraphicsDeviceControl    6.2.9
>X86PlatformPlugin  1.0.0
>IOPlatformPlugin!F 6.0.0d8
|IOGraphics!F   585.1
>!ASMBus!C  1.0.18d1
>!AActuatorDriver   4440.3
>usb.IOUSBHostHIDDevice 1.2
|Broadcom!BHost!CUSBTransport   8.0.4d18
|IO!BHost!CUSBTransport 8.0.4d18
|IO!BHost!CTransport    8.0.4d18
>usb.!UHub  1.2
>usb.cdc    5.0.0
>usb.networking 5.0.0
>usb.!UHostCompositeDevice  1.2
>!AThunderboltDPInAdapter   8.1.4
>!AThunderboltDPAdapter!F   8.1.4
>!AThunderboltPCIDownAdapter    4.1.1
>!ABSDKextStarter   3
|IOSurface  290.7
@filesystems.hfs.encodings.kext 1
>!AMultitouchDriver 4440.3
>!AInputDeviceSupport   4400.35
>!AHS!BDriver   4040.11
>IO!BHIDDriver  8.0.4d18
>!AHIDKeyboard  224
>!AHSSPIHIDDriver   61
>!AXsanScheme   3
|IONVMe!F   2.1.0
>!AThunderboltNHI   7.2.8
|IOThunderbolt!F    9.3.2
|IO80211!F  1200.12.2b1
|IOSkywalk!F    1
>mDNSOffloadUserClient  1.0.1b8
>corecapture    1.0.4
>usb.!UHostPacketFilter 1.0
|IOUSB!F    900.4.2
>!AHSSPISupport 61
>!A!ILpssSpi!C  3.0.60
>!A!ILpssI2C    3.0.60
>!A!ILpssDmac   3.0.60
>usb.!UXHCIPCI  1.2
>usb.!UXHCI 1.2
>!A!ILpssGspi   3.0.60
>!AEFINVRAM 2.1
>!AEFIRuntime   2.1
|IOSMBus!F  1.1
|IOHID!F    2.0.0
$!AImage4   3.0.0
|IOTimeSync!F   980.4
|IONetworking!F 3.4
>DiskImages 493.0.0
|IO!B!F 8.0.4d18
|IOReport!F 47
|IO!BPacketLogger   8.0.4d18
$quarantine 4
$sandbox    300.0
@kext.!AMatch   1.0.0d1
|CoreAnalytics!F    1
>!ASSE  1.0
>!AKeyStore 2
>!UTDM  511.101.1
|IOUSBMass!SDriver  184.101.1
|IOSCSIBlockCommandsDevice  436.100.4
|IO!S!F 2.1
|IOSCSIArchitectureModel!F  436.100.4
>!AMobileFileIntegrity  1.0.5
@kext.CoreTrust 1
>!AFDEKeyStore  28.30
>!AEffaceable!S 1.0
>!ACredentialManager    1.0
>KernelRelayHost    1
|IOUSBHost!F    1.2
>!UHostMergeProperties  1.2
>usb.!UCommon   1.0
>!ABusPower!C   1.0
>!ASEPManager   1.0.1
>IOSlaveProcessor   1
>!AACPIPlatform 6.1
>!ASMC  3.1.9
|IOPCI!F    2.9
|IOACPI!F   1.4
>watchdog   1
@kec.pthread    1
@kec.corecrypto 11.1
@kec.Libm   1

エラーの原因

不明です。

ただまぁ、予想はついててエラーメッセージの

panic(cpu 0 caller 0xffffff8002ac2838): nvme: "Fatal error occurred. CSTS=0xffffffff US[1]=0x0 US[0]=0x41 VID=0x1987 DID=0x5012
. FW Revision=ECFM22.6\n"@/System/Volumes/Data/SWE/macOS/BuildRoots/2288acc43c/Library/Caches/com.apple.xbs/Sources/IONVMeFamily/IONVMeFamily-557.100.13/Common/IONVMeController.cpp:5499

ここにある IONVMeFamily ですね。IO は Input/Output 、NVMe は SSD の規格名です。

ここであれかなーって思うわけです。

タイトルにある通り、この MacBook Air は容量を増やすために SSD を換装しています。

www.akibakan.com

Docker 使っている分にはこまめにイメージを消せば良いのですが、さすがに ParallelsWindows を使うようになってからは容量が厳しくなってきました。

で、快適に 1 年くらい使ってたところでこのエラーな訳です。

エラーメッセージの一部でググると、やっぱこのエラーに遭遇している人が多少いるようです。

様子見

MacOS の電源設定を行う pmset コマンドやシステム環境設定で色々やりました。

他には

  • shift + option + command + R からの OS 再インストール(ただしデータは残ってた)

  • command + option + P + R による PRAM リセット

  • command + D によるハードウェア診断(異常なしとの診断結果)

とりあえず落ち着いたみたいなのですが、色々試しすぎたため、どの設定変更が功を奏したかわからなくなってしまいました。

なので現在の設定を置いておきます。

f:id:sironekotoro:20210606113732p:plain

f:id:sironekotoro:20210606112920p:plain

f:id:sironekotoro:20210606113004p:plain

f:id:sironekotoro:20210606113026p:plain

$ pmset -g
System-wide power settings:
Currently in use:
 standbydelaylow      10800
 standby              0
 womp                 0
 halfdim              1
 hibernatefile        /var/vm/sleepimage
 powernap             0
 gpuswitch            2
 networkoversleep     0
 disksleep            0
 standbydelayhigh     0
 sleep                0 (sleep prevented by mds, mds_stores, UserEventAgent, UserEventAgent, UserEventAgent, runningboardd, runningboardd, AddressBookSourceSync)
 autopoweroffdelay    259200
 hibernatemode        3
 autopoweroff         1
 ttyskeepawake        1
 displaysleep         180
 highstandbythreshold 50
 acwake               0
 lidwake              1
$ uptime
11:45  up 2 days, 21:35, 2 users, load averages: 2.22 2.58 2.47

まだ 1TB くらい空きがあるのが笑えるわー。貧乏性だ。

うめき

追記(2021/06/07 02:11:45)

症状の再発を確認。

エラーメッセージは同一。

SSDを元に戻して( 2TB -> 256GB )清貧な暮らしをするか、買い替えするかだなぁ。

追記(2021/06/21)

諦めて元の SSD に戻して利用中です。もちろん症状は出ていません。

次の macOS が出て、余力があればもう一度試してみたいですね。

追記(2022/02/14)

sironekotoro.hateblo.jp

Sublime Text 4 で環境変数を読み込む

Sublime Text 4 にあげました

もうアップデートすることもないかな・・・って思っていた Sublime Text ですが、起動したらアップデートを促されました。

アップデートしたところ、メジャーバージョンが上がって 4 になりました。

幸い、いまのところ(3時間くらい)困ったところは出てないです。

Sublime Text 3 の頃に設定した Perl の開発環境もそのまま動いています。

エラーメッセージの出方がちょっと違うかなーとか、それくらい。

Sublime Text 4 との出会い

2013 年に受講した Perl 入学式でおすすめされたことがきっかけです。

初心者に環境を与えると、それを親と思い込む性質がある・・・かどうかはともかく、うちは Sublime Text をメインに、 VS Code をサブに、サーバー上では vim って感じで使い分けています。

Sulime Text 4 で環境変数が読み取れない?

多分、Sublime Text 4 に限らないことだと思います。

~/.zshrcexport GOOGLE_CLIENT_ID=hogehoge こんな感じで環境変数を設定しました。

$ source ~/.zshrc で設定を反映させて、ターミナルから確認します。

$ echo $GOOGLE_CLIENT_ID
hogehoge

よしよし。

で、sublime text 上の Perl で確認します。

Sublime Text には Build System というものがあり、これは VS Code でいうところの Code Runner みたいなやつです。

書いたコードをすぐに実行して、エディタの別領域に結果を出せるという。

こんな感じ。

f:id:sironekotoro:20210531173921p:plain

で、Perl環境変数を呼び出すときはこう書きます。

#!/usr/bin/env perl
use strict;
use warnings;

print $ENV{GOOGLE_CLIENT_ID}

・・・エラー!?

f:id:sironekotoro:20210531174333p:plain

いやいやいやいや、ターミナルでは表示できてまんがな。

Sulime Text 4 で環境変数が読み取る

で、結論から言うと、この時の Sublime Text 4 は Mac の Dock から起動したものでした。

これをターミナルから起動してあげると・・・あ、うちは .zshrc にこういうエイリアス書いてます。

alias subl='/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl'

$ subl ~/Desktop/test.pl

はい、このようにターミナルからの起動であれば環境変数を読み込むことができました。

Sublime Text の Build System は既存のシェルとは独立したもののようです。

というか、前も plenv で選択した Perl のバージョンが利用されないって色々やったなそういえば(今になって思い出した)

f:id:sironekotoro:20210531174923p:plain

めでたしめでたし。

Perl で Google Sheet API を使(って|わなくても) csv でダウンロードする

休日の方が仕事がらみのプログラムが捗る

ってことないですかね?

うちはそうです。

つまり逆は・・・この話やめますか

こんな業務

以下のフローで処理している業務があります。

  1. 他部署から限定公開の Google Sheet の URL がこちらに伝えられてくる

  2. Google Sheet を csv ファイルでダウンロードする

    • ファイル -> ダウンロード -> カンマ区切りの値

    f:id:sironekotoro:20210530135132p:plain

  3. ダウンロードした csv ファイルを特定のファイル名にリネームする

  4. 別なプログラムを起動して csv ファイルを読み込ませる

  5. 生成された諸々のファイルをサーバーにあげてなんたらかんたら

めんどさの水位を超えた

まず、この業務の時にいちいち Google Sheet みなきゃいけないのがめんどいです。

ダウンロードするためだけにブラウザで開くのめんどい。

コマンド一発でファイルをダウンロードできたらいいのに・・・という思いを持ってきたのですが、書きだした通りたいした手間でもない(15秒もかからない)のでずっと「めんどい」と思いながら放置してきたわけです。

しかし、めんどい値が水位を超えて溢れる日がやってきました。先週です。

というわけで、いつものように Perl で片付けます。

PerlGoogle Sheet API を使って csv でダウンロードする

まず、限定公開の Google Sheet なので、プログラムからアクセスするには OAuth による認可とったうえで API でアクセスします。

当初は Google Drive API で持ってきたら楽なのでは?と考えたのですが、シート単位でのダウンロードはできない模様。

できるという情報ある方、お待ちしてます・・・

developers.google.com

で、Google Sheet API 使いました。

ほとんど過去のコードのコピペです。

Google Sheet APIGoogle Sheet のデータを取得して、csv に書き出すってやつです。

sironekotoro.hateblo.jp

#!/usr/bin/env perl
use strict;
use warnings;
use Encode;
use HTTP::Tiny;
use JSON;
use Text::CSV_XS;
use URI;

my $ACCESS_TOKEN = access_token(
    CLIENT_ID     => '',
    CLIENT_SECRET => '',
    REFRESH_TOKEN => '',
);

my $bearer = join " ", ( 'Bearer', $ACCESS_TOKEN );

my $rows = values_get(
    bearer           => $bearer,
    SPREADSHEET_ID   => '',
    GOOGLE_SHEET_API => 'https://sheets.googleapis.com/v4/spreadsheets/',
    SHEET_NAME       => 'シート1',
);

my $csv = Text::CSV_XS->new( { eol => "\012"} );
open my $FH, '>:utf8', './test.csv';
while ( my $row = shift @${rows} ) {
    $csv->print( $FH, $row );
}
close $FH;

sub values_get {
    my %hash = @_;

    my $ht
        = HTTP::Tiny->new( default_headers => { Authorization => $bearer, } );

    my $SHEET_URI
        = $hash{GOOGLE_SHEET_API}
        . $hash{SPREADSHEET_ID}
        . '/values/'
        . "$hash{SHEET_NAME}!A1:BV";

    my $URI = URI->new($SHEET_URI);

    my $res  = $ht->get($URI);
    my $data = decode_json( $res->{content} );

    return $data->{values};
}

sub access_token {
    my %hash = @_;

    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     => $hash{CLIENT_ID},
                    client_secret => $hash{CLIENT_SECRET},
                    grant_type    => 'refresh_token',
                    refresh_token => $hash{REFRESH_TOKEN},
                }
            )
        }
    );

    my $json = decode_json( $response->{content} );

    return $json->{access_token};
}

しかし、出来上がった csv ファイルは Google Sheet の API からダウンロードした csv ファイルと内容が異なっています。

どうやら、こんな感じ

  • Web からダウンロード: セルに日本語が入っており一定の長さ以上だったり , カンマやスペースが入っている時は " ダブルクオーテーションで囲んでいる(?)、そうでない時は " ダブルクオーテーションで囲んでいない

  • 今回作ったのでダウンロード:機械的にフィールドの値を " ダブルクオーテーションで囲んでいる

うーん。

いや、csv ファイルがちょっと異なっていようが、今回 Perl でおとしてきた csv ファイルが、手順上の「その上で別なプログラムを起動して csv ファイルを読み込ませる」でも読めればいいわけです。

・・・だめでした。無念。

このまま、Google Sheet 上での csv ダウンロードの仕様に沿った csv つくる・・・?

俺が、俺こそが Google になって Google Sheet を csv にパースする?

それってもっとすんごく面倒そうだなー、となります。

PerlGoogle Sheet API を使わなくても csv でダウンロードする

色々とぐぐっている中で、こういう記事がありました。

qiita.com

あら、curlcsv にしてダウンロードできるの・・・?

この記事では公開されている Google Sheet ですが、非公開でも OAuth で通ったやつをつけてあげれば良いのでは?と考えました。

#!/usr/bin/env perl
use strict;
use warnings;
use HTTP::Tiny;
use URI;
use Encode;
use JSON;

my $ACCESS_TOKEN = access_token(
    CLIENT_ID     => '',
    CLIENT_SECRET => '',
    REFRESH_TOKEN => '',
);

my $bearer = join " ", ( 'Bearer', $ACCESS_TOKEN );

my $http = HTTP::Tiny->new( default_headers => { Authorization => $bearer, } );

my $url = 'https://docs.google.com/spreadsheets/d/xxxxxx/export?gid=0123456789&format=csv';
my $file = './test.csv';
my %options = ();
my $response = $http->mirror($url, $file, \%options);
if ( $response->{success} ) {
    print "$file is up to date\n";
}

sub access_token {
    my %hash = @_;

    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     => $hash{CLIENT_ID},
                    client_secret => $hash{CLIENT_SECRET},
                    grant_type    => 'refresh_token',
                    refresh_token => $hash{REFRESH_TOKEN},
                }
            )
        }
    );

    my $json = decode_json( $response->{content} );

    return $json->{access_token};
}

あっさりダウンロードできました。csv ファイルも Google シートからダウンロードしたものと diff をとっても違いなし。

これでまた一つ、仕事が楽になりました。

Perl で Google Sheet API をつかってセルの値を削除する

あれー?

Google Sheet API を使って Google Sheet に金融機関コードや支店ごとの情報を反映させる、ってなコードを書いてました。

以下が完成するとこうなるって画像です。

f:id:sironekotoro:20210529125309p:plain

テストのシートへの反映を確認し、いざ本番のシートに反映!

・・・で、やってみたところ確かに反映されるのですが、一部の値が重複していることがわかりました。

確認したところ、こういうことでした。

  • 支店の統廃合の結果、全金融機関の支店数は日々減少している

  • 更新に使ったGoogle Sheet APIvalues:batchUpdate は、引数で渡された範囲のセルの値を更新する

  • 対象範囲外のセルは残る

つまり、反映前に 1000 件のリストがあり、更新時のリストが 800 件しかなければ、更新されるのは 800 件のみとなります。

そして残りの 200 件はそのまま残るわけですね。

なるほど。

シートの値だけを消す

ということで、情報を反映させる前に一旦シートの中身を綺麗にすることで対応しました。

今回は値のみを削除し、書式は残します。

これの Perl で書いてみた版です。

developers.google.com

あと自分のブログのこの辺り参考 にしました。半年も前だと完全に忘れてますね。

sironekotoro.hateblo.jp

#!/usr/bin/env perl
use strict;
use warnings;
use HTTP::Tiny;
use JSON;
use URI;
use utf8;

my $ACCESS_TOKEN = access_token(
    CLIENT_ID =>'',
    CLIENT_SECRET => '',
    REFRESH_TOKEN => '',
);

my $bearer = join " ", ( 'Bearer', $ACCESS_TOKEN );

sheet_clear(
    bearer           => $bearer,
    SHEET_ID         => '123456789',    # 更新したいシートのID(gid= の後の数字)
    SPREADSHEET_ID   => '', # スプレッドシートのID
    GOOGLE_SHEET_API => 'https://sheets.googleapis.com/v4/spreadsheets/',
);

sub sheet_clear {
    my %hash = @_;

    my $ht = HTTP::Tiny->new(
        default_headers => { Authorization => $hash{bearer}, } );

    my $SHEET_URI
        = $hash{GOOGLE_SHEET_API} . $hash{SPREADSHEET_ID} . ':batchUpdate';
    my $URI = URI->new($SHEET_URI);

    my $response = $ht->request(
        'POST', $URI,
        {   content => encode_json(
                {   requests => [
                        {   updateCells => {
                                range  => { sheetId => $hash{SHEET_ID} },
                                fields => "userEnteredValue",
                            }
                        }
                    ]
                }
            )
        }
    );
}


sub access_token {
    my %hash = @_;

    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     => $hash{CLIENT_ID},
                    client_secret => $hash{CLIENT_SECRET},
                    grant_type    => 'refresh_token',
                    refresh_token => $hash{REFRESH_TOKEN},
                }
            )
        }
    );

    my $json = decode_json( $response->{content} );

    return $json->{access_token};
}

困って解決したらブログに残しておこう

困った時にすぐググる人っていますよね?

自分のことです。

自分が解決したり、困ったり、一度やったことを書いておくと、ググる様が検索して見つけてきてくれます。

ありがたい。

今回も過去の自分の記事に救われました。書いててよかったわー

この記事もきっと未来の自分と、もしかしたら Google 経由で来た誰かの助けになるでしょう。