使用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来有效地读取文件的第一行并删除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的数据源(除了循环以手动填充记录集的字段之外)?

解决方法

因此,即使您在 连接字符串 中指定CharacterSet=65001,它也会看起来像Schema.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
,

编辑:我发现,最简单的方法是使用querytable对象(请参见此good example)或通过WorkbookQuery对象(在Excel 2016中引入)加载CSV。可能是最可靠的处理方式(请参见here文档中的示例)。

旧答案:

与@Profex交谈鼓励我进一步调查该问题。原来有两个问题: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非常正确地说:

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

假设他在以简单逗号(“,”)定界的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

(注意:由于字节数组采用ANSI格式,因此可能会使用arrByte = StrConv(Replace(strBuffer,“;”,“,”),vbFromUnicode))。