Как-то раз я уже веселил народ питоновой задачкой про move_month. Было весело и полезно, мне понравилось! Ловите новую задачку.

Дано:

Задачка в том, чтобы отрезать от начала имени файла тот префикс, по которому он был найден. То есть, из

sys.path = ['/home/projects', '/home/projects/domination', '/usr/lib/python']
filename = '/home/projects/domination/world/nuclear_winter.py'

должно получится:

world/nuclear_winter.py

Оптимизировать будем по скорости и элегантности. Читаемость, как и в прошлый раз, условие не обязательное (иначе никакого веселья!), но как обычно в питоньем мире — это бонус.

И заодно расскажу практическую пользу. Это часть задачи записи ошибок веб-приложения в лог. Чтобы лог было удобно grep'ать, он однострочный, а следовательно весь traceback в него записывать нельзя (технически-то можно, но получается каша). Однако полезно вместе с текстом exception'а всё же писать имя файла и номер строки, где она произошла. Ну и в целях сокращения упомянутой каши хочется оторвать неинтересные участки путей от имён файлов.

Комментарии: 32

  1. ilih.livejournal.com

    из

    sys.path = ['/home/projects', '/home/projects/domintation', '/usr/lib/python']
    filename = '/home/projects/domination/world/nuclear_winter.py'

    не выйдет

    world/nuclear_winter.py

    опечатка в "domintation"

    простейшее решение

    def del_prefix(path, filename):
        path.sort(key=len, reverse=True)
        for folder in path:
            if filename.startswith(folder):
                return filename[len(folder)+1:]
        return filename
    
  2. ilih.livejournal.com

    или вариант покороче

    def del_prefix(path, filename):
        path.sort(key=len, reverse=True)
        return ([filename[len(x)+1:] for x in path if filename.startswith(x)] + [filename])[0]
    
  3. Сергей
    def cutpath(path, filename):
        path.sort(key=len, reverse=True)
        return [filename.replace(p, "") for p in path if filename.startswith(p)][0]
    

    Как видно, код небезопасный. По-хорошему нужна обертка в try/except или проверка на длину нового списка.

    Короче, будем считать, что в path есть необходимый путь.

  4. Алексей

    Однострочник (перенос для «удобства» чтения).

    min([filename[len(p) * filename.startswith(p):]
         for p in sys.path], key=len)[1:]
    

    Это, правда, если считать что в sys.path пути обязательно идут без оконечного «/». Если нет то надо еще поправку вносить на это.

  5. Алексей

    Ой, вру-вру-вру, грубо и некрасиво ломаюсь если в sys.path лежит '/home/dominatio'. Поправляюсь, заодно убираю ограничение на слэши в конце. Увы, уже более громоздко выходит все

    min([filename[len(p) * filename.startswith(p):]
         for p in [p + ('/' * (not p.endswith('/')))
                   for p in map(os.path.normpath, path)]],
        key=len)
    
  6. Ivan Sagalaev

    Алексей, кстати, '/' можно добавлять к os.path.normpath(p). Букв больше, но кажется понятней.

  7. ilih.livejournal.com

    угу, ту же ошибку допустил
    вместо '/' лучше использовать os.path.pathsep

    def del_prefix(path, filename):
        path.sort(key=len, reverse=True)
        return ([filename[len(x):] if x[-1]==os.path.pathsep else filename[len(x)+1:]
            for x in path
            if filename.startswith(x) and (filename[len(x)]==os.path.pathsep or x[-1]==os.path.pathsep)] + [filename])[0]
    
  8. Алексей

    Сразу к normpath(p) можно добавлять только при условии что мы никогда не вздумаем иметь какую-нибудь хитрую chroot-среду с '/' в sys.path. Именно для этого там пляска с p.endswith('/') вместо простого захода «в лоб».

    Если этот случай с путем сразу на корень игнорировать, то normpath, как я понимаю его поведение, он всегда будет убирать последний слэш и все сведется к чему-то типа

    min([filename[len(p) * filename.startswith(p):]
         for p in [os.path.normpath(p) + os.path.pathsep for p in path]],
        key=len)
    
  9. Виктор Коцеруба

    ололо

    filename.lstrip(sorted(filter(lambda dirname: filename.startswith(dirname), sys.path), reverse=True, key=len)[0])
    
  10. Андрей

    filename[len(max([p for p in sys.path if filename.startswith(p)], key=len)):].lstrip(os.path.pathsep)

  11. Андрей

    Немного оптимизировал:

    filename[max([len(p) for p in sys.path if filename.startswith(p)]):].lstrip(os.path.pathsep)
    
  12. Михаил Едошин

    import sys
    from os.path import sep

    # Это понятно
    
    # Первый вариант, однострочный
    def f1(f):
        return [f[s:] for (s, p) in [(len(p) + len(sep), p + sep) for p in reversed(sys.path)] if f[:s] == p][0]
    
    # Но для скорости лучше, конечно, paths заранее сосчитать
    
    paths = [(len(path) + len(sep), path + sep) for path in reversed(sys.path)]
    
    # Тогда будет (уже более читаемый)
    
    def f2(abspath):
        return [abspath[size:] for (size, path) in paths if abspath[:size] == path][0]
    
    # И еще более читаемый, для реального проекта
    
    def find_ambiguous_relpath(abspath):
        for size, path in paths:
            if (abspath[:size] == path):
                return abspath[size:]
    

    В зазипованных файлах он ничего не найдет, конечно, но раз это рабочие модули, то вряд ли они окажутся в zip'ах.

    (Запостил не читая комменты, так что простите, если что.)

  13. Mikhail Sayapin

    А вот с претензией на кроссплатформенность и скорость при многократном вызове при записи ошибок в лог. И вообще - забавы ради. :-)

    import sys, os, re
    filename = sys.argv[1]
    longest_matcher = re.compile(r"(?:%s)%s(.*?)$" % (
        u'|'.join(map(re.escape, map(os.path.abspath, sorted(sys.path, key=len, reverse=True)))),
        re.escape(os.sep)
    ))
    def trim_syspath(filename):
        q = longest_matcher.match(os.path.abspath(filename))
        return q and q.groups()[0] or filename
    print trim_syspath(filename)
    

    И пусть никогда не упрется запись ошибок твоих в скорость! О:-)

  14. Ivan Sagalaev

    Несколько комментариев.

    len(p) * filename.startswith(p)
    

    Не знал, что на boolean можно умножать %-). Прикольно!

    вместо '/' лучше использовать os.path.pathsep

    Только тогда уж os.path.sep. pathsep — это разделитель целых путей.

    filename.lstrip(...
    

    Отлично! lstrip, кажется, симпатичней слайса по длине.

    longest_matcher = re.compile(r"(?:%s)%s(.*?)$" %
    

    О! У нас похожая регулярка (Андрей придумал), хотя в деталях решение отличается. Собственно, это, кажется, и самое быстрое решение в сравнении с перебором строк в списке.

  15. Юрий

    min((
    (filename[len(p):] if filename.startswith(p) else filename)
    for p in (
    (p if p.endswith('/') else p+'/') for p in sys.path
    )
    ), key=len)

  16. Алексей

    Отлично! lstrip, кажется, симпатичней слайса по длине.

    Э-э-э, или я чего-то не понимаю или lstrip нельзя использовать ни в коем случае. Это же совсем не то...

    '/fail/f/a/i/l/lol.py'.lstrip('/fail/') == 'ol.py'
    
  17. Гость

    в порядке бреда, строго не пинайте)

    def trim_syspath(path, fname):
         fst, snd = fname.rsplit('/', 1)
         if fst in path:
                 return snd
         return trim_syspath(path, fst) + '/' + snd
    

    но работает вроде быстро

  18. Ivan Sagalaev
    '/fail/f/a/i/l/lol.py'.lstrip('/fail/') == 'ol.py'
    

    А, да, не работает lstrip, значит...

    def trim_syspath(path, fname):
         fst, snd = fname.rsplit('/', 1)
         if fst in path:
                 return snd
         return trim_syspath(path, fst) + '/' + snd
    

    Фактически это выражние цикла через рекурсию, так что по скорости не будет сильно отличаться от решений с for. Правда, конкретно это еще и не корректно, потому что не сортирует пути.

  19. Гость

    хм, что-то не догоняю зачем сортировать пути.. ведь поиск у нас в силу rsplit идет справа, и по этому будет выбран больший по длине путь

  20. Григорий Бакунов
    filename[
             max(
                 [len(x) for x in ('%' + y for y in sys.path)
                             if x in '%'+filename]
             ):
    ]
    

    или я недогоняю условие задачи?

  21. Ivan Sagalaev

    или я недогоняю условие задачи?

    Не, всё так. Я только не понял, почему ты используешь '%' вместо проверки на startswith?

    P.S. Клёвый смайлик на четвёртой строке :-)
    P.P.S. Аргумент под max() можно списком не делать, ему генератора достаточно.

  22. Григорий Бакунов

    ну очевидно почему % - потому что x.startswith(s) читается не так приятно как s in x.
    А я люблю красивые буковки. да и работает это, как не странно быстрее.

  23. Ivan Sagalaev

    Да, работает быстрее. В 2.5 Питоне сделали оптимизацию 'in' для строк. Только я думал, что в startswith она тоже работает. Оказывается, нет:

    value = 'abcxxxxxxxxxx'
    
    time for i in xrange(1000000): 'abc' in value
    CPU times: user 0.30 s, sys: 0.00 s, total: 0.30 s
    Wall time: 0.30 s
    
    time for i in xrange(1000000): value.startswith('abc')
    CPU times: user 0.64 s, sys: 0.00 s, total: 0.64 s
    Wall time: 0.64 s
    

    И даже если избавиться от лукапа атрибута, всё равно медленней:

    s = value.startswith
    
    time for i in xrange(1000000): s('abc')
    CPU times: user 0.52 s, sys: 0.00 s, total: 0.53 s
    Wall time: 0.54 s
    
  24. Сергей Шепелев

    Уверен, что уважаемые господа уже дали самый лучший ответ.

    Про лог ошибок. Это одна из вещей, которые мне жутко понравились в Google AppEngine (база не понравилась). У них что-то вроде сислога с удобным просмотром через веб. То есть всё из logging попадает туда, весь stdout и stderr попадает туда, трейсбеки попадают туда отдельно и читабельно. Собираюсь такое прикрутить в проекте на работе и, вообще, считаю, что это самый лучший способ.

  25. Ivan Sagalaev

    Собираюсь такое прикрутить в проекте на работе и, вообще, считаю, что это самый лучший способ.

    А я, кстати, вчера подумал срелизить наш django_errorlog, который эту задачку и породил.

  26. general

    К вопросу о трейсбэках ошибок.

    Мне тут подумалось (буквально на той неделе применительно к подобной задачке), что в трейсбэк стоило бы вместо имени файла совать имя модуля, то есть имя, по которому он был импортирован. Строчка традиционного трейсбэка выглядит так:

    File "/usr/lib/python2.6/json/encoder.py", line 344, in default

    Причем пример выше еще довольно прост, потому что там в пути не фигурируют site-packages, и всякие eggs, которые по традиции грешат длиннючими путями. Вместо этого можно наблюдать гораздо более понятное

    Module "json.encoder", line 344, in default

    И вообще, сущность задачи, надо полагать, не в том, чтобы сократить имя файла, а в том, чтобы в трейсбэке ошибки оно было понятней и читабельнее? :)

    Реализуется это довольно просто - при спуске по трейсбэку или подъеме по фреймам смотреть в frame.f_globals на предмет переменной __name__. Если она там есть - это как раз и будет имя импорта модуля, в прострастве имен которого данный фрейм и лежит.

    Могу кусок кода в ~ 40 строк приложить в качестве proof of concept :)

  27. Jungle
    def cut(paths, filename):
        ln = 0
        ph = ''
        for p in paths:
                if filename.startswith(p) and len(p) > ln:
                        ln = len(p)
                        ph = p
        return ph
    

    не знаю как будет по скорости, но по читабельности вроде нормально :)

  28. Ivan Sagalaev

    И вообще, сущность задачи, надо полагать, не в том, чтобы сократить имя файла, а в том, чтобы в трейсбэке ошибки оно было понятней и читабельнее? :)

    Угу, последним абзацем поста я как раз про это и говорил.

    Спасибо вам за наводку на __name__ в глобальных переменных фрейма! Теперь весь код облагораживания именя файла я похоронил и смотрю туда напрямую. Но для истории вот код, который у нас имя файла обрабатывал:

    def _module_name(filename):
        '''
        Приводит имя файла к удобному виду:
    
        - отрывает начальную часть питоньего пути, по которому он найден
        - отрывает расширение файла
        - заменяет / на .
        '''
        if not hasattr(_module_name, 'pattern'):
            # Составляем regexp из путей sys.path, отсортированным от
            # длинных к коротким. Этим regexp'ом будет отрезаться префикс файла.
            paths = [re.escape(os.path.normpath(p) + '/') for p in sys.path]
            paths.sort(key=len, reverse=True)
            _module_name.pattern = re.compile('^(%s)' % '|'.join(paths))
        filename = os.path.abspath(filename)
        filename = _module_name.pattern.sub('', filename)
        if not filename.startswith('/'):
            filename = os.path.splitext(filename)[0].replace(os.path.sep, '.')
        return filename
    

    Как видно, для поиска пути используется regexp, который посредством sub сам отрезает себя от имени файла. Сам regexp компилируется один раз и навешивается на атрибут функции для дальнейшего использования.

  29. http://barbuza.info/

    мне почему-то кажется, что при таком варианте часть решений будет тупить

    sys.path = ['/home/projects', '/home/projects/domination', '/usr/lib/python', '/home/projects/domination/wor']
    
  30. http://barbuza.info/

    обновленное извращение

    min([t[2] for t in [filename.partition(p) for p in sys.path] if not t[0] and t[2][0]=='/'], key=len)[1:]
    
  31. Tonal

    Это часть задачи записи ошибок веб-приложения в лог. Чтобы лог было удобно grep'ать, он однострочный, а следовательно весь traceback в него записывать нельзя (технически-то можно, но получается каша)

    У нас в лог пишется полный traceback, иногда с дополнительными строками пояснений.
    Простеньким приложением на haskell выдёргиваются все уникальные ошибки (отбрасывая дату-время):

    import Data.Char
    import Data.List
    import Text.Regex.PCRE
    import System.Environment
    
    re_test :: String -> String -> Bool
    re_test re xs = xs =~ re
    
    -- Строк больше нет
    parse [] = []
    
    -- Стандартная строка ошибки
    --2009-01-12 09:52:25 ERROR
    parse (x:xs) | re_test "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} ERROR " x =
      let (xx:xxs) = parse xs
      in (drop 20 x : xx):xxs
    
    -- Строки Traceback-а
    --Traceback (most recent call last):
    parse (x:xs) | "Traceback (most recent call last):" `isPrefixOf` x =
      let
        (l1, l2) = break (not . isSpace . head) xs
        (l2h:l2t) = l2
      in
        (x : l1 ++ [l2h]) : parse l2t
    
    -- Строки с  Traceback-а с доп.пояснениями
    --Ошибка при вызове execute: ...
    parse (x:xs) | isInfixOf " execute: " x =
      let
        (l1, l2) = break (not . isSpace . head) xs
        (l2h:l2t) = parse l2
      in
        (x : l1 ++ l2h) : l2t
    
    parse (x:xs) = [x] : parse xs
    
    toUniqErr = nub . parse . lines
    
    parseArgs [] = error "Usage:\n  log_parser log_file_name"
    parseArgs (x:_) = x
    
    main = do
      args <- getArgs
      let lname = parseArgs args
      cnt <- readFile lname
      let grps = toUniqErr cnt
      putStr $ unlines $ map unlines grps
    

    Переписывать на python лениво. :)

  32. Ivan Sagalaev

    Мы обошлись без кастомных скриптов. Просто пишем два лога: один с однострочными exception'ами, второй с полными traceback'ами. Первый удобно grep'ать, мониторить, статистику снимать. Во второй смотрим, когда надо собственно traceback почитать.

Добавить комментарий