プログラマ38の日記

主にプログラムメモです。

CSV: CSVファイルを加工する方法

CSVを加工する機会は多いと思います。データのやり取りはなんだかんだでCSVが主流で、Salesforceのデータローダでもインポート、エクスポートはCSVを使います。

 

自分がよく使うCSVの加工するやり方について書きます。

ちなみに、一番よく使うのは6の「H2 Database」です。

  1. テキストエディタで加工する
  2. Excelで加工する
  3. MS-Accessに一度取り込んだ後加工する
  4. ODBCの「Microsoft Text Driver (*.txt; *.csv)」で加工する
  5. Javaライブラリ「HSQLDB」のTEXT Tableにセットした後加工する
  6. Java ライブラリ「H2 Database」で加工する
  7. MySQLSQL Serverへ取り込んだ後加工する

1.テキストエディタで加工する

原始的にテキストエディタでテキストを開き、値の確認や修正を行います。値の中で改行が多い場合、どの行を編集しているかがわからなくなります。あと、CSVが大きくなると開けない時があります。もちろん他のデータと結合や集計などはできません。

2.Excelで加工する

ExcelCSVを開き、値の確認や修正を行います。単純に開くと"000010"のような値はゼロサプレスの対象となり、保存すると日付のフォーマットが勝手に変わってしまうリスクがあります。Excelのデータタブからテキストファイルを取り込むことで、ファイルの細かな指定をして取り込むことができますが、項目が多いと非常に大変です。
他のデータと結合や集計などは、vlookup関数やピボットテーブルを使うことで近いことはできますが限界があります。そして、CSVが大きくなるとExcelで扱うのは現実的ではありません。

3.MS-Accessに一度取り込んだ後加工する

MS-Accessでテキストファイルをインポートして、テーブルとして扱います。テーブルの列数は255までです。CSVもある程度の件数まで扱えます。(サイズが大きいとMS-Accessのファイルが壊れるので注意が必要です)
データベースなので結合や集計は可能です。(SQLエンジンがJetなので多少癖は強いです。) 一度取り込んでしまえば、クエリを駆使したり、別のテーブルを作成
(select  field1, ・・・・ into newtable from orgtable  の書き方が便利です)
したりした後、エクスポートすることで加工後のCSVを作成します。インポートとエクスポートはコマンドではなくGUIになる以外はあまり違和感がないやり方ですが、インポートするファイルの数が多かったり、エクスポートするファイルの数が多いとGUIがネックになります。


4.ODBCの「Microsoft Text Driver (*.txt; *.csv)」で加工する

Windows限定のやり方ですが、MS-Accessにデータをいれることなく、CSV自体をテーブルとして扱い、selectや、新しいCSVを作成します。これはMS-Accessと同じSQLエンジンなので、MS-Accessと同様に項目は255までとなります。これを使う際には、ODBCを扱えるSQLクライアント(cseなど、もしくはJava用のSQLクライアントでodbc driverを使うかです)か、OleDBを扱えるSQLクライアントが必要です。また、ODBCを設定する際には、64bitのPCでは、「SysWOW64」の中のODBC定義(odbcad32.exe)から設定が必要です。
ドライバで定義したフォルダに「schema.ini」を定義することで、ファイルの文字コードや項目名、型を指定できますが、CSVファイルをShiftJISで、全項目をダブルクオートの囲み文字を指定することで、全て文字列で扱うのが簡単です。

単純なselect

  select * from sample1.csv

先頭N件のselect (件数が多い場合にデータを読む場合便利)

  select top 10000 * from sample1.csv  

csvから新しいcsvを作成

  select *  into sample2.csv from sample1.csv

5.Javaライブラリ「HSQLDB」のTEXT Tableにセットした後加工する

JDBCの「HSQLDB」を使います。そして、JDBC用のSQLクライアントが必要です。(SQuirreL SQL ClientExecute Queryが便利です)

