在开发面向中国台湾地区的记账应用时,支持本地主流的“电子发票载具”数据导入是提升用户体验的核心功能。由于当地官方导出的 CSV 具有固定的结构,本文将从工程实现角度,梳理开发过程中的核心痛点、防坑指南及 Swift 解析落地代码。


一、 核心技术难点与避坑指南

在编写 iOS 端 CSV 解析器(或使用第三方库如 CHCSVParser / SwiftCSV)时,需重点处理以下 4 个本地化技术细节:

1. 字符编码容错 (Big5 vs UTF-8)

  • 技术痛点:当地官方系统或旧版网页导出的 CSV 往往采用 Big5 编码,而非现代标准的 UTF-8。若直接用 String(contentsOf: encoding: .utf8) 读取,中文商户名(如“統一超商”)会全部变成乱码。
  • 解决方案:读取文件时必须实现降级重试逻辑:优先尝试 .utf8,若失败或解析出乱码,则降级使用 String.Encoding.big5。
    
    // Swift 示例:Big5 编码兼容写法
    let big5Encoding = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.big5.rawValue)))

### 2. 日期与时间拆分处理

* **技术痛点**:官方 CSV 的时间字段被拆成了两列——发票日期(格式如 2026/10/28)和 发票时间(格式如 09:30:15)。
* **解决方案**:解析时需将两列字符串拼接为 2026/10/28 09:30:15,再配合 DateFormatter(指定 yyyy/MM/dd HH:mm:ss)转换为 iOS 的 Date 对象。

### 3. 一票多商品(行聚合)处理

* **技术痛点**:电子发票 CSV 采用的是“按商品明细逐行平铺”的结构。若用户在一笔消费中购买了 3 件商品,CSV 中会出现 3 行记录,但它们的 发票号码 和 交易金额 完全相同。
* **解决方案**:
* **明细流**:每一行生成一条账单,备注记录具体的 品名(适合精细化记账)。
* **聚合流(推荐)**:根据 发票号码 进行 Group By 聚合。金额取该发票的总金额,备注将多个 品名 用符号连接(如 美式咖啡大杯、鲜乳),避免单笔消费被拆成多笔独立账单。

### 4. 智能分类关键字匹配 (Auto-Categorization)

* **技术痛点**:发票中的 卖家名称 通常为完整的企业全称(例如:統一超商股份有限公司)。
* **解决方案**:采用包含匹配(Contains)或正则表达式,而非精确匹配:
* 包含 統一超商、全家便利商店 $\rightarrow$ 自动归类为 **餐饮/便利店**
* 包含 全聯實業、家福 $\rightarrow$ 自动归类为 **日常/超市**
* 包含 台灣中油 $\rightarrow$ 自动归类为 **交通/加油**

---

## 二、 验证用标准示例 CSV 文件

可将下方内容保存为 invoice_sample.csv(推荐编码为 **UTF-8 with BOM** 或 **Big5**),用于本地单元测试或模拟器数据解析验证:

```csv
发票号码,发票日期,发票时间,卖家名称,卖家统一编号,交易金额,载具类别,品名,数量,单价
AB12345678,2026/10/28,09:30:15,統一超商股份有限公司,22555003,105,手機條碼,美式咖啡大杯,1,55
AB12345678,2026/10/28,09:30:15,統一超商股份有限公司,22555003,105,手機條碼,茶葉蛋,2,25
CD87654321,2026/10/28,12:15:00,全聯實業股份有限公司,04322559,328,手機條碼,鮮乳946ml,1,115
CD87654321,2026/10/28,12:15:00,全聯實業股份有限公司,04322559,328,手機條碼,有機土雞蛋10入,1,113
CD87654321,2026/10/28,12:15:00,全聯實業股份有限公司,04322559,328,手機條碼,香蕉一束,1,100
EF99887766,2026/10/28,18:45:20,台灣中油股份有限公司,03722901,500,手機條碼,95無鉛汽油,25.2,19.84

三、 Swift 解析逻辑核心代码参考

以下为单行 CSV 结构化映射的 Swift 实现示例:

import Foundation

struct InvoiceItemModel {
    let invoiceNumber: String
    let date: Date
    let merchantName: String
    let amount: Decimal
    let itemName: String
    let quantity: Decimal
    let unitPrice: Decimal
}

class InvoiceParserUtil {

    // 解析单行 CSV(传入已按逗号切分出的字段数组)
    static func parseRow(_ columns: [String]) -> InvoiceItemModel? {
        guard columns.count >= 10 else { return nil }

        let invoiceNum = columns[0].trimmingCharacters(in: .whitespaces)
        let dateStr = columns[1].trimmingCharacters(in: .whitespaces)
        let timeStr = columns[2].trimmingCharacters(in: .whitespaces)
        let merchant = columns[3].trimmingCharacters(in: .whitespaces)

        guard let amount = Decimal(string: columns[5].trimmingCharacters(in: .whitespaces)),
              let qty = Decimal(string: columns[8].trimmingCharacters(in: .whitespaces)),
              let price = Decimal(string: columns[9].trimmingCharacters(in: .whitespaces)) else {
            return nil
        }

        let itemName = columns[7].trimmingCharacters(in: .whitespaces)

        // 组合日期时间
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
        dateFormatter.timeZone = TimeZone(identifier: "Asia/Taipei")
        let fullDate = dateFormatter.date(from: "\(dateStr) \(timeStr)") ?? Date()

        return InvoiceItemModel(
            invoiceNumber: invoiceNum,
            date: fullDate,
            merchantName: merchant,
            amount: amount,
            itemName: itemName,
            quantity: qty,
            unitPrice: price
        )
    }
}

四、 进阶:应用内 WebView 拦截与自动化路线
对于已经拥有一定用户规模(例如达到数千 DAU)的应用,为了将用户的操作摩擦力降到最低,可以采用应用内内置 WKWebView 登录并拦截下载回调的技术方案:

内嵌 WebView 登录:在 App 内部加载财政部电子发票平台,利用 Cookie 持久化保存用户的登录状态。

下载拦截(WKDownload):通过 iOS 的 WKNavigationDelegate 与 WKDownloadDelegate,在代码层直接拦截 CSV 的下载动作,不让其流转到外部浏览器。

静默入库:获取下载到沙盒的 CSV 文件流后,后台自动调用上述 InvoiceParserUtil 完成解析、去重与入库,实现“点一下链接,后台自动记账”的极佳体验。

长期商业演进:前期利用内嵌 WebView 方案满足 MVP 需求;待产品进入稳定盈利期后,可进一步通过当地合伙人或合规渠道申请官方 Open API,无缝升级为高阶的 Pro 会员“一键自动同步”护城河功能。