諸用で必要になったので、特定の文言を抽出するChromeの拡張機能を作りました。マーケットに出すものなどではなく、完全に自分用のものとなります。
初めてのGoogle Chrome拡張機能づくりです。
やりたいこと
諸用でWebにある膨大なテキストの中から、特定の文言を抽出する必要がありました。今まで手作業で抽出していたのですが、そろそろ自動化したいと思い、いろいろ考えた結果、Chrome拡張がよさそうということで作ってみました。
ちなみにChrome拡張で使う言語はjsとなっています。
抽出したい文言があるサイト上で拡張ボタンを押すと、文言が抽出され、そのサイトのコメント欄にその抽出した文言をコメントする。みたいなことをやりました。
もし拡張ボタンを押して何かをする拡張機能を作りたい!ということでしたら、以下の内容は少し役立つかなと思います。
必要なファイル
- manifest.json
- background.js
- content.js
それぞれ何をしているか解説します。
manifest.json
拡張機能のバージョン情報などの設定を書くファイルになります。
{
"name": "App Name", // アプリ名
"version": "1.0.0", // アプリのバージョン(適当)
"manifest_version": 3, // 最新であるV3を使う
"description": "description", // アプリの説明文
"icons": { // 拡張機能のアイコン、それぞれn*nのサイズで作成する
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": { // 右上の拡張機能ボタンを使うために必要
"default_icon": "icons/icon16.png" // 拡張機能ボタンのアイコン
},
"permission": ["activateTab", "scripting"], // アプリに渡す権限
"background": { // ボタンクリック時に走らせたいスクリプト
"service_worker": "background.js"
}
}
background.js
拡張機能ボタンを押された時に発動するスクリプトを書きます。
chromeが提供しているAPIのonClicked
イベントにリスナーを追加します。
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: ['content.js'] // 実行するスクリプトを指定
});
});
こうすることで、拡張ボタンクリック時にcontent.js
を実行してくれます。
content.js
いよいよメインの処理を書いていきます。
// (1) serialize document and parse
const serializer = new XMLSerializer();
const documentCopy = serializer.serializeToString(document);
const parser = new DOMParser();
const doc = parser.parseFromString(documentCopy, 'text/html');
const parent = doc.getElementById('main');
const child = doc.querySelector('#main div.table.conf.line');
// (2) remove unwanted
parent.removeChild(child);
// (3) extract
const text = parent.textContent;
const regex = /https:\/\/hogehoge.com\/hoge\/.*?(\d{1,})/g;
const found = text.match(regex);
const result = [];
found.forEach((value) => {
const reg = /https:\/\/hogehoge.com\/hoge\//g;
result.push(value.replace(reg, ''));
});
// (4)
if (result.length > 0) {
const labelLink = document.querySelector('a.comment-container');
labelLink.click();
// (5) fill in comment
document.forms['hogehoge-form'].elements['hogehoge-input'].defaultValue = result;
const registerLabel = document.querySelector('#hogehoge-comment');
registerLabel.click();
const closeLink = document.querySelector('a.hogehoge-close');
setTimeout(() => closeLink.click(), 500);
}
何をしているのか、ざっと説明をします。
// (1) serialize document and parse
const serializer = new XMLSerializer();
const documentCopy = serializer.serializeToString(document);
const parser = new DOMParser();
const doc = parser.parseFromString(documentCopy, 'text/html');
const parent = doc.getElementById('main');
const child = doc.querySelector('#main div.table.conf.line');
// (2) remove unwanted
parent.removeChild(child);
対象ページのすべてのテキストを取ってきて、そこから対象の文言を取りたかったのですが、
対象ページの特定の箇所の文言だけは取りたくなかったので、その要素だけremoveChild
しています。
(1)のserializer
やparser
では、DOMをstring
に変換して、それをまたDOMに変換しています。直接DOMをremoveChild
してしまうと、ページでその部分が削除されてしまうので、このようにしてあります。
詳しくは、以降のハマったポイントで説明します。
// (3) extract
const text = parent.textContent;
const regex = /https:\/\/hogehoge.com\/hoge\/.*?(\d{1,})/g;
const found = text.match(regex);
const result = [];
found.forEach((value) => {
const reg = /https:\/\/hogehoge.com\/hoge\//g;
result.push(value.replace(reg, ''));
});
ここでは、取得した文言の中から特定の文言だけを抽出し、抽出した文言をさらに整形してresult
配列に入れています。
// (4)
if (result.length > 0) {
const labelLink = document.querySelector('a.comment-container');
labelLink.click();
// (5) fill in comment
document.forms['hogehoge-form'].elements['hogehoge-input'].defaultValue = result;
const registerLabel = document.querySelector('#hogehoge-comment');
registerLabel.click();
const closeLink = document.querySelector('a.hogehoge-close');
setTimeout(() => closeLink.click(), 500);
}
そして、最後にもし対象の文言が存在していたら、ページのコメント欄を開いて、result
を入力し投稿ボタンを押して、コメント欄を閉じるといった操作をしています。
ハマりポイント
DOMではなく、文言のコピーがほしかった
// (1) serialize document and parse の箇所になるのですが、最初はシリアライズ&パースせずに直接DOMを触っていました。
そうするとremoveChild
でページ上の項目が消えてしまうので、困っていました。
データだけ取りたい・・と色々調べてたのですが、シリアライズでstring
に変換して、それを対象ページとは関係のないDOMにひとまず変換することで、
サイトと紐づいてないDOMのデータとして抽出することができ、removeChild
もできるようになりました。
DOMとは、Document Object Modelの略で、HTML/XML文書を扱うためのAPI。
https://www.javadrive.jp/javascript/dom/index1.html
正規表現
.*とか、意味がわからなかったのですが、いざ使ってみて少しはわかるようになったかな・・
/https:\/\/hogehoge.com\/hoge\/.*?(\d{1,})/g
こちらは、https://hogehoge.com/hoge/ABCD123
みたいなものを取得したい場合の正規表現になります。
上記の正規表現の意味としては、先頭がhttps://hogehoge.com/hoge/
で始まり、最後が1文字以上の数字で終わるような文字列というものです。
.*?は最短マッチと呼ばれるもので、条件にあう最短の部分にマッチします。
正規表現 | 説明 | 正規表現の例 | マッチする例 |
. | 任意の1文字 | A.A | ABA, ANA |
* | 直前の文字が0個以上 | A*A | A, AAA, AAAAA |
? | 直前の文字が0個または1個 | A?A | A, AAA |
.*? | 直前の任意の文字(=直前のなんでもいい文字)が0回以上繰り返す | A.*?A | ABA, ABNA |
書いててもいまいちわからないので、.*?は最短マッチなんだな〜と覚えています。
最後の/gは、オプションフラグといって、いろいろ指定できるのですが、今回は連続で検索するg
を付与し、すべてのマッチする文言を抽出します。
inputタグにvalueがリアルタイムに反映されない
document.forms['hogehoge-form'].elements['hogehoge-input'].defaultValue = result;
とやるとinput
のテキストボックスに指定した値が動的に入るはずなのですが、何度やっても入らなくて。
悩んだ末に、とりあえず投稿ボタンを押してみようと思ってボタンクリックのコードを入れたら無事内容がコメントできました。反映されないのが謎すぎますが。
ウィンドウ閉じるボタンが押せない
投稿ボタンを押したあとに小さいウィンドウを閉じるための閉じるボタンがあるのですが、それをクリックしても閉じず・・。
悩んだ末、setTimeout
を使って遅延を入れてクリックしてみたところボタンが押せました。投稿中のあとにすぐ閉じるボタンを押すコードを入れると競合してしまうのか、、?
非同期に実行してみたらできました。コールスタックが空になってからタスクキューを実行しているので、競合しない・・?
まとめ
簡単なChrome拡張機能を作ってみました。できると嬉しいですねっっ!
正規表現とDOMの操作に関しても勉強になりました〜!:)
にほんブログ村に参加しております!よろしければクリックお願いします 🙂
にほんブログ村
コメント