なりそこない救世主: chrome.exe --remote-debugging-port

「出来たぜやったね」を強調する手もないではないけれどだがしかし。

selenium webdriver で chrome をコントロールする動機のほとんど大半が web サービスから何らか結果を得るための操作の自動化、すなわちアウトプットが得られればいいというものなので、その向きには「開いている chrome のインスタンスにアタッチして、それをコントロールする」ことは大抵考えなくていい。つまりは、プロセスとして chrome をバックグラウンドで起動してそのインスタンスをコントロール出来れば良い。ワタシが書いたネタであれば例えばこれとか。

けれども、例えばメモリが何らかの理由で逼迫しているとして、そうしたケースで「メモリ喰いの「タブを大量に開いた」chrome を整理する術が欲しい」と思ったとする。というか思った。

今ワタシはこのページを書くための chrome を、80くらいのタブを開いた状態で書いている。色んな調べものをしつつ、エロいタブも日常的に開いている、とかだと、気付いたらこんな状態になってることが多い。ここまでタブを開いていると、結構同じページを複数開いていたりするし、もうとっくに用済みになってるページも開きっぱなしになってることも多い。そしてメモリが逼迫してる状態だと「各々のタブにマウス操作などで移動してみて、開きっぱなしにしとく意味がないと判断出来るなら閉じる」という繰り返し行為そのものが「重くて操作に耐えない」ので、ということ。このために、たとえばコンソールからタブを閉じる操作は出来ないだろうか、みたいなことを考えたわけだ。

けれどもこれが非常によろしくない。

最も望むものに近かったのが見出しにした「chrome.exe --remote-debugging-port」を使う方法なのだが、まぁ…「今開いている全タブ」にアクセス出来んのだわ。これについての情報はどこにもない。あと、キー操作なども出来ないなど制約が非常に多くて、救世主度は非常に低い。

これをするためにはまずは:

参考にしたサイトが皆 9222 を選んでるんで、指定しなければ自動でポートが開いてたりしないんだろうかと思ったけれど、そうでもなさそう。いや、あるかもしれんけど、情報は一切見つからず。ともあれ、この状態で常に chrome を起動するようにしておけば:

 1 # -*- coding: utf-8 -*-
 2 from selenium import webdriver
 3 from selenium.webdriver.chrome.options import Options
 4 
 5 chrome_options = Options()
 6 chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
 7 driver = webdriver.Chrome(options=chrome_options)
 8 for h in driver.window_handles:
 9     driver.switch_to.window(h)
10     print(h, driver.title)

ワタシのその「80くらい開いてる」タブのほとんどが無視され、このスクリプトは20くらいだけのタブ切り替えを「してくれる」。この「20くらいしか」という制約が気にならない用途であるならば、例えば「それらタブを全部閉じる」ということは出来る。

どの「20くらい」になるかは、アクティブな chrome インスタンスで最後に手動操作した状態に依存する。LRU みたいなことだろう、きっと。最後にアクティブにしたもののキャッシュにアクセス出来ている、という感じかもしれない。

あと、この debuggerAddress は REST API を提供してくれている。9222 で設定して起動したなら http://127.0.0.1:9222/json にアクセスしてみよ。これに基づいてなんらか処理をすることも無論可能で:

 1 # -*- coding: utf-8 -*-
 2 import requests  # これも標準バンドルではないので pip install しとくれ
 3 from selenium import webdriver
 4 from selenium.webdriver.chrome.options import Options
 5 
 6 chrome_options = Options()
 7 chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
 8 driver = webdriver.Chrome(options=chrome_options)
 9 res = requests.get("http://127.0.0.1:9222/json")
10 #for h in driver.window_handles:
11 #    driver.switch_to.window(h)
12 #    print(h, driver.title)
13 for d in [d for d in res.json() if d["type"] == "page"]:
14     print(d["type"])
15     driver.switch_to.window(d["id"])
16     print(driver.title)

はっきりいって本質的にやってることは何ら変わらないので、「20しか…」問題の解決にはならないけれど、まぁ知識として知っといて損はないかなというレベル。

てなわけで、メモとして書いておく価値はないとは言えないけれど、即戦力として使えるものではなかった、てことで。


2023-10-29追記:
この追記も救世主度低めなのだが、上で想定したような「たとえばコンソールからタブを閉じる操作は出来ないだろうか」という、アクティブな chrome インスタンスを操作したいというニーズとは別に、「今開いているタブの一覧が取れるだけでありがたい」ケースがほかにも少しばかりある。「全部は取れない」は諦めるとして、ね。

