【问题标题】:VBA: Keep first and last rows of duplicate column values of an Excel sheetVBA:保留 Excel 工作表重复列值的第一行和最后一行
【发布时间】:2023-02-02 06:23:21
【问题描述】:

我有一个包含 20K 行的 Excel 工作表,如下所示:

header1 header2
1 P
2 P
3 P
4 Q
5 R
6 R
7 R
8 R
9 S
10 S

我想要一个 VBA 代码来删除包含重复项的行,但是保留第一行和最后一行的重复项。结果应该是这样的:

header1 header2
1 P
3 P
4 Q
5 R
8 R
9 S
10 S

我已经修改了here 中的以下代码来做到这一点,但每次我都必须手动选择包含列 header2 中重复项的范围。

Sub Delete_Dups_Keep_Last_v2()
 Dim SelRng As Range
 Dim Cell_in_Rng As Range
 Dim RngToDelete As Range
 Dim SelLastRow As Long
 
    Application.DisplayAlerts = False
    Set SelRng = Application.InputBox("Select cells", Type:=8)
    On Error GoTo 0
    Application.DisplayAlerts = True
 
    SelLastRow = SelRng.Rows.Count + SelRng.Row - 1
    For Each Cell_in_Rng In SelRng
        
        If Cell_in_Rng.Row < SelLastRow Then
            If Cell_in_Rng.Row > SelRng.Row Then
                If Not Cell_in_Rng.Offset(1, 0).Resize(SelLastRow - Cell_in_Rng.Row).Find(What:=Cell_in_Rng.Value, Lookat:=xlWhole) Is Nothing Then
                    'this value exists again in the range
                    If RngToDelete Is Nothing Then
                        Set RngToDelete = Cell_in_Rng
                    Else
                        Set RngToDelete = Application.Union(RngToDelete, Cell_in_Rng)
                    End If
                End If
            End If
        End If
        
    Next Cell_in_Rng
 
    If Not RngToDelete Is Nothing Then RngToDelete.EntireRow.Delete

End Sub

用户 A.S.H. 发现的另一个代码 here 通过使用词典自动进行手动选择和速度,但未能产生想要的结果。

Sub keepFirstAndLast()
  Dim toDelete As Range: Set toDelete = Sheet1.Rows(999999) '(to not start with a null range)
  Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")

  Dim a As Range
  For Each a In Sheet1.Range("B2", Sheet1.Range("B999999").End(xlUp))
    If Not dict.Exists(a.Value2) Then
      dict(a.Value2) = 0 ' first appearence, dont save the row
    Else
      ' if last observed occurrence was a duplicate, add it to deleted range
      If dict(a.Value2) > 0 Then Set toDelete = Union(toDelete, Sheet1.Rows(dict(a.Value2)))
      dict(a.Value2) = a.row ' not first appearence, save the row for eventual deletion
    End If
  Next
  toDelete.Delete
End Sub

