web-dev-qa-db-fra.com

Méthode la plus rapide pour supprimer les lignes et colonnes vides des fichiers Excel à l'aide d'Interop

J'ai beaucoup de fichiers Excel qui contiennent des données et qui contiennent des lignes et des colonnes vides. comme indiqué ci-dessous

Excel preview

J'essaie de supprimer des lignes et des colonnes vides d'Excel en utilisant l'interopérabilité. J'ai créé une application Winform simple et utilisé le code suivant et cela fonctionne très bien.

Dim lstFiles As New List(Of String)
lstFiles.AddRange(IO.Directory.GetFiles(m_strFolderPath, "*.xls", IO.SearchOption.AllDirectories))

Dim m_XlApp = New Excel.Application
Dim m_xlWrkbs As Excel.Workbooks = m_XlApp.Workbooks
Dim m_xlWrkb As Excel.Workbook

For Each strFile As String In lstFiles
    m_xlWrkb = m_xlWrkbs.Open(strFile)
    Dim m_XlWrkSheet As Excel.Worksheet = m_xlWrkb.Worksheets(1)
    Dim intRow As Integer = 1

    While intRow <= m_XlWrkSheet.UsedRange.Rows.Count
        If m_XlApp.WorksheetFunction.CountA(m_XlWrkSheet.Cells(intRow, 1).EntireRow) = 0 Then
            m_XlWrkSheet.Cells(intRow, 1).EntireRow.Delete(Excel.XlDeleteShiftDirection.xlShiftUp)
        Else
            intRow += 1
        End If
    End While

    Dim intCol As Integer = 1
    While intCol <= m_XlWrkSheet.UsedRange.Columns.Count
        If m_XlApp.WorksheetFunction.CountA(m_XlWrkSheet.Cells(1, intCol).EntireColumn) = 0 Then
            m_XlWrkSheet.Cells(1, intCol).EntireColumn.Delete(Excel.XlDeleteShiftDirection.xlShiftToLeft)
        Else
            intCol += 1
        End If
    End While
Next

m_xlWrkb.Save()
m_xlWrkb.Close(SaveChanges:=True)

Marshal.ReleaseComObject(m_xlWrkb)
Marshal.ReleaseComObject(m_xlWrkbs)
m_XlApp.Quit()
Marshal.ReleaseComObject(m_XlApp)

Mais lors du nettoyage de gros fichiers Excel, cela prend beaucoup de temps. Des suggestions pour optimiser ce code? ou une autre façon de nettoyer ces fichiers Excel plus rapidement? Existe-t-il une fonction qui peut supprimer des lignes vides en un seul clic?

Je n'ai pas de problème si les réponses utilisent C #

MODIFIER:

J'ai téléchargé un exemple de fichier exemple de fichier . Mais tous les fichiers n'ont pas la même structure.

21
Hadi

J'ai trouvé que parcourir la feuille de calcul Excel peut prendre un certain temps si la feuille de calcul est grande. Ma solution a donc essayé d'éviter toute boucle dans la feuille de calcul. Pour éviter de parcourir la feuille de calcul, j'ai créé un tableau d'objets bidimensionnel à partir des cellules renvoyées par usedRange avec:

Excel.Range targetCells = worksheet.UsedRange;
object[,] allValues = (object[,])targetCells.Cells.Value;

Il s'agit du tableau que je boucle pour obtenir les index des lignes et colonnes vides. Je fais 2 listes int, l'une garde les index de ligne à supprimer, l'autre garde les index de colonne à supprimer.

List<int> emptyRows = GetEmptyRows(allValues, totalRows, totalCols);
List<int> emptyCols = GetEmptyCols(allValues, totalRows, totalCols);

Ces listes seront triées de haut en bas pour simplifier la suppression de lignes de bas en haut et la suppression de colonnes de droite à gauche. Parcourez ensuite chaque liste et supprimez la ligne/colonne appropriée.

DeleteRows(emptyRows, worksheet);
DeleteCols(emptyCols, worksheet);

