355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Хэл Фултон » Программирование на языке Ruby » Текст книги (страница 13)
Программирование на языке Ruby
  • Текст добавлен: 24 сентября 2016, 06:40

Текст книги "Программирование на языке Ruby"


Автор книги: Хэл Фултон



сообщить о нарушении

Текущая страница: 13 (всего у книги 56 страниц) [доступный отрывок для чтения: 20 страниц]

4.1. Исторические сведения и терминология

В «недобрые старые дни» становления компьютерных технологий, примерно совпадающие по времени с периодом использования перфокарт, существовало множество наборов символов. К счастью, с появлением кода ASCII в 1970-х годах эти дни миновали безвозвратно.

Аббревиатура ASCII означает American Standard Code for Information Interchange (Американский стандартный код обмена информацией). Это был большой шаг вперед, однако ключевое слово здесь «американский». Код проектировался даже без учета европейских языков, не говоря уже об азиатских.

Но в нем были и огрехи. Набор символов ASCII состоит из 128 символов (он 7-разрядный). Но как можно так расточительно относиться к дополнительному биту? Возникла естественная идея расширить набор ASCII, воспользовавшись кодами от 128 до 255 для других целей. Беда в том, что эта идея была реализована многократно и по-разному компанией IBM и другими. Не было общепринятого соглашения о том, какому символу соответствует, например, код 221.

Недостатки такого подхода очевидны. Даже если отправитель и получатель договорятся об используемом наборе символов, все равно они не смогут общаться на нескольких языках: для всех сразу не хватит символов. Если вы хотите писать по-немецки, но вставить в текст несколько цитат на греческом или иврите, то, скорее всего, ничего не получится. И эта схема не позволила даже приблизиться к решению проблем, связанных с азиатскими языками, например китайским, японским и корейским.

Было два основных способа решить эту задачу. Первый – использовать гораздо более обширный набор символов, например представляя каждый символ 16 битами (так называемые широкие символы). Второй – обратиться к многобайтовым кодировкам переменной длины. При такой схеме одни символы представляются единственным байтом, другие – двумя, а третьи – тремя или даже большим числом. При этом, очевидно, возникает масса вопросов. В частности, любая строка должна однозначно декодироваться. Первый байт многобайтового символа мог бы принадлежать специальному классу, а потому мы сумели бы понять, что следует ожидать дополнительный байт; но как быть со вторым и последующими? Разрешено ли им перекрываться с набором однобайтовых символов? Могут ли определенные символы выступать в роли второго и третьего байта или это следует запретить? Сможем ли мы перейти в середину строки и при этом не запутаться? Сможем ли просматривать строку в обратном направлении? Для разных кодировок были приняты различные проектные решения.

В конечном счете родилась идея кодировки Unicode. Считайте, что это «всемирный набор символов». Увы, на практике все не так просто.

Возможно, вы слышали, что Unicode был (или остается) ограничен 65536 символами (именно столько различных комбинаций можно представить 16 битами). Распространенное заблуждение!.. При проектировании Unicode такие ограничения не закладывались. С самого начала было ясно, что во многих случаях это будет многобайтовая схема. Количество представимых с помощью Unicode символов практически безгранично, и это хорошо, так как 65000 никогда не хватит для всех языков мира.

Говоря об интернационализации, нужно прежде всего понимать, что интерпретация строки не является внутренне присущей самой строке. Это заблуждение проистекает из уже неактуального представления, будто существует лишь один способ хранения строки.

Подчеркну, это исключительно важное положение. Внутренне строка – всего лишь последовательность байтов. Представьте себе, что в памяти машины хранится один байт в кодировке ASCII. Если это буква, которую мы называем «прописная латинская А», то реально хранится число 65.

Почему мы считаем, что 65 – это А? Потому что так мы договорились использовать (интерпретировать) это значение. Если мы складываем его с другим числом, то оно используется (интерпретируется) как число. А если отправляем его на терминал по последовательной линии связи – значит, интерпретируем как ASCII-символ.

