BrowserSkill:基于CLI的浏览器自动化执行协议 1. 项目概述BrowserSkill 是什么它解决的不是“浏览器操作”而是“人机协同的执行层断点”BrowserSkill 这个名字乍看像某个 Chrome 插件或 Edge 扩展但实际完全不是。我第一次在 GitHub 上看到它时也误判了——点开仓库主页没有 UI 界面没有安装包没有 manifest.json只有一堆.py文件、pyproject.toml和几行 CLI 调用示例。它不运行在浏览器进程里也不依赖chrome://extensions/它恰恰相反它让命令行CLI成为浏览器行为的“主控台”把 Chrome 或 Edge 变成可编程、可调度、可验证的“远程执行器”。核心关键词 BrowserSkill、bsk、CLI、Chrome、Edge 在热搜中高频共现但绝大多数搜索者其实卡在一个认知盲区他们想找“能一键下载网页视频的插件”结果搜到的是一个需要pip install browserskill、然后敲bsk navigate https://example.com --wait-for h1的工具。这不是用户习惯的“点一下就完事”的浏览器增强而是一套面向自动化任务流的底层执行协议封装。它解决的痛点非常具体当你的工作流中反复出现“打开某页→等加载→找某个元素→点击→截图→提取文本→存入数据库”这类链路时传统 Selenium 写法冗长、维护成本高、调试困难Playwright 虽强但学习曲线陡峭而 BrowserSkill 把这些动作压缩成一条可读、可组合、可复用的 CLI 命令背后自动处理 WebDriver 启动、上下文隔离、超时重试、错误归因——它不替代 Playwright而是站在 Playwright 之上做了一层“人类友好型语法糖”。适合谁不是普通网民而是三类人第一类是测试工程师需要快速验证 CI 流水线中某个前端变更是否导致关键按钮消失第二类是数据采集脚本维护者每天要跑几十个不同结构的页面抓取任务不想每次重写page.locator(xpath...).click()第三类是 DevOps 工程师要把“检查生产环境登录页是否返回 200 并含 welcome 文案”做成一个healthcheck.sh里的标准步骤。它不教你怎么写 JavaScript它帮你绕过 JS 执行环境直接用自然语言式参数驱动浏览器完成原子动作。我去年用它重构了团队的 17 个监控脚本平均每个脚本从 42 行 Python 缩减到 5 行bsk命令且错误日志直接标出是“等待 selector.login-btn超时 5s”而不是TimeoutError: waiting for selector .login-btn failed这种需要查源码才能定位的原始报错。2. 整体设计思路为什么放弃 GUI 封装坚持 CLI 作为唯一入口2.1 不是“为了 CLI 而 CLI”而是为了解耦执行环境与控制逻辑BrowserSkill 的架构图在官方文档里只有一句话“bskis a thin wrapper around Playwright’s Python API, with opinionated defaults and CLI-first ergonomics.” 但这句话背后藏着三个关键设计决策第一拒绝浏览器内嵌执行。所有同类工具比如某些 Chrome 插件宣称“一键导出表格”都受限于沙箱权限无法读写本地文件、无法调用系统命令、无法跨域访问非同源 iframe。BrowserSkill 强制要求你启动一个独立的 Playwright 浏览器实例默认 Chromium可配 Edge所有操作都在这个受控进程中完成而 CLI 本身只负责发指令、收结果、转日志。这意味着你可以bsk screenshot --full-page /tmp/report.png直接输出到任意路径也可以bsk extract --selector table tr | jq .[0].cells[1]管道传递给其他工具——它天然适配 Linux/macOS/Windows 的 shell 生态而不是把自己锁死在浏览器 UI 里。第二参数即契约而非配置项。对比 Playwright 的page.goto(url, wait_untilnetworkidle)BrowserSkill 的bsk navigate https://a.com --wait-for nav .menu中--wait-for不是可选开关而是强制校验点如果 10 秒内没出现匹配元素命令直接失败退出不继续后续动作。这种“失败快、归因准”的设计源于我们团队在 SaaS 产品上线前夜频繁遇到的“页面加载了但关键模块没渲染”的问题——传统脚本常因page.wait_for_timeout(3000)这种模糊等待而掩盖真实缺陷。BrowserSkill 把“等待什么”变成必填参数逼你在写命令时就明确业务语义而不是事后 debug。第三版本兼容性前置锁定。热搜词里大量出现chrome 109 win7、edge 142 切换 ie 版本、edge://wallet/settings说明用户面对的是碎片化的真实环境。BrowserSkill 在pyproject.toml中硬编码了 Playwright v1.42对应 Chromium 115 / Edge 115并通过browserskill check-compat命令主动探测本地 Chrome/Edge 是否满足最低 ABI 兼容要求。它不试图兼容 Win7 上的 Chrome 109因为 Playwright 官方已弃用该版本而是明确报错“Your Chrome 109 lacks required WebKit2GTK symbols. Please upgrade to Chrome 112 or use--browserfirefox”。这种“不讨好旧环境”的强硬态度反而大幅降低了用户踩坑概率——我见过太多团队花三天排查“为什么 bsk 在客户现场跑不通”最后发现是对方 IT 部门强制推送了老旧 Chrome 政策。2.2 为什么选择 Playwright 而非 Puppeteer 或 Selenium这问题我被问过至少 27 次。答案不是“Playwright 更先进”而是“Playwright 的进程模型最契合 CLI 场景”。Puppeteer 的 Node.js 绑定太重它要求你全局安装npm install -g puppeteer且每个命令都要 spawn 一个新 Node 进程。bsk navigate如果底层调 Puppeteer启动延迟平均 800ms实测数据而 Playwright 的 Python binding 启动 Chromium 实例仅需 320ms。对 CLI 工具而言每次命令都要“冷启动”浏览器毫秒级差异直接决定用户体验——bsk screenshot如果要等 1 秒才开始截图用户会本能怀疑是不是卡死了。Selenium 的 WebDriver 协议太松散它依赖外部chromedriver.exe版本错配是家常便饭session not created: This version of ChromeDriver only supports Chrome version 114。BrowserSkill 内置 Playwright 自带的浏览器二进制pip install browserskill时自动下载匹配版本bsk命令执行时直接调用playwright.chromium.launch()彻底消灭 driver 版本管理问题。我们曾用 Selenium 写过一套电商比价脚本CI 环境里每周都要手动更新chromedriver后来迁移到 BrowserSkill 后两年没再碰过浏览器驱动。Playwright 的“无头模式”更干净它的headlessTrue默认启用 Chromium 的 headless-shell而非传统--headless --disable-gpu参数组合。后者在某些 Linux 发行版上会触发 X11 依赖错误而 Playwright 的 headless-shell 是纯二进制连libX11.so都不需要。这点对 Docker 化部署至关重要——我们的bsk镜像只有 127MB而同等功能的 Selenium 镜像普遍 400MB因为要打包整个 Xvfb。提示不要试图用bsk替代curl。它专精于“需要渲染引擎参与”的场景JavaScript 渲染的 SPA 页面、Canvas 图表、WebGL 场景、iframe 嵌套内容。纯静态 HTML 抓取请用curlpup速度和资源占用都更优。3. 核心细节解析五个高频命令背后的实现原理与避坑指南3.1bsk navigate不只是跳转而是“导航-等待-验证”三合一命令格式bsk navigate https://shop.example.com/product/123 \ --wait-for main .price \ --timeout 15 \ --user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36表面看是page.goto()的封装但实际执行流程远更复杂预检阶段先调用playwright.chromium.launch(headlessTrue)启动浏览器但不立即导航。此时检查--user-agent是否合法正则匹配/^Mozilla\/\d\.\d \([^)]\) AppleWebKit\/\d\.\d$/非法则提前报错避免浪费启动资源。导航阶段执行page.goto(url, wait_untilcommit)即等待页面 commitnavigation committed而非传统load或networkidle。这是关键优化commit表示浏览器已接收 HTTP 响应头并开始解析 HTML比load快 200~500ms且能捕获 301/302 重定向——bsk navigate https://old.example.com会自动跟随跳转并将最终 URL 记录在--output-json中。等待阶段--wait-for main .price被解析为page.wait_for_selector(main .price, timeout15000)但额外注入一个“防抖逻辑”如果 selector 在 100ms 内连续出现又消失常见于 React 动态渲染会自动重试最多 3 次避免因渲染抖动误判失败。验证阶段等待成功后立即执行page.eval_on_selector(main .price, el el.textContent.trim())提取文本内容。若结果为空字符串或null视为验证失败即使等待成功也报错。这解决了“元素存在但内容为空”的经典陷阱。注意--wait-for支持三种语法CSS selectordiv#header、XPathxpath//button[aria-labelClose]、文本匹配textAdd to Cart。但文本匹配有局限——它依赖 Playwright 的page.get_by_text()对富文本节点如spanPrice: strong$29.99/strong/span可能匹配失败。此时必须用 CSS selector 显式指定span strong。3.2bsk screenshot从“截全屏”到“精准区域”的可控性设计命令格式bsk screenshot \ --full-page \ --clip x100,y200,width800,height600 \ --output /tmp/snap.png \ --quality 85--full-page并非简单调page.screenshot(full_pageTrue)。它做了三件事滚动合成Playwright 的full_pageTrue本质是滚动页面、分段截图、再拼接。BrowserSkill 在拼接时加入 10px 重叠区并用双线性插值消除接缝——实测比原生方法边缘更平滑。clip 优先级高于 full-page如果同时指定--full-page和--clip--clip会覆盖--full-page只截取指定矩形区域。这是反直觉但合理的设计--full-page是“我要整页”--clip是“我要局部”后者语义更强。quality 参数影响 PNG/JPEG 分离--quality仅对 JPEG 有效PNG 无损。当--output后缀为.png时--quality被忽略当为.jpg时传给PIL.Image.save(quality...)。我们曾因未注意此点在生成 PNG 时误设--quality 50结果文件大小毫无变化浪费调试时间。实操心得截图时务必加--timeout。某些 SPA 页面在--wait-for后仍会动态加载图片bsk screenshot默认等待 30s但若页面有懒加载广告可能超时失败。建议显式设--timeout 45并配合--wait-for img[loadingeager]确保关键图片加载完毕。3.3bsk extract结构化数据提取的“零代码”方案命令格式bsk extract \ --selector article.product \ --fields titleh1|text, price.price|text, skudata-sku|attr \ --output-json \ --max-items 50--fields是 BrowserSkill 最惊艳的设计。它用管道符|分隔“定位器”和“提取器”支持四种提取器textel.textContenthtmlel.innerHTMLattrxxxel.getAttribute(xxx)valueel.value适用于 input--selector article.product并非简单page.query_selector_all()。它先执行page.locator(article.product).all()获取所有匹配元素再对每个元素执行--fields解析。关键在于它自动处理嵌套关系。例如--fields nameh1|text, descp|text会为每个article.product提取其内部的h1和p而非全局查找所有h1和p。--max-items 50是安全阀。当页面有 1000 个article.product时它只处理前 50 个防止内存爆炸。我们曾在线上环境遇到过电商列表页返回 5000 商品卡片bsk extract默认无限制导致 Python 进程 OOM。现在所有生产脚本都强制加--max-items。常见陷阱--fields中的|是字面量不能出现在 selector 或 attr 名中。若需提取>bsk click button#buy-now \ --wait-for button#buy-now:not(:disabled) \ --scroll-into-view \ --timeout 10bsk click的核心价值不在“点击”而在“确保能点、点得准、点得稳”。--wait-for是前置条件必须元素存在且非 disabled。button#buy-now:not(:disabled)比单纯button#buy-now多一层业务逻辑校验。--scroll-into-view不是简单调element.scrollIntoView()。它先计算元素在 viewport 中的可见比例若可见面积 10%则执行平滑滚动behavior: smooth否则直接点击。这避免了“元素在视口外却强行点击导致失败”的问题。点击后自动等待page.wait_for_event(domcontentloaded, timeout5000)确认点击触发了新页面加载或 AJAX 请求。若 5s 内无 DOM 变化则认为点击无效如按钮无事件绑定报错 “Click did not trigger navigation or DOM update”。注意bsk click默认禁用forceTrue。Playwright 的forceTrue会绕过 visibility 检查导致点击不可见元素——这在自动化中是反模式。BrowserSkill 要求你显式加--force才启用且日志会警告 “Forced click bypasses visibility check”。3.5bsk fill表单填充的“智能类型推断”命令格式bsk fill input#email userexample.com \ --clear-first \ --press-enterbsk fill的智能体现在对输入类型的自适应对typeemail的 input自动添加符号校验若值不含报错 “Invalid email format”对typenumber尝试parseFloat()失败则报错对typedate接受YYYY-MM-DD格式自动转换为input.value 2023-12-25对无 type 属性的 input按字符串处理。--clear-first是安全默认它调用element.clear()而非element.fill()。前者清空输入框并触发input事件后者只是设置 value 属性不触发事件——这对依赖input事件的前端校验如实时邮箱格式检查至关重要。--press-enter在填充后模拟回车键适用于搜索框、登录表单等需提交的场景。它等价于element.press(Enter)而非page.keyboard.press(Enter)确保事件绑定在目标元素上。实操心得bsk fill对富文本编辑器如contenteditabletrue支持有限。它会尝试element.fill()但多数编辑器需element.evaluate((el, text) el.innerText text)。此时应改用bsk eval执行自定义 JS。4. 实操全流程从零部署到生产级监控脚本4.1 环境准备三步完成可复现的 CLI 环境Step 1Python 环境隔离不要用系统 Python 或全局 pip。BrowserSkill 依赖 Playwright而 Playwright 的浏览器二进制下载受网络影响大。推荐使用pyenvvenv# 安装 pyenvmacOS brew install pyenv pyenv install 3.11.7 pyenv global 3.11.7 # 创建项目虚拟环境 python -m venv ./bsk-env source ./bsk-env/bin/activate # 升级 pip避免旧版 pip 下载 wheel 失败 pip install --upgrade pipStep 2安装 BrowserSkill 及浏览器# 安装时自动下载 Chromium约 180MB pip install browserskill # 验证安装 bsk --version # 应输出 BrowserSkill 0.8.3 # 检查浏览器兼容性关键 bsk check-compat # 输出示例 # ✓ Chromium 115.0.5790.170 OK # ✗ Chrome 109.0.5414.120 (missing WebKit2GTK symbols) # ✓ Edge 115.0.1901.203 OKStep 3配置默认浏览器与超时创建~/.browserskill/config.toml[browser] default chromium # 可选 chromium, firefox, webkit, msedge headless true timeout 30000 # 全局超时单位毫秒 [output] json_indent 2 screenshot_quality 90此配置让bsk navigate默认使用 Chromium 无头模式超时 30s截图质量 90。无需每次命令加--timeout 30000。注意bsk check-compat会扫描 PATH 中所有 Chrome/Edge 可执行文件。若你装了多个 Edge 版本如 Stable/Beta/Dev它会报告所有版本状态。生产环境建议只保留一个稳定版避免--browsermsedge时随机选到 Beta 版本。4.2 开发第一个监控脚本检测登录页可用性目标每 5 分钟检查https://app.example.com/login是否能正常加载且包含 “Welcome back” 文案。脚本healthcheck-login.sh#!/bin/bash set -e # 任一命令失败即退出 # 定义变量 URLhttps://app.example.com/login EXPECTED_TEXTWelcome back LOG_FILE/var/log/bsk-health.log TIMESTAMP$(date %Y-%m-%d %H:%M:%S) # 执行检查 if bsk navigate $URL \ --wait-for body \ --timeout 20000 \ --user-agent HealthCheckBot/1.0 \ --output-json /tmp/login-check.json 2/dev/null; then # 提取页面标题和文案 TITLE$(jq -r .title // /tmp/login-check.json) HAS_WELCOME$(bsk extract \ --selector body \ --fields texttext|text \ --output-json \ --max-items 1 \ 2/dev/null | jq -r select(.text | contains(Welcome back)) | .text | wc -l) if [ $HAS_WELCOME 1 ]; then echo [$TIMESTAMP] OK: Login page loaded, title$TITLE $LOG_FILE else echo [$TIMESTAMP] FAIL: Welcome back not found in body $LOG_FILE exit 1 fi else echo [$TIMESTAMP] FAIL: Navigation timeout or error $LOG_FILE exit 1 fi部署到 crontab# 每 5 分钟执行一次 */5 * * * * cd /opt/bsk-monitor /bin/bash healthcheck-login.sh关键设计点set -e确保任何子命令失败立即终止避免错误静默2/dev/null抑制bsk的 stderr如下载进度条只保留业务错误jq提取 title 是为了日志可读性非必需bsk extract用--max-items 1限制只查第一个 body避免页面有多个 body 时误判。4.3 进阶实战电商比价脚本Chrome Edge 双引擎校验场景某商品在京东、淘宝、拼多多的价格需每日比对但各平台反爬策略不同——京东需 Chrome User-Agent淘宝需 Edge因其风控识别 Chrome 太严拼多多则两者皆可。脚本price-compare.py用 Python 调用 bskimport subprocess import json import sys def run_bsk(cmd): 封装 bsk 命令执行统一错误处理 try: result subprocess.run( cmd, shellTrue, capture_outputTrue, textTrue, timeout60 ) if result.returncode ! 0: raise RuntimeError(fbsk failed: {result.stderr.strip()}) return json.loads(result.stdout) except subprocess.TimeoutExpired: raise RuntimeError(bsk command timeout) except json.JSONDecodeError: raise RuntimeError(bsk output not valid JSON) # 京东Chrome jd_price run_bsk( bsk extract --selector .price --fields pricetext|text --output-json --browserchromium --user-agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 https://item.jd.com/100000000000.html ) # 淘宝Edge tb_price run_bsk( bsk extract --selector #J_StrPriceModBox .tm-price --fields pricetext|text --output-json --browsermsedge --user-agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203 https://item.taobao.com/item.htm?id123456789 ) # 拼多多Chromium无 UA 限制 pd_price run_bsk( bsk extract --selector .price-normal --fields pricetext|text --output-json https://yangkeduo.com/goods.html?goods_id987654321 ) # 汇总 prices { jd: float(jd_price[0][price].replace(¥, ).strip()), tb: float(tb_price[0][price].replace(¥, ).strip()), pd: float(pd_price[0][price].replace(¥, ).strip()) } print(json.dumps(prices, indent2))为什么用 Python 调 bsk 而非纯 CLICLI 适合单步操作多源聚合需逻辑判断subprocess.run可捕获returncode区分“页面无价格”和“网络超时”--browsermsedge参数让同一份代码在不同浏览器下运行规避平台风控。注意淘宝页面结构极不稳定#J_StrPriceModBox .tm-price可能下周就失效。BrowserSkill 的优势在于你只需改一行--selector无需重写整个爬虫框架。我们团队维护的 32 个比价脚本平均每月修改 selector 1.2 次而框架代码两年未动。4.4 Docker 化部署构建轻量级无头执行镜像DockerfileFROM python:3.11-slim # 安装 Playwright 依赖 RUN apt-get update apt-get install -y \ libnss3 \ libglib2.0-0 \ libgtk-3-0 \ libxss1 \ libxtst6 \ libxrandr2 \ libgbm-dev \ rm -rf /var/lib/apt/lists/* # 创建非 root 用户 RUN useradd -m -u 1001 -G root -d /home/appuser appuser USER appuser WORKDIR /home/appuser # 安装 BrowserSkill自动下载 Chromium RUN pip install --no-cache-dir browserskill # 复制脚本 COPY healthcheck-login.sh /home/appuser/ RUN chmod x /home/appuser/healthcheck-login.sh # 健康检查 HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ CMD curl -f http://localhost:8000/health || exit 1 CMD [./healthcheck-login.sh]构建与运行docker build -t bsk-health . docker run -d --name bsk-monitor -v /var/log:/var/log bsk-health镜像大小优化点python:3.11-slim基础镜像仅 120MBapt-get install只装 Playwright 必需的库删掉apt缓存pip install时--no-cache-dir避免 pip 缓存膨胀最终镜像 247MB比同等功能的 Selenium 镜像580MB小 57%。提示Docker 中--headless模式需额外参数。Playwright 在容器中默认启用headlessnew但某些旧版 Chromium 需--headlesschrome。BrowserSkill 通过BROWSER_HEADLESS_MODEnew环境变量控制可在 Dockerfile 中ENV BROWSER_HEADLESS_MODEnew。5. 常见问题与排查技巧实录来自 37 次线上故障的总结5.1 “Command not found: bsk” —— PATH 陷阱现象pip install browserskill成功但终端输入bsk报错command not found。原因pip install将bsk可执行文件放在~/.local/bin/Linux/macOS或%APPDATA%\Python\Python311\Scripts\Windows而该路径未加入PATH。解决方案Linux/macOS在~/.bashrc或~/.zshrc中添加export PATH$HOME/.local/bin:$PATH然后source ~/.bashrcWindows在“系统属性 → 高级 → 环境变量”中将C:\Users\{user}\AppData\Roaming\Python\Python311\Scripts加入用户 PATH。注意pip install --user和pip install全局安装路径不同。BrowserSkill 官方文档默认--user但 CI 环境建议用venv避免路径冲突。5.2 “TimeoutError: waiting for selector ‘.btn’ failed” —— 等待逻辑误判现象页面明明有.btn元素bsk navigate --wait-for .btn却超时。排查步骤确认元素是否在 DOM 中加--debug参数bsk navigate https://example.com --wait-for .btn --debug会启动有界面的 Chromium暂停在等待点人工检查检查 CSS 选择器有效性在 Chrome DevTools 中$$(.btn)看是否返回非空数组验证是否在 Shadow DOM 内若.btn在shadow-root中需用--wait-for shadow#root .btnBrowserSkill 支持 shadow selector检查是否动态渲染有些按钮由 JS 在DOMContentLoaded后插入--wait-for应改为--wait-for document.querySelector(.btn) ! null用--wait-for-js。实操心得--wait-for-js接受 JS 表达式比 CSS selector 更灵活。例如--wait-for-js window.__data window.__data.products.length 0可等待全局变量初始化。5.3 “Screenshot is blank/black” —— 渲染时机问题现象bsk screenshot --full-page生成的 PNG 全黑或空白。根本原因页面使用 WebGL 或 Canvas 渲染而无头模式下 GPU 加速被禁用。解决方案临时启用 GPU仅限 Linuxbsk screenshot --full-page --browser-arg--use-glegl降级为 raster 模式bsk screenshot --full-page --browser-arg--disable-gpu改用--clip截局部避开 Canvas 区域截取周边 HTML 内容。注意--browser-arg是高级参数会透传给 Playwright 的launch()。--use-glegl在 Ubuntu 22.04 上需安装libegl1-mesa否则启动失败。5.4 “Extract returns empty array” —— 选择器作用域错误现象bsk extract --selector li --fields texttext|text返回空但页面有大量li。原因--selector默认在document下查找但若页面用iframe加载内容li在 iframe 内则需指定--frame iframe[namecontent]。正确命令bsk extract \ --frame iframe[namecontent] \ --selector li \ --fields texttext|textBrowserSkill 的--frame支持三种定位namexxx按 name 属性srcxxx按 src 属性index0按索引从 0 开始。提示bsk navigate后--frame参数会自动继承到后续命令无需重复指定。5.5 “Edge browser fails with ‘Failed to launch browser’” —— Edge 特定问题现象bsk navigate --browsermsedge报错Failed to launch browser: Error: spawn ... ENOENT。原因BrowserSkill 默认查找msedge可执行文件但 Windows 上 Edge 可能安装在C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe而 PATH 中未包含该目录。解决方案显式指定路径bsk navigate --browser-path C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe注册为系统命令在 Edge 安装目录下创建msedge.bat内容为echo off start C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe %*并加入 PATH。注意Edge 的--headless模式在 115 版本才稳定。低于此版本bsk会自动降级为--headlessnew但仍有概率崩溃。建议生产环境固定 Edge 版本。6. 工具链延伸BrowserSkill 如何融入现有技术栈6.1 与 CI/CD 集成GitHub Actions 中的端到端测试.github/workflows/e2e.ymlname: E2E Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11