Engee 文档
Notebook

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" 匹配colorcolour 。 -{n,m} - 重复范围:r"a{2,4}" 匹配aa,aaa,aaaa
  • 字符类: -[abc] - 其中一个字符:r"[abc]" 匹配a,bc 。 -[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 中选择数字1234 并保存。 -(?:...) - 分组,但不会 "保存 "结果。非捕获分组有助于简化表达式的结构。
regex = r"(?:abc)+(\d+)(?:def)+(\&+)"
text = "abcabc123defdefdef&&&&&"
match(regex,text)[1] # 将返回 "123"。
match(regex,text)[2] # 返回 "&&&&".

Julia 中的函数

Julia 提供了处理正则表达式的便捷方法:

  • match(r"шаблон", строка):查找第一个匹配项。返回对象RegexMatchnothing

Julia 的一个特点是不需要双重转义(例如,用r"\d" 代替"\\d" ),这使得编写模板更加容易。

In [ ]:
m = match(r"\d+", "Возраст: 42")  #\d+  `\d` - выбери цифру  `+` - одно или больше вхождений
println(m.match)
42

-eachmatch(r"шаблон", строка)**:所有匹配项的迭代器。

In [ ]:
for m in eachmatch(r"\w+", "Здравствуй, дорогой друг!")  # `\w` - буква, цифра или _  (`,` и `!` не подходят)
    println(m.match)
end
Здравствуй
дорогой
друг
  • replace(строка, r"шаблон" => "замена"):替换匹配项。
In [ ]:
new = replace("Формат даты: 01-02-2025", r"\d" => "X")
println(new)
Формат даты: XX-XX-XXXX
  • occursin(r"шаблон", строка):检查是否有匹配项。
In [ ]:
@show occursin(r"[A-Z]", "Hello")
@show occursin(r"[A-Z]", "hello")
@show occursin(r"[A-Z]+", "HELLO");
occursin(r"[A-Z]", "Hello") = true
occursin(r"[A-Z]", "hello") = false
occursin(r"[A-Z]+", "HELLO") = true

实际应用

例 1:提取名字和姓氏

让我们拍摄一张音乐学院访客日志的照片。 然后,我们将该文件数字化并进行文本识别,得到 "文件 "OCR_text 。 结果发现,有些字母变成了小写字母,有些字母增加了额外的空格,有些字母则消失了。在某些情况下,一个笔画被识别为一个字母。

In [ ]:
# Текст с данными
OCR_text = """
Журнал посетителей:
Фамилия: иванов имя: Иван
фамилия : Петров имя : пётр     l
Фамилия - Римский-Корсаков Имя  -Николай    
"""

通过在表达式 中指定标志 -i - 我们在表达式r"..."i 中指定区分大小写。也就是说,"姓 "和 "名 "将被视为等同的。

  • 表达式r"..."m 中的m 表示 m多行。表达式^ 表示每行\n 之后的行首,而不仅仅是 "大 "行OCR_text 的行首。
  • 表达式x 中的r"..."x - 我们可以使用空格,并通过#x 来自 extended 一词)指定注释。

下面我们将讨论括号的含义。

In [ ]:
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] 来表示姓。名字是第二个

也就是说,我们用游客的姓和名创建了一个数组。

In [ ]:
fullnames = [(m.captures[1], m.captures[2]) for m in eachmatch(regex_fullname, OCR_text)]
Out[0]:
3-element Vector{Tuple{SubString{String}, SubString{String}}}:
 ("иванов", "Иван")
 ("Петров", "пётр")
 ("Римский-Корсаков", "Николай")

让我们以页眉格式输出姓和名: ``朱莉娅 titlecase("abc"); # Abc titlecase("aBC"); # Abc


In [ ]:
for (surname, name) in fullnames
    println("Здравствуйте, $(titlecase(surname)) $(titlecase(name))!")
end
Здравствуйте, Иванов Иван!
Здравствуйте, Петров Пётр!
Здравствуйте, Римский-Корсаков Николай!

