这次“趣味编程”的目的是解析字符串,从一个指定模式的字符串中提取信息。对于目前这个问题,解决方案有很多种,例如直接拆分,使用正则表达式,或是如现在本文这般按照顺序解析。总结果上来说,这些做法都是可取的,不过现在我打算举出的做法是我认为最为“典型”也最有“学习”和“展现”价值的解决方案:基于状态机的顺序字符解析。也欢迎您对此其他的做法进行深入分析。

您可能需要重新阅读上一篇文章来回忆字符串解析的具体规则,起始归纳起来,它只有以下三点:

  1. 一个text由一至多个token group组成;一个token group由一至多个token组成。
  2. token group的token之间使用单条横线“-”进行分割;text的token group之间使用两条横线“--”进行分割。
  3. token可以使用单引号包裹,单引号包裹内的横线作为普通字符处理,单引号包裹内使用两个单引号表示单个单引号。

至于最终结果,便是将一个text字符串,拆分成一个token group列表:

static List<List<string>> Parse(string text) { ... }

在这里,我们使用List<string>来表示一个token group(即token列表)。自然,表现方式可以有所不同。例如您的Parse方法如果返回“列表的数组”、“数组的列表”或是“数组的数组”都是没有任何问题的。

下面的做法基于winter-cn在上一篇文章后面的回复,再加以简单的修改和注释后得到的结果。这个做法的思路和我在“出题”时已经准备的“参考答案”不谋而合,但是winter-cn的实现要比我的更为简单、因此我的代码就不拿出来献丑了,我们现在一起来欣赏高手的劳动成果。

winter-cn的做法,是将“解析”工作拆分为5种状态,每种状态对应一种解析逻辑,而每种解析逻辑除了处理当前字符(改变一些公共状态)以外,还会返回处理下一个字符所使用的“解析逻辑”——这就是状态的迁移。winter-cn原有的做法是使用Func<char, object>来表示解析逻辑的类型,这样在每次得到新状态之后,还需要将其转化为Func<char, object>。不过为了更清晰地表达这样一种逻辑,我们也可以定义一个返回自身类型的“递归”的委托类型:

delegate StateParser StateParser(char ch);

在现在的实现中,我们把它解析过程分解为5个状态,分别对应不同“时刻”下的解析逻辑:

static List<List<string>> Parse(string text)
{StateParser p1 = null; // 用于解析token的起始字符StateParser p2 = null; // 用于解析作为分隔符的“-”的下一个字符StateParser p3 = null; // 用于解析token中或结尾的单引号的下一个字符StateParser p4 = null; // 用于解析单引号外的token字符StateParser p5 = null; // 用于解析单引号内的token字符var currentToken = new StringBuilder(); // 用于构建当前的token(即收集字符)var currentTokenGroup = new List<string>(); // 用于构建当前的token group(即收集token)var result = new List<List<string>>(); // 用于保存结果(即收集token group)...return result;
}

p1至p5便是所谓的“状态”,也就是“解析逻辑”,它们都会操作currentToken,currentTokenGroup和result三个数据,并返回下一个状态。状态的划分并非只有一种,不同的状态划分方式会形成不同的逻辑。我们接下来便要根据这样的划分方式,为每个状态指定实现了。在实现的过程中,我们需要时刻遵守“当前”状态的逻辑细节,以及其他状态的职责,这样实现状态的迁移似乎也并不是一件困难的事情。

首先是p1,它的作用是解析token的第一个字符:

