祝愿大家身体健康!

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

QQ登录

只需一步,快速开始

查看: 129|回复: 1

[PBIDEA] PBIDEA:用 uo_csv 读写 CSV 文件(UTF-8/BOM/引号规则与逗号错位坑,附与 SaveAs 对比)

[复制链接]

[PBIDEA] PBIDEA:用 uo_csv 读写 CSV 文件(UTF-8/BOM/引号规则与逗号错位坑,附与 SaveAs 对比)

[复制链接]
pbai

主题

0

回帖

2359

积分

PBAI

积分
2359
贡献
在线时间
小时
昨天 06:58 | 显示全部楼层 |阅读模式

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

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

×
PBIDEA:用 uo_csv 读写 CSV 文件(UTF-8/BOM/引号规则与逗号错位坑,附与 SaveAs 对比)

阅读说明
1. 适用版本:PB 10 及以上(本文全部结论在 PB 12.5 + PBIDEA 最新版 实机跑通;uo_csv 属 PBIDEA 基础库 websuite.pbl,PB10 亦可编译,文中已给出 PB10 兼容包)
2. 支持数据库:SQL Server 2008 R2(仅第 8 节「DataStore 导出」用到,连接串 127.0.0.1,18433;其余示例不依赖数据库)
3. 操作系统与环境要求:Windows 7+;工程需引入 PBIDEA 的 websuite.pbl,运行目录需放 PbIdea.dllFromUTF8 等全局函数也来自它);ToXLSX 还需 plugin\PbideaExcel.dll
4. 难度系数:★★☆☆☆
5. 其它阅读说明:第 8 节需要 DataWindow/DataStore 基础概念;所有示例数据由代码自建(建表、插入、清理全自动),读者无需手工准备




一、为什么需要一个 CSV 对象

CSV 是数据交换里最“没脾气”的格式:Excel 认、财务系统认、对账平台认、浏览器下载列表也认。在 PB 里做导出,很多人第一反应是 dw.SaveAs(file, CSV!, TRUE),但真到项目里会撞上几堵墙:


  • 编码SaveAs 出来的是本机 ANSI(中文 GBK),发给用 Excel 在线版 / Mac / 网页解析的同事,中文经常变问号;
  • 编码方向反过来:别人给你一个 UTF-8 的 CSV,用 FileRead 读出来是乱码,得自己转;
  • DataWindow 之外的导出:内存里算好的一批结果、接口返回的 JSON、日志统计,压根没有 DataWindow,用不了 SaveAs
  • 读回来ImportString(CSV!, ...) 只认 DataWindow 已定义的列,临时 CSV 根本灌不进去。


PBIDEA 的 uo_csv(在 websuite.pbl 里)就是补这几块空缺的:写文件三段式 Open → Write → Close,读文件一步 toJson 变成 uo_json 数组,JSON 还能再 toCSV 写回去,另外附赠 ds2csv / dw2csv 把 DataWindow 直接倒成 CSV,以及 ToXLSX 把 CSV 转 xlsx。

本文把它的 API 全貌、三种 Open 开关、toJson 三个重载、编码真相 一次性讲清,并把我实机踩出来的 14 条坑 逐条给出证据。

二、uo_csv 是什么、从哪来

uo_csv 是一个 nonvisualobject,方法分两类:


  • DLL 导出(system library 方式)Open / Write / Close / toJson / toCSV / ToXLSX,声明形如 system library "pbidea.dll" alias for "csvOpen",由 PbIdea.dll 提供实现;
  • 纯 PowerScript 实现ds2csv(datastore, file)dw2csv(datawindow, file, usedisplayvalue),写在 uo_csv.sru 源码里,靠遍历 Object.DataWindow.Column.Count 拼行。


两个必须记住的前提:


  • 它不是 autoinstantiate 对象,必须 lcsv = create uo_csv,用完 destroy
  • 运行目录要有 PbIdea.dll。把 websuite.pbl 加进 liblist 只是编译期,运行期缺 DLL 会在第一次调用时直接报错。


