[[20160727182237]] 『VBA 配列内の比較 高速化』(BK) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『VBA 配列内の比較 高速化』(BK)

いつも参照させて頂いておりますが、
この度、アイデアをお借りしたく投稿させて頂きます。

単純に以下のData1とData2の2つの文字列を比較し、部分一致すれば、
Data1の横セルに出力する内容です。
今回は出力箇所ではなく、判定する箇所の高速化のご質問です。

Data1:判定には使用しない文字列(14文字)、30万件の文字列(10文字〜100文字)
Data2:5000件の部分文字列(1文字〜10文字程度)、判定後にData1に紐付かせる文字列(6ケタ)
※それぞれ多数行x2列のイメージ

Data1の2列目文字列とData2の1列目文字列を配列内で比較し、新しい配列(Data1と同じ大きさ)にData2の紐付かせるデータ(2列目)を同じ添字に代入させます。(出力は別)

Data1/2とも2次元配列に取り込み、以下のコードで判定させていますが、
処理速度的に5秒100件程度ですので、とても使える状態ではありません。

    For I = 1 To MaxRow
        For J = 1 To FindMaxRow
            If Data1(I, 2) = Data2(J, 1) Then
                OutData(I, 1) = Data2(J, 2)
            End If
        DoEvents
        Next
        Application.StatusBar = String(Int(I / MaxRow * 10), "■") & _
                           String(10 - Int(I / MaxRow * 10), "□") & _
                           I & "検索を処理中…"

    DoEvents
    Next

言葉で記載すると難しいですが、多量データの文字列判別のアイデアが
あとはFor Eachに変える程度しかありません。

言葉足らずで申し訳ありませんが、アドバイスお願いいたします。

< 使用 Excel:Excel2010、使用 OS:Windows7 >


 いかに配列内処理をしているといっても、提示のロジックでは
 30万 x 5000 = 15億回 の比較と、マッチした場合の転記を行っているわけですから
 それは、時間がかかるでしょう。

 ・提示の流れで行くとしても、せめて、内側のループでマッチして転記したら Exit For は最低必要でしょうね。
 ・これだけのデータボリュームであれば、元データを昇順にして、二分検索のMATCH等による処理が効率的かもしれません。
 ・あるいは、データの並びを昇順にできないなら、Data2のほうをDictionaryに格納して、OutDataにはDictionaryからセットするという方式が望ましいと思います。
  (私の場合はロジックを考えるのが面倒なので、たとえ元データが昇順であってもDictionary方式でやります)

 いずれにしても、処理時間の短縮を目下のテーマとしておられるわけですから、その足をひっぱるステータスバーへの書き込み(おまけに DoEvents も30万回)は
 とりさって、すべて解決した後に、必要なら加えられてはいかがですか。

(β) 2016/07/27(水) 19:41


 追加で

 >>Data1:判定には使用しない文字列(14文字)、30万件の文字列(10文字〜100文字) 
 >>Data2:5000件の部分文字列(1文字〜10文字程度)、判定後にData1に紐付かせる文字列(6ケタ) 

 いまいち、よくわからないのですが、
 具体的なサンプルをアップして説明されれば、回答者側で、処理案回答を行うとした場合に
 より具体的な回答がつくと思います。

(β) 2016/07/27(水) 19:45


 実際のData1,Data2,OutData の中のデータがどうなっているか不明ですが、
 こちらで 30万行2列のData1、5千行2列のData2 を作成。
 Data1の2列目とData2の1列目がマッチするケースが12万件。

 これで、以下の処理をしますと、1秒もかからずにOutDataへの格納が終了します。
 (なので、ステータスバーの表示は無意味になりますね)

    Set dic = CreateObject("Scripting.Dictionary")

    For i = 1 To UBound(Data2, 1)
        dic(Data2(i, 1)) = Data2(i, 2)
    Next

    For i = 1 To UBound(Data1, 1)
        If dic.exists(Data1(i, 2)) Then OutData(i, 1) = dic(Data1(i, 2))
    Next

(β) 2016/07/27(水) 21:09


Data1は元の長い文字列であり、Data2は検索用の短い文字列ですよね? イコールで判定していますが、部分一致ならInstrかLikeではないでしょうか? もし部分一致判定だとすると、Dictionary案は×になってしまいますよ。

データ数が非常に多いのですが、これExcelの配列をインタプリンタで処理するより、何らかのデータベースに乗せた方が良くないですかね? そしてVBAでは、1件ずつ部分一致のwhereで、Joinした結果を得て表示するだけ。
(???) 2016/07/28(木) 09:31