【问题讨论】:

    标签: excel vba powerquery autofilter


    【解决方案1】:

    简单的解决方案:

    Sub KeepFirstLast()
    
    Application.ScreenUpdating = False
    
    Dim lastRow As Long
    lastRow = Sheets(1).Cells(Rows.Count, 1).End(xlUp).Row
    Dim i As Long
    Dim x As Long
    Dim currentValue As String
    
    For i = lastRow To 2 Step -1
        If i = 2 Then
            Application.ScreenUpdating = True
            Exit For
        End If
        currentValue = Sheets(1).Cells(i, 2).Value
        x = i - 1
        Do While Sheets(1).Cells(x, 2).Value = currentValue And Sheets(1).Cells(x - 1, 2).Value = currentValue
            Sheets(1).Rows(x).Delete
            x = x - 1
        Loop
        i = x + 1
    Next i
    
    
    Application.ScreenUpdating = True
    
    End Sub
    

    【讨论】:

    • 作为对 VBA 有基本了解的人,这是最容易理解和实现的代码。非常感谢!
    【解决方案2】:

    您可能会受益于 SpecialCells 根据公式选择那些行:

    Sub test()
    Dim LR As Long 'last row
    Dim LC As Long 'last column
    Dim SR As Long 'starting row
    Dim rng As Range
    
    Set rng = Range("A1") 'change this to TOP LEFT CELL OF YOUR DATA
    
    SR = rng.Row
    LR = rng.CurrentRegion.Cells(rng.CurrentRegion.Rows.Count, 1).Row
    LC = Cells(1, Columns.Count).End(xlToLeft).Column 'last column used
    
    'we add new column with formula to delete
    With Range(Cells(SR + 1, LC + 1), Cells(LR, LC + 1))
        .FormulaR1C1 = "=IF(OR(RC[-1]<>R[-1]C[-1],RC[-1]<>R[1]C[-1]),""x"",0)"
        .SpecialCells(xlCellTypeFormulas, 1).EntireRow.Delete
    End With
    
    'clear formula
    LR = rng.CurrentRegion.Cells(rng.CurrentRegion.Rows.Count, 1).Row
    Range(Cells(SR + 1, LC + 1), Cells(LR, LC + 1)).Clear
    
    Set rng = Nothing
    
    End Sub
    

    [![在此处输入图片描述][1]][1]

    棘手的部分在这里:

    .FormulaR1C1 = "=IF(OR(RC[-1]<>R[-1]C[-1],RC[-1]<>R[1]C[-1]),""x"",0)"
    .SpecialCells(xlCellTypeFormulas, 1).EntireRow.Delete
    

    第一行将创建 IF(OR) 公式来检查是否必须删除该行。如果没有,它将返回x,否则返回0

    第二行只有包含数字(零)时才会删除整行 [1]:https://i.stack.imgur.com/UlhtI.gif

    【讨论】:

      【解决方案3】:

      这也可以使用 Windows Excel 2010+ 和 Excel 365(Windows 或 Mac)中提供的 Power Query 来完成

      使用 Power Query

      • 选择数据表中的一些单元格
      • Data =&gt; Get&amp;Transform =&gt; from Table/Rangefrom within sheet
      • 当 PQ 编辑器打开时:Home =&gt; Advanced Editor
      • 记下表格名称在 2 号线
      • 粘贴下面的 M 代码代替您看到的内容
      • 将第 2 行中的表名称更改回最初生成的名称。
      • 阅读cmets并探索Applied Steps以了解算法

      M代码

      let
      
      //change next line to your actual table name in your worksheet
          Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
          #"Changed Type" = Table.TransformColumnTypes(Source,{{"header1", Int64.Type}, {"header2", type text}}),
      
      //Group by header2
      // then return the first and last table rows if there is more than a single row
          #"Grouped Rows" = Table.Group(#"Changed Type", {"header2"}, {
              {"header1", each if Table.RowCount(_) = 1 then _ 
                  else Table.FromRecords({Table.First(_),Table.Last(_)}), 
                          type table[header1=Int64.Type, header2=text]}
              }),
      
      //expand the subtables and set the column order
          #"Expanded header1" = Table.ExpandTableColumn(#"Grouped Rows", "header1", {"header1"}),
          #"Reordered Columns" = Table.ReorderColumns(#"Expanded header1",{"header1", "header2"})
      in
          #"Reordered Columns"
      

      【讨论】:

        【解决方案4】:

        保持排序范围内的第一个和最后一个

        Option Explicit
        
        Sub DeleteNotFirstNorLast()
            Const ProcName As String = "DeleteNotFirstNorLast"
            Dim RowsDeleted As Boolean ' to inform
            On Error GoTo ClearError ' enable error trapping
            
            ' Constants (adjust!)
            Const FirstCellAddress As String = "A1"
            Const CriteriaColumnIndex As Long = 2
            Const Criteria As String = "#$%"
            
            ' Reference the worksheet.
            Dim ws As Worksheet: Set ws = ActiveSheet ' improve!
            
            Application.ScreenUpdating = False
            
            ' Turn off AutoFilter.
            If ws.AutoFilterMode Then ws.AutoFilterMode = False 
            
            ' Reference the table range.
            Dim trg As Range: Set trg = RefCurrentRegion(ws.Range(FirstCellAddress))
            
            ' Write an ascending integer sequence adjacent to the right
            ' of the table range.
            AppendColumnOfAscendingIntegers trg
            
            ' Include this helper column to the table range.
            Set trg = trg.Resize(, trg.Columns.Count + 1)
            
            ' Reference the criteria column range.
            Dim crg As Range: Set crg = trg.Columns(CriteriaColumnIndex)
            
            ' It is assumed that the criteria column is already sorted favorably.
            ' If not, you could do something like the following:
            
            ' Sort the table range by the criteria column ascending.
            'trg.Sort crg, xlAscending, , , , , , xlYes
            
            ' Write the data rows (no headers) count to a variable.
            Dim drCount As Long: drCount = trg.Rows.Count - 1
            
            ' Reference the criteria column data range (headers excluded).
            Dim cdrg As Range: Set cdrg = crg.Resize(drCount).Offset(1)
            
            ' Write the values from the criteria column data range to an array.
            Dim cData As Variant: cData = GetRange(cdrg)
            
            ' Replace the unwanted values in the array with the criteria.
            KeepFirstAndLastInColumn cData
            
            ' Write the (modified) values from the array back to the range.
            cdrg.Value = cData
            
            ' Reference the table data range (no headers).
            Dim tdrg As Range: Set tdrg = trg.Resize(drCount).Offset(1)
            
            ' Filter the table range in the criteria column by the criteria.
            trg.AutoFilter CriteriaColumnIndex, Criteria
            
            ' Attempt to reference the table data visible (filtered) range.
            Dim tdvrg As Range
            On Error Resume Next ' defer error trapping
                Set tdvrg = tdrg.SpecialCells(xlCellTypeVisible)
            On Error GoTo ClearError ' re-enable error trapping
            
            ' Remove the filter.
            ws.AutoFilterMode = False
            
            ' Attempt to delete the table data visible range.
            If Not tdvrg Is Nothing Then
                tdvrg.Delete xlShiftUp
                RowsDeleted = True
            End If
            
            ' Reference the helper column.
            Dim hrg As Range: Set hrg = trg.Columns(trg.Columns.Count)
            
            ' Sort the table range by the helper column ascending.
            trg.Sort hrg, xlAscending, , , , , , xlYes
            
            ' Clear the helper column.
            hrg.Clear
            
        SafeExit:
            Application.ScreenUpdating = True ' to see any changes while reading message
            
            ' Inform.
            If RowsDeleted Then
                MsgBox "Rows deleted.", vbInformation, ProcName
            Else
                MsgBox "Nothing deleted.", vbExclamation, ProcName
            End If
        
            Exit Sub
        ClearError:
            Debug.Print "'" & ProcName & "' Run-time error '" _
                & Err.Number & "':" & vbLf & "    " & Err.Description
            Resume SafeExit
        End Sub
        
        
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        ' Purpose:      Returns a reference to the range starting with the first cell
        '               of a range and ending with the last cell of the first cell's
        '               Current Region.
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Function RefCurrentRegion( _
            ByVal FirstCell As Range) _
        As Range
            Const ProcName As String = "RefCurrentRegion"
            On Error GoTo ClearError
        
            If FirstCell Is Nothing Then Exit Function
            With FirstCell.Cells(1).CurrentRegion
                Set RefCurrentRegion = FirstCell.Resize(.Row + .Rows.Count _
                    - FirstCell.Row, .Column + .Columns.Count - FirstCell.Column)
            End With
        
        ProcExit:
            Exit Function
        ClearError:
            Debug.Print "'" & ProcName & "' Run-time error '" _
                & Err.Number & "':" & vbLf & "    " & Err.Description
            Resume ProcExit
        End Function
        
        
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        ' Purpose:      Writes an ascending integer sequence adjacent to the right
        '               of a range.
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Sub AppendColumnOfAscendingIntegers( _
                ByVal trg As Range, _
                Optional ByVal FirstInteger As Long = 1)
            Const ProcName As String = "AppendColumnOfAscendingIntegers"
            On Error GoTo ClearError
            
            With trg
                With .Resize(, 1).Offset(, .Columns.Count)
                    .Value = .Worksheet.Evaluate("ROW(" & CStr(FirstInteger) & ":" _
                        & CStr(FirstInteger + .Rows.Count - 1) & ")")
                End With
            End With
        
        ProcExit:
            Exit Sub
        ClearError:
            Debug.Print "'" & ProcName & "' Run-time error '" _
                & Err.Number & "':" & vbLf & "    " & Err.Description
            Resume ProcExit
        End Sub
        
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        ' Purpose:      Returns the values of a range ('trg') in a 2D one-based array.
        ' Remarks:      If ˙rg` refers to a multi-range, only its first area
        '               is considered.
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Function GetRange( _
            ByVal trg As Range) _
        As Variant
            Const ProcName As String = "GetRange"
            On Error GoTo ClearError
            
            If trg.Rows.Count + trg.Columns.Count = 2 Then ' one cell
                Dim Data As Variant: ReDim Data(1 To 1, 1 To 1): Data(1, 1) = trg.Value
                GetRange = Data
            Else ' multiple cells
                GetRange = trg.Value
            End If
        
        ProcExit:
            Exit Function
        ClearError:
            Debug.Print "'" & ProcName & "' Run-time error '" _
                & Err.Number & "':" & vbLf & "    " & Err.Description
            Resume ProcExit
        End Function
        
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        ' Purpose:      In the first column of a 2D one-based array of sorted values,
        '               keeps the first and last occurrence of each value and replaces
        '               the remaining occurrences with a string.
        ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
        Sub KeepFirstAndLastInColumn( _
                ByRef cData As Variant, _
                Optional ByVal Criteria As String = "#$%")
            Const ProcName As String = "KeepFirstAndLastInColumn"
            On Error GoTo ClearError
        
            Dim OldString As String: OldString = CStr(cData(1, 1))
            
            Dim r As Long
            Dim cr As Long
            Dim FirstRow As Long
            Dim NewString As String
            
            For r = 2 To UBound(cData, 1)
                NewString = CStr(cData(r, 1))
                If NewString = OldString Then
                    If FirstRow = 0 Then
                        FirstRow = r
                    End If
                Else
                    If FirstRow > 0 Then
                        For cr = FirstRow To r - 2
                            cData(cr, 1) = Criteria
                        Next cr
                        FirstRow = 0
                    End If
                    OldString = NewString
                End If
            Next r
        
        ProcExit:
            Exit Sub
        ClearError:
            Debug.Print "'" & ProcName & "' Run-time error '" _
                & Err.Number & "':" & vbLf & "    " & Err.Description
            Resume ProcExit
        End Sub
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-04-27
          • 2021-08-26
          • 2023-01-30
          • 2022-12-19
          • 1970-01-01
          • 2019-07-12
          • 1970-01-01
          • 2017-10-23
          相关资源
          最近更新 更多