個人で持っている書籍の貸し借りが多くなり、書籍を一元的に管理したい!というモチベーションから、バーコードを読んで書籍情報を取得するAppを作りました。
デザインを整えたものはこんな感じになりました。
*本当は書籍をDBで管理したいのですがその話はいずれするかもしれません。知らんけど。
今回はquagga.jsとGoogleBooksAPIを用いたシンプルなWebAppにしました。
アウトライン
- 書籍情報の管理
- quagga.js
バーコードを読み取るために用いるAPIについて。 - ISBNの受け渡し
quagga.jsで読み取ったISBNを書籍情報検索に使うための処方箋について。 - 書籍情報の検索
ISBNを使った書籍情報検索について。
書籍情報の管理
(自家本を除くほとんどの)書籍には、国際標準図書番号(ISBN: International Standard Book Number)という一意のコードが割り振られています(DBのPrimary Keyのようなもの)。
従ってISBNを知ることができれば、書籍のデータを取得することが可能です。
余談
現在使用されているISBNは13桁で、「JAN書籍コード(3桁)」「国別番号(1桁)」「出版者記号・書名記号(8桁)」「チェック数字(1桁)」からなっています。
例えば978-4-16-755602-0
は次のような意味を持ちます。
978 -> 書籍であること(978または979)
4 -> 言語(日本語)
16 -> 出版社(文春文庫)
755602 -> 書籍を指定(文庫版「大地の子 二」)
0 -> チェック数字
このチェック数字は、以下のようにして求めることができます。
1. 左から数えて奇数番目の数字を足して X とする(チェック数字は除く)
2. 左から数えて偶数番目の数字を足し、3倍して Y とする
3. Z = X + Y
4. 10から Z の1桁目を引いた数の1桁目がチェック数字
const ISBN = '9784167556020';
var Z = 0;
for(let i=0; i < ISBN.length - 1; i++){
i%2 == 0 ? Z += Number(ISBN.slice(i,i+1)) : Z += 3*Number(ISBN.slice(i,i+1));
}
const ans = (10 - Z%10)%10;
quagga.js
quagga.jsはリアルタイムでバーコードを読み取りデコードすることのできるスクリプトです。
QuaggaJS is a barcode-scanner entirely written in JavaScript supporting real- time localization and decoding of various types of barcodes such as EAN, CODE 128, CODE 39, EAN 8, UPC-A, UPC-C, I2of5, 2of5, CODE 93 and CODABAR. The library is also capable of using
getUserMedia
to get direct access to the user’s camera stream. Although the code relies on heavy image-processing even recent smartphones are capable of locating and decoding barcodes in real-time.
QuaggaJSはEAN, CODE 128, CODE 39, EAN 8, UPC-A, UPC-C, I2of5, 2of5, CODE 93, CODABARといった様々なタイプのバーコードについて、リアルタイムの特定とデコードをサポートしています!重い画像処理をしているけれど、スマートフォンでも使うことができるよ!
quagga.jsは2017/06/07を最後にメンテナンスされていません。
また、localhost以外ではhttpsでないと利用できないという指摘があります。
インストール
quagga.jsはnpm
やBower
から使うこともできますが、今回はAPIを用いて実装します。
リポジトリからquaggaJS/dist/quagga.min.js
をダウンロードして適当な場所に置き、HTMLから読み込みます。
<script src="/src/js/quagga.min.js"></script>
ダウンロードしてローカルに設置する代わりとして、CDNも利用可能です。
<script src="https://unpkg.com/@ericblade/quagga2@1.7.4/dist/quagga.min.js"></script>
quagga.jsの利用
カメラから取得した映像とISBNを表示するために次のようなHTMLを準備しました。
...
<body>
<div id="searchField">
<div id="quagga__livestream"></div>
<button id="quagga__cameraOn">カメラを起動</button>
</div>
</div>
<p id="resultField"></p>
</body>
...
const buttonCamera = document.querySelector('#quagga__cameraOn');
// buttonCameraがクリックされたら発火
buttonCamera.addEventListener('click', () => {
// quagga.jsを使ってバーコードを読み取る関数
isbnLoad();
});
button#quagga__cameraOn
でquagga.jsを起動し、
#quagga__livestream
にはquagga.jsから取得した映像を表示します。
取得したISBNは#resultField
に表示させます。
quagga.jsを動かす関数を作ります。
function isbnLoad(){
Quagga.init({
inputStream: {
type: "LiveStream",
target: document.querySelector('#quagga__livestream'),
decodeBarCodeRate: 3,
successTimeout: 500,
codeRepetition: true,
tryVertical: true,
frameRate: 15,
width: 640,
height: 480,
facingMode: "environment"
},
constraints: {
facingMode: "environment",
},
decoder: {
readers: [ "ean_reader" ]
}
},
function(err) {
if (err) {
console.log(err);
return;
}
console.log("Initialization finished. Ready to start");
Quagga.start();
});
Quagga.onProcessed((result) => {
var ctx = Quagga.canvas.ctx.overlay;
var canvas = Quagga.canvas.dom.overlay;
ctx.clearRect(0, 0, parseInt(canvas.width), parseInt(canvas.height));
});
Quagga.onDetected((result) => {
document.querySelector('#resultField').textContent = result.codeResult.code;
Quagga.stop();
});
};
詳細は省きますが、Quagga.init(config, function(err){})
で初期設定を行っています。
Quagga.onProcessed()
では、カメラを起動している間、#quagga__livestream
に映像を描画させています。
Quagga.onDetected()
はバーコードが読み取られた際の動作で、
document.querySelector('#resultField').textContent = result.codeResult.code;
で#resultField
のコンテンツにISBNの値を格納しています。
そしてQuagga.stop()
でプロセスが終了し、自動的にカメラがオフになります。
書籍のバーコードは一般に2枚あり、どちらを読み取るかわかりません。
またquagga.jsの精度が高くないこともあり、意図したデータを取得できない場合があります。
そのため、Quagga.stop()
の前に取得したコードが正しいISBNかどうか判定するのが良いでしょう。
ISBNの受け渡し
ISBNが取得できたら、次は書籍情報の検索を行います。
しかしISBNの取得は一般に0ではない時間がかかるので、
単純に書籍情報を取得する関数と並べて書くと予期せぬエラーが起こります。
isbnLoad(); //quagga.jsでISBNを読み取り、#resultFeild に格納
// #resultFeild のvalueを取得する
// isbnLoad()が終了する前にこれが実行され、isbnはnullになる
const isbn = document.getElementById("resultFeild");
function(isbn) => {
// isbnから書籍情報を取得するコード
// isbnがnullなのでエラーが起こる
}
これを回避するためにasync/await
を使うこともできそうですが、
上手くいかなかったのでここでは別の方法を取ります。
MutationObserverで変更の追跡
#resultField
のデータが変更された場合に、書籍情報を検索する関数を走らせる必要があります。
そのために使うのがMutationObserver
です。
これはその名の通り、特定のDOM Elementを監視し、変更が加えられた場合にトリガーが発火するものです。
// 監視対象のDOM Element
const ISBN = document.querySelector("#resultField");
var mo = new MutationObserver(function(record, observer) {
// 変更された場合に走らせるスクリプト
// ISBNの値を取得し、
const isbn = record[0].addedNodes[0].nodeValue;
// コンソールに出力
console.log(isbn);
});
// 監視する内容
var options = {
childList: true, //子要素の変更(テキストの変更含む)
characterData: true //テキストの変更
};
// 監視開始
mo.observe(feedISBN, options);
変更が加えられた場合にはMutationRecord
が返ります。
詳細はレファレンスを確認してください。
監視する内容はMutationObserver.observe()
の第二引数として渡します。
こちらも詳細はレファレンスに譲りますが、
An object providing options that describe which DOM mutations should be reported to
mutationObserver
'scallback
. At a minimum, one ofchildList
,attributes
, and/orcharacterData
must be true when you callobserve()
. Otherwise, aTypeError
exception will be thrown.
少なくとも
childList
,attributes
,characterData
のいずれかはtrue
に設定しておいてね!
とのことです。
書籍情報の検索
書籍情報を取得するためのAPIは複数あるのですが、今回は登録が不要なGoogleBooksAPIを利用しました。
リクエストURLは次の通り。
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
JavaScriptのfetch APIを使ってGETします。
const uri = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`;
const config = {};
const res = await fetch(uri, config);
const data = await res.json();
レスポンスの形式は次の通りです。
{
"kind": "books#volumes",
"totalItems": 1,
"items": [
{
"kind": "books#volume",
"id": "n-MqAQAAMAAJ",
"etag": "GrEhUqkN06M",
"selfLink": "https://www.googleapis.com/books/v1/volumes/n-MqAQAAMAAJ",
"volumeInfo": {
"title": "大地の子 2",
"authors": [
"山崎豊子"
],
"publishedDate": "1994",
"description": "日本人戦争孤児で、中国人の教師に養育された陸一心。肉親の情と中国への思いの間で揺れる青年の苦難の旅路を、戦争や文化大革命など",
"industryIdentifiers": [
{
"type": "OTHER",
"identifier": "WISC:89074611757"
}
],
"readingModes": {
"text": false,
"image": false
},
"pageCount": 400,
"printType": "BOOK",
"categories": [
"China"
],
"maturityRating": "NOT_MATURE",
"allowAnonLogging": false,
"contentVersion": "0.1.1.0.preview.0",
"panelizationSummary": {
"containsEpubBubbles": false,
"containsImageBubbles": false
},
"imageLinks": {
"smallThumbnail": "http://books.google.com/books/content?id=n-MqAQAAMAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=n-MqAQAAMAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
},
"language": "ja",
"previewLink": "http://books.google.co.jp/books?id=n-MqAQAAMAAJ&dq=isbn:9784167556020&hl=&cd=1&source=gbs_api",
"infoLink": "http://books.google.co.jp/books?id=n-MqAQAAMAAJ&dq=isbn:9784167556020&hl=&source=gbs_api",
"canonicalVolumeLink": "https://books.google.com/books/about/%E5%A4%A7%E5%9C%B0%E3%81%AE%E5%AD%90_2.html?hl=&id=n-MqAQAAMAAJ"
},
"saleInfo": {
"country": "JP",
"saleability": "NOT_FOR_SALE",
"isEbook": false
},
"accessInfo": {
"country": "JP",
"viewability": "NO_PAGES",
"embeddable": false,
"publicDomain": false,
"textToSpeechPermission": "ALLOWED",
"epub": {
"isAvailable": false
},
"pdf": {
"isAvailable": false
},
"webReaderLink": "http://play.google.com/books/reader?id=n-MqAQAAMAAJ&hl=&source=gbs_api",
"accessViewStatus": "NONE",
"quoteSharingAllowed": false
},
"searchInfo": {
"textSnippet": "日本人戦争孤児で、中国人の教師に養育された陸一心。肉親の情と中国への思いの間で揺れる青年の苦難の旅路を、戦争や文化大革命など"
}
}
]
}
ここには書籍自体の情報に加えて、GoogleBooksの情報も記載されています。
なお、imageLinks
に格納されているサムネイル画像は解像度が低いので利用する際は注意すべきかも知れません。
rakutenやamazonも類似のAPIを提供しています(登録が必要)。
データが取得できたらHTMLに適当なタグを準備して、データを表示します。
こんな感じでしょうか。
...
<p id="title"></p>
<p id="author"></p>
<p id="published"></p>
<p id="pageCount"></p>
<p id="description"></p>
<img src="">
...
const googleBook = data['items'][0]['volumeInfo'];
document.querySelector(`#title`).textContent = `タイトル:${googleBook['title']}`;
document.querySelector(`#author`).textContent = `著者:${googleBook['authors']}`;
document.querySelector('#published').textContent = `出版年:${googleBook['publishedDate']}`;
document.querySelector('#pageCount').textContent = `ページ数:${googleBook['pageCount']}`;
document.querySelector('#description').textContent = `説明:${googleBook['description']}`;
document.querySelector('#coverImg').src = googleBook['imageLinks']['thumbnail'];
最後に
今回はいくつかの記事を参考に、quagga.jsとgoogleBooks APIを使った書籍情報を取得するappを作りました。
参考に記事を貼り付けておきます。ありがとうございました!