NSString - 转换为纯字母(即去除重音符号和标点符号)

26

我想比较没有任何标点符号、空格、重音等的名字。 目前我正在做以下操作:

-(NSString*) prepareString:(NSString*)a {
    //remove any accents and punctuation;
    a=[[[NSString alloc] initWithData:[a dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES] encoding:NSASCIIStringEncoding] autorelease];

    a=[a stringByReplacingOccurrencesOfString:@" " withString:@""];
    a=[a stringByReplacingOccurrencesOfString:@"'" withString:@""];
    a=[a stringByReplacingOccurrencesOfString:@"`" withString:@""];
    a=[a stringByReplacingOccurrencesOfString:@"-" withString:@""];
    a=[a stringByReplacingOccurrencesOfString:@"_" withString:@""];
    a=[a lowercaseString];
    return a;
}

不过,我需要对数百个字符串执行此操作,并且我需要使其更加高效。有什么想法吗?


什么是重点?你是想将数据传递给无法处理这些字符的系统吗? - uchuugaka
13个回答

81
NSString* finish = [[start componentsSeparatedByCharactersInSet:[[NSCharacterSet letterCharacterSet] invertedSet]] componentsJoinedByString:@""];

3
刚刚记录了letterCharacterSet的内容 - 看起来包含了重音符号 - 这里是一个20个字符的片段 opqrstuvwxyzªµºÀÁÂÃÄ 这是我所用的代码:https://gist.github.com/rsaunders100/6160147 - Robert
2
Swift 中,因为 componentsJoinedByString 存在但是不同:let finish = "".join(start.componentsSeparatedByCharactersInSet(NSCharacterSet.letterCharacterSet().invertedSet)) - Aviel Gross
太好了!我正在将文件名与字符串进行比较,例如é就无法匹配。解决方法是创建一个只包含所需内容的集合:let name = "".join(theString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM").invertedSet)) - Simpa

39
在使用任何这些解决方案之前,请勿忘记使用 decomposedStringWithCanonicalMapping 来分解任何带重音的字母。这将把例如 é (U+00E9) 转换为 e ‌́ (U+0065 U+0301)。然后,在剥离非字母数字字符时,无重音字母将保留。
这样做很重要的原因是您可能不希望“dän”和“dün”*被视为相同。如果您剥离了所有重音字母(某些解决方案可能会这样做),您将得到“dn”,因此这些字符串将被比作相等。
因此,您应该首先进行分解,以便可以去掉重音并保留字母。
*来自德语的示例。感谢Joris Weimar提供。

我认为Peter试图展示两个拥有相同字母但不同重音的单词。 :-) - Quinn Taylor
有趣的德国例子。:D 它不是德语(在德语中,丹麦语是“dänisch”),但它仍然是一个很好的例子来概述问题。http://dict.leo.org/#/search=Danish - Daniel S.
所以在英语中普遍存在的误解是假设那些带有不同重音符号的字母实际上是相同的字母。在英语中它们经常被视为相同的字母,但是在其他语言环境下它们是不同的字母。这就是这个问题固有的问题。这是一种天真和错误的排序方法。 - uchuugaka

15

在一个类似的问题上,Ole Begemann建议使用stringByFoldingWithOptions:我认为这是最好的解决方案:

NSString *accentedString = @"ÁlgeBra";
NSString *unaccentedString = [accentedString stringByFoldingWithOptions:NSDiacriticInsensitiveSearch locale:[NSLocale currentLocale]];

根据您想要转换的字符串的特性,您可能希望设置一个固定的语言环境(例如英语),而不是使用用户当前的语言环境。这样,您可以确保在每台计算机上获得相同的结果。


7

在BillyTheKid18756的回答中,有一个重要的精度问题(虽然Luiz已经纠正了代码解释中并不明显的错误):

请勿使用 stringWithCString 作为去掉重音的第二步操作,因为它可能会向您的字符串末尾添加不需要的字符,因为NSData没有以NULL结尾(就像stringWithCString所期望的那样)。 或者使用它,并在您的NSData中添加额外的NULL字节,就像Luiz在他的代码中所做的那样。

我认为一个更简单的答案是替换:

NSString *sanitizedText = [NSString stringWithCString:[sanitizedData bytes] encoding:NSASCIIStringEncoding];

