马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 字符串处理全解:Len、Pos、Mid、Replace、Trim 与中文、NULL 那些坑
阅读说明
1. 适用版本:PowerBuilder 10 及以上;本文全部代码在 PB 12.5 实机编译并运行通过(ORCA 全量编译 0 错误、PBVM 真机运行 PASS、59 条断言全中)。其中 Trim 的第二参数 removeallspaces 是 PB 11.5 起才有,PB10 请只用 Trim(s)
2. 支持数据库:本文是纯 PowerScript 语言层内容,不涉及数据库
3. 操作系统与环境要求:Windows 7 及以上,装好 PB 运行时即可;不需要任何第三方组件或运行库
4. 难度系数:★☆☆☆☆
5. 其它阅读说明:文中每条 // 后面的输出值都是 PB12.5 实机跑出来的真实结果,可直接对照验证;只要知道「PB 里变量声明与赋值要分两行写」就能跟做
一、PB 的字符串到底是什么
写代码之前,先把三件事想明白,后面一半的坑都能自己推出来。
第一,PB 的字符串是变长的、没有长度上限(受内存限制),声明时写 string ls_x 就行,不像 C 要预留缓冲区。空串 '' 和未赋值是不一样的:刚声明的 string 变量默认是空串,不是 NULL。
第二,从 PB 10 开始,字符串内部是 Unicode。这一点直接决定了「长度」有两种算法:Len() 数的是字符,LenA() 数的是字节(按本机 locale 转成 DBCS 之后再算)。中文环境下 '中文ABC' 是 5 个字符、7 个字节。
第三,字符串是「有字符集」的,Blob 是「没字符集」的纯字节。做文件读写、网络发包、加密签名的时候,你必须自己决定用哪种编码落地——这一步含糊,中文必乱码。
顺带说一句:LenW()、MatchW() 这两个带 W 后缀的函数在 PB12.5 里已经被标记为废弃,编译时会有提示:
- (0009): Obsolete C0185: Function 'LenW' is now obsolete and will be removed in a future release
复制代码
ToAnsi() / ToUnicode() 同样如此。新代码一律用不带后缀的版本。
二、字符串函数全貌清单
按用途分七类,这张表可以当速查卡用(均出自 PB12.5 帮助,示例值均为实机验证)。
| 类别 | 函数 | 作用 | 返回值要点 | | 长度 | Len(s或blob) | 字符数 / blob 字节数 | 失败 -1;NULL 返回 NULL | | 长度 | LenA(s) | 按本机 locale 转 DBCS 后的字节数 | 中文环境下 '中文ABC'=7 | | 长度 | LenW(s) | 已废弃,行为同 Len | 编译报 C0185 | | 查找 | Pos(s1, s2 {, start}) | 从左找子串 | 1 基;找不到返 0;区分大小写 | | 查找 | LastPos(s1, s2 {, 搜索长度}) | 从右找子串 | 找不到返 0 | | 截取 | Mid(s, start {, 长度}) | 从 start 取若干字符 | start 越界不报错,返空串 | | 截取 | Left(s, n) / Right(s, n) | 取左 / 右 n 个字符 | 按字符算,中文安全 | | 替换 | Replace(s1, start, n, s2) | 按位置替换 n 个字符 | n=0 等于插入;n>0 且 s2 为空等于删除 | | 去空白 | Trim(s {, removeallspaces}) | 去首尾空白 | 默认只去空格;第二参 TRUE 连 Tab、全角空格一起去掉(PB11.5+) | | 去空白 | LeftTrim(s {, b}) / RightTrim(s {, b}) | 只去左 / 只去右 | 同上 | | 大小写 | Lower(s) / Upper(s) | 转小 / 大写 | 对中文无影响 | | 大小写 | WordCap(text) | 每个单词首字母大写 | 'hello power builder' → 'Hello Power Builder' | | 其他变形 | Reverse(s) | 反转字符顺序 | 'PowerBuilder' → 'redliuBrewoP' | | 构造 | Fill(字符, n) | 用字符循环填充到 n 长度 | Fill('*-',7) → '*-*-*-*' | | 构造 | Space(n) | 造 n 个空格 | | | 匹配 | Match(s, 模式) | 正则包含匹配 | 返回 boolean;要整串匹配请自己加 ^...$ | | 字符 | Asc(s) | 首字符的 Unicode 码位 | Asc('中') = 20013 | | 字符 | Char(n) | 码位转字符 | Char(65) = 'A',Char(20013) = '中' | | 编码 | Blob(s) / Blob(s, EncodingANSI!) | 字符串转 blob | 默认是 UTF-16LE,要 GBK 必须显式给编码 | | 判断 | IsNull(x) / IsNumber(s) / IsDate(s) / IsTime(s) | 类型与合法性判断 | 返回 boolean | | 格式化 | String(数据 {, 掩码}) | 按显示格式转字符串 | '0.0%' 会自动乘 100 |
三、十二段可运行示例
统一的运行前提:新建一个任意窗口 w_demo,在上面放一个按钮 cb_1;把下面每段代码贴进 cb_1 的 clicked 事件,运行窗口点按钮,结果就在 MessageBox 里。想一次性跑完,把十二段按顺序拼在一个事件里即可。所有代码都不连数据库、不依赖任何控件以外的东西。
3.1 长度:字符数 vs 字节数
- // ---------- 输入 ----------
- // 示例输入:一个中英混合串,用来对比字符数与字节数
- string ls_s
- ls_s = '中文ABC'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Len(字符数)=' + string(Len(ls_s)) // 5
- ls_msg = ls_msg + '~r~nLenA(GBK字节数)=' + string(LenA(ls_s)) // 7
- ls_msg = ls_msg + '~r~nLenW(已废弃,同Len)=' + string(LenW(ls_s)) // 5
- ls_msg = ls_msg + '~r~n纯英文=' + string(Len('PowerBuilder')) // 12
- // ---------- 输出 ----------
- MessageBox('长度', ls_msg)
复制代码
3.2 查找:Pos 与 LastPos
- // ---------- 输入 ----------
- // 示例输入:源串
- string ls_src
- ls_src = 'abcabcabc'
- // 示例输入:待查子串
- string ls_sub
- ls_sub = 'abc'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Pos(第1次)=' + string(Pos(ls_src, ls_sub)) // 1
- ls_msg = ls_msg + '~r~nPos(从第4位起)=' + string(Pos(ls_src, ls_sub, 4)) // 4
- ls_msg = ls_msg + '~r~nLastPos(最后一次)=' + string(LastPos(ls_src, ls_sub)) // 7
- ls_msg = ls_msg + '~r~n找不到=' + string(Pos('PowerBuilder', 'xyz')) // 0
- ls_msg = ls_msg + '~r~n大小写敏感=' + string(Pos('PowerBuilder', 'POWER')) // 0
- ls_msg = ls_msg + '~r~n中文按字符=' + string(Pos('中华人民共和国', '人民')) // 3
- // ---------- 输出 ----------
- MessageBox('查找', ls_msg)
复制代码
3.3 截取:Mid / Left / Right
- // ---------- 输入 ----------
- // 示例输入:源串
- string ls_src
- ls_src = 'PowerBuilder'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Mid(6,5)=' + Mid(ls_src, 6, 5) // Build
- ls_msg = ls_msg + '~r~nMid(6)不写长度=' + Mid(ls_src, 6) // Builder
- ls_msg = ls_msg + '~r~nLeft(5)=' + Left(ls_src, 5) // Power
- ls_msg = ls_msg + '~r~nRight(7)=' + Right(ls_src, 7) // Builder
- ls_msg = ls_msg + '~r~n越界=[' + Mid('abc', 99, 2) + ']' // [] 空串,不报错
- ls_msg = ls_msg + '~r~n中文=' + Mid('中华人民共和国', 3, 2) // 人民
- // ---------- 输出 ----------
- MessageBox('截取', ls_msg)
复制代码
3.4 替换:按位置替换,以及自己写「全部替换」
Replace 是新手最容易理解错的一个:它不是"把 abc 换成 xyz",而是"从第 start 个字符开始,连续 n 个字符换成 s2"。
- // ---------- 输入 ----------
- // 示例输入:一个日期串
- string ls_date
- ls_date = '2026-09-14'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = '按位置替换=' + Replace(ls_date, 1, 4, '****') // ****-09-14
- ls_msg = ls_msg + '~r~nn=0 等于插入=' + Replace('abcd', 3, 0, 'X') // abXcd
- ls_msg = ls_msg + '~r~ns2 为空等于删除=' + Replace('abcd', 2, 2, '') // ad
- // ---------- 输出 ----------
- MessageBox('替换', ls_msg)
复制代码
要做真正的"全部替换",得自己循环(Replace + Pos 推进)。下面这个 of_replaceall 建议直接放进你的公共 NVO:
- // ---------- 输入 ----------
- // 示例输入:源串、要被换掉的旧串、新串
- string ls_src
- ls_src = 'a-b-c-'
- string ls_old
- ls_old = '-'
- string ls_new
- ls_new = '/'
- // ---------- 处理 ----------
- string ls_ret
- long ll_p
- long ll_step
- ls_ret = ls_src
- ll_step = Len(ls_old)
- // 关键防线:old 是空串时 Len=0,下面会死循环,必须先挡掉
- if ll_step > 0 then
- ll_p = Pos(ls_ret, ls_old)
- do while ll_p > 0
- ls_ret = Replace(ls_ret, ll_p, ll_step, ls_new)
- // 从「替换后的位置 + 新串长度」之后继续找,避免替换结果里含 old 时死循环
- ll_p = Pos(ls_ret, ls_old, ll_p + Len(ls_new))
- loop
- end if
- // ---------- 输出 ----------
- MessageBox('全部替换', ls_ret) // a/b/c/
复制代码
3.5 去空白:Trim 只去空格,不去 Tab
这是本篇最值得记住的一条。Trim(s) 默认只删首尾的半角空格,Tab、换行、全角空格(U+3000)一概留着——从数据库或 Excel 粘过来的数据里全是这种东西。
- // ---------- 输入 ----------
- // 示例输入:前后各带一个 Tab 和三个空格(~t 是 PB 里的 Tab 转义)
- string ls_s
- ls_s = '~t PB ~t'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = '默认Trim=[' + Trim(ls_s) + ']' // [ PB ] Tab 还在
- ls_msg = ls_msg + '~r~n第二参TRUE=[' + Trim(ls_s, TRUE) + ']' // [PB] 干净
- // 全角空格 Char(12288) 也只认第二参 TRUE
- ls_msg = ls_msg + '~r~n全角空格=[' + Trim(Char(12288) + 'PB' + Char(12288), TRUE) + ']' // [PB]
- ls_msg = ls_msg + '~r~nLeftTrim=[' + LeftTrim(' PB') + ']' // [PB]
- ls_msg = ls_msg + '~r~nRightTrim=[' + RightTrim('PB ') + ']' // [PB]
- // ---------- 输出 ----------
- MessageBox('去空白', ls_msg)
复制代码
3.6 大小写、单词首字母与反转
- // ---------- 输入 ----------
- // 示例输入:源串
- string ls_src
- ls_src = 'PowerBuilder'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Lower=' + Lower(ls_src) // powerbuilder
- ls_msg = ls_msg + '~r~nUpper=' + Upper(ls_src) // POWERBUILDER
- ls_msg = ls_msg + '~r~nWordCap=' + WordCap('hello power builder') // Hello Power Builder
- ls_msg = ls_msg + '~r~nReverse=' + Reverse(ls_src) // redliuBrewoP
- // ---------- 输出 ----------
- MessageBox('大小写与反转', ls_msg)
复制代码
3.7 构造与转义
- // ---------- 输入 ----------
- // 示例输入:用来循环填充的图案
- string ls_pat
- ls_pat = '*-'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Fill(7)=' + Fill(ls_pat, 7) // *-*-*-* 循环填充到 7 个字符
- ls_msg = ls_msg + '~r~nFill(3)=' + Fill('ab', 3) // aba 不够就接着补
- ls_msg = ls_msg + '~r~nSpace=[' + Space(3) + ']' // [ ]
- ls_msg = ls_msg + '~r~n拼接=' + 'Power' + 'Builder' // PowerBuilder
- // 字面量里的单引号要写成 ~'
- ls_msg = ls_msg + '~r~n转义单引号=' + 'it~'s ok' // it's ok
- // ---------- 输出 ----------
- MessageBox('构造', ls_msg)
复制代码
3.8 模式匹配:Match 是「包含匹配」
Match 的语义是"串里有没有符合模式的一段",不是"整串是否等于这个模式"。要整串校验,必须自己补上 ^ 和 $——这两个锚点在 PB 里是生效的。
- // ---------- 输入 ----------
- // 示例输入:待校验的串
- string ls_v
- ls_v = 'PB12.5'
- // ---------- 处理 ----------
- string ls_msg
- if Match(ls_v, '^[A-Z]+[0-9.]+$') then
- ls_msg = '整串合规=TRUE'
- else
- ls_msg = '整串合规=FALSE'
- end if
- // 反例:前后各塞一个 x,锚点立刻拦住
- if Match('xPB12.5x', '^[A-Z]+[0-9.]+$') then
- ls_msg = ls_msg + '~r~n带x仍匹配=TRUE'
- else
- ls_msg = ls_msg + '~r~n带x仍匹配=FALSE' // FALSE,^ $ 确实生效
- end if
- // 不加锚点 = 包含匹配
- if Match('xPB12.5x', '[0-9]') then
- ls_msg = ls_msg + '~r~n无锚点含数字=TRUE' // TRUE
- else
- ls_msg = ls_msg + '~r~n无锚点含数字=FALSE'
- end if
- if Match('abc', '[0-9]') then
- ls_msg = ls_msg + '~r~nabc含数字=TRUE'
- else
- ls_msg = ls_msg + '~r~nabc含数字=FALSE' // FALSE
- end if
- // ---------- 输出 ----------
- MessageBox('模式匹配', ls_msg)
复制代码
3.9 字符与码位互转
- // ---------- 输入 ----------
- // 示例输入:一个汉字
- string ls_cn
- ls_cn = '中'
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = 'Asc(中)=' + string(Asc(ls_cn)) // 20013(Unicode 码位)
- ls_msg = ls_msg + '~r~nAsc(A)=' + string(Asc('A')) // 65
- ls_msg = ls_msg + '~r~nChar(65)=' + Char(65) // A
- ls_msg = ls_msg + '~r~nChar(20013)=' + Char(20013) // 中
- // ---------- 输出 ----------
- MessageBox('字符与码位', ls_msg)
复制代码
3.10 编码落地:Blob 默认是 UTF-16LE
写文件、发包、算签名之前必看。Blob(s) 不带编码参数时按 UTF-16LE 转,一个汉字 2 字节;想拿 GBK/ANSI 的字节,必须显式写 EncodingANSI!。
- // ---------- 输入 ----------
- // 示例输入:一个中英混合串,长度对比最直观
- string ls_s
- ls_s = '中A'
- // ---------- 处理 ----------
- blob lb_b
- string ls_msg
- lb_b = Blob(ls_s) // 默认 UTF-16LE:2 个字符 × 2 = 4 字节
- ls_msg = 'Blob默认=' + string(Len(lb_b)) + '字节' // 4
- lb_b = Blob(ls_s, EncodingANSI!) // GBK:'中' 2 字节 + 'A' 1 字节 = 3
- ls_msg = ls_msg + '~r~nBlob(ANSI)=' + string(Len(lb_b)) + '字节' // 3
- lb_b = ToAnsi('中文')
- ls_msg = ls_msg + '~r~nToAnsi(中文)=' + string(Len(lb_b)) + '字节' // 4
- // ---------- 输出 ----------
- MessageBox('编码与Blob', ls_msg)
复制代码
3.11 NULL:会「传染」的隐形杀手
这是本篇我最想让大家记住的坑,也是我自己实测时被它摆了一道的坑:字符串变量一旦是 NULL,任何跟它做的运算结果都是 NULL,而且会一路传染。我当时在一个汇总函数里把 NULL 拼了进去,结果整份返回值"消失",写出来的文件是空的,查了半天才反应过来。
- // ---------- 输入 ----------
- // 示例输入:一个被主动置成 NULL 的字符串
- string ls_n
- SetNull(ls_n)
- // ---------- 处理 ----------
- string ls_x
- string ls_msg
- ls_x = ls_n + 'abc' // 注意:拼接后 ls_x 整体变成 NULL,不是 'abc'
- ls_msg = 'IsNull(ls_n)=' + string(IsNull(ls_n)) // true
- ls_msg = ls_msg + '~r~n拼接后是NULL吗=' + string(IsNull(ls_x)) // true
- ls_msg = ls_msg + '~r~nLen(NULL)是NULL吗=' + string(IsNull(Len(ls_n))) // true
- ls_msg = ls_msg + '~r~nIsNumber(12.5)=' + string(IsNumber('12.5')) // true
- ls_msg = ls_msg + '~r~nIsNumber(12a)=' + string(IsNumber('12a')) // false
- ls_msg = ls_msg + '~r~nIsDate(2026-02-30)=' + string(IsDate('2026-02-30')) // false
- ls_msg = ls_msg + '~r~nIsDate(2026-02-28)=' + string(IsDate('2026-02-28')) // true
- // ---------- 输出 ----------
- MessageBox('NULL与判断', ls_msg)
复制代码
处理原则:从数据库取出来的可空字段,用之前先 if IsNull(x) then x = '',别让 NULL 流进拼接链;判断一律用 IsNull(),千万别直接把可能为 NULL 的变量拼进结果串。
3.12 String 格式化输出
- // ---------- 输入 ----------
- // 示例输入:一个日期
- date ld_d
- ld_d = 2026-09-14
- // 示例输入:一个金额(注意:绑定参数别用 decimal,这里仅演示格式化)
- dec ld_num
- ld_num = 1234.567
- // ---------- 处理 ----------
- string ls_msg
- ls_msg = '金额=' + String(ld_num, '#,##0.00') // 1,234.57
- ls_msg = ls_msg + '~r~n日期=' + String(ld_d, 'yyyy-mm-dd') // 2026-09-14
- ls_msg = ls_msg + '~r~n中文日期=' + String(ld_d, 'yyyy年mm月dd日') // 2026年09月14日
- ls_msg = ls_msg + '~r~n补零=' + String(7, '000') // 007
- // 注意:百分比掩码会自动乘 100
- ls_msg = ls_msg + '~r~n百分比=' + String(0.256, '0.0%') // 25.6%
- // ---------- 输出 ----------
- MessageBox('格式化', ls_msg)
复制代码
四、十三个必记的坑
- LenW / MatchW / ToAnsi / ToUnicode 已废弃:PB12.5 编译会报 C0185: Function 'xxx' is now obsolete,新代码别再用。
- NULL 会传染:NULL + 'abc' 结果还是 NULL,整串参与拼接的变量一旦为 NULL,最终结果整个变空。用 IsNull() 挡在前面。
- Pos 是 1 基、找不到返 0、区分大小写:写 if Pos(...) then 会把"位置 0"当假、"位置 1 及以上"当真,逻辑上刚好对,但拿返回值做下标时要小心 0。
- Replace 是按位置不是按内容:想"把所有 - 换成 /",Replace 做不到,得自己循环(见 3.4),且循环里 old 为空串会死循环。
- Trim 默认只去半角空格:Tab、换行、全角空格都不去;PB11.5+ 用 Trim(s, TRUE)。PB10 没有第二参数。
- Match 是包含匹配:Match('xPB12.5x','[0-9]') 返回 TRUE;要整串校验必须写 ^...$,实测这两个锚点是生效的。
- Blob(s) 默认 UTF-16LE:'中A' 是 4 字节;要 GBK 必须 Blob(s, EncodingANSI!)(3 字节)。跟外部系统对接时这是乱码第一嫌疑。
- String(boolean) 返回小写 true / false:不是 TRUE/FALSE,跟日志或接口字段比对时会踩。
- 百分比掩码自动乘 100:String(0.256,'0.0%') 得到 25.6%,别再自己乘一遍。
- Mid 越界不报错:返回空串,不会抛异常,所以"取不到值"时要自己判断,不能指望报错。
- 单引号要写成 ~':~ 是 PB 的转义符,写 Windows 路径时反斜杠不用转义,但 ~ 本身要写成 ~~。
- 循环里反复 s = s + x 会不断重新分配内存:几千次以上建议先用数组收集,最后一次拼完;更大量的文本直接写文件或用 DataStore。
- Left/Right/Mid 按字符算:中文不用担心截断半个字(这是 Unicode 版 PB 的好处),但按字节定长的老报文格式要用 LenA 配合字节函数。
五、与相近方案的对比
| 场景 | 推荐做法 | 说明 | | 纯字符处理(截取、查找、替换) | PowerScript 内置函数 | 零依赖、最快,本文全部内容 | | 需要处理字节、跨编码、加密签名 | 转 Blob + 显式 EncodingANSI!/EncodingUTF8! | 字符串自带字符集语义,Blob 没有,混用前先想清楚编码 | | DataWindow 表达式里处理字段 | DW 表达式函数 | Len/Left/Right/Mid/Pos/Trim/Upper/Lower/Fill/Space/WordCap/Match/Replace/String 在 DW 表达式里都有;但 Reverse、Blob 没有,且 DW 的 Trim 只有单参数版本 | | 需要分组捕获、贪婪控制等完整正则 | PBIDEA 的正则对象 | 内置 Match 只支持有限元字符集,做不了分组提取 | | 超大文本(几 MB 以上)拼装 | 写临时文件或用 DataStore | 字符串反复拼接会大量分配内存 |
六、可以往下延伸的三件事
- 定长报文按字节截断:对接银行、税务这类定长接口时,一个汉字占 2 字节,要用 LenA 算长度、配合字节向的 MidA/LeftA/RightA 处理(这几个函数在 PB 帮助里有,本文未做实机示例)。
- 写自己的 Split:PB 没有内置的字符串分割函数,用 Pos + Mid 循环是最稳的实现,注意分隔符为空串和末尾无分隔符这两种边界。
- 把常用处理收进公共 NVO:of_replaceall、of_trim_all(Trim(s, TRUE) 的 PB10 兼容实现)、of_isempty(同时挡掉空串和 NULL)这三个小函数,几乎每个项目都用得上。
实机验证情况:本文全部示例代码整理为 nvo_str_demo 对象,在 PB12.5 下完成——①pypower pbl import 导入 0 错误;②build rebuild --type full 全量编译 0 错误(仅 3 条 C0185 废弃函数提示,为演示 LenW/ToAnsi/ToUnicode 而有意保留);③PBVM 真机运行 .pbtest PASS,59 条断言全部命中(覆盖长度/查找/截取/替换/去空白/大小写/构造/转义/匹配/码位/编码/NULL/格式化十三组输出);④check_pb125.py 0 错误 0 提醒、check_enc.py 编码校验 bad=0。文中所有 // 后的输出值均取自实机运行结果,未实测的只有第六节提到的 MidA/LeftA/RightA(仅列于帮助,未做实机示例)。 |