何がときおり困るかといえば、「chrome 再起動時に開いたタブをきれいさっぱり忘れている」ことがあるから。これは普段は「再起動時に開いていたページを復元する」設定がしてあれば起こらない。ところが、ある特定の異常事態が起こると、この設定があっても忘れてしまうことが起こる。この条件は単純で、「いくつかのページだけ別ウィンドウで開いていた状態で chrome が終了した」場合。正常終了でも異常終了でも問わないが、とにかく例えば一個のページだけ別ウィンドウだった場合、chrome は次の起動時に、この一つのページ以外一切復元しようとしてくれない。

あるいは、「復元しますか?」に誤って「いいえ」してしまうこともないとは言えない。

chrome.exe --remote-debugging-porthttp://127.0.0.1:9222/json はこれそのものが直接これへの救世主になることはなくて、あくまでも「備えあれば」のノリのリカバリをスナップショットしておくのに使える、というだけのことだが、まぁないよりはマシ。だってそれがない場合、chrome://history/ から頑張って復元するしかなくなるからであり、chrome://history/ ってのは「タブの状態」なんか関係ないから。閉じて用済みとなったページと延々向き合わねばならんというわけだ。

しかるに、「備えあれば」のノリのリカバリをスナップショットしておく、のスクリプト:

dump_chromedbgportjson.py
 1 ># -*- coding: utf-8 -*-
 2 # require:
 3 #   python 3.6+
 4 #   "requests" package
 5 import io
 6 import textwrap
 7 import json
 8 from datetime import datetime
 9 
10 import requests
11 
12 
13 if __name__ == '__main__':
14     # see:
15     #   http://hhsprings.pinoko.jp/site-hhs/wp-content/uploads/2023/04/img_6427edc7a2d89.png
16     import argparse
17     ap = argparse.ArgumentParser()
18     ap.add_argument("--port", type=int, default=9222)
19     ap.add_argument("--basename", default="chrome-remote-debugging-dump")
20     ap.add_argument("dumpedjson", nargs="?")
21     args = ap.parse_args()
22     dts = datetime.now().strftime("%Y%m%d-%H%M%S")
23     outbasename_json = f"{args.basename}-{args.port}-{dts}"
24     outname_json = f"{outbasename_json}.json"
25     outname_html = f"{outbasename_json}.html"
26     if args.dumpedjson:
27         dumped = json.load(io.open(args.dumpedjson, encoding="utf-8"))
28     else:
29         resp = requests.get(f"http://127.0.0.1:{args.port}/json")
30         dumped = resp.json()
31         with io.open(outname_json, "w", encoding="utf-8") as fo:
32             json.dump(dumped, fo, ensure_ascii=False, indent=4)
33     with io.open(outname_html, "w", encoding="utf-8") as fo:
34         print(textwrap.dedent(f"""
35         <html>
36         <head>
37         <meta charset="utf-8">
38         </head>
39         <body>
40         <ul>
41         """), file=fo)
42         for page in dumped:
43             title, url, furl = page.get("title"), page.get("url"), page.get("faviconUrl")
44             typ = page.get("type")
45             if not title:
46                 title = url
47             if typ == "page":
48                 a = f'<a href="{url}">{title}</a>'
49                 ico = f'<img src="{furl}" width="16pt"/>'
50             else:
51                 a = f'<a href="{url}">{title}</a>'
52                 ico = f'[{typ}]'
53             print(
54                 f'<li>{ico} {a}</li>',
55                 file=fo)
56         print(textwrap.dedent(f"""
57         </ul>
58         </body>
59         </html>"""), file=fo)

/json をダンプしつつ、chrome://history/ よりは復元作業の足しになる html を吐き出す。それだけ。当然これは相変わらず必要:


2023-10-30追記:
昨日書いた時点でもおぼろげに気づいていたこと一つと、書いたあとで判明したこと一つの計二つ。半年しか経っていないのだが、状況がかなり変わっているようなのだよね。

まず、「ワタシのその「80くらい開いてる」タブのほとんどが無視され、このスクリプトは20くらいだけのタブ切り替えを「してくれる」。」がもうね、ダウトっぽい、今や。
http://127.0.0.1:9222/json が全タブを出力している気がする。少なくとも昨日一日試していて、見えてるタブが抜けるパターンをみていない。

そして、「この「20くらいしか」という制約が気にならない用途であるならば、例えば「それらタブを全部閉じる」ということは出来る。」として書いたスクリプトは、最新のものを使うともはやまったく機能しなくなっている。「chrome_options.add_experimental_option("debuggerAddress", f"localhost:9222")」はまったく参照されている気配がないので別の渡し方をすると、今度は「driver = webdriver.Chrome(options=chrome_options)」から処理が返ってこなくなる。

そういうわけで、最初に書いた時から救世主度は低かったが、現状ますます全然ダメになってる。debuggerAddress 問題は現在世界中見渡してもまったく情報がない。かなり最近の事象らしい。