文字列の操作(その2)


redball.gif目次へ戻る

ここでは APIVCL第5章 文字列の操作 に続いて、筆者が日記の自動更新用のプログラムを作る際に必要になった HTML のタグなどを扱うための文字列操作のための関数などを記録しておく。順不同、脈絡なく挙げていき、また、随時追加してゆくことにする。なお、以下の関数では前回作った SearchStrUnit.pas を使うものがある。そのときは明示的に uses に示す。便宜のためにリンクを以下に示しておく。

SearchStrUnit.txt

それでは始めよう。

[特定の文字列で囲まれた部分文字列をとりだす]

List 1

function ExtractStringBetween(const Str,Pre,Post:string):string;
var
  PrePos,PostPos:integer;
begin
  result := 'Error';
  PrePos := AnsiPos(Pre,Str);
  if PrePos = 0 then exit;
  PostPos := AnsiPos(Post,Copy(Str,PrePos+Length(Pre),Length(Str)));
  if PostPos = 0 then exit;
  result := Copy(Str,PrePos+Length(Pre),PostPos-1);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
begin
  s := ExtractStringBetween(Memo1.Lines[0],'<B>','</B>');
  Memo2.Lines.Add(s);

  s := ExtractStringBetween(Memo1.Lines[1],'ここから','ここまで');
  Memo2.Lines.Add(s);

  s := ExtractStringBetween(Memo1.Lines[2],'$$$','$$$');
  Memo2.Lines.Add(s);
end;
n02101.gif

実行して Button1 を押すと右図のようになる。



[特定の文字列で囲まれた部分文字列を削除する]

List 2

function DeleteStringBetween(const Str,Pre,Post:string):string;
var
  PrePos,PostPos:integer;
begin
  result := 'Error';
  PrePos := AnsiPos(Pre,Str);
  if PrePos = 0 then exit;
  PostPos := AnsiPos(Post,Copy(Str,PrePos+Length(Pre),Length(Str)));
  if PostPos = 0 then exit;
  result := Copy(Str,1,PrePos+Length(Pre)-1)+
            Copy(Str,PrePos+Length(Pre)+PostPos-1,Length(Str));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
begin
  s := DeleteStringBetween(Memo1.Lines[0],'<B>','</B>');
  Memo2.Lines.Add(s);

  s := DeleteStringBetween(Memo1.Lines[1],'ここから','ここまで');
  Memo2.Lines.Add(s);

  s := DeleteStringBetween(Memo1.Lines[2],'$$$','$$$');
  Memo2.Lines.Add(s);
end;
n02102.gif

今度はまえのものとは反対に取り出すのではなく削除する。タグに挟まれた部分の削除に相当する。



[特定の文字列で囲まれた部分文字列を囲み文字列ごと削除する]

List 3

function DeleteStringBetweenWith(const Str,Pre,Post:string):string;
var
  PrePos,PostPos:integer;
begin
  result := 'Error';
  PrePos := AnsiPos(Pre,Str);
  if PrePos = 0 then exit;
  PostPos := AnsiPos(Post,Copy(Str,PrePos+Length(Pre),Length(Str)));
  if PostPos = 0 then exit;
  result := Copy(Str,1,PrePos-1)+
            Copy(Str,PrePos+Length(Pre)+PostPos+Length(Post)-1,Length(Str));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
begin
  s := DeleteStringBetweenWith(Memo1.Lines[0],'<B>','</B>');
  Memo2.Lines.Add(s);

  s := DeleteStringBetweenWith(Memo1.Lines[1],'ここから','ここまで');
  Memo2.Lines.Add(s);

  s := DeleteStringBetweenWith(Memo1.Lines[2],'$$$','$$$');
  Memo2.Lines.Add(s);
end;
n02103.gif

こんどは囲んでいる文字列もいっしょに削除する。特定のタグで挟まれた部分をタグごと削除できる。



[特定の文字列で囲まれた部分に文字列を挿入する]

List 4

function InsertStringBetween(const Str,Pre,Post,InsertStr:string):string;
var
  PrePos,InsertPos:integer;
begin
  result := Str;
  PrePos := AnsiPos(Pre+Post,Str);
  if PrePos = 0 then exit;
  InsertPos := PrePos+Length(Pre);
  Insert(InsertStr,result,InsertPos);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
begin
  s := InsertStringBetween(Memo1.Lines[0],'<B>','</B>','タグで囲まれた文字列');
  Memo2.Lines.Add(s);

  s := InsertStringBetween(Memo1.Lines[1],'ここから','ここまで','日本語もOK');
  Memo2.Lines.Add(s);

  s := InsertStringBetween(Memo1.Lines[2],'$$$','$$$','pre と post が同じでもOK');
  Memo2.Lines.Add(s);
end;
n02104.gif

これは最初の関数の逆の操作である。



[特定の文字列で囲まれた部分の文字列を入れ換える]

List 5

function ExchangeStringBetween(const Str,Pre,Post,ExchangeStr:string):string;
var
  ret:string;
begin
  result := 'Error';
  ret := DeleteStringBetween(Str,Pre,Post);
  if ret = 'Error' then exit;
  result := InsertStringBetween(ret,Pre,Post,ExchangeStr);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
