有没有一种简单的方法可以按照特定规则从字符串中提取数字?

Is there a simplistic way to extract numbers from a string following certain rules?

我需要从字符串中提取数字并将它们放入列表中,但是这有一些规则,例如识别提取的数字是整数还是浮点数。

这个任务听起来很简单,但随着时间的推移,我发现自己越来越困惑,需要一些指导才能真正做到。


以下面的测试字符串为例:

There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.

解析字符串时遵循的规则如下:

示例 1

为了使这个更清楚一点,采用上面引用的测试字符串所需的输出应如下所示:

从上图中,浅蓝色表示浮点数,淡红色表示单个整数(但还要注意连接在一起的浮点数如何拆分为单独的浮点数)。

  • 45.826 (Float)
  • 53.91 (Float)
  • 7 (Integer)
  • 5 (Integer)
  • 66 . (Float)
  • 4 (Integer)
  • 5.40 (Float)
  • 3 . (Float)

注意 66 之间有意留出空格。和 3。以上是由于数字的格式化方式。

示例 2:

Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9

  • 4 (Integer)
  • 8.1 (Float)
  • 123.45 (Float)
  • 67.8 (Float)
  • 9 (Integer)

为了提供更好的想法,我在测试时创建了一个新项目,如下所示:


现在进入实际任务。我想也许我可以从字符串中读取每个字符并根据上述规则识别有效数字,然后将它们拉入列表。

以我的能力,这是我能做到的最好的:

代码如下:

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    btnParseString: TButton;
    edtTestString: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    lstDesiredOutput: TListBox;
    lstActualOutput: TListBox;
    procedure btnParseStringClick(Sender: TObject);
  private
    FDone: Boolean;
    FIdx: Integer;
    procedure ParseString(const Str: string; var OutValue, OutKind: string);
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.ParseString(const Str: string; var OutValue, OutKind: string);
var
  CH1, CH2: Char;
begin
  Inc(FIdx);
  CH1 := Str[FIdx];

  case CH1 of
    '0'..'9': // Found a number
    begin
      CH2 := Str[FIdx - 1];
      if not (CH2 in ['A'..'Z']) then
      begin
        OutKind := 'Integer';

        // Try to determine float...

        //while (CH1 in ['0'..'9', '.']) do
        //begin
        //  case Str[FIdx] of
        //    '.':
        //    begin
        //      CH2 := Str[FIdx + 1];
        //      if not (CH2 in ['0'..'9']) then
        //      begin
        //        OutKind := 'Float';
        //        //Inc(FIdx);
        //      end;
        //    end;
        //  end;
        //end;
      end;
      OutValue := Str[FIdx];
    end;
  end;

  FDone := FIdx = Length(Str);
end;

procedure TForm1.btnParseStringClick(Sender: TObject);
var
  S, SKind: string;
begin
  lstActualOutput.Items.Clear;
  FDone := False;
  FIdx := 0;

  repeat
    ParseString(edtTestString.Text, S, SKind);
    if (S <> '') and (SKind <> '') then
    begin
      lstActualOutput.Items.Add(S + ' (' + SKind + ')');
    end;
  until
    FDone = True;
end;

end.

它显然没有提供所需的输出(失败的代码已被注释掉)并且我的方法可能是错误的,但我觉得我只需要在这里和那里进行一些更改以获得可行的解决方案。

在这一点上,尽管认为答案非常接近,但我发现自己相当困惑和迷茫,任务变得越来越令人恼火,我真的很感激一些帮助。


编辑 1

这里我靠得更近了一点,因为不再有重复的数字,但结果仍然明显错误。

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    btnParseString: TButton;
    edtTestString: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    lstDesiredOutput: TListBox;
    lstActualOutput: TListBox;
    procedure btnParseStringClick(Sender: TObject);
  private
    FDone: Boolean;
    FIdx: Integer;
    procedure ParseString(const Str: string; var OutValue, OutKind: string);
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

// Prepare to pull hair out!
procedure TForm1.ParseString(const Str: string; var OutValue, OutKind: string);
var
  CH1, CH2: Char;
