FXParser 是一款用于 iOS 和 Mac OS 的文本解析引擎,旨在简化对基于文本的语言和数据格式的消费,例如 JSON。
FXParser 是一个解析组合器 - 一种通过组合多个简单解析器对象来构建复杂解析器对象的一类解析器。这种方法比传统的解析器简单得多,并且避免了需要一个单独的词法/分词阶段。
FXParser 在很大程度上受到了 Parcoa 解析器 (https://github.com/brotchie/Parcoa) 的影响,如果你也对解析引擎感兴趣,也应该查看。
FXParser 和 Parcoa 之间的主要区别是 FXParser 是使用正则表达式来定义个别解析器组件的,而不是基于字符的逐个处理。这大大减少了 FXParser 引擎自身以及各个解析器的语法定义的大小和复杂性。
注意:'支持' 的意思是库已经与这个版本进行了测试。'兼容' 的意思是库应该能在这个操作系统版本上运行(即它不依赖于任何不可用的 SDK 功能),但不再进行兼容性测试,可能需要调整或修复错误才能正确运行。
FXParser 需要 ARC。如果您希望在非 ARC 项目中使用 FXParser,只需将 -fobjc-arc 编译器标志添加到 FXParser.m 类中。为了做到这一点,请转到目标设置中的构建阶段标签,打开编译源文件组,双击列表中的 FXParser.m,并输入 -fobjc-arc 到弹出窗口中。
如果您希望将整个项目转换为 ARC,请注释掉 FXParser.m 中的 #error 行,然后运行 Xcode 中编辑 > 重构 > 转换为 Objective-C ARC... 工具,并确保所有您希望使用 ARC 的文件都已勾选(包括 FXParser.m)。
要使用 FXParser,只需将类文件拖放到您的项目中。FXParser 可以被继承,但通常不需要这样做 - 您可以创建实例并使用标准构造函数指定自定义规则。
要使用 FXParser,创建一个符合特定标准的 FXParser 实例,然后使用它来解析一个字符串,如下所示
//create a parser that matches the word 'dog'
FXParser *dog = [FXParser string:@"dog"];
//create a parser that matches the word 'cat'
FXParser *cat = [FXParser string:@"cat"];
//create a composite parser that matches either
FXParser *pet = [dog or:cat];
//parse a string
FXParserResult *result = [pet parse:@"cow"];
NSLog(@"%@", result); // will return failure because a cow is not a pet
更详细的示例请查看项目中的测试应用。
FXParser 包含两个类
FXParser - 该类表示一个解析实例。使用各种构造方法可以创建 FXParser 实例以匹配各种字符串,并将它们组合成更复杂的语法规则的组合解析器。
FXParserResult - 该类封装了解析操作的结果。它将返回成功或失败,以及解析结果和任何相关元数据。
FXParser 的方法可以分为三种类型,分别归入不同的类别以更清晰地展现
核心功能 - 这包括了所有标准的解析构造函数和解析字符串的方法。
组合器 - 这些方法用于组合解析器以构建更复杂的规则。
值转换器 - 这些方法用于处理解析器返回的结果,以便您可以使用特定格式创建输出。
这些是用于创建和应用解析器的基本函数。
+ (instancetype)parserWithBlock:(FXParserBlock)block description:(NSString *)description;
这是 FXParser 的指定构造函数。它创建一个解析器,其中一个块用于指定解析器试图匹配的准则,以及一个描述该块试图匹配内容的描述,这在解析失败时用于生成有意义的错误。FXParserBlock 块具有以下签名
FXParserResult *(^FXParserBlock)(NSString *input, NSRange range);
解析器块接收一个输入字符串和用于解析的范围,并返回一个指示成功或失败的 FXParserResult 对象。parserWithBlock 方法的用法相当复杂,通常您会使用其他更简单的构造方法来创建您的解析器,除非您需要非常定制的功能。
+ (instancetype)stringMatchingPredicate:(FXParserStringPredicate)predicate description:(NSString *)description;
此方法创建一个解析器,它会匹配指定的 FXParserStringPredicate 块。请注意,FXParserStringPredicate 与 NSPredicate 不完全相同,尽管它们遵循相同的原理。FXParserStringPredicate 块具有以下签名
NSRange (^FXParserStringPredicate)(NSString *input, NSRange range);
谓词块接收一个输入字符串和用于解析的范围,并返回一个 NSRange 对象,用于指示成功/失败和消耗的文本量。使用 NSRange 作为返回值可能看起来有些繁琐,但与内建的 NSString 匹配函数一起使用相当简单。对于成功匹配,range.location 应始终与提供给谓词的范围相匹配。如果它们相匹配,则假定匹配成功,即使长度为零(存在匹配零个字符的规则)。不成功匹配应返回一个 location 值为 NSNotFound 的范围(这是 NSString 的 rangeOfString: 方法的默认行为)。请注意,返回一个不以指定范围开头成功的匹配是不被接受的 - 如果您希望在谓词匹配规则中忽略前导空白,那是可以的,但请确保任何忽略的空白仍然包含在返回的范围值中。
+ (instancetype)string:(NSString *)string;
此方法创建一个解析器,它会匹配指定的字符串。该字符串可以是一个字符或整个句子。解析器需要字符串完全匹配,包括大小写和空白。如果您想要更灵活匹配,请使用正则表达式匹配器。
+ (instancetype)regexp:(NSString *)pattern;
此方法创建一个解析器,它会匹配指定的正则表达式。
+ (instancetype)regexp:(NSString *)pattern replacement:(NSString *)replacement;
这种方法创建一个解析器,该解析器可以匹配指定的正则表达式,但可以使用替换模板字符串替换捕获的文本,其中可以使用 $0-$n 来表示正则表达式中捕获的子表达式。(这种替换在技术上是一种值转换,但是将其包含在构造函数中是有意义的,这样您就不必在单独的调用中复制正则表达式模式)。
- (instancetype)withDescription:(NSString *)description;
此方法可以用来修改现有解析器的描述。例如,对于匹配正则表达式 \d 的解析器,您可能想将默认描述从 "与模式 \d 匹配的字符串" 更改为 "数值数字"。请注意,FXParser对象(主要是)不可变,因此,而不是修改解析器,将会创建并返回一个新的解析器对象,该对象的行为与原始对象相同,但使用了新的描述。
- (instancetype)withName:(NSString *)name;
此方法可以用来修改现有解析器的名称。这可以让你为解析器提供简单、易读的名称,而无需覆盖描述,在调试时失去细节。您可以通过 'name' 属性检查解析器的名称。
+ (instancetype)forwardDeclarationWithName:(NSString *)name;
有时需要创建递归规则,如果没有先定义解析器就需要引用解析器,可能会变得很困难。《forwardDeclaration》构造函数创建一个“空白”解析器,您可以在另一个解析器定义中使用它,前提是您将在尝试解析任何内容之前使用《setImplementation:》方法提供实现。如果在未设置实现之前尝试解析任何文本,将会抛出异常。此方法也设置了解析器名称,这样在以后出现错误且未设置实现时可以更容易地识别。
- (void)setImplementation:(FXParser *)implementation;
此方法用于设置使用《forwardDeclaration》构造函数创建的解析器的实现。它将提供的解析器的逻辑和描述复制到原始对象中。尝试设置已经设置或使用不同构造函数创建的解析器的实现将会抛出异常。
- (FXParserResult *)parse:(NSString *)input;
此方法尝试解析指定的字符串,并返回一个表示成功或失败的 FXParserResult 对象。
- (FXParserResult *)parse:(NSString *)input range:(NSRange)range;
此方法与前面所述类似,不同之处在于您可以指定要解析的字符串中的范围。
这些方法用于组合或修改现有的解析器,以生成更复杂的解析规则
+ (instancetype)sequence:(NSArray *)parsers;
此方法接受一个解析器数组,并将它们组装成一个单独的解析器。结果解析器只有在将提供的所有解析器按顺序应用于输入且都匹配时才会成功。
+ (instancetype)oneOf:(NSArray *)parsers;
此方法接受一个解析器数组,并将它们组装成一个单独的解析器。结果解析器如果在提供的解析器中至少有一个匹配输入,它就会成功。如果有多个解析器匹配,将使用产生最长成功匹配的解析器。
- (instancetype)optional;
此方法返回一个新的解析器,无论原始解析器是否匹配,它都会返回成功。这用于匹配无关内容,例如在标记之间可选的空白。
- (instancetype)zeroOrMoreTimes;
此方法返回一个新的解析器,它将按顺序匹配原始解析器所需字符串的零个或多个实例。
- (instancetype)oneOrMoreTimes;
此方法返回一个新的解析器,它将按顺序匹配原始解析器所需字符串的一个或多个实例。
- (instancetype)twoOrMoreTimes;
此方法返回一个新的解析器,它将按顺序匹配原始解析器所需字符串的两个或更多实例。
- (instancetype)separatedBy:(FXParser *)parser;
此方法返回一个新的解析器,它将匹配一个或多个由提供的解析器期望的字符串分隔的原始解析器期望的字符串序列。例如,如果原始解析器匹配一个数字,并且新的解析器参数匹配逗号,则结果解析器将匹配逗号分隔的数字列表。
- (instancetype)surroundedBy:(FXParser *)parser;
这是一个便利方法,返回一个新的解析器,它匹配由提供的解析器期望的字符串前后追加的原始字符串。这可能用于匹配引号中的字符串,例如,或周围有空格的令牌。它相当于[FXParser sequence:@[parser, self, parser]]
。
- (instancetype)or:(FXParser *)parser;
此方法返回一个新的解析器,它匹配原始解析器期望的字符串或提供的解析器期望的字符串。它相当于使用[FXParser oneOf:@[self, parser]]
创建一个新的解析器。
- (instancetype)then:(FXParser *)parser;
此方法返回一个新的解析器,它匹配原始解析器期望的字符串后跟提供的解析器期望的字符串。它相当于使用[FXParser sequence:@[self, parser]]
创建一个新的解析器。
这些方法用于将FXParserResult对象返回的值转换为新的值。如果您想控制解析数据转换的形式,这将很有用。请注意,将值转换应用于解析器将丢弃任何原始结果的子元素。
- (instancetype)withTransformer:(FXParserValueTransformer)transformer;
这是最灵活的值转换函数。它接受一个可以将任意函数应用于该值的块参数。FXParserValueTransformer块的签名如下
id (^FXParserValueTransformer)(id value);
谓词块接受一个输入值并返回一个输出值。从一者到另一者的转换方式由您决定,值可以任何对象类型。
- (instancetype)withComponentsJoinedByString:(NSString *)glue;
此方法通过使用NSArray的componentsJoinedByString:
方法,将数组值转换为字符串,其中使用提供的“粘合”字符串作为分隔符。如果值不是一个数组,则返回description
字符串。与asString
方法不同,如果值为nil,则不会提升为空字符串。
- (instancetype)withValueForKeyPath:(NSString *)keyPath;
有时您可能想要获取捕获值的子属性,或者在对象上调用方法的最终结果(例如[字符串 lowerCaseString]) - 您可以使用withValueForKeyPath:
转换器方法进行此操作。指定将调用在值对象上并用于返回替换值的键路径。
- (instancetype)withValue:(id)value;
有时您只想要用特定替换值替换捕获值。此方法返回指定的值而不是原始值。
- (instancetype)discard;
偶尔您需要匹配一些您不感兴趣保留的数据(例如空白)。使用discard
转换可以从结果中完全删除它。
- (instancetype)asArray;
有时您需要匹配零个或多个模式的实例,但您需要将结果始终返回为数组,即使它是一个只包含一个对象(或没有)的数组。asArray
转换将检查值,如果它是数组,则返回它未修改的值,如果是单个对象,则将其包装在数组中,如果是nil值,则创建一个新的空数组。
- (instancetype)asDictionary;
asDictionary
转换将取一个数组值,将其视为键和对象的交错序列,这些键和对象收集到一个NSDictionary中并返回。如果原始值是nil,则返回空字典。如果原始值不是一个数组,或者有奇数个项,则此方法将抛出异常。
- (instancetype)asString;
asString
转换将取一个值数组,并使用NSArray的componentsJoinedByString:
方法将它们连接成一个字符串。如果值不是一个数组,则返回description
字符串。如果值为nil,则返回空字符串。
您不需要在代码中构造一套解析器,相反,您可以使用以下方法从文本文件生成一系列解析器:
+ (FXParserResult *)parseGrammar:(NSString *)grammarString withTransformer:(FXParser *(^)(NSString *name, FXParser *parser))transformer;
语法字符串应包含一行或多行,每行格式为:
name rule
其中 "name" 是解析器的名称,"rule" 是其行为的描述。每条规则可以包含以下原始类型中的一个或多个:
"string" - an exact string to match
/pattern/ - a regular expression to match
s/pattern/replacement/ - a regular expression and replacement value (an empty replacement discards value)
name - the name of another rule
这些原始规则可以按照以下方式组合:
rule rule - multiple rules can be separated by spaces to indicate a sequence
rule | rule - multiple rules separated by the | character means either/or
rule? - a rule followed by a ? is optional
rule+ - a rule followed by a + is repeated one or more times
rule* - a rule followed by a * is repeated zero or more times
可以使用括号嵌套复合规则,如下所示:
rule1 (rule2 | rule3)+ - this would mean "rule1 followed by one or more instances of rule2 or 3"
您还可以使用 # 添加对语法文件的注释。
#comment on its own line
rulename rule #comment following a rule
"transformer" 块可用于将替换规则替换从语法中解析出的任何规则。这对于实现无法在语法文件中指定的自定义 valueTransformers 很有用。例如,这将使用格式为数组的方式替换语法中的 "foo" 解析器。
[FXParser parseGrammar:grammarString withTransformer:id^(NSString *name, FXParser *parser) {
if ([name isEqualToString:@"foo"])
{
return [parser asArray];
}
return parser;
}];
parseGrammar:withTransformer: 方法返回一个 FXParserResult,指示成功或失败。如果 result.success == YES,则 result.value 将包含从语法文件创建的 FXParser 对象的字典。
有关更多详细信息,请参阅 BASICInterpreter 和 JSONParser 例子。
FXParserResult 封装了将解析器应用于一些输入的结果。它具有以下属性:
@property (nonatomic, readonly) BOOL success;
一个布尔值,指示解析是否成功。
@property (nonatomic, readonly) id value;
解析输入后返回的值。这可以是单个值或结果数组。该值是按需计算的,并在首次访问后缓存。
@property (nonatomic, readonly) NSRange matched;
解析过程中消耗的输入字符串的匹配范围。如果您想将结果映射回输入,例如在执行语法高亮时,这可能很有用。
@property (nonatomic, readonly) NSRange remaining;
解析过程中未消耗的输入字符串的剩余范围。请注意,FXParser 要成功,并不一定必须消耗所有可用输入。如果您想将剩余输入视为错误,您可以通过自定义实现的 FXParserBlock 来强制执行此操作,或者简单地检查结果以查看剩余.length > 0。
@property (nonatomic, readonly) NSArray *children;
这是在主解析器内嵌套的 FXParsers 创建的子结果的数组(或树,因为每个子项可能有自己的子项)。如果您解析的值由更小的令牌或结构组成,您可以通过检查此值来检索有关它们的信息。例如,您可以使用子项的 remaining
值来计算子值在原始字符串中的位置。在解析失败的情况下,此值将包含直到失败点的所有成功匹配的子结果,这可能对错误恢复很有用。
@property (nonatomic, readonly) NSString *expected;
如果解析成功,则此值将为 nil。如果解析失败,则此值应包含失败点期望输入的易读(如果略显简略)描述。
您可以使用以下方法之一构建 FXParserResult:
+ (instancetype)successWithValue:(id)value matched:(NSRange)matched remaining:(NSRange)remaining;
此方法用于生成代表成功解析操作的 FXParserResult。值是解析后的结果值,匹配范围是消耗的输入字符串的范围,剩余范围是没有消耗的范围。
+ (instancetype)successWithChildren:(NSArray *)children matched:(NSRange)matched remaining:(NSRange)remaining;
此方法用于生成代表成功解析操作且匹配结果为结果数组的 FXParserResult(与单值不同)。虽然您可以使用 successWithValue:matched:remaining:
方法将此结果数组作为值数组返回,但这有两个缺点:
单个结果上下文(例如它们在字符串中的位置)没有得到保留。
嵌套规则将产生嵌套的值数组而不是单个连接的数组,这可能会更难处理(请注意,如果您想要结果中的嵌套值数组,可以使用 asArray
值转换方法重新定义结果数组为单一类型的值)。
此方法用于生成一个FXParserResult,它表示一个解析失败的操作。任何成功的子解析结果(例如,如果解析器找到了4个字符串,但预期5个)应作为子参数传递。对于没有任何子项的结果,请传递nil。期望的字符串是在解析失败时期望的下一个值的描述。在大多数情况下,这仅仅是 [parser description]
的值,但在部分成功的情况下,可能是子解析器的描述。剩余值将是字符串未消费部分的范围,在大多数情况下,如果子值数组为nil,则应与输入范围匹配,如果不匹配,则应与最后一个子项的 remaining
属性匹配。
FXParser旨在提供解析简单语言和基于文本格式的常用工具,但您可能需要一些非内置的额外功能。以下是扩展解析器以实现自定义功能的示例。
问:最佳封装常用解析器的方式是什么?例如,一个匹配数字的正则表达式?
答:最简单的方法可能是为FXParser创建一个类别,添加一个新的构造函数,例如
@interface FXParser (Primitives)
+ (instancetype)number;
@end
@implementations FXParser (Primitives)
+ (instancetype)number
{
return [[self regexp:@"-?[0-9]*\\.?[0-9]+"] withDescription:@"a number"];
}
@end
问:我需要在结果中返回额外的信息,例如一些额外的上下文元数据。我如何向FXParserResult添加额外数据?
答:最佳方法是用 withTransformer:
或 parserWithBlock:
方法后处理您解析器返回的FXParserResult,用包含额外数据的新对象替换FXParserResult.value。例如,通过遍历结果及其所有子项,并将它们的 remaining
值与原始输入范围进行比较。给定原始输入字符串的范围,应该可以创建所需的任何额外输入数据。如果您有这种情况不涵盖的使用场景,请在FXParser的GitHub页面上提交功能请求。
问:我需要实现一个运算符优先级系统,以便解析算术逻辑。
答:这里的解决方案是子类化FXParser,添加一个优先级属性和一个设置它的方法。您还需要重写 oneOf:
构造函数,并修改其优先级逻辑——目前它基于消耗的字符串长度—— để 利用您的新属性。官方的运算符优先级系统可能会在FXParser的下一个版本中添加。
版本1.2.1
版本1.2
版本 1.1
版本 1.0.1
版本 1.0