このブログは更新を終了しました。移転先はこちらです。

2023-02-21

NTA-DIY:1ヶ月目⑦~DOMとAmazonブックマークレット~

 前回、Scrapboxのページに文字列を追記するブックマークレットの話をしました。

 現在日時や開いているタブのタイトル・URLの取得を覚えたので、次はWebページの内容をScrapboxにクリップするブックマークレットを作ってみることにしました。手始めにAmazonの書籍情報です。


 いきなりゼロからでは何もできないので、先人の知恵を借りることにしました。教材はこちらです。


 Webページから情報を取得する際に必要なのはDOM(Document Object Model)の概念の知識です。要するにJavaScriptを通してHTML要素を指定したり操作したりする方法を知る必要があるわけです。

 JavaScriptのキモはDOM操作にあると思いますが、DOMはJavaScriptがHTMLやSVGなどを操作できるようにするためのAPIであって、JavaScript言語の中に含まれているものではないので、JavaScriptの基本を勉強する時にはすぐには登場しない場合も多いのではないかと思います。(DOMはあくまでAPIだ、ということはコードを書いている中ではほとんど意識しません。JavaScriptが当然に持っている機能かのように扱うことになると思います。というか、あくまでAPIだとか言われても初学者にはなんのこっちゃでしょう。)

 NTA-DIY(ノートテイキングアプリのDIY)に於いてはDOM操作がチョー重要でゴリゴリ多用することになります。逆に言うと必要に迫られるのですぐ覚えます。最初の一歩を踏み出せれば後はその都度調べてどうにかなるでしょう。

 その最初の一歩として、Amazonから情報を取得するブックマークレットを理解することは大いに役立ちました。「DOMの基本を順を追って学ぶ」というやり方だとまっさらなHTMLにひとつひとつ自分で要素を作っていくところから始めることになると思いますが、ブックマークレットの解体を通してWebページとの関連を理解することは、既に出来上がっている立派なWebページを解釈することから始まるので、実用的な知識と感覚が早く身につくのではないかと個人的には思っています。


 Webページをクリップする系のブックマークレットに必要なのは、基本的に「この要素の中身のこの文字列を取り出す」ということだと思います。「この要素」をどうやって指定して、そのうちの「この文字列」をどう絞り込むか、がまず覚えるべきことです。

 「この要素」の指定のために使うメソッドは、最低限「document.querySelector()」、「document.querySelectorAll()」を覚えれば用は足ります。他のメソッドとは「指定した後に更新された場合にそれが反映されるかどうか(動的か静的か)」「処理速度が速いか遅いか」で使い分けが発生しますが、クリップするにあたっては実行した時点のデータを取得できればいいので、静的な一方で指定方法が柔軟なquerySelector/querySelectorAllは初学者にはありがたいメソッドです。特にCSSに慣れている人はセレクタの指定方法が同じなのでわかりやすいでしょう。

 便利な反面、処理速度は少し遅くなります。大量に処理する場合には影響が生じる可能性があるので、「document.getElementById()」「document.getElementsByClassName()」などを使えた方が後々楽ということになります。

 私は今のところ、id指定の時は「document.getElementById()」、それ以外はわかりやすい「document.querySelector()」「document.querySelectorAll()」を使うことが多いですが、多分動的なgetElementBy系のメソッドをちゃんと使ってもっとうまく指定した方がいいのだろうと考えているところです。(静的なquerySelector系だけでも素人の趣味ならそれほど支障はないということでもあります。)

 さてgetElementByにしろquerySelectorにしろ、それだけでは要素全体を取得するだけでそこに含まれる文字列を取得することはできません。「この文字列」を指定するには、「textContent」か「innerText」というプロパティを使う必要があるわけです(この二つは似ていますが少し違います)。そしてこれらは子要素まで含めて取得することにも留意が必要です。場合によっては正規表現を使うことにもなります。(この記事は体験記であって説明記事ではないので詳しくは解説しません。)


 慣れれば目的の要素・目的の文字列をズバッと指定できるようになりますが、最初は取得を試みても何をどう取得したことになっているのかがよくわかりませんでした。

 そこで、対象の指定がうまくいっているかを確認するために活躍したのがコンソールと「console.log()」メソッドでした。コンソールとはWebブラウザのデベロッパーツールを開くと表示される欄で、consoleメソッドの結果が書き込まれている場所です。コードを書いてEnterすればその場で実行することもできます。

 そのコンソールでquerySelectorなどを試し、その結果をconsole.logで確認します。最初は理解が追いついていないので闇雲にならざるを得ず、とにかくうまくいくまで試しました。うまくいったらそれをブックマークレットに反映させます。console.logがいつ役に立つのかそれまでピンと来ていなかったのですが、DOM操作の試行錯誤のために繰り返し使ったのでコンソール上でもコード内でも「console.logで確認する」という習慣がすっかり身につきました。


 Amazonに関して作ったブックマークレットが以下のものです。なお挙動は当初の通りですが、コードそのものはこの記事を書くにあたって改めました。

