C# と VB.NET の質問掲示板

わんくま同盟

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

ツリー一括表示

c# エクセルのセル範囲を1次配列に格納する方法 /ユウタ (18/08/09(Thu) 22:06) #88223
Re[1]: c# エクセルのセル範囲を1次配列に格納する方法 /furu (18/08/10(Fri) 09:57) #88228
Re[1]: c# エクセルのセル範囲を1次配列に格納する方法 /魔界の仮面弁士 (18/08/10(Fri) 11:18) #88231
Re[1]: c# エクセルのセル範囲を1次配列に格納する方法 /WebSurfer (18/08/10(Fri) 13:59) #88232
  └ Re[2]: c# エクセルのセル範囲を1次配列に格納する方法 /ユウタ (18/08/11(Sat) 00:25) #88236
    └ Re[3]: c# エクセルのセル範囲を1次配列に格納する方法 /魔界の仮面弁士 (18/08/11(Sat) 02:36) #88237
      └ Re[4]: c# エクセルのセル範囲を1次配列に格納する方法 /ユウタ (18/08/11(Sat) 17:53) #88238 解決済み


親記事 / ▼[ 88228 ] ▼[ 88231 ] ▼[ 88232 ]
■88223 / 親階層)  c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ ユウタ (1回)-(2018/08/09(Thu) 22:06:57)

分類:[C#] 

forを使わずにエクセルのセル範囲をstring型で1次配列に格納したいのですが、可能でしょうか。
(大量の値を格納するためforだと処理が遅くなるため高速化したいです)

以下コードの場合、Console.WriteLineでは「System.__ComObject」と出力されてしまいます。
AAA.xlsxには、1列目に5000行目まで文字列が入っています。

using System;
using System.Linq;
using usgExcel = Microsoft.Office.Interop.Excel;

namespace Test_エクセルを1次配列化
{
    class Program
    {
        static void Main(string[] args)
        {
            string FilePath = @"C:\AAA.xlsx";
            
            usgExcel.Application myExcel = new usgExcel.Application();
            myExcel.Visible = true;
            usgExcel.Workbook myWorkBook = null;

            myWorkBook = myExcel.Workbooks.Open(FilePath); 
            usgExcel.Worksheet myWorkSheet =  myWorkBook.Sheets["Sheet1"];            
            usgExcel.Range myRange = myWorkSheet.Range[myWorkSheet.Cells[1, 1], myWorkSheet.Cells[5000, 1]];
            object[] myRangeObj = (myRange).Cast<object>().ToArray();

            Console.WriteLine(Convert.ToString(myRangeObj[1]));
            Console.WriteLine(Convert.ToString(myRangeObj[2]));
            Console.WriteLine(Convert.ToString(myRangeObj[3]));
            Console.ReadKey();
        }
    }
}

[ □ Tree ] 返信 編集キー/

▲[ 88223 ] / 返信無し
■88228 / 1階層)  Re[1]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ furu (180回)-(2018/08/10(Fri) 09:57:42)
No88223 (ユウタ さん) に返信

値はValueで、Range.Valueは2次元配列で返ってきます。

> object[] myRangeObj = (myRange).Cast<object>().ToArray();
>
> Console.WriteLine(Convert.ToString(myRangeObj[1]));
> Console.WriteLine(Convert.ToString(myRangeObj[2]));

object[,] myRangeObj = myRange.Value;

Console.WriteLine(Convert.ToString(myRangeObj[1, 1]));
Console.WriteLine(Convert.ToString(myRangeObj[2, 1]));
[ 親 88223 / □ Tree ] 返信 編集キー/

▲[ 88223 ] / 返信無し
■88231 / 1階層)  Re[1]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ 魔界の仮面弁士 (1778回)-(2018/08/10(Fri) 11:18:47)
No88223 (ユウタ さん) に返信
> forを使わずにエクセルのセル範囲をstring型で1次配列に格納したいのですが、可能でしょうか。
> (大量の値を格納するためforだと処理が遅くなるため高速化したいです)

