使用 Interop 从 excel 获取最后一个非空列和行索引

Get Last non empty column and row index from excel using Interop

我正在尝试使用 Interop Library 从 excel 文件中删除所有多余的空白行和列。

我关注了这个问题 ,我发现它很有帮助。

但我有 excel 个文件,其中包含一小部分数据,但有很多空行和空列 (从最后一个非空行(或列)到工作表)

我尝试循环遍历行和列,但循环需要几个小时。

我正在尝试获取最后一个非空行和列索引,以便我可以在一行中删除整个空范围

XlWks.Range("...").EntireRow.Delete(xlShiftUp)

注意:我正在尝试获取包含数据的最后一行以删除所有多余的空白(在此行或列之后)

有什么建议吗?


注意:代码必须兼容SSIS脚本任务环境

您应该能够找到与此类似的最后一个非空行和列:

with m_XlWrkSheet
lastRow = .UsedRange.Rows.Count
lastCol = .UsedRange.Columns.Count
end with

那是 VB.NET,但它应该或多或少起作用。那将 return 第 16 行和第 10 列(根据您上面的图片)。然后你可以用它来找到你想在一行中全部删除的范围。

我正在使用 ClosedXml,它具有有用的 'LastUsedRow' 和 'LastUsedColumn' 方法。

var wb = new XLWorkbook(@"<path>\test.xlsx", XLEventTracking.Disabled);
var sheet = wb.Worksheet("Sheet1");

for (int i = sheet.LastRowUsed().RowNumber() - 1; i >= 1; i--)
{
    var row = sheet.Row(i);
    if (row.IsEmpty())
    {
        row.Delete();
    }
}

wb.Save();

这个简单的循环在 38 秒内删除了 10000 行中的 5000 行。不快,但比 'hours' 好很多。这取决于您处理的 rows/columns 数量,当然您没有说。 但是,在对 50000 行中的 25000 行进行进一步测试后,删除循环中的空行确实需要大约 30 分钟。显然删除行不是一个有效的过程。

更好的解决方案是创建一个新的 sheet,然后复制您要保留的行。

第 1 步 - 创建 sheet 50000 行和 20 列,每隔一行和一列为空。

var wb = new XLWorkbook(@"C:\Users\passp\Documents\test.xlsx");
var sheet = wb.Worksheet("Sheet1");
sheet.Clear();

for (int i = 1; i < 50000; i+=2)
{
    var row = sheet.Row(i);

    for (int j = 1; j < 20; j += 2)
    {
        row.Cell(j).Value = i * j;
    }
}

第 2 步 - 将包含数据的行复制到新的 sheet。这需要 10 秒。

var wb = new XLWorkbook(@"C:\Users\passp\Documents\test.xlsx", XLEventTracking.Disabled);
var sheet = wb.Worksheet("Sheet1");

var sheet2 = wb.Worksheet("Sheet2");
sheet2.Clear();

sheet.RowsUsed()
    .Where(r => !r.IsEmpty())
    .Select((r, index) => new { Row = r, Index = index + 1} )
    .ForEach(r =>
    {
        var newRow = sheet2.Row(r.Index);

        r.Row.CopyTo(newRow);
    }
);

wb.Save();

第 3 步 - 这将对列执行相同的操作。

几年前我创建了一个 MSDN 代码示例,允许开发人员从作品中获取最后使用的行和列sheet。我对其进行了修改,将所有需要的代码放入一个带有 windows 表单前端的 class 库中以演示该操作。

底层代码使用Microsoft.Office.Interop.Excel。

Microsoft 一个驱动器上的位置 https://1drv.ms/u/s!AtGAgKKpqdWjiEGdBzWDCSCZAMaM

在这里,我获取 Excel 文件中的第一个 sheet,获取最后使用的行和列,并作为有效的单元格地址显示。

Private Sub cmdAddress1_Click(sender As Object, e As EventArgs) Handles cmdAddress1.Click
    Dim ops As New GetExcelColumnLastRowInformation
    Dim info = New UsedInformation
    ExcelInformationData = info.UsedInformation(FileName, ops.GetSheets(FileName))

    Dim SheetName As String = ExcelInformationData.FirstOrDefault.SheetName

    Dim cellAddress = (
        From item In ExcelInformationData
        Where item.SheetName = ExcelInformationData.FirstOrDefault.SheetName
        Select item.LastCell).FirstOrDefault

    MessageBox.Show($"{SheetName} - {cellAddress}")

End Sub

在演示项目中,我还获取了 excel 文件的所有 sheet,并将它们显示在列表框中。 Select 列表框中的 sheet 名称,并在有效的单元格地址中获取 sheet 的最后一行和最后一列。

Private Sub cmdAddress_Click(sender As Object, e As EventArgs) Handles cmdAddress.Click
    Dim cellAddress =
        (
            From item In ExcelInformationData
            Where item.SheetName = ListBox1.Text
            Select item.LastCell).FirstOrDefault

    If cellAddress IsNot Nothing Then
        MessageBox.Show($"{ListBox1.Text} {cellAddress}")
    End If

