【问题标题】:Fix BOM issues when reading UTF-8 encoded CSVs with VBA ()使用 VBA () 读取 UTF-8 编码的 CSV 时修复 BOM 问题
【发布时间】:2021-02-10 20:36:10
【问题描述】:

我想在尝试阅读时获得有关字节顺序标记(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 来高效读取文件的第一行并移除 BOM)。
  • 创建 schema.ini 文件以便 ADO 正确解析 CSV。
  • 使用一些由向导创建的模块(如W. Garcia's class module)。
  • 使用 ADODB.Stream 并设置 Charset = "UTF-8"。

最后一个解决方案(使用流)似乎很好,但执行以下操作会返回一个字符串:

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 的数据源的技巧吗(除了循环手动填充我的记录集的字段)?

【问题讨论】:

  • @GSerg 不,不幸的是“CharacterSet=65001”对我的 CSV 没有任何好处。另外,我想避免创建工作表。
  • 您不需要创建工作表。您想将数据查询到记录集中,它正是这样做的。
  • 使用CharacterSet=65001,您仍然会在第一个字段名称前加上?

标签: excel vba adodb vba7 vba6


【解决方案1】:

因此,进一步研究它,即使您在 连接字符串Schema 中指定 CharacterSet=65001,它看起来也一样。 ini 你无法真正摆脱第一个字段前面的?

如果您指定 Schema.ini 中的所有列,则可以摆脱它;但这仍然需要您为每个文件创建 Schema.ini。您必须预先知道字段名称,无论是因为它们始终相同,还是通过阅读字段名称(在此处循环运行)。

看起来任何解决方案都会让您预处理文件,...

所以问题是,这真的重要吗? ...不,似乎没有

事实上,尽管第一个字段名称前面有一个?,但它看起来并不重要。

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

【讨论】:

  • 不幸的是,这不起作用。未检测到字段的名称(实际上,字段中第一项的名称是 BOM,又名  或只是 ? 如果使用 CharacterSet=65001)
  • 如果你没有标题,那么也许使用 Schema.ini,因为你必须知道列。如果AscW() = -257,您始终可以通过删除第一个字符来清除第一列数据。
【解决方案2】:

编辑:我发现使用查询表对象(参见 good example)或通过 WorkbookQuery 对象(在 Excel 2016 中引入)加载 CSV 是最简单且可能最可靠的方法继续(参见文档here 中的示例)。

旧答案:

与@Profex 交谈鼓励我进一步调查该问题。原来有 2 个问题:用于 CSV 的 BOM 和分隔符。我需要使用的 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 说得很对:

即使第一个字段名称有 ?在它面前,它没有 看起来很重要

假设他在由简单逗号 (",") 分隔的 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 格式)。

【讨论】:

    猜你喜欢
    • 2020-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-21
    • 2015-07-29
    • 1970-01-01
    • 1970-01-01
    • 2012-02-11
    相关资源
    最近更新 更多