Enfin, une fois toutes les lignes et colonnes vides supprimées, j'enregistre le fichier sous un nouveau nom de fichier.

J'espère que cela t'aides.

MODIFIER:

Résolution du problème UsedRange de sorte que s'il y a des lignes vides en haut de la feuille de calcul, ces lignes seront désormais supprimées. Cela supprimera également toutes les colonnes vides à gauche des données de départ. Cela permet à l'indexation de fonctionner correctement même s'il y a des lignes ou des colonnes vides avant le démarrage des données. Cela a été accompli en prenant l'adresse de la première cellule dans UsedRange ce sera une adresse de la forme "$ A $ 1: $ D $ 4". Cela permettra l'utilisation d'un décalage si les lignes vides en haut et les colonnes vides à gauche doivent rester et ne pas être supprimées. Dans ce cas, je les supprime simplement. Pour obtenir le nombre de lignes à supprimer par le haut, vous pouvez calculer la première adresse "$ A $ 4" où le "4" est la ligne où les premières données apparaissent. Nous devons donc supprimer les 3 premières lignes. L'adresse de la colonne est de la forme "A", "AB" ou même "AAD", cela a nécessité une traduction et grâce à Comment convertir un numéro de colonne (par exemple 127) en une colonne Excel (par exemple AA) J'ai pu déterminer combien de colonnes à gauche doivent être supprimées.

class Program {
  static void Main(string[] args) {
    Excel.Application Excel = new Excel.Application();
    string originalPath = @"H:\ExcelTestFolder\Book1_Test.xls";
    Excel.Workbook workbook = Excel.Workbooks.Open(originalPath);
    Excel.Worksheet worksheet = workbook.Worksheets["Sheet1"];
    Excel.Range usedRange = worksheet.UsedRange;

    RemoveEmptyTopRowsAndLeftCols(worksheet, usedRange);

    DeleteEmptyRowsCols(worksheet);

    string newPath = @"H:\ExcelTestFolder\Book1_Test_Removed.xls";
    workbook.SaveAs(newPath, Excel.XlSaveAsAccessMode.xlNoChange);

    workbook.Close();
    Excel.Quit();
    System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
    System.Runtime.InteropServices.Marshal.ReleaseComObject(Excel);
    Console.WriteLine("Finished removing empty rows and columns - Press any key to exit");
    Console.ReadKey();
  }

  private static void DeleteEmptyRowsCols(Excel.Worksheet worksheet) {
    Excel.Range targetCells = worksheet.UsedRange;
    object[,] allValues = (object[,])targetCells.Cells.Value;
    int totalRows = targetCells.Rows.Count;
    int totalCols = targetCells.Columns.Count;

    List<int> emptyRows = GetEmptyRows(allValues, totalRows, totalCols);
    List<int> emptyCols = GetEmptyCols(allValues, totalRows, totalCols);

    // now we have a list of the empty rows and columns we need to delete
    DeleteRows(emptyRows, worksheet);
    DeleteCols(emptyCols, worksheet);
  }

  private static void DeleteRows(List<int> rowsToDelete, Excel.Worksheet worksheet) {
    // the rows are sorted high to low - so index's wont shift
    foreach (int rowIndex in rowsToDelete) {
      worksheet.Rows[rowIndex].Delete();
    }
  }

  private static void DeleteCols(List<int> colsToDelete, Excel.Worksheet worksheet) {
    // the cols are sorted high to low - so index's wont shift
    foreach (int colIndex in colsToDelete) {
      worksheet.Columns[colIndex].Delete();
    }
  }

  private static List<int> GetEmptyRows(object[,] allValues, int totalRows, int totalCols) {
    List<int> emptyRows = new List<int>();

    for (int i = 1; i < totalRows; i++) {
      if (IsRowEmpty(allValues, i, totalCols)) {
        emptyRows.Add(i);
      }
    }
    // sort the list from high to low
    return emptyRows.OrderByDescending(x => x).ToList();
  }