SQLクライアントの設定で、「HSQLDB」のJDBCドライバを指定し、スタンドアロンHSQLDBを起動します。メモリモードか、ファイルモードかなどいくつか選べますが、ここではファイルモードにします。

JDBCの接続文字列 (Dドライブ直下に作成する)
"jdbc:hsqldb:d:\hsqldb"

TEXT Tableの作成
create text table tablex (f1 varchar(100), f2 varchar(100) );

TEXT TableへCSVをセット
set table tablex source "xxx.txt";

これでCSVをテーブルとして扱えます。 v2.4で試したところ、set table のCSVのファイルパスがフルパスが使えませんでした。ファイルパスは接続文字列で指定したドライブからの相対パスになるようです。もちろん、set tableでオプション指定することが区切り文字やファイルのエンコーディングの指定ができます。

事前にテーブルの作成が必要になるのが手間です。 


6.Java ライブラリ「H2 Database」で加工する

JDBCの「H2 Database」を使います。5のhsqldbと同様に、JDBC用のSQLクライアントが必要です。そして、csvreadという関数を使うのですが、これが一番使いやすいと感じています。csvread関数は、全件メモリに展開するようで、ファイルサイズが大きいとエラーになります。(ヒープサイズを増やせばそれなりの件数は扱えますが)

JDBCの接続文字列 (Dドライブ直下に作成する)
jdbc:h2:mem:.


CSVにselectする
select * from csvread('D:\sample.csv')

CSVからテーブルデータを作成する
create table sampletable as select * from csvread('D:\sample.csv')

テーブルデータをCSVに出力する
call csvwrite('D:\sampleexport.csv','select * from sampletable')

csvread関数で引数を渡すことで、ファイルの区切り文字やエンコーディングを指定できます。


7.MySQLSQL Serverへ取り込んだ後加工する

データ件数が多い場合は通常のデータベースを使う事になります。

MySQLの場合

Load data Local INFILE 'D:\\sampledata.csv' into table sampletable
character set cp932
fields terminated by ','
           enclosed by '"'
lines
          terminated by '\r\n'
ignore
         1 lines
;

SQL Serverの場合

BULK INSERT sampletable
FROM 'D:\sample.csv
WITH
(
   FIRSTROW = 1,
   FIELDTERMINATOR = ',',
   ROWTERMINATOR = '\n'
);

となり、ここまでくるとちょっとした開発みたくなってしまいます。(厳密にファイルの仕様を定義しないとデータもおかしくなります)

 

最後に

6のJava ライブラリ「H2 Database」で扱う の使い勝手がとても良いです。64bitマシンでメモリに余裕があれば、-Xmx10G などとすることでたいていの加工処理が済んでしまいます。是非お試しください。

Salesforce: Shift_JISのテキストファイルを作成して添付ファイルに登録する

SalesforceのApexプログラムの中で、Shift_JISCSVファイルを作成したい時があります。

 

Salesforceの文字列とバイナリは、StringとBlobを使います。BlobからStringとその逆の変換の関数は用意されていますが、その際のエンコーディングUTF-8となります。

 

そこで、エンコーディングをShiftJISにするために工夫が必要になりますが、次の2点のやり方を考えました。

 

  1. CSV出力するVisualforceを作成し、PageReference.getContent()を使う
  2. EncodingUtil.urlEncodeとEncodingUtil.convertFromHexを利用し、Shift_JISのバイナリに変換する

 

1の方法はシンプルですが、PageReference.getContent()が利用できない(トリガでは@futureが必要となる)箇所があります。
やり方は、CSV出力用のVisualforceを作成し、

<!-- CSVPage という名前のページ -->
<apex:page controller="SampleController" contentType="text/csv;charset=Windows-31J;#test.csv">{!csvtext}</apex:page>

 コントローラ側でPageReference.getContent()を使いバイナリを取得します。

PageReference csvpage = Page.CSVPage;
Blob csvcontent = csvpage.getContent();

ポイントは、Visualforceページで文字コードを指定できるので、そこでShift_JISを指定します。

 