Если можно по-разному интерпретировать одиночный байт, то почему же нельзя так сделать для последовательности байтов? На самом деле, чтобы получилась осмысленная строка, предполагаемая схема интерпретации (или кодировка должна быть известна заранее. Кодировка – это просто соответствие между двоичными числами и символами. И снова не все так просто.

Поскольку Ruby появился в Японии, он прекрасно справляется с двумя различными японскими кодировками (и ASCII). Не буду тратить время на рассказ о поддержке японского языка; если вы японец, то в вашем распоряжении сколько угодно книг по Ruby на этом языке. А для всех остальных наиболее распространённой кодировкой является Unicode. О ней мы и будем говорить в этой главе.

Но перед тем как перейти к деталям, познакомимся с некоторыми терминами. Называть вещи полезными именами – одна из основ мудрости!

• Байт – это просто восемь битов (хотя когда-то даже это было неверно). По традиции многие считают, что байт соответствует одному символу. Ясно, что в контексте I18N это не так.

• Кодовая позиция – один элемент воображаемой таблицы, с помощью которой представляется набор символов. Хотя это и не совсем верно, можете считать, что кодовые позиции взаимно однозначно отображаются на символы. Точнее будет сказать, что иногда для уникального указания символа требуется несколько кодовых позиций.

• Глиф (печатный знак) – визуальное представление кодовой позиции. Хотя интуитивно это и не совсем очевидно, символ и его визуальное представление – разные вещи. (Я могу открыть текстовый редактор и набрать прописную А десятком разных шрифтов, но все это будет один и тот же символ А.)

 Понятие графемы близко к глифу, но о графемах мы говорим в контексте языка, а не программного обеспечения. Графема может быть комбинацией (простой или не очень) двух и более глифов. Так пользователь воспринимает символ в контексте своего родного языка. Разница настолько тонкая, что большинство программистов могут о ней никогда не задумываться.

Что же тогда такое символ? Даже в мире Unicode нет четкого понимания этого предмета, поскольку языки ведут себя по-разному, а программисты мыслят иначе, чем прочие люди. Будем говорить, что символ – это абстракция написания знака, который визуально может быть представлен одним или несколькими способами.

Перейдем к конкретике. Сначала я хочу познакомить вас с нотацией. Традиционно кодовые позиции Unicode записываются как U+, а затем четыре или более шестнадцатеричных цифр в верхнем регистре. То, что мы называем латинской буквой А, можно представить в виде U+0041.

Теперь возьмем букву é (строчная е с акутом). Ее можно представить в Unicode двумя способами. Во-первых, это одна кодовая позиция U+00E9 (СТРОЧНАЯ ЛАТИНСКАЯ Е С АКУТОМ). С другой стороны, это сочетание двух кодовых позиций: строчная е + диакритический знак акут – U+0065 и U+0301. Иными словами, СТРОЧНАЯ ЛАТИНСКАЯ Е, за которой следует АКУТ.

Обе формы одинаково правильны. Более короткая называется монолитной (precomposed) формой. Однако имейте в виду, что не для каждого языка имеются монолитные варианты, поэтому не всегда можно свести подобный символ к одной кодовой позиции.

Я назвал Unicode кодировкой, но это не вполне верно. Unicode отображает символы на кодовые позиции; существуют разные способы отобразить кодовые позиции на двоичное представление. По существу, Unicode – это семейство кодировок.

Возьмем, к примеру, строку "Matz". Она состоит из четырех кодовых позиции Unicode:

# U+004d U+0061 U+0074 U+007a

Естественнее всего сохранить их в виде простой последовательности байтов.

00 4d 00 61 00 74 00 7а

Такая кодировка называется UCS-2 (два байта) или UTF-16 (16 битов). Отметим, что эта кодировка имеет две разновидности: тупоконечную (big-endian) и остроконечную (little-endian) – в зависимости от того, старший или младший байт хранится первым.

Заметим, однако, что каждый второй байт в этой последовательности нулевой. Это не просто совпадение, английский язык редко выходит за пределы кодовой позиции U+00FF. Так разбрасываться памятью расточительно.

И это наблюдение подводит нас к идее кодировки UTF-8. В ней «традиционные» символы представлены одним байтом, а остальные – несколькими. Вот как записывается та же строка в кодировке UTF-8:

4d 61 74 7а

Мы всего лишь избавились от нулей. Однако более важен тот факт, что мы получили обычную кодировку ASCII. Так и задумано: «простой ASCII» можно считать собственным подмножеством UTF-8.

Отсюда, в частности, следует, что при интерпретации текста в кодировке UTF-8 как ASCII-текста он выглядит «как обычно» (особенно если это преимущественно англоязычный текст). Иногда вы видите, что браузер или другое приложение отображает английский текст правильно, но местами появляются «крокозябры». Это, скорее всего, означает, что программа сделала неверные предположения об используемой кодировке.

Итак, можно сказать, что UTF-8 экономит память. Конечно, я снова становлюсь на англоцентрическую точку зрения (по крайней мере, ASCII-центрическую). Если текст в основном состоит из ASCII-символов, то да, память экономится, но для других языков, например греческого или русского, размер строк даже увеличится.

Еще одно очевидное достоинство UTF-8 – «обратная совместимость» с кодировкой ASCII, которая, по-видимому, все еще остается самой распространенной однобайтовой кодировкой в мире. Наконец, у UTF-8 есть некоторые особенности, делающие ее удобной для программистов.

Во-первых, байты, входящие в состав многобайтовых символов, тщательно подобраны. Нулевой байт (ASCII 0) никогда не встречается в качестве n-ого байта в последовательности (где n > 1); то же самое справедливо для таких распространенных символов, как косая черта (обычно используется для разделения компонентов пути к файлу). На самом деле никакой байт из диапазона 0x00-0x7F не может быть частью никакого другого символа.

Второй байт многобайтового символа однозначно определяет, сколько байтов за ним следует. Этот второй байт всегда выбирается из диапазона от 0хС0 до 0хF0, а следующие за ним – из диапазона от 0x80 до 0xBF. Таким образом, схема кодирования свободна от состояния и позволяет восстанавливать пропущенные или искаженные байты.

UTF-8 – одна из самых распространенных и гибких кодировок в мире. Она применяется с начала 1990-х годов и является кодировкой по умолчанию XML-документов. В этой главе мы будем иметь дело главным образом именно с UTF-8.

4.2. Кодировки в пост-ASCII мире

«Век ASCII» прошел, хотя не все еще осознали этот факт. Многие допущения, которые программисты делали в прошлом, уже несправедливы. Нам необходимо новое мышление.

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

Эти факты оказывают на программирование тонкое влияние. Рассмотрим сначала, как следует работать с символьными строками по-современному.

4.2.1. Библиотека jcode и переменная $KCODE

Чтобы использовать в Ruby разные наборы символов, вы должны знать о глобальной переменной $KCODE, от значения которой зависит поведение многих системных методов, манипулирующих строками. (Кстати говоря, буква K – напоминание о кандзи, одной из иероглифических азбук в японском языке.) Эта переменная принимает одно из пяти стандартных значений, каждое из которых представлено одной буквой, неважно – строчной или прописной (ASCII и NONE – одно и то же).

a ASCII

n NONE (ASCII)

е EUC

s SJIS

u UTF-8

Для ясности можно пользоваться и полными названиями (например, $KCODE="UTF-8"). Важен только первый символ.

О кодировке ASCII мы уже знаем. EUC и Shift-JIS (SJIS) нам малоинтересны. Мы сосредоточимся на значении UTF-8.

Установив значение $KCODE, вы задаром получаете весьма богатую функциональность. Например, метод inspect (он автоматически вызывается при обращении к методу p для печати объекта в читаемом виде) обычно учитывает текущее значение $KCODE.

$KCODE = "n"

# Для справки: французское слово "épée"

# обозначает разновидность меча (sword).

eacute = ""

eacute << 0303 << 0251 # U+00E9

sword = eacute + "p" + eacute + "e"

p eacute # "303251"

p sword # "303251p303251e"

$KCODE = "u"

p eacute # "é"

p sword  # "épée"

Регулярные выражения в режиме UTF-8 тоже становятся несколько «умнее».

$KCODE = "n"

letters = sword.scan(/(.)/)

# [["303"], ["251"], ["p"], ["303"], ["251"], ["e"]]

puts letters.size # 6

$KCODE = "u"

letters = sword.scan(/(.)/)

# [["é"], ["p"], ["é"], ["e"]]

puts letters.size # 4

Библиотека jcode предоставляет также несколько полезных методов, например jlength и each_char. Рекомендую включать эту библиотеку с помощью директивы require всякий раз, как вы работаете с кодировкой UTF-8.

В следующем разделе мы снова рассмотрим некоторые типичные операции со строками и регулярными выражениями. Заодно поближе познакомимся с jcode.

4.2.2. Возвращаясь к строкам и регулярным выражениям

При работе с UTF-8 некоторые операции ничем не отличаются. Например, конкатенация строк выполняется так же, как и раньше:

"éр" + "éе"  # "épée"

"éр" << "éе" # "épée"

Поскольку UTF-8 не имеет состояния, то для проверки вхождения подстроки тоже ничего специально делать не нужно:

"épée".include?("é") # true

Однако при написании интернациональной программы некоторые типичные допущения все же придется переосмыслить. Ясно, что символ больше не эквивалентен байту. При подсчете символов или байтов надо думать о том, что именно мы хотим сосчитать и для чего. То же относится к числу итераций.

По общепринятому соглашению, кодовую позицию часто представляют себе как «программистский символ». Это еще одна полуправда, но иногда она оказывается полезной.

Метод jlength возвращает число кодовых позиций в строке, а не байтов. Если нужно получить число байтов, пользуйтесь методом length.

$KCODE = "u"

require 'jcode'

sword = "épée"

sword.jlength # 4

sword.length  # 6

Такие методы, как upcase и capitalize, обычно неправильно работают со специальными символами. Это ограничение текущей версии Ruby. (Не стоит считать ошибкой, поскольку получить представление слова с первой прописной буквой довольно трудно; такая задача просто не решается в схеме интернационализации Ruby. Считайте, что это нереализованное поведение.)

$KCODE = "u"

sword.upcase     # "ÉPÉE"

sword.capitalize # "épée"

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

Возможно, вы думаете, что неакцентированные символы в некотором смысле эквивалентны своим акцентированным вариантам. Это почти всегда не так. Здесь мы имеем дело с разными символами. Убедимся в этом на примере метода count:

$KCODE = "u"

sword.count("e") # 1 (не 3)

Но для составных (не монолитных) символов верно прямо противоположное. В этом случае латинская буква распознается.

Метод count возвращает сбивающий с толку результат, когда ему передается многобайтовый символ. Метод jcount ведет себя в этом случае правильно:

$KCODE = "u"

sword.count("eé")  # 5 (не 3)

sword.jcount("eé") # 3

Существует вспомогательный метод mbchar?, который определяет, есть ли в строке многобайтовые символы.

$KCODE = "u"

sword.mbchar? # 0 (смещение первого многобайтового символа)

"foo".mbchar? # nil

В библиотеке jcode переопределены также методы chop, delete, squeeze, succ, tr и tr_s. Применяя их в режиме UTF-8, помните, что вы работаете с версиями, «знающими о многобайтовости». При попытке манипулировать многобайтовыми строками без библиотеки jcode вы можете получить странные или ошибочные результаты.

Можно побайтно просматривать строку, как обычно, с помощью итератора each_byte. А можно просматривать посимвольно с помощью итератора each_char. Второй способ имеет дело с односимвольными строками, первый (в текущей версии Ruby) – с однобайтными целыми. Разумеется, мы в очередной раз приравниваем кодовую позицию к символу. Несмотря на название, метод each_char на самом деле перебирает кодовые позиции, а не символы.

$KCODE = "u"

sword.each_byte {|x| puts x } # Шесть строк с целыми числами.

sword.each_char {|x| puts x } # Четыре строки со строками.

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

Таблица 4.1. Составные и монолитные формы


Строчная латинская e с акутомéU+00E90xC3 0хА9Один символ, одна кодовая позиция, один байт
Строчная латинская ееU+00650x65Один символ, две кодовых позиции (два «программистских символа»), три байта UTF-8
Модифицирующий акут́U+03010xCC 0x81

Что еще надо учитывать при работе с интернациональными строками? Квадратные скобки по-прежнему относятся к байтам, а не к символам. Но при желании это можно изменить. Ниже приведена одна из возможных реализаций (не особенно эффективная, зато понятная):

class String

 def [](index)

  self.scan(/./)[index]

 end

 def []=(index,value)

  arr = self.scan(/./)

  arr[index] = value

  self.replace(arr.join)

  value

 end

end

Конечно, здесь не реализована значительная часть функциональности настоящего метода [], который понимает диапазоны, регулярные выражения и т.д. Если вам все это нужно, придется запрограммировать самостоятельно.

У метода unpack есть параметры, помогающие манипулировать Unicode-строками. Указав в форматной строке параметр U*, мы можем преобразовать строку в кодировке UTF-8 в массив кодовых позиций (U без звездочки преобразует только первую кодовую позицию):

codepoints = sword.unpack('U*') # [233, 112, 233, 101]

Вот несколько более полезный пример, в котором все кодовые позиции в строке, отличные от ASCII (то есть начиная с U+0080), преобразуются к виду U+XXXX, который мы обсуждали выше:

def reveal_non_ascii(str)

 str.unpack('U*').map do |cp|

 if cp < 0x80

  cp.chr

 else

  '(U+%04X)' % cp

 end

 end.join

end

У метода String#unpack есть «близкий родственник» Array#pack, выполняющий обратную операцию:

[233, 112, 233, 101].pack('U*') # "épée"

Мы можем воспользоваться им, чтобы вставить Unicode-символы, которые трудно ввести с клавиатуры:

eacute = [0хЕ9].pack('U')

cafe = "caf#{eacute}" # "café"

Регулярным выражениям тоже известно о многобайтовых символах, особенно если вы пользуетесь библиотекой Oniguruma (мы рассматривали ее в главе 3). Например, образец /./ сопоставляется с одним многобайтовым символом.

Модификатор u извещает регулярное выражение о том, что мы работаем с кодировкой UTF-8. Если $KCODE равно "u", то модификатор можно не задавать, однако это и не повредит. (К тому же такая избыточность может быть полезна, если код является частью большой программы, а какое значение переменной $KCODE в ней установлено, вам неизвестно.)

Даже без Oniguruma регулярные выражения распознают, относится ли данный многобайтовый символ к категории тех, что могут входить в состав слова:

$KCODE = "u"

sword =~ /w/ #0

sword =~ /W/ # nil

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

Регулярные выражения позволяют безопасно выполнять простые манипуляции со строками. Мы и так можем без труда усекать строки. Следующий код возвращает не более 20 символов из строки ascii_string:

ascii_string[0,20]

Однако, поскольку кодовая позиция Unicode может занимать более одного байта такую технику нельзя безопасно применять к строке в кодировке UTF-8. Есть риск, что в конце строки окажется недопустимая последовательность байтов. Кроме того, это не слишком полезно, так как мы не можем заранее сказать, сколько в результате получится кодовых позиций. На помощь приходят регулярные выражения:

def truncate(str, max_length)

 str[/.{0,#{max_length}}/m]

end

4.2.3. Распознавание кодировки

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

К счастью, обычно перед нами стоит более простая задача – выяснить, записана ли строка в кодировке UTF-8. На этот вопрос можно дать достаточно надёжный ответ. Приведем один способ (основанный на том, что метод unpack возбуждает исключение, если ему передана некорректная строка):

class String

 def utf8?

  unpack('U*') rescue return false

  true

 end

end


    Ваша оценка произведения:

Популярные книги за неделю