施工材料清单看着不过是几列文字,接入应用后却经常成为批量数据污染的入口。材料员从表格软件导出CSV,列名带有不可见的编码标记,物料名称包含逗号,说明字段有换行,某个供应商还会把数量写成带空格的字符串。如果在HarmonyOS页面里直接使用split(','),从第一条带引号的材料记录开始,列位置就会整体错位。前端列表可能显示得很整齐,后续生成的导入结果却已偏离原始字段。

我把这个功能放到同一套工地材料导入工作台:原始清单统一包括物料编码、物料名称、规格型号、单位、数量、施工区域六列。这里先建立可靠的CSV解析入口,下一篇再把来自不同供应商的列名映射到统一业务模型。具体实现分为HarmonyOS侧的文件选择与确认页面、独立的数据适配边界和可独立执行的Node.js处理逻辑。真正决定解析规则的是第三方CSV库和我们定义的输入契约,而不是列表控件的排列方式。

工地材料清单的CSV解析接入与输入边界

从一个包含引号的材料名开始排查

一份清单可能这样组织:首行六个字段,下一行物料名称是“弯头,不锈钢”,第三行规格包含双引号。标准CSV会用双引号包围带分隔符的字段,并以连续两个双引号表示字段内部的引号。回车换行既可能用于分隔记录,也可能位于被引号包围的字段内部。单纯按行切割再按逗号切割,实际上假定所有字段都没有这些合法内容,输入越接近现场真实表格,错误越隐蔽。

因此解析阶段保留二维字符串数组,不急着转换数量,更不急着写入关系数据库。字符串数组能够让后面的列映射模块准确判断:某行为什么只有五列、为什么有两列都叫“数量”、为何某个字段长得像日期却不该被转换。材料编码中的前导零尤其重要;如果把它转成数字,再回写时前导零就丢失了。CSV解析器只负责语法解析,业务校验由下游单独完成,两种错误对应不同处理路径。

三方库选用csv-parse的Node.js同步入口作为业务预处理侧的参考实现。它对引号、转义、BOM和行尾符提供明确配置,适合有文件大小上限的小型导入。如果未来需要持续处理大型文件,应切换同一库的流式入口,而不是把同步读取逻辑塞进页面生命周期。Node.js包能在预处理进程中使用,并不意味着它的全部依赖与Node专用模块能够在ArkTS应用中原样运行:后续通过接口抽离适配器,再依据目标SDK和鸿蒙第三方包管理规则选择能落地的实现。

要解决的问题:文件进入解析器前缺少硬限制

与其等解析器耗尽内存后再提示失败,不如在读取字节时就建立统一上限。这里约定单文件不超过2 MiB、单条字段缓冲不超过4096字符、最多5000条数据记录,拒绝零字节和明显不完整的UTF-8输入。数量是业务层定义的十进制数据,不借助解析库自动推断类型。入口保留BOM处理,但拒绝未知编码直接混入正常导入流程。

// Node.js业务预处理层:安装 csv-parse 后可调用
const { parse } = require('csv-parse/sync');
const MAX_BYTES = 2 * 1024 * 1024;
function parseMaterialBytes(bytes) {
  if (!Buffer.isBuffer(bytes) || bytes.length === 0 || bytes.length > MAX_BYTES) {
    throw new Error('文件为空或超出2MiB上限');
  }
  const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
  if (text.includes('\u0000')) throw new Error('包含不支持的零字节');
  const matrix = parse(text, {
    bom: true, columns: false, skip_empty_lines: true,
    relax_column_count: true, max_record_size: 4096
  });
  if (matrix.length > 5001) throw new Error('数据记录超过5000条');
  return matrix;
}

这里没有直接开启自动类型转换:例如00127是一串材料编码,0.75则要在数量校验环节按精度规则处理。relax_column_count的作用是让字段数量不一致的记录仍能够交给下一道结构检查,便于展示位置明确的错误;它不会修复缺失的引号。一旦CSV的引号语法本身无法解析,就中止当前文件,避免依据错误的记录边界继续写库。max_record_size保护字段与记录缓冲,对整份文件的容量仍要由入口上限约束。生产链路还需要对输入存储位置和文件保留期限统一管理。

解析后的字段形状也必须固定

业务的首行列名要完整且不重复,允许去掉字段名称两端多余空白,不允许空白列名。如果一个字段多出一列,不采用截掉末尾数据的方式“修复”;如果少一列,也不自动把下一行的内容拼回来。第一阶段可以把它记录为结构错误并阻止进入映射环节,第二篇再演进成有明确行位置的隔离记录。真正影响数据解释的列变动必须由人工确认,而不是让程序随意猜测。

function checkMatrixShape(matrix) {
  if (!Array.isArray(matrix) || matrix.length < 2) {
    throw new Error('缺少表头或数据记录');
  }
  const header = matrix[0].map(cell => String(cell).trim());
  if (header.some(name => !name) || new Set(header).size !== header.length) {
    throw new Error('表头存在空字段或重复列名');
  }
  if (header.length > 32) throw new Error('列数超过32');
  const failures = [];
  for (let index = 1; index < matrix.length; index++) {
    const row = matrix[index];
    if (row.length !== header.length) {
      failures.push({ record: index, reason: '列数不一致', actual: row.length });
    }
  }
  return { header, totalRows: matrix.length - 1, failures };
}

