vCard 解剖:一行一属性、75 字符折行与 iPhone item1. 分组的私有方言
通讯录的通用语
换手机、导备份、从 CRM 批量加客户——通讯录跨平台搬运全靠 vCard(.vcf 文件)。它是 1996 年就定型的纯文本格式:每个联系人一张卡,BEGIN:VCARD 开场 END:VCARD 收尾,中间一行一个属性。没有二进制结构、没有中央索引,一个记事本就能打开看。但「能打开」和「能互转」之间隔着一堆规范细节:转义次序、折行规则、结构化字段、还有各大厂商夹带的私有方言。
一张最小合规的卡长这样:
| |
转义:反斜杠必须第一个处理
vCard 一行内用分号分隔结构化组件、用逗号分隔多值,那么值本身包含分号或逗号怎么办?规范(RFC 6350)的答案是反斜杠转义:\ → \\、换行 → \n、; → \;、, → \,。这里有个所有手写实现都会踩的次序坑:反斜杠必须最先替换。如果先把 ; 换成 \;、再处理反斜杠,先前生成的 \; 里的反斜杠会被再翻倍成 \\;,数据凭空多出一层转义。反转义同样要逐字符扫描而非链式替换——遇到 \n 输出换行、\, \; \\ 还原,未知的转义序列保留原样,宽容但不篡改。
另一个容易被忽视的坑藏在空白里:折行回接时如果顺手把换行周围的空白都吞掉,hello\r\n world 会变成 helloworld,一个空格无声消失。规范只删折行处的「换行 + 单个空格/制表符」,多出来的空白属于值本身。
折行:75 字符的腰带
vCard 规定一行不应超过 75 个八位组(octet,按 UTF-8 字节算,不是字符——三个中文就是 9 个八位组)。超长行必须在任意位置折成多行,续行以一个空格或制表符开头;读取方把「换行 + 空白开头」回接成一行。这就是解析前必须先 unfold 的原因——一行 NOTE 里两句话被折成三行,逐行解析会把后半句当成未知属性丢掉。
有趣的例外恰好是超长行的重灾区:头像、二维码这类二进制数据在 vCard 里以 base64 内嵌(PHOTO;ENCODING=b: 开头,续行以空格开头),一张 50KB 的头像折出来是上千行「条文」。所以真实的通讯录文件里,折行处理的质量直接决定能不能读出头像。
N 与 FN:姓氏说了算还是显示名说了算
N 是结构化的姓与名(family;given;additional;prefix;suffix 五段),FN 是整串显示名。导入目标认哪个?答案是不一致的:多数手机导入时优先 FN,规范则说 N 是必填、FN 由 N 推导。更麻烦的是中文场景:一列「张三」该拆成姓张名三,可「欧阳锋」是复姓,程序永远无法仅凭字符串拆对所有人名。现实策略是启发式加兜底——按常见姓氏表拆,拆不准就整名塞进 FN 让显示名兜底;反向生成时中文名「姓+名」直接连接、西文名「名 姓」以空格连接,各自遵循各自的书写习惯。
版本上 2.1 / 3.0 / 4.0 三代并存:2.1 是份额最大的「方言普通话」(iPhone 至今导出 2.1 风格),3.0 是被导出工具默认选用的稳妥交集,4.0 引入了 JSON 风格的值参数与 carddav 生态但终端支持参差。生成侧选 3.0 是兼容面最广的务实解。
iPhone 的 item1. 方言
真机导出的 .vcf 里常见这样的行:
| |
item1. 前缀是 Apple 的私有分组方案:给每个号码编个号,再用同号的 X-ABLabel 挂上自定义标签(「公司」「老家」)。这不是任何版本规范里的语法,标准解析器会把 item1.TEL 当成未知属性跳过——于是号码全丢。兼容做法是剥前缀:属性名按 . 切开,取最后一段当真实属性,item1.TEL → TEL,标签行静默丢弃。通讯录互转工具的口碑,一半取决于这种厂商方言处理得多干净。
从表格到通讯录
🔧 vCard转换器
把这条链双向打通:CSV/TSV → vCard 3.0,自动识别分隔符、猜测列映射(姓名/电话/邮箱/组织/备注),一个单元格里的多个电话按分隔符拆成多条 TEL;vCard → CSV 则兼容 2.1/3.0/4.0 与 item1. 方言、自动 unfold,输出带 UTF-8 BOM 的 CSV——没有 BOM 的话 Excel 会把 UTF-8 中文当 ANSI 读成乱码,这是导出场景投诉率最高的一件事。输入侧还有一道体检:统计文本里的 U+FFFD(替换字符)数量,超量直接提示「源文件编码不对,先换 UTF-8」,把乱码拦截在上传那一刻。
顺带一提,逐字段转义、按分隔符重组这条流水线,与 🔧 CSV转换器 的结构化表格互转是同一套骨架。表格、通讯录与文件格式互转的工具,都收在文件处理流水线专题里;纯文本骨架装结构化数据的思路,EPUB 容器解剖里也能看到一回。