// 解析token的起始字符
p1 = ch =>
{if (ch == '-'){// 如果token中需要包含单引号或“-”,// 那么这个token在表示的时候一定需要用一对单引号包裹起来throw new ArgumentException();}if (ch == '\''){// 如果起始字符是单引号,// 则开始解析单引号内的token字符return p5;}else{// 如果是普通字符,则作为当前token的字符,// 并开始解析单引号外的token字符currentToken.Append(ch);return p4;}};

接着是p2:它的作用是解析分隔符“-”(不包括单引号包裹内的“-”)后的下一个字符:

// 解析作为分隔符的“-”的下一个字符
p2 = ch =>
{if (ch == '-'){// 如果当前字符为“-”,说明一个token group结束了(因为前一个字符也是“-”),// 则将当前的token group加入结果集,并且准备新的token groupresult.Add(currentTokenGroup);currentTokenGroup = new List<string>();return p1;}else if (ch == '\''){// 如果当前字符为单引号,则说明新的token以单引号包裹// 则开始解析单引号内的token字符return p5;}else{// 如果是普通字符,则算作当前token的字符,// 并继续解析单引号外的token字符currentToken.Append(ch);return p4;}
};

接着是p3:解析token内部或结尾的单引号的下一个字符:

// 解析token内部或结尾的单引号的下一个字符
p3 = ch =>
{if (ch == '\''){// 如果当前字符为单引号,则说明连续两个单引号,// 所以表明token中出现了“单个”单引号,并且当前token一定包裹在单引号内,// 因此继续解析单引号内的token字符currentToken.Append('\'');return p5;}else if (ch == '-'){// 如果当前字符为一个分隔符,则说明上一个token已经结束了// 于是将当前token加入当前token group,准备新的token,// 并解析分隔符后的下一个字符currentTokenGroup.Add(currentToken.ToString());currentToken = new StringBuilder();return p2;}else{// 单引号后面只可能是另一个单引号或者一个分隔符,// 否则说明输入错误,则抛出异常throw new ArgumentException();}
};

最后则是p4和p5,分别用于处理普通的token以及被单引号包裹的token字符:

// 用于解析单引号外的token字符,
// 即一个没有特殊字符(分隔符或单引号)的token
p4 = ch =>
{if (ch == '\''){// 如果token中出现了单引号,则抛出异常throw new ArgumentException();}if (ch == '-'){// 如果出现了分隔符,则表明当前token结束了,// 于是将当前token加入当前token group,准备新的token,// 并解析分隔符的下一个字符currentTokenGroup.Add(currentToken.ToString());currentToken = new StringBuilder();return p2;}else{// 对于其他字符,则当作token中的普通字符处理// 继续解析单引号外的token字符currentToken.Append(ch);return p4;}
};// 用于解析单引号内的token字符
p5 = ch =>
{if (ch == '\''){// 对于被单引号包裹的token内的第一个单引号,// 需要解析其下一个字符,才能得到其真实含义return p3;}else{// 对于普通字符,则添加到当前token内currentToken.Append(ch);return p5;}
};

这些状态中的逻辑都有一个特点,它们都会通过C#编译器形成的闭包来操作“外部”状态——不过这个“外部”是指这些匿名函数的外部,但是它们统统属于Parse方法本身!这意味着,虽然我们的状态并非“纯函数”,但是Parse方法是没有任何副作用(Side Effect,即除了返回值外不会影响其他外部状态,以及相同的返回值永远相同的结果)。这个特点确保了Parse方法可以被任意多个线程同时调用。winter-cn的做法巧妙地使用了C#编译器的特性,简化了Parse方法的实现。

在定义完5种状态之后,我们便要从p1开始依次处理字符串中的每个字符,并且随着状态的迁移改变处理每个字符的逻辑。当然,最后的“收尾”工作也是必不可少的:

static List<List<string>> Parse(string text)
{...text.Aggregate(p1, (sp, ch) => sp(ch));currentTokenGroup.Add(currentToken.ToString());result.Add(currentTokenGroup);return result;
}

可以看出,这种做法的优势之一是完全的“顺序处理”,只遍历一次。如果您使用字符串的分割或者正则表达式进行解析的话,一般总是会有“回溯”,以及拆分出更多的字符串。因此,根据推测,这个做法从性能上来讲应该也有一定优势,不过还是需要真实的性能比较才能得出确切的结果。本文全部代码已经存放在http://gist.github.com/214427中,您可以复制、执行,调试等等。

这次的“趣味编程”是到目前为止最为热闹的一次,在上一篇文章的回复里您还可以发现许多朋友给出的大量解决方案,不过由于时间精力有限,我无法一一浏览了。此外,由于winter-cn已经给出了与我思路接近但实现更好的做法,后来我又用F#实现了另外一个思路不同的版本,您会发现F#有一些语言特性似乎非常适合进行字符串解析工作,它对于我们编写C#代码也有一定的借鉴意义。

from: http://blog.zhaojie.me/2009/10/code-for-fun-tokenizer-answer-1.html

趣味编程:从字符串中提取信息(参考答案 - 上)相关推荐

  1. 趣味编程:从字符串中提取信息(参考答案 - 下)

    昨天我们观察了如何使用基于状态机的顺序解析方式来提取字符串中的信息,不过由于winter-cn的做法和我原始的想法不谋而合,但实现的更为清晰,因此我在不献丑的同时,又设法使用另外一种方式来解决这个问题 ...

  2. 趣味编程:从字符串中提取信息

    字符串解析是程序员工作中非常重要的一部分,也是非常考验编程能力的工作.基本上我在面试程序员的时候,一定会出一道编程题目作为考察的一方面,而这道题目有很大的可能性是做字符串的解析.例如,给出一个模式规则 ...

  3. java数字编程提,java从字符串中提取数字的简单实例

    随便给你一个含有数字的字符串,比如: String s="eert343dfg56756dtry66fggg89dfgf"; 那我们怎么把其中的数字提取出来呢?大致有以下几种方法, ...

  4. [编程题] 扫描透镜(本题还涉及如何从字符串中提取数字)

    在N*M的草地上,提莫种了K个蘑菇,蘑菇爆炸的威力极大,兰博不想贸然去闯,而且蘑菇是隐形的.只 有一种叫做扫描透镜的物品可以扫描出隐形的蘑菇,于是他回了一趟战争学院,买了2个扫描透镜,一个 扫描透镜可 ...

  5. 从html中提取手机号码,C#从字符串中提取电话号码、手机号码

    C#程序目的:从一堆字符串中提取电话号码.手机号码,要求字符串中的号码以非数字字符分割.原理:利用正则表达式提取纯数字字符串数组,然后利用长度等号码特征,筛选过滤. 代码: //思路仅供参考 //nu ...

  6. android字符串获取数字索引,从字符串中提取特定数据(Extract specific data from a string)...

    从字符串中提取特定数据(Extract specific data from a string) 我有一个带有描述的长字符串. 我想从字符串中提取一些信息. 但我无法弄明白该怎么做. 这是字符串: C ...

  7. python 从字符串中提取数字 re.findall()

    以前老用(.*?)提取数字,今天发现不对了,比如一行数字为: 0 0.248438 0.255556 0.128125 0.194444 用: re.findall('(.*?) (.*?) (.*? ...

  8. 从字母数字字符串中提取数字

    http://office.microsoft.com/zh-cn/excel-help/HA001154901.aspx 本文的作者是 Ashish Mathur,是一位 Microsoft MVP ...

  9. python关键词提取_如何从Python格式字符串中提取关键字? - python

    我想在API中提供自动字符串格式,例如: my_api("path/to/{self.category}/{self.name}", ...) 可以替换为格式化字符串中标注的属性值 ...

最新文章

  1. CNN卷积神经网络的三种基本模式(不懂的话还得多努力啊!)
  2. 两岸MVP强强联手--最硬Windows Server 2008达人
  3. Android学习笔记(5)----启动 Theme.Dialog 主题的Activity时程序崩溃的解决办法
  4. C# xml文件的创建,修改和添加节点 。
  5. Oracle sqlldr
  6. 如何设置MySQL的环境变量
  7. python中codecs_Python:如何使用codecs模块将unicode数据保存成gbk格式
  8. 基于FPGA实现Aurora高速串行接口
  9. OpenShift 4 之Service Mesh教程(5)- 断路器Circuit Breaker
  10. 为什么在python中整数的值没有限制_为什么在Python中整数是不可变的?
  11. 复用管脚_如何实现UART的分时复用
  12. 植物病害分类的深度可解释体系结构(github源码)
  13. 转载:AD的授权还原和主还原:深入浅出Active Directory系列(六)
  14. RouteOS 频繁自启
  15. bzoj3625:[Codeforces Round #250]小朋友和二叉树
  16. 论网络工程中,系统开发设计可行性研究及市面产品对比!
  17. 难得一见的数据库事务异常 Deadlock found when trying to get lock解决办法dao.DeadlockLoserDataAccessException怎么办
  18. 设计一个长方形的类,成员的变量有长与宽,成员函数要求周长与面积,然后进行测试。
  19. KVM远程迁移启动报错
  20. python编程工具-7款Python开发工具介绍,你最中意哪一款

热门文章

  1. 李开复:数位革命——创新创业的黄金时代
  2. 小工匠聊架构-超高并发秒杀系统设计 07_Plan B 的设计
  3. Spring5源码 - 06 Spring Bean 生命周期流程 概述 01
  4. Algorithms_算法专项_Bitmap原理及应用
  5. 并发编程-03线程安全性之原子性(Atomic包)及原理分析
  6. extends thread java_java学习之- 线程继承Thread类
  7. php如何转换类型,PHP数据类型转换
  8. 运行pyspider时出现 : ImportError: cannot import name ‘ContextVar‘
  9. 在Android中实现监听 返回键,主键,菜单键
  10. mysql带c的命令_mysql命令整理