  private static List<int> GetEmptyCols(object[,] allValues, int totalRows, int totalCols) {
    List<int> emptyCols = new List<int>();

    for (int i = 1; i < totalCols; i++) {
      if (IsColumnEmpty(allValues, i, totalRows)) {
        emptyCols.Add(i);
      }
    }
    // sort the list from high to low
    return emptyCols.OrderByDescending(x => x).ToList();
  }

  private static bool IsColumnEmpty(object[,] allValues, int colIndex, int totalRows) {
    for (int i = 1; i < totalRows; i++) {
      if (allValues[i, colIndex] != null) {
        return false;
      }
    }
    return true;
  }

  private static bool IsRowEmpty(object[,] allValues, int rowIndex, int totalCols) {
    for (int i = 1; i < totalCols; i++) {
      if (allValues[rowIndex, i] != null) {
        return false;
      }
    }
    return true;
  }

  private static void RemoveEmptyTopRowsAndLeftCols(Excel.Worksheet worksheet, Excel.Range usedRange) {
    string addressString = usedRange.Address.ToString();
    int rowsToDelete = GetNumberOfTopRowsToDelete(addressString);
    DeleteTopEmptyRows(worksheet, rowsToDelete);
    int colsToDelete = GetNumberOfLeftColsToDelte(addressString);
    DeleteLeftEmptyColumns(worksheet, colsToDelete);
  }

  private static void DeleteTopEmptyRows(Excel.Worksheet worksheet, int startRow) {
    for (int i = 0; i < startRow - 1; i++) {
      worksheet.Rows[1].Delete();
    }
  }

  private static void DeleteLeftEmptyColumns(Excel.Worksheet worksheet, int colCount) {
    for (int i = 0; i < colCount - 1; i++) {
      worksheet.Columns[1].Delete();
    }
  }

  private static int GetNumberOfTopRowsToDelete(string address) {
    string[] splitArray = address.Split(':');
    string firstIndex = splitArray[0];
    splitArray = firstIndex.Split('$');
    string value = splitArray[2];
    int returnValue = -1;
    if ((int.TryParse(value, out returnValue)) && (returnValue >= 0))
      return returnValue;
    return returnValue;
  }

  private static int GetNumberOfLeftColsToDelte(string address) {
    string[] splitArray = address.Split(':');
    string firstindex = splitArray[0];
    splitArray = firstindex.Split('$');
    string value = splitArray[1];
    return ParseColHeaderToIndex(value);
  }

  private static int ParseColHeaderToIndex(string colAdress) {
    int[] digits = new int[colAdress.Length];
    for (int i = 0; i < colAdress.Length; ++i) {
      digits[i] = Convert.ToInt32(colAdress[i]) - 64;
    }
    int mul = 1; int res = 0;
    for (int pos = digits.Length - 1; pos >= 0; --pos) {
      res += digits[pos] * mul;
      mul *= 26;
    }
    return res;
  }
}

EDIT 2: Pour les tests, j'ai fait une méthode qui boucle à travers la feuille de calcul et je l'ai comparée à mon code qui boucle à travers un tableau d'objets. Cela montre une différence significative.

enter image description here

Méthode pour faire une boucle dans la feuille de calcul et supprimer les lignes et colonnes vides.

enum RowOrCol { Row, Column };
private static void ConventionalRemoveEmptyRowsCols(Excel.Worksheet worksheet) {
  Excel.Range usedRange = worksheet.UsedRange;
  int totalRows = usedRange.Rows.Count;
  int totalCols = usedRange.Columns.Count;

  RemoveEmpty(usedRange, RowOrCol.Row);
  RemoveEmpty(usedRange, RowOrCol.Column);
}

