Spec-Zone.ru › Julia 1.11

Строки

Строки представляют собой конечные последовательности символов. Конечно, настоящие трудности возникают, когда задается вопрос о том, что такое символ. Символы, с которыми знакомы говорящие по-английски, — это буквы A, B, C, и т.д., а также цифры и знаки препинания. Эти символы стандартизированы вместе с сопоставлением целочисленным значениям от 0 до 127 в соответствии со стандартом ASCII. Конечно, существует множество других символов, используемых в языках, отличных от английского, включая варианты символов ASCII с диакритическими знаками и другими модификациями, связанные алфавиты, такие как кириллица и греческий, и алфавиты, не имеющие никакого отношения к ASCII и английскому, включая арабский, китайский, иврит, хинди, японский и корейский. Стандарт Unicode решает сложности определения того, что такое символ, и в целом признан окончательным стандартом, решающим эту проблему. В зависимости от ваших потребностей, вы можете либо полностью игнорировать эти сложности и просто представить, что существуют только символы ASCII, либо написать код, который может обрабатывать любые символы или кодировки, которые могут встретиться при работе с не-ASCII-текстом. Julia упрощает и эффективно обрабатывает обычный текст ASCII, а обработка Unicode максимально проста и эффективна. В частности, вы можете написать код строк в стиле C для обработки ASCII-строк, и он будет работать как ожидается, как по производительности, так и по семантике. Если такой код столкнется с не-ASCII-текстом, он корректно завершится с ясным сообщением об ошибке, а не будет молча вводить некорректные результаты. В таких случаях модификация кода для обработки данных, не являющихся ASCII, проста.

Существует несколько примечательных функций высокого уровня в строках Julia:

  • Встроенный конкретный тип, используемый для строк (и строковых литералов) в Julia, — это String. Он поддерживает весь диапазон символов Unicode через кодировку UTF-8. (Для преобразования в/из других кодировок Unicode предоставляется функция transcode.)
  • Все типы строк являются подтипами абстрактного типа AbstractString, и внешние пакеты определяют дополнительные AbstractString подтипы (например, для других кодировок). Если вы определяете функцию, ожидавшую строковый аргумент, вам следует объявить тип как AbstractString для того, чтобы принять любой тип строки.
  • Как в C и Java, но в отличие от большинства динамических языков, Julia имеет первый класс типов для представления одиночного символа, называемый AbstractChar. Встроенный подтип Char типа AbstractChar — это 32-битный примитивный тип, который может представлять любой символ Unicode (и который основан на кодировке UTF-8).
  • Как и в Java, строки неизменяемы: значение объекта AbstractString изменить нельзя. Чтобы создать другое значение строки, необходимо создать новую строку из частей других строк.
  • Концептуально, строка является частичной функцией от индексов к символам: для некоторых значений индексов не возвращается значение символа, а вместо этого генерируется исключение. Это позволяет эффективно индексировать строки по индексу байта закодированного представления, а не по индексу символа, что не может быть реализовано одновременно эффективно и просто для кодировок Unicode с переменной длиной.

Символы

Значение Char представляет собой отдельный символ: это всего лишь 32-битный примитивный тип со специальным представлением литерала и соответствующим поведением арифметических операций, который может быть преобразован в числовое значение, представляющее код символа Unicode. (Пакеты Julia могут определять другие подтипы AbstractChar, например, для оптимизации операций для других кодировок символов.) Вот как вводятся и отображаются значения Char. (Обратите внимание, что строковые литералы символов ограничиваются одинарными кавычками, а не двойными).

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> typeof(c)
Char

Вы легко можете преобразовать Char в его целочисленное значение, т. е. код символа:

julia> c = Int('x')
120

julia> typeof(c)
Int64

На 32-битных архитектурах typeof(c) будет Int32. Вы можете преобразовать целочисленное значение обратно в Char так же легко:

julia> Char(120)
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

Не все целые значения являются допустимыми кодами символов Unicode, но для повышения производительности преобразование Char не проверяет, что каждое значение символа является допустимым. Если вы хотите проверить, что каждое преобразованное значение является допустимым кодом символа, используйте функцию isvalid:

julia> Char(0x110000)
'\U110000': Unicode U+110000 (category In: Invalid, too high)

julia> isvalid(Char, 0x110000)
false

На момент написания этой документации допустимыми кодами Unicode являются U+0000 до U+D7FF и U+E000 до U+10FFFF. Всем этим значениям пока не присвоено осмысленное значение, и приложения не обязательно могут их интерпретировать, но все эти значения считаются допустимыми символами Unicode.

Вы можете вводить любые символы Unicode в одинарных кавычках, используя \u за которым следуют до четырёх шестнадцатеричных цифр, или \U за которым следуют до восьми шестнадцатеричных цифр (самое длинное допустимое значение требует только шести):

julia> '\u0'
'\0': ASCII/Unicode U+0000 (category Cc: Other, control)

julia> '\u78'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> '\u2200'
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> '\U10ffff'
'\U10ffff': Unicode U+10FFFF (category Cn: Other, not assigned)