例 2:提取电话号码

假设我们需要查找一个格式为+7-XXX-XXX-XX-XX8-XXX-XXX-XX-XX 的号码:

说明

\d{3} 表示正好是 3 位数、

\+ 将加号作为文字转义。

| 表示 *** 或 ****

(?:...) 是一个 "非捕获组",也就是说,它是我们要单独定义的表达式的一个子部分(+7 或 8,然后是一组数字和连字符)。

但是,我们对这些信息本身并不感兴趣,不管手机是用**+7**还是**8**写的。这就是为什么它不**令人兴奋。

In [ ]:
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
Найден российский номер: +7-912-345-67-89
Найден российский номер: 8-987-654-32-10

例 3:检查电子邮件地址

让我们来检查电子邮件的正确性:

In [ ]:
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
Email корректен

说明^[a-zA-Z0-9._-]+ 需要一个包含字母、数字和一些字符的用户名,而\.[a-z]{2,}$ 需要一个长度超过 2 个字符的顶级域。

例 4:处理文献脚注

提取形式为[1],[1, 2] 的脚注:

In [ ]:
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, ", "))
Сноски: [1], [2, 3], [4]

解释(?:,\s*\d+)* - 不捕捉带逗号的数字组。

例 5:获取莎士比亚的十四行诗

给出用罗马数字编号的 威廉-莎士比亚十四行诗。让我们创建一个数组,将这些十四行诗按原来的顺序编号,这样我们就可以很容易地按索引访问它们。

In [ ]:
sonnets_text = read("sonnets.txt",String);
print(sonnets_text[1:1000])
THE SONNETS

by William Shakespeare




  I

  From fairest creatures we desire increase,
  That thereby beauty's rose might never die,
  But as the riper should by time decease,
  His tender heir might bear his memory:
  But thou, contracted to thine own bright eyes,
  Feed'st thy light's flame with self-substantial fuel,
  Making a famine where abundance lies,
  Thy self thy foe, to thy sweet self too cruel:
  Thou that art now the world's fresh ornament,
  And only herald to the gaudy spring,
  Within thine own bud buriest thy content,
  And tender churl mak'st waste in niggarding:
    Pity the world, or else this glutton be,
    To eat the world's due, by the grave and thee.

  II

  When forty winters shall besiege thy brow,
  And dig deep trenches in thy beauty's field,
  Thy youth's proud livery so gazed on now,
  Will be a tatter'd weed of small worth held:
  Then being asked, where all thy beauty lies,
  Where all the treasure of thy lusty days;
  To say, within thine own deep

十四行诗包含换行符。因此,点不能用来表示任何字符(参见 "正则表达式 "一章的开头)。为了解决这个问题,我们将使用

\s 表示空白字符。

\S 表示 NE 非空格字符

因此,[\s\S] 表示任何字符。

