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>

実行結果

2022年2月5日土曜日

部活で共有しているアカウントにプライベート写真がアップロードされたかもしれない場合

部活、サークル、職場などの共有アカウントのGoogleフォト写真が登録されてしまって、皆に見られているかもしれない。という質問は結構多い。

根本的にアカウントを共有するのが大変危険です。その際の対処を書きますので、以下の順番で確認および対応を行ってください。

1.スマホのGoogleフォトアプリの設定を確認


Googleフォトアプリの右上のアカウントアイコンをタップしてください。
「フォトの設定」⇒「バックアップと同期」画面に遷移して「バックアップアカウント」を確認してください。
そのアカウントが共有アカウントだとしたらヤバいです、その画面で「バックアップと同期」をオフするか、プライベートアカウントに切り替えてください。

2.他の人が見える画像を確認


スマホのSafariブラウザのプライベートタブやChromeブラウザのシークレットモードでGoogleフォトWEB版(https://photos.google.com/login)に共有アカウントでログインしてください。
プライベートタブやシークレットモードでアクセスしないと別の問題が発生するので、もしわからなければWEBで調べてください。
GoogleフォトWEB版でのアクセスに成功すると左上が「≡」アイコンになっています。
そこで表示されている画像が、共有アカウントにログインできる人が閲覧可能な画像です。そこに画像が表示されていたら次の手順に進んでください。

3.公開されてしまった画像を削除


上の1.と2.の手順を実施してから本手順を実施してください。
手順2.と同じ手順でGoogleフォトWEB版(https://photos.google.com/login)に共有アカウントでログインしたらプライベート画像をゴミ箱アイコンでゴミ箱に入れてください。
ゴミ箱から完全に削除する前に端末に画像が残っているか確認してください。
確認はGoogleフォトアプリ以外のgallery goなど端末のデータを閲覧するアプリで行ってください。
端末から削除されていないことを確認したらGoogleフォトWEB版で「≡」⇒「ゴミ箱」に行き、ゴミ箱を空にしてください。