同事有处理json数据的需求,偶尔会希望能直观的从json数据里面提取想要的数据导出成表格,简单用AI撸了个小助手。
预览地址:JSON转表格解析助手
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSON转表格解析助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
background-color: #f5f7fa;
padding: 20px;
color: #2c3e50;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e1e8ed;
}
.header h1 {
color: #1a73e8;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.input-section {
margin-bottom: 30px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1a73e8;
margin-bottom: 15px;
}
.json-input {
width: 100%;
height: 200px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
resize: vertical;
background-color: #f8f9fa;
}
.json-input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
.controls {
margin: 15px 0;
display: flex;
gap: 15px;
align-items: center;
}
.btn {
background-color: #1a73e8;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #1557b0;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.btn-secondary {
background-color: #6c757d;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn-export {
background-color: #28a745;
}
.btn-export:hover {
background-color: #218838;
}
.result-section {
margin-top: 30px;
}
.table-container {
max-height: 600px;
overflow: auto;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background-color: #f8f9fa;
color: #495057;
font-weight: 600;
padding: 12px 8px;
text-align: left;
border-bottom: 2px solid #dee2e6;
position: sticky;
top: 0;
z-index: 10;
}
.data-table td {
padding: 10px 8px;
border-bottom: 1px solid #dee2e6;
vertical-align: top;
word-break: break-word;
max-width: 300px;
}
.data-table tbody tr:hover {
background-color: #f8f9fa;
}
.data-table tbody tr:nth-child(even) {
background-color: #fdfdfd;
}
.error-message {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 12px;
margin: 15px 0;
}
.success-message {
color: #155724;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 4px;
padding: 12px;
margin: 15px 0;
}
.info-message {
color: #0c5460;
background-color: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 4px;
padding: 12px;
margin: 15px 0;
}
.stats {
margin: 15px 0;
padding: 10px;
background-color: #e7f3ff;
border-radius: 4px;
font-size: 14px;
}
.example-section {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #1a73e8;
}
.example-title {
font-weight: 600;
margin-bottom: 10px;
color: #1a73e8;
}
.example-json {
background-color: #2d3748;
color: #e2e8f0;
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
overflow-x: auto;
margin: 10px 0;
}
.example-btn {
background-color: #17a2b8;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 10px;
}
.example-btn:hover {
background-color: #138496;
}
.virtual-fields-section {
margin: 20px 0;
padding: 15px;
background-color: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #28a745;
}
.virtual-field-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 8px;
background-color: white;
border-radius: 4px;
border: 1px solid #ddd;
}
.virtual-field-input {
flex: 1;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
}
.virtual-field-input.field-name {
max-width: 150px;
}
.virtual-field-input.field-expression {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.btn-small {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
}
.btn-danger {
background-color: #dc3545;
color: white;
border: none;
cursor: pointer;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-success {
background-color: #28a745;
color: white;
border: none;
cursor: pointer;
}
.btn-success:hover {
background-color: #218838;
}
.expression-help {
font-size: 12px;
color: #666;
margin-top: 10px;
padding: 8px;
background-color: #e9ecef;
border-radius: 3px;
}
.expression-help code {
background-color: #f8f9fa;
padding: 2px 4px;
border-radius: 2px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>JSON转表格解析助手</h1>
<p>智能解析JSON数据并转换为表格展示,支持数组对象和嵌套对象中的数组数据</p>
</div>
<div class="input-section">
<div class="section-title">输入JSON数据</div>
<textarea id="jsonInput" class="json-input" placeholder="请粘贴您的JSON数据... 支持两种格式: 1. 直接数组格式: [{"name": "张三", "age": 25}, {"name": "李四", "age": 30}] 2. 嵌套对象格式: {"data": [{"name": "张三", "age": 25}], "total": 1}"></textarea>
<div class="controls">
<button class="btn" onclick="parseJSON()">解析JSON</button>
<button class="btn btn-secondary" onclick="clearAll()">清空</button>
<button class="btn btn-export" onclick="exportToCSV()" id="exportBtn" style="display: none;">导出CSV</button>
</div>
</div>
<div class="virtual-fields-section">
<div class="section-title">虚拟字段配置</div>
<p style="margin-bottom: 15px; font-size: 14px; color: #666;">
通过JSON路径表达式提取复杂对象或数组中的特定值,创建虚拟字段在表格中显示
</p>
<div id="virtualFieldsList">
<!-- 虚拟字段列表将在这里动态生成 -->
</div>
<div style="margin: 15px 0; display: flex; align-items: center; gap: 15px;">
<button class="btn btn-success btn-small" onclick="addVirtualField()">+ 添加虚拟字段</button>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer;">
<input type="checkbox" id="showOnlyVirtualFields" onchange="toggleVirtualFieldsOnly()" style="cursor: pointer;">
<span>只显示虚拟字段</span>
</label>
</div>
<div class="expression-help">
<strong>表达式语法示例:</strong><br>
<code>$.user.name</code> - 获取user对象的name属性<br>
<code>$.tags[0]</code> - 获取tags数组的第一个元素<br>
<code>$.items.length</code> - 获取items数组的长度<br>
<code>$.price.toFixed(2)</code> - 调用price的toFixed方法保留2位小数<br>
<code>$.address?.city</code> - 安全访问(如果address不存在则返回空)
</div>
</div>
<div class="example-section">
<div class="example-title">示例数据</div>
<p style="margin-bottom: 10px;">点击下方按钮可以快速填入示例数据进行测试:</p>
<div style="margin-bottom: 15px;">
<strong>示例1 - 直接数组格式:</strong>
<button class="example-btn" onclick="loadExample1()">使用此示例</button>
<div class="example-json">[
{"姓名": "张三", "年龄": 25, "部门": "技术部", "薪资": 8000},
{"姓名": "李四", "年龄": 30, "部门": "销售部", "薪资": 7000},
{"姓名": "王五", "年龄": 28, "部门": "技术部", "薪资": 9000}
]</div>
</div>
<div>
<strong>示例2 - 嵌套对象格式:</strong>
<button class="example-btn" onclick="loadExample2()">使用此示例</button>
<div class="example-json">{
"code": 200,
"message": "success",
"data": {
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com", "status": "active"},
{"id": 2, "name": "Bob", "email": "bob@example.com", "status": "inactive"},
{"id": 3, "name": "Charlie", "email": "charlie@example.com", "status": "active"}
],
"total": 3
}
}</div>
</div>
</div>
<div class="result-section">
<div class="section-title">解析结果</div>
<div id="resultContainer">
<div class="info-message">
请在上方输入JSON数据并点击"解析JSON"按钮开始解析
</div>
</div>
</div>
</div>
<script>
let currentData = null;
let virtualFields = [];
function parseJSON() {
const input = document.getElementById('jsonInput').value.trim();
const resultContainer = document.getElementById('resultContainer');
if (!input) {
resultContainer.innerHTML = '<div class="error-message">请输入JSON数据</div>';
return;
}
try {
const data = JSON.parse(input);
const tableData = extractTableData(data);
if (tableData && tableData.length > 0) {
currentData = tableData;
displayTable(tableData, resultContainer);
document.getElementById('exportBtn').style.display = 'inline-block';
} else {
resultContainer.innerHTML = '<div class="error-message">未找到可解析的数组数据</div>';
document.getElementById('exportBtn').style.display = 'none';
}
} catch (error) {
resultContainer.innerHTML = `<div class="error-message">JSON解析错误: ${error.message}</div>`;
document.getElementById('exportBtn').style.display = 'none';
}
}
function extractTableData(data) {
// 情况1: 直接是数组
if (Array.isArray(data)) {
return data;
}
// 情况2: 对象中包含数组
if (typeof data === 'object' && data !== null) {
// 广度优先搜索找到第一个数组
const queue = [data];
const visited = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (visited.has(current)) continue;
visited.add(current);
for (const key in current) {
if (current.hasOwnProperty(key)) {
const value = current[key];
// 找到数组且数组不为空
if (Array.isArray(value) && value.length > 0) {
// 检查数组第一个元素是否为对象
if (typeof value[0] === 'object' && value[0] !== null) {
return value;
}
}
// 继续搜索嵌套对象
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
queue.push(value);
}
}
}
}
}
return null;
}
function displayTable(data, container) {
if (!data || data.length === 0) {
container.innerHTML = '<div class="error-message">没有数据可显示</div>';
return;
}
// 获取所有原始列名
const originalColumns = new Set();
data.forEach(item => {
if (typeof item === 'object' && item !== null) {
Object.keys(item).forEach(key => originalColumns.add(key));
}
});
const originalColumnArray = Array.from(originalColumns);
// 添加虚拟字段列名
const virtualColumnNames = virtualFields.map(vf => vf.name).filter(name => name.trim());
// 检查是否只显示虚拟字段
const showOnlyVirtual = document.getElementById('showOnlyVirtualFields')?.checked || false;
const allColumns = showOnlyVirtual ? virtualColumnNames : [...originalColumnArray, ...virtualColumnNames];
if (allColumns.length === 0) {
if (showOnlyVirtual) {
container.innerHTML = '<div class="error-message">没有配置虚拟字段,请先添加虚拟字段或取消"只显示虚拟字段"选项</div>';
} else {
container.innerHTML = '<div class="error-message">数据格式不正确,无法生成表格</div>';
}
return;
}
// 生成表格HTML
const virtualFieldsCount = virtualFields.length;
let statsText;
if (showOnlyVirtual) {
statsText = `解析成功!找到 ${data.length} 条记录,显示 ${virtualColumnNames.length} 个虚拟字段(已隐藏 ${originalColumnArray.length} 个原始字段)`;
} else if (virtualFieldsCount > 0) {
statsText = `解析成功!找到 ${data.length} 条记录,${originalColumnArray.length} 个原始字段,${virtualFieldsCount} 个虚拟字段`;
} else {
statsText = `解析成功!找到 ${data.length} 条记录,${originalColumnArray.length} 个字段`;
}
let tableHTML = `
<div class="stats">
${statsText}
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
${allColumns.map(col => {
const isVirtual = virtualColumnNames.includes(col);
return `<th ${isVirtual ? 'style="background-color: #e8f5e8; color: #2d7a2d;"' : ''}>${escapeHtml(col)}${isVirtual ? ' 🔧' : ''}</th>`;
}).join('')}
</tr>
</thead>
<tbody>
`;
data.forEach((item, index) => {
tableHTML += '<tr>';
if (showOnlyVirtual) {
// 只显示虚拟字段
virtualFields.forEach(vf => {
if (vf.name.trim()) {
const virtualValue = evaluateExpression(vf.expression, item);
tableHTML += `<td style="background-color: #f8fff8;">${escapeHtml(String(virtualValue))}</td>`;
}
});
} else {
// 显示原始字段
originalColumnArray.forEach(col => {
let cellValue = item[col];
if (cellValue === null || cellValue === undefined) {
cellValue = '';
} else if (typeof cellValue === 'object') {
cellValue = JSON.stringify(cellValue);
} else {
cellValue = String(cellValue);
}
tableHTML += `<td>${escapeHtml(cellValue)}</td>`;
});
// 显示虚拟字段
virtualFields.forEach(vf => {
if (vf.name.trim()) {
const virtualValue = evaluateExpression(vf.expression, item);
tableHTML += `<td style="background-color: #f8fff8;">${escapeHtml(String(virtualValue))}</td>`;
}
});
}
tableHTML += '</tr>';
});
tableHTML += `
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function clearAll() {
document.getElementById('jsonInput').value = '';
document.getElementById('resultContainer').innerHTML = '<div class="info-message">请在上方输入JSON数据并点击"解析JSON"按钮开始解析</div>';
document.getElementById('exportBtn').style.display = 'none';
document.getElementById('showOnlyVirtualFields').checked = false;
currentData = null;
virtualFields = [];
renderVirtualFields();
}
function exportToCSV() {
if (!currentData || currentData.length === 0) {
alert('没有数据可导出');
return;
}
// 获取所有原始列名
const originalColumns = new Set();
currentData.forEach(item => {
if (typeof item === 'object' && item !== null) {
Object.keys(item).forEach(key => originalColumns.add(key));
}
});
const originalColumnArray = Array.from(originalColumns);
// 添加虚拟字段列名
const virtualColumnNames = virtualFields.map(vf => vf.name).filter(name => name.trim());
// 检查是否只显示虚拟字段
const showOnlyVirtual = document.getElementById('showOnlyVirtualFields')?.checked || false;
const allColumns = showOnlyVirtual ? virtualColumnNames : [...originalColumnArray, ...virtualColumnNames];
// 生成CSV内容
let csvContent = allColumns.join(',') + '\n';
currentData.forEach(item => {
const row = allColumns.map(col => {
let value;
// 检查是否为虚拟字段
if (virtualColumnNames.includes(col)) {
const virtualField = virtualFields.find(vf => vf.name === col);
value = virtualField ? evaluateExpression(virtualField.expression, item) : '';
} else {
value = item[col];
}
if (value === null || value === undefined) {
value = '';
} else if (typeof value === 'object') {
value = JSON.stringify(value);
} else {
value = String(value);
}
// 处理包含逗号、引号或换行符的值
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
value = '"' + value.replace(/"/g, '""') + '"';
}
return value;
}).join(',');
csvContent += row + '\n';
});
// 创建并下载文件
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
const now = new Date();
const fileName = `json_data_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}.csv`;
link.setAttribute('download', fileName);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function loadExample1() {
const example1 = `[
{"姓名": "张三", "年龄": 25, "部门": "技术部", "薪资": 8000},
{"姓名": "李四", "年龄": 30, "部门": "销售部", "薪资": 7000},
{"姓名": "王五", "年龄": 28, "部门": "技术部", "薪资": 9000}
]`;
document.getElementById('jsonInput').value = example1;
}
function loadExample2() {
const example2 = `{
"code": 200,
"message": "success",
"data": {
"users": [
{
"id": 1,
"profile": {"name": "Alice", "age": 28},
"contact": {"email": "alice@example.com", "phone": "123-456-7890"},
"tags": ["developer", "team-lead"],
"score": 95.5,
"address": {"city": "北京", "district": "朝阳区"}
},
{
"id": 2,
"profile": {"name": "Bob", "age": 32},
"contact": {"email": "bob@example.com"},
"tags": ["designer"],
"score": 87.2,
"address": {"city": "上海", "district": "浦东新区"}
},
{
"id": 3,
"profile": {"name": "Charlie", "age": 25},
"contact": {"email": "charlie@example.com", "phone": "098-765-4321"},
"tags": ["developer", "junior"],
"score": 92.8
}
],
"total": 3
}
}`;
document.getElementById('jsonInput').value = example2;
// 添加一些示例虚拟字段
virtualFields = [
{id: Date.now() + 1, name: "姓名", expression: "$.profile.name"},
{id: Date.now() + 2, name: "年龄", expression: "$.profile.age"},
{id: Date.now() + 3, name: "邮箱", expression: "$.contact.email"},
{id: Date.now() + 4, name: "主要标签", expression: "$.tags[0]"},
{id: Date.now() + 5, name: "分数(保留1位小数)", expression: "$.score.toFixed(1)"},
{id: Date.now() + 6, name: "城市", expression: "$.address?.city"}
];
renderVirtualFields();
}
// 虚拟字段管理功能
function addVirtualField() {
const fieldId = Date.now();
virtualFields.push({
id: fieldId,
name: '',
expression: ''
});
renderVirtualFields();
}
function removeVirtualField(fieldId) {
virtualFields = virtualFields.filter(vf => vf.id !== fieldId);
renderVirtualFields();
// 如果有数据,重新渲染表格
if (currentData) {
displayTable(currentData, document.getElementById('resultContainer'));
}
}
function updateVirtualField(fieldId, property, value) {
const field = virtualFields.find(vf => vf.id === fieldId);
if (field) {
field[property] = value;
// 如果有数据且字段名不为空,自动重新渲染表格
if (currentData && field.name.trim() && field.expression.trim()) {
displayTable(currentData, document.getElementById('resultContainer'));
}
}
}
function renderVirtualFields() {
const container = document.getElementById('virtualFieldsList');
if (virtualFields.length === 0) {
container.innerHTML = '<div style="color: #666; font-style: italic;">暂无虚拟字段,点击下方按钮添加</div>';
return;
}
container.innerHTML = virtualFields.map(vf => `
<div class="virtual-field-item">
<input type="text" class="virtual-field-input field-name"
placeholder="字段名称"
value="${escapeHtml(vf.name)}"
oninput="updateVirtualField(${vf.id}, 'name', this.value)">
<input type="text" class="virtual-field-input field-expression"
placeholder="JSON路径表达式,如:$.user.name"
value="${escapeHtml(vf.expression)}"
oninput="updateVirtualField(${vf.id}, 'expression', this.value)">
<button class="btn btn-danger btn-small" onclick="removeVirtualField(${vf.id})">删除</button>
</div>
`).join('');
}
// JSON路径表达式评估器
function evaluateExpression(expression, data) {
try {
if (!expression || !expression.trim()) {
return '';
}
// 简单的JSON路径表达式解析
let result = data;
let path = expression.trim();
// 移除开头的$符号
if (path.startsWith('$.')) {
path = path.substring(2);
} else if (path.startsWith('$')) {
path = path.substring(1);
}
if (!path) {
return data;
}
// 分割路径
const parts = path.split('.');
for (let i = 0; i < parts.length; i++) {
let part = parts[i];
if (!part) continue;
// 处理数组索引,如 tags[0]
const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
if (arrayMatch) {
const [, arrayName, index] = arrayMatch;
result = result?.[arrayName];
if (Array.isArray(result) && index < result.length) {
result = result[parseInt(index)];
} else {
return '';
}
}
// 处理方法调用,如 length, toFixed(2)
else if (part.includes('(') && part.includes(')')) {
const methodMatch = part.match(/^(.+)\((.*)\)$/);
if (methodMatch) {
const [, methodName, argsStr] = methodMatch;
if (methodName === 'length' && Array.isArray(result)) {
result = result.length;
} else if (methodName === 'toFixed' && typeof result === 'number') {
const digits = parseInt(argsStr) || 0;
result = result.toFixed(digits);
} else if (methodName === 'toString') {
result = String(result);
} else if (methodName === 'join' && Array.isArray(result)) {
const separator = argsStr.replace(/['"]/g, '') || ',';
result = result.join(separator);
} else {
// 尝试调用其他方法
if (result && typeof result[methodName] === 'function') {
const args = argsStr ? argsStr.split(',').map(arg => {
arg = arg.trim();
if (arg.startsWith('"') && arg.endsWith('"')) {
return arg.slice(1, -1);
}
if (arg.startsWith("'") && arg.endsWith("'")) {
return arg.slice(1, -1);
}
return isNaN(arg) ? arg : Number(arg);
}) : [];
result = result[methodName](...args);
} else {
return '';
}
}
}
}
// 处理安全访问,如 address?.city
else if (part.includes('?')) {
const [safeProp] = part.split('?');
result = result?.[safeProp];
}
// 普通属性访问
else {
result = result?.[part];
}
if (result === undefined || result === null) {
return '';
}
}
return result;
} catch (error) {
console.warn('表达式评估错误:', expression, error);
return '';
}
}
// 切换虚拟字段显示模式
function toggleVirtualFieldsOnly() {
if (currentData) {
displayTable(currentData, document.getElementById('resultContainer'));
}
}
// 页面加载时的初始化
document.addEventListener('DOMContentLoaded', function() {
renderVirtualFields();
});
</script>
</body>
</html>