Spec-Zone.ru › Julia 1.10

Строки

Строки — это конечные последовательности символов. Конечно, настоящая проблема возникает, когда задается вопрос о том, что такое символ. Символы, с которыми знакомы носители английского языка, — это буквы 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, основано на 1: 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 escape-последовательностями, а также всеми стандартными escape-последовательностями 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/LF для завершения строк. Чтобы включить 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

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

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

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

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

К захватам также можно получить доступ, индексируя объект RegexMatch числом или именем группы захвата:

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"

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

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

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

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

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

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 может быть использован внутри единственного строкового аргумента для 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

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

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

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

  • Символы ASCII и ASCII-экранирования создают один байт.
  • \x и восьмеричные последовательности экранирования создают байт, соответствующий значению экранирования.
  • Последовательности экранирования Unicode создают последовательность байтов, кодирующих этот код символа в 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-коды с кодировкой в два байта.

Если это слишком сложно, попробуйте прочитать «Абсолюльный минимум, который каждый разработчик ПО должен знать о Юникоде и наборах символов». Это отличное введение в Юникод и UTF-8 и может помочь устранить некоторые неясностей по этому вопросу.

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

Номера версий можно легко выразить с помощью нестандартных строковых литералов в формате v"...". Литералы номеров версий создают объекты VersionNumber, которые следуют спецификациям семантического векторирования, и поэтому состоят из основных, второстепенных и добавочных числовых значений, за которыми следуют пре-релизные и сборкочные альфа-цифровые аннотации. Например, v"0.2.1-rc1+win64" разбивается на основную версию 0, второстепенную версию 2, добавочную версию 1, пре-релиз rc1 и сборку win64. При вводе литерала версии всё, кроме основного номера версии, является необязательным, поэтому, например, v"0.2" эквивалентно v"0.2.0" (с пустыми аннотациями пре-релиза/сборки), 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"\\ \\\"")
\\ \"

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

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

Spec-Zone.ru

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