2の方法は少々強引ですが、次のようなコードでバイナリを作成することができます。下記のstrToShiftJISBlob関数に、引数にファイルにしたい文字列を指定してバイナリに変換します。
もちろん処理のコストは高く、長い文字列は扱えませんが、短い文字なら問題ありません。

    public static Blob strToShiftJISBlob (String str){

        String body = '';

        for( Integer i = 0 ; i < str.length(); i++ ){
            String moji = str.substring(i,i+1);

            String encoded = EncodingUtil.urlEncode(moji , 'Windows-31J' );

            if(encoded.length() == 1){
                Integer charval = moji.getChars()[0];
                encoded = '%' + toHex(charval );
            }

            body += encoded;
        }


        body = body.replaceAll('%','');

        Blob blobValue = EncodingUtil.convertFromHex(body);

        return blobValue ;
    }

    private static Map<Integer,String> hexmap = new Map<Integer,String>{
        0  => '0'
       ,1  => '1'
       ,2  => '2'
       ,3  => '3'
       ,4  => '4'
       ,5  => '5'
       ,6  => '6'
       ,7  => '7'
       ,8  => '8'
       ,9  => '9'
       ,10 => 'A'
       ,11 => 'B'
       ,12 => 'C'
       ,13 => 'D'
       ,14 => 'E'
       ,15 => 'F'
    };


    private static String toHex( Integer val ){

    //処理場ありえないが、変換できないものは?
        if(val > 255 ){
            val = '?'.getChars()[0]; 
        }

        Integer moji1 = val / 16;
        Integer moji2 = Math.mod(val , 16);


        return hexmap.get(moji1 ) + hexmap.get(moji2 );
    }

 

最後に

データローダを使わずにSalesforceだけで簡潔させようとすると、エンコーディングUTF-8が標準となるので、ShiftJISにしようとするとコードが増えるなーと感じました。

あと、ShiftJIS、ShiftJISと書いていますが、コード上はWindows-31J(CP932)にしています。エンコーディングってややこしいですね。。

Java: UnixでJavaプログラムを動かした時にはまった思い出

プログラマになって間もない頃、単純なOracleへのデータ連携用プログラムを担当しました。
処理はShift_JISCSVファイルをいくつか値を変換してOracleにロードするものだったので、通常は、
1.Oracleへワークテーブルを用意し、
2.「sqlldr」でデータを投入後、
3.PL/SQLで変換して目的のテーブルにデータを入れる
という処理でいいのですが、JavaJDBCで頑張っていました。理由は覚えていません。

 

開発はWindowsのPCで、実行はUnix環境だったのですが、その時にはまった思い出になります。


はまった内容

  1. Windowsjavaコマンドのクラスパスの区切りはセミコロン (;) ですが、Unixではコロン (:) となる
  2. FileReaderを使っていたため、WindowsではShift_JISでも、Unixでは別のエンコーディングとなる。(その時のUnixではUTF-8でした)

 

1は、見よう見まねで作成したシェルでjavaコマンドの実行がエラーとなり相当焦ってたような記憶があります。
1つうまくいかないと、それまでの作業全てを疑いはじめてしまい、シェルの書き方が違うのではないか、WindowsコンパイルしたクラスをそのままUnixに持っていったのが悪いのではないか、持って行くときにおかしな変換がされてしまったのではないかなど疑心暗鬼でいっぱいになりました。

 

2は、JavaのFile IOではトップ1に来る有名なエンコーディングを指定する書き方さえも当時しらなくて、ネットで拾った次のコードを書いていました。

  FileReader in = new FileReader("file.txt");
  BufferedReader br = new BufferedReader(in);

 もちろん、そのネットのページには補足で次のエンコーディングを指定する方法も書いてあったのですが読み飛ばしていました。

FileInputStream is = new FileInputStream("file.txt");
InputStreamReader in = new InputStreamReader(is, "Shift_JIS");
BufferedReader br = new BufferedReader(in);