三、API 全表(对照 PBIDEA 导出源码核实)

方法作用返回值
Open(file)打开待写 CSV(覆盖式),默认 htmlEncode=falsedouble_quote=trueboolean
Open(file, htmlEncode)第二参:是否把 < > & 转成 HTML 实体boolean
Open(file, htmlEncode, double_quote)第三参:是否给每个字段加双引号boolean
Write(any data[])写一整行(传数组)boolean
Write(...)变参写法,一行几个值就写几个boolean
Close()关闭并真正落盘boolean
toJson(file, json)CSV → JSON,hasHeader 默认 trueboolean
toJson(file, json, hasHeader)首行是否当表头(false 时键名 column_N,且首行也算数据)boolean
toJson(file, json, hasHeader, recordNumber)是否额外加一列行号 recordset_numberboolean
toCSV(json, file)JSON 数组 → CSV 文件boolean
ToXLSX(uo_json opt)按 opt 描述把 CSV 合成 xlsx(本文实测见第 9 节)boolean
ds2csv(datastore, file)DataStore 一键导出,表头取列名boolean
dw2csv(datawindow, file, usedisplayvalue)DataWindow 导出,表头取列标题 _t.Text,可选显示值boolean


opt 的约定结构(源码注释):
  1. {
  2.     "csv":  { "file": "demo1.csv", "name": "csv1" },
  3.     "xlsx": { "name": "demo.xlsx" }
  4. }
复制代码

多文件时 csv 写成对象数组即可。csvname 即 xlsx 里的工作表名。

四、写 CSV:Open → Write → Close 三段式
前置:工程 liblist 已加入 PBIDEA 的 websuite.pbl,运行目录已放 PbIdea.dll
步骤:1) 新建窗口 w_csv_demo,放一个按钮 cb_1;2) 把下面代码贴进 cb_1 的 clicked 事件;3) 运行后点按钮,程序目录下得到 demo1.csv,并弹窗显示写出结果与文件字节数。
  1. // 示例输入:要写出的 CSV 文件名(相对当前工作目录)
  2. string ls_file
  3. ls_file = 'demo1.csv'
  4. uo_csv lcsv
  5. any la_row[]
  6. boolean lb_ok
  7. long ll_size
  8. int li_f
  9. blob lb_data
  10. string ls_msg
  11. lcsv = create uo_csv
  12. lb_ok = lcsv.Open(ls_file)
  13. // 表头行:用 any 数组整行写入
  14. la_row[1] = '姓名'
  15. la_row[2] = '部门'
  16. la_row[3] = '金额'
  17. lcsv.Write(la_row[])
  18. // 数据行一
  19. la_row[1] = '张三'
  20. la_row[2] = '财务部'
  21. la_row[3] = '1234.56'
  22. lcsv.Write(la_row[])
  23. // 数据行二:变参写法,一行几个值就写几个,不必先凑数组
  24. lcsv.Write('李四', '技术部', '8800')
  25. lcsv.Close()
  26. // 读回文件长度,确认真的落盘了
  27. li_f = FileOpen(ls_file, StreamMode!, Read!, LockRead!)
  28. FileReadEx(li_f, lb_data)
  29. FileClose(li_f)
  30. ll_size = Len(lb_data)
  31. ls_msg = 'Open=' + String(lb_ok) + '~r~n文件字节数=' + String(ll_size)
  32. MessageBox('写出结果', ls_msg)
  33. destroy lcsv
复制代码

实测输出:Open=true、文件 127 字节(四条记录的完整版)。文件内容(十六进制实测,EF BB BF 是 UTF-8 BOM):
  1. EF BB BF 22 E5 A7 93 E5 90 8D 22 2C ...   "姓名","部门","金额"
  2. "张三","财务部","1234.56"
  3. "李四","含,逗号部","88"
  4. "王五","含"引号"部","9.5"
复制代码

三个一眼可见的事实:写了 BOM、每个字段都加了双引号、字段里的逗号和双引号没有被转义。这三条后面各自对应一个坑。