begin
  s := ExchangeStringBetween(Memo1.Lines[0],'<B>','</B>','入れ換えた文字列');
  Memo2.Lines.Add(s);

  s := ExchangeStringBetween(Memo1.Lines[1],'ここから','ここまで','入れ換えた文字列');
  Memo2.Lines.Add(s);

  s := ExchangeStringBetween(Memo1.Lines[2],'$$$','$$$','入れ換えた文字列');
  Memo2.Lines.Add(s);
end;
n02105.gif

これは、上でつくった関数を二つ組み合わせたものである。



[HTML のタグの削除]

List 6

uses
  SearchStrUnit;

function StripTag(const Str:string):string;
var
  Start,Stop:TIntArray;
  Num,i:integer;
begin
  result := Str;
  Num := SearchPos(Str,'<',Start);
  if Num = 0 then exit;
  if SearchPos(Str,'>',Stop) <> Num then exit;
  for i := Num downto 1 do
    Delete(result,Start[i]+1,Stop[i]-Start[i]+1);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo2.Text := StripTag(Memo1.Text);
end;
n02106.gif

これは SearchPos() を二回つかってタグの範囲を調べ、後ろの方から削除している。



[任意のデリミタによる文字列の分離]

List 7

function IsDelimiter(const Delimiters,c:string):Boolean;
begin
  result := StrScan(PChar(Delimiters),c[1]) <> nil;
end;

function Split(const Str,Delimiters:string; SL:TStrings):integer;
var
  ss:WideString;
  i,St:integer;
begin
  SL.Clear;
  result := 0;
  if Length(Str) < 1 then exit;
  ss := Str;
  St := -1;
  for i := 1 to Length(ss) do
    if IsDelimiter(Delimiters,ss[i]) then begin
      if St <> -1 then begin
        SL.Add(Copy(ss,St,i-St));
        St := -1;
      end
    end else
      if St = -1 then St := i;

  if St <> -1 then SL.Add(Copy(ss,St,Length(Str)));
  result := SL.Count;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Label1.Caption := IntToStr(Split(Edit1.Text,Edit2.Text,Memo1.Lines));
end;
n02107.gif

デリミタには、任意の一バイト文字を複数指定できる。論理上、複数のデリミタが連続しているときは一つのデリミタとみなす。ここでは、半角空白をデリミタに加えているので、文字列の前後の空白はデリミタと見なされている。半角空白文字をデータ文字列の一部として取得したいときは、デリミタに加えず、取得した文字列の前後の空白文字は、VCLTrim() を使って取り除くことができる。この関数は、分離した文字列の数を返す。



[ Split の応用 : 文字列の並べ替えと形式化]

List 8

function MyFormatDateTime(const Source:string):string;
var
  SL:TStringList;
  Num:integer;
begin
  result := 'Error';
  SL := TStringList.Create;

  Num := Split(Source,': /',SL);
  if Num < 5 then begin
    SL.Free;
    exit;
  end;
  result := Format('%4:s年%3:s月%2:s日 %0:s時%1:s分',
                   [SL[0],SL[1],SL[2],SL[3],SL[4]]);
  SL.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Edit2.Text := MyFormatDateTime(Edit1.Text);
end;
n02108.gif

TDateTime 型の日時が得られるときは VCLFormatDateTime() を使って任意の形式化ができるが、すでに文字列となっている場合は、Split で分解して、組み立て直すことができる。上はその一例である。



[ Split の応用 : CSV ファイルの読み込み]

List 9

procedure TrimAllLines(SL:TStrings);
var
  i:integer;
begin
  if SL.Count < 1 then exit;
  for i := 0 to SL.Count-1 do
    SL[i] := Trim(SL[i]);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  SL:TStringList;
  i,j,Num:integer;
begin
  SL := TStringList.Create;
  try
    for i := 0 to Memo1.Lines.Count-1 do begin
      Num := Split(Memo1.Lines[i],Edit1.Text,SL);
      TrimAllLines(SL);
      for j := 0 to Num-1 do
        StringGrid1.Cells[j,i] := SL[j]
    end;
  finally
    SL.Free;
  end;
end;
n02109.gif

任意の一バイト文字で区切られた文字列データを読み込むのに Split() を使った例を示した。ここではデリミタに半角空白を指定していないので、半角空白文字だけのデータは空文字データとして生き残る。Trim() をつかって文字列の前後の空白文字は取り除いている。



[ デリミタで区切られた文字列のn番目を取り出す ]

List 10

function IsDelimiter(const Delimiters,c:string):Boolean;
begin
  result := StrScan(PChar(Delimiters),c[1]) <> nil;
end;

function GetToken(const Str,Delimiters:string; indx:integer):string;
var
  ss:WideString;
  i,j,St:integer;
begin
  result := 'Error';
  if Length(Str) < 1 then exit;
  ss := Str;
  St := -1;
  j := 0;
  for i := 1 to Length(ss) do
    if IsDelimiter(Delimiters,ss[i]) then begin
      if St <> -1 then begin
        Inc(j);
        if j = indx then begin
          result := Copy(ss,St,i-St);
          exit;
        end;
        St := -1;
      end
    end else
      if St = -1 then St := i;

  if St <> -1 then begin
    Inc(j);
    if j = indx then result := Copy(ss,St,Length(Str));
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Edit2.Text := GetToken(Edit1.Text,'/ |',2);
end;
n02110.gif

これは、Split() と同じ論理で分離したものの最初(indx = 1)から indx 番目の文字列を返す。



以上、とりあえずここまでとするが、以降は随時追加の「予定」です。

redball.gif目次へ戻る