Проблема с отображением ширины UTF-8 для китайских иероглифов

Когда я использую Perl или C для printf некоторых данных, я пробовал их формат для управления шириной каждого столбца, например

printf("%-30s", str);

Но когда str содержит китайский символ, столбец не выравнивается должным образом. см. прикрепленное изображение.

Кодировка моей кодировки ubuntu - zh_CN.utf8, насколько я знаю, кодировка utf-8 имеет длину 1 ~ 4 байта. Китайский иероглиф состоит из 3 байтов. В моем тесте я обнаружил, что элемент управления форматом printf считает китайский символ как 3, но на самом деле он отображается как 2 ширины ascii.

Таким образом, реальная ширина дисплея - это не постоянная величина, как ожидалось, а переменная, связанная с количеством китайских символов, т. Е.

Sw(x) = 1 * (w - 3x) + 2 * x = w - x

w - ожидаемый предел ширины, x - количество китайских символов, Sw (x) - реальная ширина дисплея.

Таким образом, чем больше символов str содержит китайский символ, тем короче он отображается.

Как я могу получить то, что хочу? Считайте китайские иероглифы перед printf?

Насколько я знаю, все китайские или даже все широкие символы, я думаю, отображаются как 2 ширины, тогда почему printf считает это как 3? Кодировка UTF-8 не имеет ничего общего с длиной отображения.


person gpanda    schedule 25.05.2012    source источник
comment
Другими словами, вы ищете многобайтовую версию printf для Perl и / или C?   -  person deceze♦    schedule 25.05.2012
comment
Я никогда не выполнял декодирование utf8 на C, но вот код Go, который считает руны в строке utf-8: golang.org/src/pkg/unicode/utf8/utf8.go?s=4824:4876#L202   -  person Denys Séguret    schedule 25.05.2012
comment
@dystroy Дело не только в подсчете кодовых точек (то есть рун). Скорее, он принимает во внимание, что разные кодовые точки представляют 0, 1 или 2 столбца печати на UAX # 11, и это довольно тонко, особенно с символами East_Asian_Width=Ambiguous. Я не знаю ни одной библиотеки Go, которая справлялась бы с этим так же, как библиотека Perl, описанная в моем ответе, но если есть такая вещь для Go, я бы хотел узнать об этом! Спасибо.   -  person tchrist    schedule 26.05.2012
comment
@tchrist: Я кое-что узнал. И я только что провел тест: go fmt неправильно форматирует структуры с длинными символами. Так что, я полагаю, все еще есть недостатки в том, как Go обрабатывает гигантского зверя, который является Unicode ...   -  person Denys Séguret    schedule 26.05.2012
comment
Ширина дисплея (количество позиций на экране), количество символов и количество байтов - это три разные вещи. printf заботится только о количестве байтов. Если вы хотите учесть количество символов, используйте wprintf (помните, он принимает формат wchar_t*). В C нет функции форматирования, учитывающей ширину отображения.   -  person n. 1.8e9-where's-my-share m.    schedule 26.05.2012
comment
@ n.m. Работа с физическими кодовыми единицами вместо логических кодовых точек всегда является серьезной ошибкой.   -  person tchrist    schedule 26.05.2012
comment
@tchrist: так сказано в спецификации printf. Возможно, будет лучше.   -  person n. 1.8e9-where's-my-share m.    schedule 26.05.2012
comment
@ n.m. Зависит от чей printf. Некоторые имеют дело с кодовыми точками. Ни один из них не касается ширины печати.   -  person tchrist    schedule 26.05.2012
comment
@tchrist: printf определяется стандартом C, любое отклонение от него, ну, нестандартно.   -  person n. 1.8e9-where's-my-share m.    schedule 26.05.2012
comment
@ n.m. Нематериальный. Прочтите четвертое слово в вопросе исходного автора. Только C printf определен стандартом C, никаким другим. Конечно, Perl printf не был бы настолько глуп, чтобы рассматривать символы как байты, и это не так. Ни printf в Ruby, ни printf в Java, ни fmt в Go, ни % в Python. Я уверен, что существует множество других языков с современной моделью обработки символов, которым не мешает побайтовое мышление.   -  person tchrist    schedule 27.05.2012
comment
@tchrist: извините, я должен был прояснить, что я говорю только о C, а не о Perl или чем-то еще.   -  person n. 1.8e9-where's-my-share m.    schedule 27.05.2012


Ответы (1)


Да, это проблема всех известных мне версий printf. Я кратко обсуждаю этот вопрос в этом ответе, а также в этот.

Что касается C, я не знаю библиотеки, которая сделает это за вас, но если она у кого-то есть, то это будет ICU.

