Python: уроки, справочник и задачи

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

Раздел: Строки в Python · Обновлено 25.10.2026
Строки программного кода на мониторе крупным планом
Фото: Markus Spiske, CC0

Проверить, содержит ли строка слово, узнать позицию фрагмента, посчитать вхождения или найти все совпадения по шаблону - это ежедневные задачи при обработке логов, пользовательского ввода и данных из файлов. В 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. Выбор между ними - вопрос того, является ли отсутствие подстроки нормальной ситуацией.

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 шаг станет нулевым и цикл никогда не закончится.

Когда искомый фрагмент описывается не точной строкой, а шаблоном (любая дата, любой 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 sboolFalseУсловие в if, фильтрация
s.find(sub)Индекс-1Позиция для среза, подстроки может не быть
s.rfind(sub)Индекс последнего вхождения-1Расширение файла, последний сегмент
s.index(sub)ИндексValueErrorПодстрока обязана быть
s.count(sub)Число0Подсчет вхождений
s.startswith(x)boolFalseПрефикс, протокол
s.endswith(x)boolFalseРасширение, суффикс
re.search(p, s)MatchNoneПоиск по шаблону

Практический пример: фильтр строк лога

Соберем приемы вместе. Функция читает строки лога, оставляет только ошибки нужного модуля и извлекает время и текст сообщения. Для больших файлов строки читаются по одной, без загрузки файла в память целиком.

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 отсекает большинство строк, и только оставшиеся проходят через регулярное выражение. На логах в сотни мегабайт такой прием заметно ускоряет обработку.

Частые ошибки

Частые вопросы

Чем 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()).