begin
  Inc(FIdx);
  CH1 := Str[FIdx];

  case CH1 of
    '0'..'9': // Found the start of a new number
    begin
      CH1 := Str[FIdx];

      // make sure previous character is not a letter
      CH2 := Str[FIdx - 1];
      if not (CH2 in ['A'..'Z']) then
      begin
        OutKind := 'Integer';

        // Try to determine float...
        //while (CH1 in ['0'..'9', '.']) do
        //begin
        //  OutKind := 'Float';
        //  case Str[FIdx] of
        //    '.':
        //    begin
        //      CH2 := Str[FIdx + 1];
        //      if not (CH2 in ['0'..'9']) then
        //      begin
        //        OutKind := 'Float';
        //        Break;
        //      end;
        //    end;
        //  end;
        //  Inc(FIdx);
        //  CH1 := Str[FIdx];
        //end;
      end;
      OutValue := Str[FIdx];
    end;
  end;

  OutValue := Str[FIdx];
  FDone := Str[FIdx] = #0;
end;

procedure TForm1.btnParseStringClick(Sender: TObject);
var
  S, SKind: string;
begin
  lstActualOutput.Items.Clear;
  FDone := False;
  FIdx := 0;

  repeat
    ParseString(edtTestString.Text, S, SKind);
    if (S <> '') and (SKind <> '') then
    begin
      lstActualOutput.Items.Add(S + ' (' + SKind + ')');
    end;
  until
    FDone = True;
end;

end.

我的问题是如何从字符串中提取数字,将它们添加到列表中并确定该数字是整数还是浮点数?

左侧淡绿色列表框(期望输出)显示结果应该是什么,右侧淡蓝色列表框(实际输出)显示我们实际得到的结果。

请指教 谢谢

请注意,我重新添加了 Delphi 标签,因为我确实使用 XE7,所以请不要删除它,尽管这个特殊问题出现在 Lazarus 中,但我最终的解决方案应该适用于 XE7 和 Lazarus。

你的规则比较复杂,你可以尝试搭建有限状态机(FSM、DFA -Deterministic finite automaton)。

每个字符都会导致状态之间的转换。

例如,当您处于状态 "integer started" 并遇到 space 字符时,您产生整数值并且 FSM 进入状态“任何想要的”。

如果您处于状态 "integer started" 并遇到 '.',FSM 将进入状态 "float or integer list started",依此类推。

答案很接近,但有几个基本错误。给你一些提示(无需为你编写代码):在 while 循环中,你必须始终递增(递增不应该在它所在的位置,否则你会得到一个无限循环)并且你必须检查你是否没有到达结束字符串(否则你会得到一个异常),最后你的 while 循环不应该依赖于 CH1,因为它永远不会改变(再次导致无限循环)。但我最好的建议是使用调试器跟踪你的代码——这就是它的用途。那么你的错误就会变得显而易见。

这是一个使用正则表达式的解决方案。我在 Delphi 中实现了它(在 10.1 中测试过,但也应该与 XE8 一起使用),我相信你可以为 lazarus 采用它,只是不确定哪些正则表达式库在那里工作。 正则表达式模式使用交替将数字匹配为 integersfloats 遵循您的规则:

整数:

(\b\d+(?![.\d]))
  • 以单词边界开头(因此前面没有字母、数字或下划线 - 如果下划线是一个问题,您可以使用 (?<![[:alnum:]]) 代替)
  • 然后匹配一位或多位数字
  • 后面既没有数字也没有点

浮动:

(\b\d+(?:\.\d+)?)
  • 以单词边界开头(因此前面没有字母、数字或下划线 - 如果下划线是一个问题,您可以使用 (?<![[:alnum:]]) 代替)
  • 然后匹配一位或多位数字
  • 可选择匹配点后跟更多数字

一个简单的控制台应用程序看起来像

program Test;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, RegularExpressions;

procedure ParseString(const Input: string);
var
  Match: TMatch;
