Поиск дубликатов данных в Python: короткое руководство
Как найти повторы в списках, строках CSV и таблицах pandas за несколько строк кода. Готовые фрагменты, которые можно скопировать и запустить прямо сейчас.

К концу этого руководства вы сможете находить дубликаты в трёх типичных случаях: повторяющиеся элементы в списке, полные и частичные повторы строк в таблице, а также похожие (но не идентичные) записи. На всё уйдёт минут пятнадцать. Код проверен на Python 3.12 и pandas 2.x.
Что нужно до начала
- Python 3.10 или новее — проверьте командой
python --version. Должна вывестись строка видаPython 3.12.3. - Для работы с таблицами — библиотека pandas. Установите её:
pip install pandas. Для стандартных списков и строк pandas не нужен. - Файл с данными, если работаете не со списком в коде, а с CSV или Excel.
Дубликаты бывают двух видов, и путать их дорого: точные (строки совпадают символ в символ) и нечёткие ("Иванов И. И." и "Иванов Иван Иванович"). Первые ищутся встроенными средствами за секунды. Вторые требуют отдельного подхода — о них в конце.
Шаг 1. Дубликаты в обычном списке
Самый частый случай: есть список, нужно понять, есть ли в нём повторы и какие. Множество (set) хранит только уникальные значения, поэтому сравнение длин сразу показывает наличие дублей.
# Python 3.12
from collections import Counter
emails = ["a@mail.ru", "b@mail.ru", "a@mail.ru", "c@mail.ru", "b@mail.ru"]
# Есть ли вообще дубликаты
est_dubli = len(emails) != len(set(emails))
print(est_dubli) # True
# Какие именно значения повторяются и сколько раз
schetchik = Counter(emails)
povtory = {znachenie: kolvo for znachenie, kolvo in schetchik.items() if kolvo > 1}
print(povtory) # {'a@mail.ru': 2, 'b@mail.ru': 2}
Проверка: в консоли должно появиться True, а затем словарь с повторяющимися адресами и числом их вхождений. Если словарь пустой ({}) — дубликатов нет.
Counter из модуля collections входит в стандартную библиотеку, ставить ничего не нужно. Он проходит по списку один раз и складывает счётчики — это быстрее, чем считать list.count() в цикле для каждого элемента.
Шаг 2. Список из словарей и объектов
Если элементы — не строки, а словари (например, распарсенный JSON), set() напрямую не сработает: словари не хешируются. Приведите каждую запись к неизменяемому виду — например, к кортежу отсортированных пар.
# Python 3.12
zapisi = [
{"id": 1, "name": "Анна"},
{"id": 2, "name": "Борис"},
{"id": 1, "name": "Анна"},
]
vidennye = set()
dublikaty = []
for zapis in zapisi:
klyuch = tuple(sorted(zapis.items()))
if klyuch in vidennye:
dublikaty.append(zapis)
else:
vidennye.add(klyuch)
print(dublikaty) # [{'id': 1, 'name': 'Анна'}]
Проверка: в списке dublikaty окажется каждая запись, которая встретилась не в первый раз. Первое вхождение в него не попадает — только повторы.
Одно предложение почему так: tuple(sorted(...)) делает из словаря хешируемый ключ, а сортировка гарантирует, что порядок ключей в словаре не повлияет на сравнение.
Шаг 3. Дубликаты строк в таблице через pandas
Когда данные — это CSV или Excel, удобнее загрузить их в pandas и работать методами DataFrame. Метод duplicated() возвращает булеву маску: True для строк, которые уже встречались выше.
# Python 3.12, pandas 2.x
import pandas as pd
df = pd.read_csv("clients.csv")
# Полные дубликаты строк (совпадают все столбцы)
maska = df.duplicated(keep=False)
print(df[maska]) # покажет все строки, у которых есть двойник
# Сколько всего дублирующихся строк
print(df.duplicated().sum())
Параметр keep управляет тем, что считать дубликатом:
keep="first"(по умолчанию) — первое вхождение НЕ помечается, помечаются только последующие;keep="last"— наоборот, первое помечается, последнее нет;keep=False— помечаются ВСЕ строки, у которых есть хотя бы один двойник.
Проверка: df[maska] выведет подтаблицу с повторами. Если она пустая — Empty DataFrame в выводе — точных дублей нет.
Дубликаты по части столбцов
Часто строки не совпадают целиком, но совпадают по ключевым полям — например, один и тот же телефон при разном написании имени. Передайте список столбцов в аргумент subset.
# pandas 2.x
# Дубли по одному столбцу
df_po_telefonu = df[df.duplicated(subset=["phone"], keep=False)]
# Дубли по комбинации столбцов
df_po_param = df[df.duplicated(subset=["phone", "email"], keep=False)]
# Удалить дубликаты, оставив первое вхождение
df_chistyy = df.drop_duplicates(subset=["phone"], keep="first")
Полезно перед сравнением привести данные к единому виду, иначе +7 900 и +7900 посчитаются разными:
df["phone"] = df["phone"].astype(str).str.replace(r"\D", "", regex=True)
Эта строка убирает из телефона всё, кроме цифр, до сравнения. Тот же приём — .str.strip().str.lower() — спасает от дублей вроде "Москва " и "москва".
Большинство "неуловимых" дубликатов — это не проблема алгоритма, а грязь в данных: лишние пробелы, разный регистр, разные форматы одного и того же телефона. Нормализация перед сравнением ловит их лучше любой умной функции.
Когда дубликаты не совпадают дословно
Точные методы бессильны против "ООО Ромашка" и "ООО \"Ромашка\"". Здесь нужна оценка похожести строк. В стандартной библиотеке для этого есть difflib — ставить ничего не надо, хотя для больших объёмов он медленный.
# Python 3.12
from difflib import SequenceMatcher
def pohozhest(a: str, b: str) -> float:
# Возвращает число от 0 до 1: 1.0 — строки идентичны
return SequenceMatcher(None, a, b).ratio()
print(round(pohozhest("ООО Ромашка", 'ООО "Ромашка"'), 2)) # ~0.88
Порог, выше которого пару считать дублем, подбирается под данные: для коротких названий 0.85 может давать ложные срабатывания, для длинных адресов работает надёжнее. Сравнение каждой строки с каждой — это операция порядка n², поэтому на десятках тысяч записей difflib заметно тормозит. Для таких объёмов смотрят в сторону специализированных библиотек нечёткого сравнения; их выбор — отдельная тема, здесь фиксируем только стандартный инструмент.
Какой способ выбрать
| Данные | Инструмент | Когда подходит |
|---|---|---|
| Список строк или чисел | set / Counter | Точные повторы, до сотен тысяч элементов |
| Список словарей | tuple(sorted(...)) + set | Нехешируемые элементы, точное совпадение |
| Таблица CSV/Excel | pandas duplicated | Строки и наборы столбцов, десятки–сотни тысяч строк |
| Похожие, но не равные строки | difflib.SequenceMatcher | Нечёткое сравнение, небольшие объёмы |
Если не получилось
TypeError: unhashable type: 'dict'— вы передали вset()список словарей. Приведите каждую запись к кортежу, как в шаге 2.FileNotFoundError: [Errno 2] No such file or directory: 'clients.csv'— pandas ищет файл относительно текущей папки запуска. Укажите полный путь или положите файл рядом со скриптом.- Дубликаты "не находятся", хотя глазами видны — почти всегда это разный регистр, пробелы или формат. Нормализуйте столбец через
.str.strip().str.lower()передduplicated(). UnicodeDecodeErrorприread_csv— файл в кодировке, отличной от UTF-8. Попробуйтеpd.read_csv("clients.csv", encoding="cp1251")для файлов из Excel по-русски.- pandas считает пустые ячейки (
NaN) разными — по умолчаниюNaNне равенNaN. Перед поиском заполните пропуски:df = df.fillna("").
Что делать дальше
Определитесь, что происходит с найденными дублями. Если нужно их удалить — переходите к drop_duplicates() с осознанным выбором keep. Если данные приходят регулярно, вынесите нормализацию (обрезку пробелов, приведение регистра, чистку телефонов) в отдельную функцию и прогоняйте её сразу после загрузки — тогда дубли будут ловиться стабильно, а не от случая к случаю. Для регулярной проверки больших таблиц смотрите на индексы в базе данных с ограничением уникальности: дешевле не пустить дубль внутрь, чем искать его потом.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: pandas.DataFrame.duplicated — документация pandas, difflib — Helpers for computing deltas, документация Python
Частые вопросы
У меня Python 3.8, код со шагов запустится?
duplicated() ничего не находит, хотя строки на вид одинаковые. Почему?
Чем drop_duplicates отличается от duplicated?
Как найти дубликаты не по всей строке, а по одному столбцу?
difflib медленный на больших данных, что делать?
Найденные дубли лучше удалять сразу в исходном файле?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.