macOS上でchromedriverを自動更新するスクリプトについて考えてみました。
silicon mac なのでchromedriverはmac-arm64を使います。
スクリプトの制御内容
・実行環境のChromeのメジャーバージョンを取得します。
・ ~/.zshrc(/Users/xxx/.zshrc)をみて、 $PATHにchromedriverの記述があれば、そのパスの既存chromedriverのバージョンとChromeのバージョンを比較します。記述がなければ、/usr/local/bin/chromedriver/に新規インストールします。
・バージョンを比較して一致する場合は更新しません。chromedriverが古い場合は \$PATHに設定されたchromedriverを更新します。
・Chrome for Testingから対応するchromedriverのzipをダウンロードします。ダウンロードしたzipと展開ディレクトリは更新後に削除します。
・新規インストールする場合は、~/.zshrcにchromedriverのパスを追記します。
・最後にChromeとchromedriverのメジャーバージョンが一致していること確認します。
新規インストール先を変更する場合はINSTALL_DIRのパスを修正してください。
python code
import subprocess
import requests
import zipfile
import os
import shutil
# ユーザーの .zshrc ファイルの絶対パスを取得
ZSHRC_PATH = os.path.expanduser("~/.zshrc")
# chromedriver のインストール先ディレクトリ
INSTALL_DIR = os.path.expanduser("/usr/local/bin/chromedriver")
# インストール後の chromedriver 実行ファイルのパス
CHROMEDRIVER_BIN = os.path.join(INSTALL_DIR, "chromedriver")
# Chrome のメジャーバージョンを取得
def get_chrome_version():
try:
result = subprocess.run(
["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "--version"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
version = result.stdout.strip().split()[-1] # 例: "Google Chrome 117.0.5938.92" → "117.0.5938.92"
print(f"[Chrome] バージョン: {version}")
return version.split('.')[0] # メジャーバージョンのみ返す → "117"
except (subprocess.SubprocessError, FileNotFoundError) as e:
print(f"[Chrome] バージョン取得失敗: {e}")
return None
# 指定された chromedriver のバージョンを取得
def get_chromedriver_version(path):
try:
result = subprocess.run([path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = result.stdout.strip()
error = result.stderr.strip()
if error:
print(f"[Chromedriver ERROR] {error}")
print(f"[Chromedriver] {path} → {output}")
parts = output.split()
if len(parts) >= 2 and parts[1][0].isdigit():
return parts[1].split('.')[0] # メジャーバージョンのみ抽出
except (subprocess.SubprocessError, FileNotFoundError) as e:
print(f"[Chromedriver] バージョン取得失敗: {e}")
return None
# Apple Silicon 用の chromedriver ダウンロード URL を取得
def fetch_arm64_download_url():
url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
for entry in data.get("channels", {}).get("Stable", {}).get("downloads", {}).get("chromedriver", []):
if entry.get("platform") == "mac-arm64":
print(f"[取得] ダウンロードURL: {entry['url']}")
return entry["url"]
except requests.RequestException as e:
print(f"[取得失敗] {e}")
return None
# macOS の Quarantine 属性を削除(Gatekeeper 対策)
def remove_quarantine(path):
try:
result = subprocess.run(["xattr", path], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
if "com.apple.quarantine" in result.stdout:
subprocess.run(["xattr", "-d", "com.apple.quarantine", path], check=False)
except subprocess.SubprocessError as e:
print(f"[Quarantine] xattr 実行失敗: {e}")
except FileNotFoundError:
print(f"[Quarantine] ファイルが見つかりません: {path}")
# chromedriver をダウンロード・展開・配置・クリーンアップ
def update_chromedriver(download_url, install_dir):
zip_path = os.path.join(install_dir, "chromedriver.zip")
try:
print(f"[ダウンロード] {download_url}")
response = requests.get(download_url)
response.raise_for_status()
with open(zip_path, "wb") as f:
f.write(response.content)
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# 展開された chromedriver 実行ファイルのパスを探索
extracted = next((os.path.join(install_dir, d, "chromedriver")
for d in os.listdir(install_dir)
if d.startswith("chromedriver-mac")), None)
if not extracted or not os.path.exists(extracted):
print("[エラー] 展開された chromedriver が見つかりません")
return False
# 実行ファイルを install_dir に移動し、実行権限を付与
dst = os.path.join(install_dir, "chromedriver")
shutil.move(extracted, dst)
os.chmod(dst, 0o755)
remove_quarantine(dst)
print(f"[完了] chromedriver 更新: {dst}")
return True
except (requests.RequestException, zipfile.BadZipFile, shutil.Error, OSError) as e:
print(f"[更新失敗] {e}")
return False
finally:
# ZIP ファイルと展開ディレクトリを削除
try:
if os.path.exists(zip_path):
os.remove(zip_path)
for d in os.listdir(install_dir):
if d.startswith("chromedriver-mac"):
shutil.rmtree(os.path.join(install_dir, d), ignore_errors=True)
except (FileNotFoundError, PermissionError) as e:
print(f"[クリーンアップ失敗] {e}")
# .zshrc から PATH 定義を抽出
def extract_zshrc_paths():
if not os.path.exists(ZSHRC_PATH):
return []
try:
with open(ZSHRC_PATH, "r") as f:
lines = f.readlines()
all_paths = []
for line in lines:
line = line.strip()
if line.startswith("export PATH="):
rhs = line.split("=", 1)[-1].strip()
if rhs.startswith('"') and rhs.endswith('"'):
rhs = rhs[1:-1]
elif rhs.startswith("'") and rhs.endswith("'"):
rhs = rhs[1:-1]
rhs = rhs.replace("$PATH", "").replace("$PATH:", "").replace(":$PATH", "")
for p in rhs.split(":"):
p = p.strip()
if p:
all_paths.append(os.path.expanduser(p))
return all_paths
except OSError as e:
print(f"[zshrc] 読み取り失敗: {e}")
return []
# .zshrc に指定パスがすでに含まれているか確認
def zshrc_contains_chromedriver_path(path):
try:
with open(ZSHRC_PATH, "r") as f:
content = f.read()
return path in content
except OSError as e:
print(f"[zshrc] チェック失敗: {e}")
return False
# .zshrc に chromedriver のパスを追記
def append_path_to_zshrc(path):
if zshrc_contains_chromedriver_path(path):
return
try:
with open(ZSHRC_PATH, "a") as f:
f.write(f'\nexport PATH="{path}:$PATH"\n')
print(f"[zshrc] PATH 追記: {path}")
except OSError as e:
print(f"[zshrc] PATH 追記失敗: {e}")
# メイン処理:バージョン整合性チェックとインストール・更新
def main():
chrome_major = get_chrome_version()
if not chrome_major:
return
path_dirs = extract_zshrc_paths()
# 各 PATH ディレクトリを走査して chromedriver を探す
for p in path_dirs:
candidate = os.path.join(p, "chromedriver")
if os.path.isfile(candidate):
version = get_chromedriver_version(candidate)
if version is None:
print(f"[再インストール] {candidate} に chromedriver があるがバージョン取得できないため再インストールします")
url = fetch_arm64_download_url()
if url and update_chromedriver(url, p):
version = get_chromedriver_version(candidate)
if version == chrome_major:
print(f"[バージョン比較] 再インストール後の chromedriver が Chrome と一致しています ({version})")
else:
print(f"[警告] 再インストール後もバージョン不一致: Chrome={chrome_major}, Driver={version}")
return
elif version == chrome_major:
print(f"[バージョン比較] {candidate} は Chrome と一致しています ({version})")
return
else:
print(f"[不一致] {candidate} → Chrome={chrome_major}, Driver={version}")
print(f"[更新] {candidate} を更新します")
url = fetch_arm64_download_url()
if url and update_chromedriver(url, p):
version = get_chromedriver_version(candidate)
if version == chrome_major:
print(f"[バージョン比較] 更新後の chromedriver が Chrome と一致しています ({version})")
else:
print(f"[警告] インストール後もバージョン不一致: Chrome={chrome_major}, Driver={version}")
return
print("[インストール] chromedriver が PATH に見つからないため新規インストールします")
os.makedirs(INSTALL_DIR, exist_ok=True)
url = fetch_arm64_download_url()
if url and update_chromedriver(url, INSTALL_DIR):
if not zshrc_contains_chromedriver_path(INSTALL_DIR):
append_path_to_zshrc(INSTALL_DIR)
version = get_chromedriver_version(CHROMEDRIVER_BIN)
if version == chrome_major:
print(f"[バージョン比較] インストール後の chromedriver が Chrome と一致しています ({version})")
else:
print(f"[警告] インストール後もバージョン不一致: Chrome={chrome_major}, Driver={version}")
if __name__ == "__main__":
main()