この書き方がおまじないだとしても、FileReaderの引数に、エンコーディングがあればいいだけなんじゃないかと今でも思っています。。(ファイル以外からのバイトのストリームにも柔軟に対応できる設計なのはわかるんですが)

 

最後に

今となっては当たり前なことですが、当時の自分には解決に丸1日かかるものでした。

以前半角英数字と記号だけならutf8とshiftjisは同じバイナリということを知らない子がいて少しやりとりをした時に、ふと自分が昔はまったことを思い出したので書いてみました。

DWH/SQL: データマートの作り方メモ

DWHで使い勝手を向上するため、粒度の異なる複数のファクトを加工して1つのデータマートを作成する時があります。

例えば、次のようなデータマートを作成します。

 

インプットデータ

  • 商品別、年月別の見込みデータがある。
  • 商品別、年月日別で実績データがある。

アウトプットデータ

  • 商品別、年月別で見込み・実績データマートを作る。
    (見込み・実績データマートは、商品、年月に対して見込値、実績値を持つ)

こういった時のデータマートを作るときは次のようなSQLになります。

select
  商品          as 商品
 ,年月          as 年月
 ,sum(見込値)   as 見込値
 ,sum(実績値)   as 実績値  
(
  select
    商品                       as 商品
   ,年月                       as 年月
   ,見込値                     as 見込値
   ,cast(null as decimal(18))  as 実績値
  from 
    見込みデータ
  union all
  select
    商品
   ,to_char(年月日,'YYYYMM')
   ,null
   ,実績値
  from 
    実績データ
) ユニオンデータ
group by
  商品          as 商品
 ,年月          as 年月

考え方としては、まずユニオンを使って縦にデータを並べて、並べた後に集計を行います。
縦に並べる際に、数値項目は、列を分けておくことで集計後に別項目として扱います。

使うのはunion all と group by なので全てのDBで可能なやり方です。

 

このやり方を使うと、見込データにはあるけど実績データにない「商品、年月の組み合わせ」やその逆で、実績データにはあるけど、見込みデータにはない「商品、年月の組み合わせ」も正しくデータを作成することができます。


実際のデータマートでは集計軸となる項目はもっとたくさんあって、時にはその項目がnullの場合もあると思いますが、このSQLは結合ではなく、集計を使うので、nullは集約できます。

 

他のやり方としては、見込みデータと実績データをそれぞれデータの粒度を揃えた後、full outer結合というやり方もなくはないですが、結合条件の値がnullだった時は結合できないのと、full outer結合ができないDBもあるのでお勧めしません。

 

次のサンプルデータでデータマートを作ってみます。

[見込みデータ]

商品 年月 見込値
P001 201001 960
P002 201001 274
P003 201001 181
P001 201002 276
P002 201002 499
P003 201002 3
P001 201003 141
P002 201003 993
P003 201003 757
P001 201004 610
P002 201004 455
P003 201004 903

 [実績データ]

商品 年月日 実績値
P001 2010/01/06 19
P003 2010/01/06 19
P001 2010/01/10 27
P003 2010/01/10 95
P004 2010/01/11 1
P003 2010/01/12 43
P001 2010/01/13 50
P003 2010/01/14 31
P001 2010/01/15 18
P004 2010/01/16 39
P001 2010/01/17 41
P003 2010/01/18 83

 [データマート]

商品 年月 見込値 実績値
P001 201001 960 155
P002 201001 274  
P003 201001 181 271
P004 201001   40
P001 201002 276  
P002 201002 499  
P003 201002 3  
P001 201003 141  
P002 201003 993  
P003 201003 757  
P001 201004 610  
P002 201004 455  
P003 201004 903  

 上のようにデータマートが作れます。

 

補足
TERADATAでユニオンを指定したselectを行うと、先頭の項目の型と桁が使われます。
(なので先頭の項目がchar(10)の場合、2番目の項目がchar(100)だとしてもchar(10)で切り取られたりします)
ただし、そのselect文を使ったinsert文ではinsert先の項目の型と桁が優先されます。

