做接口测试的人,几乎都绕不开这样一个需求:登录接口返回一长串 JSON,里面有 token、userId、用户角色,甚至还有权限列表,后面一堆接口都要拿着这些动态值去拼参数。手动复制粘贴一次两次还行,压测脚本里一百个线程同时跑,每个用户拿到的值都不一样,这时候就轮到 JMeter 的关联功能上场,也就是后置处理器里的 Regular Expression Extractor(正则表达式提取器)。
这篇文章不打算从零讲正则语法,而是聚焦一个最容易卡住人的问题:怎样用正则提取器一次性匹配多个内容。单个值谁都会写,但"一个响应要取好多个字段""同一种格式的值要全部取出来""多个正则混在一起"这些情况,很多人就开始翻车了。我会按实际项目里的用法,把控件参数、三种多值提取姿势、完整案例和踩坑经验都写清楚,适合刚接触 JMeter 关联的测试同学,也适合写过几个脚本但总在正则这里栽跟头的朋友。
1. 先想清楚:关联需求到底从哪来,提取器又该放在哪
1.1 动态数据不能写死,只能用"规则"去取
接口测试里有个很基础但很重要的设计原则:不要相信上一个环境的返回值,更不要把它写死在脚本里。比如某登录接口每次返回的 accessToken 都不同,你把 A 环境抓到的 token 直接填到 B 环境的脚本里,第一个请求必挂。
常规做法是把这种动态值"关联"出来。关联这件事本身不是 JMeter 独有的概念,而是接口测试里对动态数据传递的通用处理方式:从上一个接口的响应里,按照某个规则把需要的值取出来,存成变量,下一个接口再引用这个变量。正则表达式提取器做的就是其中"制定规则、取出值、存成变量"这一步。
举个最常见的例子。登录接口返回:
{"token":"7f8c1e2a9b0d","userId":9527,"role":"admin"}后面查询用户信息的接口要传 userId,下单接口要在请求头里带 token,操作日志接口要根据 role 判断权限。这些值都是动态的,每次登录都不一样,必须从登录响应里提取。
1.2 提取器是后置处理器,一定要放在对应的取样器下面
Regular Expression Extractor 在 JMeter 里属于后置处理器(Post Processor),添加路径是:在某个取样器(通常是 HTTP 请求)上右键 -> 添加 -> 后置处理器 -> 正则表达式提取器。
执行顺序是:HTTP 请求把请求发出去、收到响应之后,紧接着执行它下面的后置处理器,把响应数据拿给提取器做正则匹配。所以它天然适合"从最近一次响应里取数据"。你把提取器放在 A 请求下面,它拿到的就是 A 请求的响应,跟 B 请求没关系。
这里有个小细节容易被忽略:如果请求发生重定向或者页面里有嵌入资源,主请求的响应里可能没有你想要的内容,真正的内容在子请求里。这时要在提取器面板上的"应用范围"里选 Main sample and sub-samples,而不是默认的 Main sample only。反正我的习惯是一开始先用 Main sample only 调试,如果找不到值再去检查是不是子请求响应里才有。
还有一点想提前说:提取出来的变量默认只在当前线程内有效。JMeter 一个线程就是一个虚拟用户,线程之间的 JMeter 变量默认是不共享的。单线程组脚本里不用操心,多线程组之间想传值,那是另一个话题,这里先不展开。
2. 逐个拆解提取器控件:真正影响结果的就是这五个字段
2.1 引用名称:先决定变量叫什么
引用名称(Reference Name)就是你给这个变量起的名字,也是后面所有引用的基础。比如叫 token,后面用${token}引用。命名建议用英文字母、数字、下划线,别用中文,也别带点号,否则在 JMeter 属性、命令行传参、循环控制器里都容易出奇怪问题。
配合匹配数字为 -1 的场景,变量名还会自动生成带下标的变量,这部分在下一节讲,这里先建立概念:引用名称是整个提取结果的"前缀"。
2.2 正则表达式:关键不是会写正则,而是知道括号加在哪
提取器的正则表达式字段,最重要的要求是:必须包含括号捕获组。没有括号的正则只能判断"匹不匹配得上",取不到内容。比如响应里有<title>测试报告</title>,你填<title>(.*?)</title>,括号里的.*?就是被捕获的部分,也就是我们真正要的"测试报告"。
很多人写正则喜欢从网上抄一大串,结果括号加了一堆,编号全乱。这里强调一个原则:每个括号都会参与编号,$1$、$2$、$3$ 是按括号出现的先后顺序排的。如果你为了分组语义随手加括号,小心后面模板取错组。
在接口测试里,我建议针对 JSON 字符串直接按文本匹配,而不是强行解析。比如:
"token":"([^"]+)"意思是从"token":"后面开始,抓取所有非引号字符,一直到下一个引号为止。[^"]+比.*?更稳,因为它明确排除了引号,不会把后面的内容也吃进去。如果响应里可能带空格,可以宽松一点:
"token"\s*:\s*"([^"]+)"2.3 模板:决定提取出来的值长什么样
模板字段默认是$1$,意思是用第一个捕获组的值作为最终结果。如果正则里只有一个括号,你基本不用改它。
当正则里有多个括号时,模板就成了编排工具。比如要取userId和userName两个字段,正则写成:
"userId":(\d+),"userName":"([^"]+)"模板填$1$|$2$,最终变量值就是1024|tester01。你也可以写$1$$2$,这时两个值会直接拼接,中间没有分隔符。如果只想取第二个括号的值,模板直接写$2$。
还有一个不太常用的:$0$ 代表整个正则匹配到的完整文本,偶尔需要"连前缀带内容一起要"的时候有用。
2.4 匹配数字:1、0、-1 的含义完全不同
这个字段是提取多个内容时最容易搞错的地方。
- 空着或者填 1:取第一个匹配到的结果,这是最常见的情况。
- 填 0:随机取一个匹配到的结果,注意是随机,不是"全部"。
- 填 -1:取所有匹配到的结果,变量会按
名称_1、名称_2、名称_3的方式存放,另外还会生成一个名称_matchNr记录总共匹配了几个。 - 填大于 1 的数字:比如填 2,就是取第二个匹配到的结果。
我见过不少同事把 0 当成"全都要",结果压测时每个线程拿到的值都不一样,数据全乱。填 -1 才是全量提取,这个一定要记牢。
2.5 缺省值:匹配失败时留个"案底"
缺省值(Default Value)是在正则没匹配到内容时赋给变量的值。如果这里留空,变量就不会被创建,后面用到${token}的地方会被替换成空字符串,问题很隐蔽。
建议调试阶段给每个提取器填一个明显的标记,比如NOT_FOUND或提取失败。这样一旦提取有问题,后续接口的参数里就会带上这个标记,在断言和日志里一眼就能发现。
3. 匹配多个内容的三种姿势,从简单到进阶
3.1 姿势一:一个正则里放多个括号,用模板拼装
适用于"几个字段离得近、顺序固定"的响应。比如返回一段 HTML:
<form> <input name="userId" value="1024"> <input name="role" value="admin"> <input name="menuId" value="M3"> </form>要一次取出 userId、role、menuId,正则可以写成:
name="userId" value="(\d+)".*?name="role" value="(\w+)".*?name="menuId" value="(\w+)"模板填$1$|$2$|$3$,引用名称设为 userInfo,那么${userInfo}就是1024|admin|M3。这种"一取多"的方式只需要一次匹配,效率高,脚本也干净。
但要注意:如果字段之间的内容经常变化、顺序也不固定,用这种方式会比较脆。比如中间多了一个字段,后面的.*?可能匹配错位置。这种情况下别硬写一个超长正则,拆成多个提取器更稳。
另外,当某个正则里确实有多个捕获组,而且你想分别拿到每组的值,JMeter 5.x 还会顺带生成变量名_g1、变量名_g2这样的分组变量,可以直接单独引用。很多教程不提这个,但实际调试很有用。
3.2 姿势二:匹配数字填 -1,把同格式的全部值批量取出来
适用于"响应里有好多条同样格式的数据,每条都要用到"的场景。最典型的就是查询列表后,要把列表里的 ID 全部提取出来,然后循环发起详情请求。
假设接口返回:
<order id="1001"><name>OrderA</name></order> <order id="1002"><name>OrderB</name></order> <order id="1003"><name>OrderC</name></order>正则写成:
order id="(\d+)"引用名称填 orderId,匹配数字填 -1。匹配完成后,JMeter 自动生成:
- orderId_1 = 1001
- orderId_2 = 1002
- orderId_3 = 1003
- orderId_matchNr = 3
配合 JMeter 的 ForEach 控制器,可以很优雅地遍历这些值。ForEach 控制器的输入变量前缀填 orderId,勾选 Add underscore,输出变量名填 oid,它就会自动按照 orderId_1 到 orderId_matchNr 的范围内循环,循环体里的 HTTP 请求直接用${oid}引用即可。
这种做法在压测"先查列表,再逐个查看详情"的链路里非常好用。注意一点:match number 为 -1 时,不要赌基础变量${orderId}也存了值,老老实实用${orderId_1}这种带下标的引用。
3.3 姿势三:多正则提取器,以及"多个普通提取器"方案
有些响应里,要提取的字段分布很散、格式差异大。比如一个响应里既有 token,又有金额,还有一段列表。这时强行合并成一个正则,会写出又臭又长、维护起来想哭的表达式。
比较推荐的做法有两种。
第一种:同一个取样器下叠加多个普通正则提取器。每个提取器各管一个字段,变量名各不冲突,互不干扰。比如一个提取器提取 token,一个提取 userId,一个提取 role。这种方式很简单,缺点是提取器多了以后,光看控件列表有点乱,但好处是每个规则独立,出问题好排查。
第二种:用 JMeter 5.5 以后自带的 Multiple Regex Extractor(中文界面一般叫"多正则表达式提取器"),也是后置处理器的一种。它用表格形式把多个规则集中放在一起,每一行配置一组变量名、正则表达式、模板和匹配数字,本质上跟放多个普通提取器效果一样,但界面集中,规则多的时候维护更方便。
如果版本低于 5.5,普通提取器叠加使用完全够用,不一定要为了这个功能升级环境。
3.4 三种姿势到底怎么选
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 一个响应里几个字段挨得近、顺序稳定 | 一个正则多括号 + 模板 | 一次匹配,性能最好 |
| 同一种格式的值反复出现,要全部取出来 | 匹配数字 -1 + ForEach 循环 | 原生支持批量变量,天然配合遍历 |
| 字段分散、格式差异大 | 多正则提取器或多个普通提取器 | 规则独立,互不干扰,好维护 |
| 返回是标准 JSON | JSON Extractor + JSONPath | 不需要抠引号和反斜杠,最不容易错 |
| 需要复杂解析、拼接、判断 | JSR223 + Groovy | 想怎么处理就怎么处理 |
很多人一开始就想学最复杂的,其实 80% 的关联需求用前两种就能解决。真正要上复杂方案的,往往是响应结构本身就很恶心的老系统。
4. 一个能直接复制的实战案例:登录返回多个字段,后续接口全部引用
4.1 模拟返回体与正则设计
假设某个后台管理系统的登录接口 POST /api/login 返回:
{ "code": 0, "message": "success", "data": { "accessToken": "8f3a9c21-6b7e-4d2a-9f10-2c8d4e6a7b10", "userId": 1024, "userName": "tester01", "role": "operator", "permissions": ["order:view", "order:create", "order:batch"] } }我们要从登录响应里提取 accessToken、userId、userName、role,以及 permissions 里的三个权限值,供后续"下单""查单""查询权限"等接口使用。
用 Multiple Regex Extractor 配置如下几行规则:
| 变量名 | 正则表达式 | 模板 |
|---|---|---|
| accessToken | "accessToken"\s*:\s*"([^"]+)" | $1$ |
| userId | "userId"\s*:\s*(\d+) | $1$ |
| userName | "userName"\s*:\s*"([^"]+)" | $1$ |
| role | "role"\s*:\s*"([^"]+)" | $1$ |
| perm1 | "permissions"\s*:\s*\["([^"]+)","([^"]+)","([^"]+)" | $1$ |
| perm2 | 同上 | $2$ |
| perm3 | 同上 | $3$ |
注意permissions的正则里,数组的中括号需要转义成\[,因为方括号在正则里是字符类标记。JSON 数组元素之间的逗号直接照抄即可。
4.2 后续 HTTP 请求和请求头里的变量引用
在登录请求之后新增一个"创建订单"HTTP 请求,给它添加一个 HTTP Header Manager,把登录获取到的 accessToken 加到请求头里:
- Header Name: Authorization
- Header Value:
Bearer ${accessToken}
请求体我用 Body Data 传 JSON:
{ "userId": ${userId}, "operator": "${userName}", "permission": "${perm1}" }JMeter 在执行时会把${userId}、${userName}、${perm1}替换成登录响应里提取出来的实际值。只要提取规则没问题,这个请求在任何环境、任何用户下都能正常跑。
要注意一个小坑:如果变量值本身可能包含双引号、反斜杠这类 JSON 特殊字符,直接拼进 JSON 字符串里会把请求体弄坏。我们这个例子里权限值是order:view这种安全格式,所以没问题;如果遇到用户昵称带特殊字符,建议用 JSR223 生成 JSON 字符串,而不是纯文本拼接。
4.3 用 Debug Sampler 验证所有变量都取对了
调试关联脚本时,Debug Sampler 是我的标准操作。在 Thread Group 下加一个 Debug Sampler,勾选 JMeter variables 字段,运行一次后在查看结果树里找到 Debug Sampler 的响应数据,就能看到当前线程所有 JMeter 变量及其值。
比如验证时会看到这样一段:
accessToken=8f3a9c21-6b7e-4d2a-9f10-2c8d4e6a7b10 userId=1024 userName=tester01 role=operator perm1=order:view perm2=order:create perm3=order:batch如果变量没出现在列表里,说明提取失败;如果变量值是NOT_FOUND之类的缺省值,说明正则没匹配上,需要回去检查正则和响应原文。Debug Sampler 会额外产生一条采样记录,还有一定开销,所以只在调试阶段用,压测前记得删掉或禁用。
5. 实战里最常见的几个坑,按排查链路一个个排
5.1 贪婪匹配:.*把不该吃的都吃了
这是正则提取多个内容时最经典的问题。假设响应是:
<name>apple</name><price>10</price><name>banana</name><price>20</price>你想取第一个<name>里的内容,正则写了<name>(.*)</name>。结果捕获组会一直吃到最后一个</name>,取出来是apple</name><price>10</price><name>banana,明显不是你要的。
原因很简单:.*是贪婪的,会尽可能多地匹配。解决办法是用非贪婪写法.*?,也就是"遇到第一个结束标记就停"。正则改成<name>(.*?)</name>后,取到的就是apple。
在接口测试里,我习惯用([^"]+)而不是.*?来提取引号内的内容,因为[^"]+天然不会跨过引号,比非贪婪更不容易出错。
5.2 特殊字符转义:方括号、引号、换行
提取 JSON 数组、HTML 属性时,特殊字符的转义最容易出问题。
JSON 数组的中括号在正则里必须转义,比如要匹配["a","b"],起码要写成:
\["([^"]+)"否则方括号会被当成字符类。反斜杠也要小心:在 JMeter 界面的正则字段里,要匹配一个普通的反斜杠字符本身,通常需要写两个反斜杠\\;但像\d、\w这种预定义字符类,直接写一个反斜杠就行。
另一个高频坑是换行。JMeter 默认情况下.是不匹配换行的。如果接口返回的是一个格式化过的多行 JSON,正则里用.*很可能匹配不到跨行的内容。这时候可以在正则最前面加一个内联标记:
(?s)"data":\{"name":"([^"]+)"(?s)让.可以匹配包括换行在内的所有字符。很多脚本"明明单行能匹配,一到多行就取不到值",就是这个原因。
5.3 匹配数字 0 和 -1 搞反
前面已经强调过,match number 填 0 是随机取一个匹配项,不是全量提取。压测时如果这个变量要用来做数据区分,填 0 会导致每个线程拿到不同的数据,脚本行为不可预期,数据对不上都查不出原因。
填 -1 才是全量提取。用 -1 以后注意引用方式是${变量_1}、${变量_2},并可以结合${变量_matchNr}获取数量。如果你发现自己引用${id}取不到值,先想想是不是匹配数字填了 -1。
5.4 变量旧值残留:第二次取不到,用的居然是上一次的值
这个坑很隐蔽。JMeter 变量在一个线程内是长期存在的。第一次循环提取到了 token 并更新了变量;第二次循环如果正则没匹配上,而缺省值又留空,变量不会自动清空,线程内保存的还是第一次的 token。脚本不会报错,接口可能也返回 200,但数据已经不对了。
所以缺省值一定不要留空。我习惯统一填NOT_FOUND,然后在断言或者后续请求里能看到这个标记。更进一步,可以在关键提取器后面加一个简单的 JSR223 Assertion,判断变量值是不是NOT_FOUND,是就直接断言失败,让脚本快速提醒你。
5.5 一条完整的排查链路
如果你发现关联值取不到,别瞎改,按这个顺序一步步排除:
- 先用查看结果树确认响应原始文本确实包含目标内容,注意有些接口返回的 JSON 会被浏览器渲染成对象,实际网络报文长得不太一样。
- 把响应原文复制到本地正则工具或者写个临时脚本,先验证正则本身对不对。
- 检查提取器是否挂在了正确的取样器下面,有没有因为重定向导致目标内容在子请求里。
- 检查括号、模板、匹配数字是否对应。模板写 $1$ 但正则里没括号,必空。
- 加 Debug Sampler 看变量名和下标是否正确,重点看有没有
_1、_g1、_matchNr这些后缀。 - 最后检查缺省值,确认是不是旧值残留导致看起来"取到了但其实是上一次的"。
这套链路我用了很久,基本上能解决九成以上的关联提取问题。
6. 别只会正则:JSON Extractor 和 JSR223 什么时候更好用
6.1 标准 JSON 响应,优先上 JSON Extractor
如果你的接口返回的是标准 JSON,JsonPath 通常是比正则舒服得多的选择。JMeter 自带 JSON Extractor,添加路径是:右键取样器 -> 添加 -> 后置处理器 -> JSON Extractor。
它最大的优势是支持一个控件里配置多行规则。比如登录响应的 example,可以直接配三行:
| Names of created variables | JSON Path expressions | Match Numbers | Default Values |
|---|---|---|---|
| accessToken | $.data.accessToken | 1 | NOT_FOUND |
| userId | $.data.userId | 1 | NOT_FOUND |
| role | $.data.role | 1 | NOT_FOUND |
不用再纠结引号转义、配对括号和字段顺序,路径写对了就一定能取到。想取 permissions 里的全部值,可以写:
$.data.permissions[*]配合 Match Numbers 填 -1,同样会生成permissions_1、permissions_2,配合 ForEach 也一样好用。
6.2 复杂取值逻辑,用 JSR223 + Groovy 一锅端
有些响应结构嵌套很深,或者要做的不是"取一个值",而是"按条件筛选几个值、拼装成新结构",这种情况正则和 JSONPath 都会写得很别扭。JSR223 后处理器配 Groovy 就很灵活。
典型的脚本长这样:
import groovy.json.JsonSlurper def response = prev.getResponseDataAsString() def json = new JsonSlurper().parseText(response) vars.put("accessToken", json.data.accessToken) vars.put("userId", json.data.userId.toString()) vars.put("permCount", json.data.permissions.size().toString())这里prev是取样器结果对象,vars是 JMeter 变量池。脚本里想循环、想判断、想格式化,全都自由。
有一个性能提示:在压测场景里,JSR223 脚本每次执行都有成本,脚本里能不开源库就别乱开,能少写循环就少写。JMeter 的 JSR223 配置窗口里如果有 Cache compiled script 这样的选项,务必勾上,让脚本编译结果被缓存复用。
6.3 我的选型建议
我现在写 JMeter 脚本时有个习惯:先看返回格式,再决定工具,而不是拿到标题就默认用正则。返回是标准 JSON,优先 JSON Extractor;响应是 HTML、纯文本、或者老系统的自定义报文,才考虑正则提取器;需要做判断、循环、拼接这类逻辑,直接上 JSR223。
正则表达式提取器的优势在于"纯文本世界里它没有替代品",但代价是表达式本身可读性差、容易踩转义和贪婪的坑。在团队协作的项目里,脚本是给别人看的,优先选择别人一眼能看懂的方案,比炫技重要得多。
回到"匹配多个内容"这件事,我的体会是先分清楚到底属于哪种需求:是"一次取多个字段",还是"取一串同类型数据"。这两个方向对匹配数字和模板参数的配置影响非常大。方向定对了,工具选顺手了,正则提取器在 JMeter 关联里就再也不是麻烦,只是工具箱里一个普通工具。