Работа со строками в Lua: основные функции библиотек string и utf8

Опубликовано:

В этой статье на примерах рассматриваются наиболее используемые строковые функции, а также особенности языка паттернов Lua.

Работа со строками в Lua имеет свои особенности: отсутствует символьный тип, используется 1-индексация, иммутабельность (неизменчивость) с агрессивным интернированием (хранится только одна копия каждой уникальной строки в памяти), собственный механизм сопоставления шаблонов (Patterns) вместо классического Regex.

В Lua строковые методы можно вызывать как через библиотеку (string.*), так и через двоеточие от самого экземпляра строки (ООП-синтаксис), так как у всех строк имеется общая метаблица:

local text = 'Gamer'
print(string.lower(text)) -- gamer
print(text:upper())       -- GAMER

Длина строки:

local s = 'hello'

print(#s)       -- 5
print(s:len())  -- 5

В Lua строки соединяются через операцию .., но такая конкатенация в цикле снижает производительность, поэтому вместо нее обычно используют table.concat.

В Lua строки не только индексируются с 1, но нет и привычного синтаксиса для получения символа через индекс (s[i]). Вместо этого используется функция string.sub для взятия срезов.

local s = 'hello world'

print(s:sub(7))     -- world
print(s:sub(1, 1))  -- h
print(s:sub(3, 5))  -- llo
print(s:sub(-2))    -- ld

Функция string.format — форматирование

Функция string.format в Lua используется для форматирования строк и работает практически так же, как sprintf в Си.

-- Ограничение знаков после запятой (округление)
local pi = 3.14159265
print(string.format('%.2f', pi))  --> '3.14' (две цифры после запятой)
print(string.format('%.4f', pi))  --> '3.1416' (четыре цифры с округлением)

-- Добавление ведущих нулей (полезно для дат и времени)
local day, month, year = 5, 9, 2026
print(string.format('%02d/%02d/%d', day, month, year)) --> '05/09/2026'

-- Выравнивание чисел пробелами (создание таблиц)
print(string.format('%4d', 7))   --> '   7' (ширина поля 4 символа)
print(string.format('%4d', 123)) --> ' 123'

-- Перевод в шестнадцатеричную (Hex) систему
local r, g, b = 255, 165, 0
-- %02X выведет заглавные hex-символы с ведущим нулем, если число меньше 16
print(string.format('#%02X%02X%02X', r, g, b)) --> '#FFA500'

-- Перевод в восьмеричную (Octal) систему
print(string.format('%o', 8)) --> '10'

-- Вставка строк и подгонка по ширине
local name = 'Иван'
print(string.format('Привет, %s!', name)) --> 'Привет, Иван!'
-- Выравнивание строки по правой и левой стороне
print(string.format('|%10s|', 'тест'))  --> '|      тест|' (по правому краю)
print(string.format('|%-10s|', 'тест')) --> '|тест      |' (по левому краю)

Функция string.find — поиск

Функция string.find в Lua предназначена для поиска подстроки или паттерна (регулярного выражения Lua) внутри базовой строки. Функция принимает на вход исходную строку, искомую подстроку (или паттерн), и необязательные параметры: индекс начала поиска и флаг простого поиска.

В случае успешного поиска функция возвращает два индекса (начало и конец совпадения). Если в паттерне использовались захваты (захватывающие группы в круглых скобках), функция вернет их значения третьим, четвертым и последующими аргументами. Если подстрока не найдена, функция возвращает nil.

local text = 'Python Lua JS Java C Pascal'

if string.find(text, 'Lua') then
    print('В списке есть Lua!')
else
    print('Не найдено.')
end

По умолчанию поиск идет с 1-го символа. С помощью третьего аргумента можно указать, откуда начинать поиск (например, чтобы найти второе вхождение слова).

local text = 'one, two, three, one, ten'

local first_start, first_end = string.find(text, 'one')
print(first_start) --> 1

-- Ищем следующее вхождение, начиная поиск сразу за концом первого
local second_start, second_end = string.find(text, 'one', first_end + 1)
print(second_start) --> 18

По умолчанию string.find воспринимает спецсимволы (., %, *, -, ? и др.) как элементы паттернов Lua. Если нужно найти строку "как есть", передают true в качестве четвертого аргумента. Перед ним обязательно указывается начальный индекс (обычно 1).

local text = 'The file has been 100% updated.'

-- Без флага plain: % воспринимается как управляющий символ, будет ошибка или неверный поиск
-- print(string.find(text, '100%'))

-- С флагом plain = true: ищет точный текст '100%'
local s, e = string.find(text, '100%', 1, true)
print(s, e) --> 19   22

Если в паттерне есть круглые скобки, string.find извлечет совпадения и вернет их после основных индексов.

local log = 'Error: code 404, page not found'

-- Паттерн '%d+' ищет одну или более цифр подряд, круглые скобки захватывают их
local start_idx, end_idx, error_code = string.find(log, 'code (%d+)')

print(start_idx, end_idx)  --> 8   15
print(error_code)          --> 404 (это строка)

Функция string.gsub — замена

Функция string.gsub (от global substitution) в Lua предназначена для глобального поиска и замены подстрок или паттернов в строке.

Функция принимает на вход исходную строку, паттерн для поиска и правило замены (строку, таблицу или функцию). Также можно передать необязательный четвертый аргумент — максимальное количество замен. Всегда возвращает два значения: новую строку с выполненными заменами, общее количество сделанных замен (целое число).

local text = 'Банан — это желтый фрукт. Банан вкусный.'
-- Заменяем все слова 'Банан' на 'Апельсин'
local result, count = string.gsub(text, 'Банан', 'Апельсин')

print(result) --> 'Апельсин — это желтый фрукт. Апельсин вкусный.'
print(count)  --> 2 (было сделано две замены)

В строке замены можно использовать специальные маркеры %1, %2 и т.д., чтобы подставить текст, который был захвачен круглыми скобками в паттерне. Маркер %0 означает весь найденный фрагмент.

local date = '2026-10-23'
-- Меняем формат даты с ГГГГ-ММ-ДД на ДД.ММ.ГГГГ
-- (%d+) захватывает группы цифр
local formatted = string.gsub(date, '(%d+)-(%d+)-(%d+)', '%3.%2.%1')

print(formatted) --> '23.10.2026'

Если третий аргумент — таблица (словарь), то string.gsub ищет совпадение (или первый захват паттерна) в качестве ключа этой таблицы. Если ключ найден, он заменяется на значение из таблицы. Если ключа нет, текст остается без изменений.

local vars = { name = 'Алексей', role = 'Администратор' }
local template = 'Пользователь $name зашел в систему как $role.'

-- Ищем конструкцию $переменная, где имя переменной состоит из букв (%a+)
local result = string.gsub(template, '%$(%a+)', vars)

print(result) --> 'Пользователь Алексей зашел в систему как Администратор.'

Если в качестве третьего аргумента передана функция, она будет вызываться при каждом совпадении. В функцию передаются все захваченные подстроки. То, что функция вернет (должна быть строка или число), станет заменой. Если функция возвращает nil или false, замена для этого фрагмента не происходит.

local text = 'Товар стоит 100 долларов и 50 долларов доставка.'

-- Функция увеличивает каждое найденное число в 2 раза
local double_prices = string.gsub(text, '%d+', function(price)
    return tonumber(price) * 2
end)

print(double_prices) --> 'Товар стоит 200 долларов и 100 долларов доставка.'

Функция string.gmatch — итератор совпадений

Функция string.gmatch в Lua предназначена для последовательного поиска всех совпадений по паттерну в строке. В отличие от других функций, она возвращает не строки или индексы, а функцию-итератор, которую чаще всего используют в циклах for.

Чтобы пройти по каждому символу, используют шаблон "." (любой символ):

local s = 'hello'

-- string.gmatch возвращает функцию-итератор
for char in string.gmatch(s, '.') do
    print(char)
end

Разбиение строки на слова:

local text = 'one, two, three!'

-- Паттерн '%a+' находит каждое слово по очереди
for word in string.gmatch(text, '%a+') do
    print(word)
end
one
two
three

Извлечение пар "ключ = значение" (использование захватов): если в паттерне несколько круглых скобок, gmatch возвращает столько же значений за одну итерацию.

local config = 'width=1920; height=1080; fullscreen=true;'

-- Ищем имя ключа (%w+) и его значение (%w+) вокруг знака равенства
for key, value in string.gmatch(config, '(%w+)=(%w+)') do
    print('Ключ:', key, '. Значение:', value)
end
Ключ:   width   . Значение:     1920
Ключ:   height  . Значение:     1080
Ключ:   fullscreen      . Значение:     true

Разделение строки по разделителю:

local path = '/usr/local/bin/lua'

-- [^/]+ означает 'любые символы, кроме косой черты, один или более раз'
for folder in string.gmatch(path, '[^/]+') do
    print(folder)
end
usr
local
bin
lua

Паттерны Lua и функция string.match

Функция string.match и механизм паттернов (patterns) — это встроенный в Lua инструмент для поиска и извлечения текста. В отличие от многих других языков программирования, Lua не использует стандартные регулярные выражения (POSIX или PCRE). Вместо этого в нем реализована собственная, более легковесная и быстрая система паттернов.

Основные классы символов (вместо обратного слэша используется знак процента %):

Инверсия класса: если написать букву в верхнем регистре, класс станет работать от противного. Например, %D — любой символ, кроме цифры, а %S — любой непробельный символ.

Квантификаторы (повторения):

Якоря и экранирование:

В отличие от string.find, которая возвращает индексы местонахождения текста, string.match возвращает сам найденный текст. Если в паттерне нет круглых скобок, функция возвращает всю подстроку, которая подошла под паттерн. Если в паттерне есть круглые скобки (захваты), функция возвращает только то, что попало внутрь этих скобок. Если ничего не найдено, возвращается nil.

Простой поиск слова по шаблону:

local text = 'Номер заказа: ID-48291'
-- Ищем 'ID-' и все цифры после него
local result = string.match(text, 'ID%-%d+')

print(result) --> 'ID-48291'

Извлечение конкретной части строки (с помощью захвата):

local email = 'Игрок: user123@gmail.com'
-- Нам нужно только имя пользователя до собаки. 
-- (%w+) захватит буквы и цифры, стоящие перед @
local username = string.match(email, '(%w+)@')

print(username) --> 'user123'

Множественный захват:

local time = 'Текущее время 14:35:02'
-- Захватываем часы, минуты и секунды по отдельности
local hrs, mins, secs = string.match(time, '(%d+):(%d+):(%d+)')

print(hrs, mins, secs) --> 14   35   02

Использование ленивого квантификатора -. Представим, что нужно вырезать текст внутри HTML-тегов <div>...</div>:

local html = '<div>Первый</div><div>Второй</div>'

-- Жадный поиск `.*` захватит всё от первого <div> до последнего </div>
print(string.match(html, '<div>(.*)</div>')) 
--> 'Первый</div><div>Второй' (Неверно!)

-- Ленивый поиск `.-` остановится на самом ближайшем закрывающем теге
print(string.match(html, '<div>(.-)</div>')) 
--> 'Первый' (Правильно)

Библиотека utf8 для многобайтовых символов

В Lua модуль string работает с байтами, а не символами. Поскольку каждая русская буква занимает 2 байта, string.len("Привет") вернет 12, а не 6. Однако в последних версиях Lua имеется встроенная библиотека utf8, которая предоставляет функции для корректной работы со строками в кодировке UTF-8.

Функция utf8.len считает именно символы (буквы), а не байты:

local str = 'Привет'
print(string.len(str)) -- 12
print(utf8.len(str))   -- 6 

Функция utf8.codes используется в цикле for. Она возвращает байтовую позицию символа (pos) и его числовой Unicode-код (code).

local str = 'Мир'

for pos, code in utf8.codes(str) do
    -- utf8.char() превращает числовой код обратно в символ
    print(string.format('Позиция байта: %d, Символ: %s, Код: %d', pos, utf8.char(code), code))
end
Позиция байта: 1, Символ: М, Код: 1052
Позиция байта: 3, Символ: и, Код: 1080
Позиция байта: 5, Символ: р, Код: 1088

Так как string.sub работает с байтами, напрямую передавать туда номера символов нельзя. Нужно сначала найти байтовое смещение с помощью utf8.offset.

local function utf8_sub(str, start_char, end_char)
    -- Находим байтовое смещение для начального символа
    local start_byte = utf8.offset(str, start_char)
    if not start_byte then return '' end
    
    -- Находим байтовое смещение для конечного символа
    local end_byte
    if end_char then
        -- Смещение конца — это начало СЛЕДУЮЩЕГО символа минус 1 байт
        local next_byte = utf8.offset(str, end_char + 1)
        end_byte = next_byte and (next_byte - 1) or #str
    else
        end_byte = #str
    end
    
    return string.sub(str, start_byte, end_byte)
end

local text = 'Программирование'
print(utf8_sub(text, 1, 4))  -- Выведет: Прог
print(utf8_sub(text, 5, 11)) -- Выведет: раммиро

Если строка повреждена (например, обрезана посреди байтов русской буквы), utf8.len вернет nil, а вторым аргументом — позицию ошибки.

local valid_str = 'Тест'
local broken_str = string.sub('Тест', 1, 3) -- Срезали половину второй буквы

print(utf8.len(valid_str))  -- Выведет: 4
print(utf8.len(broken_str)) -- Выведет: nil    3 (ошибка на 3-м символе)