• 作成:
  • 更新:

Googleにwebサイトのタイトルを省略させないブラウザ拡張、google-search-title-qualifiedを作りました

ゴールデンウィークなので昔考えてたブラウザ拡張を作ることにしました

auto-sudoeditをssh越しのリモートファイルに対応させる - ncaq が終わってもゴールデンウィークは終わってないのでブラウザ拡張を作ることにしました。

なんでみんなゲームとかwebシステムを作ってるのに私はEmacs拡張とかブラウザ拡張とか作ってるんですか?

GitHubはこちらです。

ncaq/google-search-title-qualified: Google will omit the title of the web page. With this add-on, the original title is used as much as possible.

Firefoxのアドオンページはこちらです。

google-search-title-qualified – 🦊 Firefox (ja) 向け拡張機能を入手

ChromeはSubmitしましたが要求する権限が多いので(検索結果のページにアクセスする必要があるのでall urlsにならざるを得ないんですよね)、審査完了に時間がかかりそうです。

公開されました。 google-search-title-qualified - Chrome ウェブストア

Safariは誰かがApple Developer Programの料金払ってくれたら公開するかもしれません。

Edge, OperaはChrome Web Store使えるみたいなので特別に対応しなくて良いか。

こんな感じになります

after

Shift_JISのページにまだ対応出来てないですが、それでも情報量が増えるので良い感じです。

以下開発メモです

開発時にメモってた内容を貼り付けておきます。

マジで雑多なメモですが未解決問題とか含むので一応貼り付けておきます。

実装だるくて放置してたアイデアが出てきました

2019年09月の文書が出てきました。構想1年8ヶ月の拡張機能と言うことになります。

Google検索でタイトルを省略させないFirefox拡張機能

Google検索で長いタイトルを省略せずに全部出すことを試みる。

省略していないタイトルのデータはソースコードには含まれていないようです。

なので自前で取得するしかありません。

fetch APIで全件フルのタイトルを取得してしまいましょう。

最大でも1ページ100リクエストでHTMLしか取得しないので思ったより大した量ではありません。

実装言語

Reactを使うわけでもなくDOMを直接弄ることとWebExtensionなどの多くのAPIを使うので、 TypeScriptが適任でしょう。

キャッシュ

そのままだとアクセスするたびに毎回100件リクエストを送ります。検索クエリをちょっとだけ変えて検索することが多いことを考えるとこれはかなり無駄です。当然キャッシュしましょう。

storage - Mozilla | MDN を使うか、 IndexedDB API - Web API | MDN を使うかは悩みどころですが、古いキャッシュを選択的に消せることを考えるとIndexedDBを使っておいたほうが良さそうです。キャッシュなので同期が必要なデータではありませんし。

構造

  1. contents_scriptがURLの一覧をメッセージでbackgroundに送る
  2. backgroundはタイトルを取得次第URLと合わせてcontents_scriptに送る
  3. backgroundはデータをキャッシュする
  4. contents_scriptはメッセージを受け取ったら指定URLのタイトルを差し替える

SPA対策

JavaScriptを動かしてタイトルを構築しないとタイトルが取得できないタイプのページは、拡張機能のスコープに入ってもフロントエンドでどうこうするのは困難です。おそらくそういったページは仮のタイトルがGoogleがレンダリングしたタイトルより短いことが予想されるため、 Googleのタイトルより短いタイトルが取得された場合はbackgroundはメッセージを送らないことを選択した方が良いでしょう。短いものにしても仕方がないですし。

PDFみたいな大きなページどうするのか

だいたいはcontent-typeで弾けそうですが、ある程度の通信量を覚悟すると言っても、 MBクラスのページを取得しに行ってしまうのは避けたいところです。 responseを解決する前にContent-Lengthを見ることは出来ますがcorsだと出来ないらしい。拡張機能のbackgroundでも適用されるかはわかりませんが。

雑にread呼んでタイムアウトさせれば良いのでは。 Fetch の中断と Promise のキャンセル方法の標準化 | blog.jxck.io

名前

大ヒットした、 Google search link fix にあやかって似たような形式にしましょう。

Google search text to full… いやtextってなんだよ。 fullってなんだよ。

Google search title to qualified

これですね。

パッケージ名とかはまあ普通に、 google-search-title-to-qualified ですかね。

いや別に本名もgoogle-search-title-to-qualifiedで良いか。統一しましょう。

初期生成

雛形ある程度欲しい… yarn createか? yarn create webextension google-search-title-to-qualified してみたけど、結局TypeScript前提じゃないから自前でやっていく必要があることが分かりました。