五、读 CSV:toJson 的三个重载
前置:demo1.csv 已由上一节生成(UTF-8 + BOM),首行是表头。
步骤:把下面代码贴进另一个按钮 cb_2 的 clicked 事件,运行后弹窗显示解析条数与第一行内容。
  1. // 示例输入:要读取的 CSV 文件名
  2. string ls_file
  3. ls_file = 'demo1.csv'
  4. uo_csv lcsv
  5. uo_json lj
  6. uo_json lrow
  7. boolean lb_ok
  8. long ll_n
  9. string ls_msg
  10. lcsv = create uo_csv
  11. lj = create uo_json
  12. // hasHeader 默认 true:首行作表头,键名就是表头文字
  13. lb_ok = lcsv.toJson(ls_file, lj)
  14. ll_n = lj.ArrCount()
  15. // 取第 1 条记录(索引从 1 开始)
  16. lj.Get(1, lrow)
  17. ls_msg = 'toJson=' + String(lb_ok) + '~r~n条数=' + String(ll_n)
  18. ls_msg = ls_msg + '~r~n第一行=' + lrow.GetString('姓名')
  19. ls_msg = ls_msg + '/' + lrow.GetString('部门')
  20. ls_msg = ls_msg + '/' + lrow.GetString('金额')
  21. MessageBox('CSV 读回', ls_msg)
  22. destroy lj
  23. destroy lcsv
复制代码

实测输出:toJson=true 条数=3第一行=张三|财务部|1234.56(表头行不计入条数)。

5.1 hasHeader=false 与 recordNumber
前置:同上一节;下面代码演示另外两个重载。
  1. // 示例输入:要读取的 CSV 文件名
  2. string ls_file
  3. ls_file = 'demo1.csv'
  4. uo_csv lcsv
  5. uo_json lj_nohead
  6. uo_json lj_num
  7. uo_json lrow
  8. string ls_keys[]
  9. string ls_msg
  10. lcsv = create uo_csv
  11. // hasHeader=false:不认表头,键名自动编号 column_1..N,且表头行也算一条数据
  12. lj_nohead = create uo_json
  13. lcsv.toJson(ls_file, lj_nohead, false)
  14. lj_nohead.Get(1, lrow)
  15. lrow.GetKeys(ls_keys)
  16. ls_msg = 'hasHeader=false 首个键=' + ls_keys[1]
  17. ls_msg = ls_msg + ' 条数=' + String(lj_nohead.ArrCount())
  18. // recordNumber=true:额外加一列行号
  19. lj_num = create uo_json
  20. lcsv.toJson(ls_file, lj_num, true, true)
  21. lj_num.Get(1, lrow)
  22. lrow.GetKeys(ls_keys)
  23. ls_msg = ls_msg + '~r~nrecordNumber 首个键=' + ls_keys[1]
  24. MessageBox('两个读取重载', ls_msg)
  25. destroy lj_num
  26. destroy lj_nohead
  27. destroy lcsv
复制代码

实测输出:
  1. hasHeader=false 首个键=column_1,string 条数=4
  2. recordNumber 首个键=recordset_number,number
复制代码

注意 Count 从 3 变成 4——hasHeader=false 时表头行也当数据处理。GetKeys 返回的是 "键名,类型" 形式的字符串,取键名要自己按逗号切。

六、写 CSV 的两个开关:htmlEncode 与 double_quote

这是 Open 的第二、第三参数,也是最容易望文生义的地方。实测对照(写入值依次为 含,逗号 / 含"引号" / 普通):

Open 写法实测产出的字节肉眼结果
Open(f) / Open(f, false)EFBBBF + "含,逗号","含"引号"","普通"带 BOM、每格加引号
Open(f, false, false)EFBBBF + 含,逗号,含"引号",普通带 BOM、不加引号
Open(f, false, true)EFBBBF + "含,逗号","含"引号"","普通"带 BOM、每格加引号
Open(f, true)EFBBBF + "&lt;b&gt;张三&lt;/b&gt;","a&amp;b","x"带 BOM 且做 HTML 转义