private static void RemoveEmpty(Excel.Range usedRange, RowOrCol rowOrCol) {
  int count;
  Excel.Range curRange;
  if (rowOrCol == RowOrCol.Column)
    count = usedRange.Columns.Count;
  else
    count = usedRange.Rows.Count;

  for (int i = count; i > 0; i--) {
    bool isEmpty = true;
    if (rowOrCol == RowOrCol.Column)
      curRange = usedRange.Columns[i];
    else
      curRange = usedRange.Rows[i];

    foreach (Excel.Range cell in curRange.Cells) {
      if (cell.Value != null) {
        isEmpty = false;
        break; // we can exit this loop since the range is not empty
      }
      else {
        // Cell value is null contiue checking
      }
    } // end loop thru each cell in this range (row or column)

    if (isEmpty) {
      curRange.Delete();
    }
  }
}

Puis un Main pour tester/chronométrer les deux méthodes.

enum RowOrCol { Row, Column };

static void Main(string[] args)
{
  Excel.Application Excel = new Excel.Application();
  string originalPath = @"H:\ExcelTestFolder\Book1_Test.xls";
  Excel.Workbook workbook = Excel.Workbooks.Open(originalPath);
  Excel.Worksheet worksheet = workbook.Worksheets["Sheet1"];
  Excel.Range usedRange = worksheet.UsedRange;

  // Start test for looping thru each Excel worksheet
  Stopwatch sw = new Stopwatch();
  Console.WriteLine("Start stopwatch to loop thru WORKSHEET...");
  sw.Start();
  ConventionalRemoveEmptyRowsCols(worksheet);
  sw.Stop();
  Console.WriteLine("It took a total of: " + sw.Elapsed.Milliseconds + " Miliseconds to remove empty rows and columns...");

  string newPath = @"H:\ExcelTestFolder\Book1_Test_RemovedLoopThruWorksheet.xls";
  workbook.SaveAs(newPath, Excel.XlSaveAsAccessMode.xlNoChange);
  workbook.Close();
  Console.WriteLine("");

  // Start test for looping thru object array
  workbook = Excel.Workbooks.Open(originalPath);
  worksheet = workbook.Worksheets["Sheet1"];
  usedRange = worksheet.UsedRange;
  Console.WriteLine("Start stopwatch to loop thru object array...");
  sw = new Stopwatch();
  sw.Start();
  DeleteEmptyRowsCols(worksheet);
  sw.Stop();

  // display results from second test
  Console.WriteLine("It took a total of: " + sw.Elapsed.Milliseconds + " Miliseconds to remove empty rows and columns...");
  string newPath2 = @"H:\ExcelTestFolder\Book1_Test_RemovedLoopThruArray.xls";
  workbook.SaveAs(newPath2, Excel.XlSaveAsAccessMode.xlNoChange);
  workbook.Close();
  Excel.Quit();
  System.Runtime.InteropServices.Marshal.ReleaseComObject(workbook);
  System.Runtime.InteropServices.Marshal.ReleaseComObject(Excel);
  Console.WriteLine("");
  Console.WriteLine("Finished testing methods - Press any key to exit");
  Console.ReadKey();
}

EDIT 3 Selon la demande OP ... J'ai mis à jour et changé le code pour qu'il corresponde au code OP. Avec cela, j'ai trouvé des résultats intéressants. Voir ci-dessous.

J'ai changé le code pour qu'il corresponde aux fonctions que vous utilisez, c'est-à-dire… FullyRow et CountA. Le code ci-dessous, j'ai trouvé qu'il préforme terriblement. En exécutant certains tests, j'ai trouvé que le code ci-dessous était dans le temps d'exécution de 800+ millisecondes. Cependant, un changement subtil a fait une énorme différence.

Sur la ligne:

while (rowIndex <= worksheet.UsedRange.Rows.Count)

Cela ralentit beaucoup les choses. Si vous créez une variable de plage pour UsedRang et que vous ne la gardez pas regrabbibg à chaque itération de la boucle while, cela fera une énorme différence. Donc… quand je change la boucle while en…

Excel.Range usedRange = worksheet.UsedRange;
int rowIndex = 1;

while (rowIndex <= usedRange.Rows.Count)
and
while (colIndex <= usedRange.Columns.Count)

