2025, Oct 06 09:16
Ошибка WinError 3 при сортировке .mp3 в Windows: mutagen, shutil и пробел в конце пути
Почему при сортировке .mp3 на Windows с mutagen и shutil возникает WinError 3: пробел в конце имени каталога после разбиения регуляркой. Решение — strip().
Упорядочивание аудиофайлов по метаданным — типичная задача автоматизации, и связка mutagen с shutil в Python здесь очень кстати. Однако в Windows одна на вид пустяковая деталь способна сорвать отлаженный процесс: пробел в конце имени каталога, полученного из тега. Если ваш скрипт выбрасывает WinError 3 при перемещении отдельных .mp3 — особенно когда в теге исполнителя встречаются амперсанд или скобки, — причина, скорее всего, не в самих метаданных, а в том, что с ними происходит после разбиения регулярным выражением.
Как воспроизвести проблему
Логика проста: собрать файлы в рабочей директории, прочитать тег исполнителя, нормализовать его до «главного» артиста, создать каталог под этого исполнителя и переместить файл. Ниже показан фрагмент, в котором и возникает сбой.
import mutagen
import os
import re
import shutil
work_root = os.getcwd()
entry_names = [p for p in os.listdir('.') if os.path.isfile(p)]
for name in entry_names:
    try:
        meta = mutagen.File(name, easy=True)
        if meta is not None and "artist" in meta:
            lead_artist = re.split(r";|,|&|\(|/|\sfeat\.|\sFeat\.|\sft|\sFt|\sx\s", meta["artist"][0])[0].upper()
        else:
            lead_artist = "UNKNOWN"
    except Exception as err:
        lead_artist = "UNKNOWN"
    target_dir = os.path.join(work_root, lead_artist)
    source_path = os.path.join(work_root, name)
    os.makedirs(lead_artist, exist_ok=True)
    if source_path[-2:] != 'py':
        try:
            shutil.move(source_path, target_dir)
            print("moved ", source_path, " to ", target_dir)
        except Exception as err:
            print("Error on: ", name, " -> ", err)
            print(source_path, os.path.exists(source_path))
            print(target_dir, os.path.exists(target_dir))
Что именно идёт не так
Представьте тег исполнителя вроде «Mumford & Sons». Регулярное выражение делит строку по разделителям — амперсанду, открывающей скобке и т. п. — чтобы выделить первого упомянутого артиста. В этом случае разбиение по «&» даёт левую часть «Mumford » — обратите внимание на пробел в конце. Незаметная деталь, но решающая.
Windows по‑особому обращается с пробелами в конце имён каталогов. Когда os.makedirs вызывают с путём, оканчивающимся пробелом, этот пробел игнорируется, и папка создаётся без него. То есть из «MUMFORD » на диске появляется каталог «MUMFORD». Позже shutil.move (который использует os.rename) пытается переместить файл в назначение, где в строке по‑прежнему присутствует хвостовой пробел. Несоответствие между запрошенным путём и реальным именем каталога приводит к ошибке «путь не найден» (WinError 3).
Проблема не в метаданных как таковых; она возникает из‑за сочетания разбиения регуляркой и пробела в конце полученного имени каталога. ОС тихо отбрасывает этот пробел при создании папки, а последующее перемещение ссылается на путь, которого не существует.
Этим же объясняется и путаница в диагностике, когда оба выведенных пути кажутся корректными по проверкам os.path.exists. Каталог действительно создан (но без завершающего пробела), тогда как операция перемещения ориентируется на строку с пробелом на конце, которой не соответствует никакой реальный путь.
Как исправить
Нормализуйте полученное имя исполнителя, обрезав пробелы после применения регулярного выражения. Простого strip() достаточно, чтобы убрать хвостовой пробел перед приведением к верхнему регистру и использованием в имени каталога.
import mutagen
import os
import re
import shutil
work_root = os.getcwd()
entry_names = [p for p in os.listdir('.') if os.path.isfile(p)]
for name in entry_names:
    try:
        meta = mutagen.File(name, easy=True)
        if meta is not None and "artist" in meta:
            first_part = re.split(r";|,|&|\(|/|\sfeat\.|\sFeat\.|\sft|\sFt|\sx\s", meta["artist"][0])[0]
            lead_artist = first_part.strip().upper()
        else:
            lead_artist = "UNKNOWN"
    except Exception as err:
        lead_artist = "UNKNOWN"
    target_dir = os.path.join(work_root, lead_artist)
    source_path = os.path.join(work_root, name)
    os.makedirs(lead_artist, exist_ok=True)
    if source_path[-2:] != 'py':
        try:
            shutil.move(source_path, target_dir)
            print("moved ", source_path, " to ", target_dir)
        except Exception as err:
            print("Error on: ", name, " -> ", err)
            print(source_path, os.path.exists(source_path))
            print(target_dir, os.path.exists(target_dir))
Почему это важно
При построении систем управления файлами пути часто выводятся из полуструктурированных данных: тегов, имён файлов или пользовательского ввода. На вид безобидные символы — вроде пробелов вокруг разделителей — могут привести к платформенным особенностям, которые трудно отследить, потому что промежуточные операции выглядят успешными. Здесь создание каталога тихо нормализует путь, а последующий перенос сохраняет исходную строку. Если не фиксировать точные строки на каждом шаге, причина сбоя кажется загадочной.
Это также напоминание: простая нормализация должна выполняться сразу после парсинга и до любых действий с файловой системой. Обрезка пробелов — минимальное изменение, которое предотвращает рассинхронизацию между задуманными и реальными путями в Windows.
Вывод
Если при обработке .mp3 на Windows всплывает ошибка пути и в именах исполнителей встречаются «&» или «(», проверьте нормализацию вычисленных имён каталогов. Применяйте strip() к фрагменту, извлечённому регулярным выражением, прежде чем передавать его в os.makedirs и shutil.move. Если что‑то по‑прежнему не сходится, выведите точные строки источника и назначения и проверьте их через os.path.exists — так вы увидите реальные значения, которые получает ОС. Это сделает автоматизацию устойчивой к тонким проблемам с пробелами в тегах и обеспечит стабильные, предсказуемые перемещения по всей вашей медиатеке.
Статья основана на вопросе с StackOverflow от jstarr11235 и ответе пользователя OldBoy.