通过:

NSString *sanitizedText = [[[NSString alloc] initWithData:sanitizedData encoding:NSASCIIStringEncoding] autorelease];

如果我收回BillyTheKid18756的代码,这里是完整正确的代码:
// The input text
NSString *text = @"BûvérÈ!@$&%^&(*^(_()-*/48";

// Defining what characters to accept
NSMutableCharacterSet *acceptedCharacters = [[NSMutableCharacterSet alloc] init];
[acceptedCharacters formUnionWithCharacterSet:[NSCharacterSet letterCharacterSet]];
[acceptedCharacters formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]];
[acceptedCharacters addCharactersInString:@" _-.!"];

// Turn accented letters into normal letters (optional)
NSData *sanitizedData = [text dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// Corrected back-conversion from NSData to NSString
NSString *sanitizedText = [[[NSString alloc] initWithData:sanitizedData encoding:NSASCIIStringEncoding] autorelease];

// Removing unaccepted characters
NSString* output = [[sanitizedText componentsSeparatedByCharactersInSet:[acceptedCharacters invertedSet]] componentsJoinedByString:@""];

7
如果您想比较字符串,请使用以下方法之一。不要尝试更改数据。
- (NSComparisonResult)localizedCompare:(NSString *)aString
- (NSComparisonResult)localizedCaseInsensitiveCompare:(NSString *)aString
- (NSComparisonResult)compare:(NSString *)aString options:(NSStringCompareOptions)mask range:(NSRange)range locale:(id)locale

对于字符串操作,尤其是对于名称等内容的处理,需要考虑用户所在的本地化信息。
在大部分语言中,像ä和å这样的字符并非只有外观相似,它们实际上是有内在含义的不同字符,而且它们在不同的语境下有着不同的规则与语义。
正确的比较和排序字符串的方式是要考虑用户的本地化信息。任何其他方法都是幼稚、错误和过时的。请停止这种行为。
如果你正在尝试将数据传递给一个不支持非ASCII字符的系统,那么这显然是错误的。应该将它作为数据块传递。
此外,需要对字符串进行规范化处理(请参见Peter Hosey的文章),即采用一种规范化形式:precomposing或decomposing。 https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Strings/Articles/SearchingStrings.html
- (NSString *)decomposedStringWithCanonicalMapping
- (NSString *)decomposedStringWithCompatibilityMapping
- (NSString *)precomposedStringWithCanonicalMapping
- (NSString *)precomposedStringWithCompatibilityMapping

不,实际上这并不像我们想象的那么简单易懂。是的,它需要明智而谨慎的决策(以及一些非英语语言的经验有所帮助)。

我完全同意。如果你了解其他语言,那么简单的替换或正则表达式就没有意义了。代码不应直接包含特定于语言的字符,例如字符数组等用于替换的内容。如果不支持该特性,请尝试查找库。幸运的是,Obj-C 对本地化有很好的支持。 - Edgar Carvalho
API中最好的语言支持之一。 - uchuugaka

4

结合Luiz和Peter的答案,添加几行代码,下面是一个完整的示例。

代码实现以下功能:

  1. 创建一组可接受的字符
  2. 将带重音符号的字母转换为普通字母
  3. 删除不在该集合中的字符

Objective-C

// The input text
NSString *text = @"BûvérÈ!@$&%^&(*^(_()-*/48";

// Create set of accepted characters
NSMutableCharacterSet *acceptedCharacters = [[NSMutableCharacterSet alloc] init];
[acceptedCharacters formUnionWithCharacterSet:[NSCharacterSet letterCharacterSet]];
[acceptedCharacters formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]];
[acceptedCharacters addCharactersInString:@" _-.!"];

