watermint.org - Takayuki Okazaki's note

CLI (command-line utility) for Google Sheets

I added commands to deal with spreadsheets in watermint toolbox release 85. With these commands, you can export data from Google Sheets as CSV or JSON. Or, update Google Sheet with CSV file from the command line.

The tool runs on multiple platforms without complex dependencies. Just download & extract executable file from the archive. Currently, the tool run on Windows (x86/amd64), Linux (x86), and macOS (amd64).

Exporting data

Google Sheet Export

To retrieve data from Google Sheet, run the following command.

$ tbx services google sheets sheet export -id GOOGLE_SHEET_ID -range SHEET_NAME

If you want to specify output path, please add -data PATH_TO_EXPORT option. Please see more detail about the command at the command manual.

(Sorry for the long command name. Because the tool now has 200+ commands. :-)

Importing data

Google Sheet Import

To import data from CSV, run the following command.

$ tbx services google sheets sheet import -id GOOGLE_SHEET_ID -range SHEET_NAME -data DATA_FILE_PATH

The command changes only cell values. Please see more detail about the command at the command manual.

More commands

The latest release contains a few more commands to deal with Google Sheets.

Note:

The registration to Google is currently verification in progress.

Warning

You may see the warning on the authorization screen. Please verify safety before proceed it.

発掘昔のコード(1996年, C++): コンパイラと実行環境

ファイルを整理していたところ、高校生の頃に作ったコードが出てきたので25年ぶりに実行してみました。当時、郵便配達のアルバイトで購入したBorland C++を使って作っています。C++やオブジェクト指向についても図書館に通い少しこなれてきた頃に作ったもので、産業フェアというイベントに出展した記憶があります。プログラムは大きく分けて二つあり、BASICを参考にした簡単なスクリプト言語をバイトコードに変換するコンパイラと、MS-DOS画面ながらもマルチウインドウの仮想マシンです。

実行した様子

高校の図書室にあったコンパイラの解説本やスタックマシーンについて説明している本を参考にしつつ、当時話題のJavaやバイトコードといったエッセンスを混ぜて作ったのを思い出しました。コンパイラの部分をみてみると、実装逆ポーランド記法などへの変換などもがんばって実装しているのが懐かしいです。

void
Compile::ReversePolishNotation( int argc , char **argv )
{
  struct lsElement
  {
    int    r ;         //  優先順位
    char*  symbol ;    //  識別子
    int    output ;    //  出力
  } ;

  lsElement  elements[] =
  {
     { 0 , "'" , bc_nop }  //  NULL
  ,  { 1 , "and" , bc_and }
  ,  { 1 , "or" , bc_or }
  ,  { 1 , "xor", bc_xor }
  ,  { 2 , "<" , bc_lt }
  ,  { 2 , "<=" ,bc_le }
  ,  { 2 , "==" ,bc_eq }
  ,  { 2 , "!=" ,bc_ne }
  ,  { 2 , ">" , bc_gt }
  ,  { 2 , ">=" , bc_ge }
  ,  { 3 , "+" , bc_add }
  ,  { 3 , "-" , bc_sub }
  ,  { 4 , "*" , bc_mul }
  ,  { 4 , "/" , bc_div }
  ,  { 4 , "mod" , bc_mod }
  } ;
  
  const int  StackSize  = 256 ;
        
  //  '(' は 100
  //  としてスタックに積むことにします
  const int  vLeft    = 100 ;
  
  int  ss[ StackSize ] ;
  int  sp = 0 ;
  int  i ;
  int  j ;
  int  f ;  //  一致するものがあったかどうかのフラグ
  int elementnum = sizeof( elements ) / sizeof( elements[ 0 ] ) ;
  DataId  id ;
  
  ss[ sp ] = 0 ;
  
  for ( i = 0 ; i < argc ; i++ )
  {
    f = 0 ;
    
    for ( j = 0 ; j < elementnum ; j++ )
    {
      if ( strcmp( argv[ i ] , elements[ j ].symbol ) == 0 )
      {
        if ( ss[ sp ] == vLeft )
        {
          sp++ ;
          ss[ sp ] = j ;
        }
        else
        {
          if ( elements[ ss[ sp ] ].r >= elements[ j ].r )
          {
            id.value = 0 ;
            csPut( elements[ ss[ sp ]].output , id ) ;
            ss[ sp ] = j ;
          }
          else
          {
            sp++ ;
            ss[ sp ] = j ;
          }
        }
        f = 1 ;
        break ;
      }
    }
    
    if ( !f )
    {
      if ( strcmp( argv[ i ] , "(" ) == 0 )
      {
        sp++ ;
        ss[ sp ] = vLeft ;
        continue ;
      }
      
      if ( strcmp( argv[ i ] , ")" ) == 0 )
      {
        if ( ss[ sp ] != vLeft )
        {
          id.value = 0 ;
          csPut( elements[ ss[ sp ]].output , id ) ;
        }
        sp-- ;
        if ( sp < 0 )
          sp = 0 ;
        
        continue ;
      }
      
      if ( isalpha( argv[ i ][ 0 ] ))
      {
        id = VarId( argv[ i ] ) ;
        csPut( bc_push , id ) ;
      }
      else
      {
        long v = atol( argv[ i ] ) ;
        id.PutType( 0x01 ) ;
        id.PutVal( v ) ;
        csPut( bc_push , id ) ;
      }
    }
  }
  
  while ( sp != 0 )
  {
    if ( ss[ sp ] != vLeft )
    {
      id.value = 0 ;
      csPut( elements[ ss[ sp ]].output , id ) ;
    }
    sp-- ;
  }
}