begin
  WriteLn('---start---');
  Match := TRegex.Match(Input, '(\b\d+(?![.\d]))|(\b\d+(?:\.\d+)?)');
  while Match.Success do
  begin
    if Match.Groups[1].Value <> '' then
      writeln(Match.Groups[1].Value + '(Integer)')
    else
      writeln(Match.Groups[2].Value + '(Float)');
    Match := Match.NextMatch;
  end;
  WriteLn('---end---');
end;

begin
  ParseString('There are test values: P7 45.826.53.91.7, .5, 66.. 4 and 5.40.3.');
  ParseString('Anoth3r Te5.t string .4 abc 8.1Q 123.45.67.8.9');
  ReadLn;
end.

你的代码中有很多基本错误,我决定改正你的作业。这仍然不是一个好方法,但至少消除了基本错误。注意看评论!

procedure TForm1.ParseString(const Str: string; var OutValue,
  OutKind: string);
//var
//  CH1, CH2: Char;      <<<<<<<<<<<<<<<< Don't need these
begin
  (*************************************************
   *                                               *
   * This only corrects the 'silly' errors. It is  *
   * NOT being passed off as GOOD code!            *
   *                                               *
   *************************************************)

  Inc(FIdx);
  // CH1 := Str[FIdx]; <<<<<<<<<<<<<<<<<< Not needed but OK to use. I removed them because they seemed to cause confusion...
  OutKind := 'None';
  OutValue := '';

  try
  case Str[FIdx] of
    '0'..'9': // Found the start of a new number
    begin
      // CH1 := Str[FIdx]; <<<<<<<<<<<<<<<<<<<< Not needed

      // make sure previous character is not a letter
      // >>>>>>>>>>> make sure we are not at beginning of file
      if FIdx > 1 then
      begin
        //CH2 := Str[FIdx - 1];
        if (Str[FIdx - 1] in ['A'..'Z', 'a'..'z']) then // <<<<< don't forget lower case!
        begin
          exit; // <<<<<<<<<<<<<<
        end;
      end;
      // else we have a digit and it is not preceeded by a number, so must be at least integer
      OutKind := 'Integer';

      // <<<<<<<<<<<<<<<<<<<<< WHAT WE HAVE SO FAR >>>>>>>>>>>>>>
      OutValue := Str[FIdx];
      // <<<<<<<<<<<<< Carry on...
      inc( FIdx );
      // Try to determine float...

      while (Fidx <= Length( Str )) and  (Str[ FIdx ] in ['0'..'9', '.']) do // <<<<< not not CH1!
      begin
        OutValue := Outvalue + Str[FIdx]; //<<<<<<<<<<<<<<<<<<<<<< Note you were storing just 1 char. EVER!
        //>>>>>>>>>>>>>>>>>>>>>>>>>  OutKind := 'Float';  ***** NO! *****
        case Str[FIdx] of
          '.':
          begin
            OutKind := 'Float';
            // now just copy any remaining integers - that is all rules ask for
            inc( FIdx );
            while (Fidx <= Length( Str )) and  (Str[ FIdx ] in ['0'..'9']) do // <<<<< note '.' excluded here!
            begin
              OutValue := Outvalue + Str[FIdx];
              inc( FIdx );
            end;
            exit;
          end;
            // >>>>>>>>>>>>>>>>>>> all the rest in unnecessary
            //CH2 := Str[FIdx + 1];
            //      if not (CH2 in ['0'..'9']) then
            //      begin
            //        OutKind := 'Float';
            //        Break;
            //      end;
            //    end;
            //  end;
            //  Inc(FIdx);
            //  CH1 := Str[FIdx];
            //end;

        end;
        inc( fIdx );
      end;

    end;
  end;

  // OutValue := Str[FIdx]; <<<<<<<<<<<<<<<<<<<<< NO! Only ever gives 1 char!
  // FDone := Str[FIdx] = #0; <<<<<<<<<<<<<<<<<<< NO! #0 does NOT terminate Delphi strings

  finally   // <<<<<<<<<<<<<<< Try.. finally clause added to make sure FDone is always evaluated.
            // <<<<<<<<<< Note there are better ways!
    if FIdx > Length( Str ) then
    begin
      FDone := TRUE;
    end;
  end;
