Julia 中的正则表达式¶
简介¶
正则表达式(regex)是根据模式搜索、提取和处理文本的多功能工具。通过正则表达式,您可以完成检查电子邮件格式、提取电话号码或分析文本数据等任务。在 Julia 中,正则表达式因其易于集成和简洁的语法而特别有用。Julia 不需要额外的模块来处理 regex(与其他语言不同),它使用r"..."
前缀来创建模板,这使得代码直观易读。本教材将向您介绍如何在 Julia 中使用正则表达式,重点是实际示例和语言特点。
Julia 中的正则表达式语法¶
Julia 使用PCRE 语法,该语法支持丰富的功能。让我们来看看正则表达式的基本元素及其在 Julia 中的使用。
语法的基本元素¶
- 字面:正则字符(如
a
,b
,1
)在文本中按原样搜索。例如,r"cat"
与cat
匹配。 - etacharacters:
-
.
是换行符以外的任何字符。例如,r"c.t"
匹配cat
,cot
, 但不匹配c\nt
。 -^
- 行首:r"^hello"
只能在行首找到hello
。$
— конец строки:r"world$"
只能在行尾找到world
。 -\
- 转义字符:r"\."
查找点字符,而不是任何字符。r"\^"
专门查找^
字符。
- 量词:
-
*
- 零个或多个:r"a*"
匹配""
,a
,aa
等。 -+
- 一个或多个:r"a+"
匹配a
,aa
, 但不匹配""
。 -?
- 零个或一个:r"colou?r"
匹配color
和colour
。 -{n,m}
- 重复范围:r"a{2,4}"
匹配aa
,aaa
,aaaa
。 - 字符类:
-
[abc]
- 其中一个字符:r"[abc]"
匹配a
,b
或c
。 -[a-z]
- 范围:r"[a-z]+"
将查找任何小写单词。 -[^abc]
- 否定:r"[^abc]"
除a
,b
,c
之外的任何字符。 -\d
- 数字:r"\d+"
将查找数字,如123
。 -\w
- 字母、数字或_
:r"\w+"
将查找单词,如hello123
. -\s
- 空格字符:r"\s+"
将查找空格或制表符。 - 分组:
-
()
- 捕捉部分模式:r"(\d+)-(\d+)"
将从字符串12-34
中选择数字12
和34
并保存。 -(?:...)
- 分组,但不会 "保存 "结果。非捕获分组有助于简化表达式的结构。
regex = r"(?:abc)+(\d+)(?:def)+(\&+)"
text = "abcabc123defdefdef&&&&&"
match(regex,text)[1] # 将返回 "123"。
match(regex,text)[2] # 返回 "&&&&".
Julia 中的函数¶
Julia 提供了处理正则表达式的便捷方法:
match(r"шаблон", строка)
:查找第一个匹配项。返回对象RegexMatch
或nothing
。
Julia 的一个特点是不需要双重转义(例如,用
r"\d"
代替"\\d"
),这使得编写模板更加容易。
m = match(r"\d+", "Возраст: 42") #\d+ `\d` - выбери цифру `+` - одно или больше вхождений
println(m.match)
-eachmatch(r"шаблон", строка)
**:所有匹配项的迭代器。
for m in eachmatch(r"\w+", "Здравствуй, дорогой друг!") # `\w` - буква, цифра или _ (`,` и `!` не подходят)
println(m.match)
end
replace(строка, r"шаблон" => "замена")
:替换匹配项。
new = replace("Формат даты: 01-02-2025", r"\d" => "X")
println(new)
occursin(r"шаблон", строка)
:检查是否有匹配项。
@show occursin(r"[A-Z]", "Hello")
@show occursin(r"[A-Z]", "hello")
@show occursin(r"[A-Z]+", "HELLO");
实际应用¶
例 1:提取名字和姓氏¶
让我们拍摄一张音乐学院访客日志的照片。
然后,我们将该文件数字化并进行文本识别,得到 "文件 "OCR_text
。
结果发现,有些字母变成了小写字母,有些字母增加了额外的空格,有些字母则消失了。在某些情况下,一个笔画被识别为一个字母。
# Текст с данными
OCR_text = """
Журнал посетителей:
Фамилия: иванов имя: Иван
фамилия : Петров имя : пётр l
Фамилия - Римский-Корсаков Имя -Николай
"""
通过在表达式 中指定标志
-i
- 我们在表达式r"..."i
中指定区分大小写。也就是说,"姓 "和 "名 "将被视为等同的。
- 表达式
r"..."m
中的m
表示 m多行。表达式^
表示每行\n
之后的行首,而不仅仅是 "大 "行OCR_text
的行首。 - 表达式
x
中的r"..."x
- 我们可以使用空格,并通过#
(x 来自 extended 一词)指定注释。
下面我们将讨论括号的含义。
regex_fullname = r"
^Фамилия\s* # `Фамилия` в начале строки, а после 0 или более пробелов
[:-]\s* # далее один знак `:` или `-` и 0 или более пробелов
([\p{L}-]+) # [\p{L}-]+ - `\p{L}` - символы Unicode, `-` - дефис
\s* # после фамилии снова 0 или более пробелов
Имя\s*[:-]\s* # то же, что и с фамилией
(\p{L}+) # Любая последовательность букв (русских в том числе) это и есть имя"imx;
为了使用正则表达式regex_fullname
从我们的文档OCR_text
中提取有用的信息,让我们使用eachmatch
。
请注意,我们有 3 个人。每个人都有 2 个特征:姓和名。
eachmatch
返回一个迭代器,其中包含 RegexMatch 类型的对象,每个对象代表文本中的一个模式匹配。
我们的模式包含姓和名。姓在表达式中排在首位,因此我们将使用m.captures[1]
来表示姓。名字是第二个
也就是说,我们用游客的姓和名创建了一个数组。
fullnames = [(m.captures[1], m.captures[2]) for m in eachmatch(regex_fullname, OCR_text)]
让我们以页眉格式输出姓和名: ``朱莉娅 titlecase("abc"); # Abc titlecase("aBC"); # Abc
for (surname, name) in fullnames
println("Здравствуйте, $(titlecase(surname)) $(titlecase(name))!")
end
例 2:提取电话号码¶
假设我们需要查找一个格式为+7-XXX-XXX-XX-XX
或8-XXX-XXX-XX-XX
的号码:
说明:
\d{3}
表示正好是 3 位数、
\+
将加号作为文字转义。
|
表示 *** 或 ****
(?:...)
是一个 "非捕获组",也就是说,它是我们要单独定义的表达式的一个子部分(+7 或 8,然后是一组数字和连字符)。但是,我们对这些信息本身并不感兴趣,不管手机是用**+7**还是**8**写的。这就是为什么它不**令人兴奋。
text = "Российские номера это +7-912-345-67-89 или 8-987-654-32-10, но не +1-234-567-89-10"
russian_phone_regex = r"(?:\+7|8)-\d{3}-\d{3}-\d{2}-\d{2}"
for m in eachmatch(russian_phone_regex, text)
println("Найден российский номер: ", m.match)
end
例 3:检查电子邮件地址¶
让我们来检查电子邮件的正确性:
email = "test_User-name.123@pochta.ru"
email_regex = r"^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}$"
if match(email_regex, email) !== nothing
println("Email корректен")
else
println("Некорректный email")
end
说明:
^[a-zA-Z0-9._-]+
需要一个包含字母、数字和一些字符的用户名,而\.[a-z]{2,}$
需要一个长度超过 2 个字符的顶级域。
例 4:处理文献脚注¶
提取形式为[1]
,[1, 2]
的脚注:
text = "Текст ссылается на [1], [2, 3] и [4], и содержит математические выражения: 1 + (2{3 - x[y-z]})."
ref_regex = r"\[\d+(?:,\s*\d+)*\]"
matches = [m.match for m in eachmatch(ref_regex, text)]
println("Сноски: ", join(matches, ", "))
解释: (?:,\s*\d+)*
- 不捕捉带逗号的数字组。
例 5:获取莎士比亚的十四行诗¶
给出用罗马数字编号的 威廉-莎士比亚十四行诗。让我们创建一个数组,将这些十四行诗按原来的顺序编号,这样我们就可以很容易地按索引访问它们。
sonnets_text = read("sonnets.txt",String);
print(sonnets_text[1:1000])
十四行诗包含换行符。因此,点不能用来表示任何字符(参见 "正则表达式 "一章的开头)。为了解决这个问题,我们将使用
\s
表示空白字符。
\S
表示 NE 非空格字符
因此,[\s\S]
表示任何字符。
function split_sonnets(text)
pattern = r"""
^ # Начало строки (с флагом m — для каждой строки)
\s* # Ноль или более пробелов перед римской цифрой
[IVXLCDM]+ # Одна или более римских цифр (I, V, X, L, C, D, M)
\s* # Ноль или более пробелов после цифры
$ # Конец строки (ограничивает строку только цифрой)
\s* # Пробелы или пустые строки после цифры
#___________________________________________________________________________________________________
( # Начало захватывающей группы для текста сонета
[\s\S]*? # Любой символ (включая \n), нежадно (до ближайшей остановки)
) # Конец захватывающей группы
#___________________________________________________________________________________________________
(?= # Положительный просмотр вперёд (условие остановки)
^ # Начало следующей строки
\s* # Пробелы перед следующей цифрой
[IVXLCDM]+ # Следующая римская цифра
\s* # Пробелы после неё
$ # Конец строки с цифрой
| # Или
\z # Абсолютный конец текста (для последнего сонета)
) # Конец просмотра вперёд
"""mx # Флаги: m (многострочный режим), x (расширенный режим)
sonnets = [strip(m.captures[1]) for m in eachmatch(pattern, text)]
return sonnets
end
sonnets = split_sonnets(sonnets_text)
现在,让我们只推导前五首十四行诗的第一行。
为此,我们用split
将每首十四行诗分成两部分:
- 第 1 部分:第一行
- 第二部分:除第一行外的所有后行
s = """1 строка
2 строка
3 строка
4 строка"""
split(s,'\n',limit=2)
for (i, sonnet) in enumerate(sonnets[1:5])
println("""Соннет $i:$(split(sonnet,'\n',limit=2)[1])\n...""")
end
让我们来测量一下函数的执行速度。
Pkg.add("BenchmarkTools")
using BenchmarkTools
@btime split_sonnets(sonnets_text);
对于一个 2500 行的文件来说,1.24 毫秒是一个不错的结果。不过,我们应该意识到,正则表达式可能不如经典方法。在我们的案例中,我们可以用一种相当明确的方法来解决问题。(但你可以不深入研究它,而是看看它的执行速度)(但你可以不深入研究它,而是看看它的执行速度)。
function split_sonnets_fast(text)
sonnets = String[]
current_sonnet = String[]
in_sonnet = false
for line in eachline(text)
if !isempty(line) # Проверяем до отбрасывания пробелов и \n через функцию strip
stripped = strip(line)
# Если каждый (all) символ строки принадлежит IVXLCDM, то это римское число
if all(c -> c in "IVXLCDM", stripped)
if in_sonnet && !isempty(current_sonnet)
push!(sonnets, join(current_sonnet, '\n'))
end
current_sonnet = String[]
in_sonnet = true
elseif in_sonnet
push!(current_sonnet, line)
end
end
end
if in_sonnet && !isempty(current_sonnet)
push!(sonnets, join(current_sonnet, '\n'))
end
return sonnets
end
# Проверка работы
sonnets = split_sonnets_fast("sonnets.txt")
@btime split_sonnets_fast("sonnets.txt");
结论¶
Julia 中的正则表达式是处理文本的强大而便捷的工具。得益于其简单的语法(r"..."
)、内置功能(如match
和replace
)以及语言的高性能,正则表达式是数据处理和分析、搜索和替换任务的理想选择。但必须认识到,正则表达式在解析复杂(例如嵌套)结构的任务(如 JSON 文件、HTML 文件等)时可能会比较慢。
尽管正则表达式的语法有些复杂,但由于有了扩展功能,你可以进行注释。与 Julia 中用于处理字符和字符串的内置函数相比,这是一种用途更广的文本处理工具。