Cela a fonctionné très près de ma solution de tableau d'objets. Je n'ai pas posté les résultats, car vous pouvez utiliser le code ci-dessous et modifier la boucle while pour saisir le UsedRange à chaque itération ou utiliser la variable usedRange pour tester cela.

private static void RemoveEmptyRowsCols3(Excel.Worksheet worksheet) {
  //Excel.Range usedRange = worksheet.UsedRange;     // <- using this variable makes the while loop much faster 
  int rowIndex = 1;

  // delete empty rows
  //while (rowIndex <= usedRange.Rows.Count)     // <- changing this one line makes a huge difference - not grabbibg the UsedRange with each iteration...
  while (rowIndex <= worksheet.UsedRange.Rows.Count) {
    if (Excel.WorksheetFunction.CountA(worksheet.Cells[rowIndex, 1].EntireRow) == 0) {
      worksheet.Cells[rowIndex, 1].EntireRow.Delete(Excel.XlDeleteShiftDirection.xlShiftUp);
    }
    else {
      rowIndex++;
    }
  }

  // delete empty columns
  int colIndex = 1;
  // while (colIndex <= usedRange.Columns.Count) // <- change here also

  while (colIndex <= worksheet.UsedRange.Columns.Count) {
    if (Excel.WorksheetFunction.CountA(worksheet.Cells[1, colIndex].EntireColumn) == 0) {
      worksheet.Cells[1, colIndex].EntireColumn.Delete(Excel.XlDeleteShiftDirection.xlShiftToLeft);
    }
    else {
      colIndex++;
    }
  }
}

MISE À JOUR par @ Hadi

Vous pouvez modifier les fonctions DeleteCols et DeleteRows pour obtenir de meilleures performances si Excel contient des lignes et des colonnes vides supplémentaires après les dernières utilisées:

private static void DeleteRows(List<int> rowsToDelete, Microsoft.Office.Interop.Excel.Worksheet worksheet)
{
    // the rows are sorted high to low - so index's wont shift

    List<int> NonEmptyRows = Enumerable.Range(1, rowsToDelete.Max()).ToList().Except(rowsToDelete).ToList();

    if (NonEmptyRows.Max() < rowsToDelete.Max())
    {

        // there are empty rows after the last non empty row

        Microsoft.Office.Interop.Excel.Range cell1 = worksheet.Cells[NonEmptyRows.Max() + 1,1];
        Microsoft.Office.Interop.Excel.Range cell2 = worksheet.Cells[rowsToDelete.Max(), 1];

        //Delete all empty rows after the last used row
        worksheet.Range[cell1, cell2].EntireRow.Delete(Microsoft.Office.Interop.Excel.XlDeleteShiftDirection.xlShiftUp);


    }    //else last non empty row = worksheet.Rows.Count



    foreach (int rowIndex in rowsToDelete.Where(x => x < NonEmptyRows.Max()))
    {
        worksheet.Rows[rowIndex].Delete();
    }
}

private static void DeleteCols(List<int> colsToDelete, Microsoft.Office.Interop.Excel.Worksheet worksheet)
{
    // the cols are sorted high to low - so index's wont shift

    //Get non Empty Cols
    List<int> NonEmptyCols = Enumerable.Range(1, colsToDelete.Max()).ToList().Except(colsToDelete).ToList();

    if (NonEmptyCols.Max() < colsToDelete.Max())
    {

        // there are empty rows after the last non empty row

        Microsoft.Office.Interop.Excel.Range cell1 = worksheet.Cells[1,NonEmptyCols.Max() + 1];
        Microsoft.Office.Interop.Excel.Range cell2 = worksheet.Cells[1,NonEmptyCols.Max()];

        //Delete all empty rows after the last used row
        worksheet.Range[cell1, cell2].EntireColumn.Delete(Microsoft.Office.Interop.Excel.XlDeleteShiftDirection.xlShiftToLeft);


    }            //else last non empty column = worksheet.Columns.Count

    foreach (int colIndex in colsToDelete.Where(x => x < NonEmptyCols.Max()))
    {
        worksheet.Columns[colIndex].Delete();
    }
}