// Turn accented letters into normal letters (optional)
NSData *sanitizedData = [text dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *sanitizedText = [NSString stringWithCString:[sanitizedData bytes] encoding:NSASCIIStringEncoding];

// Remove characters not in the set
NSString* output = [[sanitizedText componentsSeparatedByCharactersInSet:[acceptedCharacters invertedSet]] componentsJoinedByString:@""];

Swift (2.2) 示例

let text = "BûvérÈ!@$&%^&(*^(_()-*/48"

// Create set of accepted characters
let acceptedCharacters = NSMutableCharacterSet()
acceptedCharacters.formUnionWithCharacterSet(NSCharacterSet.letterCharacterSet())
acceptedCharacters.formUnionWithCharacterSet(NSCharacterSet.decimalDigitCharacterSet())
acceptedCharacters.addCharactersInString(" _-.!")

// Turn accented letters into normal letters (optional)
let sanitizedData = text.dataUsingEncoding(NSASCIIStringEncoding, allowLossyConversion: true)
let sanitizedText = String(data: sanitizedData!, encoding: NSASCIIStringEncoding)

// Remove characters not in the set
let components = sanitizedText!.componentsSeparatedByCharactersInSet(acceptedCharacters.invertedSet)
let output = components.joinWithSeparator("")

输出

这两个示例的输出将为:BuverE!_-48


4
考虑使用RegexKit框架。你可以这样做:
NSString *searchString      = @"This is neat.";
NSString *regexString       = @"[\W]";
NSString *replaceWithString = @"";
NSString *replacedString    = [searchString stringByReplacingOccurrencesOfRegex:regexString withString:replaceWithString];

NSLog (@"%@", replacedString);
//... Thisisneat

我该如何使用正则表达式一次性删除所有标点符号,而不需要多个语句?我试图避免多次遍历字符串。 - deelo
你只需要遍历一次原始字符串。正则表达式一次性删除所有标点符号,用空格("")替换所有非字母数字字符。 - Alex Reynolds

4
考虑使用NSScanner,特别是方法-setCharactersToBeSkipped:(接受NSCharacterSet)和-scanString:intoString:(接受字符串并通过引用返回已扫描的字符串)。
您还可以将其与-[NSString localizedCompare:]或可能使用带-[NSString compare:options:]NSDiacriticInsensitiveSearch选项。这可以简化去除/替换重音符号的步骤,因此您可以专注于去除标点符号、空格等。
如果您必须使用像您在问题中提出的方法,至少使用NSMutableString和replaceOccurrencesOfString:withString:options:range:——这比创建大量几乎相同的自动释放字符串要高效得多。仅仅减少分配数量可能足以提升性能。

3

刚刚遇到这个问题,也许有点晚了,但是这是对我有效的解决方法:

// text is the input string, and this just removes accents from the letters

// lossy encoding turns accented letters into normal letters
NSMutableData *sanitizedData = [text dataUsingEncoding:NSASCIIStringEncoding
                                  allowLossyConversion:YES];

// increase length by 1 adds a 0 byte (increaseLengthBy 
// guarantees to fill the new space with 0s), effectively turning 
// sanitizedData into a c-string
[sanitizedData increaseLengthBy:1];

// now we just create a string with the c-string in sanitizedData
NSString *final = [NSString stringWithCString:[sanitizedData bytes]];

请注意,这段代码是可行的,但需要进行一些微小的修改:dataUsingEncoding返回的是NSData而不是NSMutableData,因此您需要执行[[[text dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES] mutableCopy] autorelease] - Matt Rix
这也将删除所有非ASCII字母,例如'жопень'中的字母。 - Mike Keskinov
太棒了!你让我感到非常开心。由于stringWithCString已被弃用,你必须使用stringWithCString:encoding。我也使用了NSASCIIStringEncoding,它运行良好! - DZenBot
[sanitizedData increaseLengthBy:1]; 导致应用程序崩溃。 - Ilker Baltaci

1
这些答案对我来说没有按照预期的那样工作。具体而言,decomposedStringWithCanonicalMapping没有像我预期的那样去掉重音符号/变音符号。

这是我所使用的变化版本,以回答问题:
// replace accents, umlauts etc with equivalent letter i.e 'é' becomes 'e'.
// Always use en_GB (or a locale without the characters you wish to strip) as locale, no matter which language we're taking as input
NSString *processedString = [string stringByFoldingWithOptions: NSDiacriticInsensitiveSearch locale: [NSLocale localeWithLocaleIdentifier: @"en_GB"]];
// remove non-letters
processedString = [[processedString componentsSeparatedByCharactersInSet:[[NSCharacterSet letterCharacterSet] invertedSet]] componentsJoinedByString:@""];
// trim whitespace
processedString = [processedString stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]];
return processedString;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接