Парсинг строк в Python: split, find, partition, re — разбираем текст

Когда я впервые открыл файл почтового лога в Python, задача звучала просто: вытащить email-адрес отправителя из каждой строки, начинающейся с "From". Строка выглядела так: From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008. Пять минут я ковырялся с индексами вручную, пока не обнаружил, что split() делает это за одну строку. Парсинг строк (parsing) это процесс разбора текста на составные части для извлечения нужных данных.
В этой статье разбираю пять подходов к парсингу: split(), find() со срезами, partition(), регулярные выражения и комбинированные методы. Каждый подход показан на реальных задачах с разбором ошибок.
Что значит "парсить строку"
Парсинг (от англ. parsing) это анализ строки с целью извлечь из неё структурированные данные. Входные данные это текст без чёткой структуры: строка лог-файла, HTML-тег, CSV-запись, email-заголовок. Результат это отдельные значения: дата, адрес, число, имя.
В Python парсинг строк строится на трёх базовых инструментах: поиск позиции (find), разбиение на части (split), извлечение подстроки (срез). Все остальные подходы это комбинации и расширения этих трёх операций.
Подход 1: split() — разбиение по разделителю
split() разбивает строку на список подстрок по заданному разделителю. Без аргумента разделяет по пробельным символам.
Базовый парсинг строки лога
line = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
words = line.split()
print(words[0]) # From
print(words[1]) # stephen.marquard@uct.ac.za
print(words[2]) # Sat
print(words[5]) # 09:14:16
split() разбил строку по пробелам и вернул список из 7 элементов. Доступ по индексу извлекает нужное поле.
Двойной split: вложенный разбор
Иногда одного split() недостаточно. Для извлечения домена из email-адреса нужен второй split по символу @.
line = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
words = line.split()
email = words[1] # stephen.marquard@uct.ac.za
domain = email.split("@")[1] # uct.ac.za
print(domain)
Первый split() разбивает строку по пробелам, второй split("@") разбивает email-адрес по @.
split() с разделителем
csv_line = "Иванов,Пётр,28,Москва"
fields = csv_line.split(",")
print(fields) # ['Иванов', 'Пётр', '28', 'Москва']
surname = fields[0] # Иванов
age = int(fields[2]) # 28
split() с аргументом-разделителем разбивает строку по указанному символу.
split() с ограничением количества разбиений
Второй аргумент maxsplit останавливает разбиение после заданного количества.
log = "ERROR: 2026-04-07: Disk space critical"
parts = log.split(": ", 1)
print(parts[0]) # ERROR
print(parts[1]) # 2026-04-07: Disk space critical
При maxsplit=1 строка разделена только по первому двоеточию с пробелом. Остальной текст попал во второй элемент целиком.
Разница между split() и split(" ")
text = " hello world "
print(text.split()) # ['hello', 'world']
print(text.split(" ")) # ['', '', 'hello', '', '', 'world', '', '']
Без аргумента split() игнорирует множественные пробелы и пустые элементы. С аргументом " " каждый пробел создаёт отдельное разделение. Для парсинга текста с непредсказуемыми пробелами используйте split() без аргумента.
Подход 2: find() + срез — поиск и извлечение
Когда данные не разделены пробелами, а находятся между маркерами, используйте find() для поиска позиции и срез для извлечения.
Извлечение хоста из email-заголовка
data = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
at_pos = data.find("@")
sp_pos = data.find(" ", at_pos)
host = data[at_pos + 1 : sp_pos]
print(host) # uct.ac.za
find("@") находит позицию 21. find(" ", at_pos) ищет первый пробел после позиции 21 и находит позицию 31. Срез data[22:31] извлекает "uct.ac.za".
Извлечение значения из заголовка
line = "X-DSPAM-Confidence: 0.8475"
colon_pos = line.find(": ")
value = float(line[colon_pos + 2 :])
print(value) # 0.8475
find(": ") находит позицию двоеточия с пробелом. Срез от colon_pos + 2 до конца строки извлекает числовое значение. float() преобразует строку в число.
Извлечение текста между тегами
html = '<a href="https://example.com">Ссылка</a>'
start = html.find('">') + 2
end = html.find("</a>")
text = html[start:end]
print(text) # Ссылка
Цепочка find() для многоуровневого разбора
line = "Received: from mail.example.com (192.168.1.1) by server.local"
# Извлечь имя хоста
from_pos = line.find("from ") + 5
paren_pos = line.find(" (", from_pos)
hostname = line[from_pos:paren_pos]
print(hostname) # mail.example.com
# Извлечь IP
ip_start = paren_pos + 2
ip_end = line.find(")", ip_start)
ip = line[ip_start:ip_end]
print(ip) # 192.168.1.1
Каждый следующий find() использует позицию от предыдущего как стартовую точку. Это гарантирует, что мы ищем в правильном участке строки.
Подход 3: partition() — разбиение на три части
partition() разделяет строку по первому вхождению разделителя и возвращает кортеж из трёх элементов: часть до, сам разделитель, часть после.
email = "user@example.com"
name, sep, domain = email.partition("@")
print(name) # user
print(domain) # example.com
partition() vs split()
text = "key=value=extra"
# split — всё разделяет
print(text.split("=")) # ['key', 'value', 'extra']
# split с maxsplit — два элемента
print(text.split("=", 1)) # ['key', 'value=extra']
# partition — три элемента, включая разделитель
print(text.partition("=")) # ('key', '=', 'value=extra')
partition() удобнее split(x, 1), когда нужно знать, нашёлся ли разделитель. Если разделитель не найден, partition() возвращает (строка, "", ""), а split(x, 1) возвращает список с одним элементом.
rpartition() — разбиение по последнему вхождению
path = "/home/user/documents/report.txt"
directory, sep, filename = path.rpartition("/")
print(directory) # /home/user/documents
print(filename) # report.txt
rpartition() ищет последнее вхождение разделителя. Для путей к файлам это удобнее, чем find() + rfind().
Подход 4: регулярные выражения (re)
Когда структура текста сложная или разделители непредсказуемы, строковых методов недостаточно. Модуль re позволяет описать шаблон и извлечь данные по нему.
Базовый поиск по шаблону
import re
line = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
match = re.search(r"\S+@\S+", line)
if match:
print(match.group()) # stephen.marquard@uct.ac.za
Шаблон \S+@\S+ означает: один или более непробельных символов, затем @, затем снова один или более непробельных символов.
findall() — все вхождения
import re
text = "Контакты: alice@example.com и bob@company.org"
emails = re.findall(r"\S+@\S+", text)
print(emails) # ['alice@example.com', 'bob@company.org']
findall() возвращает список всех совпадений. find() нашёл бы только позицию первого.
Именованные группы для структурированного парсинга
import re
log = "2026-04-07T12:25:00 ERROR Database connection failed"
pattern = r"(?P<timestamp>\S+)\s+(?P<level>\w+)\s+(?P<message>.*)"
match = re.match(pattern, log)
if match:
print(match.group("timestamp")) # 2026-04-07T12:25:00
print(match.group("level")) # ERROR
print(match.group("message")) # Database connection failed
Именованные группы (?P<name>...) позволяют обращаться к частям совпадения по имени. Код становится читаемым без комментариев.
re.split() — разбиение по шаблону
import re
text = "слово1; слово2, слово3 слово4"
words = re.split(r"[;,\s]+", text)
print(words) # ['слово1', 'слово2', 'слово3', 'слово4']
re.split() разбивает строку по регулярному выражению. Строковый split() так не умеет.
Подход 5: комбинированный — строковые методы + логика
Реальный парсинг редко укладывается в один метод. Чаще всего нужна комбинация: отфильтровать строки, разбить на части, очистить, преобразовать тип.
Парсинг лог-файла: дни недели отправки
fhand = open("mbox-short.txt")
for line in fhand:
line = line.rstrip()
if not line.startswith("From "):
continue
words = line.split()
print(words[2])
Алгоритм: rstrip() убирает перенос строки, startswith() фильтрует нужные строки, split() разбивает на слова, индекс извлекает день недели.
Парсинг с защитой от ошибок
fhand = open("mbox-short.txt")
for line in fhand:
words = line.split()
# Защита от пустых строк
if len(words) == 0:
continue
if words[0] != "From":
continue
print(words[2])
Пустая строка после split() даёт пустой список. Обращение к words[0] вызовет IndexError. Проверка len(words) == 0 предотвращает это.
Подсчёт частоты слов в файле
counts = {}
for line in fhand:
words = line.split()
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
Вложенный цикл: внешний перебирает строки файла, внутренний перебирает слова в строке. Метод dict.get() возвращает текущий счётчик или 0, если слово встретилось впервые.
Очистка текста перед парсингом
Грязные данные ломают парсинг. Перед разбором текст нужно очистить: убрать лишние пробелы, привести регистр, удалить пунктуацию.
Удаление пунктуации через translate()
import string
text = "But, soft! what light through yonder window breaks?"
clean = text.translate(str.maketrans("", "", string.punctuation))
print(clean) # But soft what light through yonder window breaks
words = clean.lower().split()
print(words)
# ['but', 'soft', 'what', 'light', 'through', 'yonder', 'window', 'breaks']
str.maketrans("", "", string.punctuation) создаёт таблицу перевода, которая удаляет все знаки пунктуации. После этого split() разбивает чистый текст на слова.
Какой подход выбрать
split() справляется с 70% задач парсинга текста. find() + срез помогает, когда позиция данных определяется маркерами, а не разделителями. Регулярные выражения нужны, когда формат строк непредсказуем или содержит несколько вариантов структуры.
Неочевидные детали
Первый факт: split() с аргументом и без него ведёт себя по-разному с пустыми строками. "".split() возвращает [], а "".split(",") возвращает [""].
print("".split()) # []
print("".split(",")) # ['']
Второй факт: find() возвращает -1 при неудаче, и срез с -1 может дать неожиданный результат. Код data[data.find("@"):] при отсутствии @ вернёт последний символ строки, потому что data[-1:] это корректный срез. Всегда проверяйте результат find() перед использованием в срезе.
data = "no at sign here"
pos = data.find("@")
print(pos) # -1
print(data[pos:]) # e (последний символ!)
Третий факт: partition() работает быстрее, чем split(x, 1), потому что всегда возвращает ровно три элемента без создания списка переменной длины.
Четвёртый факт: re.findall() с группами возвращает не полные совпадения, а только содержимое групп.
import re
text = "Цена: 100 руб, Скидка: 20 руб"
print(re.findall(r"(\d+) руб", text)) # ['100', '20']
Без скобок findall() вернул бы ['100 руб', '20 руб']. С одной группой возвращает только содержимое группы.
Пятый факт: для парсинга CSV используйте модуль csv, а не split(","): он корректно обрабатывает кавычки и переносы строк внутри полей.
import csv, io
line = '"Иванов, Пётр",28,"Москва"'
reader = csv.reader(io.StringIO(line))
fields = next(reader)
print(fields) # ['Иванов, Пётр', '28', 'Москва']
FAQ
Когда использовать find(), а когда in?
Оператор in проверяет наличие подстроки и возвращает True/False. find() возвращает позицию. Используйте in для проверки, find() когда нужна позиция.
Как парсить строку с несколькими разделителями?
Используйте re.split() с шаблоном, перечисляющим все разделители.
import re
text = "яблоко; груша, слива | банан"
fruits = re.split(r"[;,|]\s*", text)
print(fruits) # ['яблоко', 'груша', 'слива', 'банан']
Что быстрее: split() или re.split()?
split() быстрее, потому что реализован на C и не требует компиляции шаблона. Используйте re.split() только когда разделитель описывается шаблоном, а не фиксированной строкой.
Как парсить JSON-строку?
Не разбирайте JSON вручную через split() или find(). Используйте модуль json:
import json
data = '{"name": "Alice", "age": 30}'
parsed = json.loads(data)
print(parsed["name"]) # Alice
Как безопасно парсить строку, если формат может отличаться?
Используйте try/except или проверяйте длину списка после split().
line = "incomplete data"
parts = line.split(":")
if len(parts) >= 2:
key, value = parts[0], parts[1]
else:
print("Неожиданный формат")
Мой совет: начинайте парсинг с split(). Если split() не справляется, добавьте find() и срезы. Если формат данных нестабильный или содержит вложенные структуры, переходите на регулярные выражения. Для стандартных форматов (CSV, JSON, XML) всегда используйте специализированные модули, а не ручной разбор.
