提交 c632f219 authored 作者: 陈泽健's avatar 陈泽健

feat(keyword-matcher): jieba分词优化 + 评分归一化 (Phase 1&2)

Phase 1 - 关键词提取优化:
- 引入jieba分词替代暴力替换,解决语义丢失问题
- 精简修饰词列表,添加项目词汇白名单
- extract_value支持中文值提取

Phase 2 - 评分体系归一化:
- 精确匹配(1.0)高于包含匹配(0.3)
- click对INPUT减分更激进(0.1),fill优先匹配INPUT
- 归一化评分到[0,1]区间

新增测试脚本验证改动
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 3f4fffa2
......@@ -6,15 +6,25 @@
作者:czj
创建日期:2026-08-05
最后修改:2026-08-05
最后修改:2026-08-06
Phase 1 更新:
- 引入jieba分词替代暴力替换
- 精简修饰词列表
- extract_value正则增强支持中文值
"""
import logging
import re
import jieba
import jieba.posseg as pseg
from typing import List, Tuple, Optional, Dict, Any
logger = logging.getLogger(__name__)
# jieba分词初始化(首次调用时自动加载,后续缓存)
_jieba_initialized = False
# ==================== 动作词表 ====================
......@@ -43,96 +53,150 @@ ELEMENT_WORDS = [
# ==================== 关键词提取 ====================
def _init_jieba():
"""初始化jieba分词(懒加载)"""
global _jieba_initialized
if not _jieba_initialized:
# 添加项目特定词汇到词典(提高分词准确度)
# 使用 suggest_freq 确保词汇被正确识别
project_words = [
# 菜单名称
'功能中心', '系统设置', '信息发布', '通知公告',
'会议预约', '会议列表', '新建会议', '会议室管理',
'壁纸推送', '脚本命令', '集控控制',
'工单列表', '我的工单', '告警工单',
'资产信息', '资产设备', '资产管理',
'运维设备', '运维巡检', '会议运维',
'会务统筹', '会务工单', '会务管理',
'信息窗管理', '消息通知', '信息管理',
'数据统计', '数据分析', '管理看板',
# 按钮名称
'新建', '确定', '取消', '保存', '删除', '编辑', '查询', '重置',
'展开', '收起', '上传', '下载', '导出', '导入',
# 其他常见词汇
'用户名', '密码', '账号', '备注', '会议名称',
]
for word in project_words:
jieba.add_word(word, freq=1000) # 高频确保被识别
_jieba_initialized = True
logger.debug(f"jieba分词初始化完成,已添加 {len(project_words)} 个项目词汇")
def extract_keywords(description: str) -> List[str]:
"""
从自然语言描述中提取关键词
从自然语言描述中提取关键词(jieba分词版本)
策略:
1. 去除特殊符号(【】《》等
2. 去除动作词和元素类型
3. 提取剩余的关键词(名词、修饰词等)
4. 按 2-gram 分词获取更精确的关键词
1. 提取【】内完整词组(最高优先级
2. jieba分词,按词性过滤保留名词、动
3. 去除动作词和元素类型词
4. 过滤单字和无意义词汇
Args:
description (str): 步骤描述,如 "输入用户名 admin@xty"
Returns:
List[str]: 关键词列表,如 ["用户名", "admin", "admin@xty"]
List[str]: 关键词列表,如 ["用户名", "admin@xty"]
Example:
>>> extract_keywords("点击登录按钮")
["登录"]
>>> extract_keywords("输入用户名 admin@xty")
["用户名", "admin", "admin@xty"]
>>> extract_keywords("点击【功能中心】展开")
["功能中心"]
>>> extract_keywords("点击【新建会议】按钮")
["新建会议"]
>>> extract_keywords("点击展开")
["展开"]
>>> extract_keywords("输入会议名称:自动化新建会议")
["会议名称", "自动化", "新建会议"]
"""
# 去除特殊符号
cleaned = description
special_chars = ['【', '】', '《', '》', '「', '」', '『', '』', '〔', '〕', '〈', '〉']
for ch in special_chars:
cleaned = cleaned.replace(ch, '')
# 去除动作词
for word in sorted(ACTION_WORDS + ELEMENT_WORDS, key=len, reverse=True):
cleaned = cleaned.replace(word, "")
cleaned = cleaned.strip()
# 去除修饰词(这些词不是关键词,但会影响匹配)
# ⚠️ 注意:'添加'、'新增'、'完成'、'确定'、'取消' 可能是按钮名称,不应移除
modifier_words = ['展开', '分类', '操作', '查看', '是否', '正确',
'条目', '数据', '验证', '成功', '页面', '系统']
for word in modifier_words:
cleaned = cleaned.replace(word, "")
cleaned = cleaned.strip()
# ⚠️ 关键修复:先提取【】内的核心词(优先级最高)
core_keywords = []
bracket_match = re.findall(r'【([^】]+)】', description)
if bracket_match:
for match in bracket_match:
core_keywords.append(match)
# 添加 2-gram
for i in range(len(match) - 1):
sub = match[i:i+2]
if sub not in ACTION_WORDS and sub not in ELEMENT_WORDS:
core_keywords.append(sub)
keywords = []
# 添加清理后的关键词
if cleaned:
keywords.append(cleaned)
# 2-gram 分词(提取连续两个字的关键词)
if len(cleaned) >= 2:
for i in range(len(cleaned) - 1):
sub = cleaned[i:i+2]
if len(sub) == 2 and sub not in ACTION_WORDS and sub not in ELEMENT_WORDS:
keywords.append(sub)
# 单字关键词(去掉停用词)
stopwords = {'的', '在', '是', '和', '有', '等', '中', '为', '了', '与', '或', '、', ',', '。', ' '}
for ch in cleaned:
if ch.strip() and ch not in stopwords:
keywords.append(ch)
# ⚠️ 【】内的核心词放在最前面(优先级最高)
_init_jieba()
# 项目特定词汇集合(这些词汇应该被保留)
project_vocab = {
# 菜单名称
'功能中心', '系统设置', '信息发布', '通知公告',
'会议预约', '会议列表', '新建会议', '会议室管理',
'壁纸推送', '脚本命令', '集控控制',
'工单列表', '我的工单', '告警工单',
'资产信息', '资产设备', '资产管理',
'运维设备', '运维巡检', '会议运维',
'会务统筹', '会务工单', '会务管理',
'信息窗管理', '消息通知', '信息管理',
'数据统计', '数据分析', '管理看板',
# 常见按钮名称
'新建', '确定', '取消', '保存', '删除', '编辑', '查询', '重置',
'展开', '收起', '上传', '下载', '导出', '导入',
# 其他
'用户名', '密码', '账号', '备注', '会议名称',
}
# 1. 提取【】内完整词组(最高优先级)
bracket_keywords = re.findall(r'【([^】]+)】', description)
# 2. jieba分词
words = pseg.cut(description)
# 3. 按词性过滤:保留名词(n/nr/ns/nz)、动词
# 词性标记说明:
# n: 普通名词, nr: 人名, ns: 地名, nz: 其他专名
# vn: 名动词, v: 动词, eng: 英文
valid_pos_tags = {'n', 'nr', 'ns', 'nz', 'vn', 'v', 'eng', 'l'} # l: 成语
# 单字词汇白名单(这些单字应该保留)
single_char_whitelist = {
'确定', '取消', '保存', '删除', '编辑', '查询', '新建',
'展开', '收起', '上传', '下载', '导出', '导入',
}
filtered_words = []
for word, flag in words:
word = word.strip()
if not word:
continue
# 项目特定词汇,直接保留
if word in project_vocab:
filtered_words.append(word)
continue
# 英文(可能是ID、变量名等)
if flag == 'eng':
if len(word) >= 2 and word not in ACTION_WORDS and word not in ELEMENT_WORDS:
filtered_words.append(word)
continue
# 中文词汇过滤
if flag in valid_pos_tags:
# 长度>=2 或 在单字白名单中
if len(word) >= 2:
# 不在动作词/元素词表中
if word not in ACTION_WORDS and word not in ELEMENT_WORDS:
# 不是纯数字
if not word.isdigit():
filtered_words.append(word)
elif word in single_char_whitelist:
filtered_words.append(word)
# 4. 精简修饰词移除(仅保留真正的无意义修饰词)
minimal_modifiers = {'的', '了', '着', '过', '一下', '是否', '是否成功', '正确'}
filtered_words = [w for w in filtered_words if w not in minimal_modifiers]
# 5. 合并结果:【】内词组优先
result = []
seen = set()
# 先添加核心关键词
for kw in core_keywords:
# 先添加【】内的核心关键词
for kw in bracket_keywords:
if kw not in seen:
seen.add(kw)
result.append(kw)
# 再添加其他关键词
for kw in keywords:
# 再添加jieba分词结果
for kw in filtered_words:
if kw not in seen:
seen.add(kw)
result.append(kw)
logger.debug(f"关键词提取: '{description}' -> {result}")
return result
......@@ -144,6 +208,7 @@ def extract_value_from_description(description: str) -> Optional[str]:
例如:"输入账号:admin@xty" -> "admin@xty"
例如:"输入密码:Ubains@13579" -> "Ubains@13579"
例如:"输入参考价值:100" -> "100"
例如:"输入备注:这是一个测试备注" -> "这是一个测试备注"
Args:
description (str): 步骤描述
......@@ -151,13 +216,17 @@ def extract_value_from_description(description: str) -> Optional[str]:
Returns:
Optional[str]: 提取的值,未找到返回 None
"""
# 匹配模式1:动作 + 目标 + 冒号 + 值(中文冒号)
# 例如:"输入账号:admin@xty" -> "admin@xty"
match = re.search(r'(?:输入|填写|填入|键入|写入)\s*[^::]*[::]\s*([\w@.\-]+)\s*$', description)
# 匹配模式1:动作 + 目标 + 冒号 + 值(支持中文值、空格、特殊字符)
# 改进:将 [\w@.\-]+ 改为 .+? 以支持中文值
# 例如:"输入备注:这是一个测试备注" -> "这是一个测试备注"
match = re.search(r'(?:输入|填写|填入|键入|写入)\s*[^::]*[::]\s*(.+?)\s*$', description)
if match:
return match.group(1)
value = match.group(1).strip()
# 过滤掉明显不是值的内容(如"按钮"、"输入框"等元素类型词)
if value and not any(word in value for word in ELEMENT_WORDS):
return value
# 匹配模式2:动作 + 目标 + 空格 + 值
# 匹配模式2:动作 + 目标 + 空格 + 值(英文字母数字)
# 例如:"输入用户名 admin@xty" -> "admin@xty"
match = re.search(r'(?:输入|填写|填入|键入|写入)\s*.*?\s+([\w@.\-]+)\s*$', description)
if match:
......@@ -239,6 +308,98 @@ def is_verification_step(description: str) -> bool:
# ==================== 元素匹配 ====================
# ==================== 评分体系常量(Phase 2)====================
# 精确匹配 vs 包含匹配
EXACT_MATCH_SCORE = 1.0 # 精确匹配(完全相等)
CONTAINS_MATCH_SCORE = 0.3 # 包含匹配(子串包含)
# 维度权重(总和为1.0)
DIMENSION_WEIGHTS = {
'data-testid': 0.20,
'id': 0.18,
'name': 0.12,
'placeholder': 0.15,
'aria-label': 0.12,
'text': 0.15,
'action_type': 0.08, # 动作类型适配
}
def _calculate_match_score(keyword: str, target: str) -> float:
"""
计算关键词与目标文本的匹配分数
Args:
keyword: 关键词
target: 目标文本(如ID、placeholder、text等)
Returns:
float: 匹配分数 (0.0 ~ 1.0)
"""
if not keyword or not target:
return 0.0
keyword_lower = keyword.lower()
target_lower = target.lower()
# 精确匹配(完全相等)
if keyword_lower == target_lower:
return EXACT_MATCH_SCORE
# 包含匹配(关键词是目标的子串)
if keyword_lower in target_lower:
# 根据匹配长度比例调整分数
ratio = len(keyword) / len(target)
return CONTAINS_MATCH_SCORE * min(1.0, ratio * 2) # 长度比例越高,分数越高
return 0.0
def _apply_action_type_adjustment(score: float, tag: str, action: str, keywords: List[str]) -> float:
"""
根据动作类型调整分数
Args:
score: 原始分数
tag: 元素标签名
action: 动作类型
keywords: 关键词列表
Returns:
float: 调整后的分数
"""
tag = tag.upper()
if action == 'click':
# click动作对INPUT/TEXTAREA大幅减分
if tag in ['INPUT', 'TEXTAREA']:
input_keywords = ['输入框', '输入', '框', '填写', 'input']
if not any(kw in input_keywords for kw in keywords):
score *= 0.1 # 更激进(原0.3)
# click动作对BUTTON/A/DIV/SPAN/LI/I加分
elif tag in ['BUTTON', 'A', 'DIV', 'SPAN', 'LI', 'I']:
score += 0.15
elif action == 'fill':
# fill动作对INPUT/TEXTAREA加分
if tag in ['INPUT', 'TEXTAREA']:
score += 0.15
# fill动作对非输入元素减分
elif tag in ['BUTTON', 'A', 'DIV', 'SPAN']:
fill_keywords = ['按钮', '点击', 'click']
if not any(kw in fill_keywords for kw in keywords):
score *= 0.3
elif action == 'select':
if tag == 'SELECT':
score += 0.15
# 确保分数在[0, 1.5]范围内(允许略微超过1.0以体现动作适配优势)
return min(max(score, 0.0), 1.5)
def match_element_by_keywords(
page,
keywords: List[str],
......@@ -247,12 +408,13 @@ def match_element_by_keywords(
max_candidates: int = 10
) -> Tuple[Optional[Any], List[Dict[str, Any]]]:
"""
通过关键词在页面元素中直接匹配
通过关键词在页面元素中直接匹配(Phase 2 评分归一化版本)
三级匹配策略:
1. 精确匹配:ID, name, data-testid
2. 包含匹配:placeholder, aria-label, text
3. 回退策略:根据动作类型推断
评分策略:
1. 每个维度计算精确匹配/包含匹配分数
2. 按维度权重加权求和
3. 归一化到[0, 1]区间
4. 动作类型适配调整
Args:
page: Playwright Page 对象
......@@ -288,7 +450,7 @@ def match_element_by_keywords(
elements = page.locator(base_selector).all()
total_elements = len(elements)
# 🔧 新增:在所有 iframe 中查找元素
# 在所有 iframe 中查找元素
for frame in page.frames:
if frame == page.main_frame:
continue
......@@ -321,64 +483,102 @@ def match_element_by_keywords(
data_testid = el.get_attribute('data-testid') or el.get_attribute('data-test-id') or ''
class_name = el.get_attribute('class') or ''
score = 0.0
selectors = []
# ==================== Phase 2: 归一化评分 ====================
score_details = {} # 各维度得分详情
selectors = [] # 选择器列表
# 1. ID 匹配(最高优先级)
if el_id:
# 对每个关键词计算各维度得分
for kw in keywords:
if kw.lower() in el_id.lower():
score += 0.4
# 1. data-testid 匹配(最高优先级)
if data_testid:
match_score = _calculate_match_score(kw, data_testid)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['data-testid']
score_details['data-testid'] = max(score_details.get('data-testid', 0), dim_score)
if match_score == EXACT_MATCH_SCORE:
selectors.append({
'type': 'css',
'value': f'#{el_id}',
'value': f'[data-testid="{data_testid}"]',
'confidence': 0.95,
'priority': 1
'priority': 1,
'match_type': 'exact'
})
# 2. data-testid 匹配(高优先级)
if data_testid:
for kw in keywords:
if kw.lower() in data_testid.lower():
score += 0.35
# 2. ID 匹配
if el_id:
match_score = _calculate_match_score(kw, el_id)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['id']
score_details['id'] = max(score_details.get('id', 0), dim_score)
if match_score == EXACT_MATCH_SCORE:
selectors.append({
'type': 'css',
'value': f'[data-testid="{data_testid}"]',
'value': f'#{el_id}',
'confidence': 0.90,
'priority': 2
'priority': 2,
'match_type': 'exact'
})
elif not any(s['value'] == f'#{el_id}' for s in selectors):
selectors.append({
'type': 'css',
'value': f'#{el_id}',
'confidence': 0.60,
'priority': 4,
'match_type': 'contains'
})
# 3. name 匹配
if name:
match_score = _calculate_match_score(kw, name)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['name']
score_details['name'] = max(score_details.get('name', 0), dim_score)
if match_score == EXACT_MATCH_SCORE:
selectors.append({
'type': 'css',
'value': f'[name="{name}"]',
'confidence': 0.85,
'priority': 5,
'match_type': 'exact'
})
# 3. placeholder 匹配(输入框)
# 4. placeholder 匹配(输入框)
if placeholder and tag == 'INPUT':
for kw in keywords:
if kw.lower() in placeholder.lower():
score += 0.3
match_score = _calculate_match_score(kw, placeholder)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['placeholder']
score_details['placeholder'] = max(score_details.get('placeholder', 0), dim_score)
if match_score >= CONTAINS_MATCH_SCORE:
selectors.append({
'type': 'css',
'value': f'input[placeholder*="{placeholder}"]',
'confidence': 0.85,
'priority': 3
'confidence': 0.80 if match_score == EXACT_MATCH_SCORE else 0.60,
'priority': 3,
'match_type': 'exact' if match_score == EXACT_MATCH_SCORE else 'contains'
})
# 4. aria-label 匹配
# 5. aria-label 匹配
if aria_label:
for kw in keywords:
if kw.lower() in aria_label.lower():
score += 0.3
match_score = _calculate_match_score(kw, aria_label)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['aria-label']
score_details['aria-label'] = max(score_details.get('aria-label', 0), dim_score)
if match_score >= CONTAINS_MATCH_SCORE:
selectors.append({
'type': 'css',
'value': f'[aria-label*="{aria_label}"]',
'confidence': 0.80,
'priority': 4
'confidence': 0.80 if match_score == EXACT_MATCH_SCORE else 0.55,
'priority': 4,
'match_type': 'exact' if match_score == EXACT_MATCH_SCORE else 'contains'
})
# 5. 文本匹配(按钮、链接、可点击的 div/span)
# ⚠️ 扩展:允许 div、span、i、label、li 等元素进行文本匹配
# 6. 文本匹配(按钮、链接、可点击的 div/span)
text_matchable_tags = ['BUTTON', 'A', 'DIV', 'SPAN', 'I', 'LABEL', 'LI']
if text and tag in text_matchable_tags:
for kw in keywords:
if kw.lower() in text.lower():
score += 0.25
match_score = _calculate_match_score(kw, text)
if match_score > 0:
dim_score = match_score * DIMENSION_WEIGHTS['text']
score_details['text'] = max(score_details.get('text', 0), dim_score)
# 根据元素类型调整选择器
if tag in ['BUTTON', 'A']:
selector_value = f'{tag.lower()}:has-text("{text}")'
......@@ -387,68 +587,51 @@ def match_element_by_keywords(
selectors.append({
'type': 'css',
'value': selector_value,
'confidence': 0.75,
'priority': 5
'confidence': 0.85 if match_score == EXACT_MATCH_SCORE else 0.65,
'priority': 5,
'match_type': 'exact' if match_score == EXACT_MATCH_SCORE else 'contains'
})
# 6. name 匹配
if name:
for kw in keywords:
if kw.lower() in name.lower():
score += 0.2
selectors.append({
'type': 'css',
'value': f'[name="{name}"]',
'confidence': 0.70,
'priority': 6
})
# 如果没有任何匹配,跳过该元素
if not score_details:
continue
# 7. 动作类型加分/减分
if action == 'fill' and tag in ['INPUT', 'TEXTAREA']:
score += 0.20
# ⚠️ click 操作额外加分标签扩展
elif action == 'click' and tag in ['BUTTON', 'A', 'I', 'DIV', 'SPAN', 'LI']:
score += 0.25
elif action == 'select' and tag == 'SELECT':
score += 0.20
# ⚠️ 关键修复:点击操作不应该匹配输入框(除非关键词明确包含输入相关)
if action == 'click' and tag in ['INPUT', 'TEXTAREA']:
# 只有关键词明确包含输入相关词汇时才保留输入框
input_keywords = ['输入框', '输入', '框', '填写', 'input']
if not any(kw in input_keywords for kw in keywords):
score *= 0.3 # 大幅降低分数,让按钮优先
# 计算归一化分数
matched_weights = sum(DIMENSION_WEIGHTS[k] for k in score_details.keys() if k != 'action_type')
if matched_weights > 0:
base_score = sum(score_details.values()) / matched_weights
else:
base_score = 0.0
# 填充操作不应该匹配按钮(除非关键词明确包含按钮相关)
if action == 'fill' and tag in ['BUTTON', 'A']:
fill_keywords = ['按钮', '点击', 'click']
if not any(kw in fill_keywords for kw in keywords):
score *= 0.3 # 大幅降低分数
# 动作类型适配
final_score = _apply_action_type_adjustment(base_score, tag, action, keywords)
# 8. 类型匹配(password, email, tel 等)
if el_type:
if action == 'fill':
if el_type == 'password' and any(kw in '密码' for kw in keywords):
score += 0.2
selectors.append({
'type': 'css',
'value': 'input[type="password"]',
'confidence': 0.85,
'priority': 3
})
# 记录动作类型调整分数
if final_score != base_score:
score_details['action_type'] = final_score - base_score
if score > 0.15 and selectors:
# 只保留有效候选
if final_score > 0.1 and selectors:
# 按优先级排序选择器
selectors.sort(key=lambda x: x['priority'])
selectors.sort(key=lambda x: (x['priority'], -x['confidence']))
# 去重选择器
seen_selectors = set()
unique_selectors = []
for sel in selectors:
if sel['value'] not in seen_selectors:
seen_selectors.add(sel['value'])
unique_selectors.append(sel)
candidates.append({
'element': el,
'score': score,
'selectors': selectors,
'score': final_score,
'score_details': score_details,
'selectors': unique_selectors,
'info': {
'tag': tag,
'type': el_type,
'placeholder': placeholder,
'text': text,
'text': text[:50] if text else '', # 限制长度
'id': el_id
}
})
......@@ -482,7 +665,7 @@ def match_element_by_keywords(
# 日志
best = limited_candidates[0]
logger.info(f"最佳匹配元素: {best['info']}, 分数: {best['score']:.2f}, 候选数: {len(limited_candidates)}")
logger.info(f"最佳匹配元素: {best['info']}, 分数: {best['score']:.3f}, 详情: {best['score_details']}, 候选数: {len(limited_candidates)}")
return elements_list, unique_selectors
......
......@@ -11,3 +11,4 @@ websockets==12.0
requests==2.31.0
paho-mqtt==2.1.0
openpyxl==3.1.2
jieba>=0.42.1
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本:验证 Phase 1 关键词提取优化
测试项:
1. jieba分词替代暴力替换
2. 修饰词精简后不再空关键词
3. 中文值提取
"""
import sys
import os
import io
# 设置标准输出编码为UTF-8(解决Windows GBK编码问题)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.keyword_matcher import extract_keywords, extract_value_from_description
def test_extract_keywords():
"""测试关键词提取"""
print("=" * 60)
print("测试关键词提取(jieba分词版本)")
print("=" * 60)
test_cases = [
# (输入, 期望包含的关键词)
("点击【新建会议】按钮", ["新建会议"]),
("点击展开", ["展开"]), # 之前返回空列表
("点击系统设置", ["系统设置"]), # 之前返回空列表
("输入会议名称:自动化新建会议", ["会议名称", "自动化", "新建会议"]),
("点击信息发布", ["信息发布"]),
("点击功能中心图标", ["功能中心"]),
("输入备注:这是一个测试备注", ["备注"]),
("点击确定按钮", ["确定"]),
("点击取消按钮", ["取消"]),
("输入用户名:admin@xty", ["用户名"]),
]
passed = 0
failed = 0
for desc, expected_keywords in test_cases:
result = extract_keywords(desc)
# 检查期望的关键词是否在结果中
missing = [kw for kw in expected_keywords if kw not in result]
if missing:
print(f"[FAIL] '{desc}'")
print(f" 期望包含: {expected_keywords}")
print(f" 实际结果: {result}")
print(f" 缺失: {missing}")
failed += 1
else:
print(f"[PASS] '{desc}' -> {result}")
passed += 1
print()
print(f"结果: 通过 {passed}/{passed + failed}")
return failed == 0
def test_extract_value():
"""测试值提取"""
print()
print("=" * 60)
print("测试值提取(支持中文值)")
print("=" * 60)
test_cases = [
# (输入, 期望值)
("输入用户名:admin@xty", "admin@xty"),
("输入账号:admin@xty", "admin@xty"),
("输入密码:Ubains@13579", "Ubains@13579"),
("输入参考价值:100", "100"),
("输入备注:这是一个测试备注", "这是一个测试备注"),
("输入会议名称:自动化测试会议", "自动化测试会议"),
("输入用户名 admin@xty", "admin@xty"),
("输入值'test_value'", "test_value"),
("输入值\"test_value\"", "test_value"),
]
passed = 0
failed = 0
for desc, expected_value in test_cases:
result = extract_value_from_description(desc)
if result == expected_value:
print(f"[PASS] '{desc}' -> '{result}'")
passed += 1
else:
print(f"[FAIL] '{desc}'")
print(f" 期望: '{expected_value}'")
print(f" 实际: '{result}'")
failed += 1
print()
print(f"结果: 通过 {passed}/{passed + failed}")
return failed == 0
def main():
"""运行所有测试"""
print("\n" + "=" * 60)
print("Phase 1 关键词提取优化 - 验证测试")
print("=" * 60)
all_passed = True
# 测试关键词提取
if not test_extract_keywords():
all_passed = False
# 测试值提取
if not test_extract_value():
all_passed = False
print()
print("=" * 60)
if all_passed:
print("[SUCCESS] 所有测试通过!Phase 1 验证成功")
else:
print("[WARNING] 部分测试失败,请检查")
print("=" * 60)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本:验证 Phase 2 评分体系归一化
测试项:
1. 精确匹配 vs 包含匹配 - 精确匹配应排序更高
2. 动作类型适配 - click优先匹配BUTTON,fill优先匹配INPUT
"""
import sys
import os
import io
# 设置标准输出编码为UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.keyword_matcher import (
_calculate_match_score,
_apply_action_type_adjustment,
EXACT_MATCH_SCORE,
CONTAINS_MATCH_SCORE,
)
def test_match_score():
"""测试匹配分数计算"""
print("=" * 60)
print("测试匹配分数计算")
print("=" * 60)
test_cases = [
# (关键词, 目标文本, 期望精确匹配)
("确定", "确定", True),
("确定", "确定按钮", False),
("新建", "新建会议", False),
("新建会议", "新建会议", True),
("admin", "admin@xty", False),
("admin@xty", "admin@xty", True),
]
passed = 0
failed = 0
for keyword, target, expect_exact in test_cases:
score = _calculate_match_score(keyword, target)
is_exact = (score == EXACT_MATCH_SCORE)
if is_exact == expect_exact:
match_type = "精确" if is_exact else "包含"
print(f"[PASS] '{keyword}' vs '{target}' -> {match_type}匹配 (score={score:.2f})")
passed += 1
else:
print(f"[FAIL] '{keyword}' vs '{target}'")
print(f" 期望: {'精确' if expect_exact else '包含'}匹配")
print(f" 实际: score={score:.2f}")
failed += 1
print()
print(f"结果: 通过 {passed}/{passed + failed}")
return failed == 0
def test_action_type_adjustment():
"""测试动作类型适配"""
print()
print("=" * 60)
print("测试动作类型适配")
print("=" * 60)
test_cases = [
# (原分数, 标签, 动作, 期望结果描述)
(0.5, "BUTTON", "click", "应该加分"),
(0.5, "INPUT", "click", "应该减分"),
(0.5, "INPUT", "fill", "应该加分"),
(0.5, "BUTTON", "fill", "应该减分"),
(0.5, "SELECT", "select", "应该加分"),
]
passed = 0
failed = 0
for base_score, tag, action, expect_desc in test_cases:
adjusted = _apply_action_type_adjustment(base_score, tag, action, [])
if expect_desc == "应该加分" and adjusted > base_score:
print(f"[PASS] {tag}+{action}: {base_score:.2f} -> {adjusted:.2f} ({expect_desc})")
passed += 1
elif expect_desc == "应该减分" and adjusted < base_score:
print(f"[PASS] {tag}+{action}: {base_score:.2f} -> {adjusted:.2f} ({expect_desc})")
passed += 1
else:
print(f"[FAIL] {tag}+{action}: {base_score:.2f} -> {adjusted:.2f} ({expect_desc})")
failed += 1
print()
print(f"结果: 通过 {passed}/{passed + failed}")
return failed == 0
def test_exact_vs_contains_ranking():
"""测试精确匹配排名高于包含匹配"""
print()
print("=" * 60)
print("测试精确匹配排名高于包含匹配")
print("=" * 60)
# 模拟场景:关键词"确定"
# 元素A: text="确定" (精确匹配)
# 元素B: text="确定按钮" (包含匹配)
# 期望:A的分数高于B
score_exact = _calculate_match_score("确定", "确定") * 0.15 # text维度权重
score_contains = _calculate_match_score("确定", "确定按钮") * 0.15
if score_exact > score_contains:
print(f"[PASS] 精确匹配({score_exact:.4f}) > 包含匹配({score_contains:.4f})")
passed = True
else:
print(f"[FAIL] 精确匹配({score_exact:.4f}) <= 包含匹配({score_contains:.4f})")
passed = False
# 模拟场景:关键词"新建"
# 元素A: text="新建" (精确匹配)
# 元素B: text="新建会议" (包含匹配)
score_exact2 = _calculate_match_score("新建", "新建") * 0.15
score_contains2 = _calculate_match_score("新建", "新建会议") * 0.15
if score_exact2 > score_contains2:
print(f"[PASS] 精确匹配({score_exact2:.4f}) > 包含匹配({score_contains2:.4f})")
passed2 = True
else:
print(f"[FAIL] 精确匹配({score_exact2:.4f}) <= 包含匹配({score_contains2:.4f})")
passed2 = False
return passed and passed2
def main():
"""运行所有测试"""
print("\n" + "=" * 60)
print("Phase 2 评分体系归一化 - 验证测试")
print("=" * 60)
all_passed = True
if not test_match_score():
all_passed = False
if not test_action_type_adjustment():
all_passed = False
if not test_exact_vs_contains_ranking():
all_passed = False
print()
print("=" * 60)
if all_passed:
print("[SUCCESS] 所有测试通过!Phase 2 验证成功")
else:
print("[WARNING] 部分测试失败,请检查")
print("=" * 60)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论