No88228 で既に回答がついていますが、2 次元配列で読み書きすることなら可能です。


Range が単一のセル範囲を表す場合
 Value プロパティは単一の値を返します。

Range が連続した複数のセル範囲を含む場合
 Value プロパティは 2 次元配列を返します。
 1 次元目が行方向(縦)、2 次元目が列方向(横)を意味します。

Range が非連続のセル範囲を含む場合
 Values からは最初の連続領域のみの内容が返されます。
 すべてを取り出す場合は、Areas プロパティを使って
 連続領域ごとの Range に分解してから
 それぞれの Value プロパティを呼び出すようにします。


なお、Value プロパティからの返却値を受け取る場合、
C# では先頭が 1 から始まる特殊な配列として返却される可能性があります。
(VB.NET の場合は、常に 0 始まりに補正されます)




using System;
using System.Linq;
using System.Runtime.InteropServices;
using usgExcel = Microsoft.Office.Interop.Excel;
namespace Test_エクセルを1次配列化
{
 class Program
 {
  static void Main(string[] args)
  {
   string FilePath = @"C:\AAA.xlsx";

   usgExcel.Application myExcel = new usgExcel.Application();
   myExcel.Visible = true;
   usgExcel.Workbooks myWorkBooks = myExcel.Workbooks;
   usgExcel.Workbook myWorkBook = myWorkBooks.Open(FilePath);
   usgExcel.Sheets mySheets = myWorkBook.Sheets;
   usgExcel.Worksheet myWorkSheet = mySheets["Sheet1"];

   usgExcel.Range myCells = myWorkSheet.Cells;
   usgExcel.Range myCells1 = myCells[1, 1];
   usgExcel.Range myCells2 = myCells[5000, 1];
   usgExcel.Range myRange = myWorkSheet.Range[myCells1, myCells2];

   // 二次元配列として取得されます
   // 1..5000 になるか、0..4999 になるかは環境依存
   object[,] values = myRange.Value;

   Marshal.FinalReleaseComObject(myCells1);
   Marshal.FinalReleaseComObject(myCells2);
   Marshal.ReleaseComObject(myRange);
   Marshal.ReleaseComObject(myCells);
   Marshal.ReleaseComObject(myWorkSheet);
   Marshal.ReleaseComObject(mySheets);
   Marshal.ReleaseComObject(myWorkBook);
   Marshal.ReleaseComObject(myWorkBooks);

   // myExcel.Quit();
   Marshal.ReleaseComObject(myExcel);

   // object[,] を string[] に変換します
   string[] strValues = new string[values.Length];

   // 元の配列が 0 ベースの場合と 1 ベースの場合がありえるため、
   // GetLowerBound を利用して列挙しています。
   for (int x = values.GetLowerBound(0), i = -1; x <= values.GetUpperBound(0); x++)
   {
    for (int y = values.GetLowerBound(1); y <= values.GetUpperBound(1); y++, i++)
    {
     if (values[x, y] != null)
     {
      strValues[i] = values[x, y].ToString();
     }
    }
   }

   Console.WriteLine(strValues[0]);
   Console.WriteLine(strValues[1]);
   Console.WriteLine(strValues[2]);
   Console.WriteLine(strValues[3]);

   Console.ReadKey();
  }
 }
}

# 2 次元配列のまま LINQ 処理できるよう、拡張メソッドを用意しておいたほうが楽かも。
[ 親 88223 / □ Tree ] 返信 編集キー/

▲[ 88223 ] / ▼[ 88236 ]
■88232 / 1階層)  Re[1]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ WebSurfer (1578回)-(2018/08/10(Fri) 13:59:54)
No88223 (ユウタ さん) に返信
> forを使わずにエクセルのセル範囲をstring型で1次配列に格納したいのですが、可能でしょうか。
> (大量の値を格納するためforだと処理が遅くなるため高速化したいです)