content_scriptsmatchesを書かないといけないがどうしよう。 Googleのサイト一覧に限定する必要があると思います。セキュリティには気をつけていくつもりですが、 HTTP通信して、それに従ってコンテンツDOMのテキストをごちゃごちゃやる以上、なるべく影響範囲は狭めておきたいですね。

Googleの持ってるドメイン一覧ってAPIあるのかな。 Stack Exchangeにはあるのですが… Usage of /sites [GET] - Stack Exchange API

これっぽい? https://www.google.com/supported_domains

これをJSONに埋め込むには…? いやまあコピペで整形しても良いんですが、手動管理は面倒そう。

maniest.jsonをTypeScriptで書く試みは先例がいくらかあるようですが、

今回はwebpackもparcelも使わずにesbuildをこれからの本流として試してみたいという技術実験も兼ねてるので… まあ良いか適当な生成プログラムをNode向けに書くか。

esbuildを導入しましょう

まずesbuildで適当にビルドしてこれらのファイルがどこに飛んで行くのかチェックしてみましょう。

esbuild --bundle --outdir=dist src/background/main.ts src/content/main.ts

してみたら本当に出力されてえっこんな簡単で良いの…ってなってます。簡単すぎて本当に出来てるのか逆に自信がなくなる。

esbuild最高。 parcelのゼロコンフィグレーションでワンコマンドで出来ると言う思想は気に入ってましたが、 babelとか個人的に必要ないものが入るのが気になっていました。 babelを入れてまでトランスコンパイルしたいものは今のTypeScript(JavaScript)に無いのですよね。

エコシステム調べるのやめて本体のコード書いていきましょう

設計や開発支援のことを考えすぎている。そんな大した拡張機能でも無いので手を動かしましょう。

まずはGoogleの検索結果を収集してbackgroundに投げるcontent scriptからですね。

収集はquerySelectorAllで即座に完了。

メッセージパッシングは?

runtime.sendMessage() - Mozilla | MDN で良いのかな…

本当に良いのかな… メッセージパッシングを陽に扱うとチェーンが大変なことになりますぞ。 Promiseでラップして応答を受け取れないだろうか。いやこれ単体でPromiseで通信が出来るらしい。

全部スクレイピング終わらないと終わらない形式だと一部の遅いサーバによる遅延がヤバそう。一件ずつ処理。ハンドラに渡す関数をトップレベルasyncにしたりすると全部受け取ってしまうらしいですね。 ???

このようなリスナーは全ての受け取ったメッセージを消費するため、実際には他のリスナーがメッセージを受信したり処理することを妨げてしまいます。

runtime.onMessage - Mozilla | MDN

別に全部メッセージを消費しても良い気がします。 fetchは非同期的に作られるわけで… CPUバウンドだと駄目なやつ?

結局fetchを使うのでPromiseを途中でも作るのは避けられないのでasyncでも良い気がします。警告を一回無視してasync関数を割り当ててみて一部更新とかのUXが悪かったら制御を考えましょう。

バックグラウンドを書いていきましょう

別にこれcontents側だけで完結するからbackground要らないんじゃないかという疑惑があります。 fetchとか別にcontent側だけでも出来ますし。うーん… まあ将来的にタブ閉じたらキャンセルとかキャッシュの実装とかやるかもしれんしbackgroundで実装するか…?

とりあえずはキャッシュとか長さ制限とか考えないでガンガン書いていきますか。

fetchで取得した内容をHTMLとして解釈するには…?

JavaScriptのFetch APIで返ってきたものをDOMとして扱う - ひと夏の技術によるとDOMparserを使えば良いらしい。

とりあえず起動してみた。 undefinedで終了。

あれっcontent scriptの方はsource map見るのにbackgroundのは見ないんだ。いや違うわこれ両方content scriptの方ですね。

なんかbackgorund scriptが含まれてない? あーブラウザ本体のコンソール開かないとダメか。単にCORSで弾かれてるだけですね。 GETもダメだっけ? 何もかも忘れてしまった。やっぱりcontents scriptだとダメなやつだこれ。

なんかリクエストにGoogle webキャッシュにアクセスして失敗してるやつありますけど、サイト本体よりこっちからデータ取ってきた方が良いのでは? 検索エンジン向けだけにSSRしてるサイトもあるかもしれませんし、 webcacheの一つのサーバだけにアクセスするのはいろんなドメインのサーバにアクセスするより軽量そう。