vérifiez ma réponse à Obtenir la dernière colonne non vide et l'index de ligne d'Excel en utilisant Interop

18
JohnG

Peut-être quelque chose à considérer:

Sub usedRangeDeleteRowsCols()
    Dim LastRow, LastCol, i As Long

    LastRow = Cells.Find(What:="*", SearchDirection:=xlPrevious, SearchOrder:=xlByRows).Row
    LastCol = Cells.Find(What:="*", SearchDirection:=xlPrevious, SearchOrder:=xlByColumns).Column

    For i = LastRow To 1 Step -1
        If WorksheetFunction.CountA(Range(Cells(i, 1), Cells(i, LastCol))) = 0 Then
            Cells(i, 1).EntireRow.Delete
        End If
    Next

    For i = LastCol To 1 Step -1
        If WorksheetFunction.CountA(Range(Cells(1, i), Cells(LastRow, i))) = 0 Then
            Cells(1, i).EntireColumn.Delete
        End If
    Next
End Sub

I pensez il y a deux efficacités par rapport aux fonctions équivalentes dans le code original. Premièrement, au lieu d'utiliser la propriété UsedRange non fiable d'Excel, nous trouvons la dernière valeur et analysons uniquement les lignes et les colonnes dans la plage utilisée authentique.

Deuxièmement, la fonction de comptage des feuilles de calcul ne fonctionne à nouveau que dans la plage réellement utilisée - par exemple, lorsque vous recherchez des lignes vides, nous ne regardons que dans la plage de colonnes utilisées (plutôt que .EntireRow).

Les boucles For fonctionnent à l'envers car, par exemple, chaque fois qu'une ligne est supprimée, l'adresse de ligne des données suivantes change. Un travail en arrière signifie que les adresses de ligne des "données à travailler" ne changent pas.

5
David

À mon avis, la partie la plus longue pourrait être l'énumération et la recherche de lignes et de colonnes vides.

Qu'en est-il de: http://www.howtogeek.com/206696/how-to-quickly-and-easily-delete-blank-rows-and-columns-in-Excel-2013/

MODIFIER:

Qu'en est-il de:

m_XlWrkSheet.Columns("A:A").SpecialCells(xlCellTypeBlanks).EntireRow.Delete
m_XlWrkSheet.Rows("1:1").SpecialCells(xlCellTypeBlanks).EntireColumn.Delete

Testé sur des exemples de résultats de données semble correct, de meilleures performances (testé à partir de VBA mais la différence est énorme).

MISE À JOUR:

Testé sur un exemple d'Excel avec 14k lignes (faites à partir de données d'exemple) de code original ~ 30 s, cette version <1s

2
smartobelix

Le moyen le plus simple que je connaisse est de masquer les cellules non vides et de supprimer les cellules visibles:

var range = m_XlWrkSheet.UsedRange;
range.SpecialCells(XlCellType.xlCellTypeConstants).EntireRow.Hidden = true;
range.SpecialCells(XlCellType.xlCellTypeVisible).Delete(XlDeleteShiftDirection.xlShiftUp);
range.EntireRow.Hidden = false;

Les méthodes plus rapides consistent à ne rien supprimer du tout, mais à déplacer (couper + coller) les zones non vides.

La méthode Interop la plus rapide (il existe des méthodes plus rapides et plus compliquées sans ouvrir le fichier) consiste à obtenir toutes les valeurs dans le tableau, à déplacer les valeurs dans le tableau et à les remettre:

object[,] values = m_XlWrkSheet.UsedRange.Value2 as object[,];

// some code here (the values start from values[1, 1] not values[0, 0])

m_XlWrkSheet.UsedRange.Value2 = values;
2
Slai

Vous pouvez ouvrir une connexion ADO à la feuille de calcul, obtenir une liste de champs, émettre une instruction SQL qui inclut uniquement les champs connus et également exclure des enregistrements sans valeur dans les champs connus.

0
Zev Spitz