Playwright 接管本地 Chrome:四种方式对比

Playwright 默认下载并使用自带的 Chromium。想用本机装的 Google Chrome(自动化登录时保留 cookie、复用插件),有四种方式,各自适用场景不同。 方式一:channel = "chrome" 最省事——channel: "chrome" 告诉 Playwright 用系统装的稳定版 Chrome: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(channel="chrome", headless=False) page = browser.new_page() page.goto("https://example.com") input("回车退出") browser.close()优点:跨平台一行搞定 局限:不会用你日常那个 profile,不带 cookie、不带插件其它可选 channel:msedge、chrome-beta、chrome-dev、msedge-beta。 方式二:executable_path 指定路径 Chrome 装在非默认路径,或者想用 Chromium 兼容分发(Brave、Edge、Vivaldi): Windows: browser = p.chromium.launch( executable_path=r"C:\Program Files\Google\Chrome\Application\chrome.exe", headless=False, )macOS: browser = p.chromium.launch( executable_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", headless=False, )Linux: browser = p.chromium.launch( executable_path="/usr/bin/google-chrome", headless=False, )跟方式一一样,不带 cookie / 插件——只是换了浏览器可执行文件。 方式三:launchPersistentContext 复用 profile 想让 Playwright 用日常那个 Chrome 用户目录(复用登录态、书签、扩展): browser = p.chromium.launch_persistent_context( user_data_dir=r"C:\Users\<你>\AppData\Local\Google\Chrome\User Data", channel="chrome", headless=False, ) page = browser.pages[0] if browser.pages else browser.new_page() page.goto("https://example.com")注意:这个方式要求 Chrome 没在运行,否则报: Failed to launch: browser closed. Profile is already in use解法:关掉所有 Chrome 窗口再跑 或者复制一份 profile 出来单独用:xcopy "C:\...\User Data" "D:\pw-profile" /E /I想只用某个 Profile(不是 Default): browser = p.chromium.launch_persistent_context( user_data_dir=r"D:\pw-profile", args=["--profile-directory=Profile 1"], channel="chrome", headless=False, )方式四:CDP 连接已启动的 Chrome(推荐) 方式三最大的痛点是"Chrome 必须关闭"。想在 Chrome 正常开着的情况下让 Playwright 接管它,走 CDP(Chrome DevTools Protocol)。 第一步:以 debug 模式启动 Chrome,指定端口: "C:\Program Files\Google\Chrome\Application\chrome.exe" ^ --remote-debugging-port=9222 ^ --user-data-dir="D:\pw-chrome-profile"--user-data-dir 指向一个专用目录,避免和日常 Chrome 抢占用。 第二步:Playwright 连上去: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222") ctx = browser.contexts[0] page = ctx.pages[0] if ctx.pages else ctx.new_page() page.goto("https://example.com")优势:Chrome 一直开着,做完自动化 Playwright 断开就行 保留登录态、插件、cookie 可以边脚本操作边人工插手(做半自动化)生产爬虫、账号自动登录场景基本用这个。 四种方式对比方式 Chrome 需关闭 保留 profile 使用场景channel="chrome" 否 否 简单自动化,无需登录态executable_path=... 否 否 用其它 Chromium 分发launch_persistent_context 是 是 一次性用现有 profileconnect_over_cdp (CDP) 否 是 推荐:常驻 Chrome + 脚本联动顺带:无痕、隐身、下载路径 无痕模式: browser = p.chromium.launch(channel="chrome", args=["--incognito"])指定下载目录: context = browser.new_context(accept_downloads=True) page = context.new_page() async with page.expect_download() as info: await page.click("a#download") download = await info.value await download.save_as("D:/downloads/file.zip")忽略证书错误(内网自签证书): context = browser.new_context(ignore_https_errors=True)一句话总结 普通自动化用 channel="chrome"、要复用登录态用 CDP 连接(先手动启动 Chrome + --remote-debugging-port=9222)。launch_persistent_context 只在 Chrome 关闭时才能用。

Playwright 使用本地已安装的 Chrome:channel、executablePath 与 CDP 连接

Playwright 默认使用自带的 Chromium 浏览器。以下四种方式可以改用本地安装的 Google Chrome。 方法一:channel='chrome'(最简单) 需要本机已安装 Google Chrome。 Python: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(channel="chrome", headless=False) page = browser.new_page() page.goto("https://example.com") browser.close()JavaScript: const { chromium } = require('playwright');const browser = await chromium.launch({ channel: 'chrome', headless: false }); const page = await browser.newPage(); await page.goto('https://example.com'); await browser.close();方法二:指定 executablePath Python(各平台路径): # Windows executable_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"# macOS executable_path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"# Linux executable_path = "/usr/bin/google-chrome"browser = p.chromium.launch(executable_path=executable_path, headless=False)方法三:launchPersistentContext(复用登录状态) 使用已登录账号、Cookie、插件的 Chrome 用户数据目录: Python: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: context = p.chromium.launch_persistent_context( r"C:\Users\用户名\AppData\Local\Google\Chrome\User Data", channel="chrome", headless=False, ) page = context.pages[0] or context.new_page() page.goto("https://example.com")注意:Chrome 运行时会独占用户目录,使用此方法前需要关闭所有 Chrome 窗口,否则报错: Profile is already in use方法四:connectOverCDP(连接已启动的 Chrome) 先启动 Chrome 并开启远程调试端口(不影响正常使用): # Windows "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222# macOS /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222然后 Playwright 连接: Python: browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222") page = browser.contexts[0].pages[0] page.goto("https://example.com")JavaScript: const browser = await chromium.connectOverCDP('http://127.0.0.1:9222'); const page = browser.contexts()[0].pages()[0];这种方式完全复用当前 Chrome 的登录状态、Cookie 和扩展,且不独占用户目录。适合需要手动操作与自动化混合使用的场景。 方案对比方法 复用登录 Chrome 需关闭 保留插件channel='chrome' ✗ 否 ✗executablePath ✗ 否 ✗launchPersistentContext ✓ 是 ✓connectOverCDP ✓ 否 ✓

JS 逻辑赋值运算符:||= / &&= / ??= 用法和区别

ES2021 引入了三个逻辑赋值运算符:||=、&&=、??=。这三个符号都是"条件赋值"的简写,但触发条件各不相同,混淆了容易出 bug。 ||=(逻辑或赋值) 只有左侧为 falsy 时才赋值: a ||= b // 等价于 a = a || b // 即:if (!a) a = blet x = null; x ||= "default"; console.log(x); // "default"let y = 0; y ||= 100; console.log(y); // 100 ← 0 是 falsy,会被覆盖会触发赋值的值:null、undefined、false、0、''、NaN 典型用途:缓存计算结果 function checkIsRowVisible(data) { viewportRect ||= scrollContainer.getBoundingClientRect(); // 第一次为 undefined,执行右侧并缓存 // 之后 viewportRect 有值,跳过 const rowEl = data._rowEl.getBoundingClientRect(); return rowEl.bottom >= viewportRect.top && rowEl.top <= viewportRect.bottom; }??=(空值合并赋值) 只有左侧为 null 或 undefined 时才赋值: a ??= b // 等价于 a = a ?? b // 即:if (a === null || a === undefined) a = blet x = null; x ??= "default"; console.log(x); // "default"let y = 0; y ??= 100; console.log(y); // 0 ← 0 不是 null/undefined,不会被覆盖??= 比 ||= 更"精准":0、false、'' 都不会触发赋值。 典型用途:对象属性的惰性初始化 user.preferences ??= {}; user.preferences.theme ??= "dark";&&=(逻辑与赋值) 只有左侧为 truthy 时才赋值: a &&= b // 等价于 a = a && b // 即:if (a) a = blet arr = [1, 2, 3]; arr &&= arr.map(x => x * 2); console.log(arr); // [2, 4, 6]let empty = null; empty &&= [1, 2, 3]; console.log(empty); // null ← null 是 falsy,不赋值典型用途:只在值存在时更新 element &&= element.querySelector(".item"); // element 为 null 时不执行,避免报错三者对比运算符 触发赋值条件 不触发的值||= 左侧为 falsy truthy 值??= 左侧为 null/undefined 0, false, '', NaN 等&&= 左侧为 truthy falsy 值let a = 0; a ||= 1; // a = 1 (0 是 falsy) a = 0; a ??= 1; // a = 0 (0 不是 null/undefined) a = 0; a &&= 1; // a = 0 (0 是 falsy,不赋值)let b = 5; b ||= 1; // b = 5 (5 是 truthy,不赋值) b ??= 1; // b = 5 (5 不是 null/undefined,不赋值) b &&= 10; // b = 10 (5 是 truthy,赋值)选哪个想要"没有值就用默认值",且 0 / false / '' 也算有效值 → 用 ??= 想要"空/假就用默认值" → 用 ||= 想要"有值才更新" → 用 &&=对于对象初始化类的场景,??= 更安全,不会意外覆盖 false 或 0 这类合法值。

JavaScript 逻辑赋值运算符:||=、&&=、??= 和可选链动态属性访问

逻辑或赋值 ||= ES2021 引入的 ||= 只在左侧是 falsy 时才赋值: viewportRect ||= scrollContainer.getBoundingClientRect();等价于: if (!viewportRect) { viewportRect = scrollContainer.getBoundingClientRect(); }常用于"懒初始化缓存":第一次访问时计算并保存,后续复用已有值,避免重复执行开销大的操作。 触发赋值的 falsy 值:null、undefined、false、0、''、NaN。 空值合并赋值 ??= ??= 只在左侧是 null 或 undefined 时才赋值: config.timeout ??= 5000;等价于: if (config.timeout === null || config.timeout === undefined) { config.timeout = 5000; }||= 与 ??= 的区别 let x = 0; x ||= 10; console.log(x); // 10 ← 0 是 falsy,触发了赋值let y = 0; y ??= 10; console.log(y); // 0 ← 0 不是 nullish,不触发赋值场景 推荐变量为空对象/数组/字符串时初始化 ??=变量为任何假值时初始化 `变量已赋值则不覆盖 ??=(更安全)逻辑与赋值 &&= &&= 只在左侧是 truthy 时才赋值: user &&= { ...user, lastLogin: Date.now() }; // 仅当 user 存在时才更新可选链结合动态属性访问 ?.[] 是可选链加计算属性的组合: const currentValue = projection.user_property?.[attrDef.value];等价于: const currentValue = projection.user_property == null ? undefined : projection.user_property[attrDef.value];attrDef.value 是运行时计算的属性名,不能用 ?. 点语法(点语法只能接静态标识符),必须用方括号形式 ?.[]。 // 深层链式安全访问 const val = obj?.a?.b?.[dynamicKey]?.value; // 任何一级为 null/undefined 都返回 undefined,不会抛错

ThinkPHP 5.1 老项目 + Composer 2:think-installer 装不上

维护一个老 ThinkPHP 5.1 项目,composer install 直接崩: Problem 1 - topthink/think-installer is locked to version v2.0.0 and an update of this package was not requested. - topthink/think-installer v2.0.0 requires composer-plugin-api ^1.0 -> found composer-plugin-api[2.9.0] but it does not match the constraint. Problem 2 - topthink/framework v5.1.39 requires topthink/think-installer 2.* ...顺带还有: The "https://packagist.phpcomposer.com/packages.json" file could not be downloaded (404)两个问题:Composer 版本不兼容 + 镜像源死了。 症状 1:think-installer 只支持 Composer 1 topthink/think-installer v2.0.0 明确要 composer-plugin-api ^1.0,也就是 Composer 1.x 的插件接口。你机器上是 Composer 2.x,插件接口版本变成 2.x,直接不兼容。 TP5.1 上线那会儿(2018)Composer 2 还没出,这问题在当时不存在,遗留下来才成了坑。 解法:降回 Composer 1 推荐、最省事、也最不折腾业务代码。 方法 A:Composer 自己降版本 composer self-update --1Composer 会在原地切到最新的 1.x(一般是 1.10.27)。想指定: composer self-update 1.10.27方法 B:装一个独立的 Composer 1 不想动全局 Composer 的话,单独下一份: curl -sS https://getcomposer.org/installer | php -- --1 mv composer.phar /usr/local/bin/composer1 composer1 install老项目用 composer1,新项目继续用 composer。 症状 2:packagist.phpcomposer.com 已凉 老教程里推荐的 packagist.phpcomposer.com 这个镜像已经关服多年,仍然响应 404。检查 composer.json: cat composer.json | grep -A 3 repositories看到: "repositories": { "packagist": { "type": "composer", "url": "https://packagist.phpcomposer.com" } }改成官方源或者阿里镜像: # 官方 composer config -g repo.packagist composer https://repo.packagist.org# 阿里云 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/# 清华(有时更快) composer config -g repo.packagist composer https://mirrors.tuna.tsinghua.edu.cn/composer/-g 是全局,改单个项目就去掉。 也可以直接改 composer.json 里的 repositories 字段,同样效果。 完整恢复流程 # 1. Composer 降 1 composer self-update --1# 2. 换镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/# 3. 清缓存 composer clear-cache# 4. 重装依赖 rm -rf vendor composer.lock composer install90% 的老 TP5.1 项目这一套下来就恢复了。 什么时候该升级项目 如果这不是紧急救火,考虑:升到 ThinkPHP 6 或 8,从 5.1 迁移文档官方有 或者只把 topthink/think-installer 换成 topthink/framework 的现代版本,去掉这个已废弃的插件但生产环境急救时先别动这些——先降 Composer 让站起来,稳定了再排期改造。 一句话总结 think-installer 只兼容 Composer 1,最直接的解法是 composer self-update --1。顺手把死了的 phpcomposer.com 镜像换成阿里或官方源。