工作中可能需要新老报文进行比对。直接上代码,创建一个index.html的文件,代码复制进去,然后双击这个文件就能打开,无需依赖任何库,内网可用。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>报文比对工具</title>
<style>
:root {
--primary-color: #4CAF50;
--primary-hover: #45a049;
--danger-color: #f44336;
--warning-color: #ffc107;
--light-bg: #f5f5f5;
--card-bg: #ffffff;
--border-color: #ddd;
--text-color: #333;
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--light-bg);
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: var(--card-bg);
padding: 25px;
border-radius: 8px;
box-shadow: var(--shadow);
}
h1 {
text-align: center;
margin-bottom: 25px;
color: var(--text-color);
font-weight: 600;
}
.input-area {
display: flex;
gap: 20px;
margin-bottom: 25px;
flex-wrap: wrap;
}
.input-box {
flex: 1;
min-width: 300px;
}
.input-box h3 {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.input-box h3::before {
content: "";
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--primary-color);
}
.input-box:nth-child(2) h3::before {
background-color: #2196F3;
}
textarea {
width: 100%;
height: 300px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
resize: vertical;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
transition: border-color 0.3s;
}
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 25px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #d32f2f;
}
.btn-warning {
background-color: var(--warning-color);
color: #333;
}
.btn-warning:hover {
background-color: #e0a800;
}
.result {
margin-top: 20px;
padding: 20px;
border-radius: 4px;
background-color: #f9f9f9;
border: 1px solid var(--border-color);
display: none;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.result h3 {
margin: 0;
color: var(--text-color);
}
.diff {
background-color: #fff8e1;
border-left: 4px solid var(--warning-color);
padding: 15px;
margin: 15px 0;
border-radius: 0 4px 4px 0;
}
.match {
color: var(--primary-color);
font-weight: bold;
padding: 10px;
background-color: #e8f5e9;
border-radius: 4px;
}
.mismatch {
color: var(--danger-color);
font-weight: bold;
padding: 10px;
background-color: #ffebee;
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
th,
td {
border: 1px solid var(--border-color);
padding: 12px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: 600;
}
.field-name {
width: 30%;
font-weight: bold;
}
.only-a {
background-color: #e3f2fd;
}
.only-b {
background-color: #fff3e0;
}
.different {
background-color: #ffebee;
}
.same {
background-color: #e8f5e9;
}
.stats {
display: flex;
gap: 15px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.stat-item {
padding: 8px 15px;
background-color: #f0f0f0;
border-radius: 4px;
font-size: 14px;
}
.stat-item span {
font-weight: bold;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.copy-btn {
background: none;
border: none;
color: #6c757d;
cursor: pointer;
font-size: 18px;
padding: 5px;
}
.copy-btn:hover {
color: var(--primary-color);
}
.filter-controls {
margin: 15px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 12px;
border: 1px solid var(--border-color);
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.filter-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.filter-btn:hover {
background-color: #f0f0f0;
}
.filter-btn.active:hover {
background-color: var(--primary-hover);
}
@media (max-width: 768px) {
.input-area {
flex-direction: column;
}
.input-box {
min-width: 100%;
}
.controls {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<h1>报文比对工具</h1>
<div class="input-area">
<div class="input-box">
<h3>报文A</h3>
<textarea id="messageA" placeholder="请输入报文A内容..."></textarea>
</div>
<div class="input-box">
<h3>报文B</h3>
<textarea id="messageB" placeholder="请输入报文B内容..."></textarea>
</div>
</div>
<div class="controls">
<button id="compareBtn" class="btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z" />
</svg>
XML报文比对
</button>
<button id="sopCompareBtn" class="btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" />
</svg>
SOP报文比对
</button>
<button id="clearBtn" class="btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path fill-rule="evenodd"
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
</svg>
清空内容
</button>
<button id="exampleBtn" class="btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" />
</svg>
sop示例数据
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在比对报文,请稍候...</p>
</div>
<div id="result" class="result">
<div class="result-header">
<h3>比对结果</h3>
<button class="copy-btn" id="copyResultBtn" title="复制结果">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
<path
d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
</svg>
</button>
</div>
<div id="resultMessage"></div>
<div id="statsContainer" class="stats"></div>
<div class="filter-controls">
<button class="filter-btn active" data-filter="all">全部字段</button>
<button class="filter-btn" data-filter="same">匹配字段</button>
<button class="filter-btn" data-filter="different">不同字段</button>
<button class="filter-btn" data-filter="onlyA">仅A存在</button>
<button class="filter-btn" data-filter="onlyB">仅B存在</button>
</div>
<div id="diffDetails"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const messageA = document.getElementById('messageA');
const messageB = document.getElementById('messageB');
const compareBtn = document.getElementById('compareBtn');
const sopCompareBtn = document.getElementById('sopCompareBtn');
const clearBtn = document.getElementById('clearBtn');
const exampleBtn = document.getElementById('exampleBtn');
const copyResultBtn = document.getElementById('copyResultBtn');
const loading = document.getElementById('loading');
const result = document.getElementById('result');
const resultMessage = document.getElementById('resultMessage');
const statsContainer = document.getElementById('statsContainer');
const diffDetails = document.getElementById('diffDetails');
const filterBtns = document.querySelectorAll('.filter-btn');
let allFieldsData = [];
// 比对按钮点击事件
compareBtn.addEventListener('click', function () {
const textA = messageA.value.trim();
const textB = messageB.value.trim();
if (!textA || !textB) {
showError('请输入两个报文内容');
return;
}
loading.style.display = 'block';
result.style.display = 'none';
setTimeout(() => {
try {
const bodyA = extractBody(textA);
const bodyB = extractBody(textB);
if (!bodyA || !bodyB) {
throw new Error('无法提取BODY内容,请检查报文格式');
}
const fieldsA = parseBodyFields(bodyA);
const fieldsB = parseBodyFields(bodyB);
compareFields(fieldsA, fieldsB);
} catch (error) {
showError(`错误: ${error.message}`);
} finally {
loading.style.display = 'none';
}
}, 500);
});
// SOP比对按钮点击事件
sopCompareBtn.addEventListener('click', function () {
const textA = messageA.value.trim();
const textB = messageB.value.trim();
if (!textA || !textB) {
showError('请输入两个报文内容');
return;
}
loading.style.display = 'block';
result.style.display = 'none';
setTimeout(() => {
try {
const fieldsA = parseSOPFields(textA);
const fieldsB = parseSOPFields(textB);
compareFields(fieldsA, fieldsB);
} catch (error) {
showError(`错误: ${error.message}`);
} finally {
loading.style.display = 'none';
}
}, 500);
});
// 清空按钮点击事件
clearBtn.addEventListener('click', function () {
messageA.value = '';
messageB.value = '';
result.style.display = 'none';
});
// 示例按钮点击事件
exampleBtn.addEventListener('click', function () {
messageA.value = `[15:58:20.950][I][28470912][3114]Node295 体信息展示(非发送信息)
[15:58:20.950][I][28470912][3115]{" HTNGBH":"123456"}
[15:58:20.950][I][28470912][3116]{" RMBJEE":"111.00"}
[15:58:20.950][I][28470912][3117]{" QIXIAN":"12"}
[15:58:20.950][I][28470912][3118]{" CDUIRQ":"20250101"}
[15:58:20.950][I][28470912][3119]{" DAIKLX":"1"}`;
messageB.value = `业务数据:
{ On5011.sdr
TSXX []
RMBJEE [111.00]
HTBH []
DAIKLX [1]
CDUIRQ [20250101]
QIXIAN [12]
HTNGBH [123456]
}`;
});
// 复制结果按钮点击事件
copyResultBtn.addEventListener('click', function () {
const resultText = result.innerText;
navigator.clipboard.writeText(resultText)
.then(() => {
const originalTitle = copyResultBtn.getAttribute('title');
copyResultBtn.setAttribute('title', '已复制!');
setTimeout(() => {
copyResultBtn.setAttribute('title', originalTitle);
}, 2000);
})
.catch(err => {
console.error('复制失败: ', err);
});
});
// 筛选按钮点击事件
filterBtns.forEach(btn => {
btn.addEventListener('click', function () {
filterBtns.forEach(b => b.classList.remove('active'));
this.classList.add('active');
const filter = this.getAttribute('data-filter');
renderTable(filter);
});
});
// 显示错误信息
function showError(message) {
loading.style.display = 'none';
result.style.display = 'block';
resultMessage.innerHTML = `<div class="mismatch">${message}</div>`;
statsContainer.innerHTML = '';
diffDetails.innerHTML = '';
}
// 提取BODY内容
function extractBody(message) {
const bodyStart = message.indexOf('BODY');
const bodyEnd = message.indexOf('/BODY');
if (bodyStart === -1 || bodyEnd === -1) {
return null;
}
return message.substring(bodyStart + 6, bodyEnd).trim();
}
// 解析BODY中的字段
function parseBodyFields(bodyContent) {
const fields = {};
const regex = /<([^/<>]+)>([^<>]*)<\/[^>]+>/g;
let match;
while ((match = regex.exec(bodyContent)) !== null) {
const fieldName = match[1].trim();
const fieldValue = match[2].trim();
fields[fieldName] = fieldValue;
}
return fields;
}
// 解析SOP格式字段
function parseSOPFields(content) {
const fields = {};
// 解析格式A: [时间戳][日志级别][线程ID][序号]{"字段名":"值"}
const regexA = /{"\s*([^"]+)"\s*:\s*"([^"]*)"}/g;
let matchA;
while ((matchA = regexA.exec(content)) !== null) {
const fieldName = matchA[1].trim();
const fieldValue = matchA[2].trim();
fields[fieldName] = fieldValue;
}
// 解析格式B: 字段名 [值]
const regexB = /(\w+)\s+\[([^\]]*)\]/g;
let matchB;
while ((matchB = regexB.exec(content)) !== null) {
const fieldName = matchB[1].trim();
const fieldValue = matchB[2].trim();
// 如果字段名在A中已经存在,使用A中的值,否则使用B中的值
if (!fields[fieldName]) {
fields[fieldName] = fieldValue;
}
}
return fields;
}
// 比较两个报文的BODY字段
function compareFields(fieldsA, fieldsB) {
result.style.display = 'block';
// 获取所有唯一的字段名
const allFields = new Set([
...Object.keys(fieldsA),
...Object.keys(fieldsB)
]);
allFieldsData = [];
let isMatch = true;
// 检查每个字段
allFields.forEach(field => {
const valueA = fieldsA[field];
const valueB = fieldsB[field];
let status, rowClass, displayText;
if (valueA === undefined) {
status = 'onlyB';
rowClass = 'only-b';
displayText = '仅报文B存在';
isMatch = false;
} else if (valueB === undefined) {
status = 'onlyA';
rowClass = 'only-a';
displayText = '仅报文A存在';
isMatch = false;
} else if (valueA !== valueB) {
status = 'different';
rowClass = 'different';
displayText = '值不同';
isMatch = false;
} else {
status = 'same';
rowClass = 'same';
displayText = '匹配';
}
allFieldsData.push({
field,
status,
rowClass,
displayText,
valueA: valueA || '-',
valueB: valueB || '-'
});
});
// 显示统计信息
const totalFields = allFields.size;
const matchingFields = allFieldsData.filter(d => d.status === 'same').length;
const differentFields = allFieldsData.filter(d => d.status === 'different').length;
const onlyAFields = allFieldsData.filter(d => d.status === 'onlyA').length;
const onlyBFields = allFieldsData.filter(d => d.status === 'onlyB').length;
statsContainer.innerHTML = `
<div class="stat-item">总字段数: <span>${totalFields}</span></div>
<div class="stat-item">匹配字段: <span>${matchingFields}</span></div>
<div class="stat-item">不同值: <span>${differentFields}</span></div>
<div class="stat-item">仅A存在: <span>${onlyAFields}</span></div>
<div class="stat-item">仅B存在: <span>${onlyBFields}</span></div>
`;
// 显示结果
if (isMatch) {
resultMessage.innerHTML = '<div class="match">报文比对一致:两个报文的字段和值完全相同</div>';
} else {
resultMessage.innerHTML = '<div class="mismatch">报文比对不一致:发现不同的字段或值</div>';
}
// 渲染表格
renderTable('all');
}
// 渲染表格
function renderTable(filter) {
let filteredData = allFieldsData;
if (filter !== 'all') {
filteredData = allFieldsData.filter(item => item.status === filter);
}
let html = '<div class="diff">';
html += '<h4>字段详情:</h4>';
html += '<table>';
html += '<tr><th>字段名</th><th>状态</th><th>报文A的值</th><th>报文B的值</th></tr>';
if (filteredData.length === 0) {
html += `<tr><td colspan="4" style="text-align: center;">没有匹配的字段</td></tr>`;
} else {
filteredData.forEach(item => {
let statusClass = item.status === 'same' ? 'match' : 'mismatch';
html += `<tr class="${item.rowClass}">`;
html += `<td class="field-name">${item.field}</td>`;
html += `<td><span class="${statusClass}">${item.displayText}</span></td>`;
html += `<td>${item.valueA}</td>`;
html += `<td>${item.valueB}</td>`;
html += '</tr>';
});
}
html += '</table>';
html += '</div>';
diffDetails.innerHTML = html;
}
});
</script>
</body>
</html>