Salesforce: SOQLやビューでよく使う日付の検索条件文字列

ビューやSOQLでは、当日を起点にして何日前や何日後といった条件を指定したい時があります。

 

たくさんある中でよく使う書き方のメモです。

 

ビューでの日付の検索条件文字列

Salesforceのリンクはこちらです。

日付の検索条件リテラル コメント
過去n日間 演算子を"次の文字列に一致する"にすると、n日前から今日までとなる。
演算子を">"にした場合は、当日との比較となる。
演算子を">="にした場合は、n日前との比較となる。
演算子を"<"にした場合は、n日前との比較となる。
演算子を"<="にした場合は、当日との比較となる。
翌n日間 演算子を"次の文字列に一致する"にすると、翌日からn日間までとなる。
演算子を">"にした場合は、n日後との比較となる。
演算子を">="にした場合は、翌日との比較となる。
演算子を"<"にした場合は、翌日との比較となる。
演算子を"<="にした場合は、n日後との比較となる。

 

SOQLでの日付の検索条件文字列

Salesforceのリンクはこちらです。

日付の検索条件リテラル コメント
LAST_N_DAYS:n 演算子を"="にすると、n日前から今日までとなる。
演算子を">"にした場合は、当日との比較となる。
演算子を">="にした場合は、n日前との比較となる。
演算子を"<"にした場合は、n日前との比較となる。
演算子を"<="にした場合は、当日との比較となる。
NEXT_N_DAYS:n 演算子を"="にすると、翌日からn日間までとなる。
演算子を">"にした場合は、n日後との比較となる。
演算子を">="にした場合は、翌日との比較となる。
演算子を"<"にした場合は、翌日との比較となる。
演算子を"<="にした場合は、n日後との比較となる。

 

ビューの検索条件文字列の、過去n日間と LAST_N_DAYS:n、翌n日間とNEXT_N_DAYS:nは同じ動きになり、図でまとめると次のようになります。

 last_n_days:2を例とした場合

f:id:crmprogrammer38:20170828233349p:plain

 

 next_n_days:2を例とした場合

f:id:crmprogrammer38:20170828233802p:plain

 

最後に

等号("次の文字列に一致する"、"=")で指定した場合は、対象の期間でフィルタしますが、大なり、小なりと大なりイコール、小なりイコールで検索の期間が異なるので注意が必要です。

特に、データ連携などをする際には、SOQLで"LAST_N_DAYS:n"はよく使います。オブジェクトにある共通項目の「作成日」、「更新日」、「System Modstamp」にはインデックスがあらかじめ作成されており、"LAST_N_DAYS:n"を指定するとインデックスを利用した実行プランが選択されるので、クエリタイムアウトの可能性は低くなります。

 

SOQLの"LAST_N_DAYS:n"は日での指定ですが、月での指定もできて、"LAST_N_MONTHS:n"となります。

同様に、ビューでの指定も"過去n日間"の日の指定に対し、"過去 n か月"と月で指定することができます。(月の指定の場合nの前後に半角スペースが必要になります)

Salesforce/SQL: 検索条件でのnullの扱いの違いについて

Salesforceではデータの取得でSOQLを使います。
一般的なRDBSQLと似ていますが、結合処理や、検索の指定がSQLと異なります。

検索条件でのnullの指定と検索結果について、SOQLとSQLの違いがあるのでメモです。

 

次のデータをサンプルとしてSOQLとSQLで検索結果の違いを見ていこうと思います。

# 項目 備考
1 valueA  
2 valueB  
3   null値です。
4 valueC  

 

nullの条件の指定 

SOQLのnullの条件は、 「項目 = null」 または 「項目 = ''」 です。
Salesforceには空文字でデータ登録はできないのでSOQLでは、nullと空文字で区別はありません。
※Apexではnullと空文字は異なります。
「項目 = null」の条件に対して結果は次です。

# 項目 備考
3   null値です。

 

