在我的 macOS 应用程序中,我使用 UserDefaults dictionaryRepresentation。有时我会得到编码未知的字符串。有什么建议吗?

In my macOS application, I am working with UserDefaults dictionaryRepresentation. Sometimes I get strings with unknown encoding. Any suggesition?

我正在使用 Objective-C 应用程序,具体来说,我正在使用以下代码收集 NSUserDefaults 的字典表示形式:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

NSDictionary *userDefaultsDict = [defaults dictionaryRepresentation];

在枚举生成的字典的键和对象时,有时我会发现一种不透明的字符串,您可以在下图中看到:

看来是编码问题。

如果我尝试打印字符串的描述,调试器会正确打印:

Printing description of obj:
tsuqsx

但是,如果我尝试将 obj 写入文件,或以任何其他方式使用它,我会得到如下不可读的输出:

我想实现的是:

  1. 通过某种方式检测到字符串存在编码问题

  2. 将字符串转换为 UTF8 编码以在程序的其余部分使用它。

非常感谢任何帮助。谢谢

编辑:非常 Hacky 可能的解决方案,有助于解释我正在尝试做的事情。

在尝试了基于 dataUsingEncoding 的所有可能解决方案并返回之后,我最终得到了以下解决方案,这绝对很奇怪,但我 post 在这里,希望它可以帮助别人猜测编码和如何处理不可打印的字符:

- (BOOL)isProblematicString:(NSString *)candidateString {

     BOOL returnValue = YES;

     if ([candidateString length] <= 2) {
         return NO;
     }

     const char *temp = [candidateString UTF8String];

     long length = temp[0];
   
        char *dest = malloc(length + 1);
   
        long ctr = 1;
   
        long usefulCounter = 0;
        for (ctr = 1;ctr <= length;ctr++) {
       
           if ((ctr - 1) % 3 == 0) {
              memcpy(&dest[ctr - usefulCounter - 1],&temp[ctr],1);
           } else {
               if (ctr != 1 && ctr < [candidateString length]) {
                   if (temp[ctr] < 0x10 || temp[ctr] > 0x1F) {
                       returnValue = NO;
                   }
           }
               usefulCounter += 1;
           }
       
       }
    memset(&dest[length],0,1);
    free(dest);

    return returnValue;
}

- (NSString *)utf8StringFromUnknownEncodedString:(NSString*)originalUnknownString {                       

    const char *temp = [originalUnknownString UTF8String];

    long length = temp[0];

    char *dest = malloc(length + 1);

    long ctr = 1;

    long usefulCounter = 0;
    for (ctr = 1;ctr <= length;ctr++) {
    
        if ((ctr - 1) % 3 == 0) {
            memcpy(&dest[ctr - usefulCounter - 1],&temp[ctr],1);
        } else {
            usefulCounter += 1;
        }
    
    }
    memset(&dest[length],0,1);

    NSString *returnValue = [[NSString alloc] initWithUTF8String:dest];
    free(dest);


    return returnValue;
}

这 returns 我可以用来构建完整的 UTF8 字符串的字符串。我正在寻找一个干净的解决方案。任何帮助是极大的赞赏。谢谢

我们正在谈论来自 /Library/Preferences/.GlobalPreferences.plist 的字符串 (键com.apple.preferences.timezone.new.selected_city)。

NSString *city = [[NSUserDefaults standardUserDefaults]
                  stringForKey:@"com.apple.preferences.timezone.new.selected_city"];
NSLog(@"%@", city); // \^Zt\^\^]s\^]\^\u\^V\^_q\^]\^[s\^W\^Zx\^P
(lldb) p [city description]
(__NSCFString *)  = 0x0000600003f6c240 @"\x1at\x1c\x1ds\x1d\x1cu\x16\x1fq\x1d\x1bs\x17\x1ax\x10"

What I would like to achieve is the following:

  1. Detect in some way that the string has the encoding problem.
  2. Convert the string to UTF8 encoding to use it in the rest of the program.

&

After trying all possible solutions based on dataUsingEncoding and back.

此字符串没有编码问题,\x1a\x1c、...等字符是有效字符。 您可以使用 ASCII、UTF-8 等调用 dataUsingEncoding:,但所有这些字符仍将是 当前的。它们被称为 control characters(或 non-printing 个字符)。链接的维基百科页面解释了这些字符是什么以及它们是如何在 ASCII、扩展 ASCII 和 unicode 中定义的。

您正在寻找的是如何从字符串中删除控制字符的方法。

