Модуль 5 · Строки

Справочник по Python · модуль 5 · строки

Глава 5.5

5.5

Начало, окончание
и поиск в строках

startswith() и endswith() отвечают на булев вопрос «так ли начинается текст». Семейство find() и index() отвечает на другой — «на какой позиции». Разница между ними определяет и выбор метода, и способ проверки результата.

Категория
Строковый поиск
Уровень
Базовый → средний
Связанные темы
Срезы, кортежи, исключения, Unicode
Зачем это знать
Проверять формат текста и находить границы частей

§1Два разных вопроса

О подстроке спрашивают по-разному, и от вопроса зависит инструмент.

ВопросИнструментЧто вернёт
текст начинается с этого?startswith()True или False
текст заканчивается этим?endswith()True или False
подстрока вообще есть?inTrue или False
на какой позиции она стоит?find(), index()число
сколько раз встречается?count()число

Смешение первых трёх строк с четвёртой — источник самой коварной ошибки главы, и §10 разбирает её подробно.

§2startswith(): начало

Метод проверяет, начинается ли строка с указанной подстроки, и возвращает логическое значение.

main.pyпротокол в адресе
url = "https://example.com"

print(url.startswith("https://"))
print(url.startswith("http://"))
Выводвторой вариант не подошёл
True
False

Тот же результат можно получить срезом — url[:8] == "https://", — но такая запись требует вручную считать длину префикса и ломается при её изменении. Метод избавляет от арифметики.

§3endswith(): окончание

Зеркальный метод проверяет конец строки. Типичное применение — расширение файла.

main.pyкакой это формат
filename = "report.csv"

print(filename.endswith(".csv"))
print(filename.endswith(".json"))
Выводрасширение совпало один раз
True
False

Точку в суффиксе лучше не опускать: проверка endswith("csv") пропустит и файл mycsv, у которого расширения нет вовсе.

§4Кортеж вариантов

Оба метода принимают не только строку, но и кортеж строк — тогда достаточно совпадения с любым его элементом.

main.pyодно из трёх расширений
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Почему именно кортеж

Список на этом месте не работает — методы принимают только строку или кортеж.

main.pyпередан список вместо кортежа
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

Оба метода принимают ещё два параметра, ограничивающих проверяемую часть строки. Работают они как границы среза.

main.pyпроверка середины строки
text = "prefix:VALUE:suffix"

print(repr(text[7:15]))
print(text.startswith("VALUE", 7, 15))
Выводвнутри среза префикс совпал
'VALUE:su'
True

Первая строка показывает, что именно проверяется. С этими границами startswith() означает уже не «строка начинается с», а «часть строки начиная с позиции start начинается с».

§7find(): индекс или -1

find() возвращает индекс первого вхождения подстроки. Если подстроки нет, возвращается -1 — не ошибка, а особое значение.

main.pyесть и нет
text = "error: disk; error: network"

print(text.find("error"))
print(text.find("timeout"))
Выводпозиция и признак отсутствия
0
-1

У метода те же необязательные границы start и end, что у count() из главы 5.3. Поиск с позиции — основа приёма из §13.

§8index(): подстрока обязана быть

index() делает ровно то же, но отсутствие подстроки считает ошибкой.

main.pyискомого фрагмента нет
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 считается ложным значением, а любое ненулевое число — истинным.

main.pyпроверка выдаёт противоположное
text = "error: disk"

if text.find("error"):
    print("найдено")
else:
    print("не найдено")
Выводподстрока есть, а ветка выбрана неверная
не найдено
Ситуацияfind() вернётКак это оценит if
подстрока в самом начале0False — хотя найдена
подстрока дальше в строке5True — верно
подстроки нет-1True — хотя не найдена

Два случая из трёх дают неверный ответ. Правильных вариантов два: сравнивать явно — if text.find(part) != -1, — или, если позиция не нужна, использовать in.

Ошибка не проявится на первом тесте

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

§11Поиск справа: rfind()

Методы с приставкой r ищут то же самое, но начиная с конца строки, — и возвращают позицию последнего вхождения.

main.pyпервая и последняя точка
filename = "archive.tar.gz"

print(filename.find("."))
print(filename.rfind("."))
Выводдве разные позиции
7
11

Индекс, который вернул метод, — это позиция самой подстроки, а не следующего за ней символа. rindex() относится к rfind() так же, как index() к find(): при отсутствии он вызывает ValueError.

§12Практика: имя и расширение

Разделить имя файла на части — задача, где выбор между find() и rfind() решает всё.

main.pyточка в имени не одна
filename = "archive.tar.gz"
dot = filename.rfind(".")

print(filename[:dot])
print(filename[dot:])
Выводимя и последнее расширение
archive.tar
.gz

С обычным find() получилось бы archive и .tar.gz — иногда это тоже нужно, но чаще расширением считают именно последнюю часть. Тот же приём с find() разбирает пару «ключ и значение».

main.pyразбор строки настроек
line = "name=Иван"
sign = line.find("=")

print(repr(line[:sign]))
print(repr(line[sign + 1:]))
Выводключ и значение
'name'
'Иван'

Прибавление единицы обязательно: без него знак = попал бы в значение. Для этой задачи есть и готовый метод partition() — он разбирается в главе 5.9.

§13Практика: все вхождения

find() находит только первое вхождение. Чтобы собрать все, поиск повторяют в цикле, каждый раз сдвигая начало.

main.pyпозиции всех совпадений
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]
  1. Первый поиск идёт с начала строки.
  2. Найденная позиция запоминается.
  3. Следующий поиск начинается со следующего символа — отсюда position + 1.
  4. Цикл заканчивается, когда find() вернул -1.

Сдвиг на единицу, а не на длину подстроки, даёт побочный эффект: так считаются и перекрывающиеся вхождения, чего не умеет count() из главы 5.3. Для "aaaa" этот цикл найдёт три вхождения "aa", а count() — два.

§14Поиск учитывает регистр

Как и count(), все методы этой главы сравнивают символы точно.

main.pyзапрос в другом регистре
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() превращает его в реальные пробелы.

main.pyшаг табуляции — четыре позиции
row = "id\tname\trole"

print(repr(row))
print(repr(row.expandtabs(4)))
Выводразное число пробелов на месте табуляций
'id\tname\trole'
'id  name    role'

Первая табуляция превратилась в два пробела, вторая — в четыре. Метод дополняет текст до ближайшей позиции, кратной шагу.

Что вывелиЗанято позицийБлижайшая кратная 4Пробелов добавлено
id242
id name8124

Поэтому одно и то же \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 text
Табуляцииrow.expandtabs(4)

Итог главы

startswith() и endswith() отвечают на булев вопрос и принимают кортеж допустимых вариантов. find() и rfind() возвращают позицию или -1, строгие index() и rindex() вместо этого вызывают ValueError — выбор между ними это решение о том, считать ли отсутствие ошибкой. Результат find() нельзя проверять на истинность: нулевой индекс ложен, а -1 истинен.

Глава 5.5 · Поиск в строках · Справочник по Python Автор: Макаров Максим Николаевич