実行対象となるプログラムはカレンダーを表示するプログラム、素数の計算、フィボナッチ数列など4つほどでしたが、当時単体テストすらまともに実施しておらず、サンプルコードの問題なのかコンパイラの問題なのか、ランタイムの問題なのかデバッグは困難を極めあまり複雑なコードは時間切れで作れなかった記憶があります。

'  LeftScript Sample Program
'  (c) by Takayuki Okazaki 1996
'  $Id: prime.ls 1.1 1996/11/03 17:53:09 Okazaki Exp Okazaki $

print "素数を求める"

e = 1000  ' 何処まで求めるか

i = 2
c = 1
print c , "個目の素数は" , i , "です"
c = c + 1
for i = 3 to e
  y = 0
  for j = 2 to i - 1
    if i mod j == 0 then
      y = 1
    endif
  next
  
  if y == 0 then
    print c , "個目の素数は" , i , "です"
    c = c + 1
  endif
next

print "1 から" , e , "までの整数のうちの素数は以上です"

実行環境は擬似マルチタスクで、各プログラムのバイトコードを1つ実行するたびに次のプロセスの実行に移ります。一応、キー入力などの実行命令も作っていたのでIO待ちなどの状態管理もしていました。仮想マシンのクラス定義をみてみると、スタックメモリはメインメモリの制約からかファイル上にマッピングしていたようです。

class ByteComputer : public UserWin
{
private :
  char  ls_filename[ asd_BufSize ] ;
  char  cs_filename[ asd_BufSize ] ;
  char  ds_filename[ asd_BufSize ] ;
  
  FILE*  stack ;
  FILE*  ds ;
  FILE*  cs ;
  long   sp ;
  DataId io_port ;
  int    status ;

  void   Push( DataId x ) ;
  DataId Pop() ;
  DataId GetVar( DataId x ) ;
  void   Input() ;
  
public :
  ByteComputer( const char *filename ) ;
  ~ByteComputer() ;
  
  virtual  int         InputChar( int c ) ;
  virtual  procStruct  GenUser() ;
} ;

コンパイラと実行環境合わせて全体で5000行ほどのプログラムです。記憶が正しければ当初は情報処理技術者試験で使われていたCASLアセンブリ言語のコンパイラとCOMET実行環境を作る予定でしたが仕様がそれなりに大きく間に合わないと判断してより簡易的なスクリプト言語の実装という方向性になったのだと記憶しています。

Google Photos/Amazon Photos/flickr/facebookからDropbox/iCloudへ写真を移行

4年前の記事10万枚を超える写真データの整理とストレージ選びでは、NASなどのストレージに格納された18TBの重複を含む写真を、重複排除した上でAmazon Photos、Dropbox、flickr、Google Photosに移行したことを紹介しました。

今回は、次のような理由で複数クラウド上に保管しているデータを9月中旬からの約2ヶ月をかけてiCloudとDropboxの2つに集約しました。

Dropboxに移行後

今回タイミング良く(?)、11月に2021年6月よりGoogle Photosのストレージ消費ポリシーが変わることが発表された時点では移行が完了していました。本記事がこの変更を受けて同様の移行を検討されている方の参考になれば幸いです。

iCloudに移行後

