SASでの文字コードの扱い方

概要

あんまりないと思うが, 文字コードが異なるOS間でデータのやりとりをするときの話.

テキストファイルの文字コードを特定する

Windows で作成したテキストファイルはたいていシフトJISになるが, SASonDemand は UTF-8 なのでそのまま読み込もうとすると文字化けする. 文字コードの変換については, 『Shift-JIS の固定長ファイルを UTF-8 環境に読み込む (kcvt関数) - SAS | data Memorandum ; set Memory ; run ;』 のように kcvt() 関数を使ったり, LinuxSAS が入っているなら pipe エンジンを使って iconvnkf コマンドを挟むという手もある. しかし, 単にテキスト全体が異なる文字コードで, csv のようにセパレータがある*1というのなら, もう少し楽な方法がある. ENCODING=オプション で紹介されているencoding= オプションだ.

あ,イロハニ,123
あ,イロハニ,123

という内容のファイルをシフトJIScsv で作成し, SASonDemand で読み込ませてみる.

data test; infile "&homedir/sjis/test_sj_r.csv" dlm=',' encoding=sjis;
    length var1 $3 var2 $9 var3 8;
    input var1 var2 var3;
run;
proc contents data=test;
run;
proc print data=test;
run;

なお, encoding= オプションは infile だけではなく filename, でも使える. また 書き出す際の file ステートメントでも同じである.

しかし, これを実行すると期待どおりの結果が帰ってこない.

f:id:ill-identified:20150912201314p:plain
f:id:ill-identified:20150912201322p:plain

原因のヒントはログにあって,

f:id:ill-identified:20150912201330p:plain

123 としか入力してないはずなのに, 末尾に . がある. 実は, この csv の改行コードは CR になっている*2. そのため, 改行コードを正しく認識できず, 不正な文字列として処理されてしまった. 改行コードは読み込み時だけでなく, 書き出しの際にも問題になる. 例えば CR を改行コードとして出力した場合, Windows のメモ帳などでは CR+LF を改行コードとして認識するから, すべての行が改行されず1行にすべて横並びになってしまう. 今は CR 単体を改行コードとして使う場面はほとんどなさそうだが, Linux 系は LF, Windows 系は CR+LF, が普通だから, 文字コードに気をつけるときは改行コードにも気をつけた方がいい.

