修复使用 VBA () 读取 UTF-8 编码的 CSV 时的 BOM 问题
Fix BOM issues when reading UTF-8 encoded CSVs with VBA ()
我想就字节顺序标记 ( 或 EF BB BF(十六进制)尝试使用 VBA (Excel) 读取 UTF-8 编码的 CSV。请注意,我想避免使用 Workbooks.Open 或 FileSystemObject 打开 CSV。实际上,我宁愿使用 adodb.RecordSet,因为我需要执行某种 SQL 查询。
阅读很多(很多!)东西后,我认为处理这个特定问题的 4 个最佳解决方案是:
- 在使用 ADODB.Connection / ADODB.RecordSet 读取 CSV 之前删除 BOM(例如,通过 #iFile 或 Scripting.FileSystemObject-OpenAsTextStream 有效地读取文件的第一行并删除物料清单)。
- 创建 schema.ini 文件以便 ADO 正确解析 CSV。
- 使用向导创建的一些模块(如 W. Garcia's class module)。
- 使用 ADODB.Stream 并设置 Charset = "UTF-8"。
最后一个解决方案(使用流)看起来很不错但是执行以下操作 returns 一个字符串:
Sub loadCsv()
Const adModeReadWrite As Integer = 3
With CreateObject("ADODB.Stream")
.Charset = "utf-8"
.Mode = adModeReadWrite
.Open
.LoadFromFile ("C:\atestpath\test.csv")
Debug.Print .readtext
End With
End Sub
你知道有什么技巧可以帮助使用 .readtext 返回的字符串作为 ADODB.RecordSet 或 ADODB.Connection 的数据源(除了循环以手动填充我的记录集的字段之外) ?
所以,仔细研究一下,即使您在 连接字符串 中指定 CharacterSet=65001
或 Schema.ini 您无法真正摆脱第一个字段前面的 ?
。
如果在 Schema.ini 中指定所有列,则可以去掉它;但这仍然需要您为每个文件创建 Schema.ini。您必须预先知道字段名称,无论是因为它们始终相同,还是通过阅读字段名称(此处的圆圈中 运行)。
看起来任何解决方案都会为您提供 pre-process 文件,...
那么问题来了,这真的重要吗? ...不,好像不是
事实上,尽管第一个字段名称前面有一个 ?
,但看起来并不重要。
Sub ReadCSVasRecordSet()
Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H1
Dim FilePath As String, Filename As String
Dim Conn As ADODB.Connection
Dim RS As ADODB.Recordset
FilePath = "C:\temp"
Set Conn = New ADODB.Connection
'Conn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & FilePath & ";Extended Properties=""text;CharacterSet=utf-8;HDR=YES;FMT=Delimited"""
Conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & FilePath & ";Extended Properties=""text;HDR=YES;FMT=Delimited"""
Filename = "CN43N-Projects.csv"
Set RS = New ADODB.Recordset
RS.Open "SELECT * FROM [" & Filename & "] WHERE [Status] = ""REL"" AND [Lev] = 1", Conn, adOpenStatic, adLockOptimistic, adCmdText
'Checking the first field name
Debug.Print RS.Fields(0).Name ' Outputs: ?Lev
Debug.Print RS.Fields("Lev").Name ' Outputs: ?Lev
'Debug.Print RS.Fields("?Lev").Name ' Errors out if I include ?
Do Until RS.EOF
Debug.Print RS.Fields.Item("Lev"),
Debug.Print RS.Fields.Item("Proj# def#"),
Debug.Print RS.Fields.Item("Name"),
Debug.Print RS.Fields.Item("Status")
RS.MoveNext
Loop
Set RS = Nothing
If Not Conn Is Nothing Then
Conn.Close
Set Conn = Nothing
End If
End Sub
编辑 1 - 什么?
有意思的是,如果要清理字段名,不能直接匹配第一个字符“?”,因为它还是UTF-8。您可以检查 ASCII 代码值
Asc(Left(Fields(0).Name, 1)) = Asc("?")
;
或者使用 AscW
更好。您会注意到,当您使用 UTF-8 格式时,您最终会得到
AscW(Left(Fields(0).Name, 1)) = -257
(不是 63
)。
Function CleanFieldName(Fields As ADODB.Fields, Item As Variant) As String
CleanFieldName = Fields(Item).Name
' Comparing against "?" doesn't Work..
'If Left(CleanFieldName, 1) = "?" And Fields(0).Name = Fields(Item).Name Then CleanFieldName = Mid(CleanFieldName, 2)
If AscW(Left(CleanFieldName, 1)) = -257 And Fields(0).Name = Fields(Item).Name Then CleanFieldName = Mid(CleanFieldName, 2)
End Function
编辑:我发现用查询表对象加载 CSV(参见 good example) or through a WorkbookQuery object (introduced in Excel 2016) are the easiest and probably most reliable ways to proceed (see an example from the documentation here)。
旧答案:
与@Profex 交谈鼓励我进一步调查该问题。原来有 2 个问题:BOM 和用于 CSV 的定界符。我需要使用的 ADO 连接字符串是:
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\test\;Extended Properties='text;HDR=YES;CharacterSet=65001;FMT=Delimited(;)'"
但是 FMT 不适用于分号 (FMT=Delimited(;)
),至少对于 x64 系统 (Excel x64) 上的 Microsoft.ACE.OLEDB.12.0 是这样。因此,@Profex 说得很对:
even though the first field name has a ? in front of it, it doesn't
look like it actually matters
假设他在用简单逗号 (",") 分隔的 CSV 上使用 FMT=Delimited
。
有些人建议编辑注册表以接受分号分隔符。我想避免这种情况。另外,我宁愿不创建 schema.ini 文件(即使这可能是复杂 CSV 的最佳解决方案)。因此,剩下的唯一解决方案需要在创建 ADODB.Connection.
之前编辑 CSV
我知道我的 CSV 总是有问题的 BOM 以及相同的基本结构(类似于“日期”;“计数”)。因此我决定使用这段代码:
Dim arrByte() As Byte
Dim strFilename As String
Dim iFile As Integer
Dim strBuffer As String
strFilename = "C:\Users\test\t1.csv"
If Dir(strFilename) <> "" Then 'check if the file exists, because if not, it would be created when it is opened for Binary mode.
iFile = FreeFile
Open strFilename For Binary Access Read Write As #iFile
strBuffer = String(3, " ") 'We know the BOM has a length of 3
Get #iFile, , strBuffer
If strBuffer = "" 'Check if the BOM is there
strBuffer = String(LOF(iFile) - 3, " ")
Get #iFile, , strBuffer 'the current read position is ok because we already used a Get. We store the whole content of the file without the BOM in strBuffer
arrByte = Replace(strBuffer, ";", ",") 'We replace every semicolon by a colon
Put #iFile, 1, arrByte
End If
Close #iFile
End If
(注意:可能会使用 arrByte = StrConv(Replace(strBuffer, ";", ","), vbFromUnicode)因为字节数组是 ANSI 格式)。
我想就字节顺序标记 ( 或 EF BB BF(十六进制)尝试使用 VBA (Excel) 读取 UTF-8 编码的 CSV。请注意,我想避免使用 Workbooks.Open 或 FileSystemObject 打开 CSV。实际上,我宁愿使用 adodb.RecordSet,因为我需要执行某种 SQL 查询。
阅读很多(很多!)东西后,我认为处理这个特定问题的 4 个最佳解决方案是:
- 在使用 ADODB.Connection / ADODB.RecordSet 读取 CSV 之前删除 BOM(例如,通过 #iFile 或 Scripting.FileSystemObject-OpenAsTextStream 有效地读取文件的第一行并删除物料清单)。
- 创建 schema.ini 文件以便 ADO 正确解析 CSV。
- 使用向导创建的一些模块(如 W. Garcia's class module)。
- 使用 ADODB.Stream 并设置 Charset = "UTF-8"。
最后一个解决方案(使用流)看起来很不错但是执行以下操作 returns 一个字符串:
Sub loadCsv()
Const adModeReadWrite As Integer = 3
With CreateObject("ADODB.Stream")
.Charset = "utf-8"
.Mode = adModeReadWrite
.Open
.LoadFromFile ("C:\atestpath\test.csv")
Debug.Print .readtext
End With
End Sub
你知道有什么技巧可以帮助使用 .readtext 返回的字符串作为 ADODB.RecordSet 或 ADODB.Connection 的数据源(除了循环以手动填充我的记录集的字段之外) ?
所以,仔细研究一下,即使您在 连接字符串 中指定 CharacterSet=65001
或 Schema.ini 您无法真正摆脱第一个字段前面的 ?
。
如果在 Schema.ini 中指定所有列,则可以去掉它;但这仍然需要您为每个文件创建 Schema.ini。您必须预先知道字段名称,无论是因为它们始终相同,还是通过阅读字段名称(此处的圆圈中 运行)。
看起来任何解决方案都会为您提供 pre-process 文件,...
那么问题来了,这真的重要吗? ...不,好像不是
事实上,尽管第一个字段名称前面有一个 ?
,但看起来并不重要。
Sub ReadCSVasRecordSet()
Const adOpenStatic = 3
Const adLockOptimistic = 3
Const adCmdText = &H1
Dim FilePath As String, Filename As String
Dim Conn As ADODB.Connection
Dim RS As ADODB.Recordset
FilePath = "C:\temp"
Set Conn = New ADODB.Connection
'Conn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & FilePath & ";Extended Properties=""text;CharacterSet=utf-8;HDR=YES;FMT=Delimited"""
Conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & FilePath & ";Extended Properties=""text;HDR=YES;FMT=Delimited"""
Filename = "CN43N-Projects.csv"
Set RS = New ADODB.Recordset
RS.Open "SELECT * FROM [" & Filename & "] WHERE [Status] = ""REL"" AND [Lev] = 1", Conn, adOpenStatic, adLockOptimistic, adCmdText
'Checking the first field name
Debug.Print RS.Fields(0).Name ' Outputs: ?Lev
Debug.Print RS.Fields("Lev").Name ' Outputs: ?Lev
'Debug.Print RS.Fields("?Lev").Name ' Errors out if I include ?
Do Until RS.EOF
Debug.Print RS.Fields.Item("Lev"),
Debug.Print RS.Fields.Item("Proj# def#"),
Debug.Print RS.Fields.Item("Name"),
Debug.Print RS.Fields.Item("Status")
RS.MoveNext
Loop
Set RS = Nothing
If Not Conn Is Nothing Then
Conn.Close
Set Conn = Nothing
End If
End Sub
编辑 1 - 什么?
有意思的是,如果要清理字段名,不能直接匹配第一个字符“?”,因为它还是UTF-8。您可以检查 ASCII 代码值
Asc(Left(Fields(0).Name, 1)) = Asc("?")
;
或者使用 AscW
更好。您会注意到,当您使用 UTF-8 格式时,您最终会得到
AscW(Left(Fields(0).Name, 1)) = -257
(不是 63
)。
Function CleanFieldName(Fields As ADODB.Fields, Item As Variant) As String
CleanFieldName = Fields(Item).Name
' Comparing against "?" doesn't Work..
'If Left(CleanFieldName, 1) = "?" And Fields(0).Name = Fields(Item).Name Then CleanFieldName = Mid(CleanFieldName, 2)
If AscW(Left(CleanFieldName, 1)) = -257 And Fields(0).Name = Fields(Item).Name Then CleanFieldName = Mid(CleanFieldName, 2)
End Function
编辑:我发现用查询表对象加载 CSV(参见 good example) or through a WorkbookQuery object (introduced in Excel 2016) are the easiest and probably most reliable ways to proceed (see an example from the documentation here)。
旧答案:
与@Profex 交谈鼓励我进一步调查该问题。原来有 2 个问题:BOM 和用于 CSV 的定界符。我需要使用的 ADO 连接字符串是:
strCon = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\test\;Extended Properties='text;HDR=YES;CharacterSet=65001;FMT=Delimited(;)'"
但是 FMT 不适用于分号 (FMT=Delimited(;)
),至少对于 x64 系统 (Excel x64) 上的 Microsoft.ACE.OLEDB.12.0 是这样。因此,@Profex 说得很对:
even though the first field name has a ? in front of it, it doesn't look like it actually matters
假设他在用简单逗号 (",") 分隔的 CSV 上使用 FMT=Delimited
。
有些人建议编辑注册表以接受分号分隔符。我想避免这种情况。另外,我宁愿不创建 schema.ini 文件(即使这可能是复杂 CSV 的最佳解决方案)。因此,剩下的唯一解决方案需要在创建 ADODB.Connection.
之前编辑 CSV我知道我的 CSV 总是有问题的 BOM 以及相同的基本结构(类似于“日期”;“计数”)。因此我决定使用这段代码:
Dim arrByte() As Byte
Dim strFilename As String
Dim iFile As Integer
Dim strBuffer As String
strFilename = "C:\Users\test\t1.csv"
If Dir(strFilename) <> "" Then 'check if the file exists, because if not, it would be created when it is opened for Binary mode.
iFile = FreeFile
Open strFilename For Binary Access Read Write As #iFile
strBuffer = String(3, " ") 'We know the BOM has a length of 3
Get #iFile, , strBuffer
If strBuffer = "" 'Check if the BOM is there
strBuffer = String(LOF(iFile) - 3, " ")
Get #iFile, , strBuffer 'the current read position is ok because we already used a Get. We store the whole content of the file without the BOM in strBuffer
arrByte = Replace(strBuffer, ";", ",") 'We replace every semicolon by a colon
Put #iFile, 1, arrByte
End If
Close #iFile
End If
(注意:可能会使用 arrByte = StrConv(Replace(strBuffer, ";", ","), vbFromUnicode)因为字节数组是 ANSI 格式)。