2022年12月26日月曜日

Google Oneを安く契約する方法

GoogleOneを 100GB月200円で契約する方法

GoogleOneは契約方法によって値段が違います、まったく同じサービスが提供されるのであれば少しでも安い方が良いです。どの方法で契約しても手間は変わらないのでぜひ安い方法で契約してください。

結論:iPhoneのGmailアプリで契約すると¥200/月

  1. iPhoneのGmailアプリの右上のアカウントアイコンをタップ
  2. 「15GBのストレージのXX%を使用してます」をタップ
  3. 料金が表示されますが、なぜか¥200/月という最安値

Googleフォトアプリ経由だと¥240/月

GoogleOneアプリ経由だと通常価格の¥250/月

2022年11月29日火曜日

Google Formsでドロップダウン形式の日付を毎日変更する

背景

グーグルフォームのGASで自動で毎日ドロップダウン形式の日付が変わる仕組みの質問質問があり、リファレンスマニュアルをみてなんとなくできそうなことはわかっていたが、実際に作ってみた。

ソースコート

function updateForm() {
  // 毎日実行するトリガを設定
  setTrigger( arguments.callee.name );

  var forms = FormApp.getActiveForm();
  var items = forms.getItems();

  var table = [
    {'title':'発症日','start':-14 ,'end':0 }, // 二週間前から今日まで
    {'title':'診察希望日1','start':7 ,'end':1 }, // 7日後から明日まで(逆順)
    {'title':'診察希望日2','start':1 ,'end':7 } // 明日から7日後まで
  ];
  
  // 途中に日付が変わると面倒なのでここで取得
  var today = new Date();
  items.forEach( (item) =>{
    table.forEach( (range) =>{
      // リスト形式で質問がテーブルのtitleと一致したときに処理
      if( item.getType() == FormApp.ItemType.LIST && item.getTitle() == range.title )
      {
        item = item.asListItem();
        var choices = [];

        // テーブルの内容に従い選択肢を追加
        var dir = (range.start<range.end)?1:-1;
        for( var i = range.start; (range.start<=i&&i<=range.end)||(range.end<=i&&i<=range.start); i += dir )
        {
         var day = new Date( today );
          day.setDate(today.getDate() + i);
          choices.push( item.createChoice(formatDate( day, "yyyy/MM/dd" )));
        }
        item.setChoices(choices);
      }
    });

  });
}

function formatDate (date, format) {
  format = format.replace(/yyyy/g, date.getFullYear());
  format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/dd/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/HH/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  format = format.replace(/SSS/g, ('00' + date.getMilliseconds()).slice(-3));
  return format;
}

