Почему я не мог просто прочитать листы и объединить все содержимое в один вариант?

Контекст: я пытаюсь в VBA/Excel прочитать содержимое нескольких больших листов (всего около 100 тысяч строк) и сохранить его в варианте, чтобы работать с данными в памяти.

Почему: я хочу работать с этими данными в памяти, чтобы увеличить скорость, вместо того, чтобы работать и писать непосредственно на листах.

Мой код работал хорошо с одним листом, проблемы начинаются, когда листов больше одного.

Проблема: я хочу объединить содержимое разных листов только в один вариант, чтобы работать над ним.

Код :

Dim ws As Worksheet
Dim arrData As Variant ' The variant to store the data
Dim lastSheetLine As Double
Dim lineMemory As Double

lineMemory = 0

For Each ws In ThisWorkBook.Worksheets ' Parse all the sheets

     lastSheetLine = Worksheets(ws.name).Cells(Worksheets(ws.name).Rows.Count, "A").End(xlUp).Row ' find the last line of the sheet

   If lineMemory = 0 Then
     arrData(0, 15) = Worksheets(ws.name).Range("A2:O" & lastSheetLine).Value ' Store the sheet on the Variant
     lineMemory = lastSheetLine
   Else
     lineMemory = lineMemory + lastSheetLine ' Increment to get the position where to put the block
     arrData(lineMemory, 15) = Worksheets(ws.name).Range("A2:O" & lastSheetLine).Value
   End If

Next ws

В этом многомерном массиве второе измерение всегда равно 15 (столбцов всегда одинаковое), именно первое измерение будет меняться от листа к листу.

Заранее большое спасибо за помощь, я открыт для всего

Вы можете прочитать всю таблицу из листа в массив вариантов одним оператором, но синтаксис отличается от вашего: varArray = Range("здесь идет адрес").Значение. Но вы не можете добавить данные из другого диапазона в тот же массив. в одном заявлении. Вам придется использовать два варианта массива: один, который получает данные из листов, и другой, к которому вы добавляете данные. последнее придется выполнять построчно и постолбцу. Последнее замечание: замените все экземпляры «Рабочие листы (ws.name)». с помощью просто "ws".

jkpieterse 26.06.2024 10:29

Похоже, вы не очень хорошо понимаете, как работают массивы, без обид... Вы не можете использовать массив так, как пытаетесь. Тогда фраза «Мой код хорошо работал на одном листе» не может быть правдой в отношении кода, который вы показываете. arrData(0, 15) = anything Вы не можете просто добавить строки и столбцы в существующий массив. Вы можете добавлять столбцы, но только после ReDim Preserve. Процесс объединения областей совершенно другой. Насчет «этого многомерного массива» неверное определение. То, что вы пытаетесь вернуть (неверным способом), будет простым 2D-массивом. Имея только два измерения (строки и столбцы)...

FaneDuru 26.06.2024 10:34

@jkpieterse спасибо за ответ, я уже использую Value (верно, нет?)

C K 26.06.2024 10:40

@FaneDuru тоже спасибо за твой ответ; Я подтверждаю, что код работал хорошо с одним листом, потому что я использовал напрямую arrData = X, а не arrData(0, 15) ; поскольку лист был только один, у меня не было проблем с объединением нескольких блоков контента в один массив; каким было бы ваше решение объединить два 2D-массива?

C K 26.06.2024 10:41

Вы уверены, что хотите поместить все данные всех листов в один массив перед работой с ним? Будет сложно записать данные обратно на листы, потому что вам нужно будет отслеживать, где начинаются и заканчиваются данные каждого листа. На вашем месте я бы использовал один массив на листе.

FunThomas 26.06.2024 10:54

@FunThomas Спасибо за ваш ответ, Томас; Да, я хочу работать только с одним массивом, потому что мне не нужно возвращать данные на листы, это односторонняя процедура, и поэтому в одном из столбцов есть идентификатор, который может помочь мне получить их. обратно, если необходимо

C K 26.06.2024 11:12

Затем протестируйте опубликованный мной код и отправьте отзыв.

