Translate

ラベル :プログラミング の投稿を表示しています。 すべての投稿を表示
ラベル :プログラミング の投稿を表示しています。 すべての投稿を表示
Post Date:2023年3月26日 

Markdown(Typora)でMindmapを描く!

Typoraで描いたマインドマップ

Typoraは優れたMarkdownエディターで、Mac、Windows、Linuxで使えます。有料アプリですが、$14.99(約2,000円)を払う価値のあるMarkdownエディターです。

Mermaidは、Markdown形式のテキストで図を描くことができるオープンソースのJavaScriptツールですが、Typoraは、Mermaidの機能を組み込んでいるので、Mermaidのコードを書くと図形描画ができます。

Mermaidでは、シンプルなテキストを使って複雑なフローチャート、シーケンス図、ガントチャート、クラス図、そしてマインドマップなどを作成することができます。

マインドマップ自体が、階層構造になっているので、Mermaidでの構文もとても簡単です。


Mermaidを使ってマインドマップを作成する

```mermaidでMermaidのコードが書けるようになります。

```mermaid

最初の行にmindmapと書いて、次の行からがマインドマップの構文になります。

mindmap

マインドマップの各ノードを描く

マインドマップは中心のトピックから放射状にアイデアが階層構造で配置されている図です。Mermaid でマインドマップを描くときも、階層構造で記載するので。マインドマップをイメージしながら書くことができます。

下記はrootから3つのメインブランチ(a1, a2, a3)があるマインドマップの例です。階層構造で示せば下記のようになります。

  • Root
    • a1
    • a2
    • a3

mermaid でも同じようにRootから1階層下げてa1, b1, c1 と記載します。

mindmap
	root
		a1
		b1
		c1

下記のように描画されます。

Typoraでマインドマップ

次にa1の下にサブブランチを2つ、a2_1とa2_2を追加します。a1から1階層下げてa2_1, a2_2を記載します。

mindmap
	root
		a1
			a2_1
			a2_2
		b1
		c1

下記のように描画されます。

Typoraでマインドマップ

上記のマインドマップに、a2_1の下にサブブランチ、a3を追加します。

mindmap
	root
		a1
			a2_1
				a3
			a2_2
		b1
		c1

下記のように描写されますが、root --> a1 --> a2_1 --> a3 と階層が下がるとブランチが段々と細くなっていきます。

Typoraでマインドマップ

各ノードの形を変える

