Парсинг сайтов
0,0
рейтинг
26 января 2015 в 20:09

Разработка → Runscript — утилита для запуска python скриптов

Думаю многим знакома следующая ситуация. В вашем проекте есть различные действия, которые нужно выполнять время от времени. Для каждого действия вы создаёте отдельный скрипт на питоне. Чтобы далеко не лазить, скрипт кладёте в корень проекта. Через некоторое время вся корневая директория проекта замусоривается этими скриптами и вы решаете сложить их в отдельную директорию. Теперь начинаются проблемы. Если указать интерпретатору python путь до скрипта, включающий эту новую директорию, то внутри скрипта не будут работать импорты пакетов, находящися в корне проекта т.к. корня проекта не будет в sys.path. Эту проблему можно решить несколькими способами. Можно изменять sys.path в каждом скрипте, добавляя туда корень проекта. Можно написать утилитку для запуска ваших скриптов, которая будет изменять sys.path перед запуском скрипта или просто будет лежать в корне проекта. Можно ещё что-то придумать. Мне надоело каждый раз изобретать колесо и я создал велосипед runscript на котором с удовольствием катаюсь.

Установить библиотеку можно с помощью pip:

$ pip install runscript

После установки библиотеки runscript, вы получаете в вашей системе новую консольную команду run с помощью которой можно запускать скрипты. По-умолчанию, команда run ищет скрипты в под-каталоге script текущего каталога.

Давайте рассмотрим простой пример. Создадим каталог script. Создадим пустой файл script/__init__.py, превратив этот каталог в python-пакет. Теперь создадим файл script/preved.py со следующим содержимым:

def main(**kwargs):
    print(‘Preved, medved!’)


Скрипт готов. Теперь мы можем его запустить:

$ run preved
Preved, medved!

Ура! Скрипт работает. Вот собственно и всё, что делает библиотека runscript. Я серьёзно :) Команда run запускает функцию main из файла, имя которого вы ей передали в командной строке. Оказалось, что даже такой простой фунционал очень удобен. Я с удивлением заметил, что пользуюсь утилиткой run в каждом своём проекте т.к. везде есть простенькие скрипты, которые нужно запускать.

Со временем утилита run обросла рядом полезных полезностей, о которых я сейчас расскажу.

Получение параметров через командную строку


Чтобы передать вашему скрипту какие-либо параметры через командную строку, вам нужно описать эти параметры в функции setup_arg_parser внутри вашего скрипта. Эта функция получает на вход объект ArgumentParser, в который вы можете добавить нужные опции. Далее, когда скрипт будет вызван, значения параметров командной строки будут переданы фунции main. Пример скрипта:

def setup_arg_parser(parser):
    parser.add_argument(‘-w’, ‘--who’, default=’medved’)

def main(who, **kwargs):
    print(‘Preved, {}’.format(who))

Запускаем:

$ run preved
Preved, medved
$ run preved -w anti-medved
Preved, anti-medved

Обратите внимание, как фунция main получила параметры командной строки — в виде обычных именованных параметров. Всегда нужно указывать **kwargs т.к. кроме нужных вам параметров, передаются значения всех глобальных для утитилы run параметров (читайте о них ниже).

Активация Django


Если вы пытались использовать фреймворк Django в ваших консольных скриптах, то знаете, что нужно сделать кое-что, иначе ничего не будет. Кое-что заключается в создании environment переменной DJANGO_SETTINGS_MODULE, cодержащей путь до модуля с настройками. Обычно в python скрипт добавляют следующие строки:

import os
os.environ[‘DJANGO_SETTINGS_MODULE’] = ‘settings’

Начиная с django 1.7 нужно также выполнить

import django
django.setup()

Для того чтобы выполнять автоматически эти действия в скриптах, запускаемых через run, нужно создать в корне проекта файл с именем run.ini, содержащим следующие настройки:

[global]
django_settings_module = settings
django_setup = yes


Профилирование


Добавив ключик --profile при вызове скрипта, получим файл с результатами профилирования работы нашего скрипта, который можно посмотреть в kcachegrind. Результат сохраняется в каталог var/<script_name>.prof.out, так что не забудьте создать этот каталог. Также нужно установить модуль pyprof2calltree, который нужен, чтобы сохранить результат профилирования в формате kcachegrind.

$ run preved --profile
Preved, medved
$ ls var/
preved.prof.out

Настройка мест поиска скриптов


По-умолчанию, утилита run ищет скрипт в двух пакетах: grab.script и script. Пакет grab.script добавлен в этот список, потому что во многих проектах парсинга сайтов я запускаю команду crawl из grab.script пакета. Если вам нужно изменить места для поиска скриптов, создайте следующую настройку в run.ini файле:

[global]
search_path = package1.script,foo,bar