function setTrigger(name) {
  // 念のため既存のトリガはすべて削除してから次のトリガーを設定する
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( (trigger) =>{
    if( trigger.getHandlerFunction() == name )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  var setTime = new Date();
  setTime.setDate(setTime.getDate() + 1)
  setTime.setHours(12);
  setTime.setMinutes(00); 
  ScriptApp.newTrigger(name).timeBased().at(setTime).create();  
}

フォームに追加したプルダウン



2022年11月20日日曜日

GoogleTakeoutでダウンロードしたGoogleフォト画像のファイルタイムスタンプを修正するプログラム



以前の記事( https://blog.hikozaru.com/2022/11/googletakeoutgoogle.html)で紹介した作業をもう少し簡単に実行できるようにプログラムを作成したのでそのソースコードを公開する。いろいろ手を抜くために邪道なこと(変数名に日本語を使う)を行っているが、気になる人は好きに修正するとよい。プロジェクト全体が欲しい方はここから、実行できるものが欲しい方はここからダウンロードしてください。

takeout機能で複数のZIPファイルに分割された場合にはすべての同一のフォルダに統合してください。****.jpegと****.jpeg.jsonが同一フォルダに存在する場合に処理可能なのですが、****.jpegと****.jpeg.jsonが同一のZIPファイルに格納されていると限りません。

該当のファイルを実行アイコンにドラッグ&ドロップするか、実行して表示されたウインドウにドラッグ&ドロップしてください。


プログラムの大まかな処理の流れは以下です。

  1. 指定されたファイルもしくは指定されたフォルダ配下のファイルすべてを処理
    ただし、JSONファイルは処理をスキップ
  2. ファイル名の後ろに".json"を付加したファイルが存在していたらデシリアライズして撮影日時を取得
  3. 撮影日時はunixtimeなのでutcを経由してローカル時間に変換
  4. ファイルの作成日時と更新日時を変更、その処理が成功したらJSONファイルも同様に作成日時と更新日時を変更
  5. 結果をDataGridに出力
JSONファイルから撮影日時を取り出してファイルのタイムスタンプを修正する処理に関連するコードはほんの一部(コードの背景色を変えた行)です。その他は操作性やエラー発生時の処理などを考慮したコードです。

まだ、大量データでのテストは実施していませんが、DataGridへの出力がボトルネックとなる可能性があります。もしかしたら処理成功したファイルの表示は辞めるとか、そもそもDataGridへの出力は辞めて結果をログファイルに出力の方が良いかもしれません。


まずはCSファイル

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Windows;
namespace PhotosTimestamp
{
    [DataContract]
    public class ImageInfo
    {
        [DataMember(Name = "photoTakenTime")]
        public PhotoTakenTime PhotoTakenTime { get; set; }
    }
    [DataContract]
    public class PhotoTakenTime
    {
        [DataMember(Name = "timestamp")]
        public string timstamp { get; set; }
    }

    public partial class MainWindow : Window
    {
        enum E_RESULT
        {
            更新,
            失敗,
            SKIP
        };

        class Result
        {
            public int 番号 { get; set; }
            public E_RESULT 結果 { get; set; }
            public string ファイル名 { get; set; }
            public string エラーメッセージ { get; set; }
            public string 例外メッセージ { get; set; }
        }

        const string HowToUse
            = "Google TakeoutでダウンロードしたZIPファイルを展開し、展開されたファイル/フォルダをドラッグ&ドロップして下さい。\n"
            + "コマンドライン引数やプログラムアイコンへのドラッグ&ドロップでもファイル/フォルダ指定可能です。\n"
            + "画像ファイルにJSONファイル(例:IMG_1234.PNGに対しIMG_1234.PNG.json)がある場合に処理を行います。\n"
            + "TakeoutでZIPファイルが分割された場合には一つのフォルダに統合してください、画像ファイルとJSONファイルが同一のZIPに格納されるとは限りません。\n"
            + "JSONファイルに記録されているphotoTakenTime.timestampをLocal時間に変換し、画像ファイルとJSONファイルの作成日時と更新日時に設定します。";

        public MainWindow()
        {
            InitializeComponent();
            var resultList = new ObservableCollection();
            resultDataGrid.ItemsSource = resultList;
        }

        // 引数で指定されたときは即実行
        private void Window_ContentRendered(object sender, EventArgs e)
        {
            string[] args = Environment.GetCommandLineArgs();
            if (args.Length > 1)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(WorkerThread));
                thread.Start(args.Skip(1).ToArray());
            }
            else
            {
                ddPanel.AllowDrop = true;
                status.Content = HowToUse;
            }
        }

        // Drag&Drop関連処理
        private void ddPanel_PreviewDragOver(object sender, DragEventArgs e)
        {
            e.Effects = DragDropEffects.None;
            if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop, true))
            {
                e.Effects = DragDropEffects.Copy;
                e.Handled = e.Data.GetDataPresent(DataFormats.FileDrop);
            }
        }
        private void ddPanel_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                Thread thread = new Thread(new ParameterizedThreadStart(WorkerThread));
                thread.Start(e.Data.GetData(DataFormats.FileDrop));
            }
        }


        // UIが固まらないようにThreadで処理
        void WorkerThread(object arg)
        {
            Dispatcher.Invoke((Action)(() =>
            {
                ddPanel.AllowDrop = false;
                status.Content = "処理開始";
                var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                dataList.Clear();
            }));
            DoEntries((string[])arg);
            Dispatcher.Invoke((Action)(() =>
            {
                ddPanel.AllowDrop = true;
                status.Content = "処理が終了しました。\n" + HowToUse;
            }));
        }
        void DoEntries(string[] args)
        {
            foreach (string arg in args)
            {
                if (File.Exists(arg) == true)
                {
                    DoFile(arg);
                }
                else
                {
                    try
                    {
                        foreach (var path in Directory.GetFiles(arg, "*", System.IO.SearchOption.AllDirectories))
                        {
                            DoFile(path);
                        }
                    }
                    catch (Exception e)
                    {
                        Dispatcher.Invoke((Action)(() =>
                        {
                            var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                            dataList.Add(new Result() { 番号=dataList.Count+1, 結果=E_RESULT.失敗, ファイル名 = arg, エラーメッセージ="ファイルまたはフォルダが見つかりませんでした。",例外メッセージ = e.ToString() });
                        }));
                    }
                }
            }
        }

        void DoFile(string targetFilename)
        {
            var jsonFilename = targetFilename + ".json";
            if (string.Compare(".json", System.IO.Path.GetExtension(targetFilename), true) == 0)
            {
                Dispatcher.Invoke((Action)(() =>
                {
                    status.Content = targetFilename;
                }));
            }
            else if (System.IO.File.Exists(jsonFilename))
            {
                DateTime photoTakenTime;
                try
                {
                    using (var stream = new FileStream(jsonFilename, FileMode.Open, FileAccess.Read))
                    {
                        var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(ImageInfo));
                        var imageInfo = (ImageInfo)serializer.ReadObject(stream);
                        var unixtime = imageInfo.PhotoTakenTime.timstamp;
                        photoTakenTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(unixtime)).LocalDateTime;
                    }
                    var imageFile = new System.IO.FileInfo(targetFilename);
                    var jsonFile = new System.IO.FileInfo(jsonFilename);

                    var results = new Result[] {
                        new Result(){ 結果=E_RESULT.SKIP,ファイル名=targetFilename,エラーメッセージ="",例外メッセージ="" },
                        new Result(){ 結果=E_RESULT.SKIP,ファイル名=jsonFilename,エラーメッセージ="画像ファイルの更新に失敗したときはJSONファイルも更新しません。",例外メッセージ="" }
                    };

                    foreach (var result in results)
                    {
                        try
                        {
                            var file = new System.IO.FileInfo(result.ファイル名);
                            file.CreationTime = photoTakenTime;
                            file.LastWriteTime = photoTakenTime;
                            result.結果 = E_RESULT.更新;
                            result.エラーメッセージ = "";
                            result.例外メッセージ = "";
                        }
                        catch (Exception e)
                        {
                            result.結果 = E_RESULT.失敗;
                            result.エラーメッセージ = "タイムスタンプの更新に失敗しました。";
                            result.例外メッセージ = e.ToString();
                            break;
                        }
                    }
                    Dispatcher.Invoke((Action)(() =>
                    {
                        status.Content = targetFilename;
                        var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                        foreach (var result in results)
                        {
                            result.番号 = dataList.Count + 1;
                            dataList.Add(result);
                        }
                    }));
                }
                catch(Exception e)
                {
                    Dispatcher.Invoke((Action)(() =>
                    {
                        status.Content = targetFilename;
                        var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                        dataList.Add(new Result() { 番号 = dataList.Count + 1, 結果 = E_RESULT.失敗, ファイル名 = jsonFilename, エラーメッセージ = "タイムスタンプの取得に失敗しました。", 例外メッセージ = e.ToString() });
                    }));
                }
            }
            else
            {
                Dispatcher.Invoke((Action)(() =>
                {
                    status.Content = targetFilename;
                    var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                    dataList.Add(new Result() { 番号 = dataList.Count + 1, 結果 = E_RESULT.SKIP, ファイル名 = targetFilename, エラーメッセージ = "*.JSONファイルがありません。", 例外メッセージ = "" });
                }));
            }
        }
    }
}
次はXAMLファイル
<window contentrendered="Window_ContentRendered" height="450" mc:ignorable="d" title="Google Takeoutでダウンロードした画像のタイムスタンプをJSONファイルを参考に修正します。" width="1200" x:class="PhotosTimestamp.MainWindow" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:PhotosTimestamp" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <dockpanel allowdrop="false" drop="ddPanel_Drop" name="ddPanel" previewdragover="ddPanel_PreviewDragOver">
        <label dockpanel.dock="Bottom" name="status">
        <datagrid dockpanel.dock="Top" isreadonly="True" name="resultDataGrid">
    </datagrid></label></dockpanel>
</window>

2022年11月14日月曜日

GoogleTakeoutでダウンロードしたGoogleフォト画像のファイルタイムスタンプを修正する

22022/11/20追記:PowerShellは難しいという方は次の記事で専用のプログラムをダウンロードできるようにしています。

概要