End Sub

从上面的 link 打开解决方案时,乍一看,您会注意到有很多代码。代码是最优的,会立即释放所有对象。

  • 要获取最后一个非空 column/row 索引,可以使用 Excel 函数 Find。参见 GetLastIndexOfNonEmptyCell
  • 然后Excel工作表函数CountA判断单元格是否为空和union整个 rows/columns 到一个 rows/columns 范围。
  • 最后一次删除这个范围。

public void Yahfoufi(string excelFile)
{
    var exapp = new Microsoft.Office.Interop.Excel.Application {Visible = true};
    var wrb = exapp.Workbooks.Open(excelFile);
    var sh = wrb.Sheets["Sheet1"];
    var lastRow = GetLastIndexOfNonEmptyCell(exapp, sh, XlSearchOrder.xlByRows);
    var lastCol = GetLastIndexOfNonEmptyCell(exapp, sh, XlSearchOrder.xlByColumns);
    var target = sh.Range[sh.Range["A1"], sh.Cells[lastRow, lastCol]];
    Range deleteRows = GetEmptyRows(exapp, target);
    Range deleteColumns = GetEmptyColumns(exapp, target);
    deleteColumns?.Delete();
    deleteRows?.Delete();
}

private static int GetLastIndexOfNonEmptyCell(
    Microsoft.Office.Interop.Excel.Application app,
    Worksheet sheet,
    XlSearchOrder searchOrder)
{
    Range rng = sheet.Cells.Find(
        What: "*",
        After: sheet.Range["A1"],
        LookIn: XlFindLookIn.xlFormulas,
        LookAt: XlLookAt.xlPart,
        SearchOrder: searchOrder,
        SearchDirection: XlSearchDirection.xlPrevious,
        MatchCase: false);
    if (rng == null)
        return 1;
    return searchOrder == XlSearchOrder.xlByRows
        ? rng.Row
        : rng.Column;
}

private static Range GetEmptyRows(
    Microsoft.Office.Interop.Excel.Application app,
    Range target)
{
    Range result = null;
    foreach (Range r in target.Rows)
    {
        if (app.WorksheetFunction.CountA(r.Cells) >= 1)
            continue;
        result = result == null
            ? r.EntireRow
            : app.Union(result, r.EntireRow);
    }
    return result;
}

private static Range GetEmptyColumns(
    Microsoft.Office.Interop.Excel.Application app,
    Range target)
{
    Range result = null;
    foreach (Range c in target.Columns)
    {
        if (app.WorksheetFunction.CountA(c.Cells) >= 1)
            continue;
        result = result == null
            ? c.EntireColumn
            : app.Union(result, c.EntireColumn);
    }
    return result;
}

获取空范围 rows/columns 的两个函数可以重构为一个函数,如下所示:

private static Range GetEntireEmptyRowsOrColumns(
    Microsoft.Office.Interop.Excel.Application app,
    Range target,
    Func<Range, Range> rowsOrColumns,
    Func<Range, Range> entireRowOrColumn)
{
    Range result = null;
    foreach (Range c in rowsOrColumns(target))
    {
        if (app.WorksheetFunction.CountA(c.Cells) >= 1)
            continue;
        result = result == null
            ? entireRowOrColumn(c)
            : app.Union(result, entireRowOrColumn(c));
    }
    return result;
}

然后直接调用它:

Range deleteColumns = GetEntireEmptyRowsOrColumns(exapp, target, (Func<Range, Range>)(r1 => r1.Columns), (Func<Range, Range>)(r2 => r2.EntireColumn));
Range deleteRows = GetEntireEmptyRowsOrColumns(exapp, target, (Func<Range, Range>)(r1 => r1.Rows), (Func<Range, Range>)(r2 => r2.EntireRow));
deleteColumns?.Delete();
deleteRows?.Delete();

注意:有关更多信息,请查看例如在 this SO question.

编辑

尝试简单地清除最后使用的单元格之后的所有单元格的内容。

public void Yahfoufi(string excelFile)
{
    var exapp = new Microsoft.Office.Interop.Excel.Application {Visible = true};
    var wrb = exapp.Workbooks.Open(excelFile);
    var sh = wrb.Sheets["Sheet1"];
    var lastRow = GetLastIndexOfNonEmptyCell(exapp, sh, XlSearchOrder.xlByRows);
    var lastCol = GetLastIndexOfNonEmptyCell(exapp, sh, XlSearchOrder.xlByColumns);

    // Clear the columns
    sh.Range(sh.Cells(1, lastCol + 1), sh.Cells(1, Columns.Count)).EntireColumn.Clear();

    // Clear the remaining cells
    sh.Range(sh.Cells(lastRow + 1, 1), sh.Cells(Rows.Count, lastCol)).Clear();

}

更新 1

