import xml.etree.ElementTree as ET
from xml.dom import minidom
import json
from urllib.parse import urlparse
def safe_load_har(file_path):
"""安全加载 HAR 文件,支持非法字符和大文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError:
print("⚠️ 文件不是合法的 JSON,尝试忽略非法字符...")
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
return json.loads(content)
def parse_har(file_path):
har_data = safe_load_har(file_path)
entries = har_data.get('log', {}).get('entries', [])
static_extensions = ('.js', '.css', '.jpg', '.jpeg', '.png', '.gif', '.ico', '.svg', '.woff', '.ttf')
requests = []
for entry in entries:
request = entry.get('request', {})
response = entry.get('response', {})
url = request.get('url')
if not url or any(url.lower().endswith(ext) for ext in static_extensions):
continue # 过滤掉静态资源或无效 URL
req_info = {
'method': request.get('method'),
'url': url,
'headers': {h.get('name'): h.get('value') for h in request.get('headers', [])},
'postData': request.get('postData', {}).get('text', '') if request.get('postData') else '',
'mimeType': response.get('content', {}).get('mimeType', '')
}
requests.append(req_info)
return requests
def set_post_data_urlencoded(http_sampler, post_data):
"""
添加 application/x-www-form-urlencoded 类型的 POST 请求体数据
示例:username=admin&password=123456
"""
# 表单数据无需设置 postBodyRaw,JMeter 自动处理
args_element = ET.SubElement(http_sampler, 'elementProp',
name='HTTPSampler.Arguments',
elementType='Arguments',
guiclass='HTTPArgumentsPanel',
testclass='Arguments')
collection = ET.SubElement(args_element, 'collectionProp',
name='Arguments.arguments')
if not post_data.strip():
return
pairs = post_data.split('&')
for pair in pairs:
if '=' in pair:
key, value = pair.split('=', 1)
else:
key, value = pair, ''
add_argument(collection, key, value)
def add_argument(collection, key, value):
element = ET.SubElement(collection, 'elementProp',
name='', elementType='HTTPArgument')
ET.SubElement(element, 'stringProp', name='Argument.name').text = key
ET.SubElement(element, 'stringProp', name='Argument.value').text = value
ET.SubElement(element, 'stringProp', name='Argument.metadata').text = '='
ET.SubElement(element, 'boolProp', name='HTTPArgument.use_equals').text = 'true'
ET.SubElement(element, 'boolProp', name='HTTPArgument.always_encode').text = 'false'
def prettify(elem):
rough_string = ET.tostring(elem, encoding='utf-8', method='xml')
reparsed = minidom.parseString(rough_string)
return reparsed.toprettyxml(indent=" ")
def generate_jmx(requests, output_file='test_plan.jmx'):
root = ET.Element('jmeterTestPlan', version="1.2", properties="5.6.3", jmeter="5.6.3")
hash_tree = ET.SubElement(root, 'hashTree')
# TestPlan
test_plan = ET.SubElement(hash_tree, 'TestPlan',
guiclass="TestPlanGui",
testclass="TestPlan",
testname="Test Plan",
enabled="true")
test_plan_hash_tree = ET.SubElement(hash_tree, 'hashTree')
# ThreadGroup
thread_group = ET.SubElement(test_plan_hash_tree, 'ThreadGroup',
guiclass="ThreadGroupGui",
testclass="ThreadGroup",
testname="Thread Group",
enabled="true")
# Loop Controller
main_controller = ET.SubElement(thread_group, 'elementProp',
name='ThreadGroup.main_controller',
elementType='LoopController',
guiclass='LoopControlPanel',
testclass='LoopController')
ET.SubElement(main_controller, 'stringProp', name='LoopController.loops').text = '1'
ET.SubElement(main_controller, 'boolProp', name='LoopController.continueForever').text = 'false'
ET.SubElement(thread_group, 'stringProp', name='ThreadGroup.num_threads').text = '1'
ET.SubElement(thread_group, 'stringProp', name='ThreadGroup.ramp_time').text = '1'
ET.SubElement(thread_group, 'boolProp', name='ThreadGroup.scheduler').text = 'false'
ET.SubElement(thread_group, 'stringProp', name='ThreadGroup.duration').text = ''
ET.SubElement(thread_group, 'stringProp', name='ThreadGroup.delay').text = ''
thread_group_hash_tree = ET.SubElement(test_plan_hash_tree, 'hashTree')
# CookieManager
cookie_manager = ET.SubElement(thread_group_hash_tree, 'CookieManager',
guiclass="CookiePanel",
testclass="CookieManager",
testname="HTTP Cookie Manager",
enabled="true")
ET.SubElement(cookie_manager, 'boolProp', name='CookieManager.clearEachIteration').text = 'false'
ET.SubElement(cookie_manager, 'stringProp', name='CookieManager.policy').text = 'default'
ET.SubElement(thread_group_hash_tree, 'hashTree')
for i, req in enumerate(requests):
url = req['url']
method = req['method']
request_name = f"{i + 1}-{url}"
parsed_url = urlparse(url)
protocol = parsed_url.scheme
host = parsed_url.netloc
path = parsed_url.path or '/'
# HTTPSamplerProxy
http_sampler = ET.SubElement(thread_group_hash_tree, 'HTTPSamplerProxy',
guiclass="HttpTestSampleGui",
testclass="HTTPSamplerProxy",
testname=request_name,
enabled="true")
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.name').text = request_name
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.protocol').text = protocol
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.domain').text = host
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.port').text = ''
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.path').text = path
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.method').text = method
ET.SubElement(http_sampler, 'boolProp', name='HTTPSampler.follow_redirects').text = 'true'
ET.SubElement(http_sampler, 'boolProp', name='HTTPSampler.auto_redirects').text = 'false'
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.use_keepalive').text = 'true'
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.DO_MULTIPART_POST').text = 'false'
http_hash_tree = ET.SubElement(thread_group_hash_tree, 'hashTree')
# HeaderManager
header_manager = ET.SubElement(http_hash_tree, 'HeaderManager',
guiclass="HeaderPanel",
testclass="HeaderManager",
testname="HTTP Header Manager",
enabled="true")
headers_collection = ET.SubElement(header_manager, 'collectionProp',
name='HeaderManager.headers')
for name, value in req['headers'].items():
header = ET.SubElement(headers_collection, 'elementProp',
name='', elementType='Header')
ET.SubElement(header, 'stringProp', name='Header.name').text = name
ET.SubElement(header, 'stringProp', name='Header.value').text = value
ET.SubElement(http_hash_tree, 'hashTree')
# 处理 POST 请求数据(关键修复)
if method == 'POST' and req['postData']:
content_type = req['headers'].get('Content-Type', req.get('mimeType', ''))
if 'application/x-www-form-urlencoded' in content_type:
set_post_data_urlencoded(http_sampler, req['postData'])
else:
# 原始 POST 数据(JSON/XML 等)
# 关键修复:设置 postBodyRaw 和 postBodyRawValue
ET.SubElement(http_sampler, 'boolProp', name='HTTPSampler.postBodyRaw').text = 'true'
ET.SubElement(http_sampler, 'stringProp', name='HTTPSampler.postBodyRawValue').text = req['postData']
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
rough_string = ET.tostring(root, encoding='utf-8', method='xml')
reparsed = minidom.parseString(rough_string)
f.write(reparsed.toprettyxml(indent=" ")[23:]) # 去除重复的 xml 声明
print(f"✅ JMeter 测试计划已生成:{output_file}")
if __name__ == '__main__':
requests = parse_har("tt.har")
print(f"共解析到 {len(requests)} 个非静态请求")
for i, req in enumerate(requests):
print(f"\n--- 请求 {i + 1} ---")
print("URL:", req['url'])
print("Method:", req['method'])
print("Headers:", req['headers'])
if req['postData']:
print("PostData:", req['postData'])
generate_jmx(requests, "test_plan3C-v1.jmx")
问题:解决JMeter UI 中 POST 请求体实际不显示,将har文件中属于post 请求体信息补全显示到jmeter。提供完整代码