「セル範囲」というのが具体的にどういうことか分かってないので期待通りのことができるかどうか不明
ですが、ADO.NET + ACE プロバイダを使うという手段はいかがですか。

可能性の一つとして検討されてはいかがでしょう。
[ 親 88223 / □ Tree ] 返信 編集キー/

▲[ 88232 ] / ▼[ 88237 ]
■88236 / 2階層)  Re[2]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ ユウタ (2回)-(2018/08/11(Sat) 00:25:30)
皆さん

アドバイスありがとうございます。
言葉足らずですみません、私が行いたいのは、
セルA1〜A5000までの範囲を1次配列に格納するというものです。

エクセルから取得したRangeに対して、
forを使って各要素を1つ1つ1次配列に格納することは可能なのですが、
やや時間がかかるため、ここも短縮したいという趣旨でした。

また、なぜ1次配列かというと、
配列の各要素の値をforを使って取得する場合、
2次配列で取得するのと1次配列で取得するのでは、
1次配列の方が圧倒的に速いためです。


>furuさん
myRangeは2次配列ですが、myRangeObjは1次配列に変換されたものとなります。


>魔界の仮面騎士さん
やはりforで配列に格納していく方法になるのですね。
一発で変換する命令があればよいのですが…。

>WebSurferさん
ADO.NET + ACEプロバイダ
初めて聞きましたので、調べてみます。
[ 親 88223 / □ Tree ] 返信 編集キー/

▲[ 88236 ] / ▼[ 88238 ]
■88237 / 3階層)  Re[3]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ 魔界の仮面弁士 (1779回)-(2018/08/11(Sat) 02:36:08)
No88236 (ユウタ さん) に返信
> エクセルから取得したRangeに対して、
> forを使って各要素を1つ1つ1次配列に格納することは可能なのですが、
> やや時間がかかるため、ここも短縮したいという趣旨でした。

速度面が問題になるなら、COM Interop な Excel オートメーションを用いるのではなく、
ExcelDataReader や EPPlus を使う方法に変更してみるとか。


ひとまず Excel オートメーションのままで実装するとした場合、
先のように .Value で複数範囲を一括で読み取るようにすることで、
アウトプロセスである Excel との通信回数が、その 1 回だけで済むようになります。

そのため、繰り返し読みだす方法に比べれば大幅な高速化になります。

これが C# ではなく、Excel VBA 内での読み取りであったのなら、
繰り返し読み取るようにしても、そこまで遅くはならないのですけれども(インプロセスなので)。


とはいえ一括読み取りの際に、対象セル数があまりに膨大になる場合は
転送効率が急激に落ちることがあります。
その場合は複数回のブロックに分けて読み取るようにします。
(5000セル程度ならば通常は問題無いはず)


> また、なぜ1次配列かというと、
> 配列の各要素の値をforを使って取得する場合、
> 2次配列で取得するのと1次配列で取得するのでは、
> 1次配列の方が圧倒的に速いためです。


1 次元化せずにそのまま 2 次元のまま扱うと、どの程度のタイムロスになるのですか?
あるいは、2 次元配列から 1 次元配列への詰め替える場合、どの程度がかかるのでしょうか?

1 次元配列は、IL レベルで専用命令(ldelem.*)が揃っていることもあり、
確かに高速ではありますが、今回、そこまで“圧倒的”な差が付くのでしょうか。

実際に測定したわけではないですが、個人的な経験から言えば、
「Value で 2 次元配列にまとめて読み取る方法」と
「1 セルずつ for で読み取る方法」との処理時間差に比べれば、
『1 次元配列へのアクセス時間』と『2 次元配列へのアクセス時間』の時間差は
相対的には小さいものであると思っているのですが…。


> やはりforで配列に格納していく方法になるのですね。

5000行×1列や 1行×5000列 といったパターンなら、
for よりも foreach の方が楽ですね。


> 一発で変換する命令があればよいのですが…。

.Cast<>.ToArray() するという手はあります。
詰め替えているという点では同じですが。