今回の移行には次のように、いくつかのモチベーションがありました。

  • 検索性の問題 : 前掲の記事でいう★3つ以上の高評価写真についてはよく整理していたので問題ありませんでしたが、それ未満のあまり管理していない写真は複数クラウド上で管理すると、あまり検索性が高くありませんでした。
  • 通信量の問題 : 4年前はあまり意識していませんでしたが、iPhone・iPadなどのモバイル端末で昔の写真を振り返るケースが増えました。Google Photosを利用していて一晩で20GB分ぐらいの通信クレジットを使い切ってしまったことがありました。
  • 閲覧性の問題 : Google Photosは顔認識などの検索がなかなか便利ですが、iCloudの写真アプリでの閲覧がかなり進化したため見劣りするなと感じています。また、やはり通常のファイルシステム上で扱えるかどうかも重要です。特に特定の写真を加工したり、読み込んでドキュメントに貼り付けるという処理をするときにも便利です。このため、高評価としていなかった写真についてもDropbox上に管理することにしました。
  • 内容の差異 : Google Photos、iCloud、DropboxについてはiPhone・iPadアプリで撮影した写真を自動バックアップしていました。ただ、アプリの動作や通信状況によってはうまくバックアップが取得できていなかったケースがあり若干の差分が発生していました。

整理後の状態

最終的には3つの種別にわけて、次のような構成としてまとめました。

種別 保管場所 備考
RAWデータ Dropbox 撮影年ごとにCapture Oneカタログとして保管
出力データ (JPEG) Dropbox, iCloud 撮影年/撮影年-撮影月/撮影年-撮影月-撮影日 日付順に並べ替えやすいようフォルダ/ファイル名をつけ保管
プレビューデータ (JPEG) iCloud iCloud Photosに取り込み

RAWデータは★3以上のものだけを残しあとは削除して整理しています。Dropbox上に管理しているので、あとで必要になった場合でも復元できますから作業効率が高まるよう積極的に削除します。オリジナルデータはほしいときにすぐ再加工できるようCapture Oneのカタログとして管理しています。参照用形式はすべてJPEGデータとしてそろえました。 なお、残念ながらCapture One 20は執筆時点でHEIF・HEIC形式に対応しておらず、iPhoneやiPadで撮影したファイルは直接扱えません。ファイル整理整頓の後はワークフローとしても見直す部分がありそうです。

出力データはRAWデータと同じファイル名で出力して管理しています。ファイル名は撮影年-撮影月-撮影日s連番 (例: 2020-12-05s00013) のような形式で全アルバムの中で一意になるよう管理しています。これにより出力ファイル側で検索して、RAWデータを探すことができ作業効率が良くなりました。

プレビューデータは長辺が2704ピクセルとなるようリサイズしたデータです。プレビューデータとして利用する際はほとんどの場合、10〜11インチのiPadを利用します。13インチ程度のディスプレイを将来的に利用するとしても、2700ピクセル程度あれば250dpi以上を確保できます。さらに高い解像度にするよりはファイルサイズを絞ることで、プレビュー速度やダウンロード速度とのバランスをいくつか実験のうえ決定しました。2704ピクセルは中途半端な数値ですが動画の規格にFull HDと4Kの間にある2.7Kという解像度があり、ここからとっています。

クラウドに格納されたデータ

クラウド上に格納されたデータを確認するのはなかなか手間がかかります。Dropboxのようにファイルシステム上に見えるサービスの場合であれば簡単ですが、flickrやGoogle Photosのように写真のデータベースとなっている場合にはすべてダウンロードして手元で展開する必要があります。まずは1週間以上かけてすべてのデータをダウンロードしました(やり直し含めて3週間ほどかかりました)。

合計で3.7TBほどになりました。写真枚数は合計すると80万枚ほどですが、重複を排除すると15万枚ほどになりました。

サービス 写真枚数 サイズ (GiB)
Amazon Photos 135,841 2,046.39
flickr 363,440 1,053.31
Dropbox (カメラアップロード) 19,389 243.73
Google Photos 234,233 200.54
iCloud 18,965 97.80
Dropbox (高評価写真管理用) 14,428 66.98
facebook 7,742 2.04

まずはAmazon Photosです。2TB程度格納していたので、同容量の空きストレージを準備し、Amazon Photosアプリケーションでダウンロードしました。これに1週間ほど。

ほかのデータについては一度Dropboxに集約してから処理しました。

