Strings
Строки — это конечные последовательности символов. Конечно, настоящая проблема возникает, когда кто-то спрашивает, что такое символ. Символы, с которыми знакомы носители английского языка, — это буквы A
, B
, C
и т. д., а также цифры и распространенные знаки препинания. Эти символы стандартизированы вместе с отображением на целочисленные значения от 0 до 127 по стандарту ASCII. Конечно, существует много других символов, используемых в неанглийских языках, включая варианты символов ASCII с акцентами и другими модификациями, родственные письменности, такие как кириллица и греческий, и письменности, совершенно не относящиеся к ASCII и английскому, включая арабский, китайский, иврит, хинди, японский и корейский. Стандарт Unicode решает сложности того, что именно является символом, и в целом принимается как окончательный стандарт, решающий эту проблему. В зависимости от ваших потребностей вы можете либо полностью игнорировать эти сложности и просто притвориться, что существуют только символы ASCII, либо вы можете написать код, который может обрабатывать любые символы или кодировки, с которыми можно столкнуться при работе с текстом, не относящимся к ASCII. Julia упрощает работу с обычным текстом ASCII и делает обработку Unicode такой же простой и эффективной, как это возможно. В частности, вы можете написать код строк в стиле C для обработки строк ASCII, и он будет работать, как ожидалось, как с точки зрения производительности, так и семантики. Если такой код сталкивается с текстом, не относящимся к ASCII, он завершится с ясным сообщением об ошибке, а не молча введет испорченные результаты. Когда это происходит, модификация кода для обработки не-ASCII данных является простой задачей.
Есть несколько примечательных высокоуровневых особенностей строк в Julia:
- Встроенный тип данных для строк (и строковых литералов) в Julia - это
String
. Он поддерживает полный диапазон Unicode символов через кодировку UTF-8. (Функцияtranscode
предоставляется для преобразования в/из других кодировок Unicode.) - Все типы строк являются подтипами абстрактного типа
AbstractString
, и внешние пакеты определяют дополнительные подтипыAbstractString
(например, для других кодировок). Если вы определяете функцию, ожидающую аргумент строки, вы должны объявить тип какAbstractString
, чтобы принимать любой тип строки. - Как и C и Java, но в отличие от большинства динамических языков, Julia имеет первоклассный тип для представления одного символа, называемый
AbstractChar
. ВстроенныйChar
подтипAbstractChar
является 32-битным примитивным типом, который может представлять любой символ Unicode (и который основан на кодировке UTF-8). - Как и в Java, строки неизменяемы: значение объекта
AbstractString
не может быть изменено. Чтобы создать другое строковое значение, вы создаете новую строку из частей других строк. - Концептуально, строка является частичной функцией от индексов к символам: для некоторых значений индекса не возвращается значение символа, и вместо этого выбрасывается исключение. Это позволяет эффективно индексировать строки по байтовому индексу закодированного представления, а не по индексу символа, что не может быть реализовано как эффективно, так и просто для кодировок переменной ширины строк Unicode.
Characters
Значение Char
представляет собой один символ: это просто примитивный тип размером 32 бита с особым литеральным представлением и соответствующими арифметическими поведениями, и которое может быть преобразовано в числовое значение, представляющее Unicode code point. (Пакеты Julia могут определять другие подтипы AbstractChar
, например, для оптимизации операций для других text encodings.) Вот как вводятся и отображаются значения 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)
Джулия использует настройки локали и языка вашей системы, чтобы определить, какие символы можно выводить как есть, а какие необходимо выводить с использованием общих, экранированных форм ввода \u
или \U
. В дополнение к этим формам Unicode, также можно использовать все C's traditional escaped input forms.
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)
String Basics
Строковые литералы ограничены двойными кавычками или тройными двойными кавычками (не одинарными кавычками):
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 and UTF-8
Julia полностью поддерживает символы и строки Unicode. В виде discussed above, в символьных литералах кодовые точки 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 см. раздел ниже о byte array literals. Функция transcode
предоставляется для преобразования данных между различными кодировками UTF-xx, в первую очередь для работы с внешними данными и библиотеками.
Concatenation
Одной из самых распространенных и полезных операций со строками является конкатенация:
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> greet * ", " * whom * ".\n"
"Hello, world.\n"
Хотя *
может показаться неожиданным выбором для пользователей языков, которые используют +
для конкатенации строк, такое использование *
имеет прецеденты в математике, особенно в абстрактной алгебре.
В математике +
обычно обозначает коммутативную операцию, где порядок операндов не имеет значения. Примером этого является сложение матриц, где A + B == B + A
для любых матриц A
и B
, имеющих одинаковую форму. В отличие от этого, *
обычно обозначает некоммутативную операцию, где порядок операндов имеет значение. Примером этого является умножение матриц, где в общем случае A * B != B * A
. Как и в случае умножения матриц, конкатенация строк является некоммутативной: greet * whom != whom * greet
. Таким образом, *
является более естественным выбором для инфиксного оператора конкатенации строк, что соответствует общему математическому использованию.
Более точно, множество всех строк конечной длины S вместе с оператором конкатенации строк *
образует free monoid (S, *
). Элементом идентичности этого множества является пустая строка, ""
. Когда свободный монад не коммутативен, операция обычно представляется как \cdot
, *
или аналогичный символ, а не +
, что, как было сказано, обычно подразумевает коммутативность.
Interpolation
Конструирование строк с использованием конкатенации может стать немного громоздким. Чтобы уменьшить необходимость в этих многословных вызовах к 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
, поэтому новые типы должны добавлять методы к 4d61726b646f776e2e436f64652822222c20227072696e742229_40726566
или 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.
Triple-Quoted String Literals
Когда строки создаются с использованием тройных кавычек ("""..."""
), они имеют особое поведение, которое может быть полезным для создания более длинных блоков текста.
Во-первых, строки в тройных кавычках также выравниваются по уровню наименее отступленной строки. Это полезно для определения строк внутри кода, который имеет отступ. Например:
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"
.
Common Operations
Вы можете лексикографически сравнивать строки, используя стандартные операторы сравнения:
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)
количество code units в строке.codeunit(str, i)
возвращает значение единицы кода в строкеstr
по индексуi
.thisind(str, i)
дан произвольный индекс в строке, найдите первый индекс символа, на который указывает этот индекс.nextind(str, i, n=1)
найдите началоn
-го символа, начиная с индексаi
.prevind(str, i, n=1)
найдите началоn
-го символа, начиная перед индексомi
.
Non-Standard String Literals
Существуют ситуации, когда вы хотите создать строку или использовать семантику строк, но поведение стандартного конструктора строк не совсем соответствует вашим требованиям. Для таких ситуаций Julia предоставляет нестандартные строковые литералы. Нестандартный строковый литерал выглядит как обычный строковый литерал в двойных кавычках, но сразу же предшествует идентификатор, и может вести себя иначе, чем обычный строковый литерал.
Regular expressions, byte array literals, и version number literals, как описано ниже, являются примерами нестандартных строковых литералов. Пользователи и пакеты также могут определять новые нестандартные строковые литералы. Дополнительная документация представлена в разделе Metaprogramming.
Regular Expressions
Иногда вы не ищете точную строку, а определенный шаблон. Например, предположим, что вы пытаетесь извлечь одну дату из большого текстового файла. Вы не знаете, какая это дата (поэтому вы ее ищете), но вы знаете, что она будет выглядеть как YYYY-MM-DD
. Регулярные выражения позволяют вам задавать эти шаблоны и искать их.
Julia использует версию 2 совместимых с Perl регулярных выражений (regex), предоставляемую библиотекой PCRE (см. PCRE2 syntax description для получения дополнительной информации). Регулярные выражения связаны со строками двумя способами: очевидная связь заключается в том, что регулярные выражения используются для поиска регулярных шаблонов в строках; другая связь заключается в том, что регулярные выражения сами по себе вводятся в виде строк, которые разбираются в конечный автомат, который может быть использован для эффективного поиска шаблонов в строках. В Julia регулярные выражения вводятся с использованием нестандартных строковых литералов, предшествующих различными идентификаторами, начинающимися с r
. Самый простой литерал регулярного выражения без включенных опций просто использует r"..."
:
julia> re = r"^\s*(?:#|$)"
r"^\s*(?:#|$)"
julia> typeof(re)
Regex
Чтобы проверить, соответствует ли регулярное выражение строке, используйте occursin
:
julia> occursin(r"^\s*(?:#|$)", "not a comment")
false
julia> occursin(r"^\s*(?:#|$)", "# a comment")
true
Как видно здесь, occursin
просто возвращает true или false, указывая, произошло ли совпадение для данного регулярного выражения в строке. Однако обычно хочется знать не только, совпала ли строка, но и как она совпала. Чтобы зафиксировать эту информацию о совпадении, используйте функцию 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, поэтому нулевое смещение в строке недопустимо). Вот пара несколько искусственных примеров:
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, как объясняется в этом отрывке из 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
интерпретируются как буквальные символы. Это удобно для сопоставления символов, которые в противном случае были бы метасимволами регулярных выражений. Однако следует проявлять осторожность при использовании этой функции вместе с интерполяцией строк, так как интерполированная строка может содержать последовательность \E
, неожиданно завершая буквальное сопоставление. Входные данные пользователей необходимо очищать перед включением в регулярное выражение.
Byte Array Literals
Еще один полезный нестандартный строковый литерал — это литерал байтового массива: b"..."
. Эта форма позволяет использовать строковую нотацию для выражения только для чтения литералов байтовых массивов — т.е. массивов значений UInt8
. Тип этих объектов — CodeUnits{UInt8, String}
. Правила для литералов байтовых массивов следующие:
- ASCII-символы и ASCII-экранирования создают один байт.
\x
и восьмеричные escape-последовательности создают байт, соответствующий значению escape.- Unicode escape sequences производят последовательность байтов, кодирующих эту кодовую точку в 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
: первая escape-последовательность кодирует байт 255, в то время как вторая escape-последовательность представляет кодовую точку 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 и может помочь облегчить некоторые недоразумения по этому вопросу.
Version Number Literals
Номера версий можно легко выразить с помощью нестандартных строковых литералов в форме v"..."
. Литералы номеров версий создают объекты VersionNumber
, которые соответствуют спецификациям semantic versioning, и, следовательно, состоят из основных, второстепенных и патч-числовых значений, за которыми следуют аннотации предварительного релиза и сборки в алфавитно-цифровом формате. Например, 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 Literals
Сырые строки без интерполяции или распаковки могут быть выражены с помощью нестандартных строковых литералов в форме raw"..."
. Сырые строковые литералы создают обычные объекты String
, которые содержат заключенное содержимое точно так, как оно было введено, без интерполяции или распаковки. Это полезно для строк, которые содержат код или разметку на других языках, которые используют $
или \
в качестве специальных символов.
Исключение заключается в том, что кавычки все еще должны быть экранированы, например, raw"\""
эквивалентно "\""
. Чтобы сделать возможным выражение всех строк, обратные слэши также должны быть экранированы, но только когда они появляются непосредственно перед символом кавычки:
julia> println(raw"\\ \\\"")
\\ \"
Обратите внимание, что первые два обратных слэша появляются в выводе без изменений, так как они не предшествуют символу кавычки. Однако следующий символ обратного слэша экранирует следующий за ним обратный слэш, а последний обратный слэш экранирует кавычку, так как эти обратные слэши появляются перед кавычкой.
Annotated Strings
API для AnnotatedStrings считается экспериментальным и может изменяться между версиями Julia.
Иногда полезно иметь возможность хранить метаданные, относящиеся к регионам строки. AnnotatedString
оборачивает другую строку и позволяет аннотировать ее регионы с помощью помеченных значений (:label => value
). Все общие операции со строками применяются к основной строке. Однако, когда это возможно, информация о стиле сохраняется. Это означает, что вы можете манипулировать 4d61726b646f776e2e436f64652822222c2022416e6e6f7461746564537472696e672229_4072656620426173652e416e6e6f7461746564537472696e67
— брать подстроки, добавлять к ним пробелы, конкатенировать их с другими строками — и аннотации метаданных "поедут вместе с ними".
Этот тип строки является фундаментальным для 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!
.