FaneDuru 26.06.2024 11:51
Преобразование HTML-таблицы в профессиональный документ Excel
Преобразование HTML-таблицы в профессиональный документ Excel
Это самый простой способ создания Excel из HTML-таблицы.
Импорт excel в laravel в базу данных
Импорт excel в laravel в базу данных
Здравствуйте, дорогой читатель, в этой статье я расскажу практическим и быстрым способом, как импортировать файл Excel в вашу базу данных с помощью...
2
7
121
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Чтобы объединить массивы с одинаковым количеством столбцов, попробуйте следующий способ:

  1. Объявите переменную поверх стандартного модуля (в области объявлений):
  Private arrFin()
  1. Скопируйте следующий код в тот же стандартный модуль:
Sub TestMrgeArrays()
 Dim wb As Workbook, ws As Worksheet, lastRow As Long

 Set wb = ThisWorkbook
 For Each ws In wb.Worksheets ' Parse all the sheets
     lastRow = ws.cells(ws.rows.count, "A").End(xlUp).row ' find the last line of the sheet
     merge2DArrays ws.Range("A2:O" & lastRow).Value ' merge with the final one...
 Next ws
 Debug.Print UBound(arrFin), UBound(arrFin, 2): Stop
End Sub

Sub merge2DArrays(arr)
    Dim arrNew(), i As Long, j As Long, xF As Long
    
    If Not Not arrFin Then
        If UBound(arrFin, 2) <> UBound(arr, 2) Then MsgBox "Different number of columns...": Exit Sub
        ReDim arrNew(1 To UBound(arrFin) + UBound(arr), 1 To UBound(arrFin, 2))
        xF = UBound(arrFin)
        For i = 1 To UBound(arrNew)
            For j = 1 To UBound(arrNew, 2)
                If i <= xF Then
                    arrNew(i, j) = arrFin(i, j)
                Else
                    arrNew(i, j) = arr(i - xF, j)
                End If
            Next j
        Next i
        arrFin = arrNew
    Else
        arrFin = arr: Exit Sub
    End If
End Sub

Существует еще один вариант, включающий ReDim Preserve, который может взять предыдущий загруженный массив, транспонировать его и поместить содержимое в новый массив, загрузить его только содержимым нового массива и, наконец, уравнять окончательный массив с этим (временным). Но Transpose имеет некоторые ограничения с точки зрения размеров диапазона, и в вашем случае он не сработает. Я также могу выполнить транспонирование нестандартной функции без ошибок, но для этого также потребуется итерация...

Отредактировано:

В следующей версии используется зубчатый массив (массив массивов), который сначала загружается с каждым массивом листов и обрабатывается отдельной функцией:

Sub TestMrgeArrays_JGGArray()
 Dim wb As Workbook, ws As Worksheet, arrData() As Variant
 Dim lastRow, arrFin, iCount As Long, totCount As Long

 Set wb = ThisWorkbook
 ReDim arrData(wb.Worksheets.count - 1) 'ReDim it to keep all the sheets (0 based 1D array)
 For Each ws In wb.Worksheets ' Parse all the sheets
     lastRow = ws.cells(ws.rows.count, "A").End(xlUp).row ' find the last line of the sheet
     arrData(iCount) = ws.Range("A2:O" & lastRow).Value ' place it in the jagged array
     iCount = iCount + 1:  totCount = totCount + lastRow - 1 ' -1 because the array is loaded from the second row...
 Next ws
 arrFin = MergeShArrays(arrData, totCount)
 Debug.Print UBound(arrFin), UBound(arrFin, 2): Stop
End Sub

Function MergeShArrays(arrD, totCount As Long) As Variant
  Dim i As Long, j As Long, k As Long, iRow As Long, colNo As Long, arr, arrNew()
  colNo = UBound(arrD(0), 2)
  ReDim arr(1 To totCount, 1 To colNo)
  For i = 0 To UBound(arrD)
    arrNew = arrD(i)
    For j = 1 To UBound(arrNew)
        iRow = iRow + 1
        For k = 1 To colNo
            arr(iRow, k) = arrNew(j, k)
        Next k
    Next j
  Next i
  MergeShArrays = arr
End Function

Должно быть немного быстрее определение общего количества строк из итерационной части листов.

Я должен поблагодарить вас, это работает как шарм; желаю вам хорошего завершения недели, с уважением

C K 26.06.2024 14:49

насчет отредактированной версии кода, она не работает: ошибка выполнения 13, несовместимость типа в строке «For j = 1 To UBound(arrD(i)) в функции

C K 26.06.2024 15:57

@C K Я протестировал приведенный выше код на своем компьютере, и он работает как надо (64-разрядная версия Excel 2016 в Windows 11). Некоторые версии VBA не могут приводить/понимать, что элемент зубчатого массива также является массивом. Значит нужно объявить еще одну переменную и... Собственно, я буду дома через 1, 2 минуты и адаптирую код...

