Это статья про довольно неожиданный результат выполнения программы на python. Матёрым разработчикам она покажется детским лепетом, но для тех, кто изредка использует python как полезный инструмент будет несомненно интересна. Также рекомендую её как гимнастику ума. Чтобы заняться этой гимнастикой могли все желающие не добавлял в статью ни строчки кода.
Недавно мне потребовалось автоматизировать довольно сложный процесс раскладки файлов по каталогам. Опыта в этом у меня довольно немного, но всё шло хорошо. Я написал несколько скриптов bash, которые занимались сжатием/распаковкой и переименовыванием/перемещением файлов, но тут потребовалось получать данные для некоторых операций из текстового файла.
Конкретно задача выглядела так:
1) Взять первую строку файла name.txt, оканчивающуюся подстрокой |some_data
2) Вычленить из неё подстроку some_data
3) Сжать name.txt в архив some_data.zip
Незадолго до этого коллега любезно написал мне программу на Python, реализующую схожий функционал — копирование, с некоторыми условиями, первых строк всех файлов из каталога в один. Я решил слегка подправить эту программу под текущую задачу.
Код, как и обещал, не привожу, только алгоритм. Сразу скажу, что выполняется он абсолютно правильно, именно так, как я и описываю, без ошибок или неточностей.
Алгоритм:
1) Взять первую строку файла name.txt
2) Вычленить из неё всё, после символа '|' и записать в переменную s
3) Удалить из s все переносы строки (символ '\n')
4) Удалить из s все пробелы
5) Если s — пустая строка ('') вывести об этом сообщение и закончить программу
6) Добавить в конец переменной s символы '.zip'
7) Выполнить в консоли «zip [вставить значение s] name.txt»
При выполнении у меня случился экзистенциальный кризис. Программа не создавала файлы вида abcdef.zip, она создавала файлы вида .zipef. То есть вместо добавления .zip к переменной s она выводила '.zip' вместо первых четырёх символов.
Иначе говоря, получалось, что для python 'abcde' + 'fgh' == 'fghde'. Проблема усугублялась тем, что до этого я с python вообще никак не сталкивался и не был уверен, что подобное поведение не норма. В самом деле, берём адрес массива, пишем по этому адресу другой массив и считываем — получили второй массив поверх первого.
К счастью оказалось что это не так и строки должны нормально конкатенироваться.
Для устранения этой проблемы мне потребовалось около часа. Все необходимые данные у вас есть, попробуйте предположить причину этого безобразия.
2) Вычленить из неё всё, после символа '|' и записать в переменную s
3) Удалить из s все переносы строки (символ '\n')
Кое-что критично важное мы не удалили. Символ '\r' — возврат каретки. В итоге происходит вот что:
Пусть s == «abcdef\r». Мы добавили в конец '.zip' и получили «abcdef\r.zip».
Обозначим знаком _ курсор; рассмотрим три этапа вывода строки s:
//выводим 'abcdef'
>abcdef_
//выводим '\r'- курсор переводится
>_abcdef
//печатаем '.zip'
>.zip_ef
Рекомендую в схожих ситуациях проверять вообще все управляющие символы, поскольку создавать файлы с именами типа «File <табуляция>Name» тоже не очень хорошо.
Комментарии (19)
Amomum
02.04.2016 13:56+11) Взять первую строку файла name.txt
2) Вычленить из неё всё, после символа '|' и записать в переменную s
3) Удалить из s все переносы строки (символ '\n')
У меня легкий диссонанс. Если мы взяли строку из файла, разве в ней вообще могут быть символы переноса строки?
Или это зависит от ОСи и того, как именно в ней разделяются строки (CR + LF, LF или еще как-то)?Riateche
02.04.2016 14:14+1Это зависит от реализации метода и того, что написано на эту тему в документации. Вот, например:
file.readline([size])
Read one entire line from the file. A trailing newline character is kept in the string (but may be absent when a file ends with an incomplete line). [6] If the size argument is present and non-negative, it is a maximum byte count (including the trailing newline) and an incomplete line may be returned. (doc)
И такое поведение, в общем-то, распространено не только в Python.
AndrewFoma
02.04.2016 14:20+1отличается и зависит от того как в какой оси каким образом сохранен файл.
В win «обычно» — 2 символа в «конце строки», в nix — нет.
Поэтому например, если считать в список (условно):
1. [row[:-1] for row in open(path,'r')] — win
2. [row for row in open(path,'r')] — nix
AndrewFoma
02.04.2016 14:13+2откровенно, лично я в замешательстве, а проводить проверки на наличии «левых и специальных» символов не надо? В чем загадка, в специальных символах?
ivlis
02.04.2016 20:32-1filename = 'str_{var}'.format(var=var)
Так и только так и никак иначе.JIghtuse
02.04.2016 21:42+1<sarcasm>
А может, так?
filename = f'str_{filename}'
Или так...
filename = ("str_%s" % filename)
Или вот ещё...
from string import Template filename = Template('str_$filename').substitute(filename=filename)
Нет, определённо только вот так, это точно круче всего:
filename = format(i"str_{filename}")
</sarcasm>
А если серьёзно — форматирование строки никак не связано с тем, сделан ли escape:
In [1]: s = "abcdef\r" In [2]: filename = '{filename}.zip'.format(filename = s) In [3]: filename Out[3]: 'abcdef\r.zip' In [4]: print(filename) .zipef
Можно, к примеру, сделать так:
In [5]: filename = (s + ".zip").encode("unicode_escape") In [6]: print(filename) b'abcdef\\r.zip'
Ну или если решили "доверять" пользователю (что делать едва ли стоит), то самый простой способ —strip()
, как упомянул devpony:
In [7]: s Out[7]: 'abcdef\r' In [8]: filename = s.strip() + ".zip" In [9]: filename Out[9]: 'abcdef.zip' In [10]: print(filename) abcdef.zip
ivlis
02.04.2016 21:473.6 только в разработке, 2.x устарел, так что только так :) И если пользовались нормальными выводами юникодных строк, а не принтом, то всё было бы в порядке.
JIghtuse
02.04.2016 22:11Но это всё и в Python3 работает (за исключением PEP498/PEP501, которые в 3.6 планируются). По-моему, 5 способов форматирования строки это таки перебор.
Нормальный вывод или escaping — это на усмотрение. Суть, на мой взгляд, одна — обрабатывать входные данные перед использованием.lybin
03.04.2016 12:38Дзен питона твердит: Должен существовать один — и, желательно, только один — очевидный способ сделать это.
ValdikSS
03.04.2016 00:25+2А причем здесь, собственно, Python? У вас, как я понимаю, файл сохранен с переносами \r\n, а вы вручную только \n убираете.
RomanKharin
03.04.2016 04:40Когда вы что-то читаете из файла удобно применить функцию .rstrip() к каждой строке. Она удалит подобные и пробельные символы справа.
Kwent
Теперь давайте код :)
Source
Код, видимо, такой
И хоть в результате конкатенации получается строка 'abcdef\r.zip' в консоль она действительно будет выводиться как .zipef