flickrについてはアカウント情報のYour Flickr Dataからエクスポートできます。2019年1月にflickrのストレージプラン変更時にDropboxへ保存しておきました。Dropboxからのダウンロードはスムーズで丸一日程度で終わりました。flickrからダウンロードしたファイルは画像ファイルとアルバムやコメントなどのメタデータがJSON形式で保管されます。今回、メタデータは処理しませんでしたので画像ファイルのみを抽出しました。

Google PhotosについてはGoogle Takeoutにてエクスポートできます。Dropboxにそのままエクスポートする機能がありますので、まずはDropboxにまとめておき、そこからダウンロードしました。Google Photosもflickrと同様に画像ファイルとメタデータのJSONファイルが出力されます。

facebookのデータはFacebook から写真や動画をDropboxにインポートする機能ができましたのでこれを使いました。メタデータなどは無く写真ファイルがアルバム名のフォルダとして整理されて出力されます。

重複の排除とリサイズ、日付の調整

Amazon PhotosやiCloudにはすべてのRAWデータなどが格納されていましたが、評価★3未満のファイルについてはプレビューのみ残し、あとは削除することにしました。

最初重複の排除にはGeminiというアプリを使っていましたが、十分重複排除しきれないことと、ファイル数が多すぎてアプリが頻繁に停止してしまう状態なってしまいました。また、いくつかカバーできない要件もあることから、今回は重複の排除とリサイズ処理は自作プログラムで実行することにしました。

自作プログラムといっても、ImageMagickやmacOSに標準インストールされているsipsコマンド、ExifToolphashionといった既存のライブラリを組み合わせたものであまり特殊な処理はしていません。

重複排除の際は、ファイルが全く同一のバイナリであれば簡単ですが、同じ写真で異なる解像度、または同じ解像度だがEXIF情報が異なるなど複数のバリエーションがありました。このため、(1) EXIF撮影日時情報が含まれること、(2) 最も解像度が高い、(3) 評価情報があればその評価順という3つの条件で優先順位をつけて最優先する写真を選択し、ほかの写真ファイルは脱落するという処理を実施しました。

EXIF情報の処理

試行錯誤が必要だったケースを紹介します。次のファイルは、EXIFのDateTimeOriginalとModifyDateの両方が記録されているケースです。1998年に撮影されたファイルを、2002年に編集しそれがEXIF情報として記録されています。

% exiftool images/1998-03-10s29812r0.jpeg
File Name                       : 1998-03-10s29812r0.jpeg
... 略 ...
File Modification Date/Time     : 2016:05:21 08:25:18+09:00
File Access Date/Time           : 2020:10:11 19:21:39+09:00
File Inode Change Date/Time     : 2020:10:10 21:50:56+09:00
... 略 ...
Modify Date                     : 2002:12:30 20:37:20
... 略 ...
Date/Time Original              : 1998:03:10 11:29:08

これをmacOS標準コマンドsipsで情報を取得すると次のようにModify Dateが日付として表示されます。

% sips -g all images/1998-03-10s29812r0.jpeg
... 略 ...
  pixelWidth: 480
  pixelHeight: 640
  typeIdentifier: public.jpeg
  format: jpeg
  formatOptions: default
  dpiWidth: 72.000
  dpiHeight: 72.000
  samplesPerPixel: 3
  bitsPerSample: 8
  hasAlpha: no
  space: RGB
  creation: 2002:12:30 20:37:20
  make: FUJIFILM
  model: DS-30
  software: QuickTime 6.0.2
  copyright:

このように扱うライブラリやツールによって最終的に処理として利用するフィールドが異なるケースがあります。今回は、プレビューデータからはModify Dateデータを削除し、Date/Time Originalだけを利用しました。

同一画像の検出と重複排除

解像度が違ったり、編集時に明るさや色などを調整した写真など元は1枚の写真でも、多いものでは100ファイル程度のバリエーションとしてばらばらに保管されている写真もありました。これらをまとめて、重複排除するために今回はpHashというハッシュアルゴリズムを利用しました。pHashについてはより詳しく紹介されている記事が簡単に検索できますのでここでは詳しく紹介しませんが、画像の特徴抽出して画像間の”ハミング距離”を算出し、距離が0に近ければ同一または類似の画像であると推測できます。

今回はすべての画像に対するハミング距離の組み合わせまでは計算せず、ハミング距離が0のものだけを抽出して重複排除しました。これにより、80万ファイルあった写真が15万ファイル程度まで絞れました。あとはiCloudのPhotosのAIなどにがんばってもらおうと割り切り、あまり深入りしないことにしました。

