「出来たぜやったね」を強調する手もないではないけれどだがしかし。
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-port
や http://127.0.0.1:9222/json はこれそのものが直接これへの救世主になることはなくて、あくまでも「備えあれば」のノリのリカバリをスナップショットしておくのに使える、というだけのことだが、まぁないよりはマシ。だってそれがない場合、chrome://history/ から頑張って復元するしかなくなるからであり、chrome://history/ ってのは「タブの状態」なんか関係ないから。閉じて用済みとなったページと延々向き合わねばならんというわけだ。
しかるに、「備えあれば」のノリのリカバリをスナップショットしておく、のスクリプト:
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 問題は現在世界中見渡してもまったく情報がない。かなり最近の事象らしい。