各ノードに使える形状には、

  • cloud(雲吹き出し)
    ) ( で括る
  • bang(爆弾吹き出し)
    )) (( で括る
  • circle(円形)
    (( ))で括る
  • round square(角丸四角形)
    ( )で括る
  • square(四角形 )
    [ ] で括る
  • default(デフォルト)

の6種類がありますが、デフォルトは何も指定しない場合です。<br/>は改行です。

mindmap
	)cloud<br/>雲吹き出し(
	))bang<br>星吹き出し((
	((Circle<br>円形))
	(rounded square<br>角丸四角形)
	[square<br>四角形]
	default shap<br>デフォルト

描画すると下記のようになります。squareとdefaulとの違いはよく見るとデフォルトは下に線が入っています。

Typoraでマインドマップ(Shape)

アイコンを使う

ノードの中にアイコンを記載することもできます。Font Awesomrのアイコンが使用でき、class名を指定します。

Font Awesomr のアイコン

Font Awesomrのclass名は、Font Awesome4.7フォント一覧で調べられます。

アイコンを記載したノードの下に同じ階層で

::icon(fa class名)

という形式で指定します。

a1に音符、b1に封筒、c1にユーザのアイコンをそれぞれ指定する場合には下記のように記載します。

mindmap
	)Root(
		music
		::icon(fa fa-music)
		envelope
		::icon(fa fa-envelope-o)
		user
		::icon(fa fa-user)

こんな感じです。Font Awesomrのclass名で記載するので少々面倒ですが、アイコンを使うとマインドマップらしくなります。

Typoraでマインドマップ(Icon)

色の指定と配置はできない

ブランチの色を変更することはできません。またブランチの配置を変更することもできません。しかし、マインドマップ描画ソフトを使わなくてもMarkdownでマインドマップが簡単に描けるのは便利です。


円グラフを書く(おまけ)

最後に Mermaid を使った円グラフの描き方です。こちらも簡単に描けます。

```mermaid コードを記載します。

  • 1行目:pie円グラフの宣言
  • 2行目:titletitleの後が円グラフの表題
  • 3行目以降:円グラフの要素
    ”グラフの項目名”:比率

下記がサンプルコードです。

pie
	title pie chart
	"50%" : 50
	"30%" : 30
	"20%" : 20

下記が、Mermaidで描いた円グラフになります。

Mermaid で描いた円グラフ

bar chart(棒グラフ)もMermaidで描けるともっと便利になるのですが、、、。

Post Date:2022年8月13日 

GASで複数のPDFを結合する

ゾウでもわかる Google Apps Script

Google Apps Script で複数のPDFを結合できないかと探していたら、officeの杜 | PDFを結合するという記事がありました。

officeの杜で共有されているこちらのコード にある mergePdfs() を活用させていただきました。元ネタは、stack overflowにある Merge Multiple PDF's into one PDF です。


結合できるPDFファイル

stack overflowのコメントにPDFのバージョンが1.5以上である場合には大きな修正が必要(結合できない)と記載されていますが、Googleの機能(印刷)やChromeの印刷でPDFにしたPDFはマージ可能でした。

対象 PDFバージョン マージ可否
Googleドキュメント 1.4
Googleプレゼンテーション 1.7
Googleスプレットシート 1.7
Google Chrome 1.4
Office 365 1.7

PDFのバージョンはAdobe Acrobat Readerで確認できます。

下記は、Googleドキュメントの印刷からPDFを保存した場合です。バージョンは1.4となっています。

Googleスプレッドシート、プレゼンテーションは、バージョンが1.7と表示されますが、

前述したように問題なく結合することができます。

Office 365でPDFに変換したファイルは結合できませんでしたが、Googleを中心としたサービスを利用しているのであれば問題なさそうです。


mergePdfs()関数を使う

mergePdfs()関数の説明と引数をみると

mergePdfs(directory, name, pdf1, pdf2, pdf3, ....)
drirectory PDFが格納されているディレクトリID
name 結合したPDFのファイル名
pdf1, ... 結合するPDFファイル

結合するPDFファイルは2つ以上複数指定できますが、引数でファイル名をひとつづつ指定するのは煩わしい。

ということで、フォルダーにあるPDFをリスト(配列に格納)して引数として渡せるように変更してみました。

またPDFファイルの結合順番はファイル名で昇順にソートした順番にします。ファイル名の先頭に数字を入れておけば番号順で結合されます。

例えば、ファイル名を

0.test.pdf
1.test.pdf
2.test.pdf

としておけば、0.test、1.test、2.testの順番で結合されます。


mergePDFs()関数の修正

先ずは、office杜のこちらのリンクからmergePdfs()のプログラムを取得してください。

修正箇所は3箇所です。

12行目

関数の引数をファイル名から、配列を1つ渡すように変更します。

修正前:
function mergePdfs(directory, name, pdf1, pdf2, opt_pdf3) {
修正後:
function mergePdfs(directory, name, pdfList {
21行目

関数の引数ループで取得しているところを、引数に格納されているファイル数分(配列の長さ分)で処理するように変更します。

修正前:
for (var argumentIndex = 2; argumentIndex < arguments.length; argumentIndex++) {
修正後:
for (var i=0; i<pdfList.length; i++) {
23行目

マージするPDFのファイルサイズ取得を引数の配列からに変更します。

修正前:
var bytes = arguments[argumentIndex].getBlob().getBytes()
修正後:
var bytes = pdfList[i].getBlob().getBytes()

また、475行目移行はPDFを分割する関数のコードになっていますので削除してしまっても問題ありません。


mergePDFs()関数を呼び出す

次は、修正したmergePDFs()関数を呼び出すプログラムになります。指定したフォルダIDにあるPDFファイルを配列(pdfList)に格納して、ファイル名順にソートしてから mergePDFsに引き渡します。

<フォルダID>にはGoogleドライブのフォルダIDを、<ファイル名>には結合して作成するファイル名にしてください。

フォルダIDは、フォルダを開いたときのURL、https://drive.google.com/drive/folders/XXXXX XXXX部分になります。

function merge(srcFolderId, fileName){
  var srcFolderId = '<フォルダID>' //フォルダ ID
  var fileName = '<ファイル名>' //結合後のファイル名

  //フォルダ内のファイルを取得
  var srcFolder = DriveApp.getFolderById(srcFolderId)
  var srcFiles = srcFolder.getFiles()

  //PDFファイルだけを配列に格納
  var pdfList = []
  while(srcFiles.hasNext()) {
    var srcFile = srcFiles.next()
    if (srcFile.getMimeType()==='application/pdf') {
      pdfList.push(srcFile);
    }
  }
  pdfList.sort()  //照準でソート
  mergePdfs(srcFolder, fileName, pdfList)
}

13行目のgetMimeType()でPDFファイルだけを対象にしています。また17行目のsort()で配列に格納した各ファイルをファイル名でソートしています。


スプレッドシートのUI機能で汎用的にする

もう少し汎用的に使えるように、スプレッドシートのUI機能を使って、フォルダIDと結合して作成するファイル名をUI上で入力するようにします。

またスプレッドシートのメニューからGASの実行を可能にします。

スプレッドシートの機能を使うので、Googleスプレッドシートの拡張機能から Apps Script を作成する必要があります。


スプレッドシートにメニューに追加する

スプレッドシートが開かれたときにメニューを追加します。こんな感じです。

「mergePDFs」 > 「PDFを結合」 で diaLog関数を実行します。

function onOpen(){ 
  SpreadsheetApp
    .getActiveSpreadsheet()
    .addMenu('mergePDFs', [
      {name: 'PDFを結合', functionName: 'diaLog'}
    ])
}

ダイアログ

詳細の説明は省きますが、「フォルダーID」と「ファイル名」の入力の後に確認画面を出してが簡単なチェックと確認を出力して merge()関数にフォルダIDとファイル名を引き渡します。

ダイアログでフォルダーIDを指定する

またPDFの結合でエラーとなった場合にもエラー出力をするようにしています。

ダイアログでエラーメッセージを出力

下記がサンプルコードとなります。

function diaLog() {
/********************
PDFがあるフォルダを指定 
********************/
  var srcFolderId = Browser.inputBox("結合したいPDFがあるフォルダを指定してください(FolderID)",Browser.Buttons.OK_CANCEL)
  //キャンセルが押下されたら終了
  if (srcFolderId == 'cancel')  {
    return
  }
  try {
    var srcFolder = DriveApp.getFolderById(srcFolderId)
  }
  catch(e) {
    Browser.msgBox('ERROR', srcFolderId + ' is not available', Browser.Buttons.OK)
    return
  }
/********************
ファイル名の指定 
********************/
  var fileName = Browser.inputBox("結合後のファイル名を指定してください(FolderID)",Browser.Buttons.OK_CANCEL)
  //キャンセルが押下されたら終了
  if (fileName == 'cancel')  {
    return
  } else if (fileName == '') {
    Browser.msgBox('ERROR', 'File name is not defined', Browser.Buttons.OK)
    return
  }
/********************
開始の確認 
********************/
  var srcFolderName = srcFolder.getName()

  var result = Browser.msgBox('Confirm', '「' + srcFolderName + '」にあるすべてのPDFを\\n「' + fileName + '」として結合します', Browser.Buttons.OK_CANCEL)
  if (result == 'cancel')  {
    return
  }

  try {
    merge(srcFolderId, fileName)  //PDF格納フォルダ,ファイル名
  }
  catch(e) {
    Browser.msgBox('ERROR', 'Error Occurred during merging', Browser.Buttons.OK)
    return
  }
  
  Browser.msgBox('Complete', 'PDFの結合が完了しました', Browser.Buttons.OK)
}

これで完成です。

スプレッドシートのメニューから dialog()関数を呼び出し、merge()関数でフォルダ内のPDFをリスト化して、mergePDFs()関数でPDFを結合します。

先人の知恵と努力に感謝です。


GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2022年6月17日 

gasの変数について考えてみる

ゾウでもわかる Google Apps Script

Google Apps Scrit (GAS) で変数を定義するキーワードには、var, let, const の3種類があります。変数と定数の違いは明確ですが、varlet の違いはどのようなところにあるのかをサンプルプログラムを実行しながら確認してみました。


変数と定数

var, let は、再代入(値を入れ替えること)ができる変数で、const は定義した値を変更できない定数です。

再代入とは、下記の例のように、最初に x に 1を代入して、次に x に x+1(=2) を代入するということです。

function argument() {
  let x = 1
  x = x + 1
}

定数の場合には、x に 1を代入したあとに変更ができません。下記を実行するとエラーとなります。

function argument() {
  const x = 1
  x = x + 1
}

変数の利用可能範囲(スコープ)

では、varlet の違いは何でしょうか?

GoogleのV8ランタイムの説明を読むと

let および const キーワードは、ブロックスコープのローカル変数/定数を定義することができます。(和訳)

【引用】 let and const - Google Apps Script

と記載されています。

ブロックとは、{}で括られた処理のことで、例えば、if分も{}で括られた処理です。ローカル変数というのは、そのブロック内でしか使えない変数/定数ということです。

letconst は、同じ関数内であってもif文内で宣言した関数は使えないということになります。ちなみに var の場合は、同じ関数内であれば if文内で宣言した変数を利用することができます。

let and const - Google Apps Script にあるサンプルプログラムに constvar を追加してスコープについて確認してみます。

function myFunction() {
  let x = 'hello'
  const y = 'hello'
  var z = 'hello'
  if (x === 'hello') {
    let x = 'World'
    const y = 'World'
    var z = 'World'
    console.log('let x=', x)      // Prints 'world'
    console.log('const y=', y) // Prints 'world'
    console.log('var z=', z)     // Prints 'world'
  }
  console.log('let x=', x)      // Prints 'hello'
  console.log('const y=', y) // Prints 'hello'
console.log('var z=', z)     // Prints 'world' }

let, const の有効範囲はブロック内です。5行目からのif文のブロックの中では、6,7行目で変数の宣言と代入がされている 'world' が出力されます。

しかし、13,14行目のログ出力では、myFunction() として、2,3行目で変数の宣言と代入がされている 'hello' が有効な変数となります。

一方、var で定義した変数 xは、15行目のログ出力で 'world' と出力されます。これは var 変数は同一関数内で有効であり、かつ次に説明する再宣言ができるので、console.log が実行される直前(11行目)に宣言と代入がされた変数の値が出力されます。


関数で定義した変数はif, for のブロック内でも使える

上位のブロックで let, const で宣言された変数は、下位のブロックでも参照できます。

下記の例で説明します。

function checkScope() {
  let x = 0
  if (x==0)  {
    let y = 0
    for (let i=0; i<10; i++) {
      x = x + 1
      y = y + 1
    }
    console.log('y=', y)
  }
  console.log('x=', x)
}

変数 x は、checkScope() 関数で定義しているので、下位ブロックのif文、さらにその下位にあたる forループでも参照することができます。なので、x=x+1 が10回実行され、forループ、if文を抜けたあとのログ出力で x=10 と出力することができます。

変数 y は、if文の中で宣言されているので、下位ブロックのforループで参照することができます。forループでの処理で再代入された値が、if文内のログ出力で可能です。

しかし、if文を出た後にログ出力をしようとすると y は定義されていないとエラーとなります。

function checkScope() {
  let x = 0
  if (x==0)  {
    let y = 0
    for (let i=0; i<10; i++) {
      x = x + 1
      y = y + 1
    }
    console.log('y=', y)
  }
  console.log('x=', x)
  console.log('y=', y)
}

また forループで使っている変数 i も let で定義しているので forループ内だけで有効です。これを for (var i=0; ....) とすると、変数 i は、関数内で参照できる変数となります。

また 変数 i を宣言するときに、let, var のキーワードを付けないと(忘れると)、変数 i は、グローバル変数となり、変数が宣言された以降に実行されるどの関数でもアクセスできるようになってしまいます。

function checkScope() {
  let x = 0
  if (x==0)  {
    let y = 0
    for (i=0; i<10; i++) {
      x = x + 1
      y = y + 1
    }
    console.log('y=', y)
  }
  console.log('x=', x)
  print()
}
function print()	{
  console.log('i=', i)
}

再宣言

変数のスコープで var の再宣言についてふれましたが、もう少し簡単な例で説明します。下記のように var は、同じ関数内で何度でも宣言できます。

function redeclaration() {
  var x = 1
  x = x + 1
  console.log('x=', x)
  var x = 'Hello'
  console.log('x=', x)
}

let, const は同一ブロック内では1回しか宣言できません。下記のプログラムは構文エラーとなり保存できません。

function test() {
  let x = 1
  x = x + 1
  console.log('x=', x)
  let x = 'Hello'
  console.log('x=', x)
}

var, let, constの違い(まとめ)

var, int, const の違いを表にまとめます。

キーワード 再代入 再宣言 スコープ
var 可能 可能 同一関数内
let 可能 不可 ブロック内
const 不可 不可 ブロック内

参照できる範囲が少なく、再宣言ができないということは、より明示的に変数を使うということになるので、「varではなくlet を利用しましょう」というのも納得できます。


関数間で値の引き渡し

関数の中で関数を呼び出す場合には、引数として関数に値を引き渡すことができます。また、関数での実行結果を戻す場合には、戻り値として結果を返却することができます。

function argument() {
  let x = 1
  let y = 2
  let z = cal(x, y)
  console.log('z=', z)
}
function cal(a, b)	{
  let c = a + b
  return c
}

cal() を呼び出すときに引数として x, y をセットしています。7行目の function cal(x, y)では、4行目で設定した引数 x, y をそれぞれ a, b にセットしています。この関数の中では、合算した値(1+2)を cに代入して、cを戻り値としています。

関数の呼び出し側は、戻り値を変数zに代入するために let z = cal(x, y) としています。


グローバル変数

最後にグローバル変数についてです。関数内ではなくプログラムのトップレベルで定義された変数/定数は、プログラ内のどこから(同じプロジェクト内のどの関数から)でもアクセスすることができます。

const x = 1
const y = 2
function main()	{
  sub1()
  sub2()
  console.log('x=', x)
  console.log('y=', y)
}
function sub1()	{
  console.log('x=', x)
}
function sub2()	{
  console.log('y=', y)
}

変数も奥が深いですね。


GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2022年5月7日 

Googleスプレッドシートの更新された範囲をURLとして通知する

ゾウでもわかる Google Apps Script

シートの更新通知を使うと確認が必要のない更新通知も送られてくるので、自分に必要な項目が更新されたときだけに通知が欲しい。

また、どこが変更されたのかが分かると、通知を受けた人に優しい仕様となります。

象と散歩: Googleスプレッドシートの更新された値を知るで、変更された箇所の行番号と列番号を取得する方法を紹介しましたが、変更範囲をセル範囲が指定されたリンクとして作成するGASプログラムを作ってみました。


URLの取得

onEdit()関数については、象と散歩: Googleスプレッドシートの更新された値を知る で説明していますが、スプレッドシートが更新されたときに実行される関数です。

onEdit(e)	{
  var ssUrl = e.source.getUrl()	//スプレッドシートのURL
  console.log(ssUrl)
}

セルの内容が更新されると、上記の関数が実行されます。マニュアルで実行するとエラーとなりますので、セルの値を更新してログを確認してください。

onEdit()関数の引数に e を指定していますが、e.source でスプレッドシートオブジェクトが取得でき、getUrl()でスプレッドシートのURLが取得できます


シートはGIDパラメータで指定する

上記で取得したスプレッドシートのURLは、下記のような構成になっています。

https://docs.google.com/spreadsheets/d/xxxxx/edit

複数シートがある場合には、URLにGIDパラメータが付与されています。

https://docs.google.com/spreadsheets/d/xxxxx/edit#gid=749420395

最初のシート(シート 1)は、必ず gid=0 ですが、getSheetId() で GIDを取得することができます。

onEdit(e)	{
  var sheet = e.source.getActiveSheet()
  var sheetId = sheet.getSheetId()	//シートID
  console.log(sheetId)
} 

セル範囲はRANGEで指定する

セルを右クリックして"このセルへのリンクを取得"をクリックすると

範囲を指定してリンクを取得する

下記のようなURLが取得できます。

https://docs.google.com/spreadsheets/d/xxxxx/edit#gid=0&range=C2

また複数セルを選択した状態で右クリックして"この範囲のリンクを取得"をクリックすると下記のようなURLが取得できます。

https://docs.google.com/spreadsheets/d/xxxxx/edit#gid=0&range=A3:C3

"range=" で範囲を指定したURLを取得できます。


A1形式で範囲を取得する

onEdit()で実行されたときにrangeで変更範囲が取得できますが、A3:C3のようなA1形式で範囲を取得するには、range.getA1Notation() を使います

更新範囲がひとつのセルならA3、範囲ならA3:C3のように取得することができます。

onEdit(e)	{
  var sheetRange = e.range.getA1Notation()	//変更範囲
  console.log(sheetRange)
}

シートの値を変更して、ログを確認してください。複数セルの範囲は、シート上で範囲選択をしてコピペをすることで確認できます。


シートと範囲を指定したURLを作成

シートID(GID)と範囲(RANGE)を指定したURLを作成します。

function onEdit(e) {
  var ssUrl = e.source.getUrl()	//スプレッドシートのURL
  var sheet = e.source.getActiveSheet()
  var sheetId = sheet.getSheetId()	//シートID
  var sheetRange = e.range.getA1Notation()	//変更範囲
  var url = ssUrl + '#gid=' + sheetId + '&range=' + sheetRange
  console.log(url)
}

ログに出力されたURLをコピペして、範囲選択がされた状態でスプレッドシートが開くかを確認してください。


変更範囲の確認

特定の範囲が更新されたときだけ通知があるように、更新されたセルが対象としたいセル範囲内にあるかを確認します。

下記のサンプルプログラムでは、targetStartColumn, targetEndColumn で対象列を、targetStartRow, targetEndRow で対象行を指定しています。

下例では、C列の2行目から1000行目が更新されたかを確認するサンプルコードとなります。

function onEdit(e)  {
  var targetStarColumn =3 //対象セル(開始列)
  var targetEndColumn = 3 //対象セル(終了列) 
  var targetStartRow = 2  //対象セル(開始列)
  var targetEndRow = 1000 //対象セル(終了列)

  var rowStart = e.range.rowStart //更新開始行の取得
  var rowEnd = e.range.rowStart   //更新終了行の取得
  var columnStart = e.range.columnStart //更新開始列の取得
  var columnEnd = e.range.columnEnd     //更新終了列の取得

  var ssUrl = e.source.getUrl() //スプレッドシートのURL
  var sheet = e.source.getActiveSheet()
  var sheetId = sheet.getSheetId()  //シートID
  var sheetRange = e.range.getA1Notation()  //変更範囲

  var text = '変更箇所の通知\n'
  if (targetStarColumn <= columnStart &&  columnEnd <= targetEndColumn
      &&  targetStartRow <= rowStart &&  rowEnd <= targetEndRow) {
    text = text + ssUrl + '#gid=' + sheetId + '&range=' + sheetRange
  } else  {
    text = '対象範囲外の更新'
  }
  console.log(text)
}

対象範囲が更新された場合に "更新されたシートとセル範囲を指定したURL" を作成します。シートを更新して実行結果をログで確認してください。


メールを送信する

シンプルトリガーのonEdit()ではメール送信ができないので、function名を変更して、トリガーを設定します。

トリガーの設定方法については、

を参考にしてください。

メール送信は GmailAppクラスのsendEmailメソッドを使います。

GmailApp.sendEmail(宛先, 件名, 本文, {オプション})

オプションは省略可能です。(詳細はClass GmailAppを参照してください)

mailTo で指定しているメールアドレスは、自分のメアドに変更してください。

function updateNotice(e) {
  var ssUrl = e.source.getUrl()	//スプレッドシートのURL
  var sheet = e.source.getActiveSheet()
  var sheetId = sheet.getSheetId()	//シートID
  var sheetRange = e.range.getA1Notation()	//変更範囲
  var url = ssUrl + '#gid=' + sheetId + '&range=' + sheetRange

  // メール送信
  var mailTo   = 'sample@gmail.com'
  var subject  = '更新通知'
  var bodyText = '変更箇所の通知\n'
  bodyText     = bodyText + url
	
  GmailApp.sendEmail(
    mailTo,
    subject,
    bodyText
  )
}

特定のセルが更新されたときにメールを出す場合には下例を参考にしてください。

function updateNotice(e) {
  var targetStarColumn = 3	//対象セル(開始列)
  var targetEndColumn = 3	//対象セル(終了列) 
  var targetStartRow = 2	//対象セル(開始列)
  var targetEndRow = 1000	//対象セル(終了列)

  var rowStart = e.range.rowStart //更新開始行の取得
  var rowEnd = e.range.rowStart   //更新終了行の取得
  var columnStart = e.range.columnStart //更新開始列の取得
  var columnEnd = e.range.columnEnd     //更新終了列の取得

  var ssUrl = e.source.getUrl() //スプレッドシートのURL
  var sheet = e.source.getActiveSheet()
  var sheetId = sheet.getSheetId()  //シートID
  var sheetRange = e.range.getA1Notation()  //変更範囲

  var text = '変更箇所の通知\n'
  if (targetStarColumn <= columnStart &&  columnEnd <= targetEndColumn
      &&  targetStartRow <= rowStart &&  rowEnd <= targetEndRow) {
    text = text + ssUrl + '#gid=' + sheetId + '&range=' + sheetRange
    sendMail(text)
  }
}
function sendMail(text)	{
  // メール送信
  var mailTo   = 'sample@gmail.com'
  var subject  = '更新通知'
  var bodyText = text

  GmailApp.sendEmail(
    mailTo,
    subject,
    bodyText
  )
}

上記例では更新されたセル範囲でURLを作成していますが、range=開始行番号:終了行番号とすると、更新された行をフォーカスしたURLが作成できます。

https://docs.google.com/spreadsheets/d/XXXXX/edit#gid=0&range=3:3

上記も、rowStart と rowEnd を使って簡単に作成できます。


GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2022年3月6日 

GASでExcelをGoogleスプレッドシートに変換する

ゾウでもわかる Google Apps Script

ExcelファイルのデータをGASで処理するには、GoogleDrive上でExcelファイルを開いてGoogleスプレッドシートで保存する必要があります。

ExcelをGoogleスプレッドシートとして保存

小さなExcelファイルなら上記の方法でも構いませんが、大きなExcelファイルだと ”Googleスプレッドシートとして保存” で時間がかかりすぎて保存できない、、、。

また複数ファイルがあるときは一括変換できれば楽ちんです。

ということで、GASでExcelファイルをGoogleスプレッドシートに変換する2つの方法について説明します。

  1. Excelファイルを指定してスプレッドシートに変換する
  2. フォルダにあるExcelファイルを一括変換する

Excel to Google Sheets には Drive APIが必要

Google Apps Script でExcelファイルを開く場合には、Googleドライブのファイルやフォルダを操作できる DriveAppクラス を使います。

但し、そのまま、DriveAppのコードを書いて実行しても

エラー ReferenceError: Drive is not defined

と、実行できません。


Drive APIサービスを追加する

DriveAppクラスを使うためには、Apps Scriptエディターの左側のメニューにある「サービス」からDrive APIを追加する必要があります。

① サービスの右側にある+をクリック

サービスの追加

② 一覧から Drive API を選択して追加

Drive APIを選択

Drive APIが追加されると左メニューのサービスの下にDriveが表示されます。

サービスにDriveが追加

これで DriveAppクラスが利用できます。


Excelファイルを指定してスプレッドシートに変換する

Excelファイルを指定して、指定したフォルダにGoogleスプレッドシートを出力するサンプルコードとなります。

Excelファイルと出力先のフォルダはIDで指定します。


ExcelファイルとフォルダのID

GoogleドライブからExcelファイルを開いたURLのXXXXXの部分がIDとなります。

https://docs.google.com/spreadsheets/d/XXXXX/edit

ExcelのファイルID

GoogleドライブのフォルダIDは、XXXXXの部分がIDになります。

https://docs.google.com/drive/u/0/folder/XXXXX

フォルダID


サンプルコード(Excelファイル指定)

上記で説明したExcelファイルと出力先のフォルダのIDを指定する、簡単なサンプルコードです。

12行目で取得したExcelファイル名を13行目でログに出力していますが、変換したGoogleスプレッドシート名も同じファイル名にしています。

function convertExcel()  {
  //変換するExcelファイル
  let excelFileId = 'XXXXX'

  //スプレッドシート出力先フォルダー
  let destFolderId = 'XXXXX'

  //ExcelファイルをファイルIDで取得
  var excelFile = DriveApp.getFileById(excelFileId)

  //Excelファイル名取得
  var fileName = excelFile.getName()
  Logger.log(fileName)

  var option =  {
    mimeType:MimeType.GOOGLE_SHEETS,   //Google sheets
    parents:[{id:destFolderId}],                       //出力先フォルダー
    title:fileName                                          //出力先ファイル名
  }
  Drive.Files.insert(option,excelFile)
}

20行目の

Drive.Files.insert(option,excelFile)

で、変換を実施していますが、15〜19行目のオプションで、

  • MIMEタイプ
  • 出力先フォルダー
  • 出力先ファイル名

を指定しています。

スプレッドシートへの変換なのでMIMEタイプをGOOGLE_SHEETSとしていますが、Enum MimeType | Apps Script | Google Developers に全てのMIMEタイプが記載されています。


変換後に処理を続ける

Googleスプレッドシートに変換してから、そのままシートをGAS処理する場合には、20行目からを下記のように変更してください。

取得したシート名をログに出力する簡単なサンプルです。

function convertExcel()  {
  //変換するExcelファイル
  let excelFileId = 'XXXXX'

  //スプレッドシート出力先フォルダー
  let destFolderId = 'XXXXX'

  //ExcelファイルをファイルIDで取得
  var excelFile = DriveApp.getFileById(excelFileId)

  //Excelファイル名取得
  var fileName = excelFile.getName()
  Logger.log(fileName)

  var option =  {
    mimeType:MimeType.GOOGLE_SHEETS,   //Google sheets
    parents:[{id:destFolderId}],                       //出力先フォルダー
    title:fileName                                          //出力先ファイル名
  }
  var sheetObj = Drive.Files.insert(option,excelFile)

  //スプレッドシートIDを使って、スプレッドシートオブジェクトを取得
  var sheet = SpreadsheetApp.openById(sheetObj.id).getActiveSheet()
  Logger.log(sheet.getName())  
}

変換したスプレッドシートにシートが複数ある場合には、getSheetByName() でシート名を指定します。


フォルダにあるExcelファイルを一括変換する

次は、フォルダにある複数のExcelファイルを一括でGoogleスプレッドシートに変換する方法です。

フォルダにExcelファイル以外がある場合には処理されないようにしています。

function convertExcels() {
  //Excelがあるフォルダ
  let sourceFolderId = 'XXXXX'
  //スプレッドシート出力先フォルダー
  let destFolderId = 'XXXXX'

  // フォルダ配下のファイルを取得
  var sourceFolder = DriveApp.getFolderById(sourceFolderId)
  var folderFiles  = sourceFolder.getFiles()

  // Excelファイルをイテレートして順にスプレッドシートに変換
  while(folderFiles.hasNext()) {
    var file = folderFiles.next()
    //Excelファイルの判断   
    if (file.getMimeType() == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')  {
      var fileName  = file.getName()
      Logger.log(fileName)
      Logger.log(file.getMimeType())
      var option =  {
        mimeType:MimeType.GOOGLE_SHEETS,  //Google sheets
        parents:[{id:destFolderId}],      //出力先フォルダーを指定
        title:fileName                    //出力先ファイル名
      }
      Drive.Files.insert(option,file)
    }
  }
}

フォルダにあるファイル一覧を取得しているのが、8〜9行目です。

そして、12行目の while文のhasNext() で13行目の next()で取得したファイル一覧から取り出せるファイルが残っているかを調べて、順番にファイルを処理していきます。

Excelファイルだけを変換するために、取り出したファイルがEXCELファイルかを15行目で調べています。

18行目でgetMimeType()でファイルのMIMEタイプを出力していますが、application/vnd.openxmlformats-officedocument.spreadsheetml.sheet が、拡張子xlsのExcelファイルとなります。


GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2022年1月3日 

GASでシートを処理するなら配列を使え!

ゾウでもわかる Google Apps Script

GoogleスプレッドシートをGAS(Google Apps Script)で処理するのであれば配列を覚えましょう。配列を使ってシートの読み書きを一括で行うと処理が格段に速くなります。


なぜ配列を使うのか

GASには、

  • Google Sheets のアクセスに時間がかかる
  • 6分で処理が強制停止される

という制約があります。

Googleスプレッドシートを操作するときに

  • getRange()
  • getValue()/getValues()
  • setValue()/setValues()
  • getLastRow()/getLastColmun()

などのメソッドを使いますが、これらの呼び出しには時間がかかります。

行数が多いシートでは、1行づつシートを読み書きするのと、配列で一括で読み書きするのでは分単位で処理時間が違ってきます。


二次元配列とは

「でも、配列って難しそう、、、」

と、思われがちですが、二次元配列はシートと同じと考えれば簡単です。下記のように行と列で表記されます。

values[行][列]

valuesは変数名です。後ろに続く最初の[]が行で、次の[]が列です。シートの"A1"という表記はA列1行目と列行の順番ですが、配列は行列の順番です。

注意点としては配列のインデックスは0から始まるので、 A1と同じ1行1列目のデータはvalues[0][0]となります。

  • 二次元配列はシートと同じ行と列
  • 表記の順番は[行][列]
  • インデックスは0から始まる

これだけ理解できれば配列を自在に扱うことができます。

では、下記がシートで考えた場合にどのデータを指しているか考えてみてください。

  • values[1][2]
  • values[2][0]

答えは、

  • 2行3列目のデータで C2
  • 3行1列目のデータで A3

です。

また、values[0]と記載した場合は1行目のデータとなり、values と[]を付けなければ配列全体となります。


サンプルデータで配列の中身を確認する

実際に、4行3列のサンプルデータで二次元配列を確認していきます。

二次元配列のサンプル

下記のデータをGoogleスプレッドシートにコピーしてください。

グループ男性女性
A1020
B2010
C1515

シートをシートIDとシート名から取得していますので、下記のコードの sheetId と sheetName の値を変更してください。

シートIDはURLの赤字の部分となります。

https://docs.google.com/spreadsheets/d/abcd1234/edit

シート名は、シートに付けている名前です。

シート名:シート1

下記のコードを実行します。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
	
  Logger.log(values[1][2])
  Logger.log(values[2][0])
  Logger.log(values[0])
  Logger.log(values)
}

実行結果は下記の通りです。

20.0
B
[グループ,男性,女性]
[[グループ,男性,女性],[A,10.0,20.0],[B,20.0,10.0],[C,15.0,15.0]]

最後の配列全体の出力結果はカンマ区切りでデータが続いているので一見難しそうです。しかし、各要素を見ると[]が各行のデータで、その中に各列のデータがカンマで区切られていることがわかります。


シートのデータを配列に読み込む

上記サンプルコードの8行目でシート全体(データ範囲)を読み込んでいます。

var values = sheet.getDataRange().getValues()

getRange()で、下記のように範囲を指定してシートを読み込むこともできます。

getRange(開始行、開始列, データを読み込む行数, データを読み込む列数)

データを読み込む行数と列数はgetLastRow()、getLastCloumn()でそれぞれ取得できます。

//範囲を指定して読み込む
var lastRow	= sheet.getLastRow()	//最終行の取得
var lastColumn	= sheet.getLastColumn()	//最終列の取得
var values = sheet.getRange(1, 1, lastRow, lastColumn).getValues()

しかし、シート全体を読み込むのであれば、getDataRange() で範囲を指定しない方が簡単です。


ヘッダー行の削除 shift()メソッド

計算処理などをする場合にはヘッダー行は不要です。

範囲指定で2行目から最終行を読み込むこともできますが、シート全体を配列に読み込んでから1行目を削除した方が簡単です。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
	
  //ヘッダー行の削除
  values.shift()

  Logger.log(values)
}

shift()メソッドは、二次元配列先頭の一次元配列を削除します、、、。簡単にいえば、1行目のデータの削除です。

配列の全行処理は for ... in が便利

配列を全行処理するには、for...in 文が便利です。

下記の11-13行目が for...in の記載となります。これで、in の後ろに記載した配列(values)を1行づつ処理することができます。変数 i には現在処理をしている行数が格納されます。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
  
  //各行の出力
  for (var i in values) {
    Logger.log(values[i])
  }
}

前述したように配列のインデックスは0から始まりますので、変数 i の出力結果は、0, 1, 2 となります。

普通にforループで記載するならlengthプロパティで配列の要素数(行数)を求めてループさせます。

下例では要素数(length)は3です。0行目、1行目、2行目と3回ループを回すので配列の要素数未満という条件でループさせます。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
  
  //各行の出力
  numOfRow = values.length
  for (var i=0; i < numOfRow; i++) {
    Logger.log(values[i])
  }
}

for ... in文で記載した方が簡単です。

末尾に列を追加する push()メソッド

次に男性と女性の数を合算して列の最後(D列)に追加します。

繰り返しとなりますが、配列のインデックスは0から始まりますので、男性は values[i][1] で女性は values[i][2]です。

行の最後に列を追加するには、push()メソッドを使います。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
  
  //各行の出力
  for (var i in values) {
    values[i].push(values[i][1] + values[i][2])
  }
  Logger.log(values)
}

i行目の最後に男性と女性の合算値を追加して、最後に配列全体を出力しています。


ヘッダー行の追加 unshift()メソッド

合算値を追加した新しいヘッダー行を unshift()メソッドで先頭行に追加します。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
  
  //男性と女性を合算して行の末尾に列を追加
  for (var i in values) {
    values[i].push(values[i][1] + values[i][2])
  }

  //ヘッダー行の追加
  values.unshift('グループ','男性','女性','合計')

  Logger.log(values)
}

配列のデータをシートに書き戻す

最後にシートに書き戻します。シートにデータを書き込むには、書き込む範囲を getRange() で指定する必要があります。

getRange(開始行, 開始列, 行数, 列数)

開始行と開始列は、それぞれ1行目と1列目です。

行数は、values.length です。列数は配列の最初の行の長さ values[0].length で求まります。

指定した範囲に 配列 values を書き込みます。

function myFunction() {
  //シートオブジェクトの取得
  var sheetId = 'abcd1234'
  var sheetName = 'シート1'
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName)

  //配列へシート全体を読み込む
  var values = sheet.getDataRange().getValues()
  
  //男性と女性を合算して行の末尾に列を追加
  for (var i in values) {
    values[i].push(values[i][1] + values[i][2])
  }

  //ヘッダー行の追加
  values.unshift('グループ','男性','女性','合計')

  //配列をシートに書き込む
  var values = sheet.getRange(1, 1, values.length, values[0].length).setValues(values);
}

シートを確認すると、配列で計算した結果が書き込まれているのがわかります。

配列をシートに書き込んだ結果

以上が、

  1. シートから配列に読み込む
  2. 配列で処理をする
  3. 配列をシートに書き込む

という、一連の処理の簡単な説明となります。


GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2021年11月9日 

Google Apps Script でWebスクレイピング

ゾウでもわかる Google Apps Script

GAS(Google Apps Script)でWebサイトをスクレイピングする方法の説明です。

matchメソッドで正規表現を駆使してもスクレイピングをできなくはないですが、Parserライブラリを使った方が100倍簡単です。

GASでスクレイピングすれば、毎日何時にとか何時間毎にといったようにWebサイトから定期的に情報収集することができます。


GASライブラリとは

簡単にいってしまえば、他の人が作った関数を利用できる機能がライブラリです。面倒臭い作業も賢い人が作ったライブラリを使えば自分でプログラミングする必要はなくなります。


Parserライブラリを導入する

Parserは、スクレイピングをするときにとても便利なライブラリで、下記で公開されています。

https://script.google.com/home/projects/1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw/edit

ライブラリを使用するときには、スクリプトIDが必要となりますので、

1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw

の部分をコピーしてください。

Parserライブラリ

ライブラリの追加方法

スクリプトエディタを開いてライブラリの「+」をクリックします。

スクリプトエディタでライブラリを追加する

ライブラリ追加画面となりますので、スクリプトIDをペーストして、検索をクリックして追加すればParserライブラリが利用可能となります。

  1. ParserのスクリプトIDを入力
  2. 検索をクリック
  3. 追加をクリック
スクリプトIDで検索してParserライブラリを追加する

HTMLデータを取得する

毎日データが変わる 12星座運勢ランキング - Yahoo!占い を使って説明していきます。

先ずは、HTMLデータを取り込みます。

HTMLを取り込むときの注意点は文字コードとして何が使われているかです。文字コードを指定しないと文字化けしてしまいます。

Yahoo!占いで使われている文字コードは "euc-jp” です。<head>から</head>に charsetの記載がありますので探してみてください。

<meta http-equiv="Content-Type" content="text/html; charset=euc-jp">

ちなみに、このブログである「象と散歩」の文字コードは "UTF-8” です。

<meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>

GASでHTMLデータを取得するには UrlFetchAppクラスを使いますが、文字コードはgetContentTextで指定します。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  Logger.log(html)
}

これでhtmlに格納されたHTMLデータが出力されます。


Parserライブラリの使い方

Parserの書き方は簡単で、抽出したいデータの中にある開始文字列終了文字列に挟まれている文字列を抽出できます。

Parser.data(‘抽出データ').from(‘開始文字列').to(‘終了文字列').build()

開始と終了に指定する文字列はhtmlタグである必要はありません。

また最後のbuild()関数は、最初に見つけたひとつだけを抽出する場合で、複数のデータを抽出するのであれば、iterate()を使います。

Yahoo!占いのタイトルと更新日を抽出する

では、早速 Yahoo!占い 12星座運勢ランキングからタイトルと更新日を取得してみます。「12星座運勢ランキング」というのは他のところからも取得できますが、画像で指定されているところから抽出してみます。

Yahoo!12星座運勢ランキング

HTMLソースでは下記の部分となります。

<h2><img src="https://s.yimg.jp/images/fortune/images/common/yftn_tt01_txt08.gif" alt="12星座運勢ランキング"></h2>
<p class="txt">2021年11月6日(土)</p>

「12星座運勢ランキング」を取得するために、直前の文字列 alt=" と直後の文字列 "> を指定します。

更新日には、開始と終了に <p class="txt"> と </p>を指定します。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  var title = Parser.data(html).from('alt="').to('">').build()
  var lastUpdated = Parser.data(html).from('<p class="txt">').to('</p>').build()
  Logger.log('タイトル= ' + title)
  Logger.log('更新日 = ' + lastUpdated)
}

実行結果をみるとタイトルが正しく抽出されていません。

タイトル= Yahoo!占い" width="177" height="34
更新日 = 2021年11月6日(土)

前述したように build() は、最初に見つけたものとなるので、

<img src="https://s.yimg.jp/c/logo/f/2.0/fortune_r_34_2x.png" alt="Yahoo!占い" width="177" height="34">

が、先にマッチしてこの部分が抽出されたようです。

もう少し、開始文字列を多くして、他で一致しないようにします。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  var title = Parser.data(html).from('txt08.gif" alt="').to('">').build()
  var lastUpdated = Parser.data(html).from('<p class="txt">').to('</p>').build()
  Logger.log('タイトル= ' + title)
  Logger.log('更新日 = ' + lastUpdated)
}

今度は正しく取得できています。

タイトル= 12星座運勢ランキング
更新日 = 2021年11月6日(土)

複数の値を取得する

Yahoo!12星座運勢ランキングのソースをみると、1位から12位まで表形式(tableタグ)で組み立てられています。

1行毎に薄い色と濃い色とで変えているので、簡単に記載するとこんな形です。

<table>
  ランキング1位の項目
  <tr class="st01">
    <td>...</td>
  </tr>

  ランキング2位の項目
  <tr class="st02">
    <td>...</td>
  </tr>

  ランキング3位の項目
  <tr class="st01">
    <td>...</td>
  </tr>

  :

</table>

<tr class="st01">で始まっているのが奇数の順位のもので、<tr class="st02">が偶数順位ですが、iterate()を使うと一気に配列として取得できます。

list_odd[5]でランク11位、list_even[5]でランク12位の情報が取得できます。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  //奇数順位の星座
  var list_odd = Parser.data(html).from('<tr class="st01">').to('</tr>').iterate()
  //偶数順位の星座
  var list_even = Parser.data(html).from('<tr class="st02">').to('</tr>').iterate()
  Logger.log(list_odd[5])
  Logger.log(list_even[5])
}

Parserを複数回利用して値を取得する

Parserを繰り返し使用することで目的の値を簡単に取得することができます。

上で取得した各星座のデータからランク、星座名、コメントを取得します。

スクレイピングのプログラミングのコツは、共通点を見つけるということです。共通のロジックでデータが取得できるかという観点でHTMLデータを俯瞰的に見てください。


順位の取得

詳細項目の取得方法について順位を使って説明します。

<td class="st01"> ... </td> の中にランキング情報があり、alt=" ... "> 中に順位がありますので、Paserを2回使って順位を取得します。以下がGASのコードとなります。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  //奇数順位の星座
  var list_odd = Parser.data(html).from('<tr class="st01">').to('</tr>').iterate()
  //ランクを取得
  var rank = Parser.data(contents).from('<td class="st01">').to('</td>').build()
  rank = Parser.data(rank).from('alt="').to('">').build()
  Logger.log(rank)
}

上記で「1位」と表示されます。


順位、星座名、コメントを取得する

下記が、Yahoo! 12星座運勢ランキングから、順位、星座名、コメントを取得するGASのコードとなりますので参考にしてみてください。

function myFunction() {
  var url = "https://fortune.yahoo.co.jp/12astro/ranking.html" // Yahoo!占い
  var html = UrlFetchApp.fetch(url).getContentText('euc-jp')
  //奇数順位の星座
  var list_odd = Parser.data(html).from('<tr class="st01">').to('</tr>').iterate()
  //偶数順位の星座
  var list_even = Parser.data(html).from('<tr class="st02">').to('</tr>').iterate()
  for(var i=0; i<list_odd.length; i++) {
    //奇数順位の星座
    results_odd = parse(list_odd[i])
    Logger.log(results_odd)
    //偶数順位の星座
    results_even = parse(list_even[i])
    Logger.log(results_even)
  }
}
function parse(contents) {
  //ランクを取得
  var rank = Parser.data(contents).from('<td class="st01">').to('</td>').build()
  rank = Parser.data(rank).from('alt="').to('">').build()
  //星座名を取得
  var seiza = Parser.data(contents).from('<p class="seiza">').to('</p>').build()
  seiza = Parser.data(seiza).from('alt="').to('">').build()
  //コメントを取得
  var text = Parser.data(contents).from('<p class="ft01">').to('</p>').build()
  text = Parser.data(text).from('">').to('</a>').build();
  var results =[rank, seiza, text]
  return results
}

GASの基礎を学べる参考図書

ある程度プログラミンがわかっていれば、WEBやYoutubeでも十分に調べられると思いますが、初歩的なところからであれば参考図書は有効な学習手段です。

詳解! Google Apps Script完全入門 [第3版]」は、プラグラミング初心者にわかりやすく説明されています。GASを最初に学ぶ一冊として良書です。

Udemy オススメ講座

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

【新IDE対応】Google Apps Script(GAS)の基礎を完全習得

講師:事務職たらこ

印象に残りやすい手書き風スライドを用いGASの基本的なプログラミングを気軽に学ぶことができる。本講座でGASを活用した自動化ができるレベルにはなれないが、基礎としては十分。

Post Date:2021年10月10日 

VLOOKUPを使わない方法(INDIRECT+MATCH)

ゾウでもわかるGoogleスプレッドシート

GoogleスプレッドシートやEXCELで、他の表とキーが一致する行の値を取得するときにはVLOOKUP関数を使うのが定石です。

VLOOKUPはとっても便利な関数なんですが、不便なところもあります。

  • 検索できるのは指定した範囲の先頭列のみ
  • 取得する列は列番号で指定する

一番の問題は、VLOOKUP関数で検索できるのは先頭列のみで、取得する値がある列は検索する列よりも右側になければなりません。

もちろん表の列を移動させれば解決できますが、Google Sheetsで IMORTRANGE関数 で他のスプレッドシートを読み込んでいる場合などは致命的です。

また取得する値を列番号で指定するのも、AD列なら30と指定しなければならないので、列数が多くなるとわからなくなってしまいます。

ということで、VLOOKUP の代わりとなる方法(関数)の紹介です。


サンプルで使用するデータ

サンプルの表は「国番号一覧」と「国コード一覧」2つのシートです。

国番号一覧に2桁の国コードを取得するために、「国番号一覧」シートの「国名(日本語)」で「国コード一覧」シートの「日本語名」を検索して国名が一致する「2文字」から2桁の国コードを取得します。

下記の2表をコピペしてシートに貼り付けてください。

国番号一覧
国番号(電話) エリア 国名(日本語) 国名(英語) 国コード
60 アジア マレーシア Malaysia  
61 太平洋諸国 オーストラリア Australia  
62 アジア インドネシア共和国 Indonesia  
63 アジア フィリピン共和国 Philippines  
64 太平洋諸国 ニュージーランド New Zealand  
65 アジア シンガポール共和国 Singapore  
66 アジア タイ王国 Thailand  
81 アジア 日本国 Japan  
82 アジア 大韓民国 Republic of Korea  
84 アジア ベトナム社会主義共和国 Viet Nam  
86 アジア 中華人民共和国 Chaina  
90 アジア トルコ共和国 Turkey  
91 アジア インド India  
92 アジア パキスタン・イスラム共和国 Pkistan  
93 アジア アフガニスタン・イスラム共和国 Afghanistan  
94 アジア スリランカ民主社会主義共和国 Srilanka  
95 アジア ミャンマー連邦 Myanmar  
98 アジア イラン・イスラム共和国 Iran  

国コード一覧
2文字 3文字 英語名 日本語名
AF AFG Afghanistan アフガニスタン・イスラム共和国
AU AUS Australia オーストラリア
CN CHN China 中華人民共和国
IN IND India インド
ID IDN Indonesia インドネシア共和国
IR IRN Iran イラン・イスラム共和国
JP JPN Japan 日本国
KR KOR Republic of Korea 大韓民国
MY MYS Malaysia マレーシア
MM MMR Myanmar ミャンマー連邦
NZ NZL New Zealand ニュージーランド
PK PAK Pakistan パキスタン・イスラム共和国
PH PHL Philippines フィリピン共和国
SG SGP Singapore シンガポール共和国
LK LKA Sri Lanka スリランカ民主社会主義共和国
TH THA Thailand タイ王国
TR TUR Turkey トルコ共和国
VN VNM Viet Nam ベトナム社会主義共和国

EXCELならXLOOKUPを使う

Microsoft365(EXCEL)であれば VLOOKUP の上位互換である XLOOKUP があります。XLOOKUP関数を使えば、VLOOKUP の不便さが解消できます。

XLOOKUP 関数 に詳細の説明がありますが、今回のケースであれば、とってもシンプルに記載できます。

XLOOKUPの使い方

上図の国番号一覧シートの ”E2” に下記のように記載します。

=XLOOKUP(C2, 国コード一覧!D:D, 国コード一覧!A:A)

上式で国番号一覧シートのC2(国名=マレーシア)で、国コード一覧のD列(日本語名)を検索して、国名が一致する国コード一覧のA列の値が取得できます。

検索する値(C2)、検索する列(国コード一覧!D:D)、取得する値の列(国コード一覧!A:A)と、理解しやすい表記ができます。

国コード一覧シートは下記のようになっています。

国コード一覧シート(サンプル)

優れもののXLOOKUPですが、残念ながらMicrosoft365(旧Office365)でしか使えません。

Excel2019/2016では、後述するGoogleスプレッドシートで説明する方法を使ってください。


Google Sheets なら関数を組み合わせる

GoogleスプレッドシートにはXLOOKUP関数に対応する関数はありません。

しかし、関数を組み合わせることによってVOOKUPより便利にキーが一致する別表の値を取得することができます。


INDEX関数+MATCH関数

INDEX関数とMATCH関数の組み合わせる方法については、INDEX - ドキュメント エディタ ヘルプ にも記載されています。

VLOOKUPではできなかった検索する列が参照範囲の左端(1列目)になくても検索が可能になります。しかし、キーが一致する行の値を取得する列については列番号(数字)で指定する必要があります。

INDEX関数とMATCH関数を組み合わせる

上図の国番号一覧シートの ”E2” に下記のように記載します。

=INDEX('国コード一覧'!A:D, MATCH(C2, '国コード一覧'!D:D,0), 1)

先ずは、INDEX関数 の説明です。

=INDEX(参照範囲, 行番号, 列番号)

上の式を分解すると、

参照範囲 '国コード一覧'!A:D
行番号 MATCH(C2,'国コード一覧'!D:D,0)
列番号 1

となります。

行番号で指定しているMATC関数については後述しますが、'国コード一覧'!A:Dの中から1列目つのデータ(A列「2文字」の国コード)の値を取得するということを意味します。

続いて MATCH関数 の部分です。

構文は、

=MATCH(検索キー, 検索範囲, 検索種類)

です。検索キーで検索範囲の中で一致するものを探します。

MATCH(C2, '国コード一覧'!D:D,0)を分解すると、

検索キー C2 マレーシア
検索範囲 国コード一覧'!D:D 国コード一覧のD列
検索種類 0 完全一致

国コードシートのD列から「マレーシア」と完全一致するものを探してきます。戻り値は、一致するものが何番目に見つかったです。

=MATCH(C2, '国コード一覧'!D:D, 0)

とすると、D1から10個目で「マレーシア」と一致するので戻り値は10となります。

検索範囲は、'国コード一覧'!D:Dの部分を、D1:D19 としても戻り値は ”10” で表の行番号と一致します。

しかし、データが入っているのがD2からだといって

=MATCH(C2, '国コード一覧'!D2:D19, 0)

とすると、D2からマレーシアは9番目となるので戻り値が “9” となってしまいます。これだと行番号と一致しなくなってしまうので、INDEX関数で違う場所を参照してしまいます。

行番号は指定せずに D:D のように検索範囲を列だけにすれば必ず行番号と一致します。

国コード一覧シート(サンプル)

検索種類は、完全一致である 0 を必ず指定してください。省略してしまうと正しい答えが求められません。詳細は INDEXのヘルプ を参照してください。


INDIRECT関数+MATCH関数

INDEX+MATCH関数では、値を取得する列を列番号で指定しなければなりませんが、INDIRECT関数を使うと文字列として列を指定することができます。

百聞は一見に如かずです。

=INDIRECT("A" & 10)

これで、A10セルの値を取得できます。

INDIRECT関数を使って、国番号一覧シートの ”E2” に下記のように記載します。

INDIRECT関数とMATCH関数を組み合わせる

列番号は、上で説明したMATCH関数で取得します。

=INDIRECT("'国コード一覧'!A" & MATCH(C2,'国コード一覧'!D:D,0))

このように書くと、国コード一覧シートのD列の中でC2と完全一致する行の A列の値を取得することができます。

INDIRECTを分解すると下記のようになります。

行名 "'国コード一覧'!A" 文字列として指定
結合記号 &  
列番行 MATCH(C2,'国コード一覧'!D:D,0) 戻り値は列番号

国コード一覧のA列という指定は文字列なので、国コード一覧シートで列が変更されると式も変更しなければなりません。

それは、INDEX+MATCH関数の列番号でも同じなので、INDIRECTを使った方が、視覚的に理解しやすい式となります。

VLOOKを使わずにINDIRECT関数とMATCH関数を使ってみてはいかがでしょうか。

象と散歩:人気の投稿(過去7日間)