JavaScriptのライブラリー html5-qrcode を使えば、スマートフォンやPCのカメラを使ってQRコード1を連続して読み取り、スプレッドシートに記録するウェブアプリを無料で作れます。バーコードもOKです🚀
QRコードを「ぴっ」「ぴっ」と連続して読み取って、スプレッドシートに追記していくApps Scriptを作ってみました! https://t.co/lYzWHGKhbh pic.twitter.com/XH3kcYuNqo
— 高玉 広和 (@takatama_jp) February 23, 2022
デモに使ったスプレッドシートとApps Scriptはこちらです。どうぞコピーしてご自由にお使いください2。なお、Google Workspace(for Education)を使っている企業(学校)の方は、ブラウザーをシークレットモードにしてアクセスしてみてください。
使っている技術については後述していきます。
ちなみに、GoogleのノーコードツールAppSheetを使えば、連続読み取りはできないものの、QRコードを使うアプリを簡単に作れます。よろしければご覧ください。
注意事項
QRコードの読み取りは、端末やカメラの性能、光源などによって大きく左右されます。
QRコードリーダーとして使いたいスマホやPCと、読み取らせたいQRコードを準備の上、次のcodepen.ioのURLにアクセスし、期待通りの読み取りができることをお試しください。読み取りが成功すると、音が出ます。
使い方
スプレッドシートをコピーして、Apps Scriptをウェブアプリとして公開します。
- スプレッドシートにアクセスして、上部メニュー「ファイル」>「コピーを作成」でコピーします。Apps Scriptも一緒にコピーされます。
- コピーしたスプレッドシートの上部メニュー「拡張機能」>「Apps Script」でスクリプトエディターを開きます。
- Apps Scriptのスクリプトエディター右上にある「デプロイ」ボタンから「新しいデプロイ」を選び、アプリの種類で「ウェブアプリ」を選択の上、デプロイします。
- 権限が求められたら付与してください。
URLが払い出されるので、PCもしくはスマートフォンのウェブブラウザーからアクセスしてください。
- アクセスしたら、Request Camera Permissions ボタンを押して、カメラを許可します。
- QRコードを読み取ると、スプレッドシートのシート「Data」に、日付と読み取り結果を追記していきます。
- iOSで音を鳴らす場合は、一度
Sound ON (iOS)
ボタンを押してください。Androidは何もしなくても音が鳴ります。
なお、スクリプトを書き換えた場合は、スクリプトエディターで再デプロイが必要になりますのでご注意ください(後述します)。
使っている技術
複数の技術を組み合わせて実現しています。
- バックエンド
- Google Apps Script
- doGet() を使ったウェブアプリ化
- スプレッドシートへのデータ追加
- Google Apps Script
- フロントエンド
- html5-qrcode
- QRコードのスキャン
- HTMLAudioElement
- 読み取り音の再生
- Google Apps Script
- google.script.run() を使ったバックエンドのAPI呼び出し
- html5-qrcode
- その他
- IMAGE関数と、Google Charts API
- QRコードの作成
- IMAGE関数と、Google Charts API
バックエンド
Apps Script は次の通りです。
const SHEET_NAME = 'Data';
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function onScan(text) {
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME).appendRow([new Date(), text]);
}
後述しますが、HTMLファイルindex
をApps Scriptに追加しています。
doGet()
で、アクセスしてきたウェブブラウザーにindex
を渡します。スマートフォンに対応するため、addMetaTag()
で画面が拡大しないよう調整しています。
onScan(text)
はフロントエンドから呼び出します。スプレッドシートに日付と受け取ったデータtext
を追記します。
ウェブアプリの再デプロイ
ウェブアプリには2種類のURLがあります。「あれ?スクリプトを書き直したのに反映されないな?」と思ったら、URLを確認してみてください。
- 開発用
- 末尾が
/dev
のURLです。 - 「新しいデプロイ」の後、「デプロイをテスト」で表示されるURLです。
- スクリプトエディターで保存したスクリプトがそのまま動きます。
- 末尾が
- 本番用
- 末尾が
/exec
のURLです。 - 「新しいデプロイ」や「スクリプトを管理」で表示されるURLです。
- 再デプロイした時点のスクリプトで動作します。
- 末尾が
開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更します。再デプロイするには「デプロイを管理」を使います。
- スクリプトエディター右上の「デプロイ」>「デプロイを管理」を選択
- 鉛筆アイコンを押して、バージョンを「新しいバージョン」にして、デプロイ
このやり方なら、新しいデプロイで払い出されたURLはそのままで、動作を変更していくことができます。
フロントエンド
Apps Scriptは次の通りです。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<div id="reader" style="width:340px;"></div>
<button id="sound">Sound ON (iOS)</button>
<div id="result"></div>
<script src="https://unpkg.com/html5-qrcode"></script>
<script>
let CURRENT_TEXT = '';
const append = (text) => {
document.querySelector('#result').innerHTML += `${text}<br>`;
};
const onScanSuccess = (decodedText, decodedResult) => {
console.log('onScanSuccess', decodedText, CURRENT_TEXT);
if (CURRENT_TEXT !== decodedText) {
CURRENT_TEXT = decodedText;
append(decodedText);
beep();
google.script.run
.withFailureHandler(function (error) {
append(`"${decodedText}"の保存に失敗しました: ${error}`);
})
.onScan(decodedText);
}
};
const html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 240 });
html5QrcodeScanner.render(onScanSuccess);
const DATA_URL = "data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=";
const AUDIO = new Audio();
const beep = () => {
AUDIO.src = DATA_URL;
}
// for iOS audio
// https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
AUDIO.autoplay = true;
document.querySelector("#sound").addEventListener("click", () => {
beep();
});
</script>
</body>
</html>
<div id="reader">
にQRコードリーダーを表示し、<div id="result">
に読み取った文字を画面に追記していきます。追記にはappend()
を使います。
<script src="https://unpkg.com/html5-qrcode"></script>
でhtml5-qrcodeを読み込みます。
CURRENT_TEXT
は2度読み防止のための変数です。
onScanSuccess()
はQRコードの読み取りが成功したときの処理です。CURRENT_TEXT
と違う文字だった場合は、新しい読み取りです。append()
で文字を追記し、beep()
で音をならし、google.script.run
で文字をバックエンドに伝えます。
HTMLAudioElementで音を鳴らす
HTMLAudioElementで音を鳴らせます。音のデータをデータURLとして文字列にエンコードしておき、先に作っておいたAudioオブジェクトのsrc属性にデータURLを代入します。
当初はnew Audio(DATA_URL).play()
で再生していたのですが(Androidでは再生できるものの)iOSでは再生できないことが分かりました。iOSでは自動再生はできず、ユーザーが操作した時にだけ音を再生します。ボタンが押されたタイミングでAudioオブジェクトで音を鳴らし、そのオブジェクトを使いまわすことで、ボタンが押された以降は音を再生できるようにしています。
google.script.runでバックエンドのAPIを呼び出す
google.script.run
を使って、バックエンドのAPIを非同期に呼び出します。withFailureHandler()
で呼び出しに失敗したときの処理を書いています。
その他
知っておくと便利なTipsを紹介します。
GoogleスプレッドシートにQRコードを表示する
スプレッドシートを使って、QRコードをお手軽に作ることができます。
例えば、A1セルに入った文字をQRコードとして表示するには、次のように書きます。ウェブページにアクセスするためのQRコードを作るときには、ENCODEURL関数を使うようにしてください。
=IMAGE("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl="&ENCODEURL(A1))
【応用編】QRコードリーダーにプルダウンメニューを追加して、QRコードと一緒に記録する
QRコードリーダーを改造して機能追加していく様子を、順を追って説明します(最後に完成したスクリプト一式を掲載します)。ここでは、QRコードリーダーにプルダウンメニューを追加し、メニューで選んだ項目を読み取ったQRコードと一緒に記録させてみます。
上図の場合、読み取ったQRコードhttps://ja.wikipedia.org
とメニュー1
をスプレッドシートに追記します。
フロントエンドにメニューを直接書き込む
まず、フロントエンドのindex.html
にプルダウンメニューを追加し、項目として、メニュー1
、メニュー2
、メニュー3
を表示します。なお、これまでは読み取ったQRコードの二度読みを禁止してきましたが、メニューを変更した場合は二度読みを可能にします。
...
<div id="result"></div>
<!-- メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
<select id="menu" onchange="CURRENT_TEXT = '';">
<option>メニュー1</option>
<option>メニュー2</option>
<option>メニュー3</option>
</select>
<script src="https://unpkg.com/html5-qrcode"></script>
...
また、選択した項目を、読み取ったQRコードと一緒にバックエンドに送信します。バックエンド側のonScan()
は、引数を1つ増やします。
const onScanSuccess = (decodedText, decodedResult) => {
...
beep();
// 選択したメニューを取得
const menu = document.querySelector('#menu option:checked').innerText;
google.script.run
.withFailureHandler(function (error) {
append(`"${decodedText}"の保存に失敗しました: ${error}`);
})
.onScan(decodedText, menu); // 選択したメニューをスプレッドシートに送信
}
};
バックエンド側のonScan()
です。スプレッドシートに追加する行は、1列目にタイムスタンプ、2列目に選択したメニュー項目、3列目に読み取ったQRコード、としています。
// 選択したメニューの値も送信する
function onScan(text, menu) {
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME).appendRow([new Date(), menu, text]);
}
ここまでに追加したコードで、メニューで選んだ項目を読み取ったQRコードと一緒に記録できるようになりました。開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更します。
メニュー項目をシートで管理する
ただ、表示するメニューを変更するたびに、再デプロイをするのは面倒です。そこで、スプレッドシートにメニュー項目一覧シートを追加し、シートを変更したらQRコードリーダーのメニューに反映するようにしてみましょう。
まず、スプレッドシートにMenu
シートを追加して、メニュー項目を1行ごとに記述します。
そして、バックエンド側にメニュー一覧を取得するfetchMenus()
を追加します。
const MENU_SHEET_NAME = 'Menu';
// メニュー一覧を取得する
function fetchMenus() {
return SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(MENU_SHEET_NAME).getDataRange().getValues().map(row => row[0]);
}
フロントエンド側で、このfetchMenus()
を呼び出して、シートに記載されたメニュー項目一覧をHTMLにして表示します。Templated HTML(テンプレート化されたHTML)を使います。
フロントエンド側のコードは次のようになります。
...
<div id="result"></div>
<!-- メニューを表示。メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
<select id="menu" onchange="CURRENT_TEXT = '';">
<? for(let menu of fetchMenus()) { ?>
<option><?= menu ?></option>
<? } ?>
</select>
<script src="https://unpkg.com/html5-qrcode"></script>
...
また、Templated HTMLを使うために、バックエンド側のdoGet()
を忘れずに変更します。
function doGet() {
// HTMLの中で fetchMenus()を呼び出ため、テンプレートを使う
const html = HtmlService.createTemplateFromFile('index').evaluate();
html.addMetaTag('viewport', 'width=device-width, initial-scale=1');
return html;
}
開発用のURLで正しく動作することを確認したら、再デプロイをして、本番用の動作を変更しましょう。
ソースコード
完成したスプレッドシートとApps Scriptはこちらです。どうぞコピーしてご自由にお使いください2。なお、Google Workspace(for Education)を使っている企業(学校)の方は、ブラウザーをシークレットモードにしてアクセスしてみてください。
フロントエンド
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<div id="reader" style="width:340px;"></div>
<button id="sound">Sound ON (iOS)</button>
<div id="result"></div>
<!-- メニューを表示ためにCode.gsのgetMenus()を呼び出す -->
<!-- メニューを変更したら読み取り結果をリセットして二度読みを可能にする -->
<select id="menu" onchange="CURRENT_TEXT = '';">
<? for(let menu of fetchMenus()) { ?>
<option><?= menu ?></option>
<? } ?>
</select>
<script src="https://unpkg.com/html5-qrcode"></script>
<script>
let CURRENT_TEXT = '';
const append = (text) => {
document.querySelector('#result').innerHTML += `${text}<br>`;
};
const onScanSuccess = (decodedText, decodedResult) => {
console.log('onScanSuccess', decodedText, CURRENT_TEXT);
if (CURRENT_TEXT !== decodedText) {
CURRENT_TEXT = decodedText;
append(decodedText);
beep();
// 選択したメニューを取得
const menu = document.querySelector('#menu option:checked').innerText;
google.script.run
.withFailureHandler(function (error) {
append(`"${decodedText}"の保存に失敗しました: ${error}`);
})
.onScan(decodedText, menu); // 選択したメニューをスプレッドシートに送信
}
};
const html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 240 });
html5QrcodeScanner.render(onScanSuccess);
// https://stackoverflow.com/questions/879152/how-do-i-make-javascript-beep
const DATA_URL = "data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=";
const beep = () => {
AUDIO.src = DATA_URL;
}
// for iOS audio
// https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
const AUDIO = new Audio();
AUDIO.autoplay = true;
document.querySelector("#sound").addEventListener("click", () => {
beep();
});
</script>
</body>
</html>
バックエンド
const SHEET_NAME = 'Data';
const MENU_SHEET_NAME = 'Menu';
// 選択したメニューの値も送信する
function onScan(text, menu) {
SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME).appendRow([new Date(), menu, text]);
}
// メニュー一覧を取得する
function fetchMenus() {
return SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(MENU_SHEET_NAME).getDataRange().getValues().map(row => row[0]);
}
function doGet() {
// HTMLの中で fetchMenus()を呼び出ため、テンプレートを使う
const html = HtmlService.createTemplateFromFile('index').evaluate();
html.addMetaTag('viewport', 'width=device-width, initial-scale=1');
return html;
}
トラブルシューティング
Q. スプレッドシートをコピーして流用したいのですが、アクセスできないようです。
- 所属組織のGoogle Workspaceの設定で、組織外のスプレッドシートへのアクセスが禁止されている場合があります。ブラウザーのシークレットモードだとアクセスできることがあります。お試しください。
Q. スクリプトを書き換えて、ブラウザーからアクセスし直しているのですが、書き換えた機能が反映されていないようです。
- 開発用と本番用のURLがあります。開発用は最新のスクリプトが反映されていますが、本番用だと再デプロイが必要です。
Q. バックエンドからフロントエンドに値を渡すよう、スクリプトを書き換えてみました。Stringだけなら渡せるのですが、objectだとうまく渡せずnullになってしまいます。
- google.script.runを使ったやり取りですが、バックエンドから渡せる値に制限があります。特にエラーが出るわけではないので気が付きにくいです。特にDate型のやりとりが制限されていることにご注意ください。
- Date型をstring型に変換するのに、
Utilities.formatDate()
が便利です。