删除控制字符

我们可以为我们的新方法创建一个类别:

@interface NSString (ControlCharacters)

- (NSString *)stringByRemovingControlCharacters;

@end

@implementation NSString (ControlCharacters)

- (NSString *)stringByRemovingControlCharacters {
    // TODO Remove control characters
    return self;
}

@end

在下面的所有示例中,city 变量都是以这种方式创建的...

NSString *city = [[NSUserDefaults standardUserDefaults]
                  stringForKey:@"com.apple.preferences.timezone.new.selected_city"];

... 并包含 @"\x1at\x1c\x1ds\x1d\x1cu\x16\x1fq\x1d\x1bs\x17\x1ax\x10"。还有所有 下面的示例使用以下代码进行了测试:

NSString *cityWithoutCC = [city stringByRemovingControlCharacters];
// tsuqsx
NSLog(@"%@", cityWithoutCC);
// {length = 6, bytes = 0x747375717378}
NSLog(@"%@", [cityWithoutCC dataUsingEncoding:NSUTF8StringEncoding]);

拆分与合并

一种方法是利用 NSCharacterSet.controlCharacterSet。 有一个stringByTrimmingCharactersInSet: 方法 (NSString),但它仅从 beginning/end 中删除这些字符, 这不是你要找的。您可以使用一个技巧:

- (NSString *)stringByRemovingControlCharacters {
    NSArray<NSString *> *components = [self componentsSeparatedByCharactersInSet:NSCharacterSet.controlCharacterSet];
    return [components componentsJoinedByString:@""];
}

它按控制字符拆分字符串,然后将这些组件连接回去。不是一个非常有效的方法,但它有效。

ICU 变换

另一种方法是使用 ICU 变换(参见 ICU User Guide)。 有一个 stringByApplyingTransform:reverse: 方法 (NSString),但它只接受预定义的常量。文档说:

The constants defined by the NSStringTransform type offer a subset of the functionality provided by the underlying ICU transform functionality. To apply an ICU transform defined in the ICU User Guide that doesn't have a corresponding NSStringTransform constant, create an instance of NSMutableString and call the applyTransform:reverse:range:updatedRange: method instead.

让我们更新我们的实现:

- (NSString *)stringByRemovingControlCharacters {
    NSMutableString *result = [self mutableCopy];
    [result applyTransform:@"[[:Cc:] [:Cf:]] Remove"
                   reverse:NO
                     range:NSMakeRange(0, self.length)
              updatedRange:nil];
    return result;
}

[:Cc:]表示控制字符,[:Cf:]表示格式字符。两者都代表与已经提到的 NSCharacterSet.controlCharacterSet 相同的字符集。文档:

A character set containing the characters in Unicode General Category Cc and Cf.

迭代字符

NSCharacterSet 还提供了 characterIsMember: 方法。这里我们需要遍历字符 (unichar) 并检查它是否是控制字符。

让我们更新我们的实现:

- (NSString *)stringByRemovingControlCharacters {
    if (self.length == 0) {
        return self;
    }

    NSUInteger length = self.length;
    unichar characters[length];
    [self getCharacters:characters];
    
    NSUInteger resultLength = 0;
    unichar result[length];
    
    NSCharacterSet *controlCharacterSet = NSCharacterSet.controlCharacterSet;
    
    for (NSUInteger i = 0 ; i < length ; i++) {
        if ([controlCharacterSet characterIsMember:characters[i]] == NO) {
            result[resultLength++] = characters[i];
        }
    }
    
    return [NSString stringWithCharacters:result length:resultLength];
}

这里我们过滤掉属于controlCharacterSet.

的所有字符(unichar

其他方式

还有其他方法可以遍历字符 - 例如 - Most efficient way to iterate over all the chars in an NSString.

BBEdit 和其他人

让我们将此字符串写入文件:

NSString *city = [[NSUserDefaults standardUserDefaults]
                  stringForKey:@"com.apple.preferences.timezone.new.selected_city"];

[city writeToFile:@"/Users/zrzka/city.txt"
       atomically:YES
         encoding:NSUTF8StringEncoding
            error:nil];

所有这些控制字符是如何由编辑器决定的handled/displayed。这是一个示例 - Visual Studio 代码。

视图 - 渲染控制字符关闭:

视图 - 渲染控制字符:

BBEdit 显示问号(上下颠倒),但我确定有办法 切换控制字符渲染。没有安装 BBEdit 来验证它。