Строки
Строки — это конечные последовательности символов. Конечно, настоящие трудности возникают, когда спрашивают, что такое символ. Символы, с которыми знакомы носители английского языка, — это буквы 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.
Символы
Значение 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, а также всех стандартных последовательностей экранирования 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; -
110xxxxx10xxxxxx; -
1110xxxx10xxxxxx10xxxxxx; -
11110xxx10xxxxxx10xxxxxx10xxxxxx; -
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, $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
В случае, если какой-то захват не соответствует, вместо подстроки 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, как объясняется в этом отрывке из manpage 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
Также поддерживаются строковые выражения regex в тройных кавычках, имеющие вид r"""...""" (могут быть удобны для регулярных выражений, содержащих кавычки или новые строки).
Конструктор Regex() может использоваться для создания корректной строки regex программно. Это позволяет использовать содержимое строковых переменных и другие строковые операции при построении строки 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 интерпретируются как буквальные символы (после интерполяции строк). Эта последовательность экранирования может быть полезна при интерполяции, возможно, вредоносного, пользовательского ввода.
Литералы массивов байтов
Еще один полезный нестандартный строковый литерал — это литерал массива байтов: 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. Последовательность экранирования Unicode \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 последовательностью escape, поэтому различие можно безопасно игнорировать. Для последовательностей escape \x80 по \xff в сравнении с \u80 по \uff, однако, есть существенная разница: первые последовательности escape кодируют все одиночные байты, которые — если не следуют за очень специфическими продолжениями байтов — не образуют допустимые данные UTF-8, тогда как последние последовательности escape представляют все коды Unicode с двухбайтовыми кодировками.
Если это все чрезвычайно запутанно, попробуйте прочитать «Абсолюльный минимум, который каждый разработчик программного обеспечения должен знать об Unicode и наборах символов». Это отличное введение в 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–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/strings/