Преобразование многобайтового массива байтов Unicode в NSString с использованием частичного буфера

В Objective C есть ли способ преобразовать многобайтовый массив байтов Unicode в NSString, где он позволит выполнить преобразование, даже если данные массива являются частичным буфером (не на полной границе символов)?

Применение этого заключается в получении байтовых буферов в потоке, и вы хотите проанализировать строковую версию буфера данных (но есть еще данные, и данные вашего буфера не имеют полного многобайтового юникода).

Метод initWithData:encoding: NSString не работает для этой цели, как показано здесь...

Тестовый код:

    - (void)test {
        char myArray[] = {'f', 'o', 'o', (char) 0xc3, (char) 0x97, 'b', 'a', 'r'};
        size_t sizeOfMyArray = sizeof(myArray);
        [self dump:myArray sizeOfMyArray:sizeOfMyArray];
        [self dump:myArray sizeOfMyArray:sizeOfMyArray - 1];
        [self dump:myArray sizeOfMyArray:sizeOfMyArray - 2];
        [self dump:myArray sizeOfMyArray:sizeOfMyArray - 3];
        [self dump:myArray sizeOfMyArray:sizeOfMyArray - 4];
        [self dump:myArray sizeOfMyArray:sizeOfMyArray - 5];
    }

    - (void)dump:(char[])myArray sizeOfMyArray:(size_t)sourceLength {
        NSString *string = [[NSString alloc] initWithData:[NSData dataWithBytes:myArray length:sourceLength] encoding:NSUTF8StringEncoding];
        NSLog(@"sourceLength: %lu bytes, string.length: %i bytes, string :'%@'", sourceLength, string.length, string);
    }

Вывод:

sourceLength: 8 bytes, string.length: 7 bytes, string :'foo×bar'
sourceLength: 7 bytes, string.length: 6 bytes, string :'foo×ba'
sourceLength: 6 bytes, string.length: 5 bytes, string :'foo×b'
sourceLength: 5 bytes, string.length: 4 bytes, string :'foo×'
sourceLength: 4 bytes, string.length: 0 bytes, string :'(null)'
sourceLength: 3 bytes, string.length: 3 bytes, string :'foo'

Как видно, преобразование байтового массива «sourceLength: 4 байта» завершается ошибкой и возвращает (null). Это связано с тем, что символ Юникода '×' UTF-8 (0xc3 0x97) включен лишь частично.

В идеале я мог бы использовать функцию, которая возвращала бы правильную строку NString и сообщала мне, сколько байтов «осталось».


person TJez    schedule 28.08.2014    source источник


Ответы (3)


У вас во многом есть свой ответ. Если метод initWithData:dataWithBytes:encoding: возвращает nil, то вы знаете, что в конце буфера есть частичный (недопустимый) символ.

Измените dump, чтобы вернуть int. Затем попытайтесь создать NSString в цикле. Каждый раз, когда вы получаете nil, уменьшайте длину и повторяйте попытку. Как только вы получите действительный NSString, верните разницу между используемой длиной и переданной длиной.

person rmaddy    schedule 28.08.2014
comment
Спасибо ... дал вам голос за ваши мысли. То, что вы говорите, правильно, но в некоторых ситуациях производительность очень плохая. И в этом случае производительность имеет решающее значение, потому что я могу обрабатывать гигабайты данных. - person TJez; 28.08.2014

У меня была эта проблема раньше, и я забыл о ней на некоторое время. Это была возможность сделать это. В приведенном ниже коде используется информация с страницы utf-8 в Википедии. Это категория на NSData.

Он проверяет данные с конца и только четыре последних байта, потому что ОП сказал, что это может быть гигабайт данных. В противном случае с utf-8 проще пробежаться по байтам с начала.

/* 
 Return the range of a valid utf-8 encoded text by
 removing partial trailing multi-byte char.
 It assumes that all the bytes are valid utf-8 encoded char,
 e.g. it don't raise a flag if a continuation byte is preceded
 by a single char byte.
 */
 - (NSRange)rangeOfUTF8WithoutPartialTrailingMultibytes
 {
    NSRange validRange = {0, 0};

    NSUInteger trailLength = MIN([self length], 4U);
    unsigned char trail[4];
    [self getBytes:&trail
             range:NSMakeRange([self length] - trailLength, trailLength)];

    unsigned multibyteCount = 0;

    for (NSInteger i = trailLength - 1; i >= 0; i--) {
        if (isUTF8SingleByte(trail[i])) {
            validRange = NSMakeRange(0, [self length] - trailLength + i + 1);
            break;
        }

        if (isUTF8ContinuationByte(trail[i])) {
            multibyteCount++;
            continue;
        }

        if (isUTF8StartByte(trail[i])) {
            multibyteCount++;
            if (multibyteCount == lengthForUTF8StartByte(trail[i])) {
                validRange = NSMakeRange(0, [self length] - trailLength + i + multibyteCount);
            }
            else {
                validRange = NSMakeRange(0, [self length] - trailLength + i);
            } 
            break;
        }
    }
    return validRange;
}

Вот статические функции, используемые в методе:

static BOOL isUTF8SingleByte(const unsigned char c)
{
    return c <= 0x7f;
}

static BOOL isUTF8ContinuationByte(const unsigned char c)
{
    return (c >= 0x80) && (c <= 0xbf);
}

static BOOL isUTF8StartByte(const unsigned char c)
{
    return (c >= 0xc2) && (c <= 0xf4);
}

static BOOL isUTF8InvalidByte(const unsigned char c)
{
    return (c == 0xc0) || (c == 0xc1) || (c > 0xf4);
}

static unsigned lengthForUTF8StartByte(const unsigned char c)
{
    if ((c >= 0xc2) && (c <= 0xdf)) {
        return 2;
    }
    else if ((c >= 0xe0) && (c <= 0xef)) {
        return 3;
    }
    else if ((c >= 0xf0) && (c <= 0xf4)) {
        return 4;
    }
    return 1;
}
person Frédéric T.    schedule 29.08.2014

Вот моя неэффективная реализация, которую я не считаю правильным ответом. Я оставлю это здесь на случай, если другие сочтут это полезным (и в надежде, что кто-то другой даст лучший ответ, чем этот!)

Это в категории на NSMutableData...

    /**
    * Removes the biggest string possible from this NSMutableData, leaving any remainder unicode half-characters behind.
    *
    * NOTE: This is a very inefficient implementation, it may require multiple parsing of the complete NSMutableData buffer,
    * it is especially inefficient when the data buffer does not contain a valid string encoding, as all lengths will be
    * attempted.
    */
    - (NSString *)removeMaximumStringUsingEncoding:(NSStringEncoding)encoding {
        if (self.length > 0) {
            // Quick test for the case where the whole buffer can be used (is common case, and doesn't require NSData manipulation).
            NSString *result = [[NSString alloc] initWithData:self encoding:encoding];
            if (result != Nil) {
                self.length = 0; // Simple case, we used the whole buffer.
                return result;
            }

            // Try to find the largest subData that is a valid string.
            for (NSUInteger subDataLength = self.length - 1; subDataLength > 0; subDataLength--) {
                NSRange subDataRange = NSMakeRange(0, subDataLength);
                result = [[NSString alloc] initWithData:[self subdataWithRange:subDataRange] encoding:encoding];
                if (result != Nil) {
                    // Delete the bytes we used from our buffer, leave the remainder.
                    [self replaceBytesInRange:subDataRange withBytes:Nil length:0];
                    return result;
                }
            }
        }
        return @"";
    }
person TJez    schedule 28.08.2014