马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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.dll(FromUTF8 等全局函数也来自它);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=false、double_quote=true | boolean | | 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 默认 true | boolean | | toJson(file, json, hasHeader) | 首行是否当表头(false 时键名 column_N,且首行也算数据) | boolean | | toJson(file, json, hasHeader, recordNumber) | 是否额外加一列行号 recordset_number | boolean | | 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 的约定结构(源码注释):
- {
- "csv": { "file": "demo1.csv", "name": "csv1" },
- "xlsx": { "name": "demo.xlsx" }
- }
复制代码
多文件时 csv 写成对象数组即可。csv 的 name 即 xlsx 里的工作表名。
四、写 CSV:Open → Write → Close 三段式
前置:工程 liblist 已加入 PBIDEA 的 websuite.pbl,运行目录已放 PbIdea.dll。
步骤:1) 新建窗口 w_csv_demo,放一个按钮 cb_1;2) 把下面代码贴进 cb_1 的 clicked 事件;3) 运行后点按钮,程序目录下得到 demo1.csv,并弹窗显示写出结果与文件字节数。 - // 示例输入:要写出的 CSV 文件名(相对当前工作目录)
- string ls_file
- ls_file = 'demo1.csv'
- uo_csv lcsv
- any la_row[]
- boolean lb_ok
- long ll_size
- int li_f
- blob lb_data
- string ls_msg
- lcsv = create uo_csv
- lb_ok = lcsv.Open(ls_file)
- // 表头行:用 any 数组整行写入
- la_row[1] = '姓名'
- la_row[2] = '部门'
- la_row[3] = '金额'
- lcsv.Write(la_row[])
- // 数据行一
- la_row[1] = '张三'
- la_row[2] = '财务部'
- la_row[3] = '1234.56'
- lcsv.Write(la_row[])
- // 数据行二:变参写法,一行几个值就写几个,不必先凑数组
- lcsv.Write('李四', '技术部', '8800')
- lcsv.Close()
- // 读回文件长度,确认真的落盘了
- li_f = FileOpen(ls_file, StreamMode!, Read!, LockRead!)
- FileReadEx(li_f, lb_data)
- FileClose(li_f)
- ll_size = Len(lb_data)
- ls_msg = 'Open=' + String(lb_ok) + '~r~n文件字节数=' + String(ll_size)
- MessageBox('写出结果', ls_msg)
- destroy lcsv
复制代码
实测输出:Open=true、文件 127 字节(四条记录的完整版)。文件内容(十六进制实测,EF BB BF 是 UTF-8 BOM):
- EF BB BF 22 E5 A7 93 E5 90 8D 22 2C ... "姓名","部门","金额"
- "张三","财务部","1234.56"
- "李四","含,逗号部","88"
- "王五","含"引号"部","9.5"
复制代码
三个一眼可见的事实:写了 BOM、每个字段都加了双引号、字段里的逗号和双引号没有被转义。这三条后面各自对应一个坑。
五、读 CSV:toJson 的三个重载
前置:demo1.csv 已由上一节生成(UTF-8 + BOM),首行是表头。
步骤:把下面代码贴进另一个按钮 cb_2 的 clicked 事件,运行后弹窗显示解析条数与第一行内容。 - // 示例输入:要读取的 CSV 文件名
- string ls_file
- ls_file = 'demo1.csv'
- uo_csv lcsv
- uo_json lj
- uo_json lrow
- boolean lb_ok
- long ll_n
- string ls_msg
- lcsv = create uo_csv
- lj = create uo_json
- // hasHeader 默认 true:首行作表头,键名就是表头文字
- lb_ok = lcsv.toJson(ls_file, lj)
- ll_n = lj.ArrCount()
- // 取第 1 条记录(索引从 1 开始)
- lj.Get(1, lrow)
- ls_msg = 'toJson=' + String(lb_ok) + '~r~n条数=' + String(ll_n)
- ls_msg = ls_msg + '~r~n第一行=' + lrow.GetString('姓名')
- ls_msg = ls_msg + '/' + lrow.GetString('部门')
- ls_msg = ls_msg + '/' + lrow.GetString('金额')
- MessageBox('CSV 读回', ls_msg)
- destroy lj
- destroy lcsv
复制代码
实测输出:toJson=true 条数=3、第一行=张三|财务部|1234.56(表头行不计入条数)。
5.1 hasHeader=false 与 recordNumber
- // 示例输入:要读取的 CSV 文件名
- string ls_file
- ls_file = 'demo1.csv'
- uo_csv lcsv
- uo_json lj_nohead
- uo_json lj_num
- uo_json lrow
- string ls_keys[]
- string ls_msg
- lcsv = create uo_csv
- // hasHeader=false:不认表头,键名自动编号 column_1..N,且表头行也算一条数据
- lj_nohead = create uo_json
- lcsv.toJson(ls_file, lj_nohead, false)
- lj_nohead.Get(1, lrow)
- lrow.GetKeys(ls_keys)
- ls_msg = 'hasHeader=false 首个键=' + ls_keys[1]
- ls_msg = ls_msg + ' 条数=' + String(lj_nohead.ArrCount())
- // recordNumber=true:额外加一列行号
- lj_num = create uo_json
- lcsv.toJson(ls_file, lj_num, true, true)
- lj_num.Get(1, lrow)
- lrow.GetKeys(ls_keys)
- ls_msg = ls_msg + '~r~nrecordNumber 首个键=' + ls_keys[1]
- MessageBox('两个读取重载', ls_msg)
- destroy lj_num
- destroy lj_nohead
- destroy lcsv
复制代码
实测输出:
- hasHeader=false 首个键=column_1,string 条数=4
- 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 + "<b>张三</b>","a&b","x" | 带 BOM 且做 HTML 转义 |
结论:
- double_quote 只控制 “要不要给每个字段套引号”,与“转义”毫无关系;
- 无论哪种写法,字段内部的逗号和双引号都不会被转义(标准 CSV 应把 含"引号" 写成 "含""引号""");
- htmlEncode=true 的用途很明确:CSV 内容要贴进网页表格 / 富文本时先转义,避免被当成标签吃掉。
七、JSON 回写 CSV:toCSV
toCSV(json, file) 是 toJson 的逆操作,输入一个 uo_json 数组(元素为对象),输出 CSV 文件:
- // 示例输入:源 CSV 与目标 CSV
- string ls_src
- string ls_dst
- ls_src = 'demo1.csv'
- ls_dst = 'demo_out.csv'
- uo_csv lcsv
- uo_json lj
- boolean lb_ok
- string ls_msg
- lcsv = create uo_csv
- lj = create uo_json
- // 先把源 CSV 读成 JSON,再原样写回另一个文件
- lcsv.toJson(ls_src, lj)
- lb_ok = lcsv.toCSV(lj, ls_dst)
- ls_msg = 'toCSV=' + String(lb_ok)
- MessageBox('JSON 回写 CSV', ls_msg)
- destroy lj
- destroy lcsv
复制代码
实测:toCSV=true,产出 137 字节,首字节就是 22(引号)——没有 BOM。这点和 Open/Write 产出带 BOM 不一样,务必区分。
八、DataWindow / DataStore 一条龙:ds2csv 与 dw2csv
前置:已连接 SQL Server 2008 R2(本文用 127.0.0.1,18433,sa 账号);工程 liblist 含 websuite.pbl。
步骤:1) 下面代码自建测试表 t_csv41 并插入 3 行;2) 用 SyntaxFromSQL 现场生成 DataWindow 语法并 Create 出 DataStore;3) 分别用 ds2csv 和原生 SaveAs(CSV!) 导出,弹窗显示两种方式的文件内容;4) 结束前自动删表。 - // 示例输入:数据库连接信息
- string ls_server
- ls_server = '127.0.0.1,18433'
- // 示例输入:登录账号与口令(本地测试库)
- string ls_user
- string ls_pwd
- ls_user = 'sa'
- ls_pwd = 'Zkkt@667788'
- // 示例输入:导出文件名
- string ls_out1
- string ls_out2
- ls_out1 = 'demo_ds.csv'
- ls_out2 = 'demo_native.csv'
- uo_csv lcsv
- string ls_sql
- string ls_err
- string ls_syn
- datastore lds
- long ll_rows
- long ll_n
- boolean lb_ok
- int li_f
- blob lb1
- blob lb2
- string ls_msg
- SQLCA.DBMS = 'SNC SQL Native Client(OLE DB)'
- SQLCA.LogId = ls_user
- SQLCA.LogPass = ls_pwd
- SQLCA.ServerName = ls_server
- SQLCA.Database = 'master'
- SQLCA.AutoCommit = true
- SQLCA.DBParm = "Provider='SQLNCLI10',DataSource='127.0.0.1,18433'"
- CONNECT USING SQLCA;
- // 自建测试数据
- EXECUTE IMMEDIATE "if object_id('dbo.t_csv41') is not null drop table dbo.t_csv41" USING SQLCA;
- 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))"
- EXECUTE IMMEDIATE :ls_sql USING SQLCA;
- ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('张三','财务部',1234.56)"
- EXECUTE IMMEDIATE :ls_sql USING SQLCA;
- ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('李四','技术部',8800)"
- EXECUTE IMMEDIATE :ls_sql USING SQLCA;
- ls_sql = "insert into dbo.t_csv41(emp_name,dept,amount) values('王五','含,逗号部',99.9)"
- EXECUTE IMMEDIATE :ls_sql USING SQLCA;
- // 现场生成 DataStore
- ls_sql = 'select id, emp_name, dept, amount from dbo.t_csv41 order by id'
- ls_syn = SQLCA.SyntaxFromSQL(ls_sql, 'style(type=grid)', ls_err)
- lds = create datastore
- lds.Create(ls_syn, ls_err)
- lds.SetTransObject(SQLCA)
- ll_rows = lds.Retrieve()
- lcsv = create uo_csv
- lb_ok = lcsv.ds2csv(lds, ls_out1)
- ll_n = lds.SaveAs(ls_out2, CSV!, true)
- li_f = FileOpen(ls_out1, StreamMode!, Read!, LockRead!)
- FileReadEx(li_f, lb1)
- FileClose(li_f)
- li_f = FileOpen(ls_out2, StreamMode!, Read!, LockRead!)
- FileReadEx(li_f, lb2)
- FileClose(li_f)
- ls_msg = '行数=' + String(ll_rows) + ' ds2csv=' + String(lb_ok) + ' SaveAs=' + String(ll_n)
- ls_msg = ls_msg + '~r~n--- uo_csv.ds2csv(UTF-8 解码后)---~r~n' + FromUTF8(lb1)
- ls_msg = ls_msg + '~r~n--- 原生 SaveAs(CSV!)(ANSI 解码后)---~r~n' + String(lb2, EncodingANSI!)
- MessageBox('两种导出对比', ls_msg)
- destroy lds
- destroy lcsv
- EXECUTE IMMEDIATE "drop table dbo.t_csv41" USING SQLCA;
- DISCONNECT USING SQLCA;
复制代码
实测结果(文件字节级对照,这是全文最有价值的一张表):
| 对比项 | uo_csv.ds2csv | dw.SaveAs(CSV!, TRUE) | | 编码 | UTF-8 + BOM(EF BB BF 开头) | ANSI/GBK(中文 D5 C5 C8 FD 直接落盘) | | 引号 | 每个字段都加 | 只在必要时加(仅 含,逗号部 被包住) | | 转义 | 不转义内部逗号/引号 | 同样不转义(但靠“按需加引号”规避了逗号) | | decimal 精度 | 8800.00 → 8800、99.90 → 99.9 | 8800.00、99.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.csv 为 uo_csv 产出的 UTF-8 文件;下面代码演示三种解码方式。
步骤:贴进按钮 clicked,运行后弹窗对比 String(blob) / String(blob, EncodingUTF8!) / FromUTF8(blob)。 - // 示例输入:要读取的 UTF-8 CSV(由 uo_csv 产出,带 BOM)
- string ls_file
- ls_file = 'demo1.csv'
- int li_f
- blob lb_data
- string ls_msg
- li_f = FileOpen(ls_file, StreamMode!, Read!, LockRead!)
- FileReadEx(li_f, lb_data)
- FileClose(li_f)
- ls_msg = 'String(blob) = ' + Left(String(lb_data), 16)
- ls_msg = ls_msg + '~r~nString(blob,UTF8) = ' + Left(String(lb_data, EncodingUTF8!), 16)
- ls_msg = ls_msg + '~r~nFromUTF8(blob) = ' + Left(FromUTF8(lb_data), 16)
- MessageBox('三种解码方式对比', ls_msg)
复制代码
规则三:读进来的东西全是字符串。 无论源里是 1234.56 还是 88,toJson 得到的都是带引号的字符串(实测 [{"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 写成 8800、99.90 写成 99.9;SaveAs(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_csv(Open/Write/Close) | UTF-8 + BOM,Excel 与在线工具都认 | | 导出给本地财务,要求金额两位小数 | dw.SaveAs(CSV!, TRUE) | ANSI 无 BOM、按需加引号、decimal 保精度 | | 读一个来源不明的 CSV | uo_csv.toJson | GBK 与 UTF-8(含无 BOM)实测都能正确识别中文 | | DataWindow 数据一键导出 | ds2csv / dw2csv | 表头自动取列名/列标题,dw2csv 还能取显示值 | | 内存结果集(无 DataWindow)导出 | uo_csv + any[] | 不依赖 DataWindow,随算随写 | | 要出 xlsx | uo_xlsx 或 SaveAs(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-8(Open/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 控件,无界面环境无法构造),已在文中标注为源码级核对。 |