报文比对工具(xml和sop)

工作中可能需要新老报文进行比对。直接上代码,创建一个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>


相关推荐
笑醉踏歌行1 小时前
NVM 在安装老版本 Node环境时,无法安装 NPM的问题
前端·npm·node.js
YUJIANYUE1 小时前
Gemini一次成型龙跟随鼠标html5+canvas特效
前端·计算机外设·html5
abiao19811 小时前
npm WARN ERESOLVE overriding peer dependency
前端·npm·node.js
TechExplorer3651 小时前
禁用 npm 更新检查
前端·npm·node.js
行云流水6265 小时前
uniapp pinia实现数据持久化插件
前端·javascript·uni-app
zhangyao9403305 小时前
uniapp动态修改 顶部导航栏标题和右侧按钮权限显示隐藏
前端·javascript·uni-app
福尔摩斯张7 小时前
Axios源码深度解析:前端请求库设计精髓
c语言·开发语言·前端·数据结构·游戏·排序算法
aiguangyuan7 小时前
React 中什么是可中断更新?
javascript·react·前端开发
李牧九丶7 小时前
从零学算法1334
前端·算法