【问题标题】:Is there a simplistic way to extract numbers from a string following certain rules?有没有一种简单的方法可以按照某些规则从字符串中提取数字?
【发布时间】:2017-03-13 14:59:05
【问题描述】:

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

这项任务听起来很简单,但随着时间的推移,我发现自己越来越困惑,并且真的可以通过一些指导来完成。


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

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

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

  • 数字前面不能有字母。

  • 如果它找到一个数字并且不是后跟一个小数点,那么这个数字就是一个整数。

  • 如果它找到一个数字并且 后跟一个小数点,则该数字是一个浮点数,例如 5。

  • ~ 如果小数点后有更多数字,则该数字仍然是浮点数,例如 5.40

  • ~ 进一步找到的小数点应将数字分解,例如 5.40.3 变为 (5.40 Float) 和 (3 Float)

  • 如果是小数点后的字母,例如3.H,则仍将3. 作为浮点数添加到列表中(即使在技术上它无效)

    李>

示例 1

为了更清楚一点,在所需输出上方引用的测试字符串应如下所示:

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

  • 45.826(浮点数)
  • 53.91(浮点数)
  • 7(整数)
  • 5(整数)
  • 66。 (浮动)
  • 4(整数)
  • 5.40(浮点数)
  • 3。 (浮动)

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

示例 2:

Anoth3r Te5.t 字符串 .4 abc 8.1Q 123.45.67.8.9

  • 4(整数)
  • 8.1(浮动)
  • 123.45(浮点数)
  • 67.8(浮点数)
  • 9(整数)

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


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

就我的能力而言,这是我能做到的最好的:

代码如下:

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.

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

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

请指教谢谢。

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

【问题讨论】:

  • @DavidHeffernan 考虑到我花了很长时间写出我认为有效的问题(你真的不知道问题是什么?),这不是一个公平的假设,并且还展示了我的尽我所能的进步和努力。如果我希望有人为我做这一切,到目前为止,我不会为此付出太多努力,所以请不要只是假设我想要复制和粘贴答案,我只需要一些指导来帮助我,你只能从学习中发展成为一名程序员,而不是复制和粘贴,所以请不要以为我期望有人为我做这项工作。
  • 我很同情,很明显你付出了很多努力。
  • @DavidHeffernan 谢谢,我真的希望我能更好、更清楚地吸收和理解这些任务,因为它们解决的时间越长,就越难。这是我的建议,希望没有人发布解决方案,同时我会抽出一些时间来解决这个问题,收集我的想法并缓解压力和困惑,然后希望回来攻击它并尝试制作更多的进步,有限状态机看起来太先进了,所以我会尝试继续我目前的方式。祝我好运:)
  • 45.826.53.91.7 在哪个星球上解析出 45.826、53.91 和 7?您如何确定它不是 45、826.53 和 91.7,或 45、826、53.91 和 7?你从哪里得到这些随机噪声填充的数据?
  • @Craig 感谢您尝试为自己设置挑战以帮助您学习。但是,您可能需要先学习更重要的一课。大多数程序员都陷入了让事情变得过于复杂和困难的陷阱。你犯了这个错误。通过允许符号 . 执行双重任务作为小数点和项目分隔符,您将可能是一个有趣的解析练习变成了一个不切实际的问题,您不太可能从中学到任何有用的东西。最重要的一课:不要让事情过于复杂。

标签: delphi lazarus freepascal


【解决方案1】:

您的规则相当复杂,因此您可以尝试构建有限状态机(FSM,DFA -Deterministic finite automaton)。

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

例如,当您处于“整数开始”状态并遇到空格字符时,您会产生整数值并且 FSM 进入状态“任何想要的”。

如果您处于“整数开始”状态并遇到“.”,则 FSM 进入“浮点或整数列表开始”状态,依此类推。

【讨论】:

  • 状态机是要走的路。
  • 哇,如果涉及到这种事情,我似乎大大低估了这项任务。我想我可以简单地迭代字符串中的每个字符并挑选出有效的数字:)
  • 是的,您可以,但根据州的不同,您必须对字符进行不同的解释。正如 MBo 所描述的那样。
  • 在您的代码中,OutKind 已经(至少部分地)代表了您的状态,因此您已经在前往 FSM 的路上而没有意识到这一点。 FSM 形式化了这个想法,使代码比你的更清晰、更健壮。您可能需要更多中间状态,并且您倾向于分别为每个状态编写代码,以降低错误风险并在错误发生时隔离它们。但在你所处的位置上并没有那么远。所以不要绝望。
  • 我不记得名字了,但我使用了一些数学解析/公式引擎,我将它集成到我自己的脚本中。已经有很多东西可以比你的反复试验做得更好。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    您已经得到了建议使用状态机的答案和 cmets,我完全支持。从您在 Edit1 中显示的代码中,我看到您仍然没有实现状态机。从 cmets 我猜你不知道该怎么做,所以为了推动你朝这个方向发展,这里有一种方法:

    定义您需要处理的状态:

    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;
    

    状态程序应该是自我解释的。但是如果有什么不清楚的地方就问一下。

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

    数字前面不能有字母

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

    【讨论】:

    • 这么多答案和 cmets,我需要一段时间才能让它们全部融入。至于您基于“P71”的假设,那么是的,两个数字都将被忽略,因为字符串不是以一个数字。
    【解决方案4】:

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

    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;
    

    【讨论】:

    • 不必居高临下,这不是家庭作业。更仔细地阅读 cmets。
    【解决方案5】:

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

    整数:

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

    浮动:

    (\b\d+(?:\.\d+)?)
    
    • 以单词边界开头(因此之前没有字母、数字或下划线 - 如果下划线有问题,您可以改用 (?&lt;![[: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.
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多