WPS表格转JSON实战:JS宏字段映射与类型转换 1. 先搞清楚要转的到底是什么表格与JSON的映射关系WPS另存为JSON这件事很多人第一反应是找一个WPS插件点一下按钮就完事。但我接触过的需求里十次有八次最后还是要自己写脚本或者半手动调整原因很简单——表格是给人看的JSON是给程序吃的两者对一条数据的定义根本不一样。WPS表格转换成JSON本质上做的是一次结构化重组而不是简单的格式换皮。你手上那张表可能是合并单元格、可能是三层表头、可能混着备注行和小计行而目标JSON要的是干净的对象数组中间那道鸿沟得有人填。我在实际项目里遇到的情况大致分三类。第一类是配置表导出比如游戏里的道具表、活动表策划在WPS里维护程序要JSON第二类是对接接口需要把业务方给的表单数据整理成API能接受的请求体第三类是数据清洗的中间环节先从Excel to JSON转一道再交给下游脚本处理。这三类的共同点是表格的结构是长期维护的字段会增删所以导出方式必须能跟着表结构一起演进不能每次改表就重写一遍代码。所以这篇内容不适合只想点一下的人更适合愿意花半小时把规则定清楚、之后长期省事的人。下面我会把映射规则、技术路线、脚本实现、踩坑记录全部摊开讲你可以直接抄也可以只挑其中一段用。1.1 一张二维表到对象数组的四种常见映射同样是表格转JSON落地的结构差别很大选错了后面全是返工。我见过和用过的映射方式主要有四种各有各的适用边界。第一种是行式对象数组第一行当表头做键之后每一行生成一个对象。这是最通用、最符合直觉的方式也是大多数WPS导出JSON插件默认干的事。它适合字段固定、每行独立的情况比如商品列表、人员名单。第二种是键值对映射两列搞定左边是键右边是值。这种适合配置文件比如一堆零散参数、开关、阈值。它的好处是加一个参数只需要加一行不用动脚本坏处是没法表达列表和嵌套。第三种是分组嵌套某个字段相同的行归到一个对象里形成主表-子表的父子结构。典型场景是订单和订单明细、分类和分类下的条目。这种结构手工转非常痛苦但写好脚本之后是最省事的。第四种是矩阵转置型表头在第一列而不是第一行或者行列都有维度。这种多见于统计报表转JSON的需求相对少一旦遇到基本只能定制。下面这张表是我自己判断用哪种方式的依据你可以对照着看表格特征推荐映射判断依据典型翻车点一行一条记录字段固定行式对象数组表头能唯一确定字段名表头有合并单元格参数零散、经常增删键值对映射每次加参数不想改代码值里混了单位文字有明显父子层级分组嵌套一个主键对应多条明细子表排序被打乱多维度交叉统计矩阵转置行列都是分类维度空单元格含义不明我个人的建议是能上行式对象数组就上行式因为它的心智负担最低前端、后端、脚本都能直接吃。只有当你发现自己在写遍历一遍再按某个字段分组这种逻辑时才考虑升级成嵌套结构。1.2 表头即字段名命名规范先定死这一步最容易被跳过但它是后期返工的最大来源。WPS表格里的表头往往是给人看的写着商品名称必填、价格(元)、是否上架 Y/N直接拿来当JSON的key你的下游代码里就会出现data[价格(元)]这种写法谁看谁难受。我的做法是加一层字段名映射在脚本里维护一张对照表表格里怎么写随便输出的时候统一转成规范命名。规范本身没有绝对标准但要满足三条全小写、用下划线或驼峰其中一种且只用一种、不含空格和中文标点。我习惯用下划线因为它在各种语言里都不用转义。// 表头中文名 - JSON 字段名 const FIELD_MAP { 商品ID: sku_id, 商品名称必填: sku_name, 价格(元): price, 是否上架 Y/N: is_online, 备注信息: remark };映射表还有一个隐藏好处它天然是一份白名单。表格里偶尔会多出几列临时辅助计算用的数据只要不在映射表里导出时就会被自动忽略不用每次手动删列。这一点在实际协作里价值极高因为表是别人维护的你控制不了他会加什么列。注意映射表一定要和表格放在一起管理最好是同一个文件的不同工作表或者干脆写在脚本顶部的常量区。我见过把映射表放在另一个文档里、结果文档被人挪走导致导出全乱的情况。1.3 类型推断这件小事坑最多JSON是有类型的表格没有。WPS表格里一个单元格显示的123可能是数字123可能是文本123也可能是公式算出来的结果。如果你直接把单元格的值塞进JSON.stringify出来的东西大概率不是你想要的。几个高频问题我列一下。数字精度超过15位的长数字比如订单号、身份证号在表格里会被当成数值处理末尾直接变0这时候必须在表格里预先设成文本格式或者导出时统一按字符串处理。前导零编号007导出成数字就变成7了。空单元格是输出null、空字符串还是直接不输出这个键三种做法下游处理方式完全不同必须提前约定。日期表格里的日期本质是一个序列号直接取值会得到45xxx这样的数字得手动格式化。我的处理原则是凡是标识类字段ID、编号、手机号、邮编一律按字符串输出凡是参与计算的字段一律按数字输出空值统一输出 null 而不是空字符串。这条规则定下来之后下游基本不会再因为类型问题来找我。至于怎么判断靠映射表再加一个类型字段就够了const FIELD_MAP { 商品ID: { key: sku_id, type: string }, 价格(元): { key: price, type: number }, 是否上架: { key: is_online, type: boolean }, 备注信息: { key: remark, type: string } };多写这几行配置换来的是下游不用再写一堆parseFloat和类型判断这笔账怎么算都划算。2. 三条技术路线怎么选想把WPS表格转成JSON能走的路其实不止一条。市面上说的WPS导出JSON插件实际上混着好几种完全不同的东西有的是WPS自带的JS宏有的是第三方做的加载项有的压根不是插件就是让你另存成CSV再去别处转。这三条路的成本、门槛、可控性差得很远选错了会浪费很多时间。2.1 路线一WPS JS宏JSA自写导出脚本这是我现在的主力方案。WPS的JS宏官方叫JSAJavaScript for Applications允许你直接用JavaScript操作表格对象读写单元格、遍历区域、生成字符串、写文件全在本地完成不依赖网络不上传数据也不受第三方插件更新影响。它的优势很明确完全可控。字段怎么映射、空值怎么处理、要不要嵌套全部你说了算而且脚本是存在文档里的别人打开这个文件就能直接用不用额外装东西。劣势也很明显有学习成本你得懂一点JavaScript还得熟悉WPS的对象模型第一次写可能要摸索一两个小时。适合的人群是需求会长期存在、表结构会变、对数据不能外传有要求的人。如果你的表每个月都要导一次那这一两个小时的投入很快就能回本。2.2 路线二加载项/第三方插件WPS的应用市场里确实有做格式转换的加载项装上之后菜单里多一个按钮点一下选个范围就导出。对偶尔用一次的人来说这是最快的路径。但我在实际使用中遇到过几个绕不开的问题。一是字段控制粒度粗大多数插件只能做到表头当key、行当对象你想改个字段名、想过滤几列、想做嵌套基本没戏。二是数据流向不明部分在线转换工具是把表格上传到服务端处理的如果表里有业务数据这个风险得自己评估。三是版本依赖插件跟着WPS版本走某次升级之后按钮消失或者行为变化是很常见的事。我的态度是一次性、临时性的转换可以用长期维护的表不要依赖。判断标准很简单——如果这个表三个月后还要再导一次就别用插件了写脚本。2.3 路线三另存为CSV再离线转换这条最土但最稳。WPS表格另存为CSV然后用一段Python或者Node脚本把CSV读进来转JSON。好处是CSV是纯文本格式稳定任何语言都有成熟的解析库处理大文件也比在表格里跑脚本快得多。UTF-8编码的问题也更容易控制。坏处是中间多了一次手工操作容易出错——比如忘了另存为UTF-8编码的CSV中文全变乱码比如CSV里包含逗号和换行列会被撑开。而且一旦表格里有多个工作表就得一张一张存非常繁琐。我通常把这条路线当作兜底方案当表格特别大几十万行、或者JS宏跑崩了的时候退回CSV路线用Python的pandas几行代码就能搞定稳定得像块砖。2.4 三条路线的横向对比维度JS宏自写脚本第三方加载项CSV中转外部脚本上手成本中需懂JS低中需装Python/Node字段可控性完全可控弱完全可控数据安全性全本地视插件而定全本地大文件表现一般万行级尚可一般好十万行级无压力可复用性高脚本存文档里低换机器要重装中脚本单独维护适合场景长期维护的配置表临时一次性转换超大数据量、批量处理我的实际组合是日常小表用JS宏超大数据量退回CSV脚本。两条路都留着遇到什么情况都不慌。3. 环境准备与WPS版本差异动手写之前先把环境和版本确认清楚能省掉后面一半的调试时间。WPS的JS宏在不同版本、不同平台上的能力差异比想象中大尤其是文件读写这块Windows桌面版和别的版本完全不是一回事。3.1 JS宏编辑器的入口与开发设置打开WPS表格在工具或者开发工具选项卡里能找到JS宏入口不同版本位置略有差别有的在工具下的开发工具子菜单里。点进去会弹出一个编辑器窗口左边是工程结构右边是代码编辑区底部有立即窗口可以边写边看结果。第一次用建议先做三件事新建一个模块写一个最简单的函数验证环境通不通比如弹个提示框确认立即窗口能正常输出确认脚本能不能保存到当前文档里。这三步都过了再开始写正式逻辑。// 环境自检确认能拿到当前工作表 function checkEnv() { const ws Application.ActiveSheet; alert(当前表名 ws.Name 已用行数 ws.UsedRange.Rows.Count); }这段跑通说明对象模型可用后面所有操作都是在这个基础上展开的。3.2 版本差异与兼容性检查清单我在不同机器上跑同一份脚本遇到过读取正常但写文件报错、数组下标从0开始还是从1开始不一致这类问题。踩过几次之后我养成了开工前先过一遍检查清单的习惯文件写入能力Windows桌面版一般可以通过ActiveXObject调用系统的文件对象来写文本部分版本内置了文件对象还有一些版本对本地文件访问有限制只能借助单元格中转。这一点必须先测因为它决定了整个方案能不能闭环。区域取值返回的数组下标一次性读取一个区域的值返回的二维数组在不同环境里下标起点可能不同有的从0开始有的从1开始。写循环之前先打印一行看看比事后调试省事得多。编码支持写中文到文件时默认编码很可能不是UTF-8得显式指定否则下游读到的是乱码。单元格取值方式Value2、Text、Value拿到的东西不完全一样Text是显示出来的字符串Value2是原始值。要类型就用Value2要显示效果就用Text。提示把这份清单做成一个checkEnv()函数每次换机器先跑一遍比凭记忆判断靠谱得多。4. 从零写一个可用的导出脚本下面这部分是整篇的核心。我会从最简单的版本开始一层一层往上加功能每一版都能单独跑你可以按需取用。代码用的是WPS的JS宏语法和浏览器里的JavaScript有差异注意区分。4.1 第一版单表平铺导出先解决能跑的问题。目标很朴素把当前工作表的数据读出来第一行当表头之后每行生成一个对象最后打印成JSON字符串看看对不对。function exportBasic() { const ws Application.ActiveSheet; const used ws.UsedRange; const rowCount used.Rows.Count; const colCount used.Columns.Count; // 一次性把整个区域的值读进来比逐个单元格读快很多 const raw ws.Range(ws.Cells(1, 1), ws.Cells(rowCount, colCount)).Value2; // 表头 const headers []; for (let c 1; c colCount; c) { headers.push(String(raw[1][c] || ).trim()); } // 数据行 const result []; for (let r 2; r rowCount; r) { const obj {}; for (let c 1; c colCount; c) { const key headers[c - 1]; if (!key) continue; // 空表头直接跳过 let val raw[r][c]; if (val undefined || val null || val ) { obj[key] null; // 空值统一给 null } else { obj[key] val; } } // 整行全空的跳过很多表末尾会有这种行 if (Object.keys(obj).some(k obj[k] ! null)) { result.push(obj); } } console.log(JSON.stringify(result, null, 2)); }这段代码有三个地方值得展开说。第一为什么用Value2而不是逐个Cells(r,c).Value2。逐单元格读取每读一次都是一次跨进程调用一千行二十列就是两万次调用实测下来要好几秒一次性读区域只调用一次同样规模几十毫秒就完了。这个差距在数据量上来之后是数量级的。第二为什么下标从1开始。区域取值返回的二维数组在这个环境里下标是从1起的raw[1][c]是第一行。这一点和浏览器里的JavaScript完全不同是新手最容易翻车的地方。如果你发现取出来的第一行是空的八成就是这个原因改成从0开始试试。第三为什么整行全空的要跳过。实际维护的表末尾经常留着几行格式刷过的空行UsedRange会把它们算进来不跳过的话JSON里就会多出一堆全是null的对象。4.2 第二版批量读取区域与性能优化第一版能跑但有两个隐患。一是UsedRange有时候会比你想象的大比如表格下方曾经填过内容又删了格式还在它就认为那片区域是已用的读进来一堆空行二是列数超过实际字段数时会多出很多空表头虽然代码里跳过了但循环还是白跑。更稳的做法是自己扫一遍确定边界。另外如果表里有公式列Value2拿到的是计算结果一般够用但如果某列是纯展示用的拼接列可以只读需要的列。// 扫描实际数据边界找到最后一行有内容的行 function findLastRow(ws, colCount) { let last 1; for (let r ws.Rows.Count; r 1; r--) { let hasValue false; for (let c 1; c colCount; c) { const v ws.Cells(r, c).Value2; if (v ! undefined v ! null String(v).trim() ! ) { hasValue true; break; } } if (hasValue) { last r; break; } } return last; }这个函数的思路是从下往上扫找到第一个有内容的行就停。缺点是最坏情况要扫很多行所以可以加一个优化先从UsedRange的最后一行往下找通常几次就能命中。关于性能我做过一次粗略的对比数据规模是一千行、二十列在同一台机器上跑方案平均耗时说明逐单元格读取约 4~6 秒两万次跨调用肉眼可见卡顿一次性区域读取约 60~120 毫秒推荐做法一次性读取 跳过空行约 80~150 毫秒多一次判断但结果更干净数据量再往上走比如五万行脚本就开始吃力了这时候老实退回CSV路线别硬扛。4.3 第三版写文件UTF-8 与 BOM 的坑到这里数据在内存里已经是正确的JSON字符串了但还没落地。最直接的做法是先输出到控制台复制粘贴出去临时用一次可以长期用太蠢。得写文件。Windows桌面版的WPS里常见的做法是通过系统提供的文件流对象写入。这里有个很关键的坑普通的文本写入对象写出来的UTF-8带BOM或者干脆只能写别的编码下游解析器读到开头的不可见字符直接报语法错误。function writeUtf8File(path, text) { // 用支持指定编码的流对象写入避免乱码 const stream new ActiveXObject(ADODB.Stream); stream.Type 2; // 文本模式 stream.Charset utf-8; // 明确指定 UTF-8 stream.Open(); stream.WriteText(text); stream.SaveToFile(path, 2); // 2 覆盖写入 stream.Close(); }用这个方式写出来的文件中文字符是完整的。但它同样绕不开BOM的问题——文件开头会多出三个字节的标记。大多数解析库能自动处理但有些严格的JSON解析器会因此报错。如果你的下游对BOM敏感有两个处理方式一是下游读文件时按utf-8-sig打开二是写入之后再做一次去除不过这一步在JS宏里比较麻烦。我的做法是在下游做兼容比在导出端做手脚简单得多。这是个务实的取舍导出端已经够复杂了能少一件事就少一件。如果所在版本的WPS不支持ActiveXObject还有个退路把JSON字符串写进一个单元格里然后手动复制出去或者用WPS的另存为把那个单元格存成文本文件。这招土但确实能救命。4.4 第四版多工作表合并与嵌套结构单个表导出只是热身真实需求往往是多个工作表一起导或者父子结构嵌套。多工作表的情况很好处理遍历Sheets、对每张表跑一遍前面的逻辑最后合成一个对象键是表名function exportAllSheets() { const out {}; const count Application.Sheets.Count; for (let i 1; i count; i) { const ws Application.Sheets.Item(i); const name ws.Name; if (name.startsWith(_)) continue; // 下划线开头的是辅助表跳过 out[name] sheetToArray(ws); // 复用前面的单表逻辑 } return out; }嵌套结构稍微费点脑子。以订单-明细为例表格里的样子往往是这样的订单号这一列同一个单号连续出现多行每行对应一条明细。要转成{order_id: A001, items: [{...}, {...}]}就得做分组。function groupByKey(rows, keyField, childFields) { const map new Map(); for (const row of rows) { const key row[keyField]; if (!map.has(key)) { // 主表字段去掉子表字段 const head {}; for (const k in row) { if (!childFields.includes(k)) head[k] row[k]; } head.items []; map.set(key, head); } const child {}; for (const k of childFields) child[k] row[k]; map.get(key).items.push(child); } return Array.from(map.values()); }这里有个细节必须提分组依赖行的顺序。如果表格被人按某个列重新排过序同一个订单号的行不再连续用上面的写法还是能正确分组因为用的是Map不看连续性但如果你用检测到单号变化就新建对象的写法就会裂成多个对象。这是我在实际项目里踩过的坑后来全部改成Map方案稳。4.5 第五版字段过滤、类型转换与空值处理最后一层是清洗。前面几版都是表格里有什么就输出什么真实场景里往往要做减法去掉内部字段、转换类型、补齐默认值。我把这些规则集中写成一个转换函数和读取逻辑解耦const SCHEMA { sku_id: { type: string, required: true }, sku_name: { type: string, required: true }, price: { type: number, default: 0 }, is_online: { type: boolean, default: false }, remark: { type: string, default: } }; function convertRow(raw, fieldMap) { const obj {}; for (const cn in fieldMap) { const rule fieldMap[cn]; let v raw[cn]; if (v undefined || v null || String(v).trim() ) { obj[rule.key] rule.default ! undefined ? rule.default : null; continue; } if (rule.type number) { const n Number(String(v).replace(/[^\d.\-]/g, )); obj[rule.key] isNaN(n) ? 0 : n; } else if (rule.type boolean) { const s String(v).trim().toLowerCase(); obj[rule.key] [1, y, yes, 是, true, 上架].includes(s); } else { obj[rule.key] String(v).trim(); } } return obj; }这段代码里有几个刻意的设计。数字清洗用了正则去掉非数字字符是因为实际表格里经常写成¥99.00或者99元直接Number()会得到NaN。布尔转换列了一堆可能的写法因为不同人维护同一张表有人写Y有人写是有人写1不兼容就会静默出错。默认值的存在让下游少写一堆判空尤其是布尔字段缺省成false比null安全。注意布尔转换千万别用Boolean(v)。表格里的false字符串在JavaScript里是真值Boolean(false)返回true。这个坑非常隐蔽我调了半天才发现问题出在类型转换上。5. 常见问题与排查实录脚本写完之后真正的挑战才开始——各种莫名其妙的现象。下面这些是我这两年攒下来的按发生频率排序。5.1 数据层面的问题导出的JSON里日期全变成五位数。表格里的日期本质是序列号1900年1月1日算起的天数。解决办法是导出时判断这一列的类型用格式化函数转成YYYY-MM-DD字符串。最简单的方式是用单元格的Text属性拿到显示值但那样会受单元格格式影响不够稳。长数字末尾变0。订单号、身份证号这类超过15位的数字表格的数值精度撑不住。唯一的解法是在表格里就把它设成文本格式光靠导出端补救没用因为精度在录入那一刻就丢了。中文字段名排序错乱。用for...in遍历对象时键的顺序不保证。如果要保证字段顺序和表头一致得用数组存键名输出时按数组顺序拼接或者用JSON.stringify之前的中间数组处理。某些行莫名消失。八成是空行判断写得过于激进。我早期写过一个所有字段都是空就跳过的逻辑结果有一行数据只填了备注列被误判成空行。后来改成所有字段都为空才跳注意是所有不是任一。5.2 脚本运行层面的问题提示对象不支持该属性。环境能力不一致通常是文件写入那一块。先跑checkEnv()确认能不能创建文件对象不能的话换单元格中转方案。跑着跑着卡死。逐单元格访问大区域越跑越慢。参考前面的表格改成一次性区域读取。同一个脚本换台机器就报错。检查数组下标起点、检查是否有版本特有的API、检查路径分隔符。这三项占兼容性问题的绝大多数。中文乱码。九成是编码没指定。写文件时显式指定UTF-8读取时也确认编码两头对齐才行。5.3 问题速查表现象最可能原因处理方式日期变成数字单元格值是序列号按列类型做格式化长数字末尾变0数值精度丢失表格里预设文本格式中文乱码写入未指定编码流对象显式设 UTF-8JSON解析报错文件带 BOM下游按 utf-8-sig 读多出空对象空行未过滤判断所有字段均为空再跳过布尔值全为 true用了 Boolean() 转换显式判断字符串取值脚本换机器失效下标起点/API 差异先跑环境自检分组结果裂成多条依赖行顺序改用 Map 按主键聚合表头字段名带括号未做字段映射维护映射表统一命名运行缓慢逐单元格访问一次性读取区域6. 实战场景与扩展玩法前面讲的是通用能力这一节说说不同场景下怎么调。同样一套脚本改几行就能适配完全不同的需求。6.1 配置表/词条表导出这类表的特点是字段多、行数不一定多、但需要每次改动都重新导一遍。所以脚本要能一键跑完最好还能自动带上版本号或者时间戳。我的做法是给脚本加一个入口函数导出时自动在文件名里塞时间和表名避免覆盖function exportWithTimestamp() { const json exportAllSheets(); const d new Date(); const stamp d.getFullYear() String(d.getMonth() 1).padStart(2, 0) String(d.getDate()).padStart(2, 0) _ String(d.getHours()).padStart(2, 0) String(d.getMinutes()).padStart(2, 0); const path D:\\export\\config_ stamp .json; writeUtf8File(path, JSON.stringify(json)); alert(已导出 path); }时间戳这一步看着不起眼但省掉了到底哪个是最新版的反复确认。团队协作里这个价值很大。另外配置表通常需要校验。比如某个字段必须有值、某个字段的取值范围固定。我在导出前会跑一遍检查发现问题直接弹窗列出来而不是等下游跑起来才报错。校验逻辑很简单遍历一遍看必填字段是否为空就行但能挡住大半的低级错误。6.2 数据校验与导出前自检我把校验分成三档。阻断级必填字段为空、主键重复这类直接不让导出。警告级字段值格式可疑比如价格是负数、日期超出合理范围弹窗提示但允许继续。提示级表里有疑似多余的空列或者辅助列提醒一下。function validate(rows, schema) { const errors []; const seen new Set(); rows.forEach((row, i) { for (const key in schema) { const rule schema[key]; const v row[key]; if (rule.required (v null || v undefined || v )) { errors.push(第 (i 2) 行 key 必填但为空); } } if (row[sku_id]) { if (seen.has(row[sku_id])) { errors.push(第 (i 2) 行主键重复 row[sku_id]); } seen.add(row[sku_id]); } }); return errors; }行号加2是有讲究的数组下标从0开始表头占一行所以实际行号是下标加2。这个提示让维护表的人能直接定位到出问题的那一行比笼统说有错误有用得多。6.3 把导出做成半自动流水线如果导出频率高到每天好几次手动点按钮也烦。可以再往前推一步把脚本挂到文档的某个事件上或者做成一个简单的按钮控件点一下就完成读取-校验-转换-写文件-提示全流程。按钮的做法很简单在表格里插入一个形状右键指定宏指向你的入口函数。这样维护表的人完全不用懂代码看到按钮点一下就行。我做过一个版本按钮点完之后还会在指定单元格里写一行最后导出时间让所有人心里有数。再往上如果你们有版本管理工具可以让导出脚本把文件直接写进项目目录配合提交前检查基本就实现了改表即生效。不过这一步要谨慎最好保留人工确认的环节不然一次误操作可能把坏数据推上去。我在实际使用中的几点体会说几个文档里不会写、但真正影响体验的细节。别追求一次写完美。我第一版脚本只做最简单的平铺导出用了两个月之后才加嵌套和校验。一开始就想着支持所有情况结果写了一堆用不上的分支反而难维护。把映射表当资产管。字段映射、类型定义、默认值这三样东西组合起来其实就是一份轻量的数据契约。它值得单独维护、加注释、必要时进版本库。我后来把所有表的映射抽到一个专门的配置文件里脚本只负责读配置和执行改字段完全不用动逻辑代码。留一个调试开关。脚本里加个DEBUG常量打开的时候把中间结果打印到立即窗口关掉的时候安静执行。排查问题时这个开关能省掉大量加删打印语句的时间。大表别硬刚。遇到几万行的表与其花半天优化脚本不如老老实实另存CSV丢给Python处理。工具是用来省时间的不是用来证明技术的。