FaneDuru 26.06.2024 16:53

@C K Пожалуйста, протестируйте адаптированную функцию. Я только объявил новую переменную (arrNew()) и присвоил ей значение arrD(i). (arrNew = arrD(i)). Тогда используйте его вместо этого... Работает ли он так, как вам нужно? Теоретически должно быть быстрее. Несколько менее необходимых итераций...

FaneDuru 26.06.2024 16:59

Полезно и поучительно +:); К вашему сведению, опубликована альтернатива через динамический временный лист, построенный только на прямой вставке табличных формул. @FaneDuru

T.M. 28.06.2024 12:39

Вы можете прочитать значения диапазона в переменной Variant. Если диапазон содержит только одну ячейку, вариант получит это значение. Если диапазон содержит более одной ячейки, вариант будет представлять собой двумерный массив.

Назначением этого чтения должен быть вариант. Это не может быть предопределенный массив:

ReDim a(1 To 10, 1 To 15) 
' This will give a runtime error 13 (Type mismatch)
a = ThisWorkbook.Sheets(1).Range("A1:O10").Value

Dim b()
' This will give a runtime error 13 (Type mismatch)
b = ThisWorkbook.Sheets(1).Range("A1:O10").Value

Dim c As Variant
' Only this is okay:
c = ThisWorkbook.Sheets(1).Range("A1:O10").Value

И, как следствие, вы не можете считать данные диапазона в часть предопределенного массива (за исключением случаев, когда диапазон содержит только одну ячейку).

Так что если вам действительно нужен один большой массив, содержащий все данные всех листов, вам нужно создать этот массив самостоятельно.


Следующий код будет считывать данные каждого листа отдельно в коллекцию двумерных массивов. Во время чтения подсчитывается общий размер.

Когда чтение завершено, мы знаем, насколько большим должен быть конечный массив. Создается массив, и в него копируются все данные лист за листом, значение за значением.

Dim ws As Worksheet
Dim Data As New Collection
Dim totalRows As Long

' Step 1: Read data of every sheet into separate arrays
For Each ws In ThisWorkbook.Worksheets ' Parse all the sheets

    Dim lastSheetLine As Long
    lastSheetLine = ws.Cells(ws.Rows.Count, "A").End(xlUp).row ' find the last line of the sheet
    Data.Add ws.Range("A2:O" & lastSheetLine).Value, ws.Name
    totalRows = totalRows + (lastSheetLine - 1)
Next

' Step 2: Now Copy all data into one big array
ReDim AllInOneData(1 To totalRows, 1 To 15)
Dim allInOneRow As Long, sheetIndex As Long, row As Long, col As Long
For sheetIndex = 1 To Data.Count
    For row = 1 To UBound(Data(sheetIndex), 1)
        allInOneRow = allInOneRow + 1
        For col = 1 To UBound(Data(sheetIndex), 2)
            AllInOneData(allInOneRow, col) = Data(sheetIndex)(row, col)
        Next col
    Next row
Next sheetIndex

Обратите внимание, что вам не нужно писать Worksheets(ws.name).Cells, если ws уже является рабочим листом. Просто напишите ws.Cells.

Спасибо за вашу помощь ; к сожалению, когда я компилирую код, Excel зависает и выключается (я даже не могу остановить выполнение, нажав кнопку Ctrl + Stop), не волнуйтесь, код @FaneDuru работает. Я должен поблагодарить вас за вашу помощь и ваше время, вы мне очень помогли!

C K 26.06.2024 15:17

Прекрасный ответ +:); к вашему сведению, вы опубликовали альтернативу, используя вашу более раннюю вспомогательную функцию. @FunThomas

T.M. 28.06.2024 12:37

Альтернатива использованию новых функций динамических массивов

Вместо чтения всех элементов нескольких массивов в общий массив этот подход

  • добавляет только формулы динамического диапазона на лист (например, =A2:O100000) в скрытый временный лист и
  • назначает динамические блоки в общий массив data за один раз.

Поскольку это решение сводит к минимуму взаимодействие между прямыми табличными вычислениями Excel и VBA (не говоря уже о циклах по строкам), оно должно получить выгоду от хорошего или даже лучшего поведения во времени.