Googleフォトのデータをtakeout(https://takeout.google.com/?hl=ja)機能でダウンロードするとファイルのタイムスタンプがダウンロード実施した日になってしまう。Takeoutで出力したデータに含まれているJSONデータに撮影日時が記録されているので、その情報をもとに修正する方法を説明します。Windowsパソコンが必要です。
なお、takeout機能で複数のZIPファイルに分割された場合にはすべての同一のフォルダに統合してください。****.jpegと****.jpeg.jsonが同一フォルダに存在する場合に処理可能なのですが、****.jpegと****.jpeg.jsonが同一のZIPファイルに格納されていると限りません。

①PowerShellのスクリプトを実行できるようにする

PowerShell 管理者権限として実行します。ウインドウズメニューを開いた状態で「PowerSell」と入力すると見つかるので、「管理者として実行する」を選択してください。

以下のコマンドを実行し現在の権限を確認しメモしておいてください。

Get-ExecutionPolicy

以下のコマンドを実行して権限を変更してください。

Set-ExecutionPolicy RemoteSigned

変更してよいか確認されるので Y キーを押してください。

なお、権限(RestrictedやRemoteSignedなどの)の詳細は以下を参照してください。

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3#powershell-execution-policies


②スクリプトを作成

拡張子を表示した状態で「CorrectTimestamp.ps1」というファイルを作成しメモ帳で以下の様に編集してください。

$files = Get-ChildItem $PSScriptRoot -File -Recurse -Exclude *.json,*.html,*.ps1,*.zip
foreach($i in $files){
  Write-Host $i.FullName
  $jsondata = Get-Content -Path $i".json" | ConvertFrom-JSON
  $j = $i.FullName+".json"
  
  $UnixTime = $jsondata.photoTakenTime.timestamp.Trim('"')
  $UtcTime = ([DateTime]::Parse("1970/01/01 00:00:00")).addSeconds($UnixTime)
  $LocalTime= [TimeZoneInfo]::ConvertTimeFromUtc($UtcTime,[TimezoneInfo]::local)

  Set-ItemProperty $i -name CreationTime -value $LocalTime
  Set-ItemProperty $j -name CreationTime -value $LocalTime
  Set-ItemProperty $i -name LastWriteTime -value $LocalTime
  Set-ItemProperty $j -name LastWriteTime -value $LocalTime
}
Write-Host "Processing has finished. Please press any key. . ." -NoNewLine
[Console]::ReadKey($true) > $null

JSON内にはいくつかの日時が記録されております、上記のスクリプトではphotoLastModifiedTimeを更新日時に設定していますが、photoTakenTimeを採用するなど好みに合わせて変更してください。更新日時もphotoTakenTimeにしています。


③Zipファイルの展開とスクリプトの配置

Takeout機能でダウンロードしたZIPファイルを全て展開し、展開されたフォルダに「CorrectTimestamp.ps1」をコピーしてください。


④スクリプトの実行

「CorrectTimestamp.ps1」ファイルを右クリックして「PowerShellで実行」を行うとタイムスタンプが修正されます。


その他

JSONファイルに記録されているデータを使ってファイル名を変えたりすることもできるが、PowerShellでなくWSH(Windows Script Host)などでも処理できると思います。




2022年10月9日日曜日

バッチファイル(*.bat)でファイルをゴミ箱に入れる

バッチファイルでファイルを直接削除するのは怖いのでゴミ箱を経由する

バッチファイルで直接ゴミ箱に入れることはできないので、WSH経由で削除します。

PowerShellを使う方法もありますが、セキュリティ的に使えない事が多いのでWSHを選択しました。
また、そもそも全てをWSHで実現すれば良いと思いますが、BATファイルの方が楽なケースも多いので結構役に立ちます。

sample.bat (バッチファイルサンプル)
@echo on
CScript.exe trash.js c:\folder\test1.txt c:\folder\test2.txt
trash.js
var args = WScript.Arguments;
var sa = WScript.CreateObject("Shell.Application");
var fso = WScript.CreateObject("Scripting.FileSystemObject");

for (var i = 0; i < args.length; i++ ) {
  arg = args(i);
  WScript.Echo(arg);
  sa.NameSpace(10).MoveHere( arg );
  while( fso.FileExists( arg ) ){
    WScript.Sleep(100);
  }
}

// ゴミ箱の取得(NameSpace関数の10の意味)
// https://learn.microsoft.com/ja-jp/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
// WScript.Echo( sa.NameSpace(10).Title );

今後改善したい項目

  • エラー処理
  • 相対パス指定対応

2022年8月28日日曜日

WSH(JScript)で*.xlsmを*.xlsxに変換する

マクロを有効にした XML ブック(*.xlsm)を既定のXML ブック(*.xlsx)に変換する。

xlsm2xlsx.jsのアイコンに変換したい *.xlsm ファイルを複数Drag&Dropすると *.xlsx ファイルを生成する。

xlsm2xlsx.js

var xlWorkbookDefault = 51;

var ExcelApp = new ActiveXObject( "Excel.Application" );

// *.jsファイルにDrag&Dropされたファイルを順番に処理
var args = WScript.Arguments;
for (var i = 0; i < args.length; i++ ) {
  var orgFilename = args(i);

  // 特定の拡張子のみ処理を実施
  var orgExtension = orgFilename.split('.').pop();
  if( orgExtension == 'xlsm' ){
    var baseName = orgFilename.substring(0, orgFilename.indexOf('.'+orgExtension))

    // ファイルを開いて形式を変更して保存
    var book = ExcelApp.Workbooks.Open( orgFilename );
    ExcelApp.DisplayAlerts= false;
    book.SaveAs( baseName + '.xlsx' ,  xlWorkbookDefault );
    ExcelApp.DisplayAlerts= true;
    ExcelApp.Quit();
  }
}
WScript.Echo( "finish" );
ExcelApp = null;

参考

https://docs.microsoft.com/ja-jp/office/vba/api/excel.xlfileformat

https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbook.saveas

2022年7月3日日曜日

HTML+CSSでトグルボタン

すぐに忘れるので画像も文字列も変化するトグルボタン。デザインは適当。

まずはコードから

<!DOCTYPE html>
<html>
<head>
  </head>
  <body>
    <style>
      .btn,.btn:checked+label,.btn+label+label
      {
        display:none;
      }
      .btn+label,.btn:checked+label+label
      {
        display:inline-block;
        user-select: none;
        background-repeat: no-repeat;
      }
      .btn+label
      {
          background-color:aqua;
      }
      .btn:checked+label+label
      {
        background-color:pink;
      }
      .btn:active+label,
      .btn:active+label+label
      {
        background-color: lightgray;
      }
  
      .vertical+label,.vertical+label+label
      {
        padding-top: 2em;
        background-position:top;
        background-size: auto 2em;
      }
      .horizontal+label,.horizontal+label+label
      {
        padding-left: 2em;
        background-position:left;
        background-size: 2em auto;
      }
      .image+label,.image+label+label
      {
        height: 2em;
        width: 2em;
        background-position:center;
        background-size: contain;
        text-indent:100%;
        white-space:nowrap;
        overflow:hidden;
      }
      .text+label,.text+label+label
      {
        background-position:center;
        background-size: 0 0;
      }
    </style>
      <table>
      <TR><TH>ボタン</TH><TH>トグル</TH></TR>
      <TR><TD>
        <input class="btn vertical" type="button" id="1"/>
        <label for="1" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn horizontal" type="button" id="2"/>
        <label for="2" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn image" type="button" id="5"/>
        <label for="5" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn text" type="button" id="6"/>
        <label for="6" style="background-image:url(send.png)">メール送信</label>
        <hr>
      </TD>
      <TD>
        <input class="btn vertical" type="checkbox" id="3"/>
        <label for="3" style="background-image:url(opened.png)">既読メール</label>
        <label for="3" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn horizontal" type="checkbox" id="4" checked/>
        <label for="4" style="background-image:url(opened.png)">既読メール</label>
        <label for="4" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn image" type="checkbox" id="7"/>
        <label for="7" style="background-image:url(opened.png)">既読メール</label>
        <label for="7" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn text" type="checkbox" id="8"/>
        <label for="8" style="background-image:url(opened.png)">既読メール</label>
        <label for="8" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
    </TD></TR>
    </table>
  </body>
<html>

動作は以下のような感じ

普通のボタントグルするボタン








2022年5月28日土曜日

GASでドライブの画像を表示する

知恵袋への回答は質問に直接答えたが、画面ロード時に時間がかかりそうなので処理を全体的に変えてみた。


コード.gs
function doGet(e) {
  return HtmlService.createTemplateFromFile("index.html").evaluate().setTitle("タイトル");
}

function GetBase64Image(fileId,param)
{
  var file = DriveApp.getFileById(fileId);
  var blob = file.getBlob();
  var contentType = blob.getContentType();
  var base64 = Utilities.base64Encode(blob.getBytes());
  var imageSrc = "data:" + contentType + ";base64, " + base64;
  return {"name":file.getName(),"id":fileId,"base64":imageSrc,"param":param};
}
index.html
<html>
<head>
  <script>
    <?
      var folder = DriveApp.getFolderById("1MdeTdaYDv9BK9QTaZes5_bTDRpKiLRnA");
      var files = folder.getFiles();
      var list = [];

      while (files.hasNext()) {
        var file = files.next();
        list.push({"id":file.getId(),"name":file.getName(),"url":file.getUrl()});
      }
    ?>
    var list = <?!= JSON.stringify(list) ?>;
  
    function onSelected(event)
    {
      var img = document.getElementById("image")
      img.alt = "Loading....";
      img.src = null;
      var fileId = event.currentTarget.value;
      google.script.run
        .withSuccessHandler( SuccessGetBase64Image )
        .withFailureHandler( Failure )
        .GetBase64Image( fileId, img.id );
    }
    function Failure(){
      alert("失敗");
    }
    function SuccessGetBase64Image(entry)
    {
      document.getElementById(entry.param).alt = entry.name;
      document.getElementById(entry.param).src = entry.base64;
    }

    function onLoad()
    {
      var select = document.getElementById("select");
      select.addEventListener('change', onSelected);
      list.forEach( file => {
        var option = document.createElement("option");
        option.value = file.id;
        option.innerText = file.name;
        select.appendChild(option);
      });
    }
  </script>
</head>
<body onload="onLoad();">

<label for="select">Choose a menu:</label>
<select name="select" id="select">
<option value="">--Please choose an option--</option>
</select>

<p><img src="" height="562" width="750" alt="選択されてません" align="top" id="image">ここに画像表示</p>
</script>

</body>
</html>

2022年5月14日土曜日

JavaScriptで○×ゲームを作ってみた

JavaScriptでcanvasを使って、三目並べの○×ゲームを作ってみた。
canvasで作るよりもTableやButtonで作った方が大分楽。
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <script>
      let canvas;
      let context;
      let Magnification = 100;
      let Finished = false;
      let FirstStrike=true;
      let MarubatsuTable={};
      function drawFrame()
      {
        // #を描画
        context.beginPath();
        context.moveTo(Magnification*0, Magnification*1);
        context.lineTo(Magnification*3, Magnification*1);
        context.moveTo(Magnification*0, Magnification*2);
        context.lineTo(Magnification*3, Magnification*2);
        context.moveTo(Magnification*1, Magnification*0);
        context.lineTo(Magnification*1, Magnification*3);
        context.moveTo(Magnification*2, Magnification*0);
        context.lineTo(Magnification*2, Magnification*3);
        context.stroke();
      }
      function onClick(e)
      {
        if( Finished )
        {
          // ゲームが終わっている場合はリセット
          context.clearRect(0, 0, canvas.width, canvas.height);
          drawFrame();
          MarubatsuTable = {};
          Finished = false;
          FirstStrike = true;
          return;
        }
        // クリックされたセルを検出
        let rect = e.target.getBoundingClientRect();
        let x = Math.floor( (e.clientX - rect.left) / Magnification );
        let y = Math.floor( (e.clientY - rect.top) / Magnification );
        let id = "X"+ x + "Y" + y;
        if( !MarubatsuTable[id]  )
        {
          if( FirstStrike )
          {
            MarubatsuTable[id] = "○";
            context.beginPath();
            context.arc( (x+0.5)*Magnification, (y+0.5)*Magnification, Magnification/3 , 0, 2 * Math.PI, false ) ;
            context.stroke();
          }
          else
          {
            MarubatsuTable[id] = "×";
            context.beginPath();
            context.moveTo((x+0.2)*Magnification, (y+0.2)*Magnification);
            context.lineTo((x+0.8)*Magnification, (y+0.8)*Magnification);
            context.moveTo((x+0.8)*Magnification, (y+0.2)*Magnification);
            context.lineTo((x+0.2)*Magnification, (y+0.8)*Magnification);
            context.stroke();
          }
          // 結果判定
          const decisionTable =
            [ {dicision:["X0Y0","X1Y0","X2Y0"],line:{x0:0,y0:0.5,x1:3,y1:0.5}},
              {dicision:["X0Y1","X1Y1","X2Y1"],line:{x0:0,y0:1.5,x1:3,y1:1.5}},
              {dicision:["X0Y2","X1Y2","X2Y2"],line:{x0:0,y0:2.5,x1:3,y1:2.5}},
              {dicision:["X0Y0","X0Y1","X0Y2"],line:{x0:0.5,y0:0,x1:0.5,y1:3}},
              {dicision:["X1Y0","X1Y1","X1Y2"],line:{x0:1.5,y0:0,x1:1.5,y1:3}},
              {dicision:["X2Y0","X2Y1","X2Y2"],line:{x0:2.5,y0:0,x1:2.5,y1:3}},
              {dicision:["X0Y0","X1Y1","X2Y2"],line:{x0:0,y0:0,x1:3,y1:3}},
              {dicision:["X2Y0","X1Y1","X0Y2"],line:{x0:0,y0:3,x1:3,y1:0}} ];
          decisionTable.forEach((a)=>
          {
            var temp = MarubatsuTable[a.dicision[0]] + MarubatsuTable[a.dicision[1]] + MarubatsuTable[a.dicision[2]];
            if( temp == "○○○" || temp == "×××" )
            {
              context.beginPath();
              context.moveTo( a.line.x0*Magnification, a.line.y0*Magnification);
              context.lineTo( a.line.x1*Magnification, a.line.y1*Magnification);
              context.stroke();
              Finished = true;
            }
            if( Object.keys(MarubatsuTable).length == 9 )
            {
              Finished = true;
            }
          });
          // 先攻後攻変更
          FirstStrike = !FirstStrike;
        }
      }
      function onLoad(){
        canvas = document.getElementById('sampleCanvas');
        context = canvas.getContext('2d');
        if ( ! canvas || ! context ) {
          return false;
        }
        canvas.style.width = Magnification * 3+"px";
        canvas.style.height = Magnification * 3+"px";
        canvas.width = Magnification * 3;
        canvas.height = Magnification * 3;

        drawFrame();
        canvas.addEventListener('click', onClick, false);
      }
    </script>
  </head>
  <body onload="onLoad();">
    <canvas id="sampleCanvas"></canvas>
  </body>
</html>

おまけ:Canvas縛りがない場合はもっと簡単

<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <script>
      let FirstStrike=true;
      let Finished = false;
      let Count = 0;
      function onClick(event){
        // 勝敗決定後のクリックは結果クリア
        if( Finished )
        {
          for (const td of event.currentTarget.querySelectorAll("td")) {
            td.innerText = " ";
            td.style.backgroundColor = "";
          }
          FirstStrike=true;
          Finished = false;
          Count = 0;
          return;
        }

        // クリックされたセルに"○"/"×"を記録
        if(event.target.tagName=="TD")
        {
          if( event.target.innerText==" " ){
            event.target.innerText = FirstStrike?"○":"×";
            FirstStrike =! FirstStrike;
            Count++;

            // 結果判定
            const decisionTable =
              [ ["00","01","02"],
                ["01","11","12"],
                ["02","21","22"],
                ["00","10","20"],
                ["01","11","21"],
                ["02","12","22"],
                ["00","11","22"],
                ["20","11","02"] ];
            decisionTable.forEach((a)=>
            {
              var temp = document.getElementById(a[0]).innerText+document.getElementById(a[1]).innerText+document.getElementById(a[2]).innerText;
              if( temp == "○○○" || temp == "×××" )
              {
                Finished = true;
                document.getElementById(a[0]).style.backgroundColor = "lightblue"; 
                document.getElementById(a[1]).style.backgroundColor = "lightblue"; 
                document.getElementById(a[2]).style.backgroundColor = "lightblue"; 
              } 
            });
            if( Count >=9 )
            {
              Finished = true;
            }
          }
        }
      }
    </script>
  </head>
  <body>
    <table onclick="onClick(event);" border="1">
      <tr><td id="00"> </td><td id="01"> </td><td id="02"> </td></tr>
      <tr><td id="10"> </td><td id="11"> </td><td id="12"> </td></tr>
      <tr><td id="20"> </td><td id="21"> </td><td id="22"> </td></tr>
    </table>
  </body>
</html>

2022年4月24日日曜日

Googleフォトの「ロックされたフォルダ」の使い方と注意事項

  1. 「ロックされたフォルダ」の概要
    • 「ロックされたフォルダ」に入れるとロック解除しないと画像が閲覧できなくなります。他人に見られたくない画像などを保存する目的だと思います。
    • しかし、ゴミ箱を経由せずに消えるなどいろいろと危険な使用があるので大切な画像を「ロックされたフォルダ」に入れてはいけません。
    • 大切ではないけど他人には見られたくない画像を保存するのが「ロックされたフォルダ」の用途です。
  2. 「ロックされたフォルダ」の使い方
    • 入れる方法
      Googleフォトアプリで写真を表示/写真を選択し、「︙」アイコンから「ロックされたフォルダに移動」を行う。
    • 参照する方法
      「ロックされたフォルダ」に入れる操作を行った端末のGoogleフォトアプリで 「ライブラリ」 > 「ユーティリティ」 > 「ロックされたフォルダ」 と選んでください。ロック解除はスマホのロック解除と同じ方法です。
    • 出す方法
      「ロックされたフォルダ」内の画像を選択して「移動」してください。
  3. ロックされたフォルダに入れるとどうなるか
    • 画像が端末にダウンロードされ、Googleフォトアプリの「ロックされたフォルダ」に保存される。
    • 操作を行ったときに選択されていたアカウントのGoogleフォト( https://photos.google.com/login )から写真が削除される。
  4. ロックされたフォルダを使用するうえでの注意事項 
    • 「ロックされたフォルダ」内での削除操作はゴミ箱を経由しないので復元できない。
    • 操作を行った端末のみに保存されるので端末故障/端末紛失/機種変更/端末初期化の際には画像がなくなる
    • 操作を行ったときに選択されていたアカウントのGoogleフォト( https://photos.google.com/login )から写真が削除されるが、他のアカウントからは消えない、パートナー共有などで複数のアカウントに画像があるときはすべてのアカウントで「ロックされたフォルダ」に入れる必要がある。
    • 他のアプリから参照できなくなる。
    • 「ロックされたフォルダ」から出すと再度バックアップ対象になるので、2021/5/31以前にバックアップし容量無制限の対象だった画像も容量カウントの対象になる。
    • 撮影日時が保存されていない画像形式(PNG形式で保存されたスクリーンショット画像)はロックされたフォルダに入れるとファイルタイムスタンプが更新されていつ撮影されたか分からなくなる。
  5. ロックされたフォルダの活用方法
    1. Pixel 5以前の端末など、節約画質で容量無制限の特典が利用できる端末で「ロックされたフォルダ」に入れた後に、「ロックされたフォルダ」から出すと特典が適用される。iPhoneなど対象外の端末からバックアップした画像を無制限の対象に変換できる。
    2. AndroidのGoogleフォトアプリには画像を一括ダウンロードする方法が提供されていないが、「ロックされたフォルダ」には一括でダウンロードできる。「ロックされたフォルダ」に入れ、すぐに出すことで一括ダウンロードが可能になるが、、、再度バックアップが行われるので注意が必要

AndroidのGoogleフォトアプリを再インストールする方法


 Googleフォトは基本的にクラウドにデータを保存しているため、Googleフォトアプリを再インストールしても問題ないと思いがちですが、以下のデータ消えるので注意が必要です。

  • アカウントにログインするための情報
  • バックアップと同期設定
  • ロックされたフォルダの画像

Googleフォトアプリを再インストール手順を示します。流れとしては以下になります。

  1. アカウント確認
  2. 前アカウントへのログイン確認
  3. Googleフォトアプリの設定確認
  4. バックアップと同期オフ設定
    (ロックされたフォルダのあったデータをバックアップされないようにする)
  5. ロックされたフォルダから画像移動
  6. アプリの削除
  7. アプリの再インストール
  8. ロックされたフォルダの復元
  9. バックアップと同期やその他の設定の復元

上記で操作できる人は移行は読まなくてもよいです。勘違いする人が多かったり、間違えてほしくない部分だけ画像を貼ってます。画像を貼ってほしい手順があればコメントください。

  1. アカウント確認
    Googleフォトアプリの右上のアカウントアイコンをタップし、表示されたメニューのアカウントの右側をタップして登録しているアカウントを全てメモする


  2. 前アカウントへのログイン確認
    ChromeブラウザのシークレットモードでGoogleフォト( https://photos.google.com/login )にアクセスして上記でメモしたアカウントすべてでログイン操作を行い、以下のことを確認する。
    ・左上が「≡」アイコンである事(アプリに飛ばされていないことの確認)
    ・今までバックアップしていた画像が存在する事
    ・全てのアカウントにログインできる事を確認する



  3. Googleフォトアプリの設定確認
    Googleフォトアプリの右上のアカウントアイコンをタップし、「フォトの設定」に行き、設定をメモする。


  4. バックアップと同期オフ設定
    「フォトの設定」 > 「バックアップと同期」画面に行き、バックアップと同期をオフにする。



  5. ロックされたフォルダから画像移動
    「ライブラリ」 > 「ユーティリティ」 > 「ロックされたフォルダ」を開き、そこにある画像すべてを移動する。ここで「削除」を押すと復元できないので要注意。
    どんな画像があったか覚えておく、写真の枚数が多い場合には撮影日時などをメモしておくとよい。

  6. アプリの削除
    Googleフォトアプリを削除(アンインストール)する。

  7. アプリの再インストール
    Play StoreアプリでGoogleフォトを検索してインストールを行う。インストールの途中でバックアップと同期を有効にする手順があった場合には有効にしないように注意する。

  8. ロックされたフォルダの復元
    フォトの中からロックされたフォルダに移動したい写真を選択して、ロックされたフォルダに移動する。

  9. バックアップと同期やその他の設定の復元
    アプリの設定をすべて元に戻す。

2022年4月18日月曜日

ネットワーク速度の確認方法

1. https://www.google.co.jp/にアクセスする

2. 「スピードテスト」を検索する

3. 速度テストを実行






2022年3月21日月曜日

G Suite アカウントを無料で継続利用する方法

Google Workspaceに課金せずデータを引き継ぐ


G Suite Legacy(無料版Google Workspace)から有料Google Workspaceに移行せずにアカウントを維持する方法をサポートに教えてもらったので共有します。
メールは他のサーバーに移行すれば良いので、YouTube、Google フォト、Google Play、有料コンテンツが引き続き利用できるのは非常に大きいが、ヘルプにより記載内容が異なっているので問い合わせてみました。
問い合わせた結果から実際に試してみました。(2022/3/23)
他の方法も用意されているようなのでもう少し様子見がよさそうです。:2022/4/29追記


背景

まず公式ヘルプ間の内容からおさらいです。 「従来の無償版 G Suite からのアップグレード」によると2022/5以降に自動で有料のGoogle Workspaceに移行し、2022/7/1までは無料で、それ以降は有料になります。
ただし、有料に移行したくなければGoogle Workspace のコアサービス(Gmail、カレンダー、Meet など)が利用できない無料アカウントを引き続き利用できると書かれています。

問い合わせた内容

有料に移行せずに無料のままその他の Google サービス(YouTube、Google フォト、Google Play など)や、有料コンテンツ(YouTube や Play ストアでの購入など)を引き続き利用する方法お問い合わせてみました。

回答してもらった内容

・「有償版 Google Workspace のお支払い設定を未実施」であったとしてもYouTube、Google フォト、Google Play 、Google ドライブ、ドキュメント、スプレッドシート、スライド、Keep、Meetは継続利用可能。(その他のサービスに関しての質問はあきらめました、知りたいサービスがある場合には有料サービスに契約したうえで、サポートに問い合わせてください。)

・ただし、Google Workspace の支払い設定が未実施の状態だと放置アカウントとみなされて削除される可能性があるので「Cloud Identity Free エディション」の追加をお勧めされました。

・さらに、Google Workspace支払い設定が未実施の状態だと管理コンソールで毎回支払い設定を促されるので、「Cloud Identity Free エディション」を追加をするために一旦Google Workspace のサブスクリプションを契約したうえで、「Cloud Identity Free エディション」を追加後にサブスクリプションを解約することを推奨する、とのことです。

・Cloud Identity については「Cloud Identity とは」に書いてある。

・2022/3/2時点で問い合わせ完了

アカウントを削除されずにアカウントを使用し続ける方法

Cloud Identity Freeエディションを追加するためには一度Google Workspaceの有料サブスクリプションにアップグレードする必要があります。Cloud Identity Freeエディションを追加後にGoogle Workspace サブスクリプションをキャンセルします。

1.Google Workspace の有料サブスクリプションへのアップグレード手順

管理コンソール > お支払い >  その他のサービスを利用する > 左側 Google Workspace Business Starterの切り替えるをクリック > 開始 > フレキシブルプランを選択してご購入手続きをクリック


2.Cloud Identity Free エディションの追加手順

管理コンソール > お支払い >  その他のサービスを利用する > 左側 Cloud Identity クリック > Cloud Identity Free を開始

3.Google Workspace サブスクリプションのキャンセル手順

管理コンソール > お支払い > サブスクリプションを管理 > サブスクリプションをキャンセル

使える機能、使えなくなる機能

使えなくなるのはコアサービスです、コアサービスに関しては「Google Workspace 利用規約」を見て下さい。しかし、「従来の無償版 G Suite からのアップグレード」や「Cloud Identity とは」の記載内容を見る限りコアサービス全てが使えなくなるんけでは無さそうですし、ヘルプにより記載内容も異なるのでもう少し調査しました。

Cloud Identity Free Editionで使えない機能

実際に使用できないことを確認したサービス。
  • Gmail
  • カレンダー
  • Google Meet(会議開催できない):2022/4/29追記
  • Google Chat
  • Currents
  • Google Jamboard
  • Google ToDo リスト
  • Workspace アドオン
  • Google Voice

Cloud Identity Free Editionで使える機能

Google Adminコンソールで有効/無効設定が可能なサービスおよび動作確認を行ったサービス。(*を付けた行は実際に使えることを確認した項目)
使えても機能制限がある可能性もあるようですので、例えばMeetは使えると回答をもらい、実際に会議にも参加できましたが、参加専用になるようです。:2022/4/29追記
  • Googleサイト*
  • Keep*
  • ドライブとドキュメント*
    • ドキュメント*
    • スプレッドシート*
    • スライド*
    • Forms*
  • ビジネス向けGoogleグループ*
  • Blogger
  • Chrome Web Store*
  • Colab*
  • FeedBurner
  • Google AdSense
  • Google Arts & Culture
  • Google Bookmarks
  • Google Chrome 同期
  • Google Cloud Platform
  • Google Developers
  • Google Domains
  • Google Earth*
  • Google Fi
  • Google Meet* (会議への参加のみ可能):2022/4/29追記
  • Google My Maps
  • Google Pay
  • Google Play
  • Google Play Console
  • Google Play ブックスパートナーセンター
  • Google Public Data
  • Google Search Console
  • Google Translator Toolkit
  • Google Trips
  • Google Voice
  • Google アドマネージャー
  • Google アナリティクス
  • Google アラート
  • Google カスタム検索
  • Google クラウドプリント
  • Google グループ
  • Google コンタクト
  • Google データエクスポート
  • Google データポータル
  • Google ニュース*
  • Google フォト*(データが引き継がれることを確認しました)
  • Google ブックス
  • Google マイビジネス
  • Google マップ*
  • Google 広告
  • Google 翻訳
  • Location History
  • Managed Google Play
  • Material Gallary
  • Merchant Center
  • Partner Dash
  • Pinpoint
  • Scholar プロフィール
  • Tour Creator
  • YouTube
  • ウェブとアプリのアクティビティ
  • キャンペーンマネージャー
  • スタジオ
  • 応用デジタルスキル
  • 検索とアシスタント
  • 検索高校360
  • 個人用ストレージ
  • Cloud Identity サービス
  • Google CloudでCloud Identity アカウントを利用可能
  • ChromeでCloud Identity アカウントを利用可能
  • Android Enterprise Upgrade
  • Androidの管理
  • 数多くのサードパーティ製アプリケーション


ヘルプの記載から使えるか不明な機能

  • Currents
  • Google Cloud Search
  • Google Vault
  • Google Workspace Assured Controls
  • 検索およびインテリジェンスの機能

無償で使用し続けるお勧めの手順

他の方法も用意されているようなのでもう少し様子見がよさそうです。:2022/4/29追記

  1. G Suite経由でGoDaddyで購入したドメインを使用している場合には念のため他のレジストラに管理を移管する。(必須かどうか不明だが、メールサーバー移管と合わせて移管先レジストラを考えた方が良い。)
  2. メールを別サーバーに移行する。
  3. @gmail.com アドレスを取得し、2のメールを転送する、2のSMTPサーバー経由でメールを送れる様に設定する。(移行先サーバーのメールボックス容量が潤沢であれば不要)
  4. 無料期間中(~2022/6/30)にGoogle Workspace Business Xxxx のフレキシブルプランにアップグレードする。(Enterpriseを選んだり年間プランを選ぶと有料になるので注意が必要)。
  5. Cloud Identity Freeエディションを追加する。
  6. Google Workspace 有料サブスクリプションをキャンセルする。

注意事項:

  • カレンダーは使用できないので@gmail.comで使用する
  • カレンダーに追加されているイベントを削除できなくなるという情報を得たので、事前に削除しておくことをお勧めします。:2022/4/29追記
  • Meetは参加専用になるようです。その他の機能も「使える」けど制限がある可能性がありそうです。:2022/4/29追記

2022年3月5日土曜日

GoogleのLens機能で読み取った文字列をパソコンにコピーする

手書きの文字列をパソコンに取り込む方法

今回は以下のような手書き文字を認識した結果をパソコンに取り込む方法を説明します。

普通に書いたら完璧に認識されて面白くなかったので、認識しない程度の汚い字を頑張って書いてみた。「レンズのテスト 手書き文字認識」と書いたつもりです。

認識した文字をパソコンに取り込む方法

今回は2つの方法を説明します。

  1. 写真を保存してパソコンのGoogleフォトで文字を認識する方法
  2. スマホのGoogle Lensアプリを使用してパソコンに取り込む

1の方が手順が簡単ですが、画像が容量を圧迫するし、うまく認識せずに再撮影が必要になり無駄な画像が増えるのでお勧めしない。

順番に説明します。

1.写真を保存してパソコンのGoogleフォトで文字を認識する方法

  1. 手書き文字をスマホのカメラで撮影する
  2. Googleフォトアプリでバックアップする
  3. パソコンのWEBブラウザ(ChromeブラウザやEdgeブラウザなど)でGoogleフォト( https://photos.google.com )にアクセスしてその写真を開く
  4. 「画像からテキストをコピー」を行う


2.スマホのGoogle Lensアプリを使用してパソコンに取り込む

  1. パソコンにChromeブラウザをインストールしてアカウントと同期する
    同期するアカウントは スマホの設定 > デバイス情報 > Googleアカウント で確認する


  2. スマホにGoogle Lensアプリをインストールする
  3. Google Lensアプリで画面下の「文字」を選択して、手書き文字を読み取る。読み取れた内容を選択&確認したら「パソコンにコピー」を行い、コピー先のパソコンを選択する


  4. パソコンに通知が来たらパソコンのクリップボードに文字列がコピーされている








2022年2月26日土曜日

GoogleDriveで共有されたデータをマイドライブにコピーする

G Suite無料のアカウントを無料で使い続ける方法を知らない時にドライブのデータを頑張って移動するために作ったスクリプトを紹介します。
オーナー権限を変更するスクリプトもあったのですが、G Suite(Google Workspace)の制約で組織外のユーザーに権限移行ができないため、今回はフォルダ階層を維持したままコピーするスクリプトを作成しましたり

アカウントを廃止するなどの目的でGoogleDriveで共有されたフォルダを自分のアカウントにコピーするスクリプトです。ご利用は自己責任で、、、
var me;
  function CopyFolders() {
  var url = "https://drive.google.com/drive/folders/6dpqdZ1ESK<< ここはコピー元のフォルダURL >>yPoQBdhwEOP";

  var idAndResoucekey = url.replace(/.*\//g,"").split("?");
  var id = idAndResoucekey[0];
  var resoucekey = null;
  me = Session.getActiveUser();
  if( idAndResoucekey.length == 2 )
  {
    resoucekey = idAndResoucekey[1].replace(/.*=/,"");
  }
  var srcFolder = DriveApp.getFolderByIdAndResourceKey(id,resoucekey);
  var dstFolder = getFolder( null, '共有フォルダからのコピー', false );
  CopyFolder( srcFolder, dstFolder );
  
}

function CopyFolder(src,dst){
  var folders = src.getFolders();
  var files = src.getFiles();
  while( folders.hasNext() )
  {
    var folder = folders.next();
    CopyFolder( folder, getFolder( dst, folder.getName() ) );
    if( src.getFiles().hasNext() )
    {
      Logger.log("削除できないファイルが残ってる?");
    }else{
      folder.removeEditor(me);
    }
  }
  while( files.hasNext() )
  {
    var file = files.next();
    try
    {
      file.makeCopy(file.getName(),dst);
      file.removeEditor(me);
    }
    catch(e)
    {
      Logger.log(file.getName()+" 削除失敗");
    }
  }
}


/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}

2022年2月22日火曜日

Googleスプレッドシートに書かれたデータに関してWEBからデータを取得する

サンプルとしてGoogle Apps Scriptでスプレッドシートに書かれた郵便番号から住所を調べるスクリプトを作成した。郵便番号を取得するなら別の方法をお勧めする、あくまでGASの使い方説明の素材にしただけです。
エラー処理などを皆無だし、HTMLから必要なデータを抽出する部分も適当です。
スプレッドシートなどから呼び出した際にすぐに処理を抜けたい場合はstart関数を呼び出す、いきなり処理したい場合やデバッグ時はzipSearch関数を呼び出す。5分経過したらいったん中断し、1分後に処理を再開しまう。

A:B列を取得してB列が空の行のみを処理します。すべて処理が終わるか5分経過するかで結果をスプレッドシートに書き込みます。都度書き込むとAPI呼び出し回数が増え、処理も遅くなります。


function start(){
  // 1分後にスタート
  setTrigger("zipSearch");
}
function zipSearch() {
  // 一旦トリガを削除
  clearTrigger( arguments.callee.name );
  // スクリプト実行開始時間を取得(6分制限用)
  var StartTime   = new Date();

  // シートから入出力部分を取得
  // A列に郵便番号(”222-0001”)が入力されており、対応する住所をB列に入力する
  let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シート1');
  let range = ss.getRange(1,1,ss.getLastRow(),2);
  let values = range.getValues();

  // 一行ずつ処理を実施
  values.forEach( row =>{
    if( row[0] != "" && row[1] == "" )
    {
      let url = "https://www.post.japanpost.jp/cgi-zip/zipcode.php?zip="+row[0];

      // 今回は<td class="data"><small>.*?</small></td>を抽出して処理
      let table = UrlFetchApp.fetch(url)
                  .getContentText()
                  .match(/<td class="data"><small>.*?<\/small><\/td>/gis)
     if( table!=null )
     {
       // 結合しタグを削除
       row[1] = table.join(" ").replace(/<.*?>/gis,"");
     }
     else
     {
       row[1] = "error";
     }
    }
    // スクリプト開始から5分経過したら一旦終了
    var CurrentTime = new Date();
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      // 結果をシートに書き戻す
      range.setValues(values);
      //トリガをセットして終了
      setTrigger(arguments.callee.name);
      return;
    }
  });
  // 結果をシートに書き戻す
  range.setValues(values);
}

function setTrigger(func)
{
  trigger = ScriptApp.newTrigger(func)
            .timeBased()
            .everyMinutes(1)
            .create();
}

function clearTrigger(func)
{
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( trigger=>{
    if( trigger.getHandlerFunction() == func )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

2022年2月20日日曜日

Googleドライブのフォルダに保存している画像をOCRしてドキュメントを生成する

画像をOCRして、画像とOCR結果が貼り込まれたドキュメントを生成するスクリプトです。

function main() {
  // マイドライブ\'GoogleAppsScript\dev\test-data\変換元データ
  var srcFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換前データ'], false );
  var dstFolder = getFolder( null, ['GoogleAppsScript','dev','test-data','変換後データ'], false );


  let option = {
    "ocr": true,// OCRを行うかの設定です
    "ocrLanguage": "ja",// OCRを行う言語の設定です
  }
  var files = srcFolder.getFiles();
  while( files.hasNext() )
  {
    var file = files.next();
    var resource = {
      title: file.getName()
    };
    let doc = DriveApp.getFileById( Drive.Files.copy(resource, file.getId(), option).id);
    doc.moveTo( dstFolder );
  }

}

/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}

2022年2月12日土曜日

AdSenseのデータをスプレッドシートに書き込むスクリプト

以下のスクリプトを実行するとAdSenseのデータをスプレッドシートに取り込みます。 未取得分だけを取得して追加するので、時間駆動のトリガで1時間おきにでも実施するのが良いです。 取り込んだデータはスプレッドシート側でデータ集計したり、グラフを作成してください。
function myFunction() { // 1時間おきのトリガを設定したい場合はこちらを実行
  trigger = ScriptApp.newTrigger("report")
  .timeBased()
  .everyHours(1)
//  .everyMinutes(5)
  .create();
  report();
}

function report() {
  var adsenseClientID = 'ca-pub-NNNNNNNNNNNNNNNN';
  var adsenseAccounts = 'pub-NNNNNNNNNNNNNNNN';
  var metrics = ['ESTIMATED_EARNINGS', 'PAGE_VIEWS', 'CLICKS', 'PAGE_VIEWS_CTR', 'COST_PER_CLICK', 'PAGE_VIEWS_RPM'];
  var sheetName = 'report';
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(sheetName);
  if (sheet == null) {
    sheet = ss.insertSheet(sheetName);
  }

  const today = new Date();
  var lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    var last = new Date('1900/1/1');
    sheet.getRange(1, 1, 1, metrics.length+1).setValues([['Date'].concat(metrics)]);
    lastRow = 2;
  }
  else {
    var last = new Date(sheet.getRange(sheet.getLastRow(), 1).getDisplayValue());
  }

  var report = AdSense.Accounts.Reports.generate(
    'accounts/'+adsenseAccounts,
    {
      // Specify the desired ad client using a filter.
      filters: ['AD_CLIENT_ID=='+adsenseClientID],
      metrics: metrics,
      dimensions: ['DATE'],
      dateRange: 'CUSTOM',
      'startDate.year': last.getFullYear(),
      'startDate.month':last.getMonth() + 1,
      'startDate.day':last.getDate(),
      'endDate.year': today.getFullYear(),
      'endDate.month':today.getMonth() + 1,
      'endDate.day':today.getDate(),
      // Sort by ascending date.
      reportingTimeZone: 'ACCOUNT_TIME_ZONE',
      orderBy: ['+DATE']
    } );

  sheet.getRange(lastRow, 1, report.rows.length, report.rows[0].cells.length)
    .setValues(report.rows.map(row => row.cells.map(cell => cell.value)));
}

Reports.generate関数はヘルプ(Method: accounts.reports.generate)を、metricsやdimensionsに指定する値はヘルプ(Metrics and Dimensions)を参照してください。

2022年2月7日月曜日

厚生労働省のオープンデータから新規陽性者数の推移(日別)を取得してスプレッドシートに書き込むスクリプト

厚生労働省のオープンデータから新規陽性者数の推移(日別)を取得してスプレッドシートに書き込む処理を毎日行うGoogle Apps Script

function myFunction() {
  trigger = ScriptApp.newTrigger("scraping")
  .timeBased()
  .everyDays(1)
  .create();
  scraping();
}
  
function scraping() {
  var content = UrlFetchApp.fetch("https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv").getContentText();
  var values = Utilities.parseCsv(content);
  var sheet = SpreadsheetApp
            .getActiveSpreadsheet()
            .getSheetByName('シート1');
  sheet.getRange(1, 1, values.length, values[0].length).setValues(values);  
}

2022年2月6日日曜日

GoogleAppsScriptを使用しHTML内のテーブルをスプレッドシートに書き込む

Googleドライブの特定のフォルダに格納されているHTMLファイルの中に記載されているテーブルをスプレッドシートに書き込むスクリプト

どんな利用シーンがあるか分かりませんが、知恵袋で質問があったので作成してみた。なにかのツールが吐き出す結果のHTMLファイルにテーブルが含まれていて、その結果をシートにまとめたいという要望だと思う。

まったく同じようなシーンはあまり考えにくいが、何かの役に立つかもしれないので自分用のメモ代わりに残しておく。

6分越え対策やテーブルタグ(<tr>や<td>)内のデータ抽出などもふくめてエラー処理などかなり雑です、コメントで指摘してくれたら修正するかもしれません。


ソースコード

function myFunction() {
  // HTMLファイルが格納されているフォルダ階層を配列形式で指定する。
  // マイドライブ/AAA/BBB/ フォルダを指定するときは["AAA","BBB"]と指定する。
  var srcFolder = getFolder( null,["GoogleAppsScript","dev","HTML内の表を抜き出す","未処理"]);
  // HTMLファイルが格納されているフォルダ階層を配列形式で指定する。
  var dstFolder = getFolder( null,["GoogleAppsScript","dev","HTML内の表を抜き出す","処理済み"]);

  // スプレッドシートの指定は環境に合わせてください。
  var sheet = SpreadsheetApp
            .getActiveSpreadsheet()
            .getSheetByName('シート1');

  // スプレッドシートに書き込む二次元配列
  var table = [];

  // 一気に処理すると6分で終わらないので適当な回数で区切る。
  // 数が多いときは時間主導のトリガで繰り返し実行するとよい。
  var numberOfFiles = 50;

  var htmlFiles = srcFolder.getFiles();
  while (htmlFiles.hasNext()) {
    // 指定したファイル数の処理が完了したら抜ける。
    numberOfFiles--;
    if( numberOfFiles < 0 ){
      break;
    }

    var file = htmlFiles.next();
    var text =  file.getBlob().getDataAsString();
    
    // ここからが必要なデータの取り出しとシートへの貼り込み
    // <tr></tr>の抜き出して1行分を取り出す。
    var rows = text.match(/<tr(?:\s.+?)?>.*?<\/tr\s*?>/gis);
    for( var r=0; r<rows.length; r++ )
    {
      // 1行分の配列
      var arrayCols = [];
      // <th></th>もしくは<td></td>を抜き出す。
      var cols = rows[r].match(/(<th(?:\s.+?)?>.*?<\/th\s*?>)|(<td(?:\s.+?)?>.*?<\/td\s*?>)/gis);
      for( var c=0; c<cols.length; c++ )
      {
        // <th>,</th>,<td>,</td>を削除して配列に格納する
        arrayCols.push( cols[c].replace(/^(<(td|th)(?:\s.+?)?>)|(<\/(td|th)\s*?>$)/ig,""));
      }
      // 1行分の配列を追加
      table.push(arrayCols);
    }
    // 処理が完了したファイルは処理済みフォルダに移動
    file.moveTo( dstFolder );
  }
  // ファイル移動と書き込みは同じ場所が理想
  // 処理高速化を考えてファイル移動はループ内
  // スプレッドシートへの書き込みはループの外に移動
  sheet.getRange(sheet.getLastRow()+1,1,table.length,table[0].length).setValues(table);
}


/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}

1つ目のHTMLファイル

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ひこざるさん</title>
</head>
<body>
    <a href="https://blog.hikozaru.com">ひこざるさんのブログ</a>
    <table border="1">
      <tr>
        <th>XXX</th>
        <th>YYY</th>
        <th>zzz</th>
      </tr>
      <tr>
        <td>111X</td>
        <td>111Y</td>
        <td>111Z</td>
      </tr>
    </table>
</body>
</html>

2つ目のHTMLファイル

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ひこざるさん</title>
</head>
<body>
    <a href="https://blog.hikozaru.com">ひこざるさんのブログ</a>
    <table border="1">
      <tr>
        <th>XXX</th>
        <th>YYY</th>
        <th>zzz</th>
      </tr>
      <tr>
        <td>222X</td>
        <td>222Y</td>
        <td>222Z</td>
      </tr>
    </table>
</body>
</html>

実行結果