从零搭建Python+Selenium自动化测试框架的实战指南
做PythonSelenium自动化测试这些年我见过太多“能跑就行”的脚本变成后期维护的噩梦。最典型的场景是什么用例写了一大堆第一次执行全绿隔了两周前端改版一个按钮的id变了瞬间崩掉二十条用例。然后几个人围着定位表达式改半天改完这里又炸那里。后面我花了不少时间把整套体系重新捋了一遍从驱动初始化、页面对象封装、数据驱动到报告输出和CI集成一层一层搭出了一个真正能长期维护的框架才把这些坑基本填平。这篇东西是我从零搭建PythonSelenium自动化测试框架的实操总结不是教科书也没有废话。主要面向正在做Web UI自动化的测试工程师或者是刚准备从“脚本阶段”过渡到“框架阶段”的同学。我会把框架的架构分层、核心封装、用例编写、数据驱动、报告输出、常见故障和CI落地全部串起来讲一遍。1. 框架设计的核心思路与选型1.1 自动化测试框架到底解决了什么问题先说清楚一件事自动化测试框架不是用来自嗨的它的核心价值是解决“回归测试做不动”的痛点。一个业务系统登录、下单、审批、支付这些链路每次发版前都要回归。人肉点一遍少说两小时流程一多还要排班轮换效率非常低。把高频、稳定、有明确断言的流程脚本化让机器替代人去跑这是自动化最基本的目标。但只有脚本远远不够。脚本和框架的差别在哪里脚本是一个个孤立的用例文件各写各的互相之间没有任何约束框架是把用例、数据、页面对象、公共方法、日志、报告、持续集成全部串联起来的一套工程化体系。框架给项目带来的核心收益是三个词可维护、可扩展、可复用。可维护指的是页面元素变动时改动范围被限制在很小的范围内不会牵一发动全身。可扩展指的是新增业务模块时可以按照既有的模式快速补充页面对象和用例不需要推翻重来。可复用指的是登录、数据准备这类公共能力被封装起来每一条用例调用即可不用反复造轮子。我见过不少团队自动化测试做了半年用例数不少但维护成本居高不下最后跑一次全公司都要围观然后一起叹气。根子就在于没搭框架或者搭了个假框架——只是把脚本集中放在一个目录里并没有真正分层。1.2 技术栈为什么选Python Selenium pytest做Web自动化技术栈选项其实不少。早期有人用QTP、UFT这类商业工具也有人用Robot Framework这种关键字驱动方案。我最终把方向定在Python Selenium pytest主要基于这几点考量语言门槛。测试团队的成员不一定都是科班开发出身Python语法的学习曲线相对平缓业务测试人员也能快速看懂用例、写简单脚本。这一点在团队协作里特别重要框架不是一个人的玩具必须让整个团队都能上手维护。生态成熟度。Selenium是Web自动化领域事实上的标准覆盖面广踩过的坑都有人踩过了遇到问题搜一下基本都有答案。Python生态里pytest、allure、xdist这些配套工具的完整度也很高组合起来能覆盖从用例管理、数据驱动、并发执行到报告输出的全链路。封装友好度。Selenium WebDriver的API设计还算规整定位、点击、输入、切换窗口等操作边界清晰适合做二次封装。封装之后用例层的代码可以完全屏蔽Selenium的细节以后就算换底层驱动用例代码也不需要大面积改动。当然Selenium也不是银弹。它在处理canvas画布、Flash这类复杂元素时有天然的短板执行速度上UI自动化比API测试慢一到两个数量级。但针对绝大多数Web业务系统的表单、流程、页面交互回归它依然是当前最务实、最主流的选择。2. 框架分层与目录设计2.1 四层架构到底怎么分框架搭得好不好分层是关键。我采用的是经典的四层架构从上到下分别是测试用例层存放具体用例调用页面对象的方法配合断言和数据组成完整的测试场景。页面对象层把每个页面的元素定位和操作行为封装成独立的类一个页面一个类。基础封装层WebDriver初始化、公共定位方法、等待机制、截图、日志等通用能力都放在这里。数据与配置层存放测试数据、环境地址、账号信息、定位表达式等外部资源。各层之间的依赖方向是单向的用例层依赖页面对象层页面对象层依赖基础封装层数据配置层被各层按需引用。反向依赖是大忌——比如用例层直接去操作driver.find_element页面对象层去读测试数据文件这些都会让分层失效。这里最核心的概念是Page Object Model也就是POM。简单说就是把页面当成一个对象把页面上的元素和操作封装进这个对象里。用例只跟页面对象打交道不直接接触Selenium API。这么做的直接好处是前端改版的时候UI元素的定位变了你只需要修改对应页面对象里的定位表达式几十条用例一行都不用动。2.2 目录结构与模块职责我常用的目录结构长这样可以直接参考使用project/ ├── config/ │ ├── config.yaml # 环境地址、浏览器类型、超时参数 │ └── locators.yaml # 页面元素定位表达式 ├── data/ │ ├── test_login.json # 登录模块测试数据 │ └── test_order.json # 下单模块测试数据 ├── pages/ │ ├── base_page.py # 基础页面封装 │ ├── login_page.py │ ├── home_page.py │ └── order_page.py ├── testcases/ │ ├── conftest.py # pytest 夹具driver 管理和全局钩子 │ ├── test_login.py │ └── test_order.py ├── utils/ │ ├── driver.py # WebDriver 初始化 │ ├── logger.py # 日志封装 │ ├── screenshot.py # 截图工具 │ └── assert_utils.py # 断言工具 ├── reports/ # allure 报告输出 ├── logs/ # 运行日志 ├── requirements.txt └── pytest.ini几个模块的核心职责说明一下config/这一层的价值在于把环境差异集中管理。测试环境、预发环境的地址不一样账号权限不一样如果这些散落在各个用例里换环境就是灾难。统一放到配置文件里运行时解析成全局参数想切换环境只需要改一行配置。pages/这一层是框架的核心资产。每个页面类的命名规则统一类名对应页面名方法名对应业务操作。比如登录页的input_username、input_password、click_login_button方法内部放的是定位表达式和操作细节外部使用者根本不需要关心。utils/放置与业务无关的通用能力driver的创建、logger的初始化、失败截图的生成都被封装成独立模块。这样做的目的是让这些能力可以被复用也方便单独维护。2.3 Page Object模式的落地要点POM模式看着简单落地的时候有几个细节容易翻车。第一页面对象里的方法命名要面向业务不要面向操作。比如input_username是面向业务操作而send_keys_to_element_by_xpath就是反面教材。页面对象要表达的是“用户在登录页输入了用户名”这个动作不是“在某条xpath定位到的元素里输入了字符串”。第二页面对象不要塞入断言逻辑。页面对象负责“做什么”断言属于用例层“验什么”。如果把断言写进页面对象一旦页面对象被多条用例复用不同用例的断言需求不同就会越改越乱。第三一个页面对象只对应一个页面不要把多个页面塞到一个类里。登录后跳转到首页登录页对象只负责登录操作首页的检查放首页对象。跨页面的流程编排由用例层负责。3. 核心代码封装与实现细节3.1 WebDriver初始化稳定启动是第一步Driver初始化是整个框架的地基这里偷懒后面全是坑。我封装了一个driver.py负责创建浏览器实例并处理各类启动参数。# utils/driver.py from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(browserchrome, headlessFalse, window_size1920,1080): if browser chrome: options Options() options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(f--window-size{window_size}) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-logging]) if headless: options.add_argument(--headlessnew) return webdriver.Chrome(optionsoptions) # firefox / edge 分支略这里每个参数都有它的用途简单拆解一下--disable-gpu和--no-sandbox在CI容器环境里几乎必配不然Chrome启动容易报错。--disable-dev-shm-usage解决的是/dev/shm空间不足的问题容器环境里尤其常见。--window-size显式固定了浏览器窗口大小这一点经常被忽略——如果窗口不固定响应式页面在不同尺寸下的元素布局会变化直接导致定位失败。--headlessnew是新版Chrome无头模式的参数老参数--headless在新版本里会有兼容问题。excludeSwitches里的enable-logging必须加上否则Chrome的各种INFO日志会疯狂刷屏排查问题的时候有效日志全被冲掉了。除了这些启动参数还有一点我建议在初始化时就做掉为整个driver实例设置一个默认的页面加载超时。用driver.set_page_load_timeout(30)限制页面加载时间避免某个页面卡死把整个测试拖垮。3.2 在BasePage里封装统一操作入口BasePage是所有页面对象的父类也是封装Selenium细节的关键位置。我在里面提供了一套统一的定位和操作入口用例层和页面对象层都不直接去调用driver.find_element。# pages/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(self.driver, 10) def find_element(self, locator): 定位元素locator 为元组例如 (By.ID, username) return self.wait.until(EC.presence_of_element_located(locator)) def click(self, locator): self.find_element(locator).click() def input_text(self, locator, text): element self.find_element(locator) element.clear() element.send_keys(text) def get_text(self, locator): return self.find_element(locator).text def is_visible(self, locator): try: self.wait.until(EC.visibility_of_element_located(locator)) return True except Exception: return False这些方法的实现都不复杂但统一入口本身就是价值。举一个实际例子input_text里先clear()再send_keys()这个细节能避免很多输入框残留旧值的偶发问题。如果每个人在用例里各写各的输入方式这种细节就无法统一。另外注意我把显式等待写进了find_element里。为什么要这样做往下看。3.3 等待策略显式等待的细节与重试机制等待是Selenium自动化里最容易被低估、也最容易出事的环节。我见过很多人写的代码是这样的time.sleep(5) driver.find_element(By.ID, username).send_keys(admin)这其实是非常糟糕的写法。time.sleep是强制等待无论页面加载快慢都固定等5秒。页面慢了5秒不够照样报错页面快了白白多等好几秒。几十条用例跑下来浪费的时间非常可观。Selenium官方推荐用显式等待。WebDriverWait配合expected_conditions可以精确控制等待的条件和超时时间。元素什么时候出现、什么时候可点击、什么时候可见这些状态都能精确等待。它的内部机制是轮询每隔一小段时间就检查一次条件是否满足满足就立即继续执行不满足一直等到超时。我在BasePage里统一使用显式等待并且完全不用隐式等待。这里解释一下原因。隐式等待是全局设置的一次设置对后面所有的find_element生效。它的问题在于一是只能等元素出现等不了可点击、可见这些更具体的状态二是在某些混合使用显式等待的场景下两类等待时间叠加会让“元素不存在”的判断慢到难以忍受三是排查问题的时候一个全局配置的隐性因素会让定位失败的响应变得很吃力。封装完显式等待之后还有最后一层保障重试机制。页面加载偶尔抽风比如某个资源加载慢导致元素延迟出现10秒超时可能恰好差那么一点点。所以我在find_element里加了重试逻辑——第一次超时后等待1秒再尝试一次还不成功才抛出异常。这个策略在实际执行中能明显降低偶发失败率且代价极小。关于定位表达式的优先级我始终坚持这个顺序有id用idid唯一且稳定解析最快没有id时用name、class_name这些都不靠谱时才考虑XPath或CSS。XPath不要用浏览器复制出来的那种/html/body/div[1]/div[2]/...绝对路径一定要写相对路径配合元素属性和文本内容定位比如//*[idlogin-form]//input[nameusername]。4. 用例层的数据驱动与pytest集成4.1 pytest fixture实现浏览器生命周期管理用例层我选择pytest作为测试执行引擎。它最核心的机制是fixture用于管理和复用测试资源。一个典型场景是每条用例执行前需要启动浏览器、初始化页面对象执行完要关掉浏览器这个前后置逻辑不需要在每条用例里重复写放在conftest.py里统一管理。# testcases/conftest.py import pytest from utils.driver import create_driver from pages.login_page import LoginPage pytest.fixture() def driver(): _driver create_driver(browserchrome, headlessTrue) _driver.maximize_window() yield _driver _driver.quit() pytest.fixture() def login_page(driver): return LoginPage(driver)这里有个值得注意的关键词yield。普通函数里return就结束了但yield会把driver对象交给用例使用并且在用例跑完之后代码会回到yield后面的部分继续执行_driver.quit()。这就是pytest fixture实现“后置清理”的原理也是整个生命周期管理的核心。fixture还有作用域的概念。默认是function级别每条用例都重新创建driver。如果对一些不依赖页面状态的操作可以设置scopeclass或scopemodule让同一组用例共享一个浏览器实例减少启动开销。但要注意共享driver意味着用例之间会互相影响页面状态除非你清楚自己在做什么否则不建议轻易改作用域。4.2 参数化数据与代码分离自动化用例最忌讳把数据硬编码在代码里。我个人的原则是测试数据和测试逻辑必须分离。pytest里最常用的做法是parametrize参数化。# testcases/test_login.py import pytest import allure allure.title(登录-{case_name}) pytest.mark.parametrize( case_name, username, password, expected_tip, [ (正确账号登录, admin, admin123, 登录成功), (密码错误场景, admin, wrong, 用户名或密码错误), (用户名为空场景, , admin123, 请输入用户名), (密码为空场景, admin, , 请输入密码), ], ids[success, wrong_pwd, empty_user, empty_pwd], ) def test_login(case_name, username, password, expected_tip, login_page): login_page.input_username(username) login_page.input_password(password) login_page.click_login() assert expected_tip in login_page.get_login_tip()参数化的好处是清晰可见的一套登录逻辑四组测试数据覆盖四个场景。以后要新增场景只需要往参数列表里加一行用例代码一行都不用动。有些团队的数据量特别大比如几十家供应商的信息验证参数写在代码里会显得臃肿。这种情况可以把数据提取到JSON文件在用例里动态加载。import json import pytest def load_test_data(file_name): with open(fdata/{file_name}, r, encodingutf-8) as f: return json.load(f) class TestLogin: pytest.mark.parametrize( username,password,expected_tip, load_test_data(test_login.json), ids[item[case_name] for item in load_test_data(test_login.json)], ) def test_login_from_json(self, username, password, expected_tip, login_page): # 用例体同前 pass这里还有一个容易忽略的点ids参数。如果不设置idspytest报告里每条参数化用例的名称是一串元组值比如login_page[成功-abc-123]完全看不懂是哪条场景。设置了ids之后报告里显示的就是“正确账号登录”“密码错误场景”这种一目了然的名称失败时定位问题快得多。4.3 失败场景自动截图与现场保留自动化用例跑在无人值守的CI环境里失败之后如果不能留下现场证据排查会非常痛苦。所以框架里必须内置失败截图能力。pytest提供一个钩子函数pytest_runtest_makereport可以在每条用例执行完成后拿到执行结果再结合fixture来实现失败时截图。# testcases/conftest.py import allure import pytest pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver is not None: screenshot driver.get_screenshot_as_png() allure.attach( screenshot, name失败截图, attachment_typeallure.attachment_type.PNG, )这段代码的逻辑是用例执行结束后检查结果是否失败如果失败就把driver对象从用例参数里取出来截图并附到allure报告里。这样每一次CI失败报告里都会自动带上当时页面的截图。同样的思路还可以附加页面源码HTML排查前端解析类问题时页面源码比截图更有价值。这个“失败现场保留”机制是框架成熟度的重要标志。5. 测试报告与日志体系5.1 allure报告接入自动化测试跑完项目方最关心的不是代码覆盖率而是“这轮测试过了没有、挂了哪几条、为什么挂”。一套清晰美观的报告价值不亚于用例本身。allure是我目前用过的最顺手的报告工具和pytest配合特别紧密。接入方式不复杂安装依赖pip install allure-pytest执行时加参数pytest --alluredirreports/allure-results生成可视化报告allure generate reports/allure-results -o reports/allure-html --clean。接入之后allure会给每条用例自动生成结构化报告包含执行时间、用例步骤、失败日志、附带的截图和页面源码。还可以用装饰器让报告内容更丰富allure.feature(登录模块) allure.story(用户密码校验) allure.title(登录-{case_name})feature和story会在报告里生成层级分类测试主管一眼就能看到哪个模块挂得最多。title可以动态取参数值报告里每一个用例都有名字。5.2 日志让失败现场自动关联上下文截图能展示失败时的页面状态但链条上的前因后果往往要靠日志还原。我在框架里封装了logger工具统一管理日志输出级别和格式并且推荐在页面对象的关键操作里加上DEBUG级别的日志。# utils/logger.py import logging def setup_logger(nameauto_test, log_filelogs/autotest.log): logger logging.getLogger(name) logger.setLevel(logging.DEBUG) fmt logging.Formatter(%(asctime)s - %(levelname)s - %(message)s) file_handler logging.FileHandler(log_file, encodingutf-8) file_handler.setFormatter(fmt) console_handler logging.StreamHandler() console_handler.setFormatter(fmt) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger logger setup_logger()然后在整个框架的使用过程中把关键行为写入日志。比如登录页面对象的click_login方法里可以加一句logger.info(点击登录按钮等待跳转)。用例跑挂了打开日志一看执行到哪一步、点了什么、切换了什么窗口顺序一目了然。这和截图是互补的关系截图展示的是“现场”日志还原的是“经过”。6. 高频问题与实操避坑6.1 元素定位失败的排查思路Selenium自动化跑起来之后遇到最多的坑就是元素定位失败。报错信息往往就是一句NoSuchElementException解决办法不能蒙着头改要有排查思路。第一步先确认页面是否加载完成。有些元素不出现不是因为定位写错而是页面还在异步加载。这时候先看是否用了显式等待等待条件选择得对不对。等“元素出现”用presence_of_element_located等“元素可见可交互”要用visibility_of_element_located或element_to_be_clickable这几个条件是有区别的。第二步把实际页面结构拉出来对比。在失败的时候输出当前页面的HTML源码或者直接用driver的page_source属性存下来和定位表达式对照。很多定位问题是因为页面结构和预期不一致——多了一层div、class名带了动态后缀、元素被iframe包裹等等。不看实际页面凭空猜定位表达式是改不对的。第三步检查是否在iframe里面。这是新手最容易忽视的问题。如果目标元素在iframe里直接用driver.find_element怎么也找不到。必须先切换进去这个在6.2节详说。第四步确认元素是否被遮挡。定位到了元素但点击时报ElementClickInterceptedException通常是有弹窗、悬浮层、loading遮罩盖住了目标元素。这时可以等一下弹窗消失或者用JS强制点击作为兜底。JS点击能解决遮挡问题但会绕过正常用户的操作路径能不用的场景尽量不要用。排查这些问题的顺序比技巧本身更重要。建议的顺序是等待机制是否到位 - 定位表达式是否准确 - 是否存在iframe或遮挡 - 页面结构是否发生变化。6.2 iframe与多窗口切换的细节iframe的处理是Selenium自动化里的经典难题。页面里嵌了一个iframe里面可能就是登录表单或者某个业务模块。切换进入iframe的标准写法是from selenium.webdriver.common.by import By # 按索引切换页面第一个iframe driver.switch_to.frame(0) # 按id或name切换 driver.switch_to.frame(login_frame) # 按定位表达式切换 driver.switch_to.frame(driver.find_element(By.XPATH, //iframe[idlogin_frame])) # 操作完成后切回主文档 driver.switch_to.default_content()这里有几个易错点切进iframe之后主页面里的元素就访问不了了必须切回主文档才能继续操作主页面元素iframe还存在嵌套的情况一层套一层切到最里层之前要逐层进入另外如果iframe是动态加载的切换前要先确保iframe元素存在否则会报NoSuchFrameException。我用过的稳妥做法是用显式等待等iframe可见再执行切换。多窗口切换和iframe类似但方向不同。点击一个按钮会打开新标签页这时要操作新页面的元素需要先切换窗口# 点击打开新窗口前的句柄 main_window driver.current_window_handle # 执行打开新窗口的操作比如点击链接 # 等新窗口出现 windows driver.window_handles for handle in windows: if handle ! main_window: driver.switch_to.window(handle) break # 操作完切回主窗口 driver.switch_to.window(main_window)这个场景的坑在于window_handles这个列表的顺序不总是和打开顺序一致有时候切换窗口的时机不对可能切到的是旧窗口。所以我的习惯是循环遍历按句柄和当前窗口做对比找到非当前窗口的那个。6.3 用例偶发失败与并发执行稳定性偶发失败是自动化测试最磨人的问题。一条用例跑第一次挂了再单独跑一次又是绿的这种问题最难查。我总结了几个常见的偶发因素以及对应的加固措施可能原因典型表现加固措施页面资源加载慢元素时有时无显式等待超时重试机制动画过渡未结束点击报ElementClickIntercepted等待元素可点击后再操作接口响应延迟数据区域为空轮询等待目标数据出现浏览器崩溃/资源占用高整条用例进程消失限制并发数量容器内加资源限制测试环境数据被污染数据断言失败用例前置做数据清理或准备另外pytest-xdist支持用例并发执行能大幅缩短整体执行时间。比如四进程并行pytest -n 4 --distloadscope但并发也会引入新的问题多个用例共用同一个测试账号同时登录导致会话互踢用例之间有数据依赖先执行的用例改了数据状态后执行的用例。所以框架里做并发之前先要保证用例是可隔离的——数据上互不影响账号上要么各自独立要么用token等方式避免会话冲突。还有一个我强烈建议加上的机制失败重跑。给pytest配置失败自动重试有些偶发问题重跑一两次就过了。这个能力可以通过pytest-rerunfailures插件实现比如设置最多重试2次pytest --reruns 2 --reruns-delay 5加了重跑之后偶发失败的影响显著降低。但要记住重跑只是兜底不是万能药同一用例反复重跑还是不稳定的必须去查根因。7. 环境准备与CI落地7.1 环境准备要点不管是在本地跑还是在CI上跑环境准备都是绕不开的一环。核心步骤就三步装Python、装依赖、装浏览器驱动。Python版本建议3.9以上Selenium新版本对老Python支持并不好。依赖安装用requirements.txt统一管理pip install -r requirements.txtrequirements.txt内容大致如下selenium4.21.0 pytest8.2.0 allure-pytest2.13.5 pytest-rerunfailures14.0 pytest-xdist3.6.1 PyYAML6.0.1浏览器驱动是另一个常见的坑。新版Selenium 4.6之后内置了Selenium Manager可以自动下载匹配的driver省去了不少麻烦。如果团队用的还是老版本注意Chrome和chromedriver的版本必须对应否则启动必报错。我的建议是本地开发环境直接用最新版Selenium省心CI环境里注意保证driver和镜像中浏览器版本一致。7.2 让框架在CI里跑起来本地跑自动化只是第一步框架真正发挥价值是在CI里定时跑、提交代码触发跑。以GitLab CI为例一个最简的配置思路stages: - test autotest: stage: test script: - pip install -r requirements.txt - pytest --alluredirreports/allure-results --reruns 2 - allure generate reports/allure-results -o reports/allure-html --clean artifacts: paths: - reports/allure-html - logs/ when: always这个配置里有几个细节值得注意when: always表示即使测试执行失败报告和日志也要打包保存。如果不加这个配置用例失败导致CI Job退出非零状态时artifacts可能不会被上传那就没有任何现场可以排查了。--reruns 2直接在CI命令里带上比在pytest.ini里配置更灵活。在开发容器里跑的时候大家可能不想要重跑逻辑不影响调试。如果用例数量大可以按模块拆分成多个Job并行。比如把test_login.py和test_order.py放到不同的Job里跑总体时间能压缩到单次跑的一半以下。但前提还是前面提到的用例之间必须可隔离。另外CI里跑自动化一定要用无头模式。没有显示器不开--headlessnew根本跑不了。还要注意CI容器里Chrome的沙箱问题初始化driver时的--no-sandbox参数就是为了应对这个场景。最后聊几句个人体会搭这个框架的过程里我最大的感悟是框架的复杂度不是越高越好而是越贴团队实际越好。刚开始搭框架的时候我也走过堆功能的弯路什么都要封装抽象好多层结果团队成员学起来吃力维护起来更吃力。后来慢慢精简把真正有用的部分沉淀下来这套框架才真正被大家用起来。最后再分享一个小建议无论框架搭得多完善都不要忽视手工探索测试的价值。自动化能保障的是确定性的回归逻辑而探索性测试发现的是预期之外的问题。把这层边界想清楚你搭框架的时候心态会稳很多知道哪些场景该投入自动化哪些场景人工跑更高效。