2012-07-26

Force.com : VF 画面から CSV ファイルをアップロードし、内容を画面に表示したい (ファイル文字コード Shift_JIS 編)


VF 画面から CSV ファイルをアップロードし、内容を画面に表示したい (※ ファイル文字コード Shift_JIS)



● あらすじ

前回、
なる記事を書いてみた。
しかし、CSV ファイルが、UTF-8 or 1byte文字のみなファイルではなく、Shift_JIS なファイルである必要が出ていたために今回に至るのであった。
※ 1byte 文字のみ含まれるファイルであれば前回の記述で OK なのである



○ 考察

問題は、下記。

public class ImportController {

    public Attachment d { get; set; }

        :

    public PageReference read() {

        :

        Blob data = d.Body;
        String body = data.toString();

        :

        return null;
    }

UTF-8 以外の 2byte 文字が来るとエラーが発生する。
しかし、Blob の値が UTF-8 以外の文字コードのものを String にする術がわからない ;-(。
Google 先生に聞くと、このサイト を教えてくれた!ので、試す。

public class ImportController {

    public Attachment d { get; set; }

        :

    public PageReference read() {

        :

        // ※ コピペさせていただいてます ※ //
        Blob data = d.Body;
        String hex = EncodingUtil.convertToHex(data);
        String result = '';
        for (Integer i = 0, size = hex.length(); i < size; i = i + 2) {
            result = result + '%' + hex.substring(i, i + 2);
        }
        String body = EncodingUtil.urlDecode(result, 'Windows-31J');

        :

        return null;
    }

... 130KB くらいのファイルで試すと(体感的に)遅い。場合によってはセッションタイムアウトが発生する。
for 文のところがかなり問題なのである。たぶん、String#substring() 。

要は 16 進数文字列の 16 進数の頭に % を付加すればよいのだろ。文字列置換と正規表現を組み合わせればどうにかなるんじゃないか!?。

public class ImportController {

    public Attachment d { get; set; }

        :

    public PageReference read() {

        :

        Blob data = d.Body;
        String hex = EncodingUtil.convertToHex(data);
        String result = hex.replaceAll('[[0-9][a-fA-F]]{2}', '%$0');
        String body = EncodingUtil.urlDecode(result, 'Windows-31J');

        :

        return null;
    }

再び、130KB くらいのファイルで試すと。なんとか、(体感的に)早くなったのでもうこれで良しとする。
( ... しかし、変態的過ぎる。

※ 追記: 2012-07-26 19:00 くらい
160KB 超えのファイルをアップロードしたところ、
System.LimitException: Regex too complicated
なる例外が ... 正規表現のヒープサイズを超えたよう。
もう、心折れそう ;-(。

※ 追記: 2012-07-27 15:00 くらい
もういいよ。
正規表現で置換する 16 進数文字列のサイズを分割してやればいいんじゃないか(やけくそ ;-(。

public class ImportController {

    public Attachment d { get; set; }

        :

    public PageReference read() {

        :

        Blob data = d.Body;
        String hex = EncodingUtil.convertToHex(data);

        String result = '%';
        Integer hexSize = 300000;    // ※ この数字は適当です ※ //
        Integer qu = hex.length() / hexSize;
        Integer i = 0;
        while(true) {
            if(i >= qu) {
                if(i == qu) {
                    result += hex.substring(i*hexSize).replaceAll('[[0-9][a-fA-F]]{2}', '$0%');
                    result = result.substring(0, result.length()-1);
                }
                break ;
            } else {
                result += hex.substring(i*hexSize, (i+1)*hexSize).replaceAll('[[0-9][a-fA-F]]{2}', '$0%');
            }
            i++;
        }
        String body = EncodingUtil.urlDecode(result, 'Windows-31J');

        :

        return null;
    }

340KB くらいのファイルで試す。正規表現の例外は発生しなくなった。
因みに、分割サイズは適当。「500000」だと例外が発生した。「300000」だと例外は発生しなかった。その間は試していない。ヒープサイズの最大は 6MB なのはわかったのだけれども、その割り振りがわからないでいる。
String#substring() かむばっく。なので、残すは 5MB くらいのファイルに耐えれるか否かである。... より、レスポンスとファイルサイズで折り合いつけよう(もう、半ばどうでもよくなってきた。

それより最初、ビューステートのエラーが発生していたので VF 画面に javascript でファイルサイズのチェックを行っていたのだけれども、今は、135KB のファイルサイズチェックなくても大丈夫っぽ(?)。5MB 超えると勝手に「アップロードできません」なエラーメッセージ出してくれる。ビューステート回避のためにコード書き換えた記憶はあるのだけれども、どう書き換えたのかまではもう覚えていない ;-)。取っ払っておこう。そして、そのうち調べてまとめておこう(希望的観測 :-)。

※ 追記: 2012-07-30 19:00 くらい
3MB くらいのファイルをアップロードしたところ、
System.StringException: String length exceeds maximum: 6000000
なる例外が発生 ... ;-(。
「result += ...」あたりで発生しているらしい。
google 先生に聞いたら、たぶん理由はこんな感じ?
ただ、これが攻略できてもまた
System.LimitException: Regex too complicated
が発生するね。
List lines = body.split('\r\n');
のとこで。てか、UTF-8 版で 5MB 弱のファイルで試したら発生した。

あと、ユーザの設定で「Apex 警告メールの送信」のチェックボックスを ON にしている場合、画面上はエラー画面に遷移はしないけれども「Apex governor limit warning」なメールがやってくる。(16 進数文字列を URL パラメータに正規表現で変換しているところか body.split('\r\n') なところでだろ)。
ログを見ると、
Maximum heap size: 3184485 out of 6000000 ******* CLOSE TO LIMIT
だって。


要は「VF 画面から CSV ファイルをアップロードし、内容をごにょごにょする」なんて思わないが吉!


※ この件はホントの仕様に合わせてみて、そこで折り合いつけることにしたのでここまでとする :-)。



○ 要件

  • CSV のみアップロードを可能とする
  • アップロードするファイルの文字コードは Shift_JIS とする
  • アップロードするファイルの改行コードは \r\n とする



○ Apex クラス : ImportController

■ Shift_JIS 用
public class ImportController {

    public Attachment d { get; set; }

    public String filename { get; set; }

    public List<List<String>> rows { get; set; }

    public ImportController() {
        if(d == null) {
            d = new Attachment();
        }
    }

    public PageReference read() {
        if(d == null || d.Name == null) {
            return null;
        }

        filename = d.Name;

        // Shift_JIS
        Blob data = d.Body;
        String hex = EncodingUtil.convertToHex(data);
        String result = '%';
        Integer hexSize = 300000;
        Integer qu = hex.length() / hexSize;
        Integer i = 0;
        while(true) {
            if(i >= qu) {
                if(i == qu) {
                    result += hex.substring(i*hexSize).replaceAll('[[0-9][a-fA-F]]{2}', '$0%');
                    result = result.substring(0, result.length()-1);
                }
                break ;
            } else {
                result += hex.substring(i*hexSize, (i+1)*hexSize).replaceAll('[[0-9][a-fA-F]]{2}', '$0%');
            }
            i++;
        }
        String body = EncodingUtil.urlDecode(result, 'Windows-31J');

        List<String> lines = body.split('\r\n');
        rows = new List<List<String>>();
        for(String line : lines) {
            if(line.startsWith('"')) line = line.substring(1);
            if(line.endsWith('"')) line = line.substring(0, line.length()-1);
            rows.add(line.split('","', -1));
        }

        return null;
    }
}



○ Visualforce : Import

<apex:page controller="ImportController">
<script type="text/javascript">
    function getExtention(fileName) {
        if (!fileName) {
            return "";
        }
        var fileTypes = fileName.split(".");
        if (fileTypes.length === 0) {
            return "";
        }
        return fileTypes[fileTypes.length - 1];
    }
    function isCsvFile(file) {
        var extention = getExtention(file.name);
        if (extention.toLowerCase() === "csv") {
            return true;
        } else {
            alert("CSVファイルを設定してください.");
            return false;
        }
    }
    function getFile(frm) {
        return frm.elements[2].files[0];
    }
</script>

    <apex:messages />
    <apex:form >
        <apex:pageBlock >
            <apex:pageBlockButtons >
                <apex:commandButton value="read" action="{!read}" onclick="return (isCsvFile(getFile(this.form)));" />
            </apex:pageBlockButtons>
            <apex:pageBlockSection >
                <apex:outputPanel >
                    <apex:outputText value="file: " /><apex:inputFile value="{!d.body}" filename="{!d.name}" />
                </apex:outputPanel>
            </apex:pageBlockSection>
        </apex:pageBlock>
        <apex:pageBlock rendered="{!NOT(ISNULL(rows))}">
            <apex:outputText value="{!filename}" />
            <table>
                <apex:repeat value="{!rows}" var="row">
                <tr>
                    <apex:repeat value="{!row}" var="col">
                    <td style="width:80px;"><apex:outputText value="{!col}" /></td>
                    </apex:repeat>
                </tr>
                </apex:repeat>
            </table>
        </apex:pageBlock>
    </apex:form>
</apex:page>



◇ 環境

  • Salesforce - Developer Edition - API バージョン 25.0



◇ 参考


0 件のコメント:

コメントを投稿