如果您的目标是使用 c# 导入 excel 数据,假设您已确定工作表中使用率最高的索引 (在您发布的图片中,它是 Col = 10 , Row = 16),您可以将最大使用索引转换为字母,这样它将 J16 和 select 仅使用范围 OLEDBCommand

SELECT * FROM [Sheet1$A1:J16]

否则,我认为找到更快的方法并不容易。

您可以参考这些文章将索引转换为字母并使用 OLEDB 连接到 excel:

  • How to convert a column number (eg. 127) into an excel column (eg. AA)

初始答案

正如您所说,您是从以下问题开始的:

而您正在尝试 "get the last row containing data to remove all extra blanks (after this row , or column)"

所以假设你正在使用接受答案(由 @JohnG 提供),那么你可以添加一些代码行来获取最后使用的行和列

空行存储在整数列表中rowsToDelete

您可以使用以下代码获取索引小于最后一个空行的最后一个非空行

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

并且如果 NonEmptyRows.Max() < rowsToDelete.Max() 最后一个非空行是 NonEmptyRows.Max() 否则它是 worksheet.Rows.Count 并且在最后一个使用的行之后没有空行。

可以做同样的事情来获取最后一个非空列

代码在 DeleteColsDeleteRows 函数中编辑:

    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();
        }
    }

假设最后一个带有数据的角单元格是 J16 - 因此 K 列以上或第 17 行以下没有数据。你为什么要删除它们?场景是什么,你想达到什么目的?是否清除我们的格式?是否正在清除显示空字符串的公式?

无论如何,循环不是办法。

下面的代码显示了一种使用 Range 对象的 Clear() 方法来清除范围中的所有内容和公式以及格式的方法。或者,如果您确实要删除它们,可以使用 Delete() 方法一次删除整个矩形范围。会比循环快很多...

//code uses variables declared appropriately as Excel.Range & Excel.Worksheet Using Interop library
int x;
int y;
// get the row of the last value content row-wise
oRange = oSheet.Cells.Find(What: "*", 
                           After: oSheet.get_Range("A1"),
                           LookIn: XlFindLookIn.xlValues,
                           LookAt: XlLookAt.xlPart, 
                           SearchDirection: XlSearchDirection.xlPrevious,
                           SearchOrder: XlSearchOrder.xlByRows);

if (oRange == null)
{
    return;
}
x = oRange.Row;

// get the column of the last value content column-wise
oRange = oSheet.Cells.Find(What: "*",
                           After: oSheet.get_Range("A1"),
                           LookIn: XlFindLookIn.xlValues, LookAt: XlLookAt.xlPart,
                           SearchDirection: XlSearchDirection.xlPrevious,
                           SearchOrder: XlSearchOrder.xlByColumns);
y = oRange.Column;

// now we have the corner (x, y), we can delete or clear all content to the right and below
// say J16 is the cell, so x = 16, and j=10

Excel.Range clearRange;

//set clearRange to ("K1:XFD1048576")
clearRange = oSheet.Range[oSheet.Cells[1, y + 1], oSheet.Cells[oSheet.Rows.Count, oSheet.Columns.Count]];
clearRange.Clear(); //clears all content, formulas and formatting
//clearRange.Delete(); if you REALLY want to hard delete the rows

//set clearRange to ("A17:J1048576")            
clearRange = oSheet.Range[oSheet.Cells[x + 1, 1], oSheet.Cells[oSheet.Rows.Count, y]];
clearRange.Clear(); //clears all content, formulas and formatting
//clearRange.Delete();  if you REALLY want to hard delete the columns

看来你的问题已经被微软解决了。看看 Range.CurrentRegion Property,其中 returns 由空白行和空白列的任意组合界定的范围。有一个不便之处:此 属性 不能用于受保护的工作表

详情请见:How to Find Current Region, Used Range, Last Row and Last Column in Excel with VBA Macro

一些 SO 成员提到了 UsedRange property,这可能也很有用,但与 CurrentRegion 的不同之处在于 UsedRange returns 范围包括任何单元格曾经被使用过。
所以,如果你想得到一个LAST(row)LAST(column)被数据占用,你必须使用End propertyXlDirectionxlToLeftand/or xlUp

注意#1:
如果您的数据是表格格式,您可以简单地找到最后一个单元格,使用:

lastCell = yourWorkseet.UsedRange.End(xlUp)
firstEmtyRow = lastCell.Offset(RowOffset:=1).EntireRow

注意#2:
如果您的数据不是表格格式,您需要遍历行和列的集合以找到最后一个非空白单元格。

祝你好运!

我想你可以尝试使用 Range。

Application excel = new Application();
Workbook workBook=  excel.Workbooks.Open("file.xlsx")
Worksheet excelSheet = workBook.ActiveSheet;
Range excelRange = excelSheet.UsedRange.Columns[1, Missing.Value] as Range;

var lastNonEmptyRow = excelRange.Cells.Count;

上面的代码对我有用。