シートの行を直接消す前に、重複を除いた結果を確認したいことがあります。元のシートはそのままにして、重複を除いた一覧を別の場所に作りたい場合もあるでしょう。このようなときは、Range.Value で表を2次元配列に読み込み、VBAの中で重複を除いてから書き出す方法が向いています。
この記事では、Dictionary(連想配列)を使い、1列または複数列をキーにして重複を判定します。最初に出てきた1行だけを残した新しい配列を作り、書き出す前に件数を確認するところまで説明します。
前提
- Windows版Excelを前提にしています。Dictionaryには
Scripting.Dictionaryを使うため、Mac版Excelでは動きません。 Range.Valueで読み込んだ配列は、行・列とも1から始まる2次元配列です。コードはこの形を前提にしています。- 元のシートには何も書き込みません。結果は新しいシートに書き出します。
処理の流れ
- 表全体を
Range.Valueで2次元配列に読み込む - 行ごとにキー(重複の判定に使う文字列)を作る
- キーがまだDictionaryにない場合だけ、その行番号を「残す行」として記録する
- 残す行の数が決まってから、その行数分の新しい配列を作って値をコピーする
- 元の件数・残す件数・除く件数を表示して確認する
- 確認できたら、新しいシートに書き出す
4で行数が決まってから配列を作るのは、VBAの ReDim Preserve では2次元配列の最後の次元(列)しか広げられないためです。行を1つずつ追加しながら配列を広げることはできないので、先に残す行の番号だけを集めておきます。
キーの作り方を先に決める
重複かどうかは、キーが同じかどうかだけで判定されます。コードを書く前に、次の点を決めておきます。
| 決めること | 選択肢と注意点 |
|---|---|
| キーにする列 | 1列(例:顧客コード)か、複数の列の組み合わせ(例:顧客コード+日付)か |
| 複数の列のつなぎ方 | 間に区切り文字を入れる。区切り文字なしでつなぐと、「AB」+「C」と「A」+「BC」がどちらも「ABC」になってしまう |
| 大文字・小文字 | 区別しない場合は、Dictionaryの CompareMode を vbTextCompare にする(キーを追加する前に設定する) |
| 前後の空白 | Trim で取り除くかどうか。全角スペースもそろえたい場合は、Replace で明示的に置き換える |
| 数値と文字列 | セルの数値の1と文字列の「001」は、文字列にすると「1」と「001」になり、別のキーとして扱われる |
| 日付 | 時刻を含むセルは、同じ日付でも時刻が違えば別のキーになる |
| エラー値のセル | #N/A などのエラー値は、そのままでは文字列にできない。下のコードでは同じ文字列に置き換えるため、エラー値どうしの行は重複と判定される |
コード1:重複を除いた配列を返す関数
Option Explicit
' 2次元配列から、キーが重複する行を除いた新しい配列を返す
' data : Range.Valueで読み込んだ配列(1始まり)
' keyCols : キーにする列番号 例) Array(1) / Array(1, 3)
' hasHeader : True なら1行目を見出しとして必ず残す
' ignoreCase : True なら大文字・小文字を区別しない
Function RemoveDuplicateRows(data As Variant, keyCols As Variant, _
ByVal hasHeader As Boolean, _
Optional ByVal ignoreCase As Boolean = False) As Variant
Dim dict As Object
Dim keepRows() As Long
Dim result() As Variant
Dim r As Long, c As Long, i As Long
Dim firstRow As Long, keepCount As Long
Dim key As String
Set dict = CreateObject("Scripting.Dictionary")
If ignoreCase Then dict.CompareMode = vbTextCompare
ReDim keepRows(1 To UBound(data, 1))
firstRow = 1
If hasHeader Then
keepCount = 1
keepRows(1) = 1
firstRow = 2
End If
' 1回目:残す行番号だけを集める(最初に出てきた行を残す)
For r = firstRow To UBound(data, 1)
key = MakeKey(data, r, keyCols)
If Not dict.Exists(key) Then
dict.Add key, r
keepCount = keepCount + 1
keepRows(keepCount) = r
End If
Next r
' 2回目:残す行だけを新しい配列にコピーする
ReDim result(1 To keepCount, 1 To UBound(data, 2))
For i = 1 To keepCount
For c = 1 To UBound(data, 2)
result(i, c) = data(keepRows(i), c)
Next c
Next i
RemoveDuplicateRows = result
End Function
' 指定した列の値をつないで、1行分のキーを作る
Private Function MakeKey(data As Variant, ByVal r As Long, keyCols As Variant) As String
Dim sep As String
Dim k As Long
Dim v As Variant
Dim s As String
sep = ChrW(31) ' データに含まれにくい制御文字を区切りに使う
For k = LBound(keyCols) To UBound(keyCols)
v = data(r, keyCols(k))
If IsError(v) Then
s = "#ERROR"
Else
s = Trim$(CStr(v))
End If
If k > LBound(keyCols) Then MakeKey = MakeKey & sep
MakeKey = MakeKey & s
Next k
End Function
コードを読むときのポイントは次のとおりです。
- 残るのは、上から順に見て最初に出てきた行です。
keyColsに指定する列番号は、シート上の列番号ではなく、読み込んだ範囲の左端から数えた位置です。表がB列から始まっている場合は、B列が1になります。- 前後の空白をそのままキーに含めたい場合は、
Trim$(CStr(v))をCStr(v)に変えます。 - Dictionaryには、キーと一緒に残した行番号も入れています。あとで紹介する「除いた行の記録」で使います。
コード2:件数を確認してから新しいシートに書き出す
Sub DedupeToNewSheet()
Dim src As Range
Dim data As Variant
Dim result As Variant
Dim outWs As Worksheet
Dim beforeCount As Long, afterCount As Long
Set src = ThisWorkbook.Worksheets("元データ").Range("A1").CurrentRegion
If src.Rows.Count < 2 Then
MsgBox "見出し以外のデータがないため処理しません。"
Exit Sub
End If
data = src.Value
' 1列目と3列目の組み合わせをキーにする(見出しあり)
result = RemoveDuplicateRows(data, Array(1, 3), True)
beforeCount = UBound(data, 1) - 1 ' 見出しを除いた件数
afterCount = UBound(result, 1) - 1
If MsgBox("元の件数:" & beforeCount & vbCrLf & _
"残る件数:" & afterCount & vbCrLf & _
"除く件数:" & (beforeCount - afterCount) & vbCrLf & vbCrLf & _
"新しいシートに書き出しますか?", vbYesNo + vbQuestion) = vbNo Then
Exit Sub
End If
Set outWs = ThisWorkbook.Worksheets.Add( _
After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
outWs.Range("A1").Resize(UBound(result, 1), UBound(result, 2)).Value = result
End Sub
シート名「元データ」、開始セル、キーにする列は、実際の表に合わせて変えてください。CurrentRegion は空白の行や列があるとそこで止まるため、実行前に Debug.Print src.Address で読み込む範囲を確認しておくと安心です。
数式が入っているセルは、Range.Value で計算結果の値として読み込まれます。書き出した結果は数式ではなく値になります。
書き出すときに値が変わらないようにする
読み込んだ値をそのまま書き出すと、書き出し先のセルの書式によって値が変わることがあります。よくあるのは、文字列として入力されていた「001」のようなコードです。書き出し先の書式が標準のままだと、数値の1として入力されてしまいます。
先頭の0を残したい列は、書き出す前にその列の表示形式を文字列にしておきます。
' 1列目(コード列)を文字列として書き出す
outWs.Columns(1).NumberFormat = "@"
outWs.Range("A1").Resize(UBound(result, 1), UBound(result, 2)).Value = result
桁数の多い番号や、日付のように見える文字列も、同じように別の値に変わることがあります。こうした列があれば、同じように表示形式を指定してください。
どの行を残すかを変えたいとき
この関数は、上から順に見て最初に出てきた行を残します。「日付が最も新しい行を残したい」場合は、日付の新しい順に並べ替えてから読み込めば、コードを変えずに済みます。元のシートの並び順を変えたくない場合は、シートを複製し、複製したほうを並べ替えて読み込みます。
除いた行を記録する
どの行がどの行の重複として除かれたかを確認したい場合は、関数の中の If 文を次のように書き換えます。
If Not dict.Exists(key) Then
dict.Add key, r
keepCount = keepCount + 1
keepRows(keepCount) = r
Else
Debug.Print "除外:" & r & "行目(同じキーで残した行:" & dict(key) & "行目)"
End If
ここでの行番号は、読み込んだ範囲の中の行番号です。範囲がA1から始まっていれば、シートの行番号と一致します。イミディエイトウィンドウに表示できる行数には限りがあり、件数が多いと古い行から消えていきます。除いた行が多い場合は、記録用の配列を作ってシートに書き出してください。
結果を確かめる
書き出したら、次の点を確認します。
- 件数の関係:元の件数−残る件数=除く件数になっているか(メッセージに表示された数字で確認する)
- 結果にキーの重複が残っていないか:書き出したシートを同じキー列で関数にもう一度通し、件数が変わらないかを確かめる
- 除いた行の中身:除かれた行のキーをいくつか選び、元データの該当する行と見比べて、本当に重複として扱ってよいかを確認する
- 値が変わっていないか:先頭の0、桁数の多い番号、日付が、元データと同じように表示されているか
- 元のシート:元データのシートの行数や内容が変わっていないか
2つ目の確認は、次のようなマクロで行えます。書き出したシートを選択した状態で実行し、イミディエイトウィンドウに表示される2つの数字が同じなら、キーの重複は残っていません。
Sub CheckNoDuplicates()
Dim data As Variant
data = ActiveSheet.Range("A1").CurrentRegion.Value
Debug.Print UBound(data, 1), UBound(RemoveDuplicateRows(data, Array(1, 3), True), 1)
End Sub
つまずきやすい点
| 症状 | 原因と確認すること |
|---|---|
| 「型が一致しません」というエラーが出る | 読み込んだ範囲が1セルだけで、配列になっていない |
| 読み込まれる件数が少ない | CurrentRegion が空白の行や列で止まっている。Debug.Print src.Address で範囲を確認する |
| 重複のはずの行が残る | 前後の全角スペース、大文字と小文字、数値と文字列の違い、日付に含まれる時刻 |
| 重複ではない行まで除かれる | キーにする列が足りない、または列番号を範囲の左端から数えていない |
| オブジェクトを作成できないというエラーが出る | Mac版で実行している、またはその環境で Scripting.Dictionary が使えない |
シートの行を直接消す方法と違い、この方法なら、結果を確かめてから使うかどうかを決められます。まず新しいシートに書き出し、件数と中身を確認してから、本来の作業に使ってください。