βさん、???さんご回答ありがとうございます。

βさんにご教示頂いたDictionary案コード実行いたしました。
仰る通り1秒未満でしたが、???さんの仰る通り、完全一致のみの検索となります。

※私が急ぎで書き載せてしまったサンプル例(部分一致にしていない)が悪く、
 βさんに誤解を与えてしましましたので、申し訳ございません

???の仰る方法か、後は比較件数を分割するか、
夜通し動かすかアナログな方法くらいでしょうか。。。

お二人ともありがとうございました。

(BK) 2016/07/28(木) 11:20


 >>完全一致のみの検索となります。 

 はい。元コードを忠実に継承しましたので。

 >>夜通し動かすかアナログな方法くらいでしょうか。。。 

 正規表現チェックを行えば、たぶん短い時間の処理で可能だと思います。
 ちょっと、書いてみて、結果がよければアップします。

(β) 2016/07/28(木) 12:46


 ちょこっと書いて動かしてみました。
 データは、同じ30万件、5千件。部分一致件数も同じく12万件ぐらい。

 さすがに時間がかかりますねぇ。(私のコードがおそまつなのかもしれませんが)

 Dictionaryだけで1秒弱だったんですが、正規表現をからめると 配列に格納するのに 21秒もかかりました。
 もう少し、やりくりしてみて、さまになればアップしますが、さすがに 21秒だと、かっこわるいので現段階のコードアップは
 控えます。

(β) 2016/07/28(木) 14:02


DB案です。 サーバを立てると環境構築が面倒なので、ここは簡単に、シート自体をDB扱いする方法にしてみました。
Sheet1のA〜C列1行目には、タイトル文字として、"A列"、"B列"、"C列" と入力されていて、2行目以降がデータであるものとします。
(シートをDB扱いする場合、項目名を表す文字列は1行目であること必須です)

部分一致があれば、Sheet1のC列に、Sheet2のB列を代入するロジックです。
大量のデータでは試験していませんので、是非お試しください。

 Sub test()
    Dim wk1 As Worksheet
    Dim wk2 As Worksheet
    Dim CN As Object
    Dim strSQL As String
    Dim i As Long

    Application.ScreenUpdating = False

    Set wk1 = Sheets("Sheet1")
    Set wk2 = Sheets("Sheet2")

    Set CN = CreateObject("ADODB.Connection")

    CN.Provider = "Microsoft.ACE.OLEDB.12.0"
    CN.Properties("Extended Properties") = "Excel 12.0"
    CN.Open ThisWorkbook.FullName

    With wk2
        For i = 1 To .Cells(.Rows.Count, "A").End(xlUp).Row
            strSQL = "UPDATE [Sheet1$] SET C列='" & .Cells(i, "B").Value & _
                     "' WHERE B列 LIKE '%" & .Cells(i, "A").Value & "%';"
            CN.Execute strSQL
        Next i
    End With

    Set CN = Nothing
    Application.ScreenUpdating = True
 End Sub
(???) 2016/07/28(木) 17:31

 いろいろやってみましたが30万件/5千件で21秒から改善することはできていません。
 とりあえず、参考までにコードをアップしておきます。
 以下の、テストデータ作成部分は、こちらで、シート上にランダムに文字列をセットしたものから配列に
 収めているところで無視してください。
 実際の処理は、その下の部分です。

 Sub Test()
    Dim t As Double
    Dim Data1 As Variant
    Dim Data2 As Variant
    Dim OutData As Variant
    Dim reg As Object
    Dim dic As Object
    Dim i As Variant
    Dim mt As Object

    'テストデータ作成
    Data1 = Sheets("Sheet1").Range("A1:B300000").Value
    Data2 = Sheets("Sheet2").Range("A1:B5000").Value
    ReDim OutData(1 To UBound(Data1, 1), 1 To UBound(Data1, 2))
    'テストデータ作成終了

    'ここから処理

    t = Timer

    Set dic = CreateObject("Scripting.Dictionary")
    Set reg = CreateObject("VBScript.RegExp")
    reg.Global = True
    reg.Pattern = "([$()|\-\^\\[\]{}+*?.])"

    For i = 1 To UBound(Data2, 1)
        dic(Data2(i, 1)) = Data2(i, 2)
    Next

    reg.Pattern = Replace(reg.Replace(Join(dic.keys, vbTab), "\$1"), vbTab, "|")
    reg.Global = False

    For i = 1 To UBound(Data1, 1)
        Set mt = reg.Execute(Data1(i, 1))
        If mt.Count > 0 Then
            OutData(i, 1) = dic(mt(0).Value)
        End If
    Next

    MsgBox Timer - t

 End Sub

