Можно ли декодировать байты в UTF-8, преобразовывая ошибки в escape-последовательности в Rust?

В Rust можно получить UTF-8 из байтов, выполнив следующие действия:

if let Ok(s) = str::from_utf8(some_u8_slice) {
    println!("example {}", s);
}

Это либо работает, либо нет, но Python может обрабатывать ошибки, например:

s = some_bytes.decode(encoding='utf-8', errors='surrogateescape');

В этом примере аргумент surrogateescape преобразует недопустимые последовательности utf-8 в escape-коды, поэтому вместо игнорирования или замены текста, который не может быть декодирован, они заменяются байтовым литеральным выражением, которое допустимо utf-8. см. подробности в Python.

Есть ли в Rust способ получить строку UTF-8 из байтов, которая избегает ошибок, а не полностью терпит неудачу?


person ideasman42    schedule 04.01.2017    source источник


Ответы (2)


Да, через String::from_utf8_lossy:

fn main() {
    let text = [104, 101, 0xFF, 108, 111];
    let s = String::from_utf8_lossy(&text);
    println!("{}", s); // he�lo
}

Если вам нужен больший контроль над процессом, вы можете использовать std::str::from_utf8. , как предложено в другом ответе. Однако нет причин дважды проверять байты, как это предлагается.

Быстро взломанный пример:

use std::str;

fn example(mut bytes: &[u8]) -> String {
    let mut output = String::new();

    loop {
        match str::from_utf8(bytes) {
            Ok(s) => {
                // The entire rest of the string was valid UTF-8, we are done
                output.push_str(s);
                return output;
            }
            Err(e) => {
                let (good, bad) = bytes.split_at(e.valid_up_to());

                if !good.is_empty() {
                    let s = unsafe {
                        // This is safe because we have already validated this
                        // UTF-8 data via the call to `str::from_utf8`; there's
                        // no need to check it a second time
                        str::from_utf8_unchecked(good)
                    };
                    output.push_str(s);
                }

                if bad.is_empty() {
                    //  No more data left
                    return output;
                }

                // Do whatever type of recovery you need to here
                output.push_str("<badbyte>");

                // Skip the bad byte and try again
                bytes = &bad[1..];
            }
        }
    }
}

fn main() {
    let r = example(&[104, 101, 0xFF, 108, 111]);
    println!("{}", r); // he<badbyte>lo
}

Вы можете расширить это, чтобы принимать значения для замены плохих байтов, закрытие для обработки плохих байтов и т. д. Например:

fn example(mut bytes: &[u8], handler: impl Fn(&mut String, &[u8])) -> String {
    // ...    
                handler(&mut output, bad);
    // ...
}
let r = example(&[104, 101, 0xFF, 108, 111], |output, bytes| {
    use std::fmt::Write;
    write!(output, "\\U{{{}}}", bytes[0]).unwrap()
});
println!("{}", r); // he\U{255}lo

Смотрите также:

person Shepmaster    schedule 04.01.2017
comment
Обратите внимание, что from_utf8_lossy не предоставляет различных способов обработки ошибок, как это делает Python. Вместо экранирования недопустимые последовательности utf-8 заменяются на U+FFFD (что соответствует поведению Python replace). Поэтому я думаю, что короткий ответ на этот вопрос - нет, хотя from_utf8_lossy все же стоит упомянуть. - person ideasman42; 04.01.2017
comment
Краткий ответ на любой из поставленных вопросов (Возможно ли декодировать байты в UTF-8, преобразовывая ошибки в escape-последовательности в Rust? Или есть ли в Rust способ получить строку UTF-8 из байтов, которая обрабатывает ошибки без полного сбоя? ?) нет? Я почти уверен, что этот код делает именно это. - person Shepmaster; 04.01.2017
comment
Документы для from_utf8_lossy гласят: Во время этого преобразования from_utf8_lossy() заменит все недопустимые последовательности UTF-8 символом замены U+FFFD, который выглядит следующим образом: �. Так что это замена, а не управляющая последовательность. Первая часть этого ответа показывает, как можно записать преобразование с помощью управляющей последовательности: stackoverflow.com/a/41450295/432509 - person ideasman42; 04.01.2017
comment
@ideasman42, что вы подразумеваете под escape-последовательностью в данном случае? Какой пример? - person Shepmaster; 04.01.2017
comment
Вместо замены символа escape-последовательность показывает символ, используя, например, некоторый идентификатор \N{...}, поэтому вместо того, чтобы быть с потерями, она включает символы в строке (обычно в виде числа). См.: docs.python.org/3/library/codecs.html# обработчики ошибок для некоторых примеров. Как отмечено в OP, Python может использовать для этого surrogateescape. Уточню вопрос, поскольку любой, кто не знаком с Python, не сочтет его столь полезным. - person ideasman42; 04.01.2017
comment
Для примера, приведенного в ответе, bytes([104, 101, 0xFF, 108, 111]).decode('utf-8', 'surrogateescape') будет оцениваться как 'he\udcfflo', где U+DCFF будет escape-символом (обычно это кодовая точка недействителен в Unicode), используемый для представления недопустимого байта 0xff. Замена 0xff на 0xfe дает \udcfe и так далее. - person user4815162342; 04.01.2017
comment
@user4815162342 user4815162342 Но surrogateescape было бы совершенно бессмысленно в Rust; это похоже на альтернативную реализацию OsStr. - person Shepmaster; 04.01.2017
comment
Кроме того, это не будет работать в сегодняшнем Rust, чьи строки и символы отвергают кодовые точки — например, "he\u{dcff}lo" является ошибкой времени компиляции, а ::std::char::from_u32(0xdcff) возвращает None. - person user4815162342; 04.01.2017
comment
Вопрос касается только экранирования строки, а не того, как выполнить surrogateescape в Rust, это всего лишь пример распространенного метода экранирования, используемого в Python. - person ideasman42; 04.01.2017

Вы также можете:

  1. Создайте его самостоятельно, используя строгое декодирование UTF-8, который возвращает ошибку, указывающую позицию, в которой произошел сбой декодирования, которую затем можно избежать. Но это неэффективно, так как вы будете декодировать каждую неудачную попытку дважды.

  2. Попробуйте сторонние ящики, которые предоставляют более настраиваемые декодеры кодировки.

person the8472    schedule 04.01.2017
comment
декодировать каждую неудачную попытку дважды — не могли бы вы рассказать об этом подробнее? Я не вижу попытки двойного декодирования. - person Shepmaster; 04.01.2017
comment
Re: Но это неэффективно, так как вы будете декодировать каждую неудачную попытку дважды. кажется, что должен быть лучший способ, который можно сделать в небольшой функции, похожей на этот ответ, но поддерживающей действительный utf8: stackoverflow.com/a/41450295/432509 - person ideasman42; 04.01.2017
comment
@Shepmaster, где вы видите, что это возможно с одним проходом при наличии ошибок? - person the8472; 04.01.2017
comment
@ ideasman42 лучший способ - это второй вариант, который я предложил. - person the8472; 04.01.2017
comment
Начиная с самого начала, вы выполняете синтаксический анализ до тех пор, пока не столкнетесь с ошибкой, пропускаете ошибку/добавляете любой маркер, который вам нужен, а затем продолжаете синтаксический анализ после ошибки. Вы читаете каждый байт только один раз, выполняя один проход по всем данным. Итак, почему я спрашиваю, чего мне не хватает. - person Shepmaster; 04.01.2017
comment
@Shepmaster, чтобы найти позицию ошибки, вам нужно вызвать from_utf8, затем вам нужно снова вызвать ее по префиксу, чтобы получить действительный частичный результат. так что это 2 прохода по входу. в stdlib нет ничего, что бы постепенно увеличивало u8s (в основном декодер набора символов), что, похоже, ищет OP. - person the8472; 04.01.2017