Sub MrgArrays()
Const firstRow = 2, firstCol = "A", lastCol = "O"
'0) define array of needed sheets (or choose empty array for ALL sheets)
    Dim SheetNames As Variant
    SheetNames = Array("Tabelle1", "Tabelle3") '<< define only needed sheets
'1) use temporary worksheet
    Dim temp As Worksheet
    Set temp = getTempSheet(ClearIt:=True)
'2) get individual sheet data address
    Dim nxt As Long: nxt = firstRow         ' start temp sheet row counter
    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Sheets(SheetNames)
    'a) find last sheet row
        Dim lastRow As Long
        lastRow = ws.cells(ws.Rows.Count, "A").End(xlUp).Row
        If ws.Name = temp.Name Then lastRow = 0
    'b) define next sheet address & add further dynamic formula to temp sheet
        Dim addr As String
        If lastRow >= firstRow Then
            'define next sheet address
            addr = firstCol & firstRow & ":" & lastCol & lastRow
            'add dynamic formula to temporary sheet
            temp.Range("A" & nxt).Formula2 = " = " & ws.Range(addr).Address(external:=True)
            'increment nxt
            nxt = nxt + lastRow - firstRow + 1
        End If
    Next
'3) build total array of temp sheet data
    lastRow = temp.cells(temp.Rows.Count, "A").End(xlUp).Row
    Dim data As Variant
    data = temp.Range(firstCol & firstRow & ":" & lastCol & lastRow).Value2
End Sub

Вспомогательная функция getTempSheet() (спасибо @funthomas)

Function getTempSheet(Optional wsName As String = "TEMP", _
    Optional ByVal ClearIt As Boolean = False, _
    Optional ByVal HideIt As Boolean = True) As Worksheet
'Site: https://stackoverflow.com/questions/76719083/populate-a-listbox-with-two-tables/76721164#76721164
'Auth: https://stackoverflow.com/users/7599798/funthomas
'Note: adapted 2024-06-28 by T.M. (https://stackoverflow.com/users/6460297/t-m)
'Purp: Create the helper sheet if it is not present.
    On Error Resume Next
    Set getTempSheet = ThisWorkbook.Sheets(wsName)
    On Error GoTo 0                 ' referenced sheet is not present
    If getTempSheet Is Nothing Then
        ThisWorkbook.Sheets.Add
        Set getTempSheet = ActiveSheet
        getTempSheet.Name = wsName
    End If
    ' optional code lines
    If ClearIt Then getTempSheet.cells.Clear                
    getTempSheet.Visible = IIf(HideIt, xlSheetHidden, xlSheetVisible)
End Function

Я проголосовал за ваш код. Но только потому, что мне (нравится верить) я знаю тебя... Много лет назад я был virtuoso of formulas. Пока рабочая книга не стала огромной и все значительно не замедлилось. С тех пор я больше не верю в силу формул. До сих пор ничто не доказывало (мне), что любые формулы, применяемые к огромным диапазонам, могут быть быстрее, чем VBA, работающий в памяти. Но это не значит, что вы не можете быть правы. Вы тестировали этот способ в диапазоне, содержащем 100 тыс. строк? Сопоставимо ли это по скорости с методом массивов?

FaneDuru 30.06.2024 11:27

@FaneDuru Спасибо за комментарий; после быстрого теста кажется, что начиная с 4 листов мое решение с 100 000 строк и 15 столбцами работает быстрее.

T.M. 01.07.2024 19:51

Хорошо, я скопирую код и попробую использовать это решение в будущем. Когда вы говорите о новых функциях динамических массивов, что вы имеете в виду? С каких пор этот метод доступен в VBA? Но простое размещение последовательных диапазонов в массиве и копирование их один за другим не будет ли это быстрее? Вы пробовали? Мне до сих пор не нравятся формулы массовой обработки...

FaneDuru 06.07.2024 14:34

Apprec.feedback: Повышенная скорость Excel по сравнению с 64-разрядной версией (по сравнению с 2010 годом) и особенно с MS365 также делает использование формул более привлекательным, чем раньше. Конечно, следует отдавать предпочтение вычислениям в памяти, но целенаправленное использование формул также может быть полезным - что действительно замедляет работу, так это взаимодействие между Excel и VBA, например. при циклическом переборе диапазонов строка за строкой. Мой подход заключался в том, чтобы ограничить присваивание массивами и использовать n раз мини-формулы с одним общим присваиванием вместо n раз по 100 000 циклов. - К сожалению, у меня пока нет времени на систематическое тестирование. @FaneDuru

