在开发自动化工具或定制化浏览器时,我们经常使用 Python 的 Playwright 库。但如果要将工具分发给非技术用户,要求对方安装 Python 环境、下载对应版本的浏览器内核是非常痛苦的。
本文介绍一种方案:将 Python 代码、Playwright 依赖库以及 Chromium 浏览器内核全部打包进一个独立的 .exe 文件中,实现真正的'零依赖'运行。
核心挑战
Playwright 默认将浏览器二进制文件下载到用户目录(如 %AppData%)。在打包时,我们需要解决两个核心问题:
- 构建时包含:如何把浏览器文件'塞'进 exe 里?
- 运行时定位:打包后的程序运行在临时目录,如何告诉 Playwright 去哪里找浏览器?
解决方案
1. Python 代码适配 (test-01.py)
我们需要在代码中动态检测当前是否在 PyInstaller 的打包环境中运行。如果是,则将环境变量 PLAYWRIGHT_BROWSERS_PATH 指向解压后的临时目录(sys._MEIPASS)。
此外,为了更好的用户体验,我们增加了错误捕获:如果程序崩溃(特别是在无控制台模式下),会弹出 Windows 错误对话框,而不是直接静默退出。
import argparse
import os
from pathlib import Path
import sys
import time
import traceback
from playwright.sync_api import sync_playwright
SUPPORTED_DEFAULT_DEVICE = "Pixel 5"
def resolve_browser_root() -> Path | None:
"""
核心逻辑:定位浏览器路径
1. 如果是打包环境,资源位于 sys._MEIPASS 下的 ms-playwright 目录
2. 如果是开发环境,可能在环境变量指定位置、当前目录或系统默认目录
"""
candidates = []
# PyInstaller 解压临时目录
if hasattr(sys, "_MEIPASS"):
candidates.append(Path(sys._MEIPASS) / "ms-playwright")
# 手动环境变量
if os.environ.get("PLAYWRIGHT_BROWSERS_PATH"):
candidates.append(Path(os.environ["PLAYWRIGHT_BROWSERS_PATH"]))
# 当前目录或默认目录
candidates.append(Path.cwd() / "ms-playwright")
candidates.append(Path.home() / "AppData" / / )
c candidates:
c.exists():
c
() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=)
parser.add_argument(, default=, =)
parser.add_argument(, default=SUPPORTED_DEFAULT_DEVICE, =)
parser.add_argument(, action=, =)
parser.add_argument(, =, default=, =)
parser.parse_args()
() -> :
browser_root = resolve_browser_root()
browser_root:
os.environ[] = (browser_root)
sync_playwright() p:
devices = p.devices
device_name devices:
()
sys.exit()
browser = p.chromium.launch(headless=headless)
context = browser.new_context(ignore_https_errors=, **devices[device_name])
page = context.new_page()
page.goto(url)
()
browser_root:
()
:
timeout > :
time.sleep(timeout)
:
:
time.sleep()
KeyboardInterrupt:
:
browser.close()
():
:
args = parse_args()
launch_mobile(args.url, args.device, args.headless, args.timeout)
Exception:
(sys, , ):
ctypes
ctypes.windll.user32.MessageBoxW(, , , )
:
traceback.print_exc()
sys.exit()
__name__ == :
main()