Теперь если мы выполним команду `run preved`, то утилита run попытается импортировать модуль preved в следующем порядке:

  • package1.script.preved
  • foo.preved
  • bar.preved


Использование lock-файлов


Иногда бывает нужно запретить одновременную работу нескольких экземпляров скрипта. Например, мы вызываем скрипт каждую минуту с помощью cron и хотим не допустить одновременной работы нескольких копий скрипта, что может произойти, если работа одной из копий затянется больше, чем на минуту. С помощью опции --lock-key мы можем передать имя lock-файла, который будет создан в каталоге var/run. Например, --lock-key foo приведёт к созданию файла var/run/foo.lock.

Другой способ задать имя lock-файла — создание функции get_lock_key внутри вашего скрипта. Результат её работы будет использован утилитой run, для формирования имени lock-файла. Фунция будет полезна на тот, случай, если вы хотите генерировать имя lock-файла в зависимости от параметров, передаваемых скрипту.

import time 

def get_lock_key(who, **kwargs):
    return 'the-{}-lock'.format(who)


def setup_arg_parser(parser):
    parser.add_argument('-w', '--who', default='medved')


def main(who, **kwargs):
    print('Preved, {}'.format(who))
    time.sleep(1)

Запускаем одновременно две копии скрипта и видим:

$ run preved -w anti-medved & run preved -w anti-medved
[1] 25277
Trying to lock file: var/run/the-anti-medved-lock.lock
Preved, anti-medved
Trying to lock file: var/run/the-anti-medved-lock.lock
File var/run/the-anti-medved-lock.lock is already locked. Terminating.
[1]+ Done run preved -w anti-medved


Я рассказал об основных возможностях библиотеки runscript. Надеюсь, она окажется вам полезной.