あっダメだこれGoogleさん機械的アクセスとしてCaptcha返してきますわ。

うーんfetchで済まそうと思ってましたが、普通に無理っぽいですね。

あれっCORSをbackgroundスクリプトで回避する方法存在しない…?

キャッシュの処理自前で実装するより履歴見に行った方が良いかも

履歴API使って取得した方が個別に持つより簡潔? と思ったけど、この拡張機能が取得したけどユーザがアクセスしないやつを持てないからやっぱりダメ。

CORS回避

いや存在しないってことはないでしょ。

例えば、 gorhill/uBlock: uBlock Origin - An efficient blocker for Chromium and Firefox. Fast and lean. とか外部のフィルタを見ています。

既に実現してるOSSあるならコード見てやり口みれば良いじゃん。気が付かなかった。

いや、豆腐フィルタがGitHub経由じゃないと取得できなくしたと嘆いてた気もする。

iorate/uBlacklist: Blocks specific sites from appearing in Google search results の実装を見てみましょう。 …普通にfetchしてるぞ…?

もしかしてpermissionにwebRequestとか付けないとだめ? 関数が関係ないと思っていましたが。

webRequestだけじゃなくてublock originのpermission全部コピペしたら動いた。やったぜ。最小のやつを探っていきましょう。どうもこれ、 permissions: ["<all_urls>"], だけで良いみたいです。 fetchのエラーからこれが即座にわかる人いる?

uBlacklistで購読開始すると権限求められるのはどういう仕組みだったんだ… コードちょっと見ても分からなかった。まあ検索のたびに全部のドメインとの通信を認証するのは実用的ではないのでもう良いんですけど。

実装TODO

最低限作ったところで残りの実装しないといけないやつリスト。

  • CSP強化
  • SPA対策
  • PDF対策
  • デカすぎファイル対策
  • キャッシュ
  • 取得できなかったやつをどうするか
  • デプロイの自動化
  • UI(設定項目とか、一時的に無効化出来る感じ?)
  • アイコン作成

CSP強化

Content Security Policy - Mozilla | MDN

textContentでやり取りしてるので多分プレーンテキストになるので大丈夫だと思うのですが、一応script流し込みとかに2つ目の対策をしていきましょう。多重対策は大事。と思ったのですが、デフォルトの値が、 "script-src 'self'; object-src 'self';" でかなりセキュアなんですよね。 scriptの流し込みとかに対応してます。

一応イメージタグとかは外部見れるようになってるのでイメタグ攻撃とかは出来なくはないんですが、それが問題になるリスクかなり小さいですし、下手に素人がデフォルトの設定を弄るリスクの方が高そうですね。

文字化け対策

適当にサンプル検索してたら見つけたんですが、「自分くすぐり」でストレス緩和? 他人にくすぐられている感覚得られる「くすぐってみ~な」を奈良先端大が開発:Innovative Tech - ITmedia NEWS みたいな、 <meta http-equiv="content-type" content="text/html;charset=shift_jis"> のようにShift_JISのサイトをUTF-8として読み込んで破綻してしまうようですね… webブラウザのAPIなのでそのへん自動判定してくれるかと思ってました、よく考えてみたら文字コード自動推定はブラウザ依存激しいのでそれが勝手に入ってもそれはそれで辛いかもしれませんね。

Fetch API で Shift_JIS の HTML をDOM として読み込む - Qiita とか見ましたけど、内容がShift_JISだと分かってたらともかく自動判定はどうすれば良いんだ…? EUC-JPとかだったらどうするんだ…?

どうしようかな、 xhr使う?

ちょっと効率が悪いけど一回パースしてどうも文字コードが違ったら再読込しますかね。今どきUTF-8でないサイトの方が少ないし、そういうサイトはデータ数も小さい傾向にある気がします。

はー? document.characterSetが必ずUTF-8になるんだが。ちゃんとcharset書いてるんだが~? 一回読み込ませた時点でダメか。

このライブラリ使えばある程度自動判定出来そう。 encoding.js/README_ja.md at master · polygonplanet/encoding.js 日本語圏以外は知らねえ。いや本当は知らねえってしたくないんですが、「文字化けしたこと」を検出するのってどうやるんですかね? 変なバイトが入ってないことを検出するんでしょうが、別の地域の文字コードでは普通に使われているものかもしれない…

ダメだこれ全然自動判定出来ない。自前である程度判定するしかない。

ダメだ判定するのややこしすぎる。 document.characterSet - Web API | MDN 使っても一回UTF-8として入力してパースさせたせいかcharsetがUTF-8じゃなくてもUTF-8だと返してきてしまう。とりあえずUTF-8であることだけ推定して、それ以外は諦めて初期版にしよう。

