Справочник по Python · модуль 5 · строки
Глава 5.5
Начало, окончание
и поиск в строках
startswith() и endswith() отвечают на булев вопрос «так ли начинается текст». Семейство find() и index() отвечает на другой — «на какой позиции». Разница между ними определяет и выбор метода, и способ проверки результата.
- Категория
- Строковый поиск
- Уровень
- Базовый → средний
- Связанные темы
- Срезы, кортежи, исключения, Unicode
- Зачем это знать
- Проверять формат текста и находить границы частей
§1Два разных вопроса
О подстроке спрашивают по-разному, и от вопроса зависит инструмент.
| Вопрос | Инструмент | Что вернёт |
|---|---|---|
| текст начинается с этого? | startswith() | True или False |
| текст заканчивается этим? | endswith() | True или False |
| подстрока вообще есть? | in | True или False |
| на какой позиции она стоит? | find(), index() | число |
| сколько раз встречается? | count() | число |
Смешение первых трёх строк с четвёртой — источник самой коварной ошибки главы, и §10 разбирает её подробно.
§2startswith(): начало
Метод проверяет, начинается ли строка с указанной подстроки, и возвращает логическое значение.
url = "https://example.com" print(url.startswith("https://")) print(url.startswith("http://"))
True False
Тот же результат можно получить срезом — url[:8] == "https://", — но такая запись требует вручную считать длину префикса и ломается при её изменении. Метод избавляет от арифметики.
§3endswith(): окончание
Зеркальный метод проверяет конец строки. Типичное применение — расширение файла.
filename = "report.csv" print(filename.endswith(".csv")) print(filename.endswith(".json"))
True False
Точку в суффиксе лучше не опускать: проверка endswith("csv") пропустит и файл mycsv, у которого расширения нет вовсе.
§4Кортеж вариантов
Оба метода принимают не только строку, но и кортеж строк — тогда достаточно совпадения с любым его элементом.
filename = "photo.webp" allowed = (".jpg", ".png", ".webp") print(filename.endswith(allowed))
True
Это заметно короче и понятнее, чем цепочка через or:
filename.endswith(".jpg") or filename.endswith(".png") or filename.endswith(".webp")
filename.endswith((".jpg", ".png", ".webp"))
§5Почему именно кортеж
Список на этом месте не работает — методы принимают только строку или кортеж.
print("photo.webp".endswith([".jpg", ".png"]))
TypeError: endswith first arg must be str or a tuple of str, not list
Если варианты уже лежат в списке, его превращают в кортеж: filename.endswith(tuple(extensions)). Разница между списком и кортежем разбиралась в главе 4.1.
§6Границы start и end
Оба метода принимают ещё два параметра, ограничивающих проверяемую часть строки. Работают они как границы среза.
text = "prefix:VALUE:suffix" print(repr(text[7:15])) print(text.startswith("VALUE", 7, 15))
'VALUE:su' True
Первая строка показывает, что именно проверяется. С этими границами startswith() означает уже не «строка начинается с», а «часть строки начиная с позиции start начинается с».
§7find(): индекс или -1
find() возвращает индекс первого вхождения подстроки. Если подстроки нет, возвращается -1 — не ошибка, а особое значение.
text = "error: disk; error: network" print(text.find("error")) print(text.find("timeout"))
0 -1
У метода те же необязательные границы start и end, что у count() из главы 5.3. Поиск с позиции — основа приёма из §13.
§8index(): подстрока обязана быть
index() делает ровно то же, но отсутствие подстроки считает ошибкой.
text = "error: disk" print(text.index("timeout"))
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(text.index("timeout"))
^^^^^^^^^^^^^^^^^^^^^
ValueError: substring not found
Это тот же ValueError, что у list.index() из главы 3.11: смысл одинаков — «искали то, чего нет».
§9find() или index()
| Метод | Найдено | Не найдено | Когда выбирать |
|---|---|---|---|
find() | индекс | -1 | отсутствие — нормальный исход |
index() | индекс | ValueError | отсутствие — нарушение формата |
Выбор — это решение о том, что считать ошибкой. Если разбирается строка вида ключ=значение и знака равенства в ней нет, продолжать бессмысленно — уместен index(). Если проверяется, встречается ли слово в тексте пользователя, отсутствие ожидаемо — нужен find() или in.
§10Ловушка проверки результата
Результат find() нельзя проверять на истинность напрямую. Причина — в главе 3.16: 0 считается ложным значением, а любое ненулевое число — истинным.
text = "error: disk" if text.find("error"): print("найдено") else: print("не найдено")
не найдено
| Ситуация | find() вернёт | Как это оценит if |
|---|---|---|
| подстрока в самом начале | 0 | False — хотя найдена |
| подстрока дальше в строке | 5 | True — верно |
| подстроки нет | -1 | True — хотя не найдена |
Два случая из трёх дают неверный ответ. Правильных вариантов два: сравнивать явно — if text.find(part) != -1, — или, если позиция не нужна, использовать in.
Если проверять на строке, где подстрока стоит не в начале, код отработает верно, и проблема останется незамеченной до случая с нулевым индексом.
§11Поиск справа: rfind()
Методы с приставкой r ищут то же самое, но начиная с конца строки, — и возвращают позицию последнего вхождения.
filename = "archive.tar.gz" print(filename.find(".")) print(filename.rfind("."))
7 11
Индекс, который вернул метод, — это позиция самой подстроки, а не следующего за ней символа. rindex() относится к rfind() так же, как index() к find(): при отсутствии он вызывает ValueError.
§12Практика: имя и расширение
Разделить имя файла на части — задача, где выбор между find() и rfind() решает всё.
filename = "archive.tar.gz" dot = filename.rfind(".") print(filename[:dot]) print(filename[dot:])
archive.tar .gz
С обычным find() получилось бы archive и .tar.gz — иногда это тоже нужно, но чаще расширением считают именно последнюю часть. Тот же приём с find() разбирает пару «ключ и значение».
line = "name=Иван" sign = line.find("=") print(repr(line[:sign])) print(repr(line[sign + 1:]))
'name' 'Иван'
Прибавление единицы обязательно: без него знак = попал бы в значение. Для этой задачи есть и готовый метод partition() — он разбирается в главе 5.9.
§13Практика: все вхождения
find() находит только первое вхождение. Чтобы собрать все, поиск повторяют в цикле, каждый раз сдвигая начало.
log = "error: disk; error: network" positions = [] position = log.find("error") while position != -1: positions.append(position) position = log.find("error", position + 1) print(positions)
[0, 13]
- Первый поиск идёт с начала строки.
- Найденная позиция запоминается.
- Следующий поиск начинается со следующего символа — отсюда
position + 1. - Цикл заканчивается, когда
find()вернул-1.
Сдвиг на единицу, а не на длину подстроки, даёт побочный эффект: так считаются и перекрывающиеся вхождения, чего не умеет count() из главы 5.3. Для "aaaa" этот цикл найдёт три вхождения "aa", а count() — два.
§14Поиск учитывает регистр
Как и count(), все методы этой главы сравнивают символы точно.
text = "Python Documentation" query = "PYTHON" print(text.find(query)) print(text.casefold().find(query.casefold()))
-1 0
Важная деталь: индекс, найденный в нормализованной строке, годится и для исходной — casefold() для русского и английского длину не меняет. Но для текста с немецкой ß из главы 5.2 длина изменится, и позиции разойдутся.
§15in, find или count
Три способа спросить о подстроке, и выбирать стоит по тому, что нужно на самом деле.
| Нужно | Запись | Проход по строке |
|---|---|---|
| факт наличия | part in text | до первого совпадения |
| позиция | text.find(part) | до первого совпадения |
| количество | text.count(part) | вся строка целиком |
| начало или конец | text.startswith(part) | только край |
Запись text.find(part) != -1 вместо part in text формально верна, но заставляет читателя разбирать сравнение с числом там, где вопрос был про «да или нет».
§16expandtabs()
Символ табуляции \t — это не «несколько пробелов», а команда «перейти к следующей позиции табуляции». expandtabs() превращает его в реальные пробелы.
row = "id\tname\trole" print(repr(row)) print(repr(row.expandtabs(4)))
'id\tname\trole' 'id name role'
Первая табуляция превратилась в два пробела, вторая — в четыре. Метод дополняет текст до ближайшей позиции, кратной шагу.
| Что вывели | Занято позиций | Ближайшая кратная 4 | Пробелов добавлено |
|---|---|---|---|
id | 2 | 4 | 2 |
id name | 8 | 12 | 4 |
Поэтому одно и то же \t даёт разную ширину: оно выравнивает по колонкам, а не отступает на фиксированное расстояние. Отсюда и практический вывод — смешивать в одном файле табуляции и пробелы не стоит, результат будет зависеть от настроек редактора.
§17Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
if text.find(part): | индекс 0 ложен, а -1 истинен — ответ наоборот | if part in text: |
name.endswith([".jpg", ".png"]) | TypeError: нужен кортеж, а не список | name.endswith((".jpg", ".png")) |
name.endswith((".jpg")) | скобки без запятой — это строка, а не кортеж | name.endswith((".jpg",)) |
text.index(part) при допустимом отсутствии | ValueError останавливает программу | text.find(part) или in |
text[text.find("="):] для значения | знак = попадёт в результат | text[text.find("=") + 1:] |
find() вместо rfind() для расширения | archive.tar.gz разделится по первой точке | rfind(".") |
| ждать поиска без учёта регистра | совпадение не найдётся | нормализовать обе строки |
считать \t фиксированным числом пробелов | ширина зависит от текущей позиции | expandtabs(tabsize) |
§18Проверьте себя
Почему if text.find(part): — ошибка?
Индекс 0 приводится к False, а признак отсутствия -1 — к True. Проверка даёт неверный ответ и когда подстрока в начале, и когда её нет.
Когда index() уместнее find()?
Когда подстрока обязана присутствовать, и её отсутствие означает нарушение формата, которое нужно заметить сразу.
Как проверить несколько расширений одним вызовом?
Передать кортеж: filename.endswith((".jpg", ".png")). Список вызовет TypeError.
Чем rfind() отличается от find()?
Возвращает позицию последнего вхождения, а не первого. Для archive.tar.gz это разница между .tar.gz и .gz.
Как найти все вхождения подстроки?
Циклом: искать с позиции position + 1, пока find() не вернёт -1.
Почему один и тот же \t даёт разное число пробелов?
Табуляция выравнивает текст до ближайшей позиции, кратной шагу, а не добавляет фиксированное количество пробелов.
§19Шпаргалка
text.startswith(prefix)text.endswith(suffix)text.endswith((".a", ".b"))text.find(part)text.index(part)text.rfind(part)if text.find(part) != -1:part in textrow.expandtabs(4)Итог главы
startswith() и endswith() отвечают на булев вопрос и принимают кортеж допустимых вариантов. find() и rfind() возвращают позицию или -1, строгие index() и rindex() вместо этого вызывают ValueError — выбор между ними это решение о том, считать ли отсутствие ошибкой. Результат find() нельзя проверять на истинность: нулевой индекс ложен, а -1 истинен.