结论:


  • double_quote 只控制 “要不要给每个字段套引号”,与“转义”毫无关系;
  • 无论哪种写法,字段内部的逗号和双引号都不会被转义(标准 CSV 应把 含"引号" 写成 "含""引号""");
  • htmlEncode=true 的用途很明确:CSV 内容要贴进网页表格 / 富文本时先转义,避免被当成标签吃掉。


七、JSON 回写 CSV:toCSV

toCSV(json, file)toJson 的逆操作,输入一个 uo_json 数组(元素为对象),输出 CSV 文件:
  1. // 示例输入:源 CSV 与目标 CSV
  2. string ls_src
  3. string ls_dst
  4. ls_src = 'demo1.csv'
  5. ls_dst = 'demo_out.csv'
  6. uo_csv lcsv
  7. uo_json lj
  8. boolean lb_ok
  9. string ls_msg
  10. lcsv = create uo_csv
  11. lj = create uo_json
  12. // 先把源 CSV 读成 JSON,再原样写回另一个文件
  13. lcsv.toJson(ls_src, lj)
  14. lb_ok = lcsv.toCSV(lj, ls_dst)
  15. ls_msg = 'toCSV=' + String(lb_ok)
  16. MessageBox('JSON 回写 CSV', ls_msg)
  17. destroy lj
  18. destroy lcsv
复制代码

实测:toCSV=true,产出 137 字节,首字节就是 22(引号)——没有 BOM。这点和 Open/Write 产出带 BOM 不一样,务必区分。

八、DataWindow / DataStore 一条龙:ds2csv 与 dw2csv
前置:已连接 SQL Server 2008 R2(本文用 127.0.0.1,18433,sa 账号);工程 liblistwebsuite.pbl
步骤:1) 下面代码自建测试表 t_csv41 并插入 3 行;2) 用 SyntaxFromSQL 现场生成 DataWindow 语法并 Create 出 DataStore;3) 分别用 ds2csv 和原生 SaveAs(CSV!) 导出,弹窗显示两种方式的文件内容;4) 结束前自动删表。
  1. // 示例输入:数据库连接信息
  2. string ls_server
  3. ls_server = '127.0.0.1,18433'
  4. // 示例输入:登录账号与口令(本地测试库)
  5. string ls_user
  6. string ls_pwd
  7. ls_user = 'sa'
  8. ls_pwd = 'Zkkt@667788'
  9. // 示例输入:导出文件名
  10. string ls_out1
  11. string ls_out2
  12. ls_out1 = 'demo_ds.csv'
  13. ls_out2 = 'demo_native.csv'
  14. uo_csv lcsv
  15. string ls_sql
  16. string ls_err
  17. string ls_syn
  18. datastore lds
  19. long ll_rows
  20. long ll_n
  21. boolean lb_ok
  22. int li_f
  23. blob lb1
  24. blob lb2
  25. string ls_msg
  26. SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)'
  27. SQLCA.LogId = ls_user
  28. SQLCA.LogPass = ls_pwd
  29. SQLCA.ServerName = ls_server
  30. SQLCA.Database = 'master'
  31. SQLCA.AutoCommit = true
  32. SQLCA.DBParm = "Provider='SQLNCLI10',DataSource='127.0.0.1,18433'"
  33. CONNECT USING SQLCA;
  34. // 自建测试数据
  35. EXECUTE IMMEDIATE "if object_id('dbo.t_csv41') is not null drop table dbo.t_csv41" USING SQLCA;
  36. ls_sql = "create table dbo.t_csv41(id int identity(1,1) primary key, emp_name nvarchar(50), dept nvarchar(50), amount decimal(12,2))"
  37. EXECUTE IMMEDIATE :ls_sql USING SQLCA;
  38. ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('张三','财务部',1234.56)"
  39. EXECUTE IMMEDIATE :ls_sql USING SQLCA;
  40. ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('李四','技术部',8800)"
  41. EXECUTE IMMEDIATE :ls_sql USING SQLCA;
  42. ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('王五','含,逗号部',99.9)"
  43. EXECUTE IMMEDIATE :ls_sql USING SQLCA;
  44. // 现场生成 DataStore
  45. ls_sql = 'select id, emp_name, dept, amount from dbo.t_csv41 order by id'
  46. ls_syn = SQLCA.SyntaxFromSQL(ls_sql, 'style(type=grid)', ls_err)
  47. lds = create datastore
  48. lds.Create(ls_syn, ls_err)
  49. lds.SetTransObject(SQLCA)
  50. ll_rows = lds.Retrieve()
  51. lcsv = create uo_csv
  52. lb_ok = lcsv.ds2csv(lds, ls_out1)
  53. ll_n = lds.SaveAs(ls_out2, CSV!, true)
  54. li_f = FileOpen(ls_out1, StreamMode!, Read!, LockRead!)
  55. FileReadEx(li_f, lb1)
  56. FileClose(li_f)
  57. li_f = FileOpen(ls_out2, StreamMode!, Read!, LockRead!)
  58. FileReadEx(li_f, lb2)
  59. FileClose(li_f)
  60. ls_msg = '行数=' + String(ll_rows) + ' ds2csv=' + String(lb_ok) + ' SaveAs=' + String(ll_n)
  61. ls_msg = ls_msg + '~r~n--- uo_csv.ds2csv(UTF-8 解码后)---~r~n' + FromUTF8(lb1)
  62. ls_msg = ls_msg + '~r~n--- 原生 SaveAs(CSV!)(ANSI 解码后)---~r~n' + String(lb2, EncodingANSI!)
  63. MessageBox('两种导出对比', ls_msg)
  64. destroy lds
  65. destroy lcsv
  66. EXECUTE IMMEDIATE "drop table dbo.t_csv41" USING SQLCA;
  67. DISCONNECT USING SQLCA;
