Как найти подстроку в строке Python

Проверить, содержит ли строка слово, узнать позицию фрагмента, посчитать вхождения или найти все совпадения по шаблону - это ежедневные задачи при обработке логов, пользовательского ввода и данных из файлов. В Python для них есть несколько инструментов, и у каждого свое назначение: оператор in отвечает "да или нет", find и index возвращают позицию, count считает, а модуль re ищет по шаблону. Разберем, когда что выбирать и какие подводные камни встречаются на практике.
Оператор in: есть ли подстрока в строке
Если нужен только ответ на вопрос "встречается ли фрагмент", используйте in. Это самый читаемый и самый быстрый способ: внутри работает оптимизированный алгоритм поиска на C, а результат сразу имеет тип bool.
line = "2026-10-25 12:00:01 ERROR Database connection lost"
print("ERROR" in line) # -> True
print("WARNING" in line) # -> False
print("error" in line) # -> False (регистр учитывается)
print("" in line) # -> True (пустая строка есть в любой строке)
if "ERROR" not in line:
print("Все спокойно")
Обратите внимание на последнюю проверку в первом блоке: пустая строка считается подстрокой любой строки. Если искомое значение приходит от пользователя или из конфигурации, проверьте, что оно не пустое, иначе условие всегда будет истинным.
Частая ошибка новичков - писать if line.find("ERROR"):. Метод find возвращает 0, если подстрока стоит в самом начале, и -1, если ее нет. Ноль ложен, минус один истинен, поэтому условие работает ровно наоборот. Для проверки наличия используйте только in.
Метод find: позиция первого вхождения
str.find(sub, start, end) возвращает индекс первого символа найденной подстроки или -1, если ее нет. Необязательные аргументы start и end ограничивают область поиска так же, как срез, но индексы в результате считаются от начала всей строки.
url = "https://example.org/catalog/books?page=2&sort=price"
pos = url.find("?")
print(pos) # -> 33
print(url[:pos]) # -> https://example.org/catalog/books
print(url[pos + 1:]) # -> page=2&sort=price
print(url.find("/", 8)) # -> 19 (первый слеш после "https://")
print(url.find("#")) # -> -1
Связка "найти позицию, затем взять срез" встречается постоянно. Если вы не уверены в синтаксисе срезов, загляните в статью срезы в Python. Главное правило: всегда проверяйте результат на -1 до того, как использовать его в срезе. Запись url[:-1] не вызовет ошибку, а просто отрежет последний символ, и такая логическая ошибка может долго оставаться незамеченной.
def get_query(url: str) -> str:
pos = url.find("?")
if pos == -1:
return ""
return url[pos + 1:]
print(get_query("https://example.org/a?x=1")) # -> x=1
print(get_query("https://example.org/a")) # -> ''
rfind: поиск с конца
rfind работает так же, но возвращает позицию последнего вхождения. Классический пример - получение расширения файла или последнего сегмента пути.
name = "archive.tar.gz"
print(name.find(".")) # -> 7
print(name.rfind(".")) # -> 11
print(name[name.rfind(".") + 1:]) # -> gz
path = "/var/log/nginx/access.log"
print(path[path.rfind("/") + 1:]) # -> access.log
Если разделитель нужен, чтобы разбить строку на части, удобнее методы partition и rpartition: они возвращают кортеж из трех частей и не требуют ручной работы с индексами. "archive.tar.gz".rpartition(".") дает ('archive.tar', '.', 'gz'), а при отсутствии разделителя возвращает исходную строку и две пустые, без исключений.
Метод index: то же, но с исключением
str.index и str.rindex ищут так же, как find и rfind, но при отсутствии подстроки выбрасывают ValueError: substring not found. Выбор между ними - вопрос того, является ли отсутствие подстроки нормальной ситуацией.
- Если подстроки может не быть и это обычный сценарий, берите
findи проверяйте -1. - Если подстрока обязана быть, а ее отсутствие означает испорченные данные, берите
index: ошибка всплывет сразу, а не превратится в неправильный срез через три функции.
header = "Content-Type: application/json"
sep = header.index(":")
print(header[:sep], "|", header[sep + 1:].strip())
# -> Content-Type | application/json
try:
"no colon here".index(":")
except ValueError as exc:
print("Ошибка формата:", exc) # -> Ошибка формата: substring not found
Как правильно перехватывать и обрабатывать такие исключения, подробно описано в статье Python обработка исключений. Не путайте str.index с list.index: метод списка ищет элемент целиком, а не подстроку внутри элементов.
Метод count: сколько раз встречается подстрока
str.count(sub, start, end) возвращает количество непересекающихся вхождений. Слово "непересекающихся" важно: после найденного совпадения поиск продолжается с конца этого совпадения.
text = "мама мыла раму, мама устала"
print(text.count("мама")) # -> 2
print(text.count("ма")) # -> 4
print("aaaa".count("aa")) # -> 2, а не 3
print("abc".count("")) # -> 4 (пустая строка между каждым символом и по краям)
Если нужно считать пересекающиеся вхождения, придется искать вручную со сдвигом на один символ, пример будет ниже. Для подсчета слов, а не подстрок, сначала разбейте текст на слова, иначе "ма" найдется и внутри "мама", и внутри "рама".
startswith и endswith: проверка начала и конца
Когда важно не просто наличие, а позиция в начале или в конце строки, используйте startswith и endswith. Это читается лучше, чем s[:4] == "http", и не требует считать длину префикса. Оба метода принимают кортеж вариантов, что удобно для проверки нескольких расширений или протоколов сразу.
files = ["photo.JPG", "doc.pdf", "image.png", "notes.txt", "scan.jpeg"]
images = [f for f in files if f.lower().endswith((".jpg", ".jpeg", ".png"))]
print(images) # -> ['photo.JPG', 'image.png', 'scan.jpeg']
link = "https://example.org"
print(link.startswith(("http://", "https://"))) # -> True
# Можно проверять с определенной позиции
print("ID:12345".startswith("123", 3)) # -> True
Методу нужен именно кортеж: список вызовет TypeError. Если варианты хранятся в списке, преобразуйте его: s.endswith(tuple(exts)). Для удаления префикса или суффикса с Python 3.9 есть removeprefix и removesuffix, они безопаснее, чем lstrip, который удаляет набор символов, а не строку.
Поиск без учета регистра
Все строковые методы чувствительны к регистру. Чтобы искать без учета регистра, приведите обе строки к одному виду. Для русского и английского текста хватает lower(), но для полностью корректного сравнения Unicode используйте casefold(): он, например, превращает немецкую "ß" в "ss".
text = "Python - Язык Программирования"
query = "язык"
print(query in text) # -> False
print(query.lower() in text.lower()) # -> True
print(text.lower().find(query.lower())) # -> 9
print("STRASSE".casefold() == "straße".casefold()) # -> True
print("STRASSE".lower() == "straße".lower()) # -> False
Позиция, найденная в строке после lower(), совпадает с позицией в оригинале для обычного текста, поэтому по ней можно резать исходную строку. С casefold() длина строки иногда меняется, и индексы уже нельзя переносить. Подробнее о методе lower в статье lower python.
Как найти все вхождения подстроки
Встроенного метода, который вернул бы список всех позиций, у строк нет. Есть два надежных способа: цикл с find и регулярное выражение с finditer.
def find_all(text: str, sub: str, overlap: bool = False) -> list[int]:
if not sub:
return []
positions = []
start = 0
step = 1 if overlap else len(sub)
while (pos := text.find(sub, start)) != -1:
positions.append(pos)
start = pos + step
return positions
print(find_all("абабаба", "аба")) # -> [0, 4]
print(find_all("абабаба", "аба", overlap=True)) # -> [0, 2, 4]
Здесь используется оператор присваивания := (появился в Python 3.8), который позволяет найти позицию и сразу проверить ее в условии цикла. Проверка на пустую подстроку обязательна, иначе при overlap=False шаг станет нулевым и цикл никогда не закончится.
Регулярные выражения: re.search, findall, finditer
Когда искомый фрагмент описывается не точной строкой, а шаблоном (любая дата, любой email, слово целиком), переходите к модулю re. Основные функции:
| Функция | Что возвращает | Когда использовать |
|---|---|---|
re.search(p, s) | Первый объект Match или None | Найти первое совпадение в любом месте |
re.match(p, s) | Match или None, только с начала строки | Проверить начало строки |
re.fullmatch(p, s) | Match или None, если совпала вся строка | Валидация формата |
re.findall(p, s) | Список строк (или кортежей групп) | Получить все значения |
re.finditer(p, s) | Итератор объектов Match | Нужны позиции и группы всех совпадений |
import re
log = "user=anna id=17; user=oleg id=42; user=ivan id=7"
m = re.search(r"id=(\d+)", log)
if m:
print(m.group(1), m.start(), m.end()) # -> 17 10 15
print(re.findall(r"user=(\w+)", log)) # -> ['anna', 'oleg', 'ivan']
for m in re.finditer(r"user=(\w+) id=(\d+)", log):
print(m.start(), m.group(1), int(m.group(2)))
# -> 0 anna 17
# -> 17 oleg 42
# -> 34 ivan 7
Поиск слова целиком
Оператор in найдет "кот" и в слове "который". Чтобы искать только отдельное слово, используйте границы слова \b. В Python 3 они корректно работают с кириллицей, потому что строковые шаблоны по умолчанию используют Unicode.
import re
text = "Который час? Кот спит."
print("кот" in text.lower()) # -> True (ложное срабатывание)
print(bool(re.search(r"\bкот\b", text, re.IGNORECASE))) # -> True
print(re.findall(r"\bкот\b", text, flags=re.I)) # -> ['Кот']
Экранирование пользовательского ввода
Если подстрока для поиска приходит извне и подставляется в шаблон, обязательно оборачивайте ее в re.escape. Иначе точка, плюс или скобка будут восприняты как часть синтаксиса регулярного выражения, и поиск даст неверный результат или упадет с re.error.
import re
needle = "c++"
text = "Знаю C++ и Python"
pattern = re.compile(re.escape(needle), re.IGNORECASE)
print(bool(pattern.search(text))) # -> True
# re.search("c++", text) -> re.error: multiple repeat
Больше шаблонов и разбор синтаксиса есть в статьях Модуль re и "Регулярные выражения в Python: примеры". Для простого поиска точной строки регулярные выражения не нужны: in и find быстрее и понятнее.
Что делать с найденной подстрокой
После поиска обычно следует действие: вырезать фрагмент, заменить его или разбить строку. Для замены всех вхождений не нужно искать позиции вручную, метод replace(old, new, count) сделает это сам, а третий аргумент ограничит число замен. Для замены по шаблону используйте re.sub, который принимает в качестве замены и строку, и функцию.
import re
s = "цена: 100 руб., скидка: 15 руб."
print(s.replace("руб.", "RUB")) # -> цена: 100 RUB, скидка: 15 RUB
print(s.replace("руб.", "RUB", 1)) # -> цена: 100 RUB, скидка: 15 руб.
print(re.sub(r"\d+", lambda m: str(int(m.group()) * 2), s))
# -> цена: 200 руб., скидка: 30 руб.
Если нужно вырезать текст между двумя маркерами, комбинируйте find с аргументом start: найдите открывающий маркер, затем ищите закрывающий начиная с позиции после него. Так вы не захватите закрывающий маркер, который случайно встретился раньше открывающего.
def between(text: str, left: str, right: str) -> str | None:
i = text.find(left)
if i == -1:
return None
i += len(left)
j = text.find(right, i)
return text[i:j] if j != -1 else None
print(between("id=[A-17] ok", "[", "]")) # -> A-17
print(between("нет скобок", "[", "]")) # -> None
Сравнение способов поиска
| Способ | Результат | Если не найдено | Типичный сценарий |
|---|---|---|---|
sub in s | bool | False | Условие в if, фильтрация |
s.find(sub) | Индекс | -1 | Позиция для среза, подстроки может не быть |
s.rfind(sub) | Индекс последнего вхождения | -1 | Расширение файла, последний сегмент |
s.index(sub) | Индекс | ValueError | Подстрока обязана быть |
s.count(sub) | Число | 0 | Подсчет вхождений |
s.startswith(x) | bool | False | Префикс, протокол |
s.endswith(x) | bool | False | Расширение, суффикс |
re.search(p, s) | Match | None | Поиск по шаблону |
Практический пример: фильтр строк лога
Соберем приемы вместе. Функция читает строки лога, оставляет только ошибки нужного модуля и извлекает время и текст сообщения. Для больших файлов строки читаются по одной, без загрузки файла в память целиком.
import re
from pathlib import Path
LINE_RE = re.compile(r"^(\d{2}:\d{2}:\d{2}) (\w+) \[(\w+)\] (.*)$")
def errors_of(path: Path, module: str) -> list[tuple[str, str]]:
result = []
with path.open(encoding="utf-8") as f:
for line in f:
if "ERROR" not in line: # дешевая проверка до регулярки
continue
m = LINE_RE.match(line.rstrip("\n"))
if m and m.group(2) == "ERROR" and m.group(3) == module:
result.append((m.group(1), m.group(4)))
return result
sample = Path("app.log")
sample.write_text(
"12:00:01 INFO [db] connected\n"
"12:00:05 ERROR [db] timeout after 30s\n"
"12:00:07 ERROR [api] bad request\n",
encoding="utf-8",
)
print(errors_of(sample, "db")) # -> [('12:00:05', 'timeout after 30s')]
Обратите внимание на порядок проверок: сначала быстрый in отсекает большинство строк, и только оставшиеся проходят через регулярное выражение. На логах в сотни мегабайт такой прием заметно ускоряет обработку.
Частые ошибки
if s.find(x):вместоif x in s:- условие ложно при совпадении в позиции 0 и истинно при отсутствии.- Срез по результату
findбез проверки на -1 - вместо ошибки получаете обрезанную строку. - Поиск слова через
inтам, где нужны границы слова. - Подстановка пользовательского ввода в регулярное выражение без
re.escape. - Ожидание, что
countпосчитает пересекающиеся вхождения. - Передача списка в
startswithвместо кортежа.
Частые вопросы
Чем find отличается от index?
Ищут одинаково, разница в поведении при отсутствии подстроки: find возвращает -1, index выбрасывает ValueError. Кроме того, у списков и кортежей есть только index, метода find у них нет.
Какой способ проверки самый быстрый?
Оператор in. Он реализован на C, не создает объектов и не возвращает индекс. Регулярные выражения медленнее на простых задачах, их стоит брать только для шаблонов.
Как проверить, есть ли в строке хотя бы одна из нескольких подстрок?
Используйте any с генератором: any(w in text for w in ("ошибка", "сбой", "error")). Для проверки, что есть все подстроки, замените any на all. Если вариантов сотни, эффективнее собрать одно регулярное выражение через "|".join(map(re.escape, words)).
Как найти подстроку в списке строк?
Пройдите по списку генератором: [s for s in items if "abc" in s] вернет все строки с подстрокой, а next((i for i, s in enumerate(items) if "abc" in s), -1) вернет индекс первой такой строки или -1.
Работают ли эти методы с bytes?
Да, у bytes есть find, index, count, startswith и оператор in, но искомое значение тоже должно быть байтами: b"GET" in data. Смешивать str и bytes нельзя, это вызовет TypeError.
Почему поиск не находит слово, которое видно в тексте?
Чаще всего виноваты невидимые различия: неразрывный пробел вместо обычного, буква "е" вместо "ё", латинская "с" вместо кириллической или разные формы записи одного символа в Unicode. Выведите repr() обеих строк и сравните коды символов через ord(). Для приведения составных символов к единой форме используйте unicodedata.normalize("NFC", s) перед поиском, а лишние пробелы уберите через " ".join(s.split()).