(β) 2016/07/28(木) 18:22


βさん、???さんご回答ありがとうございます。

本日は時間があまり無かったため、
一旦、βさんの正規表現案を稼働させてみました。

テスト的に30万件×1000件で行い、処理的には3分くらいでした。
しかしながら、マッチするはずのものがマッチしなかったため、
中身を確認する状態です。

取り急ぎご連携までです。

(BK) 2016/07/29(金) 18:16


 >>しかしながら、マッチするはずのものがマッチしなかったため

 ありえますね、それは。

 膨大な件数の処理時間の圧縮を中心にコードを組み立て、そこでのアプリ要件は、BKさんが仮に書かれた
 完全一致処理コードから推測していますので、こちらの要件理解ミスもあるかと思います。

 処理時間に、ある程度のめどがつけば、今度は、マッチするはずなのにマッチしないといったデータを中心に
 Sheet1 も Sheet2 も 10件程度のデータにして実行してみませんか?

 そこで、このデータは、マッチしてこうなるはずだけど、マッチしなかったといった具体的な情報をいただければ
 今度は、アプリロジックの正常化に注力するお手伝いもできるかと思います。

(β) 2016/07/29(金) 19:34


 ちなみに

 ●Sheet1

     |[A]              |[B]
 [1] |a1               |b1 
 [2] |a2               |b2 
 [3] |もしかしてcccかな|b3 
 [4] |a4               |b4 
 [5] |a5               |b5 
 [6] |aaaだよ          |b6 
 [7] |a7               |b7 
 [8] |a8               |b8 
 [9] |これはbbb        |b9 
 [10]|a10              |b10

 ●Sheet2

     |[A]|[B]    
 [1] |x1 |y1     
 [2] |x2 |y2     
 [3] |aaa|結果aaa
 [4] |x4 |y4     
 [5] |x5 |y5     
 [6] |ccc|結果ccc
 [7] |x7 |y7     
 [8] |bbb|結果bbb
 [9] |x9 |y9     
 [10]|x10|y10    

 こんなデータをつくリ、アップ済みのコードの配列を小さくしたもので最後に結果をSheet1のC列に落としこんでみると

 ●Sheet1結果

     |[A]              |[B]|[C]    
 [1] |a1               |b1 |       
 [2] |a2               |b2 |       
 [3] |もしかしてcccかな|b3 |結果ccc
 [4] |a4               |b4 |       
 [5] |a5               |b5 |       
 [6] |aaaだよ          |b6 |結果aaa
 [7] |a7               |b7 |       
 [8] |a8               |b8 |       
 [9] |これはbbb        |b9 |結果bbb
 [10]|a10              |b10|       

 こんなようになっています。

(β) 2016/07/29(金) 19:50


βさんご回答ありがとうございます。
遅くなり申し訳ございません。

結果として・・・βさんのコードで全く問題ありませんでした。
途中で仕様変更などして、私が少し間違えていました。(最終以下のデータ&コードとなりました)
ご報告も含めまして、簡単ですがUPしておきます。

【仕様】
DataSheetのNAMEと各検索シート(CharaSheet@)の検索キーをマッチングさせ、
マッチしたら出力結果を処理結果に出力する。
尚、検索シートは優先順位があり、例えば以下のマッチ1やマッチ2は多数ヒットするため、
優先順位を下げるために検索シートの後方へ設定。
逆に単独ヒットする分は優先度の高い検索シートへ記載。
※尚〜の仕様は当方の都合ですので無視して頂いて可

【データ/処理結果】