この改行コードも, 実は簡単に指定できて, termstr= オプションを使う(14178 - INFILE's TERMSTR= option facilitates reading files between UNIX and Windows). そしてこれも infile, file, filename の全てのステートメントで使用できる. 今回の場合は

data test; infile "&homedir/sjis/test_sj_r.csv" dlm=',' encoding=sjis termstr=CR;
    length var1 $3 var2 $9 var3 8;
    input var1 var2 var3;
proc contents;
proc print;run;

で読み込める. SASonDemand だとオートコンプリート機能があるので, 自動で候補が現れるが, 指定する名称は CRLF とか LF とかそのまんまである (なお小文字でもいい).

文字コードの異なるデータセット

先の contents プロシジャのスクリーンショットで見たように, SASのデータセット (sas7bdat) は, 文字のエンコードが何かを記録している. よって, 基本的に SAS文字コードが現在のセッションと異なるものでも自動で変換してくれる. そのとき, こういうメッセージが表示される.

f:id:ill-identified:20150912201439p:plain

ただし, 文字コードの異なるデータセットを読み込む際に, 問題が起こる場合が2通りある.

1つは, 何らかの理由で, sas7bdat から文字コード情報が欠落しているため文字化けが起こる場合, もう1つは, 変換後の文字列のバイト数が文字変数の長さを超えてしまうため文字列の切り捨てが起こる場合である.

1つ目の, 文字コード情報が欠落している場合は, 対処が簡単で, テキストファイルのときと同様に, 文字コードを指定するオプションを使う. これには2つ方法があり, 1つは libname ステートメントinencoding= オプションを使うことでライブラリのデータセットに一括で指定する方法で, もう1つはデータセットオプションの encoding= を使う方法がある (Technical Support, SAS(R) 9.2 National Language Support (NLS): Reference Guide).また, これらはどうやら v9 以降でしか使えないようなので注意. 具体的には, こういうようなコードになる.

libname sjis "&homedir/sjis" inencoding=sjis;
proc print data=sjis.ds;
run;
/*出力の場合は outencoding*/
libname utf "&homedir/utf" outencoding='utf-8';
/*proc copy では文字コード変換されない*/
data utf.ds;
    set sjis.ds;
run;
libname sjis "&homedir/sjis";
proc print data=sjis.ds(encoding=sjis);
run;

もちろん, 指定する文字コードを間違えると文字化けしたりエラーが起こったりする*3.

なお, これまでは sjis を指定してきたが, utf-8 のようにハイフンを含む名称を指定する場合, 引用符で囲まないとエラーが発生する.

次に, 変換後のバイト数が文字変数の長さを超えてしまう場合だが, これは sjis から utf-8 へ変換するときによく起こる (UTF-8 のほうが1文字のバイト数が大きくなる場合が多いので ). 加えて言うと, 文字コードが指定されていても発生する. こういう場合, inencoding=’utf-8’ に加えて cvp (character variable padding)エンジンを利用して, 読み込み時に文字変数の長さを拡張する.

libname sjis cvp "&homedir/sjis" inencoding=sjis;
proc print data=sjis.ds;
run;

(参考: CVPエンジンを使用した文字データ切り捨てへの対応 ).

cvp エンジンについては, 公式サイトではないが 『SASでのトランスエンコーディングあれこれ。 - The Nameless City』 でいろいろ細かく解説されていて参考になる. cvp によりデフォルトでは 1.5倍に拡張される (SAS(R) 9.4 National Language Support (NLS): Reference Guide, Fourth Edition). sjis から utf-8 への変換ならだいたいこれでなんとかなるが, 半角カナ等が含まれると足りなくなる可能性がある. そういうときは cvpmultiplier= オプション (省略形 cvpmult=) で倍率を指定する.

また, cvp エンジン使用時の注意点として, cvp を使ったライブラリは強制的に読み込み専用になる, ということがある. これはあくまで, 読み込み時に変換しているだけなので, 新しい文字コードに変換したデータセットを作成したい場合, 別のライブラリに出力する必要がある. よって, sjis から utf-8 へ変換するのに最も安全な方法は

libname sjis cvp "&homedir/sjis" inencoding=sjis cvpmult=3;
libname utf "&homedir/utf";
data utf.ds;
    set sjis.ds;
run;

のようにすることになる (データセット文字コード情報を保持しているなら, inencoding= は不要).

最後に, cvp オプションの弱点を言っておくと, これは全文字変数の長さを一律に拡張するもので, 個別に長さを指定できない, ということがある. つまり, 例えば date=20150912 とか, sex=”F” のように, 文字コードを変えてもバイト数が変わらないような文字列しか入っていない文字変数についても長さを拡張してしまう. よって,ファイルサイズの膨張が気になる場合, cvp はあまり使えないかもしれない.

不要な文字変数の長さの拡張をしたくない場合, 例えば データセットを変換する前にデータセットを読み込んで, 全オブザベーションを確認して, バイト数の変わるカナや漢字が含まれるかどうかを判定し, contents プロシジャで各文字変数の長さを取得し, マクロ変数に取り込み, あらためてデータステップで カナ漢字を含む文字変数だけマクロを使って length に倍数を掛けて..., という風に, プログラムを書くのも処理するのもそれなりに時間のかかることをしなければならない.

*1:固定長ならば, 変換後のバイト数を考えて読み込み列を指定するか, そのまま読み込んで, データステップで kcvt 関数で変換することになる.

*2:上記のテキストを単にコピーしても, 改行コードがCRにならないので, これを再現するのは少し工夫がいる

*3:このへんも実演したかったのだが, SASonDemand では sjis のバイト数に合わせた長さの文字変数を作ることができなかった. プリファレンスにエンコーディングを変更する項目があるのだが, セッションエンコーディングは変更されなかった.