Public Function GetDataFromClosedWorkbook(ByVal SourceFile As String, _ ByVal SourceRange As String, _ Optional ByRef FieldNames As String = "", _ Optional ByVal SkipHeaders As Boolean = False, _ Optional ByVal LocalCopyLifetime As Double = 1#, _ Optional ByVal ForceRecopy As Boolean = False, _ Optional ByVal Asynchronous As Boolean = False) As Variant Application.Volatile False On Error GoTo ErrSub ' Read a Range in a closed workbook (which remains closed throughout the ' operation - we do not open the file in Excel.exe) ' Returns a TRANSPOSED 2-dimensional variant array, in which the first column will be the headers ' If your range is a worksheet, append "$" to the worksheet name ' If your range is a defined set of cells on a worksheet, use this format: ' Sheet_Name$B1:G1024 (spaces are OK in the worksheet name) ' If you're using workbook-level named range, just supply the name ' If you're querying a csv file, don't bother with a sheet or range name. The filename is the 'table' ' SkipHeaders = TRUE means that the top row of your data range will NOT be ' treated as part of the data to be returned ' Set SkipHeaders=True if you pass the parameter SourceRange as ' a SQL query instead of a range or table name ' FieldNames will be populated by a comma-delimited string containing ' the field names if SkipHeaders is True ' Note that we do not attempt to examine files on network folders: we always copy to a temporary folder ' - However, we'll only overwrite a pre-existing local copy if the pre-existing ' file is older than LocalCopyLifetime days ' - While the copy-to-local-folder operation in running in asynchronous mode, ' the function will return #WAITING FOR FILE TRANSFER Dim objFSO As Object ' late-binding: imperfect, but it means we can drag-and-drop this sheet without creating references Dim objConnect As Object ' ADODB.Connection Dim rst As Object ' ADODB.Recordset Dim strConnect As String Dim i As Long Dim j As Long Dim arrData As Variant Dim TempFile As String Dim strTest As String Dim SQL As String Dim iColCount As Long Dim strPathFull As String Dim strHeaders As String If SourceFile = "" Then Exit Function End If ' **** Parse out web folder paths ' **** **** **** **** **** **** **** **** **** **** **** **** If Left(SourceFile, 5) = "http:" Then SourceFile = Right(SourceFile, Len(SourceFile) - 5) SourceFile = Replace(SourceFile, "%20", " ") SourceFile = Replace(SourceFile, "%160", " ") SourceFile = Replace(SourceFile, "/", "\") End If strPathFull = SourceFile If Len(Dir(SourceFile)) = 0 Then ReDim arrTemp(1 To 1, 1 To 1) arrTemp(1, 1) = "#ERROR Source file not found" GetDataFromClosedWorkbook = arrTemp Erase arrTemp Exit Function End If ' **** Copy remote files to the local drive: **** **** **** **** **** **** **** **** **** **** **** If objFSO Is Nothing Then Set objFSO = CreateObject("Scripting.FileSystemObject") ' New Scripting.FileSystemObject End If If objFSO Is Nothing Then Shell "Regsvr32.exe /s scrrun.dll", vbHide Application.Wait (Now() + 5 / 3600 / 24) Set objFSO = CreateObject("Scripting.FileSystemObject") End If If objFSO Is Nothing Then Exit Function End If TempFile = objFSO.GetSpecialFolder(2).Path & "\" & Filename(SourceFile) If ForceRecopy Then If Len(VBA.FileSystem.Dir(TempFile)) > 0 Then VBA.FileSystem.Kill TempFile End If End If If Not (Left(SourceFile, 3) = "C:\" Or Left(SourceFile, 3) = "D:\") Then If Len(VBA.FileSystem.Dir(TempFile)) > 0 Then On Error Resume Next If VBA.FileSystem.FileDateTime(TempFile) < VBA.FileSystem.FileDateTime(SourceFile) Then VBA.FileSystem.Kill TempFile ElseIf objFSO.GetFile(TempFile).dateLastAccessed < (Now - LocalCopyLifetime) Then VBA.FileSystem.Kill TempFile End If End If If Len(VBA.FileSystem.Dir(TempFile)) = 0 Then If Asynchronous Then Shell "cmd /c COPY " & Chr(34) & SourceFile & _ Chr(34) & " " & Chr(34) & TempFile & Chr(34), vbHide ReDim arrTemp(1 To 1, 1 To 1) arrTemp(1, 1) = "#WAITING FOR FILE TRANSFER. Please try again in a minute." GetDataFromClosedWorkbook = arrTemp Erase arrTemp Exit Function Else VBA.FileSystem.FileCopy SourceFile, TempFile End If Else SourceFile = TempFile End If End If ' **** Decide whether we need to read a header row separately from the main body of the data: ' **** **** If InStr(1, SourceRange, "SELECT", vbTextCompare) > 0 And _ InStr(7, SourceRange, "FROM", vbTextCompare) > 1 _ Then strHeaders = "HDR=Yes" 'SkipHeaders = True ElseIf SkipHeaders = True Then strHeaders = "HDR=Yes" Else strHeaders = "HDR=No" End If ' **** Connect to the file: ' **** **** **** **** **** **** **** **** **** **** **** ****' **** **** Application.StatusBar = "Connecting to " & SourceFile & "..." If Right(SourceFile, 4) = ".xls" Then ' 'strConnect = "DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" & _ "ReadOnly=1;DBQ=" & Chr(34) & SourceFile & Chr(34) & ";" & _ ";Extended Properties=" & _ Chr(34) & "HDR=No;IMEX=1;MaxScanRows=0" & Chr(34) & ";" strConnect = "Provider=Microsoft.Jet.OLEDB.4.0;_ Data Source=" & Chr(34) & SourceFile & Chr(34) & ";_ Extended Properties=" & Chr(34) & "Excel 8.0;" & _ strHeaders & ";IMEX=1;MaxScanRows=0" & Chr(34) & ";" ' strConnect = "Provider=Microsoft.ACE.OLEDB.12.0;_ Data Source=" & Chr(34) & SourceFile & Chr(34) & ";_ Extended Properties=" & Chr(34) & "Excel 8.0;" & _ strHeaders & ";IMEX=1;MaxScanRows=0" & Chr(34) & ";" ElseIf Right(SourceFile, 5) = ".xlsx" Then strConnect = "Provider=Microsoft.ACE.OLEDB.12.0;_ Data Source=" & Chr(34) & SourceFile & Chr(34) & ";_ Extended Properties=" & Chr(34) & "Excel 12.0 Xml;" & _ strHeaders & ";IMEX=1;MaxScanRows=0" & Chr(34) & ";" ElseIf Right(SourceFile, 5) = ".xlsm" Then 'strConnect = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};"_ & "ReadOnly=1;_ DBQ=" & SourceFile & ";" & Chr(34) & SourceFile & Chr(34) & ";" & ";_ Extended Properties=" & Chr(34) & "Excel 12.0;" & _ strHeaders & ";IMEX=1;MaxScanRows=0" & Chr(34) & ";" strConnect = "Provider=Microsoft.ACE.OLEDB.12.0; _ Data Source=" & Chr(34) & SourceFile & Chr(34) & ";_ Extended Properties=" & Chr(34) & "Excel 12.0 Macro;" & _ strHeaders & ";IMEX=1;MaxScanRows=0" & Chr(34) & ";" ElseIf Right(SourceFile, 5) = ".xlsb" Then 'strConnect = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" & _ '"ReadOnly=1;DBQ=" & SourceFile & ";" & Chr(34) & SourceFile & Chr(34) & ";" & _ ' ";Extended Properties=" & Chr(34) & "Excel 12.0;" & strHeaders & "; _ ' IMEX=1;MaxScanRows=0" & Chr(34) & ";" ' This ACE driver is unstable on xlsb files... ' But it's more likely to return a result, if you don't mind crashes: strConnect = "Provider=Microsoft.ACE.OLEDB.12.0; _ Data Source=" & Chr(34) & SourceFile & Chr(34) & _ ";Extended Properties=" & Chr(34) & "Excel 12.0;" & strHeaders _ & "; _ IMEX=1;MaxScanRows=0" & Chr(34) & ";" ElseIf Right(SourceFile, 4) = ".csv" Or Right(SourceFile, 4) = ".txt" Then ' JET OLEDB text driver connection string: ' Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\txtFilesFolder\; ' Extended Properties="text;HDR=Yes;FMT=Delimited;MaxScanRows=;IMEX=1;"; ' ODBC text driver connection string: ' Driver={Microsoft Text Driver ' (*.txt; *.csv)};Dbq=c:\txtFilesFolder\;Extensions=asc,csv,tab,txt; strConnect = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & _ Chr(34) & Folder(SourceFile) & Chr(34) & ";" strConnect = strConnect & "Extended Properties=" & Chr(34) & _ "text;HDR=Yes;IMEX=1;MaxScanRows=0" & Chr(34) & ";" SourceRange = Filename(SourceFile) ElseIf Right(SourceFile, 4) = ".tab" Or Right(SourceFile, 4) = ".dat" Then ' JET OLEDB text driver connection string: ' Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\txtFilesFolder\;Extended ' Properties="text;HDR=Yes;FMT=Delimited;MaxScanRows=;IMEX=1;"; ' ODBC text driver connection string: ' Driver={Microsoft Text Driver ' (*.txt; *.csv)};Dbq=c:\txtFilesFolder\;Extensions=asc,csv,tab,txt; strConnect = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & _ Chr(34) & Folder(SourceFile) & Chr(34) & ";" strConnect = strConnect & "Extended Properties=" & Chr(34) & _ "text;HDR=Yes;IMEX=1;MaxScanRows=0;DELIMITER=TAB" & Chr(34) & ";" SourceRange = Filename(SourceFile) Else ReDim arrTemp(1 To 1, 1 To 1) arrTemp(1, 1) = "#ERROR - file format not known" GetDataFromClosedWorkbook = arrTemp Erase arrTemp End If On Error GoTo ErrSub Set objConnect = CreateObject("ADODB.Connection") ' New ADODB.Connection With objConnect .ConnectionTimeout = 60 .CommandTimeout = 90 .Mode = 1 ' adModeRead = 1 .ConnectionString = strConnect .Open End With ' **** Retrieve the data: ' **** **** **** **** **** **** **** **** **** **** **** **** **** Set rst = CreateObject("ADODB.Recordset") ' New ADODB.Recordset With rst .CursorLocation = 3 If Right(ThisWorkbook.Name, 4) = ".xls" Then .MaxRecords = 65535 Else .MaxRecords = 1048575 End If If InStr(1, SourceRange, "SELECT", vbTextCompare) > 0 _ And InStr(7, SourceRange, "FROM", vbTextCompare) > 1 Then SQL = SourceRange Else SQL = "SELECT * FROM [" & SourceRange & "] " End If Application.StatusBar = "Querying " & SourceFile & "..." '.Open SQL, objConnect, adOpenStatic, adLockReadOnly, adCmdText + adAsyncFetch .Open SQL, objConnect, 3, 1, 1 + 32 i = 0 Do While .State > 1 i = (i + 1) Mod 3 Application.StatusBar = "Retrieving data from " & SourceFile & String(i, ".") Application.Wait Now + (0.25 / 24 / 3600) Loop End With ' **** Handle the returned data ' **** **** **** **** **** **** **** **** **** **** **** **** On Error Resume Next ' resume next is required, as the errors we anticipate cannot be trapped: ' they can only be detected after the fact For i = 0 To rst.Fields.Count - 1 FieldNames = FieldNames & rst.Fields(i).Name & "," Next i FieldNames = Left(FieldNames, Len(FieldNames) - 1) If rst.EOF And rst.BOF Then 'return a single empty rpw, so that the caller doesn't error out ReDim arrTemp(1 To rst.Fields.Count, 1 To 1) 'remember, its a transposed array arrTemp(1, 1) = "#NO MATCHING DATA IN '" & SourceFile & "' USING '" & SQL & "'" GetDataFromClosedWorkbook = arrTemp Else Err.Clear rst.MoveFirst GetDataFromClosedWorkbook = rst.GetRows ' note that this often fails on the first try. If IsEmpty(GetDataFromClosedWorkbook) Then rst.MoveFirst GetDataFromClosedWorkbook = rst.GetRows End If If IsEmpty(GetDataFromClosedWorkbook) Then ' ...And on the second try. GetRows is fast when it works, but cannot be relied on rst.MoveFirst ReDim arrTemp(0 To rst.Fields.Count - 1, 0 To rst.RecordCount) i = 0 j = 0 Do Until rst.EOF Err.Clear If i > UBound(arrTemp, 2) Then ReDim Preserve arrTemp(0 To rst.Fields.Count - 1, 0 To i) End If For j = 0 To rst.Fields.Count - 1 arrTemp(j, i) = rst.Fields(j).Value If Err.Number = &HBCD Then Exit For End If Next j i = i + 1 If Err.Number <> &HBCD Then rst.MoveNext End If If Err.Number <> &H80004005 And Err.Number <> 0 Then Exit Do End If Loop GetDataFromClosedWorkbook = arrTemp Erase arrTemp End If ' IsEmpty(GetDataFromClosedWorkbook) End If ' rst.EOF And rst.BOF Then ExitSub: On Error Resume Next rst.Close objConnect.Close ' close the database connection Set rst = Nothing Set objConnect = Nothing Exit Function ErrSub: ReDim arrTemp(1 To 1, 1 To 1) If InStr(Err.Description, "not a valid name") Then arrTemp(1, 1) = "#ERROR: cannot retrieve data from '" & SourceRange & "'t" MsgBox "Cannot open the file: " & vbCrLf & vbCrLf & strPathFull & vbCrLf & vbCrLf & _ "This error message probably means that the source file is locked because another _ user has this file open. Please wait a few minutes, and try again." & vbCrLf & _ vbCrLf & "If this error persists, please contact the tech team.", vbCritical, _ APP_NAME & ": file access error:" ElseIf InStr(Err.Description, "cannot open the file") Then arrTemp(1, 1) = "#ERROR: Cannot open the file '" & SourceRange & "'t" MsgBox "Cannot open the file: " & vbCrLf & vbCrLf & strPathFull & vbCrLf _ & vbCrLf & "This error message probably means that the source file is _ locked because another user has this file open. Please wait a few minutes, _ and try again." & vbCrLf & vbCrLf & "If this error persists, please contact _ the tech team.", vbCritical, APP_NAME & ": file access error:" ElseIf InStr(Err.Description, "not find the object") Then arrTemp(1, 1) = "#ERROR: Invalid object name in '" & SourceRange & "'" _ MsgBox Err.Description & vbCrLf & vbCrLf & "This error message probably _ means that the worksheet or range has been renamed, or does not exist in _ the file. Please check your file: if you can't see an obvious error, ask _ for help from the tech team.", vbCritical, APP_NAME & ": file data error:" ElseIf InStr(Err.Description, "Permission Denied") Then arrTemp(1, 1) = "#ERROR Access to file" MsgBox "Cannot open the file: " & vbCrLf & vbCrLf & strPathFull & vbCrLf & vbCrLf & _ "Another user probably has this file open. Please wait a few minutes, and try again." _ & vbCrLf & vbCrLf & "If this error persists, please contact tech team.", _ vbCritical, APP_NAME & ": file access error:" Else arrTemp(1, 1) = "#ERROR " & Err.Number & ": " & Err.Description End If GetDataFromClosedWorkbook = arrTemp Erase arrTemp Resume ExitSub Resume End Function
Sunday, 31 August 2014
Reading a closed Excel workbook using ADODB
Here's a rough code sample (check for line breaks!) for reading closed Excel files:
Subscribe to:
Posts (Atom)