DataSheet(処理結果含め)
CODE NAME 処理結果
A00001 データ マッチ1 結果マッチ1
A00002 データ マッチ2 結果マッチ2
A00003 データ マッチ3 結果マッチ3
A00004 データ マッチ4 結果マッチ4
A00005 データ マッチ5 結果マッチ5
A00006 データ マッチ6 結果マッチ6
A00007 データ マッチ7 結果マッチ7
A00008 データ マッチ8 結果マッチ8
A00009 データ マッチ9 結果マッチ9
A00010 (データ マッチ10) 結果マッチ10
A00011 (データ マッチ11) 結果マッチ11
A00012 (データ マッチ12) 結果マッチ12
A00013 (データ マッチ13) 結果マッチ13
A00014 (データ マッチ14) 結果マッチ14
A00015 (データ マッチ15) 結果マッチ15
A00016 (データ マッチ16) 結果マッチ16
A00017 (データ マッチ17) 結果マッチ17
A00018 (データ マッチ18) 結果マッチ18
A00019 (データ マッチ19) 結果マッチ19
A00020 データ (マッチ20) 結果(マッチ20)
A00021 データ (マッチ21) 結果(マッチ21)
A00022 データ (マッチ22) 結果(マッチ22)
A00023 データ (マッチ23) 結果(マッチ23)
A00024 データ (マッチ24) 結果(マッチ24)
A00025 データ (マッチ25) 結果(マッチ25)
A00026 データ (マッチ26) 結果(マッチ26)
A00027 データ (マッチ27) 結果(マッチ27)
A00028 データ (マッチ28) 結果(マッチ28)
A00029 データ (マッチ29) 結果(マッチ29)
A00030 データ (マッチ30) 結果(マッチ30)
A00031 データ アンマッチ1 結果マッチ1
A00032 データ アンマッチ2 結果マッチ2
A00033 データ アンマッチ3 結果マッチ3
A00034 データ アンマッチ4 結果マッチ4
A00035 データ アンマッチ5 結果マッチ5
A00036 データ アンマッチ6 結果マッチ6
A00037 データ アンマッチ7 結果マッチ7
A00038 データ アンマッチ8 結果マッチ8
A00039 データ アンマッチ9 結果マッチ9
A00040 データ アン マッチ10 結果アン マッチ
A00041 データ アン マッチ11 結果アン マッチ
A00042 データ アン マッチ12 結果アン マッチ
A00043 データ アン マッチ13 結果アン マッチ
A00044 データ アン マッチ14 結果アン マッチ
A00045 データ アン マッチ15 結果アン マッチ
A00046 データ アン マッチ16 結果アン マッチ
A00047 データ アン マッチ17 結果アン マッチ
A00048 データ アン マッチ18 結果アン マッチ
A00049 データ アン マッチ19 結果アン マッチ
A00050 データ アン マッチ20 結果アン マッチ

CharaSheet1(検索シート1枚目)
検索キー 出力結果
マッチ10 結果マッチ10
マッチ11 結果マッチ11
マッチ12 結果マッチ12
マッチ13 結果マッチ13
マッチ14 結果マッチ14
マッチ15 結果マッチ15
マッチ16 結果マッチ16
マッチ17 結果マッチ17
マッチ18 結果マッチ18
マッチ19 結果マッチ19
(マッチ20) 結果(マッチ20)
(マッチ21) 結果(マッチ21)
(マッチ22) 結果(マッチ22)
(マッチ23) 結果(マッチ23)
(マッチ24) 結果(マッチ24)
(マッチ25) 結果(マッチ25)
(マッチ26) 結果(マッチ26)
(マッチ27) 結果(マッチ27)
(マッチ28) 結果(マッチ28)
(マッチ29) 結果(マッチ29)
(マッチ30) 結果(マッチ30)
アン マッチ 結果アン マッチ

CharaSheet2(検索シート2枚目)
検索キー 出力結果
マッチ1 結果マッチ1
マッチ2 結果マッチ2
マッチ3 結果マッチ3
マッチ4 結果マッチ4
マッチ5 結果マッチ5
マッチ6 結果マッチ6
マッチ7 結果マッチ7
マッチ8 結果マッチ8
マッチ9 結果マッチ9

【コード】上記に合わせてFindCellData=各CharaSheetのデータ DataSheetCellData=DataSheetのデータ
Set Reg = CreateObject("VBScript.RegExp")
Reg.Global = True
Reg.Pattern = "([$()|\-\^\\[\]{}+*?.])"

Set FindDic = CreateObject("Scripting.Dictionary")

For J = 1 To UBound(FindCellData, 1)

    FindDic(FindCellData(J, 1)) = FindCellData(J, 2)
Next

Reg.Pattern = Replace(Reg.Replace(Join(FindDic.keys, vbTab), "\$1"), vbTab, "|")
Reg.Global = False

For J = 1 To UBound(DataSheetCellData, 1)

    Set Mt = Reg.Execute(DataSheetCellData(J, 2))
    If Mt.Count > 0 Then
        If Len(OutputData(J, 1)) = 0 Then
            OutputData(J, 1) = FindDic(Mt(0).Value)
        End If
    End If
DoEvents
Next

Set FindCellData = Nothing

βさんのおかげで形に出来ました。
補足情報としまして、本データ30万件+検索キー1000件(1シートのみ)では
7分45秒掛りましたが十分現実的ですので問題ありません。
長々となりましたが、ありがとうございました。
(BK) 2016/08/01(月) 18:54


コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.