Julia использует настройки локали и языка вашей системы для определения, какие символы могут быть напечатаны как есть, а какие необходимо выводить с помощью универсальных, экранированных форм ввода \u или \U. В дополнение к этим форматам экранирования Unicode могут также использоваться все традиционные формы экранирования ввода C:

julia> Int('\0')
0

julia> Int('\t')
9

julia> Int('\n')
10

julia> Int('\e')
27

julia> Int('\x7f')
127

julia> Int('\177')
127

Вы можете выполнять сравнения и ограниченное количество арифметических операций со значениями Char:

julia> 'A' < 'a'
true

julia> 'A' <= 'a' <= 'Z'
false

julia> 'A' <= 'X' <= 'Z'
true

julia> 'x' - 'a'
23

julia> 'A' + 1
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)

Основы строк

Строковые литералы ограничиваются двойными кавычками или тройными двойными кавычками (не одинарными кавычками):

julia> str = "Hello, world.\n"
"Hello, world.\n"

julia> """Contains "quote" characters"""
"Contains \"quote\" characters"

Длинные строки в строках могут быть разбиты путём предшествования символу новой строки обратной косой чертой (\):

julia> "This is a long \
       line"
"This is a long line"

Если вы хотите извлечь символ из строки, вы обращаетесь к нему по индексу:

julia> str[begin]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[1]
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[end]
'\n': ASCII/Unicode U+000A (category Cc: Other, control)

Многие объекты Julia, включая строки, могут быть проиндексированы целыми числами. Индекс первого элемента (первого символа строки) возвращается firstindex(str), а индекс последнего элемента (символа) — lastindex(str). Ключевые слова begin и end могут использоваться внутри оператора индексирования в качестве сокращений для первых и последних индексов соответственно вдоль заданного измерения. Индексация строк, как и большинство индексаций в Julia, является однооснованной: firstindex всегда возвращает 1 для любого AbstractString. Однако, как мы увидим ниже, lastindex(str) не в общем случае равно length(str) для строки, так как некоторые символы Unicode могут занимать несколько «кодовых единиц».

Вы можете выполнять арифметические и другие операции со end, как и с обычным значением:

julia> str[end-1]
'.': ASCII/Unicode U+002E (category Po: Punctuation, other)

julia> str[end÷2]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

Использование индекса меньше begin (1) или больше end приводит к ошибке:

julia> str[begin-1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [0]
[...]

julia> str[end+1]
ERROR: BoundsError: attempt to access 14-codeunit String at index [15]
[...]

Также можно извлечь подстроку с помощью индексирования диапазоном:

julia> str[4:9]
"lo, wo"

Обратите внимание, что выражения str[k] и str[k:k] не дают одинакового результата:

julia> str[6]
',': ASCII/Unicode U+002C (category Po: Punctuation, other)

julia> str[6:6]
","

Первое является значением отдельного символа типа Char, а второе — значением строки, которая случайно содержит только один символ. В Julia это очень разные вещи.

Индексирование диапазоном создает копию выбранной части исходной строки. Или можно создать представление строки, используя тип SubString. Проще говоря, использование макроса @views для блока кода преобразует все срезы строк в подстроки. Например:

julia> str = "long string"
"long string"

julia> substr = SubString(str, 1, 4)
"long"

julia> typeof(substr)
SubString{String}

julia> @views typeof(str[1:4]) # @views converts slices to SubStrings
SubString{String}

Несколько стандартных функций, таких как chop, chomp или strip, возвращают SubString.

Unicode и UTF-8

Julia полностью поддерживает символы и строки Unicode. Как обсуждалось в выше, в строковых литералах символов коды символов Unicode могут быть представлены с помощью Unicode \u и \U управляющих последовательностей, а также всех стандартных управляющих последовательностей C. Они также могут быть использованы для записи строковых литералов:

julia> s = "\u2200 x \u2203 y"
"∀ x ∃ y"

Отображаются ли эти символы Unicode как экранированные или как специальные символы зависит от настроек локали вашей терминальной и ее поддержки Unicode. Строковые литералы закодированы с помощью кодировки UTF-8. UTF-8 — это кодировка переменной длины, что означает, что не все символы кодируются одинаковым количеством байт («кодовых единиц»). В UTF-8 символы ASCII — т.е. символы с кодами символов меньше 0x80 (128) — кодируются так же, как в ASCII, используя один байт, а символы с кодами символов 0x80 и выше кодируются с использованием нескольких байт — до четырёх на символ.

Индексы строк в Julia относятся к кодовым единицам (= байтам для UTF-8), фиксированным блокам ширины, используемым для кодирования произвольных символов (коды символов). Это означает, что не каждый индекс в String обязательно является допустимым индексом для символа. Если вы обратитесь по такому недопустимому индексу байта в строке, произойдет ошибка:

julia> s[1]
'∀': Unicode U+2200 (category Sm: Symbol, math)

julia> s[2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[3]
ERROR: StringIndexError: invalid index [3], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[4]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

В этом случае символ ∀ — это трёхбайтовый символ, поэтому индексы 2 и 3 недопустимы, а индекс следующего символа — 4; этот следующий допустимый индекс может быть вычислен с помощью nextind(s,1), а следующий индекс после него с помощью nextind(s,4) и так далее.

Поскольку end всегда является последним допустимым индексом в коллекции, end-1 ссылается на недопустимый индекс байта, если предпоследний символ многобайтовый.

julia> s[end-1]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

julia> s[end-2]
ERROR: StringIndexError: invalid index [9], valid nearby indices [7]=>'∃', [10]=>' '
Stacktrace:
[...]

julia> s[prevind(s, end, 2)]
'∃': Unicode U+2203 (category Sm: Symbol, math)

Первый случай работает, потому что последний символ y и пробел являются символами с одним байтом, тогда как end-2 индексирует середину ∃ многобайтового представления. Правильный способ в этом случае – использовать prevind(s, lastindex(s), 2) или, если вы используете это значение для индексирования в s, вы можете написать s[prevind(s, end, 2)] и end расширяется до lastindex(s).

Извлечение подстроки с помощью индексирования по диапазону также ожидает допустимые индексы байтов или выбрасывает ошибку:

julia> s[1:1]
"∀"

julia> s[1:2]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'∀', [4]=>' '
Stacktrace:
[...]

julia> s[1:4]
"∀ "

Из-за кодировок переменной длины количество символов в строке (заданное length(s)) не всегда совпадает с последним индексом. Если вы итерируетесь по индексам от 1 до lastindex(s) и индексируете в s, последовательность символов, возвращаемая при отсутствии ошибок, – это последовательность символов, составляющих строку s. Таким образом length(s) <= lastindex(s), поскольку каждый символ в строке должен иметь свой собственный индекс. Следующий способ итерации по символам s неэффективен и громоздок:

julia> for i = firstindex(s):lastindex(s)
           try
               println(s[i])
           catch
               # ignore the index error
           end
       end
∀

x

∃

y

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

julia> for c in s
           println(c)
       end
∀

x

∃

y

Если вам нужно получить допустимые индексы для строки, вы можете использовать функции nextind и prevind для инкремента/декремента до следующего/предыдущего допустимого индекса, как указано выше. Вы также можете использовать функцию eachindex для итерации по допустимым индексам символов:

julia> collect(eachindex(s))
7-element Vector{Int64}:
  1
  4
  5
  6
  7
 10
 11

Чтобы получить доступ к исходным кодовым единицам (байтам для UTF-8) кодировки, вы можете использовать функцию codeunit(s,i), где индекс i последовательно изменяется от 1 до ncodeunits(s). Функция codeunits(s) возвращает оболочку AbstractVector{UInt8}, которая позволяет получить доступ к этим исходным кодовым единицам (байтам) как к массиву.

Строки в Julia могут содержать недопустимые последовательности кодовых единиц UTF-8. Эта конвенция позволяет рассматривать любую последовательность байтов как String. В таких ситуациях правило состоит в том, что при парсинге последовательности кодовых единиц слева направо символы формируются из самой длинной последовательности 8-битовых кодовых единиц, которая соответствует началу одной из следующих битовых последовательностей (каждая x может быть 0 или 1):

  • 0xxxxxxx;
  • 110xxxxx 10xxxxxx;
  • 1110xxxx 10xxxxxx 10xxxxxx;
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx;
  • 10xxxxxx;
  • 11111xxx.

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

julia> s = "\xc0\xa0\xe2\x88\xe2|"
"\xc0\xa0\xe2\x88\xe2|"

julia> foreach(display, s)
'\xc0\xa0': [overlong] ASCII/Unicode U+0020 (category Zs: Separator, space)
'\xe2\x88': Malformed UTF-8 (category Ma: Malformed, bad data)
'\xe2': Malformed UTF-8 (category Ma: Malformed, bad data)
'|': ASCII/Unicode U+007C (category Sm: Symbol, math)

julia> isvalid.(collect(s))
4-element BitArray{1}:
 0
 0
 0
 1

julia> s2 = "\xf7\xbf\xbf\xbf"
"\U1fffff"

julia> foreach(display, s2)
'\U1fffff': Unicode U+1FFFFF (category In: Invalid, too high)

Мы видим, что первые две кодовые единицы в строке s образуют избыточное кодирование символа пробела. Оно недействительно, но принимается в строке как один символ. Следующие две кодовые единицы образуют допустимое начало трехбайтовой последовательности UTF-8. Однако пятая кодовая единица \xe2 не является ее допустимым продолжением. Поэтому кодовые единицы 3 и 4 также интерпретируются как некорректные символы в этой строке. Аналогично, кодовая единица 5 образует некорректный символ, потому что | не является допустимым продолжением для неё. Наконец, строка s2 содержит слишком большое кодовое значение.

Julia по умолчанию использует кодировку UTF-8, и поддержку новых кодировок можно добавить с помощью пакетов. Например, пакет LegacyStrings.jl реализует типы UTF16String и UTF32String Дополнительные обсуждения других кодировок и способы реализации их поддержки на данный момент выходят за рамки этого документа. Для дальнейшего обсуждения проблем кодирования UTF-8 см. раздел ниже о литералах массивов байтов. Функция transcode предназначена для преобразования данных между различными кодировками UTF-xx, главным образом для работы с внешними данными и библиотеками.

Конкатенация

Одним из наиболее распространенных и полезных операций со строками является конкатенация:

julia> greet = "Hello"
"Hello"

julia> whom = "world"
"world"

julia> string(greet, ", ", whom, ".\n")
"Hello, world.\n"

Важно знать о потенциально опасных ситуациях, таких как конкатенация некорректных строк UTF-8. Результирующая строка может содержать другие символы, чем входные строки, а ее количество символов может быть меньше суммы количества символов конкатенированных строк, например:

julia> a, b = "\xe2\x88", "\x80"
("\xe2\x88", "\x80")

julia> c = string(a, b)
"∀"

julia> collect.([a, b, c])
3-element Vector{Vector{Char}}:
 ['\xe2\x88']
 ['\x80']
 ['∀']

julia> length.([a, b, c])
3-element Vector{Int64}:
 1
 1
 1

Эта ситуация может произойти только для некорректных строк UTF-8. Для корректных строк UTF-8 конкатенация сохраняет все символы в строках и аддитивность длин строк.

Julia также предоставляет * для конкатенации строк:

julia> greet * ", " * whom * ".\n"
"Hello, world.\n"

Хотя * может показаться неожиданным выбором для пользователей языков, которые предоставляют + для конкатенации строк, использование * имеет прецедент в математике, особенно в абстрактной алгебре.

В математике + обычно обозначает коммутативную операцию, где порядок операндов не имеет значения. Примером этого является сложение матриц, где A + B == B + A для любых матриц A и B одинаковой формы. В противоположность этому, * обычно обозначает некоммутативную операцию, где порядок операндов имеет значение. Примером этого является умножение матриц, где, вообще говоря, A * B != B * A. Как и при умножении матриц, конкатенация строк некоммутативна: greet * whom != whom * greet. Таким образом, * является более естественным выбором для инфиксного оператора конкатенации строк, согласуясь с общим математическим использованием.

Более точно, множество всех строк конечной длины S вместе с операцией конкатенации строк * образует свободную полугруппу (S, *). Элементом тождества этого множества является пустая строка, "". В тех случаях, когда свободная полугруппа не коммутативна, операция обычно обозначается символом \cdot, *, или аналогичным символом, а не +, который, как сказано, обычно предполагает коммутативность.

Интерполяция строк

Однако построение строк с помощью конкатенации может стать несколько громоздким. Чтобы уменьшить необходимость этих многословных вызовов к string или многократных умножений, Julia допускает интерполяцию в строковые литералы с помощью $, как в Perl:

julia> greet = "Hello"; whom = "world";

julia> "$greet, $whom.\n"
"Hello, world.\n"

Это более удобочитаемо и удобно и эквивалентно приведенной выше конкатенации строк – система переписывает этот, казалось бы, единственный строковый литерал в вызов string(greet, ", ", whom, ".\n").

Самое короткое полное выражение после $ рассматривается как выражение, значение которого должно быть интерполировано в строку. Таким образом, вы можете интерполировать любое выражение в строку с помощью круглых скобок:

julia> "1 + 2 = $(1 + 2)"
"1 + 2 = 3"

И конкатенация, и интерполяция строк вызывают string для преобразования объектов в строковую форму. Однако string фактически просто возвращает результат print, поэтому новые типы должны добавлять методы к print или show, а не к string.

Большинство объектов, не являющихся AbstractString, преобразуются в строки, очень близкие к тому, как они вводятся в качестве литеральных выражений:

julia> v = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> "v: $v"
"v: [1, 2, 3]"

string является тождеством для AbstractString и AbstractChar значений, поэтому они интерполируются в строки как есть, без кавычек и экранирования:

julia> c = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> "hi, $c"
"hi, x"

Чтобы включить литеральный $ в строковый литерал, экранируйте его обратной косой чертой:

julia> print("I have \$100 in my account.\n")
I have $100 in my account.

Литералы строк в тройных кавычках

Когда строки создаются с использованием тройных кавычек ("""...""") у них есть некоторые специальные особенности, которые могут быть полезными для создания более длинных блоков текста.

Во-первых, строки в тройных кавычках также отступают до уровня самой левой строки. Это полезно для определения строк внутри отступающего кода. Например:

julia> str = """
           Hello,
           world.
         """
"  Hello,\n  world.\n"

В этом случае последняя (пустая) строка перед закрывающей """ устанавливает уровень отступа.

Уровень отступа определяется как самая длинная общая начальная последовательность пробелов или табуляций во всех строках, за исключением строки, следующей за открывающей """, и строк, содержащих только пробелы или табуляции (строка, содержащая закрывающую """ всегда включается). Затем для всех строк, за исключением текста после открывающей """, общая начальная последовательность удаляется (включая строки, содержащие только пробелы и табуляции, если они начинаются с этой последовательности), например:

julia> """    This
         is
           a test"""
"    This\nis\n  a test"

Далее, если открывающая """ следует за новой строкой, новая строка удаляется из результирующей строки.

"""hello"""

эквивалентно

"""
hello"""

но

"""

hello"""

будет содержать литеральную новую строку в начале.

Удаление новой строки выполняется после отступа. Например:

julia> """
         Hello,
         world."""
"Hello,\nworld."

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

julia> """
         Averylong\
         word"""
"Averylongword"

Конечные пробелы остаются неизменными.

Литералы строк в тройных кавычках могут содержать " символы без экранирования.

Обратите внимание, что разрывы строк в строковых литералах, будь то одинарные или тройные, приводят к символу новой строки (LF) \n в строке, даже если ваш редактор использует сочетание возврата каретки \r (CR) или CRLF для завершения строк. Чтобы включить CR в строку, используйте явное экранирование \r; например, вы можете ввести литеральную строку "a CRLF line ending\r\n".

Общие операции

Для лексикографического сравнения строк можно использовать стандартные операторы сравнения:

julia> "abracadabra" < "xylophone"
true

julia> "abracadabra" == "xylophone"
false

julia> "Hello, world." != "Goodbye, world."
true

julia> "1 + 2 = 3" == "1 + 2 = $(1 + 2)"
true

Для поиска индекса определенного символа можно использовать функции findfirst и findlast:

julia> findfirst('o', "xylophone")
4

julia> findlast('o', "xylophone")
7

julia> findfirst('z', "xylophone")

Для начала поиска символа с заданным смещением можно использовать функции findnext и findprev:

julia> findnext('o', "xylophone", 1)
4

julia> findnext('o', "xylophone", 5)
7

julia> findprev('o', "xylophone", 5)
4

julia> findnext('o', "xylophone", 8)

Для проверки, содержится ли подстрока в строке, можно использовать функцию occursin:

julia> occursin("world", "Hello, world.")
true

julia> occursin("o", "Xylophon")
true

julia> occursin("a", "Xylophon")
false

julia> occursin('o', "Xylophon")
true

Последний пример показывает, что occursin также может искать символьные литералы.

Две другие полезные функции для работы со строками — repeat и join:

julia> repeat(".:Z:.", 10)
".:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:..:Z:."

julia> join(["apples", "bananas", "pineapples"], ", ", " and ")
"apples, bananas and pineapples"

Некоторые другие полезные функции включают:

  • firstindex(str) возвращает минимальный (байтовый) индекс, который можно использовать для индексирования в str (всегда 1 для строк, необязательно для других контейнеров).
  • lastindex(str) возвращает максимальный (байтовый) индекс, который можно использовать для индексирования в str.
  • length(str) количество символов в str.
  • length(str, i, j) количество допустимых индексов символов в str от i до j.
  • ncodeunits(str) количество кодовых единиц в строке.
  • codeunit(str, i) возвращает значение кодовой единицы в строке str по индексу i.
  • thisind(str, i) для произвольного индекса в строке находит первый индекс символа, на который указывает индекс.
  • nextind(str, i, n=1) находит начало n-го символа, начиная после индекса i.
  • prevind(str, i, n=1) находит начало n-го символа, начиная перед индексом i.

Нестандартные строковые литералы

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

Регулярные выражения, литералы массивов байтов и литералы номеров версий, как описано ниже, являются примерами нестандартных строковых литералов. Пользователи и пакеты также могут определять новые нестандартные строковые литералы. Более подробная информация приведена в разделе Метапрограммирование.

Регулярные выражения

Иногда вам нужно не точное совпадение строки, а определенный шаблон. Например, предположим, что вы пытаетесь извлечь одну дату из большого текстового файла. Вы не знаете, какая это дата (поэтому вы ее и ищете), но знаете, что она будет выглядеть примерно так: YYYY-MM-DD. Регулярные выражения позволяют задать эти шаблоны и искать их.

Julia использует версию 2 совместимых с Perl регулярных выражений (regex), предоставляемых библиотекой PCRE (см. описание синтаксиса PCRE2 для получения дополнительной информации). Регулярные выражения связаны со строками двумя способами: очевидная связь заключается в том, что регулярные выражения используются для поиска регулярных шаблонов в строках; другая связь заключается в том, что регулярные выражения сами вводятся как строки, которые анализируются в конечный автомат, который может использоваться для эффективного поиска шаблонов в строках. В Julia регулярные выражения вводятся с помощью нестандартных строковых литералов, предваряемых различными идентификаторами, начинающимися с r. Самый базовый литерал регулярного выражения без включенных опций использует r"...":

julia> re = r"^\s*(?:#|$)"
r"^\s*(?:#|$)"

julia> typeof(re)
Regex

Для проверки соответствия regex строке используйте occursin:

julia> occursin(r"^\s*(?:#|$)", "not a comment")
false

julia> occursin(r"^\s*(?:#|$)", "# a comment")
true

Как видно здесь, occursin просто возвращает true или false, указывая, есть ли совпадение заданного regex в строке. Однако, как правило, необходимо знать не только то, совпала ли строка, но и как она совпала. Для получения этой информации о совпадении используйте функцию match:

julia> match(r"^\s*(?:#|$)", "not a comment")

julia> match(r"^\s*(?:#|$)", "# a comment")
RegexMatch("#")

Если регулярное выражение не совпадает с заданной строкой, match возвращает nothing — специальное значение, которое ничего не выводит в интерактивном режиме. Помимо того, что оно ничего не выводит, это совершенно нормальное значение, и вы можете программно проверить его:

m = match(r"^\s*(?:#|$)", line)
if m === nothing
    println("not a comment")
else
    println("blank or comment")
end

Если регулярное выражение совпадает, возвращаемое значение match является объектом RegexMatch. Эти объекты записывают, как выражение совпадает, включая подстроку, с которой совпадает шаблон, и любые захваченные подстроки, если они есть. В этом примере захватывается только часть подстроки, которая совпадает, но, возможно, мы хотим захватить любой текст, не содержащий пробелов, после символа комментария. Мы могли бы сделать следующее:

julia> m = match(r"^\s*(?:#\s*(.*?)\s*$)", "# a comment ")
RegexMatch("# a comment ", 1="a comment")

При вызове match можно указать индекс, с которого начать поиск. Например:

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",1)
RegexMatch("1")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",6)
RegexMatch("2")

julia> m = match(r"[0-9]","aaaa1aaaa2aaaa3",11)
RegexMatch("3")

Из объекта RegexMatch можно извлечь следующую информацию:

  • вся совпавшая подстрока: m.match
  • захваченные подстроки как массив строк: m.captures
  • смещение, с которого начинается всё совпадение: m.offset
  • смещения захваченных подстрок как вектор: m.offsets

Когда capture не совпадает, вместо подстроки m.captures содержит nothing в этой позиции, а m.offsets имеет смещение ноль (вспомните, что индексы в Julia основаны на 1, поэтому смещение ноль в строке недопустимо). Вот пара несколько искусственных примеров:

julia> m = match(r"(a|b)(c)?(d)", "acd")
RegexMatch("acd", 1="a", 2="c", 3="d")

julia> m.match
"acd"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 "c"
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 2
 3

julia> m = match(r"(a|b)(c)?(d)", "ad")
RegexMatch("ad", 1="a", 2=nothing, 3="d")

julia> m.match
"ad"

julia> m.captures
3-element Vector{Union{Nothing, SubString{String}}}:
 "a"
 nothing
 "d"

julia> m.offset
1

julia> m.offsets
3-element Vector{Int64}:
 1
 0
 2

Удобно, когда captures возвращаются как массив, поэтому можно использовать синтаксис деструктурирования для привязки их к локальным переменным. Для удобства объект RegexMatch реализует методы итератора, которые передаются в поле captures, поэтому вы можете напрямую деструктурировать объект совпадения:

julia> first, second, third = m; first
"a"

Доступ к capture также можно получить, индексируя объект RegexMatch с помощью номера или имени группы capture:

julia> m=match(r"(?<hour>\d+):(?<minute>\d+)","12:45")
RegexMatch("12:45", hour="12", minute="45")

julia> m[:minute]
"45"

julia> m[2]
"45"

Capture можно ссылаться в строке подстановки при использовании replace с помощью \n для ссылки на n-ю группу capture и добавлением s к строке подстановки. Группа capture 0 ссылается на весь объект совпадения. Наименованные группы capture могут быть использованы в подстановке с \g<groupname> . Например:

julia> replace("first second", r"(\w+) (?<agroup>\w+)" => s"\g<agroup> \1")
"second first"

Нумерованные группы capture также могут быть использованы как \g<n> для разбора, как в:

julia> replace("a", r"." => s"\g<0>1")
"a1"

Вы можете изменить поведение регулярных выражений с помощью комбинации флагов i, m, s и x после закрывающей двойной кавычки. Эти флаги имеют такое же значение, как и в Perl, как объяснено в этом фрагменте из perlre manpage:

i   Do case-insensitive pattern matching.

    If locale matching rules are in effect, the case map is taken
    from the current locale for code points less than 255, and
    from Unicode rules for larger code points. However, matches
    that would cross the Unicode rules/non-Unicode rules boundary
    (ords 255/256) will not succeed.

m   Treat string as multiple lines.  That is, change "^" and "$"
    from matching the start or end of the string to matching the
    start or end of any line anywhere within the string.

s   Treat string as single line.  That is, change "." to match any
    character whatsoever, even a newline, which normally it would
    not match.

    Used together, as r""ms, they let the "." match any character
    whatsoever, while still allowing "^" and "$" to match,
    respectively, just after and just before newlines within the
    string.

x   Tells the regular expression parser to ignore most whitespace
    that is neither backslashed nor within a character class. You
    can use this to break up your regular expression into
    (slightly) more readable parts. The '#' character is also
    treated as a metacharacter introducing a comment, just as in
    ordinary code.

Например, следующее регулярное выражение имеет все три флага включенными:

julia> r"a+.*b+.*d$"ism
r"a+.*b+.*d$"ims

julia> match(r"a+.*b+.*d$"ism, "Goodbye,\nOh, angry,\nBad world\n")
RegexMatch("angry,\nBad world")

Литерал r"..." создается без интерполяции и избегания символов (за исключением кавычек ", которые все еще необходимо экранировать). Вот пример, демонстрирующий разницу со стандартными строковыми литералами:

julia> x = 10
10

julia> r"$x"
r"$x"

julia> "$x"
"10"

julia> r"\x"
r"\x"

julia> "\x"
ERROR: syntax: invalid escape sequence

Также поддерживаются строковые регулярные выражения в тройных кавычках, вида r"""...""". (Они могут быть удобными для регулярных выражений, содержащих кавычки или новые строки).

Конструктор Regex() может быть использован для создания корректной строки регулярного выражения программно. Это позволяет использовать содержимое строковых переменных и другие операции со строками при построении строки регулярного выражения. Любой из приведенных выше кодов регулярных выражений может быть использован в единственном строковом аргументе Regex(). Вот некоторые примеры:

julia> using Dates

julia> d = Date(1962,7,10)
1962-07-10

julia> regex_d = Regex("Day " * string(day(d)))
r"Day 10"

julia> match(regex_d, "It happened on Day 10")
RegexMatch("Day 10")

julia> name = "Jon"
"Jon"

julia> regex_name = Regex("[\"( ]\\Q$name\\E[\") ]")  # interpolate value of name
r"[\"( ]\QJon\E[\") ]"

julia> match(regex_name, " Jon ")
RegexMatch(" Jon ")

julia> match(regex_name, "[Jon]") === nothing
true

Обратите внимание на использование escape-последовательности \Q...\E. Все символы между \Q и \E интерпретируются как литеральные символы. Это удобно для сопоставления символов, которые в противном случае были бы метасимволами regex. Однако необходимо проявлять осторожность при использовании этой функции совместно с интерполяцией строк, так как интерполированная строка сама может содержать последовательность \E, неожиданно завершая литеральное сопоставление. Входные данные пользователя необходимо очищать перед включением в regex.

Литералы массивов байтов

Еще один полезный нестандартный строковый литерал — это литерал строки массива байтов: b"...". Этот вид позволяет использовать строковую нотацию для выражения неизменяемых литеральных массивов байтов — то есть массивов значений UInt8. Тип этих объектов — CodeUnits{UInt8, String}. Правила для литералов массивов байтов следующие:

  • Символы ASCII и ASCII-экранирование создают один байт.
  • \x и восьмеричные escape-последовательности генерируют байт, соответствующий значению escape.
  • Unicode-escape-последовательности генерируют последовательность байтов, кодирующую этот код символа в UTF-8.

Между этими правилами существует некоторое перекрытие, поскольку поведение \x и восьмеричных эскейпов меньше 0x80 (128) охватывается первыми двумя правилами, но здесь эти правила совпадают. Вместе эти правила позволяют легко использовать символы ASCII, произвольные байтовые значения и последовательности UTF-8 для создания массивов байтов. Вот пример с использованием всех трех:

julia> b"DATA\xff\u2200"
8-element Base.CodeUnits{UInt8, String}:
 0x44
 0x41
 0x54
 0x41
 0xff
 0xe2
 0x88
 0x80

Строка ASCII «DATA» соответствует байтам 68, 65, 84, 65. \xff генерирует одиночный байт 255. Юникод-эскейп \u2200 закодирован в UTF-8 как три байта 226, 136, 128. Обратите внимание, что полученный массив байтов не соответствует допустимой строке UTF-8:

julia> isvalid("DATA\xff\u2200")
false

Как было упомянуто, тип CodeUnits{UInt8, String} ведет себя как массив UInt8 только для чтения, и если вам нужна стандартная векторная структура, вы можете преобразовать её используя Vector{UInt8}:

julia> x = b"123"
3-element Base.CodeUnits{UInt8, String}:
 0x31
 0x32
 0x33

julia> x[1]
0x31

julia> x[1] = 0x32
ERROR: CanonicalIndexError: setindex! not defined for Base.CodeUnits{UInt8, String}
[...]

julia> Vector{UInt8}(x)
3-element Vector{UInt8}:
 0x31
 0x32
 0x33

Также обратите внимание на существенное различие между \xff и \uff: первый эскейп кодирует байт 255, в то время как второй эскейп представляет кодовый пункт 255, который кодируется как два байта в UTF-8:

julia> b"\xff"
1-element Base.CodeUnits{UInt8, String}:
 0xff

julia> b"\uff"
2-element Base.CodeUnits{UInt8, String}:
 0xc3
 0xbf

Литералы символов используют такое же поведение.

Для кодовых пунктов меньше \u80, UTF-8 кодирование каждого кодового пункта является просто одиночным байтом, созданным соответствующим \x эскейпом, поэтому различие можно безопасно игнорировать. Для эскейпов \x80 по \xff в сравнении с \u80 по \uff, однако, есть существенное различие: первые эскейпы кодируют все одиночные байты, которые — если не следуют за очень специфическими байтами продолжения — не образуют валидные данные UTF-8, в то время как последние эскейпы представляют все кодовые пункты Unicode с кодированием в два байта.

Если это всё слишком сложно, попробуйте прочитать "The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets". Это отличное введение в Unicode и UTF-8 и может помочь устранить некоторые неясноты по этому вопросу.

Литералы версий

Версии можно легко выразить с помощью нестандартных строковых литералов следующего вида v"...". Литералы номеров версий создают объекты VersionNumber, которые следуют спецификациям семантической версии, и, следовательно, состоят из числовых значений major, minor и patch, а также альфа-числовых аннотаций pre-release и build. Например, v"0.2.1-rc1+win64" разбивается на большую версию 0, малую версию 2, исправленную версию 1, пре-релиз rc1 и сборку win64. При вводе литерала версии все, кроме номера основной версии, необязательно, поэтому, например, v"0.2" эквивалентно v"0.2.0" (с пустыми аннотациями pre-release/build), v"2" эквивалентно v"2.0.0", и так далее.

Объекты VersionNumber в основном полезны для легкого и правильного сравнения двух (или более) версий. Например, константа VERSION содержит номер версии Julia в виде объекта VersionNumber, и поэтому можно определить некоторое поведение, специфичное для версии, с помощью простых операторов, как:

if v"0.2" <= VERSION < v"0.3-"
    # do something specific to 0.2 release series
end

Обратите внимание, что в приведенном выше примере используется нестандартный номер версии v"0.3-" с заключительным -: эта запись является расширением Julia стандартной записи и используется для обозначения версии, которая ниже любой версии 0.3 выпуска, включая все ее предварительные версии. Таким образом, в приведенном выше примере код будет выполняться только со стабильными версиями 0.2, исключая такие версии, как v"0.3.0-rc1". Для того, чтобы также разрешить нестабильные (т.е. предварительные) версии 0.2 , проверка нижней границы должна быть изменена следующим образом: v"0.2-" <= VERSION.

Другое нестандартное расширение спецификации версии позволяет использовать заключительный + для выражения верхнего предела версий сборки, например, VERSION > v"0.2-rc1+" может использоваться для обозначения любой версии выше 0.2-rc1 и любой её сборки: она вернёт false для версии v"0.2-rc1+win64" и true для v"0.2-rc2".

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

Помимо использования в константе VERSION, объекты VersionNumber широко используются в модуле Pkg, для указания версий пакетов и их зависимостей.

Необработанные строковые литералы

Необработанные строки без интерполяции или разбора могут быть выражены с помощью нестандартных строковых литералов вида raw"...". Необработанные строковые литералы создают обычные String объекты, которые содержат заключённое содержимое точно так, как оно было введено, без интерполяции или разбора. Это полезно для строк, содержащих код или разметку на других языках, которые используют $ или \ в качестве специальных символов.

Исключением является то, что кавычки всё ещё должны быть экранированы, например, raw"\"" эквивалентно "\"". Чтобы можно было выразить все строки, обратные слэши также должны быть экранированы, но только когда они появляются непосредственно перед символом кавычки:

julia> println(raw"\\ \\\"")
\\ \"

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

Аннотированные строки

API для аннотированных строк считается экспериментальным и может меняться между версиями Julia.

Иногда полезно хранить метаданные, относящиеся к областям строки. AnnotatedString обертывает другую строку и позволяет аннотировать её области с помощью именованных значений (:label => value). Все общие операции со строками применяются к базовой строке. Однако, когда это возможно, информация об оформлении сохраняется. Это означает, что вы можете манипулировать AnnotatedString — извлекая подстроки, добавляя отступы, конкатенируя их с другими строками — и метаданные аннотаций будут «следования за вами».

Этот тип строк является фундаментальным для модуля StyledStrings stdlib, который использует аннотации с метками :face для хранения стилистической информации.

При конкатенации AnnotatedString, используйте annotatedstring вместо string, если хотите сохранить аннотации строки.

julia> str = Base.AnnotatedString("hello there",
               [(1:5, :word, :greeting), (7:11, :label, 1)])
"hello there"

julia> length(str)
11

julia> lpad(str, 14)
"   hello there"

julia> typeof(lpad(str, 7))
Base.AnnotatedString{String}

julia> str2 = Base.AnnotatedString(" julia", [(2:6, :face, :magenta)])
" julia"

julia> Base.annotatedstring(str, str2)
"hello there julia"

julia> str * str2 == Base.annotatedstring(str, str2) # *-concatenation still works
true

К аннотациям AnnotatedString можно получить доступ и изменить их с помощью функций annotations и annotate!.

© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/manual/strings/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API