魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

Amazonの綺麗なURLを取得したい 【Chrome拡張版】

ブックマークレット版はすでに紹介しましたが、もともとは Chrome 拡張として作りたいと思っていました。

kesumita.hatenablog.com

とりあえず動くものはできたので紹介します。

先に成果物

github.com

初期案

動かし方のイメージとしては、疑惑のリンクを右クリックしたときにコンテキストメニュー(右クリックで出てくるメニュー)の中にその「綺麗なURLを別タブで開く」ボタンが選択できる形式です。

なので、選択されたURLに対して GET メソッドの HTTP リクエストをしないといけないわけですが、使ったのが fetch API です。

こんな感じで、 res オブジェクトの url 属性には最終的なリダイレクト先のURLが入ってきます。

fetch(srcUrl).then(res => {
    if (!res.ok) {
        return Promise.reject(new Error(`${res.status}: ${res.statusText}`));
    } else {
        return res.url;
    }
});

developer.mozilla.org

ちなみに Promise とか async / await のあたりを全く知らなかったので一から勉強しました。

jsprimer.net

このページがおすすめです。

話を戻しまして、これで取得したURLを加工して余計な文字列を排除すればOKということで作ってみたのですが、URLの加工のパターンを考慮漏れがない形で実装するのが面倒だと思いました。

例えばこちらですが、

www.amazon.co.jp

URLをそのままコピペすると

https://www.amazon.co.jp/%E8%B3%83%E5%8A%B4%E5%83%8D%E3%81%A8%E8%B3%87%E6%9C%AC-%E5%B2%A9%E6%B3%A2%E6%96%87%E5%BA%AB-%E3%82%AB%E3%83%BC%E3%83%AB-%E3%83%9E%E3%83%AB%E3%82%AF%E3%82%B9/dp/400341246X/?_encoding=UTF8&pd_rd_w=umEiV&content-id=amzn1.sym.1a81fc55-56fb-47c1-b953-191894c78d90&pf_rd_p=1a81fc55-56fb-47c1-b953-191894c78d90&pf_rd_r=ZGNPH0H3JVV4WQPXT1PS&pd_rd_wg=w7ctw&pd_rd_r=d4bb6a84-0fcb-488a-998d-16b42e401c84&ref_=pd_gw_ci_mcx_mi

のようになります。これを省略したいとします。

まずクエリストリングは全部除去できます。(なおかつ ? より後ろを無条件にカットするだけなので簡単)

https://www.amazon.co.jp/%E8%B3%83%E5%8A%B4%E5%83%8D%E3%81%A8%E8%B3%87%E6%9C%AC-%E5%B2%A9%E6%B3%A2%E6%96%87%E5%BA%AB-%E3%82%AB%E3%83%BC%E3%83%AB-%E3%83%9E%E3%83%AB%E3%82%AF%E3%82%B9/dp/400341246X

その上で日本語の商品名がURLエンコーディングされたこの文字列を削除します。

%E8%B3%83%E5%8A%B4%E5%83%8D%E3%81%A8%E8%B3%87%E6%9C%AC-%E5%B2%A9%E6%B3%A2%E6%96%87%E5%BA%AB-%E3%82%AB%E3%83%BC%E3%83%AB-%E3%83%9E%E3%83%AB%E3%82%AF%E3%82%B9

最終的には以下が求まります。

https://www.amazon.co.jp/dp/400341246X

この dp の後ろの文字列は、ブックマークレット編 でも述べた通り ASIN と呼ばれる商品コードで、各ページに記載されているものになります。

ただ、毎回このロジックで取れるわけではなく例外もあります。そしてその例外がどれぐらいあのかもよくわかっていない。

DOM から ASIN を読み取る編

ググったところ、以下のように id = 'ASIN' の要素を見れば値が取れるようです。

let asin = document.getElementById('ASIN').value

これを開いたページ内で実行するのが、前回のブックマークレット編でした。

gist.github.com

が、自分はブックマークバーを表示しない派(画面を広くしたい)であり、 Chrome 拡張としてのアウトプットもほしいので別の方法を考えます。

manifest v3 では service_worker は DOM にアクセスできない

一番苦労したのはここです。

Chrome 拡張では、 manifest.json というファイルに「どのスクリプトを実行するのか」や「どんなアクションをこの拡張に許可するのか」を定義します。 で、この manifest.json ですが、今は V3 が最新になっています。

developer.chrome.com