В случае вопросов по поводу работы библиотеки можно всегда посмотреть в исходный код, который на данный момент довольно маленький: github.com/lorien/runscript/blob/master/runscript/cli.py
@itforge
карма
71,2
рейтинг 0,0
Парсинг сайтов
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +4
    Псст, парень,
    if __name__ == '__main__':

    Только никому!
    • 0
      Не понял.
      • 0
        Имелось в виду, что это условие позволяет запускать скрипт без дополнительных приблуд:
        как-то так
        • 0
          Это условие

          if __name__ == '__main__':
              main()
          

          позволяет как раз *не* запускать фунцию main(), в том случае, если мы импортировали модуль скрипта в другом модуле. Именно эту проблему, решает вышеприведённое условие.

          Чтобы запустить на выполнение фунцию main, достаточно её запустить :)) Без всяких условий.

          main()
          
    • +3
      Как это решает проблему, описанную выше?

      ├── program.py
      └── scripts
          └── script.py
      

      program.py:
      def hello_world():
          print "Hello, world!"
      

      scripts/script.py:
      import program
      
      def actions():
          program.hello_world()
      
      actions()
      
      • +4
        Видимо, проблема настолько не очевидна, что многие, включая меня, вообще никакой проблемы не видят.
        Может я что-то не понимаю?
        В чем собственно проблема выполнить скрипт на питоне?
        • +1
          Если прописать python scripts/script.py вылетит ImportError, говорящий что нету модуля program в пределе досягаемости. Если я правильно понял, автор решает именно этот кейс.

          Так вот я и спросил, как
          if __name__ == '__main__':
          

          Решает эту проблему.
          • +3
            python -m scripts/script.py?
            • +6
              Тфу, python -m scripts.script
              • –1
                Действительно, нужно просто упаковать скрипт в пакет.
                Не знал, что так можно. Спасибо!
                • +1
                  По сути ключ -m добавляет текущую директорию в sys.path
                  • –2
                    Для меня это ничего не меняет. У меня есть альтернатива:

                    Вариант 1:

                    def main():
                        pass
                    
                    if __name__ == '__main__':
                        main()
                    


                    $ python -m script.preved
                    


                    Вариант 2:

                    def main(**kwargs):
                        pass
                    


                    $ run preved
                    


                    Я выбираю вариант 2 — нужно меньше букв печатать, как в скриптах, так и в консоли. Для меня это важно. И мне очень приятно, что я экономлю это время. Как я уже заметил в статье, ничего революционного пакет runscript не делает, но то малое, что он делает, очень удобно.

                    Кроме, краткости, вариант 2 предоставляет упрощённую обработку аргументов командной строки и другие плюшки, о которых я написал в статье.

                    Я просто поделился тем, как запускаю скрипты в своих проектах. Никому ничего не навязываю и не говорю, что мой способ самый «лучший» (что бы это ни значило).
                    • +1
                      > Я выбираю вариант 2
                      Это «неправильный» выбор. Если вы планируете запускать скрипт из консоли, то надо писать
                      if __name__ == "__main__":
                      

                      хотя бы потому, что
                      $ python -m script.preved
                      

                      не требует установки пакета для работы.

                      Исключение — вы пишете код для себя и никто никогда не будет с ним работать и запускаться он будет на одной машине.
                      • 0
                        Очень часто пишу код именно для себя. Если мы говорим о каком-то проекте, где работает несколько людей, то тоже не вижу особенной проблемы. Обычно проект это virtualenv + requirements.txt, при разворачивании проекта на новой машине, все зависимости, и в том числе, runscript, поставятся автоматом.

                        Я, честно, плохо понимаю, зачем каждый раз писать «python -m package.module» в вашем проекте для запуска скриптов, если можно договориться написать какой-то запускальщик, который будет решать рутинные операции, специфичные для большинства скриптов этого проекта. В роли запускальщика может выступить в том числе и runscript.
  • +3
    Вы знаете несколько минусов:
    1. Взять docopt и сделать так, чтобы он делал импорт нужного модуля и вызов main. Весьма просто, и дело одного абзаца
    2. Если у вас уже есть django — то там есть стандартные commands с интерфейсом argparse. Зачем еще runscript?
    3. lock-file это наверно хорошая штука если у вас только 1 сервер, который выполняет этот код. Например, если на уровне кода нет разделения, то любой девелоперская машина выполнит тот же код что и на проде — итог, очень просто выстрелить себе в ногу.
    • 0
      1. Про decopt не понял, не использовал этот пакет. Можно подробнее?

      2. Во-первых, django я не в каждом проекте использую. Во-вторых, мне не нравятся django management commands. Мне нравится писать простые скрипты с одной функцией и складывать их в одну директорию в проекте. В случае джанго нужно запрятать скрипты подальше — в management/commands каталог какого-либо приложения. Да и сами скрипты становятся сложнее. Сравните:

      django

      from django.core.management.base import BaseCommand
      from optparse import make_option
      
      
      class Command(BaseCommand):
          option_list = BaseCommand.option_list + (
              make_option('-w', '--who'),
          )
      
          def handle(self, who, *args, **options):
              print(who)
      


      runscript

      def setup_arg_parser(parser):
          parser.add_argument('-w', '--who')
      
      def main(who, **kwargs):
          print(who)
      


      3. Не очень понял, что вы имели в виду. Можно пример выстрела?
      • 0
        Ok,

        1. Вот github.com/docopt/docopt, предлагается просто завести первым аргументом, что-то и делать согласно ему import_module().main(args).

        2. Ну так ок, это не так сложно вроде в папку commands бросить файл, синтаксис я бы не сказал что невыносимый. Только еще плюсом идет факт, что там стоит немаленького размера optparse, котором намного больше функций, чем runscript.add_argument.

        3. Я имел что lock-file работает только на одной машине, у другого сервера lock-file нет и следовательно ваш механизм блокировки не работает.
        • 0
          1. Если я вас правильно понял, я это и делаю, только без docopt :)

          2. По поводу optparse вы наверное не совсем поняли меня. Дело в том, что runscript тоже использует optparse, вернее даже не optparse, а argpase. Я в статье писал про это. Сложно или не сложно вам создать файл, кажется ли вам синтаксис django management commands выносимым или нет, я не обсуждаю. Это ваше личное дело. Я просто рассказал, что лично я думаю о django management commands. Вообще этот пункт особо не имеет смысла обсуждать т.к. я использую джангу не во всех проектах, я чуть выше уже говорил об этом.

          3. Да, блокировка через lock-файл работает только в пределах одной машины. Меня это устраивает. Фунциональность runscript — это решение моих задач. Задачи лочить выполнение скриптов сразу на нескольких машинах у меня не воникало.
          • 0
            1. да
            2. используйте docopt или click (http://click.pocoo.org/3/)
            3. да

            Ну мы говорим не только о Вас, а о нас программистах в целом.
            • 0
              2. На данный момент меня вполне удовлетворяет argparse, идущий в stdlib питона.

              Я всё же скорее про себя говорю. Мол были такие-то задачи, я их так-то решил. Это конкретный разговор. О программистах в целом мне рассуждать не интересно. У каждого свои проблемы и каждый решает их по разному. Если кому-то понравится идея runscript, он может использовать эту библиотеку. Если кто-то считает, что она стреляет в ногу или не делает ничего полезного, он не будет её использовать
              • +1
                > Если кому-то понравится идея runscript, он может использовать эту библиотеку

                Хей, да в чем идея то? Я же Вам привожу и привожу ссылки, а Вам неинтересно рассуждать о программистах в целом. Экий, Вы не уловимый Джо.
                • 0
                  Извините, я не понимаю, что вы хотите сказать.
      • +2
        В django_extensions есть команда runscript
  • 0
    Однажды я взял и потратил день на изучение virtualenv и setuptools. С тех пор на тех кто манипулирует sys.path я смотрю свысока
    • 0
      Держите нас в курсе, на кого вы смотрите свысока :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.