这种结果对象很适合在HarmonyOS页面展示文件摘要。它不需要携带整份原始内容到每个列表项,而是用记录总量、表头、结构异常数量驱动页面渲染。材料员点击文件后可以先看到“表头六列、数据三行、结构错误零行”,再决定是否继续。不应把“解析完毕”等同于“可以入库”;字段名称未映射、数量未检查之前,导入按钮仍应处于待确认状态。UI只更新当前有效请求的数据,用户快速切换文件时,应丢弃上一份文件迟到的处理结果。

处理异常要区分语法错误、结构错误和业务错误

语法错误包括引号未闭合、转义规则被破坏,以及编码损坏。这类错误影响记录划分,不宜逐行跳过。结构错误包括列数不齐和重复表头,可以在原始二维数据的基础上定位。业务错误则是物料编码为空、数量超范围或施工区域不在项目配置内;这些应交给后续统一字段模型处理。三种错误不能合成一条“读取失败”,否则运维只能让用户不停重新导出文件,无法指出真正的问题。

对外部输入还需要控制日志内容。不要在错误日志里输出整份清单或每一行的物料说明,记录文件指纹、错误类型、记录序号和少量结构摘要即可。对于错误提示,尽量使用“第3条记录列数为5,预期6”这种可复查的信息,不要把解析器内部堆栈原样显示给材料员。临时文件与字节缓存的生命周期跟随用户选择动作,旧文件被替换时可回收未提交的预览数据;已确认的导入审计记录另行保留。

要解决的问题:不同三方库返回值需要收敛成一个契约

如果页面直接依赖某个三方库的错误对象和字段命名,将来切换解析引擎,连UI和导入存储层都要一起改。我把适配接口压成一个明确的输入输出契约:输入是UTF-8内容与解析限制,输出是二维字符串数组;结构检查返回标准错误对象。库升级以后,只需要核对解析选项是否改变、同一份样本的字段边界是否一致,而不是在所有页面里查找parse()调用。

const assert = require('node:assert/strict');
function createCsvAdapter(parseFunction) {
  return {
    decode(text) {
      const rows = parseFunction(text);
      if (!Array.isArray(rows) || rows.some(row => !Array.isArray(row))) {
        throw new Error('解析器返回格式不符合二维数组契约');
      }
      return rows.map(row => row.map(value => String(value)));
    }
  };
}
const adapter = createCsvAdapter(() => [
  ['物料编码', '物料名称', '规格型号', '单位', '数量', '施工区域'],
  ['MAT-001', '弯头,不锈钢', 'DN50', '个', '12', '东区一层']
]);
assert.equal(adapter.decode('材料清单')[1][1], '弯头,不锈钢');

这里的注入方式还方便对比不同解析器:在各自入口喂入相同的CSV文本,比较记录数量、单元格原始字符串、边界异常和报错位置。对于带BOM、嵌入换行、逗号、双引号、空字段、重复表头、超长记录和无效编码,建立覆盖这些情况的回归样本。不要用“所有样本都能解析”作为通过标准,因为对于格式损坏的文件,正确行为反而是拒绝导入。

开发环境中的CSV适配接口与字段边界处理

HarmonyOS页面与预处理模块如何交接

页面主要关心选择文件、提示大小、预览列名、展示拒收原因和确认导入。较小的清单可以由业务模块把已解析的前几十行返回给列表,完整数据则留在预处理层等待确认。连接方式可以是应用内可用的纯ArkTS适配实现,也可以是受控后台预处理服务;选择哪种架构需要考虑离线要求、工程依赖以及施工现场网络。尤其是带Node内建模块的包,不能只因npm能安装就认定ArkTS也能导入。

第三方包接入时,先核对oh-package.json5中的生产依赖、目标SDK兼容情况和入口文件,再核对锁文件能否固定相同的解析器构建。界面代码不应依赖解析器返回的原始异常对象,应用层应收到有类型的失败原因,例如INVALID_ENCODING、MALFORMED_CSV、HEADER_INVALID、TOO_MANY_ROWS。同一失败类型在手机和平板上可以采用不同布局,但解释含义必须一致。

调试时我会从文件字节数、解析器入口选项和表头顺序入手:中文列名乱码先查UTF-8,第一列名前多一个不可见字符先查BOM,遇到带逗号的物料名称被拆成两列先查是否还在用字符串切割。若某个记录显示成两条,查带引号字段中的换行与转义;若清单一大就卡住页面,确认是否把同步解析误放进了交互回调。到这里解析得到稳定的二维文本记录,业务字段的可靠性仍需下一篇的映射和逐行校验来保证。

材料导入页面的CSV解析摘要与输入校验

对施工材料导入来说,三方库提供的是符合CSV语法的切分能力,真正决定工程质量的,是库外侧那圈可追踪的输入限制、错误分类与适配契约。把语法、结构、业务规则分开,后面的列映射、错误行隔离、大文件续接与版本回归才能在同一套基础上持续推进。

官方开发资料可参阅DevEco Studio 的 oh-package.json5 配置、华为 OHPM 三方库中心仓。第三方解析器的具体参数参阅csv-parse 同步接口、BOM 处理选项和解析参数清单。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