In [ ]:
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
Out[0]:
split_sonnets (generic function with 1 method)
In [ ]:
sonnets = split_sonnets(sonnets_text)
Out[0]:
154-element Vector{SubString{String}}:
 "From fairest creatures we desir" ⋯ 579 bytes ⋯ "'s due, by the grave and thee."
 "When forty winters shall besieg" ⋯ 597 bytes ⋯ "arm when thou feel'st it cold."
 "Look in thy glass and tell the " ⋯ 576 bytes ⋯ "nd thine image dies with thee."
 "Unthrifty loveliness, why dost " ⋯ 558 bytes ⋯ "sed, lives th' executor to be."
 "Those hours, that with gentle w" ⋯ 593 bytes ⋯ "r substance still lives sweet."
 "Then let not winter's ragged ha" ⋯ 580 bytes ⋯ "est and make worms thine heir."
 "Lo! in the orient when the grac" ⋯ 552 bytes ⋯ "n diest unless thou get a son."
 "Music to hear, why hear'st thou" ⋯ 612 bytes ⋯ "'Thou single wilt prove none.'"
 "Is it for fear to wet a widow's" ⋯ 585 bytes ⋯ " such murd'rous shame commits."
 "For shame! deny that thou bear'" ⋯ 591 bytes ⋯ "ill may live in thine or thee."
 "As fast as thou shalt wane, so " ⋯ 645 bytes ⋯ "t more, not let that copy die."
 "When I do count the clock that " ⋯ 594 bytes ⋯ " him when he takes thee hence."
 "O! that you were your self; but" ⋯ 574 bytes ⋯ "a father: let your son say so."
 ⋮
 "Lo, as a careful housewife runs" ⋯ 597 bytes ⋯ "back and my loud crying still."
 "Two loves I have of comfort and" ⋯ 542 bytes ⋯ "ad angel fire my good one out."
 "Those lips that Love's own hand" ⋯ 475 bytes ⋯ "v'd my life, saying 'not you'."
 "Poor soul, the centre of my sin" ⋯ 576 bytes ⋯ "d, there's no more dying then."
 "My love is as a fever longing s" ⋯ 563 bytes ⋯ "ack as hell, as dark as night."
 "O me! what eyes hath Love put i" ⋯ 594 bytes ⋯ "g thy foul faults should find."
 "Canst thou, O cruel! say I love" ⋯ 548 bytes ⋯ "e thou lov'st, and I am blind."
 "O! from what power hast thou th" ⋯ 576 bytes ⋯ "orthy I to be belov'd of thee."
 "Love is too young to know what " ⋯ 576 bytes ⋯ "ose dear love I rise and fall."
 "In loving thee thou know'st I a" ⋯ 602 bytes ⋯ "ainst the truth so foul a lie!"
 "Cupid laid by his brand and fel" ⋯ 578 bytes ⋯ "t new fire; my mistress' eyes."
 "The little Love-god lying once " ⋯ 563 bytes ⋯ "s water, water cools not love."

现在,让我们只推导前五首十四行诗的第一行。

为此,我们用split 将每首十四行诗分成两部分:

  • 第 1 部分:第一行
  • 第二部分:除第一行外的所有后行
In [ ]:
s = """1 строка
       2 строка
       3 строка
       4 строка"""
split(s,'\n',limit=2)
Out[0]:
2-element Vector{SubString{String}}:
 "1 строка"
 "2 строка\n3 строка\n4 строка"
In [ ]:
for (i, sonnet) in enumerate(sonnets[1:5])
    println("""Соннет $i:$(split(sonnet,'\n',limit=2)[1])\n...""")
end
Соннет 1:
From fairest creatures we desire increase,
...
Соннет 2:
When forty winters shall besiege thy brow,
...
Соннет 3:
Look in thy glass and tell the face thou viewest
...
Соннет 4:
Unthrifty loveliness, why dost thou spend
...
Соннет 5:
Those hours, that with gentle work did frame
...

让我们来测量一下函数的执行速度。

In [ ]:
Pkg.add("BenchmarkTools")
In [ ]:
using BenchmarkTools
@btime split_sonnets(sonnets_text);
  1.244 ms (621 allocations: 39.19 KiB)

对于一个 2500 行的文件来说,1.24 毫秒是一个不错的结果。不过,我们应该意识到,正则表达式可能不如经典方法。在我们的案例中,我们可以用一种相当明确的方法来解决问题。(但你可以不深入研究它,而是看看它的执行速度)(但你可以不深入研究它,而是看看它的执行速度)。

In [ ]:
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");
  888.738 μs (3870 allocations: 392.40 KiB)

结论

Julia 中的正则表达式是处理文本的强大而便捷的工具。得益于其简单的语法(r"..." )、内置功能(如matchreplace )以及语言的高性能,正则表达式是数据处理和分析、搜索和替换任务的理想选择。但必须认识到,正则表达式在解析复杂(例如嵌套)结构的任务(如 JSON 文件、HTML 文件等)时可能会比较慢。

尽管正则表达式的语法有些复杂,但由于有了扩展功能,你可以进行注释。与 Julia 中用于处理字符和字符串的内置函数相比,这是一种用途更广的文本处理工具。