马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-10-5 07:24 编辑
PBIDEA:用 uo_dbf 直接读写 DBF 数据表(字段定义、三段式写入与 {field,data} 读回结构,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:以 PowerBuilder 10 为基准编写,全部代码在 PB 10 与 PB 12.5 双版本下实机编译并运行通过。依赖 PBIDEA 的 uo_dbf(非可视用户对象)与 uo_json,PBIDEA 版本以维护方核验的当前最新版本为准。
2. 支持数据库:本文不依赖任何数据库。DBF 是 dBASE / FoxPro 时代的平面文件格式,uo_dbf 直接读写文件,不经过 DBMS。
3. 操作系统与环境要求:Windows 7+;需安装 PBIDEA 运行库(PbIdea.dll + Pbidea_cs.dll),且运行目录须能找到这两个 DLL。示例会在当前工作目录生成 demo_*.dbf 等文件,需要写权限。
4. 难度系数:★★★☆☆(三星。需要读者熟悉 PowerScript 基本语法、uo_json 的数组与路径取值;本文不涉及 DataWindow 与数据库)
5. 其它阅读说明:本文所有读数均来自真实 PBVM 运行输出(两版逐条对照),非文档摘抄。特别提醒:dbf2json 的返回结构不是数组而是 {field:[...], data:[...]} 对象,这一点与直觉差别最大,务必先看第三节。
一、DBF 是什么,为什么今天还要读它
DBF 是 dBASE III 时代的数据表格式,FoxPro / FoxBase / Visual FoxPro 都用它,文件就是一个带固定头部的平面文件——没有索引服务器、没有连接池、没有驱动,一个文件就是一张表。
在 PB 这类传统 MIS/ERP 项目里,DBF 至今仍会在这些场景冒出来:
- 老系统(DBCSVFP6.0 之类)导出的中间交换文件,只能是 DBF;
- 财务、药厂、交易所的国产老硬件,软件包之间用 DBF 传数据;
- 现场设备、串口转来的文本落库前先转成 DBF 再导入;
- 需要把查询结果交给一个只认 DBF 的第三方程序。
好消息是:读 DBF 并不需要装 FoxPro。PBIDEA 的 uo_dbf 已经把格式读写包在 PbIdea.dll 里,一条 CreateDBF + 三段式写入就能生成一个标准 DBF 文件,任何 dBASE/FoxPro 工具都能打开。
坏消息是:它的 API 极其薄,且有若干不直观的语义,不看实测很容易踩坑。本文把 uo_dbf 全部 8 个方法逐个跑过一遍,把真实行为写清楚。
二、uo_dbf API 全貌:只有 8 个方法
先看全部方法签名(取自本机 PBIDEA 导出源码 websuite.pbl/uo_dbf.sru):
| 方法 | 原型 | 作用 | | CreateDBF | function boolean CreateDBF(readonly string dbfName, readonly uo_json fields) | 按字段定义新建 DBF 文件 | | AppendRecord | function boolean AppendRecord() | 追加一个空行(不写盘) | | AppendRecord | function boolean AppendRecord(boolean fillSpace) | 同上,fillSpace 默认 false | | SetField | function boolean SetField(int field, readonly string value) | 按序号(从 1 起)给当前行赋值 | | SetField | function boolean SetField(readonly string field, readonly string value) | 按字段名给当前行赋值 | | CommitRecord | function long CommitRecord() | 把当前行真正写入文件,返回累计行数 | | CloseDBF | subroutine CloseDBF() | 关闭文件(析构时自动调用) | | dbf2json | function boolean dbf2json(readonly string dbfname, ref uo_json js, boolean lineno) | 读整个 DBF 到 uo_json | | dbf2json | function boolean dbf2json(readonly string dbfname, ref uo_json js, boolean lineno, boolean utf8) | 同上,第 4 参指定文件编码 |
uo_dbf 是非可视对象(源码里 from nonvisualobject),必须 create / destroy:
- // 演示步骤:声明变量 → create → 使用 → destroy
- // 运行环境:PB10 / PB12.5 均可,需 PbIdea.dll 在运行目录
- uo_dbf ld_dbf // DBF 文件操作对象
- uo_json lj_fields // 字段定义数组
- long ll_rows // 累计写入行数
- ld_dbf = create uo_dbf
- // …… 业务代码 ……
- destroy ld_dbf
复制代码
⚠️ 它不是 autoinstantiate 对象。写成全局变量 uo_dbf ld_dbf 后直接调方法,会得到 Null object reference。
三、字段定义怎么写:四个键,以及一个必须避开的写法
CreateDBF 的第二个参数是一个 uo_json 数组,每个元素描述一个字段,含四个键:
| 键 | 含义 | 取值 | | name | 字段名 | 字符串,建议大写(DBF 传统) | | type | 字段类型 | C 字符、N 数值、D 日期、L 逻辑(源码注释只写了 C/D/N,实测 L 也能建) | | length | 字段宽度 | 整数,字符型就是字节数 | | decimal | 小数位数 | 整数,实测对 N 型无效,详见第五节 |
3.1 推荐写法:一次 Parse
- // 演示步骤:把字段定义写成一段 JSON 文本,Parse 成 uo_json 数组
- // 运行环境:PB10 / PB12.5 均可
- uo_json lj_fields // 字段定义数组
- string ls_def // 字段定义 JSON 文本
- // 三条字段:编号 N 型、姓名 C 型 20 字节、金额 N 型 12 宽
- ls_def = '[{"name":"ID","type":"N","length":6,"decimal":0},'
- ls_def += '{"name":"NAME","type":"C","length":20,"decimal":0},'
- ls_def += '{"name":"AMT","type":"N","length":12,"decimal":2}]'
- lj_fields = create uo_json
- lj_fields.Parse(ls_def) // ← 关键:Parse 会真正把文本解析成数组结构
- MessageBox('字段数', '共 ' + String(lj_fields.ArrCount()) + ' 个字段')
- // ↑ 实测输出:共 3 个字段
- MessageBox('第一个', lj_fields.GetString('/0/name') + ' / ' + lj_fields.GetString('/0/type'))
- // ↑ 实测输出:ID / N
复制代码
3.2 ★坑 1:Set('{...}') 会把 JSON 文本当字符串存进数组
这是本对象最容易踩、后果最隐蔽的一个坑。很多人会这样写:
- // ❌ 错误写法:单参 Set 传 JSON 文本
- uo_json lj
- lj = create uo_json
- lj.Set('{"name":"ID","type":"N","length":6}')
- lj.Set('{"name":"NAME","type":"C","length":20}')
- // 看上去像建了个 2 元素的数组,实际不是
复制代码
实测:这样写完 ArrCount() 确实返回 2,IsArray() 也是 true,看起来一切正常。但取值全取不到:
- MessageBox('取值', lj.GetString('/0/name'))
- // ↑ 实测输出:空串(不是 ID)
复制代码
根因:uo_json 的单参 Set(readonly any val) 是「把这个 值追加进数组」,它不会解析 JSON 文本。把它的 ToString() 打出来就明白了:
- ["{"name":"ID","type":"N","length":6}","{"name":"NAME","type":"C","length":20}"]
复制代码
数组里装的是两个字符串字面量(JSON 字符串本身,双引号被转义了),不是两个对象。
正解有两条:
- // 正解 A:一次 Parse 整段(推荐,字段多时最省事)
- uo_json lj
- lj = create uo_json
- lj.Parse('[{"name":"ID","type":"N","length":6},{"name":"NAME","type":"C","length":20}]')
- // 正解 B:先建子对象,再用单参 Set 追加
- uo_json lj
- uo_json ljv
- lj = create uo_json
- ljv = create uo_json
- ljv.Set('name', 'ID')
- ljv.Set('type', 'N')
- ljv.Set('length', 6)
- lj.Set(ljv) // ← 传对象,不是传文本
- ljv.Set('name', 'NAME')
- ljv.Set('type', 'C')
- ljv.Set('length', 20)
- lj.Set(ljv)
- destroy ljv
- // 实测:Parse 与正解 B 得到的结构完全一致
复制代码
如果字段是动态的(用户自己配列),正解 B 更好用——可以抽成一个 of_addfield() 帮助方法反复调用。
四、写入:三段式,以及一个会崩进程的顺序错误
写入是标准的三段式:建表 → 逐行(AppendRecord → SetField → CommitRecord)→ 关闭。下面是完整可运行示例:
- // 演示步骤:把下面整段贴进任意窗口的按钮事件即可运行
- // 运行环境:PB10 / PB12.5 均可;需 PbIdea.dll;当前目录需写权限
- // 输出:当前目录生成 dbf_demo.dbf,含 2 行数据
- uo_dbf ld_dbf // DBF 操作对象
- uo_json lj_fields // 字段定义数组
- uo_json lj_read // 读回用的容器
- string ls_file // DBF 文件名(相对当前工作目录)
- string ls_msg // 汇总信息
- long ll_rows // CommitRecord 返回的累计行数
- ls_file = 'dbf_demo.dbf'
- ls_msg = ''
- // ① 建表:三个字段(编号 N、姓名 C、金额 N)
- lj_fields = create uo_json
- lj_fields.Parse('[{"name":"ID","type":"N","length":6,"decimal":0},{"name":"NAME","type":"C","length":20,"decimal":0},{"name":"AMT","type":"N","length":12,"decimal":2}]')
- ld_dbf = create uo_dbf
- ld_dbf.CreateDBF(ls_file, lj_fields)
- // ② 写第一行:追加空行 → 逐字段赋值 → 提交
- ld_dbf.AppendRecord()
- ld_dbf.SetField('ID', '1001')
- ld_dbf.SetField('NAME', '张三')
- ld_dbf.SetField('AMT', '123.45')
- ll_rows = ld_dbf.CommitRecord() // 返回 1
- // ③ 写第二行:SetField 也可以按序号传(从 1 起)
- ld_dbf.AppendRecord()
- ld_dbf.SetField(1, '1002')
- ld_dbf.SetField(2, '李四')
- ld_dbf.SetField(3, '88.00')
- ll_rows = ld_dbf.CommitRecord() // 返回 2
- ld_dbf.CloseDBF()
- ls_msg = '写入完成,累计 ' + String(ll_rows) + ' 行~r~n'
- // ④ 读回验证
- lj_read = create uo_json
- ld_dbf = create uo_dbf
- ld_dbf.dbf2json(ls_file, lj_read, true)
- ls_msg += '记录数 = ' + String(lj_read.ArrCount('data')) + '~r~n'
- ls_msg += '第 1 行 = ' + lj_read.GetString('/data/0/ID') + ' / ' + lj_read.GetString('/data/0/NAME') + '~r~n'
- ls_msg += '第 2 行 = ' + lj_read.GetString('/data/1/ID') + ' / ' + lj_read.GetString('/data/1/NAME')
- destroy ld_dbf
- destroy lj_fields
- destroy lj_read
- MessageBox('运行结果', ls_msg)
- // ↑ 实测输出:
- // 写入完成,累计 2 行
- // 记录数 = 2
- // 第 1 行 = 1001 / 张三
- // 第 2 行 = 1002 / 李四
复制代码
各方法的返回值语义(实测):
| 调用 | 返回 | 含义 | | CreateDBF | boolean | true=文件已生成;false=字段定义非法或目录不存在 | | AppendRecord | boolean | true=空行已开好;此时数据还没落盘 | | SetField | boolean | true=该字段赋值成功;false=字段名/序号不存在 | | CommitRecord | long | 累计写入行数(第 1 次返回 1,第 2 次返回 2),不是本次行号 | | dbf2json | boolean | true=解析成功;false=文件不存在或不是合法 DBF |
4.1 ★坑 2:不写 AppendRecord 直接 CommitRecord —— 直接崩进程
源码注释说「追加一行,实际数据不会写进 DBF,除非执行 CommitRecord」,读起来像是 CommitRecord 会自己开行。不是这样。
- // ❌ 危险写法:没有 AppendRecord 就 CommitRecord
- uo_dbf ld
- ld = create uo_dbf
- ld.CreateDBF('x.dbf', lj_fields)
- ld.SetField('A', 'hello')
- ld.CommitRecord() // ← 实测:进程直接崩,退出码 0xC0000005
复制代码
实测结果:PB10 与 PB12.5 都在这里访问冲突崩溃(exit code 3221225477 = 0xC0000005),TRY-CATCH 兜不住,整个 PBVM 进程被带走。同理,SetField 也会崩——因为内部没有「当前行」的概念。
这条是本对象最危险的坑:不崩的时候看起来一切正常(CommitRecord 返回 1),一崩就是整个应用退出,且只在数据量大时才发作(内部缓冲刚好没触发越界时)。
铁律:AppendRecord → SetField → CommitRecord 三步一组,一组都不能少;且每次 CommitRecord 前必须重新 AppendRecord。连续两次 CommitRecord 之间如果没重新 append,第二次同样崩。
4.2 ★坑 3:没提交的 AppendRecord 数据会随 CloseDBF 一起丢
- ld_dbf.AppendRecord()
- ld_dbf.SetField('A', 'lost')
- ld_dbf.CloseDBF() // 没 CommitRecord
- // 实测:文件存在,字段结构正确,但 data 数组长度 = 0,这行数据彻底丢了
复制代码
和「事务」的直觉一致:AppendRecord 只是把当前行搬进内存缓冲区,CommitRecord 才写文件。批量写时如果中途异常,已经 commit 的行留在文件里,没 commit 的那行消失——这一点在「写一半失败要回滚」的场景下要特别注意,DBF 没有回滚接口,只能重建文件。
4.3 AppendRecord(fillSpace) 的 fillSpace 实测无效
源码给了 AppendRecord(boolean fillSpace) 重载,注释说 fillSpace 默认 false,直觉是「true = 用空格填满字段宽度」。实测两种参数返回结果完全一致:
| 写法 | 未赋值的 B 字段读回 | | AppendRecord() | ""(空串) | | AppendRecord(true) | ""(空串) |
两种读回都是 {"recordno":1,"A":"aa","B":""}。结论:不要依赖这个参数,未赋值的 C 型字段读回一律是空串。
五、读回:返回的是 {field, data} 对象,不是数组
这是本对象第二个必须记住的点,也是我第一次实测时读错的地方。
dbf2json 的第二个参数是 ref uo_json,调用后这个对象被填成这样的结构:
- {
- "field": [
- {"name":"ID","type":"N","length":6,"decimal":0},
- {"name":"NAME","type":"C","length":20}
- ],
- "data": [
- {"recordno":1,"ID":1001,"NAME":"张三","QTY":12},
- {"recordno":2,"ID":1002,"NAME":"LiSi","QTY":99}
- ]
- }
复制代码
也就是说:
- 顶层是对象不是数组 —— ArrCount() 返回 0,IsArray() 返回 false;
- 记录在 data 键下 —— 要用 ArrCount('data') 取长度;
- 字段元信息在 field 键下 —— 可以据此还原列类型,不必自己记;
- 行号在 recordno,从 1 起。
正确读法:
- // 演示步骤:先取 data 数组长度,再按 /data/<行号>/<字段名> 取值
- // 运行环境:PB10 / PB12.5 均可
- uo_json lj_read // 读回容器
- long ll_n // 记录条数
- long ll_i // 循环下标
- lj_read = create uo_json
- ld_dbf.dbf2json('dbf_demo.dbf', lj_read, true)
- ll_n = lj_read.ArrCount('data') // ← 不是 lj_read.ArrCount()
- ls_msg = '共 ' + String(ll_n) + ' 行~r~n'
- for ll_i = 1 to ll_n
- ls_msg += lj_read.GetString('/data/' + String(ll_i - 1) + '/NAME') + '~r~n'
- next
- destroy lj_read
复制代码
路径下标从 0 起(/data/0/ 是第 1 行),这与 uo_json 的通用规则一致;但 recordno 字段的值从 1 起,两者别混。
5.1 lineno 与 utf8 两个开关
- lineno:true 时每行多一个 recordno 键;false 时没有。实测 dbf2json(path, js, false) 返回 true、记录数正常,只是 GetString('/data/0/recordno', 'NORECNO') 取默认值 NORECNO。
- utf8:第 4 参,声明文件编码,默认 false(即按 GBK/ANSI 读)。这个开关实测与直觉相反:
| 调用 | 读出的中文 | | dbf2json(path, js, true, false) | 张三丰(正确) | | dbf2json(path, js, true, true) | ??????(六个问号) |
uo_dbf 写出来的文件是 GBK/ANSI,所以读的时候 utf8 传 false。传 true 会把 GBK 字节当 UTF-8 解,中文全部变成问号,且返回值仍然是 true——不报错,静默出错。
5.2 ★坑 4:decimal 定义对 N 型无效,小数会被吃掉
字段定义里 decimal 是白写的。实测:
- lj_fields.Parse('[{"name":"AMT","type":"N","length":12,"decimal":2}]')
- ld_dbf.CreateDBF('x.dbf', lj_fields)
- ld_dbf.AppendRecord()
- ld_dbf.SetField('AMT', '123.45')
- ld_dbf.CommitRecord()
- // 读回:AMT = 123 ← 小数部分没了
复制代码
读回的 field 元信息里 "decimal":0,值也确实是 123(GetDouble 同样得 123)。length 是生效的(12 就是 12),只有 decimal 丢失。
这不是 bug,是 DBF 格式本身如此:dBASE 的 N 型字段在文件里是定长右对齐的 ASCII 文本(" 123.45"),小数点只是这段文本里的一个字符。uo_dbf 显然按整数解析这段文本再转成 JSON 数字,于是小数点后被丢弃。
如果你的业务真的有金额小数,两个办法:
- 把 N 型当 C 型用:{"name":"AMT","type":"C","length":12,"decimal":0},然后自己在 PB 里 Double() 转换。读回是字符串 "123.45",精度不丢。
- 导出时用字符串拼接:C 型存 "123.45",目标系统(FoxPro 侧)自己再转。
顺带一个坑:N 型读回的是 JSON 数字,不是字符串。GetString('/data/0/AMT') 能拿到 "123",但如果值本身是 0,或者你用 GetLong 去接一个本该是小数的值,类型转换会掩盖问题——读 N 型统一用 GetDouble,C 型统一用 GetString。
5.3 ★坑 5:日期型读回被转成 yyyy/mm/dd,且空值是空串
- lj_fields.Parse('[{"name":"CDATE","type":"D","length":8,"decimal":0}]')
- // ... 写入 '20261005' ...
- // 读回:{"CDATE":"2026/10/05"} ← 不是 "20261005"
复制代码
DBF 的 D 型是 8 字节 YYYYMMDD,但 uo_dbf 读回时会格式化成 yyyy/mm/dd。如果你要拿这个值回写给别的系统,必须先 Replace(日期串, '/', '') 去掉斜杠。
更糟的是往 D 型塞非法日期:
- ld_dbf.SetField('D1', 'notadate') // 返回 true,不报错
- // 读回:{"D1":"nota/da/te"} ← 8 个字符被硬塞进去,还插了斜杠
复制代码
SetField 返回 true,因为从它的角度「8 个字符,放得下」。所以 D 型必须自己保证格式:String(Date(...,'yyyymmdd')) 之类,写之前校验长度和数字性。
D 型空值倒是正常:写 '' 读回 ""。
六、边界与失败行为实测表
下表全部是双版本实跑结果,PB10 与 PB12.5 行为完全一致:
| 场景 | 实测行为 | | CreateDBF 传空数组 [] | 返回 false,不生成文件 | | 字段数组缺 name 键 | 返回 false,不生成文件(其它键齐也不行) | | 字段 type 传非法值 "X" | 返回 true 并生成文件,field 里原样保留 "type":"X",不校验 | | 目标目录不存在 | 返回 false,不生成文件 | | SetField 字段名不存在 | 返回 false,当次赋值被忽略,其余字段正常写 | | SetField 序号越界(如第 9 个字段,表中只有 1 个) | 返回 false,同上 | | C 型写入超长值(字段 length=4,写 ABCDEFGH) | 返回 false,该字段留空,不是截断 | | 重复字段名(两个都叫 A) | CreateDBF 返回 true 能建;读回 JSON 里出现两个 "A" 键,后者覆盖前者(A":"v1" 与 A":"" 并存,解析时只认一个) | | L 逻辑型({"type":"L","length":1}) | 能建能写;读回 field 里 type 变成 "C",值保留写入的 "T" | | N 型写负数 -456 | 正常,读回 -456 | | N 型写空串 '' | 返回 true,读回 0(不是空串,也不是空) | | 对已存在的文件再 CreateDBF | 返回 true,但新表头覆盖旧表头、数据全清空(实测旧文件 1 行 → 重开后 data 长度为 0) | | dbf2json 读不存在的文件 | 返回 false,目标 uo_json 保持空对象 {} | | 未 CreateDBF 就 AppendRecord / SetField | 全部返回 false,CommitRecord 返回 0,不崩(与「有表但不 Append」不同,那种会崩) | | dbf2json 取不存在的字段 | 返回传入的默认值(GetString(path, 'DEF') → DEF) |
三条最需要记住的:
- CreateDBF 覆盖同名文件不报错。它返回 true,看起来像「建好了」,但你之前写的数据全没了。要做「追加导出」必须先判断 FileExists(),走另一条路(读出旧数据 → 重建 → 一起写回)。
- C 型超长赋值返回 false 而不是截断。这其实比截断好——至少你知道数据丢了。但要注意:只看 CreateDBF 的返回值不够,每行至少要检查 CommitRecord 的返回值是不是递增。
- 类型不校验。type 传 "X" 也能建文件,FoxPro 那边打开可能报错。字段定义是给外部工具看的,合法性要自己保证。
七、完整边界对照演示
把上面那些坑逐条跑一遍,方便你对照自己的代码:
- // 演示步骤:逐条验证边界行为,观察每个返回值
- // 运行环境:PB10 / PB12.5 均可;当前目录需写权限
- // 输出:弹窗列出每条边界的真实返回值
- uo_dbf ld
- uo_json lj
- uo_json ljr
- string ls_r
- boolean lb_b
- long ll_n
- ls_r = ''
- // ① 空字段数组 → 建不出文件
- lj = create uo_json
- lj.Parse('[]')
- ld = create uo_dbf
- lb_b = ld.CreateDBF('b1.dbf', lj)
- ls_r += '空数组建表 = ' + String(lb_b) + '(文件存在=' + String(FileExists('b1.dbf')) + ')~r~n'
- ld.CloseDBF()
- destroy ld
- destroy lj
- // ② 非法 type → 居然能建
- lj = create uo_json
- lj.Parse('[{"name":"A","type":"X","length":4,"decimal":0}]')
- ld = create uo_dbf
- lb_b = ld.CreateDBF('b2.dbf', lj)
- ls_r += '非法type建表 = ' + String(lb_b) + '(文件存在=' + String(FileExists('b2.dbf')) + ')~r~n'
- ld.CloseDBF()
- destroy ld
- destroy lj
- // ③ 未建表直接写 → 全部 false,不崩
- ld = create uo_dbf
- lb_b = ld.AppendRecord()
- ls_r += '未建表追加 = ' + String(lb_b) + '~r~n'
- lb_b = ld.SetField('A', 'x')
- ls_r += '未建表赋值 = ' + String(lb_b) + '~r~n'
- ld.CloseDBF()
- destroy ld
- // ④ C 型超长 → false,字段留空
- lj = create uo_json
- lj.Parse('[{"name":"S","type":"C","length":4,"decimal":0}]')
- ld = create uo_dbf
- ld.CreateDBF('b4.dbf', lj)
- ld.AppendRecord()
- lb_b = ld.SetField('S', 'ABCDEFGH')
- ls_r += '超长赋值 = ' + String(lb_b) + '~r~n'
- ld.CommitRecord()
- ld.CloseDBF()
- ljr = create uo_json
- ld = create uo_dbf
- ld.dbf2json('b4.dbf', ljr, true)
- ls_r += '读回 S = [' + ljr.GetString('/data/0/S') + '](长度' + String(Len(ljr.GetString('/data/0/S'))) + ')~r~n'
- destroy ljr
- destroy ld
- destroy lj
- // ⑤ 覆盖已有文件 → 数据被清空
- lj = create uo_json
- lj.Parse('[{"name":"A","type":"C","length":4,"decimal":0}]')
- ld = create uo_dbf
- ld.CreateDBF('b5.dbf', lj)
- ld.AppendRecord()
- ld.SetField('A', 'old')
- ld.CommitRecord()
- ld.CloseDBF()
- lj = create uo_json
- lj.Parse('[{"name":"B","type":"C","length":4,"decimal":0}]')
- ld = create uo_dbf
- lb_b = ld.CreateDBF('b5.dbf', lj)
- ls_r += '覆盖建表 = ' + String(lb_b) + '~r~n'
- ld.CloseDBF()
- ljr = create uo_json
- ld = create uo_dbf
- ld.dbf2json('b5.dbf', ljr, true)
- ll_n = ljr.ArrCount('data')
- ls_r += '覆盖后记录数 = ' + String(ll_n) + '~r~n'
- destroy ljr
- destroy ld
- destroy lj
- // ⑥ 非法日期照收不误
- lj = create uo_json
- lj.Parse('[{"name":"D1","type":"D","length":8,"decimal":0}]')
- ld = create uo_dbf
- ld.CreateDBF('b6.dbf', lj)
- ld.AppendRecord()
- lb_b = ld.SetField('D1', 'notadate')
- ls_r += '非法日期赋值 = ' + String(lb_b) + '~r~n'
- ld.CommitRecord()
- ld.CloseDBF()
- ljr = create uo_json
- ld = create uo_dbf
- ld.dbf2json('b6.dbf', ljr, true)
- ls_r += '读回 D1 = ' + ljr.GetString('/data/0/D1') + '~r~n'
- destroy ljr
- destroy ld
- destroy lj
- // ⑦ 读不存在的文件
- ljr = create uo_json
- ld = create uo_dbf
- lb_b = ld.dbf2json('not_exist.dbf', ljr, true)
- ls_r += '读不存在 = ' + String(lb_b) + ',容器 = ' + ljr.ToString() + '~r~n'
- destroy ljr
- destroy ld
- MessageBox('边界实测', ls_r)
- // ↑ 实测输出:
- // 空数组建表 = false(文件存在=false)
- // 非法type建表 = true(文件存在=true)
- // 未建表追加 = false
- // 未建表赋值 = false
- // 超长赋值 = false
- // 读回 S = [](长度0)
- // 覆盖建表 = true
- // 覆盖后记录数 = 0
- // 非法日期赋值 = true
- // 读回 D1 = nota/da/te
- // 读不存在 = false,容器 = {}
复制代码
八、与其它方案的选型
| 方案 | 适用 | 优点 | 缺点 | | uo_dbf(PBIDEA) | 读写 DBF 交换文件 | 不装 FoxPro;8 个方法够用;PB 直接对接 | 只支持平面 DBF,无索引/关系/备注文件;decimal 无效;中文只走 GBK | | FoxPro / Visual FoxPro ODBC | 需要完整 DBF 特性 | 支持索引、备注、关系 | 要装 FoxPro 或 ODBC 驱动,部署重;中文代码页易配错 | | 文本 CSV(uo_csv 或 dw.SaveAs) | 只为交换数据,不在乎格式 | 双方都能读;无二进制结构 | 逗号/引号/编码坑;丢类型(N 型变字符串) | | uo_kv / uo_config | 自己的中间格式 | 完全可控 | 外部工具读不了,通用性差 |
选型建议:如果对方明确说「要 DBF」,就用 uo_dbf,并主动确认三件事——① 小数字段能否改成 C 型自己转;② 中文是不是 GBK;③ 要不要行号。确认完再动手,能省掉后面 90% 的扯皮。
如果只是「导出个文件给同事导进 Excel」,优先 CSV——DBF 的中文和类型转换坑不值得趟。
九、PB12.5 差异
本文全部 API 调用与边界行为在 PB10 与 PB12.5 双版本下逐条对跑,读数逐字节一致。未发现版本差异:
- 8 个方法的签名、返回值语义、崩溃行为完全相同;
- {field, data} 返回结构相同,lineno / utf8 两个开关行为相同;
- 5.2 的 decimal 丢失、5.3 的日期格式化与非法日期硬塞、6. 的全部边界行为两版一致;
- 4.1 的「不 AppendRecord 直接 CommitRecord 崩溃」两版都是 0xC0000005。
唯一的环境差异是依赖库:uo_dbf 与 uo_json 都通过 SYSTEM library "PbIdea.dll" 调用外部函数,所以运行时必须让 PbIdea.dll 和 Pbidea_cs.dll 都在可执行文件的搜索路径里(PB 的 Path 环境变量或 exe 同目录)。缺任何一个都会在 create uo_json 时报「Error opening DLL library PbIdea.dll」。这与 PB 版本无关,PB10 和 PB12.5 一样。
十、坑清单速查
- ★不 AppendRecord 直接 CommitRecord / SetField 会崩进程(0xC0000005),TRY-CATCH 兜不住。三段式一组都不能少,每次 commit 前都要重新 append。
- ★dbf2json 返回 {field:[...], data:[...]} 对象,不是数组。ArrCount() 得 0,要用 ArrCount('data'),路径是 /data/<行号>/<字段名>,下标从 0 起。
- ★decimal 对 N 型无效,"123.45" 写进去读出来是 123。有小数用 C 型自己转。
- ★utf8 第 4 参与直觉相反:uo_dbf 写的文件是 GBK,读的时候要传 false;传 true 中文全变问号,且返回值仍是 true(静默出错)。
- ★Set('{...}') 不会解析 JSON 文本,会把整段当字符串存进数组。要么 Parse 整段,要么先建 uo_json 子对象再 Set(对象)。
- ★CreateDBF 覆盖同名文件返回 true 且清空数据。导出前必须 FileExists() 判断。
- ★D 型读回是 yyyy/mm/dd,不是 YYYYMMDD,回写前要 Replace(日期串,'/','')。
- D 型不校验日期格式:SetField('D1','notadate') 返回 true,读回 nota/da/te。
- CommitRecord 返回累计行数,不是本次行号;可以不检查,但要检查它是否在递增(不递增说明上一行没写成功)。
- AppendRecord(fillSpace) 参数无效,true / false 行为一致,未赋值字段读回一律空串。
- C 型超长赋值返回 false 而不是截断,该字段留空。
- N 型空串写入读回 0;负数正常。
- type 不校验,传 "X" 也能建文件,出错时由 FoxPro 侧报。
- 重复字段名能建能写,但读回的 JSON 里有重复键,解析时只认一个。
- L 型读回 type 变成 "C",值保留。
- uo_dbf 非 autoinstantiate,必须 create/destroy。
- 必须 CloseDBF,未 commit 的数据会随关闭丢失;析构里也会自动调一次,但显式关闭更保险。
- PbIdea.dll 与 Pbidea_cs.dll 都要在运行路径,缺则在 create uo_json 时报错。
十一、实机验证情况
| 验证项 | PB 10 | PB 12.5 | | 源码导入(pypower pbl import) | 0 错 | 0 错 | | 全量编译(rebuild --type full) | 0 错 | 0 错 | | PBVM 运行(.pbtest) | 通过 | 通过 | | 断言数 | 主用例 60 项 + 边界 14 例 | 同左 | | 两版诊断输出比对 | 逐字节一致 | 同左 |
验证方式说明:
- 写入链路:建表 → 追加 2 行 → 提交,文件从 32 字节(仅表头)增长到 231 字节,磁盘文件真实落盘;
- 读回链路:dbf2json 读出 recordno 1/2、ID 1001/1002、NAME 张三/LiSi、中文 MEMO 完整;
- 边界矩阵:14 个独立用例(decimal、utf8 开关、fillSpace、跳过 AppendRecord、覆盖文件、负数/空值、逻辑型、重复字段名、非法 type、缺 name、非法目录、非法日期、D 型空值、读不存在文件),每例单独跑一次(崩溃隔离),lineno 开关另测;
- 崩溃点定位:4.1 的崩溃用「一例一次独立运行」复现,两版退出码均为 3221225477(0xC0000005);
- 未实测部分:本对象无未实测段落。所有涉及文件系统的行为都在 Windows 本机真实文件上跑过;DBF 头结构的字节级校验(对照 FoxPro 打开验证)本机没有 FoxPro 环境,未做——文中关于「DBF 格式本身不含小数类型定义」的解释来自格式规范与读回元信息(field[].decimal 恒为 0)两条证据推断,而非 FoxPro 侧交叉验证。
本文所有「实测」标记的读数都可复现。
|