简介:面向 Windows 平台进行字符编码转换开发的 C/C++ 工程人员,这份 iconv 库同时提供 32 位与 64 位版本,可用于解决跨编码文本处理、国际化项目中的 GBK/UTF-8/UTF-16 等转换需求。压缩包共 98 个文件,约 1.58MB,主体由 66 个 mo 本地化消息文件、若干 html 手册页与 h 头文件组成,并配有 lib 导入库、dll 动态库和 exe 命令行程序,兼顾源码级集成与直接调用两种使用方式,目录按 Win32、Win64 分设 include、lib、bin、share 等模块,结构清晰便于按目标平台取用。该库已有 1215 人学习下载,说明其在 Windows 下 iconv 移植与部署场景中具有一定参考价值。读者可获得已编译好的 charset、iconv 相关库文件与头文件,以及 html 格式的 API 说明,能够较快完成工程配置、编码转换接口调用和本地化消息适配,减少自行编译 libiconv 的时间成本。
1. 一个 iconv.dll 为什么要分 32 位和 64 位:先看进程位数再谈编码
接手一份从老系统导出的日志时,最先卡住人的往往不是编码表,而是LoadLibrary直接返回 NULL。iconv.dll 就是那种“体积不大、出事很安静”的组件:它把 GNU libiconv 的iconv_open、iconv、iconv_close三个 C 接口以动态库形式带到 Windows,负责 GBK、GB18030、UTF-8、UTF-16LE、BIG5、Shift_JIS、ISO-8859 系列之间的字节级转换。32 位和 64 位是两份互不兼容的二进制,位数对不上,DLL 根本进不了进程地址空间。
它解决的是 Windows 原生MultiByteToWideChar覆盖不到的那部分编码需求——BIG5、EUC-KR、ISO-8859-x 这些在原生 API 里要写一堆 CodePage 常量,用 iconv 就是一行编码名字符串。适合三类人:C/C++ 工程里要处理多语言文本的、Python 脚本要清洗异构数据源的、以及需要把转换逻辑做成独立可替换模块的。
2. 接进工程之前:位数核对与三条调用路径
DLL 放错目录只是表象,真正让人返工的是“位数对了但依赖没对”。这一章先把二进制身份确认清楚,再分别给出 C 静态导入、C 显式加载、Python ctypes 三条路径,选哪条取决于你要不要为这一个库改构建脚本。
2.1 用 PE 头把位数和依赖说死
不要靠文件名里的 “x64” 猜。目录名是人起的,PE 头是编译器写的。最省事的办法是读 PE 的 Machine 字段,几行 PowerShell 就能跑,不用装额外工具。
function Get-PEMachine($path) { $fs = [System.IO.File]::OpenRead($path) $br = New-Object System.IO.BinaryReader($fs) $fs.Seek(0x3C, 'Begin') | Out-Null # e_lfanew,PE 头偏移 $peOffset = $br.ReadInt32() $fs.Seek($peOffset + 4, 'Begin') | Out-Null # 跳过 "PE\0\0" $machine = $br.ReadUInt16() # Machine 字段 $br.Close(); $fs.Close() switch ($machine) { 0x014c { 'x86' } 0x8664 { 'x64' } 0xAA64 { 'ARM64' } default { '0x{0:X4}' -f $machine } } } Get-PEMachine '.\iconv.dll'0x3C处是 e_lfanew,指向 PE 签名;签名后 2 字节是 Machine。0x014c表示 i386,0x8664表示 x86-64,这两个值记熟能省很多事。顺带用Get-PEMachine把同目录下所有 DLL 扫一遍,位数不一致的立刻暴露。
依赖关系用 VS 自带的 dumpbin 看,比图形化工具更适合贴进构建日志:
dumpbin /dependents iconv.dllGNU libiconv 编译出来的 iconv.dll 常见还会带一个libiconv-2.dll或libcharset-1.dll;轻量实现(比如单文件的 win-iconv)则把所有东西静态链进去,只有一个 DLL。这一步决定你部署时要拷几个文件——只拷 iconv.dll 而漏了依赖,就是后面 126 错误的根源。
再说进程侧。C 里最直接的判断是sizeof(void*),指针 8 字节就是 64 位进程;Python 里用struct.calcsize("P") * 8,比platform.architecture()更可靠,后者在某些嵌入式解释器上会读错。
import struct, platform print(struct.calcsize("P") * 8, platform.python_version())提示:位数判断要在加载 DLL 之前做,别等
LoadLibrary失败再回头查。把这段写成启动自检,出错时直接打印“进程 64 位,DLL 32 位”这种明确信息,比错误码 193 友好得多。
2.2 C/C++ 两条路:静态导入库还是显式 LoadLibrary
如果工程本来就有构建脚本,最顺的是静态导入:把头文件和.lib加进工程,运行时系统自己找 DLL。好处是代码干净,函数原型由头文件保证;坏处是 DLL 缺失时进程启动就挂,用户看到的是一个没有任何信息的崩溃框。
#include <iconv.h> #pragma comment(lib, "iconv.lib") /* MSVC 下指定导入库 */ iconv_t cd = iconv_open("UTF-8", "GBK"); /* 目标在前,源在后 */ if (cd == (iconv_t)-1) { /* 处理失败 */ }显式加载则把“找库”这件事挪到运行时,失败可以降级、可以提示、可以换路径。做工具类软件时我一般选这条,因为能把错误码原样打出来。
// iconv_win.c —— 显式加载,cl iconv_win.c #include <windows.h> #include <stdio.h> #include <string.h> typedef void *iconv_t; typedef iconv_t (*pfn_open)(const char *, const char *); typedef size_t (*pfn_iconv)(iconv_t, char **, size_t *, char **, size_t *); typedef int (*pfn_close)(iconv_t); #define ICONV_FAIL ((size_t)-1) int main(void) { HMODULE h = LoadLibraryW(L"iconv.dll"); if (!h) { printf("LoadLibrary 失败, GetLastError=%lu\n", GetLastError()); return 1; } pfn_open i_open = (pfn_open) GetProcAddress(h, "iconv_open"); pfn_iconv i_conv = (pfn_iconv) GetProcAddress(h, "iconv"); pfn_close i_close = (pfn_close) GetProcAddress(h, "iconv_close"); if (!i_open || !i_conv || !i_close) { printf("导出表中缺少 iconv 三件套\n"); return 2; } iconv_t cd = i_open("UTF-8", "GBK"); if (cd == (iconv_t)-1) { printf("iconv_open 失败\n"); return 3; } /* "中文" 的 GBK 字节,写死十六进制避免源码文件编码干扰 */ char src[] = { (char)0xD6, (char)0xD0, (char)0xCE, (char)0xC4, 0 }; char out[64]; char *pin = src, *pout = out; size_t inleft = strlen(src), outleft = sizeof(out) - 1; size_t r = i_conv(cd, &pin, &inleft, &pout, &outleft); if (r == ICONV_FAIL) { printf("转换失败\n"); i_close(cd); return 4; } *pout = '\0'; printf("结果: %s, 剩余输入 %zu 字节\n", out, inleft); i_close(cd); FreeLibrary(h); return 0; }LoadLibraryW用宽字符版本,省掉一次隐式转换;GetProcAddress拿到的地址必须转成和 DLL 导出签名完全一致的函数指针,参数类型差一个const都可能让编译器给出 C4191 警告,运行时则表现为栈被踩。iconv的inbuf在头文件里究竟是char **还是const char **取决于构建时的ICONV_CONST宏,显式加载时按你自己那份iconv.h来写,别照抄网上的片段。
iconv的返回值是“本轮不可逆转换的字符数”,正常情况返回 0,返回(size_t)-1才是出错,此时要靠 errno 区分原因。上面代码里out留了 63 字节,够放下 4 个 GBK 字符转出来的 UTF-8 结果,真实场景要按 3.2 节的公式算容量。
2.3 Python ctypes:十分钟验证 DLL 到底能不能用
不写 C 也能验证。ctypes 直接把 DLL 拉进当前解释器,好处是“解释器位数就是进程位数”,你立刻知道该用哪份 DLL。
import ctypes from ctypes import c_char_p, c_size_t, c_void_p, byref, create_string_buffer, cast # iconv 是 cdecl 导出,用 CDLL;WinDLL 走 stdcall,调用即崩 dll = ctypes.CDLL(r".\iconv.dll", use_errno=True) dll.iconv_open.argtypes = [c_char_p, c_char_p] dll.iconv_open.restype = c_void_p dll.iconv.argtypes = [c_void_p, ctypes.POINTER(c_char_p), ctypes.POINTER(c_size_t), ctypes.POINTER(c_char_p), ctypes.POINTER(c_size_t)] dll.iconv.restype = c_size_t dll.iconv_close.argtypes = [c_void_p] dll.iconv_close.restype = ctypes.c_int def convert_once(data: bytes, src: str, dst: str) -> bytes: cd = dll.iconv_open(dst.encode(), src.encode()) # 目标在前 if cd in (None, 0) or cd == c_void_p(-1).value: raise OSError(ctypes.get_errno(), f"iconv_open({src}->{dst})") buf = create_string_buffer(data, len(data)) pin = cast(buf, c_char_p) inleft = c_size_t(len(data)) out = bytearray() try: while True: obuf = create_string_buffer(4096) pout = cast(obuf, c_char_p) oleft = c_size_t(4096) r = dll.iconv(cd, byref(pin), byref(inleft), byref(pout), byref(oleft)) out += obuf.raw[:4096 - oleft.value] if r != c_size_t(-1).value: break if ctypes.get_errno() == 7: # E2BIG,输出缓冲不够 continue raise OSError(ctypes.get_errno(), "转换中断") finally: dll.iconv_close(cd) return bytes(out) print(convert_once(b"\xd6\xd0\xce\xc4", "GBK", "UTF-8").decode("utf-8"))use_errno=True是必须的,否则ctypes.get_errno()永远返回 0,你只能看到转换失败却不知道为什么。(iconv_t)-1在 64 位下是0xFFFFFFFFFFFFFFFF,c_void_p(-1).value会自动按平台算出对应的整数,别硬写-1,Python 侧拿到的永远是无符号值。E2BIG的数值在 7 左右,不同 CRT 可能略有差异,严谨点写import errno; errno.E2BIG。
注意:这段代码跑通只能证明“DLL 能被加载、三个符号存在、位数匹配”。编码名是否正确、缓冲区够不够,是下一章的事。
3. 编码名、缓冲区与状态:三个参数填错等于没接
调通接口只是开始。真正让转换结果出错的,基本集中在iconv_open的两个字符串、inbytesleft/outbytesleft的指针语义、以及换行边界处的状态复位这三处。
3.1 iconv_open 的参数顺序与编码别名
函数签名是iconv_open(tocode, fromcode),目标编码在前。名字很像“从哪来到哪去”,于是写成iconv_open("GBK", "UTF-8")的人不在少数,结果当然是把 UTF-8 当 GBK 解析,一路 EILSEQ。我的记法是“先写你要的结果,再写你手里的东西”。
编码名大小写不敏感,但同一套编码在不同实现里有别名,写规范名少踩坑。
| 写法 | 实际语义 | 备注 |
|---|---|---|
GBK | 双字节中文 | 与 CP936 基本重合 |
GB2312 | 通常按 GBK 解析 | libiconv 里 GB2312 常映射到 GBK,能转出超出 GB2312 范围的字符 |
GB18030 | 二/四字节中文 | 覆盖面最大,冷僻字、少数民族文字走这个 |
UTF-8 | Unicode 变长 | 无状态,最安全的中间格式 |
UTF-16LE | 小端双字节 | 别写裸UTF-16,那会带 BOM 语义 |
UCS-2LE | 定长两字节 | 只覆盖 BMP,遇增补平面字符会丢 |
BIG5 | 繁体 | Windows 原生要 CP950,iconv 一个词搞定 |
SHIFT_JIS | 日文 | 同上,原生要 CP932 |
CP1252/WINDOWS-1252 | 西欧 | 与 ISO-8859-1 在 0x80-0x9F 段不同,别混用 |
还有两个后缀值得记住://TRANSLIT让不可表示的字符转成近似字符(é 变 e),//IGNORE直接静默丢弃。两者都会把“转换失败”变成“转换成功但内容变了”,调试阶段建议一个都别加,等问题定位清楚再考虑。
3.2 输出缓冲区怎么估:E2BIG 循环与放大系数
iconv不会帮你分配内存,*outbytesleft归零且还有输入没处理完时,它返回 -1 并置 errno 为 E2BIG。这意味着任何“一次调用搞定”的写法都必须先算清最坏情况。
放大系数取最大公约数:GBK 汉字 2 字节转 UTF-8 是 3 字节,1.5 倍;UCS-2 转 UTF-8 也是 1.5 倍;UTF-8 的 4 字节增补字符转 UTF-16LE 是 4 字节,1 倍;有状态编码如 ISO-2022-JP 在切换字符集时会插入转义序列,可能超过 1.5 倍。工程上我统一按输入长度 * 2 + 16申请,多出来的 16 字节是给转义序列和结尾留的余量。
size_t out_cap = in_len * 2 + 16; char *out = (char *)malloc(out_cap); char *pout = out; size_t outleft = out_cap; while (inleft > 0) { size_t r = iconv(cd, &pin, &inleft, &pout, &outleft); if (r != (size_t)-1) break; /* 输入吃完了 */ if (errno == E2BIG) { /* 输出不够,扩容后继续 */ size_t used = out_cap - outleft; out_cap *= 2; out = (char *)realloc(out, out_cap); pout = out + used; outleft = out_cap - used; continue; } break; /* EILSEQ / EINVAL 交给上层 */ }pout是“游标”而不是缓冲区首地址,扩容后必须用out + used重新定位,直接赋out会把已经写出来的数据覆盖掉。inleft同理,它记录的是“还剩多少字节没被消费”,循环条件要看它而不是看返回值。
还有一种省事的写法:一次给足in_len * 4 + 32,对小文件够用,对大文件就是浪费。分块处理时按块算容量,比按全文件算容量更实际。
3.3 状态复位:iconv(cd, NULL, NULL, NULL, NULL)
无状态编码(UTF-8、GBK)转换完就结束了,但 ISO-2022-JP、UTF-7 这类编码在流中间会保留状态。两段文本用同一个cd连续转,第二段的开头可能带着上一段遗留的字符集状态,结果就是“每段单独转都对,拼起来就错”。
复位只需要一次调用:
if (iconv(cd, NULL, NULL, NULL, NULL) == (size_t)-1) { /* 复位失败,通常意味着上一个字符序列本身不完整 */ }参数全 NULL 是约定好的“重置到初始状态”,返回 0 表示成功。什么时候需要复位:同一个cd复用于多份独立文本时;分块读取时每块之间如果人为断开了语义边界,也应该在合适位置复位。但注意——分块读同一个文件时不能复位,因为半个多字节字符可能正卡在块边界上,复位等于把状态抹了,下一块从字节流中间开始解析必然失败。这两件事的区别,是“独立的多个文本”和“同一个文本被切开”,判断标准是数据源,不是块大小。
4. 从单次转换到流式管道:大文件与分块边界的处理
几百 MB 的日志文件不可能一次读进内存,分块是必然选择。麻烦在于多字节编码的字符可能正好被切在两块之间,第二块开头的半个字符会让iconv返回 EINVAL。
4.1 EINVAL 不是错误,是“信息不全”
iconv的三类失败要分清:
| errno | 含义 | 处理方式 |
|---|---|---|
| E2BIG | 输出缓冲满 | 扩容或换新缓冲,继续 |
| EILSEQ | 输入里有源编码不合法的字节 | 定位偏移,决定是丢弃还是报错 |
| EINVAL | 输入末尾是“半个字符” | 把剩余字节留到下一块前面 |
三者里只有 EINVAL 是可以自动恢复的。分块读文件时,正确姿势是每次把上一块的尾部残留拼到本块前面,遇到 EINVAL 就把inleft对应的那段字节存起来,下一轮继续。
import ctypes, errno from ctypes import c_char_p, c_size_t, byref, create_string_buffer, cast, string_at E2BIG = getattr(errno, "E2BIG", 7) EILSEQ = getattr(errno, "EILSEQ", 42) EINVAL = getattr(errno, "EINVAL", 22) def convert_stream(dll, fin, fout, src: str, dst: str, in_chunk: int = 1 << 16, out_chunk: int = 1 << 16): cd = dll.iconv_open(dst.encode(), src.encode()) if cd in (None, 0) or cd == ctypes.c_void_p(-1).value: raise OSError(ctypes.get_errno(), f"iconv_open({src}->{dst})") tail = b"" try: while True: block = fin.read(in_chunk) if not block: break data = tail + block # 拼上上一块的残留 buf = create_string_buffer(data, len(data)) pin = cast(buf, c_char_p) inleft = c_size_t(len(data)) while inleft.value > 0: obuf = create_string_buffer(out_chunk) pout = cast(obuf, c_char_p) oleft = c_size_t(out_chunk) r = dll.iconv(cd, byref(pin), byref(inleft), byref(pout), byref(oleft)) fout.write(obuf.raw[:out_chunk - oleft.value]) if r != c_size_t(-1).value: break # 本块处理完 e = ctypes.get_errno() if e == E2BIG: continue # 换一块输出缓冲继续 if e == EINVAL: break # 尾部是半个字符 raise OSError(e, f"{src}->{dst} 非法序列") tail = string_at(pin, inleft.value) # 剩余字节留到下轮 if tail: raise ValueError(f"文件尾部残留 {len(tail)} 字节不完整序列") finally: dll.iconv_close(cd)关键点在两个地方。其一是tail = string_at(pin, inleft.value),pin已经被iconv推进到未消费数据的位置,inleft是剩余长度,两者一合就是残留。其二是while inleft.value > 0这个内层循环——E2BIG 时输入没吃干净,必须换输出缓冲继续,不能跳出到读下一块。
in_chunk取 64KB 是平衡点:太小则函数调用开销占比高,太大则内存占用和残留字节的处理窗口都变大。out_chunk可以比in_chunk小,因为 E2BIG 分支本来就是设计好的循环。
4.2 判断“到底有没有转换完”
一个容易被忽略的收尾检查:循环结束后tail必须为空。如果不为空,说明文件末尾是半个字符,数据源本身就是截断的。这种情况在日志轮转、断点续传的文件里非常常见,静默忽略会让最后几个字符变成乱码,而且是那种“偶尔出现一次”的乱码,极难复现。
我在转换函数里统一加了这个检查,宁可抛异常也不让脏数据流到下游。下游拿到“转换成功”的结果,就不会再校验编码,问题会一路带到业务层才暴露。
提示:如果数据源确实允许不完整结尾(比如实时追加的日志),把
tail返回给调用方,下次打开文件时拼在前面继续,而不是丢掉。
4.3 用文件对象还是字节流
上面的例子接受的是带read()的文件对象。如果你的数据来自网络、数据库 BLOB 或压缩包,只要能提供“按块产出 bytes”的能力,把它包成一个小适配器就行。别为了复用去改convert_stream的签名,让它只依赖read()这一个方法,边界最清晰。
另一个细节是输出端。示例里直接fout.write(),如果下游需要原子性,应该先写临时文件再os.replace,否则转换中途抛异常会留下半个文件,下次跑的时候容易被误认为“已完成”。
5. 现场排查:五类症状对应五个根因
这一节的每一条都是能对上号的现象,不是理论分类。排错时按“加载阶段 → 调用阶段 → 语义阶段”的顺序走,能省掉大量猜测。
5.1 加载阶段:126 和 193 指向的根本不是同一个问题
现象:LoadLibrary返回 NULL,GetLastError()是 126。
原因:ERROR_MOD_NOT_FOUND,DLL 本体或者它依赖的 DLL 不在搜索路径里。最常见的是只拷了 iconv.dll,漏了同目录的libiconv-2.dll;其次是放在了系统目录之外又没有加进PATH。
解决:先dumpbin /dependents iconv.dll列出依赖,逐个确认同目录存在;运行时用SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)配合AddDllDirectory把私有目录加进搜索范围,比直接改PATH干净。开发机上还有一种可能是被别的同名 DLL 抢先加载了,用 Process Monitor 过滤LoadImage事件看实际路径。
现象:错误码 193。
原因:ERROR_BAD_EXE_FORMAT,位数不匹配。64 位进程加载 32 位 DLL,或者反过来。
解决:用 2.1 节的 PowerShell 函数重新确认 DLL 的 Machine 字段,同时确认宿主进程位数。如果是插件式架构(宿主由第三方决定),就两份 DLL 都带着,运行时先读自己的sizeof(void*)再决定加载哪个,别写死路径。
5.2 调用阶段:加载成功但一调就崩
现象:DLL 加载正常,GetProcAddress也拿到了地址,第一次调用iconv_open就直接 0xC0000005 访问违例。
原因:调用约定不匹配。GNU 工具链导出的函数是 cdecl,如果 C 里的函数指针类型没写对,或者 Python 侧用了WinDLL(stdcall),调用完栈指针就错位了。位数不匹配时也可能表现为这个错误码,但那种情况通常连加载都过不去。
解决:C 侧显式加载时函数指针类型严格照头文件写;Python 侧一律用ctypes.CDLL,不要用WinDLL。判断方法很简单——换成CDLL就不崩,那问题就在调用约定。
现象:转换偶发崩溃,只在处理大文件时出现。
原因:函数返回值被截断。iconv返回size_t,64 位下是 8 字节,如果存进int变量,(size_t)-1会变成-1或者别的值,后续判断r == (size_t)-1永远不成立,代码继续用已经失效的缓冲区。
解决:返回值一律用size_t或intptr_t接;比较时用(size_t)-1而不是-1。
5.3 语义阶段:转换成功但结果是错的
现象:iconv返回 0,没有报错,但输出里的中文变成了问号或者空白。
原因:目标编码不支持源字符,而且调用时加了//IGNORE或//TRANSLIT,把失败静默掉了。
解决:去掉后缀重新跑一次,如果能复现 EILSEQ,说明确实存在不可表示字符;此时的选择是把目标编码换成GB18030或UTF-8(覆盖更全),而不是继续加//IGNORE掩盖。要统计丢了多少字符,可以在去掉后缀的前提下逐段转换,记录每次 EILSEQ 的偏移。
现象:同一个文件,用编辑器打开正常,用 iconv 转出来是乱码。
原因:文件里其实混了两种编码。典型场景是从多个系统合并的日志,主体是 GBK,中间插了几段 UTF-8。iconv 遇到第一个非法字节就返回 EILSEQ,前面的内容已经写进输出缓冲,看起来就像“转了一半”。
解决:先用字节频率粗判——GBK 汉字的高字节集中在 0xB0-0xF7,UTF-8 汉字是三字节且首字节 0xE4-0xE9。按行切分后分别判断,比整文件暴力转换再人工比对高效。写进排查记录的是:单次转换失败时,inleft的剩余量能告诉你已经处理到哪,用len(data) - inleft.value定位偏移,再 dump 前后 32 字节看真实内容。
6. 收口:封装成可回归的转换层与验证清单
零散调用写多了必然乱,我一般会在 iconv 上包一层薄封装,对外只暴露convert(data, src, dst)和convert_file(...)两个函数,errno 统一翻译成异常,DLL 句柄在模块级加载一次并检查位数。封装的价值不在于“结构好看”,而在于让每一处失败都有明确出处。
# 自检脚本:三条用例覆盖 90% 的现场问题 CASES = [ ("gbk2utf8", b"\xd6\xd0\xce\xc4", "GBK", "UTF-8", "中文"), ("utf8togbk", "中文".encode("utf-8"), "UTF-8", "GBK", "中文"), ("big5_2utf8", b"\xa4\xa4\xa4\xe5", "BIG5", "UTF-8", None), ] for name, data, src, dst, expect in CASES: got = convert_once(data, src, dst) if expect is not None: assert got.decode("utf-8") == expect, f"{name} 结果不符: {got!r}" print(name, "ok", got.hex())三条用例分别验证:双字节中文往返、UTF-8 到本地编码的回退、以及一个非中文语系的编码路径。第三条故意不校验内容,只验证“能不能走通”,因为 BIG5 的字节序列我记不牢,但走通与否能反映 DLL 的编码表是否完整——有些精简版实现只编进了拉丁语系,iconv_open("UTF-8","BIG5")会直接返回 -1。
部署前我会固定过一遍这几项:
| 检查项 | 方法 | 不通过的表现 |
|---|---|---|
| DLL 位数 | 读 PE Machine 字段 | 加载错误 193 |
| 依赖完整 | dumpbin /dependents | 加载错误 126 |
| 调用约定 | 换CDLL试一次 | 首次调用即崩 |
| 编码表覆盖 | iconv_open对目标编码试开 | 返回 -1 |
| 尾部完整性 | 流式转换后检查残留 | 文件末尾乱码 |
| 大缓冲路径 | 单次转换 10MB 文本 | 输出截断或崩溃 |
有一个教训到现在我还记得:早年做数据迁移时,图省事把//IGNORE加在了所有转换调用上,结果一批用户昵称里的特殊字符被静默删掉,表面上转换成功率 100%,实际上库里有几千条记录对不上账,靠人工抽检才发现。从那以后我每次接新数据源,都会先关掉所有忽略后缀跑一遍全量,让错误码把问题都吐出来,确认干净后再决定哪些地方可以加容错。这个习惯不省事,但能避免“转换成功却是错的”这种最难的排查场景。
希望帮到你。
本文还有配套的精品资源,点击获取