Книга: Программирование на языке Ruby
4.2.2. Возвращаясь к строкам и регулярным выражениям
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. Составные и монолитные формы
Монолитная форма "?" | ||||
---|---|---|---|---|
Название символа | Глиф | Кодовая позиция | Байты UTF-8 | Примечания |
Строчная латинская e с акутом | ? | U+00E9 | 0xC3 0хА9 | Один символ, одна кодовая позиция, один байт |
Составная форма "?" | ||||
Название символа | Глиф | Кодовая позиция | Байты UTF-8 | Примечания |
Строчная латинская е | е | U+0065 | 0x65 | Один символ, две кодовых позиции (два «программистских символа»), три байта UTF-8 |
Модифицирующий акут | ? | U+0301 | 0xCC 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
- Индексы по выражениям
- Работа со строками
- Пример A-20. Функции для работы со строками
- Работа с ячейками, строками и столбцами таблицы
- 4.6.3. Возвращаясь к open()
- 5.4.4.2. Возвращаясь к V7 cat
- 15.4.1.2. По возможности избегайте макросов с выражениями
- Альтернативные функции для работы с обобщенными строками
- Расстояния между словами и строками, поля
- Функции XPath для работы со строками
- Сопоставление строк с регулярными выражениями
- Работа с областями памяти и строками