SQLのnullの条件は「項目 is null」です。この条件での結果は次です。

# 項目 備考
3   null値です。

 

in句でのnullの条件の指定

SOQLで、in句でnullを指定可能です。 逆にin内でnullが入っていると検索結果に含まれてしまいます。
「項目 in ('valueA',null,'ValueC')」の条件に対して結果は次です。

# 項目 備考
1 valueA  
3   null値です。
4 valueC  

 

 SQLで、in句にnullを指定しても結果には含まれません。
「項目 in ('valueA',null,'ValueC')」の条件に対して結果は次です。

# 項目 備考
1 valueA  
4 valueC  

 

XX以外の条件でnullの扱い

SOQLでは、例えば 「項目 != 'valueA'」のように、XX以外を指定した場合、
指定された項目がnullの値は検索結果に含まれます。
「項目 != 'valueA'」の条件に対して結果は次です。

# 項目 備考
2 valueB  
3   null値です。
4 valueC  

 

 SOQLでは、例えば 「項目 != 'valueA'」のように、XX以外を指定した場合、
指定された項目がnullの値は検索結果に含まれません。
「項目 != 'valueA'」の条件に対して結果は次です。

# 項目 備考
2 valueB  
4 valueC  

 

 最後に

プログラムで、nullの結果が含まれる、含まれないで動きがだいぶ違うため
扱いに注意が必要です。私はSQLに慣れ過ぎていて、Salesforceの動きで大分戸惑いましたが。。

Salesforce: 最終更新日(LastModifiedDate)とSystem Modstamp(SystemModstamp)の違い

最終更新日(LastModifiedDate)とSystem Modstamp(SystemModstamp)は、今まで特に意識もせず、同じタイムスタンプが入っているなーぐらいの認識でした。

 

ですが、「レコードの作成時に監査項目を設定」を有効化して、最終更新日(LastModifiedDate)に直接タイムスタンプを指定した時に違いがでるということを知りました。

 

例えば、監査項目を設定を有効にして次のデータを登録します。

NAME CREATEDDATE LASTMODIFIEDDATE
sample01 2017-07-27T00:44:38.000Z 2017-07-27T00:44:38.000Z
sample02 2017-07-27T00:23:48.000Z 2017-07-27T00:23:48.000Z
sample03 2017-07-27T00:31:56.000Z 2017-07-27T23:38:28.000Z

 すると登録後は、System Modstamp(SystemModstamp)は、登録時刻がセットされます。

NAME CREATEDDATE LASTMODIFIEDDATE SYSTEMMODSTAMP
sample01 2017-07-27T00:44:38.000Z 2017-07-27T00:44:38.000Z 2017-08-21T23:36:34.000Z
sample02 2017-07-27T00:23:48.000Z 2017-07-27T00:23:48.000Z 2017-08-21T23:36:34.000Z
sample03 2017-07-27T00:31:56.000Z 2017-07-27T23:38:28.000Z 2017-08-21T23:36:34.000Z

 

最終更新日(LastModifiedDate)で判定すると、直接、最終更新日(LastModifiedDate)に値を指定されると対象から漏れてしまうかもしれません。
Apexプログラムや、APIの中で、この項目の値の違いを理解して設計しておいた方がいいのだと思います。

 

最後に

監査項目に値を指定するために、以前はSalesforceの内部設定を変更してもらう必要があったのですが、Winter16から画面の設定でできるように変更になっていたんですね。やり方は次の通りでした。

設定>ビルド>ユーザインタフェース
「レコードの作成時に監査項目を設定」および「無効な所有者のレコードを更新」ユーザ権限を有効化 にチェック

f:id:crmprogrammer38:20170822090544p:plain
プロファイルで「レコードの作成時に監査項目を設定」をチェック

f:id:crmprogrammer38:20170822090758p:plain
さらに、プロファイルで「無効な所有者のレコードを更新」をチェックすると所有者に無効ユーザが指定できます

f:id:crmprogrammer38:20170822091032p:plain