2022年12月26日月曜日
2022年11月29日火曜日
Google Formsでドロップダウン形式の日付を毎日変更する
背景
ソースコート
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)で紹介した作業をもう少し簡単に実行できるようにプログラムを作成したのでそのソースコードを公開する。いろいろ手を抜くために邪道なこと(変数名に日本語を使う)を行っているが、気になる人は好きに修正するとよい。プロジェクト全体が欲しい方はここから、実行できるものが欲しい方はここからダウンロードしてください。
プログラムの大まかな処理の流れは以下です。
- 指定されたファイルもしくは指定されたフォルダ配下のファイルすべてを処理
ただし、JSONファイルは処理をスキップ - ファイル名の後ろに".json"を付加したファイルが存在していたらデシリアライズして撮影日時を取得
- 撮影日時はunixtimeなのでutcを経由してローカル時間に変換
- ファイルの作成日時と更新日時を変更、その処理が成功したらJSONファイルも同様に作成日時と更新日時を変更
- 結果を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次はXAMLファイル(); 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ファイルがありません。", 例外メッセージ = "" }); })); } } } }
<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フォト画像のファイルタイムスタンプを修正する
概要
①PowerShellのスクリプトを実行できるようにする
PowerShell 管理者権限として実行します。ウインドウズメニューを開いた状態で「PowerSell」と入力すると見つかるので、「管理者として実行する」を選択してください。
以下のコマンドを実行し現在の権限を確認しメモしておいてください。
Get-ExecutionPolicy
以下のコマンドを実行して権限を変更してください。
Set-ExecutionPolicy RemoteSigned
変更してよいか確認されるので Y キーを押してください。
なお、権限(RestrictedやRemoteSignedなどの)の詳細は以下を参照してください。
②スクリプトを作成
拡張子を表示した状態で「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ファイルの方が楽なケースも多いので結構役に立ちます。
@echo on CScript.exe trash.js c:\folder\test1.txt c:\folder\test2.txt
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でドライブの画像を表示する
知恵袋への回答は質問に直接答えたが、画面ロード時に時間がかかりそうなので処理を全体的に変えてみた。
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で○×ゲームを作ってみた
<!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フォトの「ロックされたフォルダ」の使い方と注意事項
- 「ロックされたフォルダ」の概要
- 「ロックされたフォルダ」に入れるとロック解除しないと画像が閲覧できなくなります。他人に見られたくない画像などを保存する目的だと思います。
- しかし、ゴミ箱を経由せずに消えるなどいろいろと危険な使用があるので大切な画像を「ロックされたフォルダ」に入れてはいけません。
- 大切ではないけど他人には見られたくない画像を保存するのが「ロックされたフォルダ」の用途です。
- 「ロックされたフォルダ」の使い方
- 入れる方法
Googleフォトアプリで写真を表示/写真を選択し、「︙」アイコンから「ロックされたフォルダに移動」を行う。 - 参照する方法
「ロックされたフォルダ」に入れる操作を行った端末のGoogleフォトアプリで 「ライブラリ」 > 「ユーティリティ」 > 「ロックされたフォルダ」 と選んでください。ロック解除はスマホのロック解除と同じ方法です。 - 出す方法
「ロックされたフォルダ」内の画像を選択して「移動」してください。 - ロックされたフォルダに入れるとどうなるか
- 画像が端末にダウンロードされ、Googleフォトアプリの「ロックされたフォルダ」に保存される。
- 操作を行ったときに選択されていたアカウントのGoogleフォト( https://photos.google.com/login )から写真が削除される。
- ロックされたフォルダを使用するうえでの注意事項
- 「ロックされたフォルダ」内での削除操作はゴミ箱を経由しないので復元できない。
- 操作を行った端末のみに保存されるので端末故障/端末紛失/機種変更/端末初期化の際には画像がなくなる
- 操作を行ったときに選択されていたアカウントのGoogleフォト( https://photos.google.com/login )から写真が削除されるが、他のアカウントからは消えない、パートナー共有などで複数のアカウントに画像があるときはすべてのアカウントで「ロックされたフォルダ」に入れる必要がある。
- 他のアプリから参照できなくなる。
- 「ロックされたフォルダ」から出すと再度バックアップ対象になるので、2021/5/31以前にバックアップし容量無制限の対象だった画像も容量カウントの対象になる。
- 撮影日時が保存されていない画像形式(PNG形式で保存されたスクリーンショット画像)はロックされたフォルダに入れるとファイルタイムスタンプが更新されていつ撮影されたか分からなくなる。
- ロックされたフォルダの活用方法
- Pixel 5以前の端末など、節約画質で容量無制限の特典が利用できる端末で「ロックされたフォルダ」(バックアップしない設定)に入れた後に、節約画質でバックアップする設定にしてから「ロックされたフォルダ」から出すと特典が適用される。iPhoneなど対象外の端末からバックアップした画像を無制限の対象に変換できる。
- AndroidのGoogleフォトアプリには画像を一括ダウンロードする方法が提供されていないが、「ロックされたフォルダ」には一括でダウンロードできる。「ロックされたフォルダ」に入れ、すぐに出すことで一括ダウンロードが可能になるが、、、再度バックアップが行われるので注意が必要
AndroidのGoogleフォトアプリを再インストールする方法
Googleフォトは基本的にクラウドにデータを保存しているため、Googleフォトアプリを再インストールしても問題ないと思いがちですが、以下のデータ消えるので注意が必要です。
- アカウントにログインするための情報
- バックアップと同期設定
- ロックされたフォルダの画像
- どの画像がバックアップ済みかの管理情報
Googleフォトアプリを再インストール手順を示します。流れとしては以下になります。
- アカウント確認
- 全アカウントへのログイン確認
- Googleフォトアプリの設定確認
- バックアップと同期オフ設定
(ロックされたフォルダのあったデータをバックアップされないようにする) - ロックされたフォルダから画像移動
- アプリの削除
- アプリの再インストール
- ロックされたフォルダの復元
- バックアップと同期やその他の設定の復元
上記で操作できる人は移行は読まなくてもよいです。勘違いする人が多かったり、間違えてほしくない部分だけ画像を貼ってます。画像を貼ってほしい手順があればコメントください。
- アカウント確認
Googleフォトアプリの右上のアカウントアイコンをタップし、表示されたメニューのアカウントの右側をタップして登録しているアカウントを全てメモする - 全アカウントへのログイン確認
ChromeブラウザのシークレットモードでGoogleフォト( https://photos.google.com/login )にアクセスして上記でメモしたアカウントすべてでログイン操作を行い、以下のことを確認する。
・左上が「≡」アイコンである事(アプリに飛ばされていないことの確認)
・今までバックアップしていた画像が存在する事
・全てのアカウントにログインできる事を確認する - Googleフォトアプリの設定確認
Googleフォトアプリの右上のアカウントアイコンをタップし、「フォトの設定」に行き、設定をメモする。 - バックアップ
と同期オフ設定
「フォトの設定」 > 「バックアップと同期」画面に行き、バックアップと同期をオフにする。 - ロックされたフォルダから画像移動
「ライブラリ」 > 「ユーティリティ」 > 「ロックされたフォルダ」を開き、そこにある画像すべてを移動する。ここで「削除」を押すと復元できないので要注意。
どんな画像があったか覚えておく、写真の枚数が多い場合には撮影日時などをメモしておくとよい。 - アプリの削除
Googleフォトアプリを削除(アンインストール)する。 - アプリの再インストール
Play StoreアプリでGoogleフォトを検索してインストールを行う。インストールの途中でバックアップと同期を有効にする手順があった場合には有効にしないように注意する。 - ロックされたフォルダの復元
フォトの中からロックされたフォルダに移動したい写真を選択して、ロックされたフォルダに移動する。 - バックアップと同期やその他の設定の復元
アプリの設定をすべて元に戻す。
2022年4月18日月曜日
2022年3月21日月曜日
G Suite アカウントを無料で継続利用する方法
Google Workspaceに課金せずデータを引き継ぐ
他の方法も用意されているようなのでもう少し様子見がよさそうです。: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は継続利用可能。(その他のサービスに関しての質問はあきらめました、知りたいサービスがある場合には有料サービスに契約したうえで、サポートに問い合わせてください。)アカウントを削除されずにアカウントを使用し続ける方法
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で使える機能
使えても機能制限がある可能性もあるようですので、例えば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追記
- G Suite経由でGoDaddyで購入したドメインを使用している場合には念のため他のレジストラに管理を移管する。(必須かどうか不明だが、メールサーバー移管と合わせて移管先レジストラを考えた方が良い。)
- メールを別サーバーに移行する。
- @gmail.com アドレスを取得し、2のメールを転送する、2のSMTPサーバー経由でメールを送れる様に設定する。(移行先サーバーのメールボックス容量が潤沢であれば不要)
- 無料期間中(~2022/6/30)にGoogle Workspace Business Xxxx のフレキシブルプランにアップグレードする。(Enterpriseを選んだり年間プランを選ぶと有料になるので注意が必要)。
- Cloud Identity Freeエディションを追加する。
- Google Workspace 有料サブスクリプションをキャンセルする。
注意事項:
- カレンダーは使用できないので@gmail.comで使用する
- カレンダーに追加されているイベントを削除できなくなるという情報を得たので、事前に削除しておくことをお勧めします。:2022/4/29追記
- Meetは参加専用になるようです。その他の機能も「使える」けど制限がある可能性がありそうです。:2022/4/29追記
2022年3月5日土曜日
GoogleのLens機能で読み取った文字列をパソコンにコピーする
手書きの文字列をパソコンに取り込む方法
今回は以下のような手書き文字を認識した結果をパソコンに取り込む方法を説明します。
普通に書いたら完璧に認識されて面白くなかったので、認識しない程度の汚い字を頑張って書いてみた。「レンズのテスト 手書き文字認識」と書いたつもりです。
認識した文字をパソコンに取り込む方法
- 写真を保存してパソコンのGoogleフォトで文字を認識する方法
- スマホのGoogle Lensアプリを使用してパソコンに取り込む
1の方が手順が簡単ですが、画像が容量を圧迫するし、うまく認識せずに再撮影が必要になり無駄な画像が増えるのでお勧めしない。
順番に説明します。
1.写真を保存してパソコンのGoogleフォトで文字を認識する方法
- 手書き文字をスマホのカメラで撮影する
- Googleフォトアプリでバックアップする
- パソコンのWEBブラウザ(ChromeブラウザやEdgeブラウザなど)でGoogleフォト( https://photos.google.com )にアクセスしてその写真を開く
- 「画像からテキストをコピー」を行う
2.スマホのGoogle Lensアプリを使用してパソコンに取り込む
- パソコンにChromeブラウザをインストールしてアカウントと同期する
同期するアカウントは スマホの設定 > デバイス情報 > Googleアカウント で確認する - スマホにGoogle Lensアプリをインストールする
- Google Lensアプリで画面下の「文字」を選択して、手書き文字を読み取る。読み取れた内容を選択&確認したら「パソコンにコピー」を行い、コピー先のパソコンを選択する
- パソコンに通知が来たらパソコンのクリップボードに文字列がコピーされている
2022年2月26日土曜日
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月23日水曜日
2022年2月22日火曜日
Googleスプレッドシートに書かれたデータに関してWEBからデータを取得する
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のデータをスプレッドシートに書き込むスクリプト
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>
実行結果