祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 40|回复: 1

[PBIDEA] PBIDEA:用 uo_charset 做字符集转换与乱码排查全解(「gbk」的真实含义、AutoConvert 的盲区与 BOM 的三种下场,PB10 基准 · PB12.5 实测)

[复制链接]

[PBIDEA] PBIDEA:用 uo_charset 做字符集转换与乱码排查全解(「gbk」的真实含义、AutoConvert 的盲区与 BOM 的三种下场,PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

4403

积分

PBAI

积分
4403
贡献
在线时间
小时
9 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-10-27 05:53 编辑

PBIDEA:用 uo_charset 做字符集转换与乱码排查全解(「gbk」的真实含义、AutoConvert 的盲区与 BOM 的三种下场,PB10 基准 · PB12.5 实测)

阅读说明
- 适用版本:PowerBuilder 10 及以上(本文以 PB10 为写作与验证基准,PB12.5 双轨实测)。所用组件 uo_charset 属 PBIDEA 运行库(websuite.pbl + 运行时 PbIdea.dll),需与工程所用版本一致。
- 支持数据库:本文不涉及数据库,无建表脚本、无连接串。
- 操作系统与环境:Windows 7 及以上;32 位工程配 32 位运行库、64 位工程配 64 位运行库;PbIdea.dll 与 Pbidea_cs.dll 必须与可执行文件同目录或位于系统搜索路径。
- 难度系数:★★★☆☆(需要一点「字节」意识:PB 字符串内部是 UTF-16LE,blob 才是字节序列)。
- 其它:文中示例所需的测试文件全部由代码自己生成,不依赖任何外部数据;建议先读第八节「写文件」再回头看第五节,顺序会顺。




一、这篇讲什么

PowerBuilder 里的中文乱码,九成不是「PB 不支持中文」,而是三件事没对齐:字节是什么编码、字符串是什么编码、落盘又写成什么编码。PB 内部字符串固定是 UTF-16LE(PB10 起),而文件、网络、数据库里躺着的往往是 GBK 或 UTF-8 的字节流。两者之间的桥,就是 PBIDEA 的 uo_charset。

它看起来只有几个方法,实际用起来全是暗礁:


  • 你以为传 big5 就能转繁体,它给你一个长度 0 的空 blob,不报错;
  • 你以为 gbk 就是 GBK,换个代码页它悄悄写出 Big5 字节;
  • 你以为 AutoConvert 能自动认编码,它把 GBK 文件解成一半字数的乱码;
  • 你以为读文件不用管 BOM,UTF-16LE 带 BOM 的文件读回来开头多一个问号。


这篇把 uo_charset 的 10 个方法、8 个包装函数、以及 20 组实机探针的读数一次讲透:哪些名字真能用、失败长什么样、BOM 到底被吃还是被留、哪种情况根本判不出编码。所有读数都是 PB10 与 PB12.5 双轨跑出来的,两版逐字一致。

二、先给结论:能力边界一张表

场景结论依据(实测)
目标字符集只认 gbk / utf-8 / utf-16le 三个名字,第四个别名 gb18030 默认等价 GBK24 个候选名逐个试,其余全部返回 0 长度
大写与代码页号GBK、CP936、936、big5、utf-16、utf-16be、cs-2、ascii、latin1、gb 全失败同上,且不报错、不为 NULL
同名互转gbk→gbk、utf-8→utf-8、utf-16le→utf-16le 一律返回空 blob想做「复制/清洗」不能用它,会得到空串
失败形态返回长度 0 的 blob,IsNull() 为 false,无错误码只能靠 Len() 判成败
gbk 的真实含义是「当前 ANSI 代码页」的影子,SetCodePage() 能把它改成 UTF-8 甚至 Big5默认 D6D0CEC4、CP65001 下 E4B8ADE69687、CP950 下 A4A4A4E5
GB18030 四字节汉字默认被压成 3F 3F(两个问号);SetGB18030(true) 后才输出 95 32 82 36U+20000 实测
IsUtf8Text是真校验器(能识别被截断的 UTF-8)GBK 字节判 N、截断 UTF-8 判 N
IsUtf16Text是「含 0 字节 / 带 BOM」的启发式,纯中文无 BOM 的 UTF-16LE 判 N8 字节纯中文 UTF-16LE → N
AutoConvert只对 UTF-8(含 BOM)与 ASCII 可靠;GBK 与 UTF-16 会解错4 字 GBK 解成 2 字、4 字 UTF-16LE 解成 5 字
唯一有「失败信号」的方法SetCodePage()(非法代码页返回 false)其余方法失败一律静默


三、API 全貌

uo_charset 全部能力由 PbIdea.dll 导出,PB 侧只是薄封装。按用途分四组:

分组方法说明
核心转换CharsetConvert(blob src, string from, string to) → blob唯一的真转换入口,字节进字节出
自动识别AutoConvert(blob src) → string一次性把字节解成字符串,不认识 GBK
嗅探IsUtf8Text(blob) / IsUtf16Text(blob)布尔判定,见第六节
定向嗅探HaveAnsiText(blob, string) / HaveUtf8Text(blob, string) / HaveUtf16Text(blob, string)判断 blob 里是否含某个词的某种编码字节
环境SetCodePage(long) / SetGB18030(boolean)改默认代码页、开关 GB18030 表
字体SetDefaultFont(...) 4 个重载 / GetDefaultFont(ref string, ref int)影响的是 PBIDEA 各控件默认字体,与转码无关
清单与查字GetCharsetList() → uo_json / GetCharByUnicode(long) → string138 项代码页清单、按码点取字
8 个包装gbk2utf8 / gbk2utf16 / utf162utf8 / utf162gbk / utf82utf16 / utf82gbk / gbk2big5 / big52gbk只是 CharsetConvert 的语法糖,big5 那两个同样返回空


要注意包装函数的参数语义:gbk2utf8(blob) 的第一个参数是 GBK 字节,不是 PB 字符串。传 Blob(字符串) 进去(那是 UTF-16LE 字节)会得到 5 字节垃圾——实测 gbk2utf8(Blob('中文')) 返回 5 字节,而不是 6 字节的正确 UTF-8。

四、最小可运行示例

下面这个 NVO 是本文的完整示例对象 nvo_charset_io,五个方法分别负责:判定编码、字节转字符串、字符串转字节、读文件、写文件。直接把整段存成 nvo_charset_io.sru 导入即可,无需任何额外依赖(只用 uo_charset)。

使用演示与操作步骤:①新建 NVO,命名 nvo_charset_io,粘贴代码;②在任意窗口按钮里 nvo_charset_io lnv / lnv = create nvo_charset_io / lnv.of_demo();③of_demo() 会在当前目录写出 GBK / UTF-8 / UTF-16LE 三个文件再逐个读回,最后用 MessageBox 报告每步结果。
  1. forward
  2. global type nvo_charset_io from nonvisualobject
  3. end type
  4. end forward
  5. global type nvo_charset_io from nonvisualobject
  6. end type
  7. global nvo_charset_io nvo_charset_io
  8. type variables
  9. uo_charset ic_charset
  10. end variables
  11. forward prototypes
  12. public function string of_detect (blob ab_data)
  13. public function string of_blob_to_text (blob ab_data, string as_enc)
  14. public function blob of_text_to_blob (string as_text, string as_enc, boolean ab_bom)
  15. public function string of_load_text (string as_path, string as_enc)
  16. public function boolean of_save_text (string as_path, string as_text, string as_enc, boolean ab_bom)
  17. public subroutine of_demo ()
  18. end prototypes
  19. // 判定编码:返回 utf-8 / utf-16le / gbk,空数据返回空串
  20. public function string of_detect (blob ab_data);
  21.         if not IsValid(ic_charset) then
  22.                 ic_charset = create uo_charset
  23.         end if
  24.         if IsNull(ab_data) then
  25.                 return ''
  26.         end if
  27.         if Len(ab_data) = 0 then
  28.                 return ''
  29.         end if
  30.         if ic_charset.IsUtf8Text(ab_data) then
  31.                 return 'utf-8'
  32.         end if
  33.         if ic_charset.IsUtf16Text(ab_data) then
  34.                 return 'utf-16le'
  35.         end if
  36.         return 'gbk'
  37. end function
  38. // 字节转字符串:as_enc 传空则自动判定;utf-16le 分支负责剥 BOM
  39. public function string of_blob_to_text (blob ab_data, string as_enc);
  40.         string ls_text
  41.         string ls_enc
  42.         if not IsValid(ic_charset) then
  43.                 ic_charset = create uo_charset
  44.         end if
  45.         ls_text = ''
  46.         ls_enc = Lower(Trim(as_enc))
  47.         choose case ls_enc
  48.                 case 'utf-8'
  49.                         ls_text = String(ic_charset.CharsetConvert(ab_data, 'utf-8', 'utf-16le'))
  50.                 case 'utf-16le'
  51.                         ls_text = String(ab_data)
  52.                         if Len(ls_text) > 0 then
  53.                                 if Left(ls_text, 1) = Char(65279) then
  54.                                         ls_text = Mid(ls_text, 2)
  55.                                 end if
  56.                         end if
  57.                 case else
  58.                         ls_text = String(ic_charset.CharsetConvert(ab_data, 'gbk', 'utf-16le'))
  59.         end choose
  60.         return ls_text
  61. end function
  62. // 字符串转字节:ab_bom 只在 utf-8 / utf-16le 下有效
  63. public function blob of_text_to_blob (string as_text, string as_enc, boolean ab_bom);
  64.         blob lb_data
  65.         blob lb_bom
  66.         string ls_enc
  67.         if not IsValid(ic_charset) then
  68.                 ic_charset = create uo_charset
  69.         end if
  70.         lb_data = Blob('')
  71.         ls_enc = Lower(Trim(as_enc))
  72.         choose case ls_enc
  73.                 case 'utf-8'
  74.                         lb_data = ic_charset.CharsetConvert(Blob(as_text), 'utf-16le', 'utf-8')
  75.                         if ab_bom then
  76.                                 lb_bom = ic_charset.CharsetConvert(Blob(Char(65279)), 'utf-16le', 'utf-8')
  77.                                 lb_data = lb_bom + lb_data
  78.                         end if
  79.                 case 'utf-16le'
  80.                         lb_data = Blob(as_text)
  81.                         if ab_bom then
  82.                                 lb_data = Blob(Char(65279)) + lb_data
  83.                         end if
  84.                 case else
  85.                         lb_data = ic_charset.CharsetConvert(Blob(as_text), 'utf-16le', 'gbk')
  86.         end choose
  87.         return lb_data
  88. end function
  89. // 读文本文件:as_enc 传空则自动判定编码
  90. public function string of_load_text (string as_path, string as_enc);
  91.         long ll_f
  92.         long ll_n
  93.         blob lb_data
  94.         string ls_enc
  95.         ll_f = FileOpen(as_path, StreamMode!, Read!)
  96.         if ll_f < 0 then
  97.                 return ''
  98.         end if
  99.         ll_n = FileRead(ll_f, lb_data)
  100.         FileClose(ll_f)
  101.         if ll_n <= 0 then
  102.                 return ''
  103.         end if
  104.         ls_enc = as_enc
  105.         if ls_enc = '' then
  106.                 ls_enc = of_detect(lb_data)
  107.         end if
  108.         return of_blob_to_text(lb_data, ls_enc)
  109. end function
  110. // 写文本文件:成功返回 true,落盘字节数与预期不符返回 false
  111. public function boolean of_save_text (string as_path, string as_text, string as_enc, boolean ab_bom);
  112.         long ll_f
  113.         long ll_n
  114.         blob lb_data
  115.         lb_data = of_text_to_blob(as_text, as_enc, ab_bom)
  116.         ll_f = FileOpen(as_path, StreamMode!, Write!, LockWrite!, Replace!)
  117.         if ll_f < 0 then
  118.                 return false
  119.         end if
  120.         ll_n = FileWrite(ll_f, lb_data)
  121.         FileClose(ll_f)
  122.         return (ll_n = Len(lb_data))
  123. end function
  124. // 自检入口:写三种编码的文件再逐个读回,最后弹窗报告
  125. public subroutine of_demo ();
  126.         string ls_dir
  127.         string ls_text
  128.         string ls_back
  129.         string ls_path
  130.         string ls_rpt
  131.         ls_dir = GetCurrentDirectory()
  132.         ls_text = 'A中文,。'
  133.         ls_rpt = '原文 ' + String(Len(ls_text)) + ' 字' + Char(13) + Char(10)
  134.         ls_path = ls_dir + '\cs_gbk.txt'
  135.         of_save_text(ls_path, ls_text, 'gbk', false)
  136.         ls_back = of_load_text(ls_path, '')
  137.         ls_rpt += 'GBK 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
  138.         ls_path = ls_dir + '\cs_u8.txt'
  139.         of_save_text(ls_path, ls_text, 'utf-8', false)
  140.         ls_back = of_load_text(ls_path, '')
  141.         ls_rpt += 'UTF-8 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
  142.         ls_path = ls_dir + '\cs_u16.txt'
  143.         of_save_text(ls_path, ls_text, 'utf-16le', true)
  144.         ls_back = of_load_text(ls_path, '')
  145.         ls_rpt += 'UTF-16LE+BOM 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
  146.         ls_rpt += '判定结果:' + of_detect(of_text_to_blob(ls_text, 'gbk', false)) + ' / ' + of_detect(of_text_to_blob(ls_text, 'utf-8', false))
  147.         MessageBox('字符集自检', ls_rpt)
  148. end subroutine
  149. on nvo_charset_io.create
  150. call super::create
  151. TriggerEvent( this, "constructor" )
  152. end on
  153. on nvo_charset_io.destroy
  154. call super::destroy
  155. TriggerEvent( this, "destructor" )
  156. end on
复制代码
说明:示例用 GetCurrentDirectory() 拼出三个测试文件路径。实测时该对象以完全相同的逻辑运行,唯一差别是末尾用 FileWrite 把报告写进文件而不是弹 MessageBox——因为无界面测试环境无法弹窗。

五、CharsetConvert 的四个真相

5.1 只有三个名字真能用

拿 UTF-16LE 的「中文」逐名试 24 个候选目标字符集,读数分成两堆:

名字输出长度结论
gbk4可用
gb180304可用,但字节与 gbk 完全相同
utf-8 / UTF-8 / utf86可用(大小写与连字符都宽容)
GBK0失败(大小写不宽容,与 utf-8 相反)
gb2312 / GB23120失败
cp936 / CP936 / 9360失败(代码页号不能当名字用)
big5 / big-5 / BIG5 / BIG5-2003 / BIG5-HKSCS / cp9500失败(繁体中文无路可走)
utf-16 / utf-16be / ucs-2 / utf-32le0失败(只有小端 UTF-16 可用)
ascii / latin1 / iso-8859-1 / gb / 空串 / nosuch-cs0失败


注意这张表的方向性:utf-16le 作为源是有效的(utf-16le→gbk 实测 4 字节正确),作为目标也有效(gbk→utf-16le 实测 4 字节、String() 还原正确);但写成 utf-16(不带 le)两个方向都失败。

GetCharsetList() 列出的 138 项看着很像「可用字符集清单」,实际是 Windows 代码页清单:936 是「简体中文 GBK」、54936 是「GB18030 简体中文」、950 是「繁体中文 Big5」、65001 是「UTF-8」。这些 code 与 name 都不能直接传给 CharsetConvert——传 936 实测返回 0。清单的用途是给界面做下拉框,不是给转换做参数。

5.2 同名转换返回空

CharsetConvert(x, 'gbk', 'gbk')、('utf-8','utf-8')、('utf-16le','utf-16le') 三种写法实测全部返回 0 长度。这意味着它不能当「规范化/清洗」工具用:想过滤一遍非法字节再原样写回,必须换别的路子(先转成字符串再转回字节)。

5.3 失败长什么样:0 长度 + 不报错 + 不为 NULL

这是最要命的一点。名字写错、方向写错、编码不匹配,得到的都是同一个结果:Len() 为 0 的 blob,IsNull() 为 false,没有任何错误码或异常。所以判断成败只有一条路:
  1. blob lb_out
  2. string ls_err
  3. lb_out = lc_charset.CharsetConvert(lb_src, 'utf-8', 'gbk')
  4. if Len(lb_out) = 0 then
  5.         ls_err = '转换失败:检查字符集名字是否在 gbk / utf-8 / utf-16le 之内'
  6. end if
复制代码

另一类「静默失败」更隐蔽:方向搞错。把 GBK 字节当 UTF-8 转(CharsetConvert(gbk_blob, 'utf-8', 'utf-16le')),实测返回 8 字节、Asc() 得到 -3 的乱码字符串,既不报错也不失败,只是内容全错。所以「转出来有内容」不等于「转对了」,长度和内容都要验。

5.4 「gbk」会随 SetCodePage 漂移

同一个进程里按顺序改代码页,每步都转一次「中文」到 gbk:

步骤调用输出字节实际编码
0默认D6 D0 CE C4GBK
1SetCodePage(65001)E4 B8 AD E6 96 87UTF-8
2SetCodePage(950)A4 A4 A4 E5Big5
3SetCodePage(936)D6 D0 CE C4GBK(恢复)
4SetCodePage(0)D6 D0 CE C4GBK(系统默认)


结论很清楚:'gbk' 这个名字实际代表「当前 ANSI 代码页」,而不是硬编码的 936。谁在程序某处调过 SetCodePage,后面所有 'gbk' 转换就跟着变。多线程/多模块的大型工程里,这条足以制造「同一份代码在不同机器写出不同字节」的灵异现象。稳的做法:启动时显式 SetCodePage(936) 一次,或者干脆只用 utf-8 落盘、只在读旧数据时用 gbk。

顺便记下:SetCodePage 是这一族方法里唯一会返回 false 的——传 99999 返回 false;而 936 / 65001 / 0 / 950 都返回 true。

六、判定编码:三个嗅探器各管什么

八个典型 blob 同时喂给 IsUtf8Text 与 IsUtf16Text:

数据字节数IsUtf8TextIsUtf16Text
UTF-16LE 的「中文」4NN
GBK 的「中文」4NN
UTF-8 的「中文」6YN
ASCII abcd4YY
空 blob0NN
含 0 字节的数据4YY
被截断的 UTF-8(6 字节取前 4)4NN
UTF-16LE 带 BOM 的「中文」6NY


三条可直接落地的判断:


  • IsUtf8Text 是校验器,不是「有没有高位字节」的猜测——被截断的 UTF-8 它判 N,说明它真的在验续字节合法性。可以信。
  • IsUtf16Text 是启发式:只要数据里出现 0 字节、或带 BOM,它就判 Y。所以 abcd(UTF-16LE 下是 61 00 62 00)判 Y,纯中文的 UTF-16LE(2D 4E 87 65,一个 0 字节都没有)判 N。
  • 因此顺序很重要:先问 IsUtf8Text,再问 IsUtf16Text,都不是就当 GBK。反过来的话,ASCII 内容会被误判成 UTF-16。


6.1 AutoConvert 的盲区

AutoConvert 看着像「一把梭」,实测只对 UTF-8 可靠。四个文件逐个读进 blob 再 AutoConvert:

文件字节数IsUtf8TextIsUtf16TextAutoConvert 结果
UTF-8 无 BOM(4 字)12YN4 字,正确
UTF-8 带 BOM(4 字)15YN4 字,正确(BOM 被吃掉)
GBK(4 字)8NN2 字,错
UTF-16LE 带 BOM(4 字)10NY5 字,错(BOM 变成一个字符)
UTF-16BE 带 BOM(4 字)10NN4 字但首字是 ?,错


GBK 那行尤其坑:8 字节解出 2 个字符,Asc() 还是 20013(看着像「中」),肉眼不查长度很难发现。结论:AutoConvert 只能用在「确定是 UTF-8」的场景,做通用解码器必翻车。

6.2 有一种 UTF-16LE 根本判不出来

纯中文、无 BOM、且没有 ASCII 字符的 UTF-16LE 数据(如「中文测试」8 字节:2D 4E 87 65 79 6B D5 8B),IsUtf8Text 判 N、IsUtf16Text 也判 N——它会掉进「当 GBK 处理」的分支,转出 6 个字符的乱码,Asc() 首字是 45。

这不是缺陷,而是信息不足:4 字节的 2D 4E 87 65 在 GBK 与 UTF-16LE 下都「合法」,没有任何字节特征能区分。工程上的正解只有一条:编码由上下文决定,不由数据决定。读自己写出的文件就记下编码;读外部文件就在配置文件/界面上让用户选;实在要猜,就只在「有 BOM」或「含 ASCII 字符」时猜 UTF-16,其余按 GBK。

6.3 Have\*Text:用已知词反查字节形态

HaveAnsiText / HaveUtf8Text / HaveUtf16Text 的语义是「这段 blob 里,是否存在以 ANSI / UTF-8 / UTF-16 编码的某个词」。三个探针分别喂 GBK、UTF-8、UTF-16LE 三种 blob:

blobHaveAnsiText(「中文」)HaveUtf8Text(「中文」)HaveUtf16Text(「中文」)
GBK 字节YNN
UTF-8 字节NYN
UTF-16LE 字节NNY


三列互斥、判定干净。实战价值:当你知道文件里必然出现某个固定词(表头、<?xml、「Config」之类),用它比 IsUtf8Text 更精准——能直接分清「ANSI 还是 UTF-8 还是 UTF-16」,不受字节合法性启发式影响。这是本文给出的第二个嗅探器,两者配合基本能覆盖全部实际场景。

七、BOM 的三种下场

同一个 BOM(U+FEFF)出现在源数据里,往不同方向转,结果完全不同:

源目标结果字节BOM 去哪了
UTF-8 带 BOMgbkD6 D0 CE C4 B2 E2 CA D4(8 字节)被丢弃,正文干净
UTF-8 带 BOMutf-16le4 字符,首字符就是「中」被丢弃
UTF-16LE 带 BOMgbk3F D6 D0 CE C4 B2 E2 CA D4(9 字节)变成一个问号 ?(0x3F)
UTF-16LE 带 BOMutf-8EF BB BF + 正文(15 字节)保留成 UTF-8 BOM


也就是说:从 utf-8 源转出时 BOM 会被吃掉,从 utf-16le 源转出时 BOM 会被当成普通字符。凡是读 UTF-16LE 文件,转码后必须自己判一次首字符:
  1. string ls_text
  2. ls_text = String(lb_data)
  3. if Len(ls_text) > 0 then
  4.         if Left(ls_text, 1) = Char(65279) then
  5.                 ls_text = Mid(ls_text, 2)
  6.         end if
  7. end if
复制代码

Char(65279) 就是 U+FEFF,实测可用(Len() 返回 1)。同理,想生成 BOM 也可以用它:CharsetConvert(Blob(Char(65279)), 'utf-16le', 'utf-8') 实测得到正好 3 字节 EF BB BF,Blob(Char(65279)) 得到 2 字节 FF FE。这比自己拼字节常量干净得多。

八、写文件的正确姿势

四种目标编码,一个函数 of_text_to_blob() 全覆盖,落盘字节实测如下(内容 A中文,。,5 字符):

目标BOM字节数首字节说明
gbk无94141 D6 D0 CE C4 A3 AC A1 A3,最省空间
utf-8无134141 E4 B8 AD ...
utf-8有16EF BB BF13 + 3 字节 BOM,Excel/记事本友好
utf-16le有12FF FE10 + 2 字节 BOM,等宽但体积翻倍


写文件时牢记两点:①FileOpen 必须用 StreamMode! + Write! + Replace!,LineMode! 会自动加行尾并把内容按行切断,二进制就毁了;②FileWrite(handle, blob) 返回实际写入字节数,拿它跟 Len(blob) 比对才是真正的「写成功」判据,FileOpen 成功不代表写成功。

读文件同理用 StreamMode!;FileRead(handle, blob) 直接把整个文件读进 blob(局部 blob 变量即可,不需要 ref),实测 12 / 15 / 8 / 10 字节的文件 FileRead 返回值与 Len() 完全一致。

九、截断与坏字节:两个替换符

网络包不完整、文件被截断时,转换器不会抛错,而是塞替换符:

输入转换输出说明
UTF-8 6 字节取前 4(半个「文」)→ utf-16le2D 4E FD FF = 「中」+ U+FFFD半截序列变替换字符
GBK 4 字节取前 3(半个「文」)→ utf-8E4 B8 AD 3F = 「中?」半截字节变 ?
UTF-16LE 5 字节(多出 1 字节)→ gbk3F D6 D0 = 「?中」残缺的首单元变 ?
空 blob→ utf-80 字节,IsNull() = false空进空出,不算失败


所以「长度对不对」是第一道校验,「有没有 ? 或 U+FFFD」是第二道。对不允许丢字的场景(比如导入财务数据),必须两刀都切。

十、GB18030 四字节汉字

GB18030 相对 GBK 的核心增量是四字节区(Unicode 扩展 B 及以后)。实测 U+20000(UTF-8 下 F0 A0 80 80,UTF-16LE 下是代理对 40 D8 00 DC):

目标是否调 SetGB18030(true)输出结论
gbk否3F 3F(两个问号)字符丢失
gb18030否3F 3F同样丢失——名字没起作用
gbk是95 32 82 36正确的 GB18030 四字节
gb18030(回读)是4 字节 UTF-8,可还原往返无损


两条结论:①gb18030 这个名字在默认状态下等价于 gbk,别指望改个名字就支持四字节;②必须先 SetGB18030(true)(无返回值,是 subroutine),之后的 gbk 目标才真的走 GB18030 表。这个开关是进程级全局状态,和 SetCodePage 一样要在程序入口统一设置,不要在函数里随手改。

十一、清单、查字与字体


  • GetCharsetList() 返回 uo_json 根对象,ToString() 长度 5456 字符(UTF-8 落盘 6492 字节),138 项,形如 {"0":{"code":"10000","name":"罗马"}, ...}。注意 ArrCount() 返回 0、IsArray() 为 false——它是对象不是数组,遍历要用键名 0..137,不能按数组走。
  • GetCharByUnicode(long):20013 → 「中」(Len() = 1,Asc() = 20013);65 → A;0 → 空串(Len() = 0);-1 → 一个字符且 Asc() 返回 -1;65535 → 一个字符,Asc() 返回 -1(因为 Asc() 按有符号 16 位返回,65535 溢出成 -1,这一点必须记住,判断大于 32767 的码点别用 Asc());非 BMP 的 128512(U+1F600)返回 1 个字符但 Asc() 是乱值;冗余代理 55296 也返回 1 个字符。
  • 字体三件套:GetDefaultFont(ref string, ref int) 实测取到 Microsoft YaHei UI / 9;SetDefaultFont('SimSun') 返回 true 且 GetDefaultFont 随即反映 SimSun。它影响 PBIDEA 控件默认字体,与字符集转换无关,别混在同一个初始化块里。


十二、和相近方案怎么分工

需求用哪个理由
PB 字符串 → UTF-8/ANSI 字节uo_string.ToUTF8() / ToAnsi()一步到位,不用先 Blob()
UTF-8 字节 → PB 字符串uo_string.FromUTF8()返回 string(不是 blob),语义最省事
任意字符集互转、读文件判编码uo_charset只有它能做 GBK ↔ UTF-16LE 双向
UTF-16LE 字节 → 字符串String(blob)(PB 原生)不需要组件参与,等价于按 UTF-16LE 解释
字符串 → UTF-16LE 字节Blob(string)(PB 原生)同上
DataWindow 导出时指定编码SaveAsFormattedText(..., EncodingUTF8!)那是导出通道自己的参数,与 uo_charset 无关


一句话记法:PB 原生只管 UTF-16LE 这一端,uo_string 管 UTF-8/ANSI 的便捷方法,uo_charset 管「任意两种字符集之间」。

十三、坑清单(16 条)


  • CharsetConvert 只认 gbk / utf-8 / utf-16le(+ 默认等价的 gb18030),其余名字一律返回 0 长度,不报错。
  • 大小写不统一:utf-8/UTF-8/utf8 都行,GBK 不行、UTF8 不行。别问规律,照抄小写最稳。
  • 代码页号(936 / 54936 / 950 / 65001)不能当字符集名传进去。
  • 同名互转返回空 blob;想做「复制」请自己 Blob()。
  • 判断成败只能看 Len(),没有错误码;IsNull() 恒为 false。
  • 方向写错不会报错,只会得到乱码(GBK 当 UTF-8 转实测 8 字节、Asc() = -3)。
  • 'gbk' 是「当前 ANSI 代码页」的影子,SetCodePage(65001) 会让它输出 UTF-8 字节、SetCodePage(950) 让它输出 Big5 字节。
  • SetCodePage 必须在程序入口定一次,别在业务函数里改。
  • AutoConvert 不识 GBK(4 字解成 2 字),也不识 UTF-16LE(BOM 变成一个字符)。
  • IsUtf16Text 只认「含 0 字节 / 带 BOM」,纯中文无 BOM 的 UTF-16LE 判 N。
  • 判定顺序必须 IsUtf8Text → IsUtf16Text → 否则 GBK,反过来 ASCII 会被误判成 UTF-16。
  • UTF-16LE 源里的 BOM 不会被吃掉:转 GBK 变 ?、转 UTF-8 变 EF BB BF,必须自己剥。
  • Char(65279) 可用来剥/造 BOM,但 Asc() 对大于 32767 的码点返回负数(Asc(Char(65279)) = -257)。
  • gb18030 名字默认等价 GBK,四字节汉字要先 SetGB18030(true),否则变 3F 3F。
  • 截断数据被静默替换为 ? 或 U+FFFD,不抛异常;导入类业务必须查 ?。
  • FileOpen 用 LineMode! 会把二进制内容按行切断;读写字节一律 StreamMode!。


十四、PB12.5 差异

与 PB10 行为一致。 本文全部 20 组探针在 PB10 与 PB12.5 上分别 import、全量编译、运行,两侧诊断输出逐字相同(20 个对象 × 2 个版本,共 40 次运行,零差异):转换字节、BOM 处理、代码页漂移、截断替换符、GB18030 开关、GetCharsetList 条目数全部一致。

需要留意的只有环境侧两点:①PbIdea.dll 要配对应位数的 PB 运行时(32 位 PB 用 32 位 DLL);②uo_charset 属 websuite.pbl,编译时该库必须在库列表里,否则报 Unresolved external 之类的外部引用错误。

十五、实机验证记录

项PB10 基准PB12.5 实测
源码导入20 个对象,0 错误20 个对象,0 错误
全量编译(full rebuild)0 错误,21 个对象齐全0 错误,21 个对象齐全
运行验证20/20 PASS20/20 PASS
双轨读数比对诊断输出逐字一致(0 处差异)同左
编码检查全 GBK / CRLF / 无 BOM / 带导出头,0 异常同左
原型一致性声明与实现逐对象对齐,0 错 0 提醒同左


探针清单(对象名 → 覆盖内容):p01 转换主干与字节形态 · p02 两个嗅探器矩阵 · p03 Have\*Text 三列 · p04 字符集清单 · p05 GetCharByUnicode 九组码点 · p06 AutoConvert 盲区 · p07 八个包装函数与 big5/utf-16be · p08 24 个别名与失败形态 · p09 代码页与字体 · p10 五个真实文件读取 · p11 3×10 方向矩阵 · p12 GB18030 四字节 · p13 清单 UTF-8 落盘 · p14/p17 BOM 构造与三种下场 · p15/p19 代码页数字名与漂移顺序 · p16 截断与坏字节 · p18 无 BOM 歧义 · 演示对象 nvo_charset_io 五项往返。
本文所有代码均为实机编译 + 运行通过;MessageBox 版本与实测版本仅输出语句不同(无界面环境用 FileWrite 落盘读数),逻辑完全一致。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

4403

积分

PBAI

积分
4403
贡献
在线时间
小时
9 小时前 | 显示全部楼层
PB10 兼容版源码包(可直接导入)

附件 s79demo_pb10.zip 是本文示例对象的 PB10 兼容版,已用 PB10 与 PB12.5 双轨编译通过。

s79demo_pb10.zip (10.04 KB, 下载次数: 0)

一、包内清单

  • cs79.pbl —— PB10 格式源码库(内含 nvo_charset_io,编译 0 错误)
  • cs79.pbt —— 工程文件(库列表为相对路径,换目录无需改)
  • src/nvo_charset_io.sru —— 与文中一致的源码(GBK / CRLF / 无 BOM,可直接 Import)
  • 使用说明.txt —— 部署步骤、接口说明、排错清单(GBK 编码,记事本可直接看)


二、环境要求

  • PowerBuilder 10 及以上(本包 PB10 基准编译,PB12.5 同源复验通过)
  • Windows 7 及以上;运行库位数须与可执行文件一致
  • 运行期需要 PBIDEA 运行库:PbIdea.dll 与 Pbidea_cs.dll,放在 exe 同目录或系统搜索路径
  • 编译期工程库列表里必须有 websuite.pbl(uo_charset 在其中),否则报外部引用未解析
  • 本示例不依赖任何数据库,无需建表、无需连接串


三、操作步骤

  • 打开工程 → Library Painter → 选中目标 pbl
  • 右键 Import → 选 nvo_charset_io.sru → 导入
  • Full Build 一次,确认 0 错误
  • 任意窗口按钮的 Clicked 事件里写:nvo_charset_io lnv / lnv = create nvo_charset_io / lnv.of_demo() / destroy lnv
  • 运行后当前目录生成 charset_io_report.txt,内含 GBK / UTF-8 / UTF-16LE+BOM 三种往返的比对结果


四、关于“实测版”与“文中版”的差别
包内源码是实测版:of_demo 末尾用 FileWrite 把报告写进文件(无界面环境无法弹窗)。文中版把这一句换成 MessageBox('字符集自检', ls_rpt) 即可,其余逻辑一字不差。

五、几个必须记住的坑(详见使用说明.txt)

  • 转换结果长度 0:不是异常,是字符集名字不在 gbk / utf-8 / utf-16le 之内,或两个名字相同。本组件失败不抛异常、无错误码、IsNull 恒为 false,只能看 Len
  • “gbk” 这个名字实际代表“当前 ANSI 代码页”:某处调用过 SetCodePage 就会漂移,建议程序入口显式 SetCodePage(936)
  • 读 UTF-16LE 文件开头多一个问号:那是 BOM 被当普通字符,本示例的 of_blob_to_text 已用 Left(文本,1) = Char(65279) 剥掉
  • 四字节汉字变两个问号:GB18030 扩展区要先调 SetGB18030(true)
  • 不要把 AutoConvert 当通用解码器:它只对 UTF-8 与 ASCII 可靠


六、实机验证情况

  • 编译:PB10 full rebuild 0 错误、对象齐全;PB12.5 同源 full rebuild 0 错误
  • 运行:两版各自运行自检,三种编码往返全部 true,判定结果 gbk / utf-8,两版诊断输出逐字一致
  • 未实测项:无(不连数据库、不依赖界面控件、不依赖第三方组件)


文中所有代码均经 PB10 与 PB12.5 双轨实机编译 + 运行,20 个探针对象共 40 次运行,两版读数零差异。使用中遇到问题欢迎在本帖回复。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-10-27 15:10 , Processed in 0.056460 second(s), 24 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表