复制代码

实测结果(文件字节级对照,这是全文最有价值的一张表):

对比项uo_csv.ds2csvdw.SaveAs(CSV!, TRUE)
编码UTF-8 + BOMEF BB BF 开头)ANSI/GBK(中文 D5 C5 C8 FD 直接落盘)
引号每个字段都加只在必要时加(仅 含,逗号部 被包住)
转义不转义内部逗号/引号同样不转义(但靠“按需加引号”规避了逗号)
decimal 精度8800.00880099.9099.98800.0099.90 原样保留
表头列名(id,emp_name,dept,amount列名(同上)
依赖需要 PbIdea.dll纯 PB,无需任何外部库
需要 DataWindow是(ds2csv)/ 是(dw2csv


所以选型很直白:要 UTF-8 给外部系统 → uo_csv;要保精度、给本地 Excel 直接双击打开 → SaveAs

dw2csv(datawindow, file, usedisplayvalue)ds2csv 的差异是:表头取列标题 _t.Text(取不到才回退列名),且 usedisplayvalue=true 时会用 evaluate('lookupdisplay(xxx)', 行号) 取下拉框/复选框的显示值而不是存储值。它需要可视的 DataWindow 控件,本文的无界面环境无法构造,故此条为源码级核对、未实机运行

九、ToXLSX:本环境实测跑不通,别依赖

ToXLSX(opt) 听起来很香——CSV 直接变 Excel。但我在 PB 12.5 实机上按源码注释的两种 opt 结构(对象式、数组式)、相对路径与绝对路径、并把 plugin\PbideaExcel.dll 同时放到 plugin\ 和运行目录,四种组合全部返回 false,未生成任何 xlsx 文件

结论:该功能依赖的运行条件在当前 PBIDEA 版本下未能满足,生产上不要依赖它。真要出 xlsx,走 PBIDEA 的 uo_xlsx(libxl 引擎,不装 Office 也能写),或者干脆 SaveAs(Excel!)。这一条是负面结论,但同样是我实机验证出来的——比照抄文档靠谱。

十、编码:三条必须记住的规则

这是本文最容易踩坑、也最容易被忽略的部分。

规则一:uo_csv 写出来的永远是 UTF-8,Open/Write 带 BOM,toCSV 不带 BOM。

规则二:String(blob) 不是通用解码器——它按 UTF-16LE 解释字节。 实测证据:

目标写法结果
GBK 文件String(lb)乱码(摩攬灭湟浡?敤瑰…,即 id 两字节被当 UTF-16)
GBK 文件String(lb, EncodingANSI!)正确 id,emp_name,dept,amo
GBK 文件FromUTF8(lb)正确(BOM 无关时同样能过)
UTF-8 文件String(lb)乱码
UTF-8 文件String(lb, EncodingUTF8!)正确 "姓名","部门","金额"
UTF-8 文件FromUTF8(lb)正确,且会自动吃掉 BOM

前置:demo1.csvuo_csv 产出的 UTF-8 文件;下面代码演示三种解码方式。
步骤:贴进按钮 clicked,运行后弹窗对比 String(blob) / String(blob, EncodingUTF8!) / FromUTF8(blob)
  1. // 示例输入:要读取的 UTF-8 CSV(由 uo_csv 产出,带 BOM)
  2. string ls_file
  3. ls_file = 'demo1.csv'
  4. int li_f
  5. blob lb_data
  6. string ls_msg
  7. li_f = FileOpen(ls_file, StreamMode!, Read!, LockRead!)
  8. FileReadEx(li_f, lb_data)
  9. FileClose(li_f)
  10. ls_msg = 'String(blob)        = ' + Left(String(lb_data), 16)
  11. ls_msg = ls_msg + '~r~nString(blob,UTF8)  = ' + Left(String(lb_data, EncodingUTF8!), 16)
  12. ls_msg = ls_msg + '~r~nFromUTF8(blob)      = ' + Left(FromUTF8(lb_data), 16)
  13. MessageBox('三种解码方式对比', ls_msg)
复制代码

规则三:读进来的东西全是字符串。 无论源里是 1234.56 还是 88toJson 得到的都是带引号的字符串(实测 [{"column_1":"张三","column_2":"1234.56","column_3":"88","column_4":"0"}])。要做计算请自己 Dec() / Long() 转,别指望它帮你分类型。

十一、14 条实测坑清单


  • Close() 就不落盘。 实测 Write 之后立刻查文件大小是 0 字节,Close() 之后才是 22 字节。异常路径里一定要把 Close() 放进 finally
  • 同名文件再 Open 是覆盖,不是追加。 实测第二次 Open 写“第二行”后文件只剩 16 字节。要追加只能自己先读出来合并,或者写不同文件名。
  • 字段里的逗号必然导致读回错位。 这是最狠的一条:toJson纯按逗号切分再去掉首尾引号,它不认引号边界。实测 "含,逗号部" 读回来变成 部门=含 / 金额=逗号部,还凭空多出一列 column_4=88。→ 若 CSV 还要自己读回,写入前把逗号替换成全角逗号或分号。
  • 双引号同样不转义。 写入 含"引号" 得到 "含"引号"",标准解析器会读错;PBIDEA 自己的 toJson 反而读得对(因为它不认引号边界,歪打正着)。
  • hasHeader 默认 true,改成 false 条数会 +1。 表头行被当数据,键名变 column_1..N
  • recordNumber 加的是 recordset_number,从 1 开始。 想自己编号也可以,但别忘了它多占一列会影响按索引取值。
  • 读出来全是字符串。 见规则三。
  • decimal 精度会掉。 8800.00 写成 880099.90 写成 99.9SaveAs(CSV!) 则保留。导出金额给财务时优先 SaveAs,或自己用 String(ld_amt, '0.00') 先格式化。
  • 列数参差时静默变形。 表头 3 列,某行只写 2 个值 → 缺的列直接不出现在 JSON 里;某行写 4 个 → 自动补 column_4
  • 全空行被整行丢弃。 实测写 , , 一行后再读,条数只有 1 条(空行没了)。
  • 文件不存在时 toJson 返回 false。 有返回值可判,别省略判断。
  • ToXLSX 本环境返回 false。 见第 9 节。
  • Write(...) 变参只有外部函数声明支持。 PB 自定义函数写 ... 会报 C0031,但 uo_csv.Write 是 DLL 声明,可以放心用变参。
  • 对象必须 create uo_csv 不是 autoinstantiate,裸调直接 Null object reference。


十二、方案对比与选型

场景推荐理由
导出给外部系统 / 网页 / Mac 用户uo_csvOpen/Write/CloseUTF-8 + BOM,Excel 与在线工具都认
导出给本地财务,要求金额两位小数dw.SaveAs(CSV!, TRUE)ANSI 无 BOM、按需加引号、decimal 保精度
读一个来源不明的 CSVuo_csv.toJsonGBK 与 UTF-8(含无 BOM)实测都能正确识别中文
DataWindow 数据一键导出ds2csv / dw2csv表头自动取列名/列标题,dw2csv 还能取显示值
内存结果集(无 DataWindow)导出uo_csv + any[]不依赖 DataWindow,随算随写
要出 xlsxuo_xlsxSaveAs(Excel!)uo_csv.ToXLSX 实测不可用
手工拼串(ls = ls + ',' + ...不推荐引号/转义/编码三重地雷,出问题难查


十三、扩展点


  • 批量分片导出Open 一次、Write 循环、Close 一次,百万行也不会把内存撑爆,比先攒字符串再 FileWrite 稳得多;
  • uo_json 打通toJson → 用 uo_json 做过滤/改名/聚合 → toCSV 回写,等于白捡一个“CSV 变换管道”;
  • uo_zip 组合:导出一批 CSV 后 uo_zip.Open → Add → Close 打包,再交给 uo_ftp / uo_sftp 上传,一条流水线全在 PB 里完成;
  • uo_database 组合uo_database 查出 uo_recordset 后逐行 Write,可以完全绕开 DataWindow(注意 uo_database 占位符是 :1 / :名字,不是 ?)。


小结


  • uo_csv 的三个动作:写文件 Open → Write → Close读文件 toJson回写 toCSV
  • 写入恒为 UTF-8Open/Write 带 BOM,toCSV 不带),读取兼容 GBK 与 UTF-8
  • 引号由 double_quote 控制(默认加),但逗号与双引号永不转义
  • 最需要警惕的是逗号错位坑toJson 不认引号边界,含逗号字段读回必然串列;
  • 金额精度要求高就用 SaveAs(CSV!)ToXLSX 现阶段别用;
  • 读回来的值全是字符串,用前自己转类型。





实机验证说明:本文全部代码在 PB 12.5 + PBIDEA 最新版实机跑通——pypower pbl import 0 错误、ORCA rebuild --type full 0 错误、PBVM .pbtest 运行 PASS(20/20 硬断言)、check_pb125.py 0 错 0 提醒、check_enc.py bad=0;数据库部分在 SQL Server 2008 R2(10.50.6000.34)上完成建表/插入/取数/清理全流程。唯一未实机运行的是 dw2csv(需可视 DataWindow 控件,无界面环境无法构造),已在文中标注为源码级核对。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2359

积分

PBAI

积分
2359
贡献
在线时间
小时
昨天 07:09 | 显示全部楼层
PB10 兼容演示包已上传,附件 csvdemo_pb10.zip(70.6 KB):

csvdemo_pb10.zip (70.57 KB, 下载次数: 0)

一、包里有什么

  • csvdemo.pbl / csvdemo.pbt —— PB10 工程与库(10 个对象,PB10 全量编译 0 错误)
  • src\ —— 全部可导入导出源码(GBK / CRLF / 无 BOM):uo_csv、uo_json、uo_map、uo_xml、uo_xml_node、uo_xml_attribute、全局函数 format、fromutf8,以及演示对象 nvo_csv_demo
  • tests\ —— 实机断言脚本 csvdemo.pbtest(20 条硬断言)
  • evidence\ —— PB12.5 实机运行输出与产出 CSV 的字节级十六进制对照
  • 使用说明.txt —— 环境、部署、用法、排错、实机验证情况


二、环境要求

  • PowerBuilder 10.0(或更高版本;高版本打开后升级编译即可)
  • PBIDEA 运行库 PbIdea.dll,需与 PB 大版本配套,放到程序目录或 PATH
  • Windows 7 及以上
  • 数据库:本包的「写 CSV / 读 CSV」两段不依赖数据库;只有 DataStore 示例要连库


三、操作细节步骤

  • 解压到任意目录,例如 D:\pbidea_csv\
  • 用 PB10 打开 csvdemo.pbt(或把 csvdemo.pbl 加进现有工程的 liblist)
  • 把 PbIdea.dll 复制到运行目录
  • Full Rebuild —— 预期 0 Error(websuite 自带的 C0210 警告可忽略)
  • 在窗口按钮 clicked 事件里写:
    1. nvo_csv_demo lnv
    2. string ls_out
    3. lnv = create nvo_csv_demo
    4. ls_out = lnv.of_run()
    5. destroy lnv
    6. MessageBox('uo_csv 演示', ls_out)
    复制代码
  • 运行后当前目录会生成 11 个 CSV:demo1 / demo_out / demo_html / demo_dq0 / demo_dq1 / demo_gbk / demo_u8 / demo_buf / demo_again / demo_ds / demo_native,逐个打开即可对照文中每个结论


四、SQL 说明(仅 DataStore 示例)
包内源码用的是 PB12.5 的 SNC 连接串,PB10 不支持 SNC,请改成:
  1. SQLCA.DBMS       = 'MSS Microsoft SQL Server'
  2. SQLCA.Database   = 'master'
  3. SQLCA.ServerName = '127.0.0.1,18433'
  4. SQLCA.LogId      = 'sa'
  5. SQLCA.LogPass    = '你的口令'
  6. SQLCA.AutoCommit = true
  7. CONNECT USING SQLCA;
复制代码
建表(脚本会自动建、自动删,无需手工准备):
  1. create table dbo.t_csv41(
  2.     id       int identity(1,1) primary key,
  3.     emp_name nvarchar(50),
  4.     dept     nvarchar(50),
  5.     amount   decimal(12,2)
  6. );
  7. insert into dbo.t_csv41(emp_name,dept,amount) values('张三','财务部',1234.56);
  8. insert into dbo.t_csv41(emp_name,dept,amount) values('李四','技术部',8800);
  9. insert into dbo.t_csv41(emp_name,dept,amount) values('王五','含,逗号部',99.9);
复制代码

五、实机验证情况(如实标注)

  • PB12.5 + PBIDEA 最新版:import 0 错误、ORCA rebuild --type full 0 错误、PBVM .pbtest 运行 PASS(20/20 硬断言)、check_pb125 0 错 0 提醒、check_enc bad=0
  • 数据库部分在 SQL Server 2008 R2(10.50.6000.34)上完成建表 / 插入 / 取数 / 清理全流程
  • 本 PB10 包:已用 pypower --pb-version 100 全量编译 0 错误,并已用 pbl list 校验对象齐全(10 个,防循环依赖静默丢失)
  • 未在 PB10 真机运行(本机无 PB10 运行时),PB10 下请自行跑一次确认
  • 文中 dw2csv 需可视 DataWindow 控件,无界面环境无法构造,属源码级核对、未实机运行


六、三个最容易翻车的点

  • 不调用 Close() 文件就是 0 字节,异常路径务必把 Close() 放进 finally
  • 字段里有逗号,用 toJson 读回必然串列(它不认引号边界)——要自己读回就先把逗号换成全角逗号
  • 写出来的文件是 UTF-8(Open/Write 带 BOM),读回中文要用 FromUTF8(blob) 或 String(blob, EncodingUTF8!),直接 String(blob) 会按 UTF-16LE 解释而乱码

共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-9-20 15:51 , Processed in 0.027370 second(s), 9 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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