Модуль fnmatch в Python: поиск файлов по шаблонам в стиле Unix
Для кого эта статья:
- Python-разработчики, стремящиеся улучшить свои навыки работы с файлами в Python
- Начинающие программисты, ищущие простые решения для автоматизации задач
Специалисты по DevOps и автоматизации, заинтересованные в оптимизации своих скриптов для работы с файлами
Работа с файлами в Python часто превращается в кошмар, когда нужно оперировать сотнями или тысячами объектов с разными именами. Искать конкретные файлы, соответствующие определённому шаблону, можно регулярными выражениями, но это избыточно и сложно. Модуль fnmatch — это элегантное решение, которое упрощает поиск файлов по шаблонам в стиле командной оболочки Unix. Это мощный и при этом простой инструмент, незаменимый при автоматизации работы с файловой системой. 🔍
Хотите научиться эффективно работать с файловыми системами и освоить все тонкости Python? Обучение Python-разработке от Skypro предлагает глубокое погружение в работу с файлами, шаблонами и регулярными выражениями. Наши эксперты научат вас не просто использовать модуль fnmatch, но и создавать мощные скрипты автоматизации, обработки данных и файлового менеджмента, которые сэкономят часы вашего времени.
Модуль fnmatch: основное предназначение и характеристики
Модуль fnmatch входит в стандартную библиотеку Python и предназначен для сопоставления имен файлов с шаблонами в стиле оболочки Unix (shell-style wildcards). Он решает простую, но важную задачу: определить, соответствует ли строка заданному шаблону.
Ключевые особенности модуля fnmatch:
- Использует упрощённый синтаксис шаблонов по сравнению с регулярными выражениями
- Поддерживает базовые метасимволы (* ? [] и [!])
- Интегрирован с файловой системой для более эффективного поиска
- Может работать как с учётом регистра, так и без него
- Имеет высокую производительность благодаря внутренней конвертации шаблонов в регулярные выражения
Важно понимать, что fnmatch — это не инструмент для поиска файлов на диске. Он только определяет, соответствует ли имя файла шаблону. Для непосредственного поиска файлов его необходимо комбинировать с функциями из модулей os, glob или pathlib.
| Функция | Описание | Учёт регистра |
|---|---|---|
| fnmatch() | Проверяет соответствие имени файла шаблону | Зависит от ОС |
| fnmatchcase() | Проверяет соответствие с учётом регистра | Всегда учитывает |
| filter() | Фильтрует список имен файлов по шаблону | Зависит от ОС |
| translate() | Преобразует шаблон в регулярное выражение | Не применимо |
Антон Васильев, Python-разработчик с опытом автоматизации
Помню случай, когда мне поручили обработать архив научных статей с тысячами файлов разных форматов. Требовалось отфильтровать только PDF-документы с определенной структурой имени: "DATAYYYYMM_*.pdf". Первое решение с использованием регулярных выражений работало, но выглядело громоздко:
PythonСкопировать кодimport re, os pattern = r"DATA_\d{4}_\d{2}_.*\.pdf$" matching_files = [f for f in os.listdir(".") if re.match(pattern, f)]После перехода на fnmatch код стал элегантнее:
PythonСкопировать кодimport fnmatch, os matching_files = fnmatch.filter(os.listdir("."), "DATA_????_??_*.pdf")Это не только сократило код, но и повысило читаемость. Для коллег, не знакомых с синтаксисом регулярных выражений, шаблон fnmatch оказался интуитивно понятным, что улучшило поддерживаемость скрипта.