T.M. 06.07.2024 21:07

Получить данные со всех листов

Приложение

Sub GetDataFromAllSheetsTEST()
    
    Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
    Dim Data As Variant: Data = GetDataFromAllSheets(wb, , True)
    If IsEmpty(Data) Then Exit Sub
    
    ' Continue working with the data...
    
End Sub

Вспомогательная функция

Function GetDataFromAllSheets( _
    ByVal wb As Workbook, _
    Optional ByVal HeaderRowsCount As Long = 1, _
    Optional ByVal DisplayMessages As Boolean = False) _
As Variant
    Const PROC_TITLE As String = "Get Data From All Sheets"
    
    ' Validate number of worksheets.
    
    Dim iwCount As Long: iwCount = wb.Worksheets.Count
    If iwCount = 0 Then
        If DisplayMessages Then
            MsgBox "No worksheets found in " _
                & IIf(Len(wb.Path) = 0, "the never saved ", "") _
                & "workbook """ & wb.Name & """" _
                & IIf(Len(wb.Path) = 0, "", " located in """ & wb.Path & """") _
                & "!", vbExclamation, PROC_TITLE
        End If
        Exit Function
    End If
    
    ' Loop through the worksheets to create a reference to each range
    ' in the 1st column and return the number of rows in the 2nd column
    ' of the Worksheets Data array ('wsData').
    
    Dim wsData() As Variant: ReDim wsData(1 To iwCount, 1 To 2)
    
    Dim ws As Worksheet, rg As Range, wCount As Long
    Dim srCount As Long, cCount As Long, trCount As Long, tcCount As Long
    Dim IsColumnsCountDetermined As Boolean, IsNoDataInSheet As Boolean
    
    For Each ws In wb.Worksheets
        ' Assuming the list starts in 'A1' with no empty rows or columns.
        With ws.Range("A1").CurrentRegion
            srCount = .Rows.Count - HeaderRowsCount
            If srCount > 0 Then
                Set rg = .Resize(srCount).Offset(HeaderRowsCount)
                cCount = .Columns.Count
            Else
                IsNoDataInSheet = True
            End If
        End With
        If IsNoDataInSheet Then
            IsNoDataInSheet = False
        Else
            If Not IsColumnsCountDetermined Then
                tcCount = cCount
                IsColumnsCountDetermined = True
            End If
            If cCount <> tcCount Then
                MsgBox "The number of columns is different in worksheet """ _
                    & ws.Name & """!", vbExclamation, PROC_TITLE
                Exit Function
            End If
            wCount = wCount + 1
            Set wsData(wCount, 1) = rg
            wsData(wCount, 2) = srCount
            trCount = trCount + srCount
        End If
    Next ws
        
    ' Validate the number of worksheets with data (ranges).
    
    If wCount = 0 Then
        If DisplayMessages Then
            MsgBox "No data found in " _
                & IIf(Len(wb.Path) = 0, "the never saved ", "") _
                & "workbook """ & wb.Name & """" _
                & IIf(Len(wb.Path) = 0, "", " located in """ & wb.Path & """") _
                & "!", vbExclamation, PROC_TITLE
        End If
        Exit Function
    End If
    
    ' Loop through the Worksheets Data array and return the values
    ' from each range in the resulting Total array ('tData').
    
    Dim tData() As Variant: ReDim tData(1 To trCount, 1 To tcCount)
    
    Dim sData() As Variant, w As Long, sr As Long, tr As Long, c As Long
    
    For w = 1 To wCount
        srCount = wsData(w, 2)
        If srCount + cCount = 2 Then
            ReDim sData(1 To 1, 1 To 1): sData(1, 1) = wsData(w, 1).Value
        Else
            sData = wsData(w, 1).Value
        End If
        For sr = 1 To srCount
            tr = tr + 1
            For c = 1 To tcCount
                tData(tr, c) = sData(sr, c)
            Next c
        Next sr
    Next w
    
    GetDataFromAllSheets = tData
    
    If DisplayMessages Then
        MsgBox "Retrieved data from " & wCount & "(out of " & iwCount _
            & ") worksheet" & IIf(iwCount = 1, "", "s") & "." & vbLf & vbLf _
            & "Number of rows: " & trCount & vbLf _
            & "Number of columns: " & tcCount, vbInformation, PROC_TITLE
    End If
    
End Function

Другие вопросы по теме