なお、pHashなどの計算処理は最新のIntel Core i9-10910 CPUをほぼフルに使っていてもそれなりに重い処理で、80万ファイルに実行するには一晩かかる処理でした。これを数回試行錯誤していましたので、最終的に満足のゆく状態になるまでには1〜2週間はかかったかと思います。

ファイル形式の統一とリサイズ

ファイルはJPEGだけでなく、ARW、NEF、DNG、PNG、GIF、TIFF、HEICなど複数のファイル形式がありました。プレビューとして扱いやすいようすべてJPEG形式に統一しました。

リサイズ処理は最初に何度か失敗を繰り返しています。最初、RAWで保管してあった全てのデータをCapture Oneに取り込み、自動調整をかけてJPEGに出力する予定でした。しかしながら、Capture Oneはライブラリに取り込んだファイル数が1万ファイルを超えたあたりから指数関数的に重くなり、10万ファイルを超える頃には実質使い物にならない状態になっていました。CPU負荷などを見ても何もしていないように見えたので、なんらかデッドロックしているものと考えられます。

また、10万ファイルを処理キューに入れた後失敗したので再試行しようとすると処理キューが開けなくなりCapture Oneが実質使えなくなる状態にまで陥りました。バッチジョブのデータは1件ずつ bplist形式で$HOME/Library/Application Support/Capture One/Batch Queue 13.0 フォルダ以下に保存されるようで、これらを削除するとなんとかジョブデータを初期化できました。

こうした結果を受けて、リサイズ処理については調整をかけずにすべてsipsコマンドでリサイズすることにしました。もう少し時間があればImageMagickとsipsでどちらが良いかなど比較したかったのですが、今回は妥協しました。

まとめ

2ヶ月程度の努力も実って無事、複数あったクラウド上のデータを2カ所にまとめることができました。3.7TBあったデータは1.04TBまで削減できました。 データ容量が減ったこともありますが、プレビューはiPhone・iPadから見違えるほど見やすくなりましたし、編集用ファイルも探しやすくなりました。十分投資対効果があったと感じています。また、この課程でいろいろ過去の写真を振り返るのも楽しい時間でした(このためなかなか整理が進まない)。

残念ながら重複排除などで利用したプログラムはあまり汎用性が高くないので、現時点では公開には耐えられないのですが、また別の機会に整理整頓が必要となった際には調整して公開したいと思っています。

No more user tracking on this site: Removed Google Analytics and Amazon Affiliate

I used to use Google Analytics and Amazon Affiliate links on this blog, but I have removed them. I will no longer track visitors to my site. I have also removed the past Google Analytics logs.

This site currently use cookies only for delivery control, of delivery server Cloudflare’s cookie.

This time it’s not because there was any particular problem to deal with Google Analytics or Amazon Affiliate. But because I didn’t usually use of analytics or affiliates. Additionally, privacy protection would be essential in the near future.

More positively, the site’s display speed improved from a few seconds to about a second when I moved from WordPress to Tumblr and then from Tumblr to Github Pages. This time, I changed the delivery infrastructure from Github Pages to Cloudflare Workers. As a result, the display time became about 0.3 seconds, with the removal of Google Analytics. Finally, the site received a score of 100 in the evaluation by PageSpeed.

Google AnalyticsとAmazon Affiliateのトラッキングを削除しました

本ブログではGoogle AnalyticsとAmazon Affiliateのリンクをいくつか利用していましたが、それらを削除しました。これにより、本サイトにお越しいただいた方を追跡することはなくなりました。また、過去のGoogle Analyticsログについても削除いたしました。

クッキーについては現在は、配信サーバであるCloudflareのクッキーのみでこちらは主に配信制御用のものです。

今回は特に何か問題があったための対処ではなく、もともと分析やアフィリエイトも活用していなかったことと、プライバシー保護は今後基本的なものとなるだろうとの考えからです。

PageSpeedで100点になりました

もっと良い作用としてはサイトの表示スピードが速くなりました。WordPressからTumblrへ移行し、TumblrからGithub Pagesへ移行したことで数秒かかっていた表示が1秒台ぐらいに改善していました。今回配信インフラをGithub PagesからCloudflare Workersに変更したことが大きいですが、最後の一押しとしてGoogle Analyticsの削除でさらに高速化され表示時間は0.3秒程度になり、PageSpeedによる評価も無事100点をいただけました。