【问题标题】:Get Last non empty column and row index from excel using Interop使用互操作从 excel 中获取最后一个非空列和行索引
【发布时间】:2017-10-10 03:26:45
【问题描述】:

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

我关注了这个问题Fastest method to remove Empty rows and Columns From Excel Files using Interop,我觉得它很有帮助。

但我有包含一小组数据但有很多空行和列的 excel 文件(从最后一个非空行(或列)到工作表的末尾) p>

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

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

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

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

有什么建议吗?


注意:代码必须兼容 SSIS Script Task 环境

【问题讨论】:

  • 您是要向上删除第 17 行还是第 7、8、13 行?
  • 我刚刚尝试创建一个包含 10000 行的工作表,每隔一行都是空的。删除 5000 个空行需要 38 秒。
  • 好吧,我现在很困惑。应该删除什么?请编辑问题并添加将显示预期结果的图像。应该删除第 7、8、13 行吗?应该删除列HE 吗?
  • @Yahfoufi 我已经编辑了答案,请看一下。
  • “如果您将文件导入其他来源,有时您将有超过 100000 个空行” - 真正的问题是:如何清理工作表以便其 @ 987654327@ 属性仅包含数据(文本或数字)?此范围内的行和列可以为空吗?

标签: c# excel vb.net ssis etl


【解决方案1】:

更新 1

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

SELECT * FROM [Sheet1$A1:J16]

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

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


初步回答

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

而您正在尝试“获取包含数据的最后一行以删除所有额外的空白(在此行或列之后)”

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

空行存储在整数列表中rowsToDelete

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

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

如果NonEmptyRows.Max() &lt; 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();
        }
    }

【讨论】:

    【解决方案2】:

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

    基础代码使用 Microsoft.Office.Interop.Excel。

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

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

    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 文件的所有工作表,将它们显示在一个列表框中。从列表框中选择一个工作表名称,并在有效单元格地址中获取该工作表的最后一行和最后一列。

    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
    

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

    【讨论】:

    • 如果库代码在这个答案中或在 GitHub 或 CodeProject 中,那将非常方便,因为大多数工作场所不允许从 OneDrive 等下载
    • 不知道。我刚刚将我的解决方案推送到了 GitHub。 github.com/karenpayneoregon/excel-usedrowscolumns
    • 不错的一个,良好的代码,没有双点和良好的 COM 清理。一个建议是扩展方法ToDataTable它使用反射很慢,如果您像这样使用FastMember,您会惊讶于性能改进:Dim data As IEnumerable(Of AccountInfo) = Accounts.GetAccounts(False) Using reader = FastMember.ObjectReader.Create(data, properties) dt.Load(reader) End Using
    • @JeremyThompson 感谢您的评论。关于 ToDataTable,我同意。我最初是在很多年前写的,从未更新过。这个周末我会更新它(当我有时间的时候)。
    【解决方案3】:

    我正在使用 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 行。不快,但比“小时”好得多。这当然取决于您要处理的行/列数,当然您没有说。 但是,在对 50000 个空行中的 25000 个空行进行进一步测试后,在循环中删除空行确实需要大约 30 分钟。显然删除行不是一个有效的过程。

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

    第 1 步 - 创建包含 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 步 - 将包含数据的行复制到新工作表中。这需要 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 步 - 这将对列执行相同的操作。

    【讨论】:

    • 提示:只要您可以使用object[,] 数组来克服因使用每个单元格而产生的互操作和 RCW 对性能的影响。见stackoverflow.com/a/2294087/495455
    • @JeremyThompson ClosedXml 和 OpenXml 不使用互操作
    • Doh,这是一个#fail
    【解决方案4】:
    • 要获取最后一个非空列/行索引,可以使用 Excel 函数Find。见GetLastIndexOfNonEmptyCell
    • 然后Excel 工作表函数CountA 用于确定单元格是否为空并将整个行/列联合到一个行/列范围。
    • 这个范围最终被立即删除。

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

    用于获取空行/列范围的两个函数可以重构为一个函数,如下所示:

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

    【讨论】:

    • 感谢您的更新。这种方法类似于旧的接受的答案,但速度并不快。
    • 也许尝试添加新工作表并将使用过的范围复制粘贴到其中,但我认为它也不会更快。
    【解决方案5】:

    假设最后一个带有数据的角单元格是 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
    

    【讨论】:

      【解决方案6】:

      您应该能够找到最后一个非空行和列,类似于以下内容:

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

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

      【讨论】:

        【解决方案7】:

        您的问题似乎已由 Microsoft 解决。看看Range.CurrentRegion Property,它返回一个由空白行和空白列的任意组合限定的范围。有一个不便之处:此属性不能用于受保护的工作表

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

        一些 SO 成员提到了 UsedRange property,这可能也很有用,但与 CurrentRegion 的不同之处在于 UsedRange 返回的范围包括曾经使用过的任何单元格。
        因此,如果您想获得数据占用的LAST(row)LAST(column),则必须使用End propertyXlDirectionxlToLeft 和/或xlUp

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

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

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

        祝你好运!

        【讨论】:

        • 感谢您提供有用的信息。但是在我正在处理的用例中它不起作用,因为它不是格式正确的表格格式
        【解决方案8】:

        我认为你可以尝试使用 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;
        

        上面的代码对我有用。

        【讨论】:

        • 它不适用于额外的空白字段。这仅适用于最佳情况
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-11-18
        • 1970-01-01
        • 2014-11-10
        • 1970-01-01
        • 1970-01-01
        • 2011-07-20
        相关资源
        最近更新 更多