Базовые функции fnmatch() и fnmatchcase() с кодом
Две основные функции модуля fnmatch — это fnmatch() и fnmatchcase(). Они служат для проверки соответствия строки заданному шаблону, но с разным подходом к регистру символов.
Функция fnmatch() определяет, соответствует ли имя файла шаблону, используя правила сравнения, свойственные операционной системе. В Windows сравнение происходит без учета регистра, а в Unix/Linux — с учетом регистра:
import fnmatch
# Базовое сравнение строки с шаблоном
result = fnmatch.fnmatch('example.txt', '*.txt') # True
result = fnmatch.fnmatch('example.doc', '*.txt') # False
# Работа с метасимволами
result = fnmatch.fnmatch('file-123.txt', 'file-???.txt') # True
result = fnmatch.fnmatch('config.ini', '*.[iI][nN][iI]') # True
# В Windows независимо от регистра
# result = fnmatch.fnmatch('Example.TXT', '*.txt') # True в Windows, False в Unix
Функция fnmatchcase() всегда выполняет сравнение с учетом регистра символов, независимо от операционной системы:
import fnmatch
# Сравнение с учетом регистра (case-sensitive)
result = fnmatch.fnmatchcase('example.txt', '*.txt') # True
result = fnmatch.fnmatchcase('Example.txt', '*.txt') # False
result = fnmatch.fnmatchcase('EXAMPLE.TXT', '*.txt') # False
# Использование групп символов
result = fnmatch.fnmatchcase('file-a.log', 'file-[abc].log') # True
result = fnmatch.fnmatchcase('file-d.log', 'file-[abc].log') # False
result = fnmatch.fnmatchcase('file-A.log', 'file-[abc].log') # False
Эти функции особенно полезны, когда нужно быстро проверить соответствие имени файла определенному шаблону без необходимости писать сложные регулярные выражения. 📄
Важные моменты, которые следует учитывать при использовании этих функций:
- fnmatch() может вести себя по-разному в зависимости от платформы
- fnmatchcase() гарантирует единообразное поведение на всех платформах
- Обе функции возвращают булево значение (True/False)
- Шаблоны применяются ко всей строке, а не к отдельным частям
- Внутренне шаблоны преобразуются в регулярные выражения для более быстрого выполнения
Поиск и фильтрация файлов с помощью filter()
Функция filter() модуля fnmatch позволяет отфильтровать список имен файлов, оставив только те, которые соответствуют заданному шаблону. Это удобное решение для выборки нужных файлов из большого списка.
import fnmatch
import os
# Получить все .py файлы в текущем каталоге
python_files = fnmatch.filter(os.listdir('.'), '*.py')
# Найти файлы логов с определенным форматом имени
log_files = fnmatch.filter(os.listdir('/var/log'), 'app_*.log')
# Работа с вложенными директориями
all_files = []
for root, dirs, files in os.walk('/project'):
python_files = fnmatch.filter(files, '*.py')
all_files.extend([os.path.join(root, file) for file in python_files])
Функция filter() по сути представляет собой удобную обертку над более низкоуровневым кодом. Её реализация эквивалентна следующему выражению списка:
def my_filter(names, pattern):
return [name for name in names if fnmatch.fnmatch(name, pattern)]
Однако использование встроенной функции filter() обычно более эффективно и читаемо. 🧪
Максим Петров, DevOps-инженер
В одном из проектов мне пришлось настраивать систему мониторинга логов для микросервисной архитектуры. У нас было более 20 микросервисов, каждый генерировал логи в нескольких форматах: access[service].log, error[service].log и debug[service]*.log.
Нужно было ежедневно архивировать только логи ошибок старше 3 дней. Начальное решение с regexp было функциональным, но громоздким:
PythonСкопировать кодimport os import re import time from datetime import datetime, timedelta current_time = time.time() three_days_ago = current_time – (3 * 24 * 60 * 60) for filename in os.listdir('/var/logs'): if re.match(r'error_.*_\d{4}-\d{2}-\d{2}\.log', filename): file_path = os.path.join('/var/logs', filename) if os.path.getmtime(file_path) < three_days_ago: # архивирование файлаПереписав код с использованием fnmatch, я получил более компактное и понятное решение:
PythonСкопировать кодimport os import fnmatch import time from datetime import datetime, timedelta current_time = time.time() three_days_ago = current_time – (3 * 24 * 60 * 60) # Получаем только логи ошибок error_logs = fnmatch.filter(os.listdir('/var/logs'), 'error_*_????-??-??.log') for log_file in error_logs: file_path = os.path.join('/var/logs', log_file) if os.path.getmtime(file_path) < three_days_ago: # архивирование файлаЭто решение не только сократило код, но и ускорило его выполнение, поскольку мы предварительно отфильтровали только нужные файлы, а затем обрабатывали уже меньший набор данных.
Дополнительные приемы использования filter():
- Комбинирование с другими функциями обработки списков (map, sorted)
- Многоуровневая фильтрация с помощью последовательных вызовов
- Применение нескольких шаблонов с помощью списковых включений
- Фильтрация по нескольким критериям
# Многоуровневая фильтрация
files = os.listdir('.')
python_files = fnmatch.filter(files, '*.py')
test_files = fnmatch.filter(python_files, 'test_*')
# Применение нескольких шаблонов
patterns = ['*.py', '*.ipynb', '*.pyx']
python_related = [f for f in os.listdir('.') if any(fnmatch.fnmatch(f, pattern) for pattern in patterns)]
# Фильтрация с дополнительными условиями
recent_python_files = [
f for f in fnmatch.filter(os.listdir('.'), '*.py')
if os.path.getmtime(f) > time.time() – 86400 # файлы за последние 24 часа
]
Синтаксис шаблонов и сравнение с регулярными выражениями
Синтаксис шаблонов fnmatch намного проще регулярных выражений, что делает его более доступным для быстрой разработки и обслуживания кода. Однако это также означает, что возможности шаблонов fnmatch ограничены по сравнению с мощью регулярных выражений.
Основные метасимволы шаблонов fnmatch:
| Метасимвол | Описание | Пример | Эквивалент в regex |
|---|---|---|---|
| * | Соответствует любому количеству символов (включая ноль) | *.txt | .\.txt$ |
| ? | Соответствует ровно одному символу | file?.txt | file.\.txt$ |
| [...] | Соответствует любому символу из указанных в скобках | file[abc].txt | file[abc]\.txt$ |
| [!... ] | Соответствует любому символу, кроме указанных в скобках | file[!abc].txt | fileabc\.txt$ |
Сравнение шаблонов fnmatch с регулярными выражениями:
- Простота 🎯: Шаблоны fnmatch проще читать и писать, что снижает вероятность ошибок
- Ограниченность: Fnmatch не поддерживает сложные конструкции (квантификаторы, группировку, опережающие проверки)
- Производительность: Для простых случаев fnmatch может быть эффективнее, так как не требует сложного парсинга
- Конвертируемость: Функция translate() преобразует шаблон fnmatch в регулярное выражение
- Ориентация на файлы: Шаблоны fnmatch специально оптимизированы для работы с именами файлов
Примеры задач, для которых лучше подходят шаблоны fnmatch или регулярные выражения:
import fnmatch
import re
# Задача 1: Проверить, заканчивается ли файл расширением .txt
filename = "document.txt"
# Вариант с fnmatch (простой и читаемый)
fnmatch_result = fnmatch.fnmatch(filename, "*.txt")
# Вариант с регулярными выражениями (избыточный для этой задачи)
regex_result = bool(re.match(r".*\.txt$", filename))
# Задача 2: Проверить, содержит ли имя файла дату в формате YYYY-MM-DD
filename = "report-2023-05-15.pdf"
# Вариант с fnmatch (ограниченные возможности)
fnmatch_limited = fnmatch.fnmatch(filename, "*-????-??-??.pdf") # Работает, но не проверяет валидность даты
# Вариант с регулярными выражениями (более точная валидация)
regex_precise = bool(re.match(r".*-\d{4}-\d{2}-\d{2}\.pdf$", filename))
# Еще точнее с проверкой значений:
regex_validation = bool(re.match(r".*-(20\d{2})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])\.pdf$", filename))
Функция translate() позволяет увидеть, как fnmatch преобразует шаблон в регулярное выражение под капотом:
import fnmatch
# Преобразование шаблона fnmatch в регулярное выражение
pattern = "*.py"
regex_pattern = fnmatch.translate(pattern)
print(regex_pattern) # Выводит: '(?s:.*\\.py)\\Z'
pattern = "file-[0-9]?.txt"
regex_pattern = fnmatch.translate(pattern)
print(regex_pattern) # Выводит: '(?s:file\\-[0-9].?\\.txt)\\Z'
Практические сценарии использования fnmatch в реальных задачах
Модуль fnmatch находит применение во множестве практических задач, связанных с работой с файловой системой. Рассмотрим несколько типичных сценариев, где этот модуль особенно полезен. 🛠️
1. Рекурсивный поиск файлов определенного типа
import os
import fnmatch
def find_files(directory, pattern):
matches = []
for root, dirnames, filenames in os.walk(directory):
for filename in fnmatch.filter(filenames, pattern):
matches.append(os.path.join(root, filename))
return matches
# Найти все изображения в формате JPEG
jpeg_files = find_files('/path/to/photos', '*.jpg')
# Найти файлы конфигурации
config_files = find_files('/etc', '*.conf')
2. Группировка файлов по расширениям
import os
import fnmatch
from collections import defaultdict
def group_by_extension(directory):
file_groups = defaultdict(list)
for filename in os.listdir(directory):
if os.path.isfile(os.path.join(directory, filename)):
for pattern in ['*.txt', '*.pdf', '*.csv', '*.py', '*.js']:
if fnmatch.fnmatch(filename, pattern):
ext = pattern[1:] # Удаляем звездочку
file_groups[ext].append(filename)
break
else:
file_groups['other'].append(filename)
return file_groups
# Пример использования
groups = group_by_extension('/path/to/documents')
print(f"Найдено {len(groups['pdf'])} PDF-файлов")
print(f"Найдено {len(groups['txt'])} текстовых файлов")
3. Фильтрация логов для анализа
import fnmatch
import gzip
import os
from datetime import datetime, timedelta
def analyze_logs(log_dir, days_back=7):
# Получаем дату, от которой нужно анализировать логи
start_date = datetime.now() – timedelta(days=days_back)
date_format = start_date.strftime('%Y%m%d')
# Шаблон для файлов логов, которые нас интересуют
log_pattern = f'app_log_{date_format}*.gz'
error_count = 0
for filename in fnmatch.filter(os.listdir(log_dir), log_pattern):
filepath = os.path.join(log_dir, filename)
with gzip.open(filepath, 'rt') as f:
for line in f:
if 'ERROR' in line:
error_count += 1
return error_count
# Пример использования
errors = analyze_logs('/var/log', days_back=3)
print(f"Обнаружено {errors} ошибок в логах за последние 3 дня")
4. Выполнение операций над группой файлов
import os
import fnmatch
import shutil
def process_by_pattern(source_dir, target_dir, pattern, operation='copy'):
"""
Выполняет операцию над файлами, соответствующими шаблону.
:param source_dir: Исходная директория
:param target_dir: Целевая директория
:param pattern: Шаблон для файлов
:param operation: Операция ('copy' или 'move')
"""
if not os.path.exists(target_dir):
os.makedirs(target_dir)
matching_files = fnmatch.filter(os.listdir(source_dir), pattern)
for filename in matching_files:
source_path = os.path.join(source_dir, filename)
target_path = os.path.join(target_dir, filename)
if operation == 'copy':
shutil.copy2(source_path, target_path)
print(f"Копирование: {filename}")
elif operation == 'move':
shutil.move(source_path, target_path)
print(f"Перемещение: {filename}")
return len(matching_files)
# Копирование всех PDF файлов в архив
processed = process_by_pattern('/work/documents', '/archive/2023', '*.pdf')
print(f"Обработано {processed} файлов")
5. Создание простой системы управления пакетами
import os
import fnmatch
import zipfile
import json
def build_package(src_dir, package_name, patterns=None):
"""
Создает пакет из файлов, соответствующих шаблонам.
:param src_dir: Исходная директория с файлами
:param package_name: Имя создаваемого пакета
:param patterns: Список шаблонов для включения файлов
:return: Путь к созданному пакету
"""
if patterns is None:
patterns = ['*.py'] # По умолчанию включаем только Python файлы
# Создаем временную директорию для файлов пакета
temp_dir = os.path.join(os.getcwd(), 'temp_package')
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
# Собираем все файлы, соответствующие шаблонам
package_files = []
for root, _, files in os.walk(src_dir):
for pattern in patterns:
for filename in fnmatch.filter(files, pattern):
source_path = os.path.join(root, filename)
relative_path = os.path.relpath(source_path, src_dir)
package_files.append((source_path, relative_path))
# Создаем метаданные пакета
metadata = {
"name": package_name,
"version": "1.0.0",
"files": [rel_path for _, rel_path in package_files],
"patterns": patterns
}
# Записываем метаданные
with open(os.path.join(temp_dir, 'package.json'), 'w') as f:
json.dump(metadata, f, indent=2)
# Создаем zip-архив
package_path = f"{package_name}.zip"
with zipfile.ZipFile(package_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# Добавляем метаданные
zipf.write(os.path.join(temp_dir, 'package.json'), 'package.json')
# Добавляем файлы пакета
for source_path, relative_path in package_files:
zipf.write(source_path, relative_path)
return package_path
# Пример создания пакета
package_file = build_package('/path/to/project', 'my_package', ['*.py', '*.md', 'data/*.csv'])
print(f"Пакет создан: {package_file}")
Использование fnmatch для поиска и фильтрации файлов может значительно упростить ваш код, сделать его более читаемым и понятным. Этот модуль особенно ценен, когда вам нужно быстро отфильтровать файлы по определенным шаблонам без необходимости писать сложные регулярные выражения. Помните, что для более сложных сценариев (например, когда требуется глубокая валидация или извлечение данных из строк) вам, скорее всего, понадобится модуль re. Однако для работы с именами файлов fnmatch остается простым и эффективным решением, которое стоит иметь в вашем арсенале Python-инструментов. 🐍