image
Это статья про довольно неожиданный результат выполнения программы на 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 вообще никак не сталкивался и не был уверен, что подобное поведение не норма. В самом деле, берём адрес массива, пишем по этому адресу другой массив и считываем — получили второй массив поверх первого.
К счастью оказалось что это не так и строки должны нормально конкатенироваться.

Для устранения этой проблемы мне потребовалось около часа. Все необходимые данные у вас есть, попробуйте предположить причину этого безобразия.
А дело вот в чём
1) Взять первую строку файла name.txt
2) Вычленить из неё всё, после символа '|' и записать в переменную s
3) Удалить из s все переносы строки (символ '\n')
Кое-что критично важное мы не удалили. Символ '\r' — возврат каретки. В итоге происходит вот что:
Пусть s == «abcdef\r». Мы добавили в конец '.zip' и получили «abcdef\r.zip».
Обозначим знаком _ курсор; рассмотрим три этапа вывода строки s:

//выводим 'abcdef'
>abcdef_

//выводим '\r'- курсор переводится
>_abcdef

//печатаем '.zip'
>.zip_ef

Рекомендую в схожих ситуациях проверять вообще все управляющие символы, поскольку создавать файлы с именами типа «File <табуляция>Name» тоже не очень хорошо.

Комментарии (19)


  1. Kwent
    02.04.2016 10:57
    +2

    Теперь давайте код :)


    1. Source
      02.04.2016 11:42
      +3

      Код, видимо, такой

      print 'abcdef\r'+'.zip'

      И хоть в результате конкатенации получается строка 'abcdef\r.zip' в консоль она действительно будет выводиться как .zipef


  1. devpony
    02.04.2016 11:55
    +9

    Потому что пункт 3 нужно было выполнить с помощью метода strip().


  1. zo_oz
    02.04.2016 11:58

    удалил


  1. zo_oz
    02.04.2016 12:00

    Работает везде для второго питона:)

    line.rstrip('\r\n')
    


  1. Amomum
    02.04.2016 13:56
    +1

    1) Взять первую строку файла name.txt
    2) Вычленить из неё всё, после символа '|' и записать в переменную s
    3) Удалить из s все переносы строки (символ '\n')

    У меня легкий диссонанс. Если мы взяли строку из файла, разве в ней вообще могут быть символы переноса строки?
    Или это зависит от ОСи и того, как именно в ней разделяются строки (CR + LF, LF или еще как-то)?


    1. 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.


    1. 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


  1. AndrewFoma
    02.04.2016 14:13
    +2

    откровенно, лично я в замешательстве, а проводить проверки на наличии «левых и специальных» символов не надо? В чем загадка, в специальных символах?


  1. ivlis
    02.04.2016 20:32
    -1

    filename = 'str_{var}'.format(var=var)

    Так и только так и никак иначе.


    1. 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


      1. ivlis
        02.04.2016 21:47

        3.6 только в разработке, 2.x устарел, так что только так :) И если пользовались нормальными выводами юникодных строк, а не принтом, то всё было бы в порядке.


        1. JIghtuse
          02.04.2016 22:11

          Но это всё и в Python3 работает (за исключением PEP498/PEP501, которые в 3.6 планируются). По-моему, 5 способов форматирования строки это таки перебор.
          Нормальный вывод или escaping — это на усмотрение. Суть, на мой взгляд, одна — обрабатывать входные данные перед использованием.


          1. lybin
            03.04.2016 12:38

            Дзен питона твердит: Должен существовать один — и, желательно, только один — очевидный способ сделать это.


  1. ivlis
    02.04.2016 21:47

    del


  1. ValdikSS
    03.04.2016 00:25
    +2

    А причем здесь, собственно, Python? У вас, как я понимаю, файл сохранен с переносами \r\n, а вы вручную только \n убираете.


  1. DeKaNszn
    03.04.2016 04:40
    +1

    Удалить из s все переносы строки (символ '\n')
    s.strip() решило бы проблему на этом этапе


    1. DeKaNszn
      03.04.2016 08:48

      Комментарий устарел, пока ждал одобрения.


  1. RomanKharin
    03.04.2016 04:40

    Когда вы что-то читаете из файла удобно применить функцию .rstrip() к каждой строке. Она удалит подобные и пробельные символы справа.