誰かブラウザの文字コード変換機能をJavaScriptから使う方法を教えてください。

英語になってしまうサイトがある

pixivとか。ちゃんと日本語指定してHTTPを送らねばならない?

headers: {
  "Accept-Language": "ja",
},

で解決。

解決じゃねーよja決め打ちはまずいだろ。

i18n.getAcceptLanguages() - Mozilla | MDN で読み込めばよし。

あっ違うweb-ext runで動かしてるプロファイルがデフォルトenになってるだけだわ。本当はブラウザの設定に従う。

じゃあ何もする必要ないですね。

SSRしないサイト対策

これは実際に運用していかないと間違いパターンが収集できないのですが、とりあえずはGoogleの持つものより小さいものは取得失敗で良さそうですね。

キャッシュ機能要らなくね?(2回め)

HTTPキャッシュがあるので明示的にこちらでキャッシュする必要性をまた感じなくなってきました。パースとかの短縮にはなりますが。

警告対策

18禁系のコンテンツとかGoogleだけには本来のタイトル出してクライアントには警告を出す。基本的に長さ検出で十分っぽいですが、アダルトとかそういう文章を弾いた方が良いのかもしれません。

日本語圏以外全く分からないことになりますが…

PDF対策

最初は取得しない方針で良くねと思ってましたが、 PDF.jsとかで割といけそうなのでやってみます。

pdfjs-dist - npm を使えば単体でimport出来る?

全然読み込めないんですが…

これscriptとして読み込まないとダメでバンドル出来ないやつ?

<unavailable>しか出力されない。

全然わからん…

諦めました。とりあえずPDFは読み込まないで初期リリースします。

これやっぱりバックグラウンドスクリプト不要なのでは?

コンテンツスクリプトの方のfetchも特権モードで動くからバックグラウンドで処理する必要性があまりない。コンテンツスクリプトにコードをたくさん置く理由も特に無いんですが…

Google検索とか頻繁にするし常駐しておけるスクリプト部分は多い方がJITとかに優しいかなあ。

今はどちらもそんなにフットプリントは大きくないですが、将来的にPDF.jsを利用出来るようになるとフットプリントが大きくなるので、毎回初期化をしないで済むバックグラウンドスクリプトにしておくのは価値があるかもしれません。

Manifest v3(Firefoxが対応する予定が無いのでまだ対応予定はありませんが…)になるとコンテンツスクリプトでのfetchは制限されるようなので、今から特権コードを分離しておくと移行しやすくなるかもしれません。

クロスオリジンの通信は、コンテントスクリプトはWebページのスクリプトと同じルールに基づくこととして、拡張機能のページは過去にアクセスしたことがあるサイトに対してはクロスオリジンの通信を可能とする。

Latest topics > Chrome Extensionsのマニフェストのバージョン3とアメリカの銃規制 - outsider reflex

アイコン作成

Googleのテキスト短縮表記である...を消すと言う意味で...にバツマークみたいなのを出せば良さそう。

かなり雑。テキスト配置以外のまともなデザインがしたい… このアイコンデザインがまともなのかと言う疑問もある。

web-ext-configのignoreFilesをホワイトリスト方式にする

なんか前の拡張を見たらsrcとか色々入ってたりした。 CircleCIで生成していったので問題ない感じですが。

今回はホワイトリスト形式で必要なものだけを含めることにしました。

og:titleも考慮に入れようかと思いましたがボツ

SSRでtitleを生成せずにog:titleだけを生成するサイト向けに、こちらも入れて長い方を採用するようにしようかと思いましたが、肝心のTwitterがツイートのog:titleをユーザ名のみの表示だけだったのでやめます。

よしリリースしよう

リリースして、残りのタスクはGitHubのissueにまとめていきましょう。

Firefoxアドオンに文字数制限で登録出来なかった

アドオンのタイトルって30文字までなのか… google-search-title-to-qualifiedは32文字でアウトですね。知らなかった。

google-search-title-qualified(29文字)にしましょう。

Chromeは45文字までなので気にする必要なさそう。

ソフトウェア名で説明をつけようとしてなろうタイトルみたいになるのクソダサいのでやめたいんですが、意味が分からない名前になるのも嫌だなと思います。

似非原さんのコメント: 「このサイトのタイトルがこんなに短いわけがない、とかだったらなろうっぽい」「そのタイトルはなろうと言うよりジャバ」