この V3 ですが、 V2 と比べて色々変更されたらしく、ググって出てきた V2 時代のやり方を適用しようにも根本的に適用できないパターンがありました。 それがこの service_worker では DOM にアクセスできないという話です。

なので、コンテキストメニューでクリックされたときにページのDOMを読み込んで、先ほどの ASIN を取得して…というのができないです。

問題点を整理

今の問題点を一旦整理します。

  • ASIN をURLから確実に取得する方法がわからない
    • A-1. 該当ページのDOMが取得できれば、そこから ASIN を取得できるので確実
    • A-2. 正規表現で頑張る
  • Chrome 拡張の service_worker では DOM を取得できない → そのままでは A-1. が使えない
    • B-1. 外部スクリプトを呼び出して A-1. を実行する
    • B-2. fetch API で取得した URL から A-2. を実行する(できる?できそう)
    • B-3. fetch API で取得した URL からその先の DOM を取得し、 A-1. を実行する(できる?)

そして A-1. 単体が前回のブックマークレットというわけですね。

本題

いくつか方法はありそうでしたが、 B-1. の外部のスクリプトで実行するようにしました。 こんな感じです。

  • backgroud.js
// 外部スクリプトを実行
const res = await chrome.scripting.executeScript({
    target: {tabId: tab.id},
    files: ['./content.js'],
});

// 外部スクリプトからの戻り値
const url = res[0].result;
if (url) {
    chrome.tabs.update({url: url});
};
  • content.js
// DOM にアクセスする
(() => {
  try {
    const baseUrl = 'https://www.amazon.co.jp/dp/';
    const asin = document.getElementById('ASIN');
    if (asin) {
      return baseUrl + asin.value;
    };
  } catch (err) {
    console.error(`エラーが発生しました (${err})`);
  }
})();

全体

何がベストかを語れるレベルはないので、コア部分をそのまま載せておきます。

backgroud.js

  • AmazonのURLかどうかを判定する(ガバガバ)
function isAmazonUrl(pageUrl) {
  const pattern = 'https://www.amazon.co.jp';
  return pageUrl.includes(pattern);
}
  • 右クリックメニューを追加する
chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({
    id: 'open-good-amazon-url',
    type: 'normal',
    title: 'Amazonの綺麗なURLを開く',
    contexts: ['page']
  });
});
  • 右クリック時の挙動を定義する
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  try {
    const pageUrl = info.pageUrl;
    if (!isAmazonUrl(pageUrl)) {
      console.log('Amazon のリンクではありません')
      return
    };

    const res = await chrome.scripting.executeScript({
      target: {tabId: tab.id},
      files: ['./content.js'],
    });

    const url = res[0].result;
    if (url) {
      chrome.tabs.update({url: url});
    };
  } catch (err) {
    console.error(`エラーが発生しました (${err})`);
  }
});

content.js

(再掲)

// DOM にアクセスする
(() => {
  try {
    const baseUrl = 'https://www.amazon.co.jp/dp/';
    const asin = document.getElementById('ASIN');
    if (asin) {
      return baseUrl + asin.value;
    };
  } catch (err) {
    console.error(`エラーが発生しました (${err})`);
  }
})();

まとめ

途中であげた問題点を再掲します。

  • ASIN をURLから確実に取得する方法がわからない
    • A-1. 該当ページのDOMが取得できれば、そこから ASIN を取得できるので確実
    • A-2. 正規表現で頑張る
  • Chrome 拡張の service_worker では DOM を取得できない → そのままでは A-1. が使えない
    • B-1. 外部スクリプトを呼び出して A-1. を実行する
    • B-2. fetch API で取得した URL から A-2. を実行する(できる?できそう)
    • B-3. fetch API で取得した URL からその先の DOM を取得し、 A-1. を実行する(できる?)

今回できたのは、 A-1.B-1. でした。

本当は初期案のように、該当ページを開く前に綺麗なURLを取得して右クリックで開けるようにしたかったです。 なので、次に無難そうな B-2. をやるかもしくは(出来るかどうかそもそも知らないのですが) B-3. も挑戦したいです。

というより B-3. でなんとかできないか、という方向で頑張ってたんですが、結局何がうまくいってないかのかわからなくなって諦めた感じです。リベンジしたい。

得たもの

  • JavaScript ちょっとだけわかった
  • JavaScript と Promise, async, await について学ぶことができた
  • Chrome 拡張を作ることができた

参考ドキュメント

developer.chrome.com

developer.chrome.com

developer.chrome.com