end;

你的答案和评论建议使用状态机,我完全支持。从您在 Edit1 中显示的代码,我看到您仍然没有实现状态机。从评论中我猜你不知道该怎么做,所以要将你推向这个方向,这是一种方法:

定义您需要使用的状态:

type
  TReadState = (ReadingIdle, ReadingText, ReadingInt, ReadingFloat);
  // ReadingIdle, initial state or if no other state applies
  // ReadingText, needed to deal with strings that includes digits (P7..)
  // ReadingInt, state that collects the characters that form an integer
  // ReadingFloat, state that collects characters that form a float

然后定义状态机的框架。为了尽可能简单,我选择使用一种直接的程序方法,一个主要程序和四个子程序,每个状态一个。

procedure ParseString(const s: string; strings: TStrings);
var
  ix: integer;
  ch: Char;
  len: integer;
  str,           // to collect characters which form a value
  res: string;   // holds a final value if not empty
  State: TReadState;

  // subprocedures, one for each state
  procedure DoReadingIdle(ch: char; var str, res: string);
  procedure DoReadingText(ch: char; var str, res: string);
  procedure DoReadingInt(ch: char; var str, res: string);
  procedure DoReadingFloat(ch: char; var str, res: string);

begin
  State := ReadingIdle;
  len := Length(s);
  res := '';
  str := '';
  ix := 1;
  repeat
    ch := s[ix];
    case State of
      ReadingIdle:  DoReadingIdle(ch, str, res);
      ReadingText:  DoReadingText(ch, str, res);
      ReadingInt:   DoReadingInt(ch, str, res);
      ReadingFloat: DoReadingFloat(ch, str, res);
    end;
    if res <> '' then
    begin
      strings.Add(res);
      res := '';
    end;
    inc(ix);
  until ix > len;
  // if State is either ReadingInt or ReadingFloat, the input string
  // ended with a digit as final character of an integer, resp. float,
  // and we have a pending value to add to the list
  case State of
    ReadingInt: strings.Add(str + ' (integer)');
    ReadingFloat: strings.Add(str + ' (float)');
  end;
end;

那就是骨架。主要逻辑在四个状态过程中。

  procedure DoReadingIdle(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := ch;
        State := ReadingInt;
      end;
      ' ','.': begin
        str := '';
        // no state change
      end
      else begin
        str := ch;
        State := ReadingText;
      end;
    end;
  end;

  procedure DoReadingText(ch: char; var str, res: string);
  begin
    case ch of
      ' ','.': begin  // terminates ReadingText state
        str := '';
        State := ReadingIdle;
      end
      else begin
        str := str + ch;
        // no state change
      end;
    end;
  end;

  procedure DoReadingInt(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := str + ch;
      end;
      '.': begin  // ok, seems we are reading a float
        str := str + ch;
        State := ReadingFloat;  // change state
      end;
      ' ',',': begin // end of int reading, set res
        res := str + ' (integer)';
        str := '';
        State := ReadingIdle;
      end;
    end;
  end;

  procedure DoReadingFloat(ch: char; var str, res: string);
  begin
    case ch of
      '0'..'9': begin
        str := str + ch;
      end;
      ' ','.',',': begin  // end of float reading, set res
        res := str + ' (float)';
        str := '';
        State := ReadingIdle;
      end;
    end;
  end;

状态程序应该是自我解释的。不过有什么不清楚的就问吧。

您的两个测试字符串都会产生您指定的值。你的一个规则有点模棱两可,我的解释可能是错误的。

numbers cannot be preceeded by a letter

您提供的示例是 "P7",并且在您的代码中您只检查了前一个字符。但是如果它会读作 "P71" 呢?我解释为“1”应该像“7”一样被省略,即使“1”的前一个字符是“7”。这是 ReadingText 状态的主要原因,该状态仅在 space 或句点结束。