(function () {
// Amazonでなければ無効
const href = window.location.href;
if(!href.includes('www.amazon.co.jp') || !href.includes('/dp/')) return alert('Amazonの書籍ページではありません。');
// 自分のScrapboxのプロジェクトURLを指定
const projectName = 'hoge';
// 書名の欄を取得する
const titleElm = document.getElementById('productTitle') || document.getElementById('ebooksProductTitle');
// 取得した書名をデフォルト値としてページタイトルを決める
let title = window.prompt('選択範囲を概要としてスクラップします。', titleElm.innerText.trim()).trim();
if (!title) return;
title = '『' + title + '』'; // 『』をつける
// サブタイトル(書名直後のバージョンと発売日が書いてある部分)を取得する
const subtitleElm = document.getElementById('productSubtitle');
const sub = subtitleElm ? '\n' + subtitleElm.innerText : ''; // サブタイトルがあれば内容を取得して頭に改行を付与
// 選択範囲を取得する
const getSelection = window.getSelection(); // 選択範囲を文字列として取得
let selection = '';
if (getSelection && getSelection.toString()) { // 選択範囲があるとき
selection = getSelection.toString().replace(/(\W+)( )(\W+)/g, '$1$3'); // 字間に時々紛れている半角スペースを除去
selection = '\n>' + getSelection.toString().replace(/\n/g, '\n> '); // 行ごとに引用の「>」を付与
}
// 書影を取得する
const image = document.getElementById('imgBlkFront') || document.getElementById('ebooksImgBlkFront');
const imageSrc = image.getAttribute('src');
// 著者を取得する
const authors = [];
const authorElms = document.getElementsByClassName('author');
for (const elm of authorElms) {
const content = elm.innerText.replace(/\r?\n|\t/g, '').replace(/,/, '');
const category = content.match(/\(.+\)/); // (著)などの部分
const authorName = content.replace(/\(.+\)/, '').replace(/ /g, '');
authors.push(`${category}[${authorName}]`);
}
// 以上の情報を、実際に書き込みたい形に整えてひとつの文字列にまとめる(必要があれば任意の文字列を付け足す)
const body = `[${imageSrc} ${window.location.href}]\n${authors.join(' ')}${sub}\n\n${selection}`;
// エンコードしてURLをつくる
const url = `https://scrapbox.io/${projectName}/${encodeURIComponent(title)}?body=${encodeURIComponent(body)}`;
// URLを開いてページを作成(または同名のページに追記)
window.open(url);
})();
// お試し(実行するとプロジェクト名の入力欄が表示されます)
/*
javascript:(function(){const href=window.location.href;if(!href.includes('www.amazon.co.jp')||!href.includes('/dp/')) return alert('Amazonの書籍ページではありません。');const projectName=prompt('今開いている書籍情報をScrapboxに記録します。\nプロジェクトURLを入力してください。');if(!projectName)return;const titleElm=document.getElementById('productTitle')||document.getElementById('ebooksProductTitle');let title=window.prompt('選択範囲を概要としてスクラップします。',titleElm.innerText.trim());if(!title)return;title='『'+title.trim()+'』';const subtitleElm=document.getElementById('productSubtitle');const sub=subtitleElm?'\n'+subtitleElm.innerText:'';const getSelection=window.getSelection();let selection='';if(getSelection&&getSelection.toString()){selection=getSelection.toString().replace(/(\W+)( )(\W+)/g,'$1$3');selection='\n>'+getSelection.toString().replace(/\n/g,'\n> ')};const image=document.getElementById('imgBlkFront')||document.getElementById('ebooksImgBlkFront');const imageSrc=image.getAttribute("src");const authors=[];const authorElms=document.getElementsByClassName('author');for(const elm of authorElms){const content=elm.innerText.replace(/\r?\n|\t/g,'').replace(/,/,'');const category=content.match(/\(.+\)/);const authorName=content.replace(/\(.+\)/,'').replace(/ /g,'');authors.push(`${category}[${authorName}]`)};const body=`[${imageSrc} ${window.location.href}]\n${authors.join(' ')}${sub}\n\n${selection}`;const url=`https://scrapbox.io/${projectName}/${encodeURIComponent(title)}?body=${encodeURIComponent(body)}`;window.open(url)})()
*/

 これは冒頭で紹介したお手本の理解に挑みながら、少しのチャレンジを試みています。具体的には、手本にはないHTML要素からの文字列の取得と、選択範囲の取得です。その他の部分はほぼ手本のコピペになっています。

 このブックマークレットは手本をちょっと弄った程度のものですが、この他にGooglePlayや他の出版社からクリップするブックマークレットを作りました。上述の悪戦苦闘はそちらに挑んだ時のことです。

 一連のブックマークレット作りによって、デベロッパーツールを用いてHTML要素を探してDOM操作するということが身につき、自分のツールを一から作る土台ができたと思います。



このシリーズ記事の概要はこちら→ノートテイキングアプリDIY体験記