Для Perl вы должны использовать модуль Unicode :: GCString из CPAN, чтобы рассчитать количество столбцов печати, которые займет строка Unicode. При этом учитывается стандартное приложение № 11 Unicode: ширина Восточной Азии.

Например, некоторые кодовые точки занимают 1 столбец, а другие - 2 столбца. Есть даже такие, которые вообще не занимают столбцов, например, объединение символов и невидимых управляющих символов. В классе есть метод columns, который возвращает количество столбцов, занимаемых строкой.

У меня есть пример использования этого для выравнивания текста Unicode по вертикали здесь. Он отсортирует набор строк Unicode, в том числе некоторые с комбинированными символами и «широкими» азиатскими идеограммами (символы CJK), и позволит вам выровнять их по вертикали.

образец вывода терминала

Ниже приведен код небольшой umenu демонстрационной программы, которая распечатывает этот красиво выровненный вывод.

Возможно, вас заинтересует гораздо более амбициозный модуль Unicode :: LineBreak из который вышеупомянутый класс Unicode::GCString является лишь меньшим по размеру компонентом. Этот модуль намного круче и учитывает стандартное приложение № 14 Unicode: алгоритм разрыва строки Unicode.

Вот код небольшой umenu демонстрации, протестированной на Perl v5.14:

 #!/usr/bin/env perl
 # umenu - demo sorting and printing of Unicode food
 #
 # (obligatory and increasingly long preamble)
 #
 use utf8;
 use v5.14;                       # for locale sorting
 use strict;
 use warnings;
 use warnings  qw(FATAL utf8);    # fatalize encoding faults
 use open      qw(:std :utf8);    # undeclared streams in UTF-8
 use charnames qw(:full :short);  # unneeded in v5.16

 # std modules
 use Unicode::Normalize;          # std perl distro as of v5.8
 use List::Util qw(max);          # std perl distro as of v5.10
 use Unicode::Collate::Locale;    # std perl distro as of v5.14

 # cpan modules
 use Unicode::GCString;           # from CPAN

 # forward defs
 sub pad($$$);
 sub colwidth(_);
 sub entitle(_);

 my %price = (
     "γύρος"             => 6.50, # gyros, Greek
     "pears"             => 2.00, # like um, pears
     "linguiça"          => 7.00, # spicy sausage, Portuguese
     "xoriço"            => 3.00, # chorizo sausage, Catalan
     "hamburger"         => 6.00, # burgermeister meisterburger
     "éclair"            => 1.60, # dessert, French
     "smørbrød"          => 5.75, # sandwiches, Norwegian
     "spätzle"           => 5.50, # Bayerisch noodles, little sparrows
     "包子"              => 7.50, # bao1 zi5, steamed pork buns, Mandarin
     "jamón serrano"     => 4.45, # country ham, Spanish
     "pêches"            => 2.25, # peaches, French
     "シュークリーム"    => 1.85, # cream-filled pastry like éclair, Japanese
     "막걸리"            => 4.00, # makgeolli, Korean rice wine
     "寿司"              => 9.99, # sushi, Japanese
     "おもち"            => 2.65, # omochi, rice cakes, Japanese
     "crème brûlée"      => 2.00, # tasty broiled cream, French
     "fideuà"            => 4.20, # more noodles, Valencian (Catalan=fideuada)
     "pâté"              => 4.15, # gooseliver paste, French
     "お好み焼き"        => 8.00, # okonomiyaki, Japanese
 );

 my $width = 5 + max map { colwidth } keys %price;

 # So the Asian stuff comes out in an order that someone
 # who reads those scripts won't freak out over; the
 # CJK stuff will be in JIS X 0208 order that way.
 my $coll  = new Unicode::Collate::Locale locale => "ja";

 for my $item ($coll->sort(keys %price)) {
     print pad(entitle($item), $width, ".");
     printf " €%.2f\n", $price{$item};
 }

 sub pad($$$) {
     my($str, $width, $padchar) = @_;
     return $str . ($padchar x ($width - colwidth($str)));
 }

 sub colwidth(_) {
     my($str) = @_;
     return Unicode::GCString->new($str)->columns;
 }

 sub entitle(_) {
     my($str) = @_;
     $str =~ s{ (?=\pL)(\S)     (\S*) }
              { ucfirst($1) . lc($2)  }xge;
     return $str;
 }

Как видите, ключом к тому, чтобы заставить его работать в этой конкретной программе, является эта строка кода, которая просто вызывает другие функции, определенные выше, и использует модуль, который я обсуждал:

print pad(entitle($item), $width, ".");

Это расширит элемент до заданной ширины, используя точки в качестве символа заливки.

Да, это намного менее удобно, чем printf, но, по крайней мере, это возможно.

person tchrist    schedule 26.05.2012