SHIFT-JISコードに変換


ガリレオ  2008-10-18 04:31:59  No: 32256  IP: [192.*.*.*]

JISコードで書かれたテキストファイルをSHIFT-JISコードに変換したいため、以下のようなコードを書いて実行してみました。
100KBほどのテキストファイルならば素早く変換できるのですが、20MBぐらいのテキストファイルの場合、やはり時間がかかってしまいます。
フリーソフトの中には大きい容量のファイルでも2〜3秒で変換してしまうものがあります。どのような工夫が必要かどなたか教えてください。


//Jisコード1文字をSJisコード1文字に変換
  function Jis_SJis(c0,c1: AnsiChar): AnsiString;
  var
    b0,b1,off: byte;
  begin
    b0 := Byte(c0);
    b1 := Byte(c1);
    Result := '';
    if (b0 < 33) or (b0 > 126) then exit;
    off := 126;
    if b0 mod 2 = 1 then
      if b1 < 96 then off := 31 else off := 32;
    b1 := b1 + off;
    if b0 < 95 then off := 112 else off := 176;
    b0 := ((b0 + 1) shr 1) + off;
    Result := AnsiChar(b0) + AnsiChar(b1);
  end;



//Jisコードの文字列をSJisコードの文字列に変換
  function JisToSJis(const s: AnsiString): AnsiString;
  var
    i: integer;
    flg: boolean;
  begin
    flg := false;
    Result := '';
    i := 1;
    while (i <= Length(s)) do
    begin
      if Copy(s,i,3) = #27 + '$B' then
      begin
        flg := true;
        i   := i + 3;
        continue;     //空の文字列がありえるので
      end;
      if (Copy(s,i,3) = #27 + '(B') or (Copy(s,i,3) = #27 + '(J') then
      begin
        flg := false;
        i   := i + 3;
        continue;     //空の文字列がありえるので
      end;

      if flg then
      begin
        if Length(s) > i then Result := Result + Jis_Sjis(s[i],s[i+1]);
        inc(i);
      end else
        Result := Result + s[i];

      inc(i);
    end;
  end;

編集    削除
monaa  2008-10-18 13:02:19  No: 32257  IP: [192.*.*.*]

大量テキスト操作で時間がかかるのは変換ではなく文字のつなぎ合わせです。
str := str + strA;
一回一回この操作をするたびにメモリー領域の再確保が行われているめです。
あらかじめ必要とするメモリーを一度に確保してしまえばびっくりするほど速くなります。

編集    削除
ガリレオ  2008-10-19 01:22:52  No: 32258  IP: [192.*.*.*]

monaaさん、回答ありがとうございます。
ところで、メモリを一度に確保する、とは具体的にどうすればいいのですか?

編集    削除
Mr.XRAY  2008-10-20 05:06:12  No: 32259  IP: [192.*.*.*]

Mr.XRAYです.

>大きい容量のファイルでも2〜3秒

その20MBぐらいのですか? やっばりWindows 95のマシンででしょうか?  (笑)

文字列の操作を高速にする解決方法ではありませんが,文字コードの変換
でしたら,以下の様な方法もあります.参考までに.
テストした結果,JIS --> Shift-JIS のテキストの変換が,
  60MBの場合  約5.3秒
  30MBの場合  約2.6秒
でした.テスト環境は,ページ上部の記載の通りです.
マシンのスペックは秘密です.

http://mrxray.on.coocan.jp/Delphi/plSamples/886_ChangeCodePage.htm

編集    削除
monaa  2008-10-20 07:15:45  No: 32260  IP: [192.*.*.*]

メモリを一度に確保するってのは
var str:String;
SetLength(str,目的のサイズ);
とか、TMemoryStream.size := 目的のサイズ;
とか、array [0..目的のサイズ] of Byte
の事です。
Mr.XRAYさんのソースもMSのDLL参照ですが、
ほぼ確実にメモリは一度に確保していると想像できます。
上のソースを元にサンプルでもつくってあげたいところですが、
元気がないのでパスします。

編集    削除
ガリレオ  2008-10-20 09:01:36  No: 32261  IP: [192.*.*.*]

monaaさん、できましたら簡単でもよろしいので、サンプルを作ってもらえませんか?

編集    削除
monaa  2008-10-21 03:43:32  No: 32262  IP: [192.*.*.*]

こんな感じです。
速度と可読性を重視して書きました。
もっと早くできると思いますが、
サンプルということなのでご了承ください。
あと、ネットを徘徊すればjis-sjis変換は結構見つけることができると思います。
今回はググッて最初にみつかった「とほほのwww入門」を参考にしました。
http://www.tohoho-web.com/wwwkanji.htm
実用レベルとは思いますが、
Mr.XRayさんの紹介するコードより高速かどうか検証はしていません。(多分この程度では勝てません。)

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
//漢字コードの変換
procedure ConvertISO_2022_JPtoShiftJIS(var c1,c2:byte);
begin
  if (c1 mod 2)=1 then
  begin
      c1 := ((c1 + 1) div 2) + $70;
      c2 := c2 + $1f;
  end else begin
      c1 := (c1 div 2) + $70;
      c2 := c2 + $7d;
  end;
  if (c1 >= $a0) then c1 := c1 + $40;
  if (c2 >= $7f) then c2 := c2 + 1;
end;

function ISO_2022_JPtoShiftJIS(s:AnsiString; CRLF:Boolean):AnsiString;
type
  TJisStyle = (jsNone,jsASCII,jsRoman,jsKana,jsOldKanji,jsNewKanji,jsOpKanji);
var
  i,p,r,Len,skip:Integer;
  b1,b2: Byte;
  jStyle,jStyleConst:TJisStyle;
begin
{
ISO_2022_JP(7bit JIS)
                           00-1F,7F     制御コード
    1B 28 42 -  ESC ( B  - 20-7E        ASCII
    1B 28 4A -  ESC ( J  - 20-7E        JISローマ字
    1B 28 49 -  ESC ( I  - 21-5F        JISカナ(半角カナ)
    1B 24 40 -  ESC $ @  - 21-7E 21-7E  旧JIS漢字 (1978)
    1B 24 42 -  ESC $ B  - 21-7E 21-7E  新JIS漢字 (1983/1990)
    1B 24 44 -  ESC $ D  - 21-7E 21-7E  JIS補助漢字
SJIS
                           00-1F, 7F    制御コード
                           20-7E        JISローマ字(ASCII)
                           A1-DF        JISカナ(半角カナ)
                           81-9F 40-7E  JIS漢字
                           E0-EF 80-FC  JIS漢字

改行コードの扱いについて
  ISO_2022_JPの改行は $0A ですがTStringListで読み込むと $0D$0A に補完されます。
}
  p:=1;
  skip:=0;
  Len := Length(s);
  //文字バッファーを一度に確保
  //メモリーサイズ jis >= sjisだからjis分確保すれば問題ないはず。
  //改行コードを変換を入れるときは適当に大きめにする。
  //これは大きいほど処理が早くなるがその分無駄にメモリーを消費する場合がある。
  //あらかじめ改行をカウントすれば適切なサイズを得ることができるが、
  //今回は速度重視のため、文字走査を一回にするため敢えてカウントしていません。
  if CRLF then Len:= Round(Len*1.5);

  SetLength(Result,Len);
  jStyleConst := jsASCII;
  for i := 1 to Len do  //メインループ
  begin
    if skip = 0 then
    begin
      jStyle:=jsNone;
      //エスケープシーケンス判定
      if i < Len-3 then
      if (byte(s[i]) = $1B) then
      begin
        if (byte(s[i+1]) = $28) then
        begin
          if (byte(s[i+2]) = $42) then
            jStyle:=jsASCII  else
          if (byte(s[i+2]) = $4A) then
            jStyle:=jsRoman  else
          if (byte(s[i+2]) = $49) then
            jStyle:=jsKana;
        end else
        if (byte(s[i+1]) = $24) then
        begin
          if (byte(s[i+2]) = $40) then
            jStyle:=jsOldKanji  else
          if (byte(s[i+2]) = $42) then
            jStyle:=jsNewKanji  else
          if (byte(s[i+2]) = $44) then
            jStyle:=jsOpKanji;
        end;
      end;

      if jStyle <> jsNone then
      begin
        skip:=3;
        jStyleConst:=jStyle;
      end else
        jStyle := jStyleConst;

      //文字変換
      case jStyle of
        jsASCII,jsRoman
                  : begin
                      if (CRLF) and (byte(s[i+skip]) = $0A) then
                      begin
                        //改行コード
                        Result[p] := AnsiChar($0D);
                        Inc(p);
                      end;
                      Result[p] := s[i+skip];
                      Inc(p);
                    end;
        jsKana    : begin Result[p] := AnsiChar(byte(s[i+skip]) + $80); Inc(p) end;
        jsOldKanji,
        jsNewKanji,
        jsOpKanji
                  : begin
                      b1:= Byte(s[i+skip]);
                      b2:= Byte(s[i+skip+1]);
                      ConvertISO_2022_JPtoShiftJIS(b1,b2);
                      Result[p]:=AnsiChar(b1); Inc(p);
                      Result[p]:=AnsiChar(b2); Inc(p);
                      inc(skip);
                    end;
      end;

      //改行コード変換付きの場合バッファが足りなくなる事があるのでチェックを入れます。
      if (CRLF) and (p > Len-10) then
      begin
        Len:=Len + 10000;
        SetLength(Result,Len);
      end;

    end else begin
      Dec(skip);
    end;
  end;
  Result[p]:=#0;
  SetLength(Result,p);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  strList:TStringList;
  s:AnsiString;
  time:Cardinal;
begin
  //TStringListを用いて読み込み
  //改行を認識するため若干遅い
  strList:= TStringList.Create;
  strList.LoadFromFile('jis.txt');
  time:=GetTickCount;
  s:=ISO_2022_JPtoShiftJIS(strList.Text,False);
  Caption:=IntToStr(GetTickCount - time) + 'ms';
  //Memo1.Text:=s;  //大容量ファイルの場合表示は省略
  beep;
  strList.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  mStream:TMemoryStream;
  s:AnsiString;
  time:Cardinal;
begin
  //TMemoryStreamを用いて読み込み
  //TStringListより処理が簡素化しているため高速
  mStream:=TMemoryStream.Create;
  mStream.LoadFromFile('jis.txt');
  mStream.Position:=0;
  SetLength(s,mStream.Size);
  mStream.Read(s[1],mStream.Size);
  time:=GetTickCount;
  s:=ISO_2022_JPtoShiftJIS(s,True);
  Caption:=IntToStr(GetTickCount - time) + 'ms';
  //Memo1.Text:=s;  //大容量ファイルの場合表示は省略
  beep;
  mStream.Free;
end;

end.

編集    削除
Mr.XRAY  2008-10-21 04:49:32  No: 32263  IP: [192.*.*.*]

>Mr.XRayさんの紹介するコードより高速かどうか検証はしていません。(多分この程度では勝てません。)

やつてみました.そんなことないですよ.
mlang.dllを利用する場合のオーバヘッドがありますからね.
以下測定結果です.使用したファイルは前と同じです.
どちらもプログラムを起動した一回目の測定結果で,結果の保存時間も含みます.

Button1の結果  2.261秒
Button2の結果  2.099秒

編集    削除
Mr.XRAY  2008-10-21 04:54:34  No: 32264  IP: [192.*.*.*]

>使用したファイルは前と同じです.

30MBのほうです.

編集    削除
monaa  2008-10-21 05:05:25  No: 32265  IP: [192.*.*.*]

Mr.XRAYさん、わざわざ検証ありがとうございます。
意外と戦えたのには驚きです。
もうちょっと磨きをかければそこそこのレベルまで行きそうですね。
さっきのソースですが、今読み返したらLenが改行付の場合余計にループしていたので、そこだけ修正させてください。
あと、速度をもっと上げるなら余計な判定を削ったり、ConvertISO_2022_JPtoShiftJISのアセンブル化をしたりできそうです。
アセンブル化は私の守備範囲ではないですが。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}
//漢字コードの変換
procedure ConvertISO_2022_JPtoShiftJIS(var c1,c2:byte);
begin
  if (c1 mod 2)=1 then
  begin
      c1 := ((c1 + 1) div 2) + $70;
      c2 := c2 + $1f;
  end else begin
      c1 := (c1 div 2) + $70;
      c2 := c2 + $7d;
  end;
  if (c1 >= $a0) then c1 := c1 + $40;
  if (c2 >= $7f) then c2 := c2 + 1;
end;

function ISO_2022_JPtoShiftJIS(s:AnsiString; CRLF:Boolean):AnsiString;
type
  TJisStyle = (jsNone,jsASCII,jsRoman,jsKana,jsOldKanji,jsNewKanji,jsOpKanji);
var
  i,p,r,Len,Len2,skip:Integer;
  b1,b2: Byte;
  jStyle,jStyleConst:TJisStyle;
begin
{
ISO_2022_JP(7bit JIS)
                           00-1F,7F     制御コード
    1B 28 42 -  ESC ( B  - 20-7E        ASCII
    1B 28 4A -  ESC ( J  - 20-7E        JISローマ字
    1B 28 49 -  ESC ( I  - 21-5F        JISカナ(半角カナ)
    1B 24 40 -  ESC $ @  - 21-7E 21-7E  旧JIS漢字 (1978)
    1B 24 42 -  ESC $ B  - 21-7E 21-7E  新JIS漢字 (1983/1990)
    1B 24 44 -  ESC $ D  - 21-7E 21-7E  JIS補助漢字
SJIS
                           00-1F, 7F    制御コード
                           20-7E        JISローマ字(ASCII)
                           A1-DF        JISカナ(半角カナ)
                           81-9F 40-7E  JIS漢字
                           E0-EF 80-FC  JIS漢字

改行コードの扱いについて
  ISO_2022_JPの改行は $0A ですがTStringListで読み込むと $0D$0A に補完されます。
}
  p:=1;
  skip:=0;
  Len := Length(s);
  //文字バッファーを一度に確保
  //メモリーサイズ jis >= sjisだからjis分確保すれば問題ないはず。
  //改行コードを変換を入れるときは適当に大きめにする。
  //これは大きいほど処理が早くなるがその分無駄にメモリーを消費する場合がある。
  //あらかじめ改行をカウントすれば適切なサイズを得ることができるが、
  //今回は速度重視のため、文字走査を一回にするため敢えてカウントしていません。
  if CRLF then Len2:= Round(Len*1.5);

  SetLength(Result,Len2);
  jStyleConst := jsASCII;
  for i := 1 to Len do  //メインループ
  begin
    if skip = 0 then
    begin
      jStyle:=jsNone;
      //エスケープシーケンス判定
      if i < Len-3 then
      if (byte(s[i]) = $1B) then
      begin
        if (byte(s[i+1]) = $28) then
        begin
          if (byte(s[i+2]) = $42) then
            jStyle:=jsASCII  else
          if (byte(s[i+2]) = $4A) then
            jStyle:=jsRoman  else
          if (byte(s[i+2]) = $49) then
            jStyle:=jsKana;
        end else
        if (byte(s[i+1]) = $24) then
        begin
          if (byte(s[i+2]) = $40) then
            jStyle:=jsOldKanji  else
          if (byte(s[i+2]) = $42) then
            jStyle:=jsNewKanji  else
          if (byte(s[i+2]) = $44) then
            jStyle:=jsOpKanji;
        end;
      end;

      if jStyle <> jsNone then
      begin
        skip:=3;
        jStyleConst:=jStyle;
      end else
        jStyle := jStyleConst;

      //文字変換
      case jStyle of
        jsASCII,jsRoman
                  : begin
                      if (CRLF) and (byte(s[i+skip]) = $0A) then
                      begin
                        //改行コード
                        Result[p] := AnsiChar($0D);
                        Inc(p);
                      end;
                      Result[p] := s[i+skip];
                      Inc(p);
                    end;
        jsKana    : begin Result[p] := AnsiChar(byte(s[i+skip]) + $80); Inc(p) end;
        jsOldKanji,
        jsNewKanji,
        jsOpKanji
                  : begin
                      b1:= Byte(s[i+skip]);
                      b2:= Byte(s[i+skip+1]);
                      ConvertISO_2022_JPtoShiftJIS(b1,b2);
                      Result[p]:=AnsiChar(b1); Inc(p);
                      Result[p]:=AnsiChar(b2); Inc(p);
                      inc(skip);
                    end;
      end;

      //改行コード変換付きの場合バッファが足りなくなる事があるのでチェックを入れます。
      if (CRLF) and (p > Len2-10) then
      begin
        Len2:=Len2 + 10000;
        SetLength(Result,Len2);
      end;

    end else begin
      Dec(skip);
    end;
  end;
  Result[p]:=#0;
  SetLength(Result,p);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  strList:TStringList;
  s:AnsiString;
  time:Cardinal;
begin
  //TStringListを用いて読み込み
  //改行を認識するため若干遅い
  strList:= TStringList.Create;
  strList.LoadFromFile('jis.txt');
  time:=GetTickCount;
  s:=ISO_2022_JPtoShiftJIS(strList.Text,False);
  Caption:=IntToStr(GetTickCount - time) + 'ms';
  //Memo1.Text:=s;  //大容量ファイルの場合表示は省略
  beep;
  strList.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  mStream:TMemoryStream;
  s:AnsiString;
  time:Cardinal;
begin
  //TMemoryStreamを用いて読み込み
  //TStringListより処理が簡素化しているため高速
  mStream:=TMemoryStream.Create;
  mStream.LoadFromFile('jis.txt');
  mStream.Position:=0;
  SetLength(s,mStream.Size);
  mStream.Read(s[1],mStream.Size);
  time:=GetTickCount;
  s:=ISO_2022_JPtoShiftJIS(s,True);
  Caption:=IntToStr(GetTickCount - time) + 'ms';
  //Memo1.Text:=s;  //大容量ファイルの場合表示は省略
  beep;
  mStream.Free;
end;

end.

編集    削除