// [0..3, 0..0] な object 型の 2 次元配列を
object[,] obj2dArray = { { "AB" }, { 12.0 }, { null }, { DateTime.UtcNow } };

// [0..3] な string 型の 1 次元配列に変換
string[] str1dArray = obj2dArray.Cast<object>().Select(o => o != null ? o.ToString() : null).ToArray();



// [1..5000, 1..1] な object 型の 2 次元配列を
obj2dArray = (object[,])Array.CreateInstance(typeof(object), new int[] { 5000, 1 }, new int[] { 1, 1 });
obj2dArray[1, 1] = "AB";
obj2dArray[2, 1] = 12.0;
obj2dArray[3, 1] = null;
obj2dArray[4, 1] = DateTime.UtcNow;

// [0..4999] な string 型の 1 次元配列に変換
str1dArray = obj2dArray.Cast<object>().Select(o => o != null ? o.ToString() : null).ToArray();



> >WebSurferさん
> ADO.NET + ACEプロバイダ
> 初めて聞きましたので、調べてみます。

この方法だと、Excel が入っていない環境でも読み書きできるというメリットがあります。
幾つかの制限はあるものの、要件次第では Excel オートメーションより高速な場合さえありますね。


ただし取得される結果は DataTable もしくは DataReader になりますので、
最終的に 1 次元配列であることを望むのであれば、
やはり LINQ なり for/foreach なりでの詰め替えは必要ですね。


あるいは
ADO.NET + ACE プロバイダの代わりに、
ADO + ACE プロバイダの組み合わせにすれば、
ADODB.Recordset に受け取り、
 // object[,] array2D = recordset.GetRows();
 string[] strArray1D = recordset.GetString(ADODB.StringFormatEnum.adClipString, 5000, "\t", "\r\n").Split(new string[] { "\r\n" }, StringSplitOptions.None);
のようにするという手が使えるかもしれません。
[ 親 88223 / □ Tree ] 返信 編集キー/

▲[ 88237 ] / 返信無し
■88238 / 4階層)  Re[4]: c# エクセルのセル範囲を1次配列に格納する方法
□投稿者/ ユウタ (3回)-(2018/08/11(Sat) 17:53:24)
魔界の仮面弁士さん

丁寧にありがとうございます。
度々すみません、実際に私がやりたい処理としては、約39000行、6000列程のデータを扱うものとなります。
(例示と実際が結構違ってすみません。。)

行ごとに特定の値がある場合は、その付近の値を調べることを繰り返すため、
なるべく早く処理をしたいというものです。

提示いただいたコードなど、速度を見てみました。

<処理内容>
前提:セルA1〜A39000までに、文字列があるセルと空白セルがある。
処理:セルA1〜A39000までの、文字列があるセルの個数をforでカウントする。

@Range <格納> 2次配列 <変換> 1次配列 → 処理実行
・配列化:0.378秒
・処理 :0秒

ARange <格納> 2次配列 → 処理実行
・配列化:0.362秒
・処理 :0.002秒

BRange <for> 1次配列 → 処理実行
・配列化:41.652秒
・処理 :0.001秒

CRange → 処理実行
・配列化:0.346秒(Rangeのみ)
・処理 :25.421秒

DRange <変換> 1次配列 → 処理実行
・配列化:36.603秒
・処理 :0秒
 →ただし、文字認識できない


結果として、
・エクセルのRangeをそのまま処理すると遅い
・1次配列・2次配列では、速度にさほど差はない
ということでした。

私が、圧倒的に速いと感じていたのは、エクセルのRangeでの処理速度と配列での処理速度の差でした。
1次配列・2次配列においての速度は、誤差の範囲になるので、
複数行・列を取得できる2次配列の方が利便性が高い場合もあると思いますので、
適宜使い分けていこうと思います。

ありがとうございました。
解決済み
[ 親 88223 / □ Tree ] 返信 編集キー/


管理者用

- Child Tree -