Как я выбирал скриптовый язык и остановился на Python

8 декабря 2015

Задачи в программировании бывают разными. Например, часто нужно написать скрипт на 10 строк кода. И, соответственно, нужно для этого что-то вроде Bash, только не Bash :) Долгое время я использовал для этих задач Perl. Но, увы, во многих командах Perl недолюбливают. Не исключено даже, что заслуженно. Поэтому возникает вопрос поиска хорошего скриптового языка. После долгих и мучительных раздумий я пришел к выводу, что такой язык — это, скорее всего, Python, и в этой заметке будет рассказано, почему.

Отмечу, что для скриптовых языков существует огромная ниша. Фактически, во всех задачах, где скорость выполнения программы не очень важна, имеет смысл использовать скриптовые языки, так как скорость разработки на этих языках существенно выше. Во-первых, так происходит по той причине, что скриптовые языки изначально не претендуют на высокую производительность кода, что позволяет делать их более высокоуровневыми и выразительными. Во-вторых, скриптовые языки позволяют программисту видеть результат мгновенно, без перекомпиляции проекта, перезапуска веб-сервера, и так далее. Такого рода не требовательных к скорости перемножения матричек задач намного больше, чем может показаться на первый взгляд.

Например:

  • Написание небольших скриптов в стиле «пропарсил логи, вывел статистику» или, например, наборы админских скриптов для неверующих (возможно, вполне оправданно) в Chef, Puppet и Ansible;
  • Разработка тестов — интеграционных, системных, нагрузочных, …;
  • Всем нужен простой консольный калькулятор, а у скриптовых языков обычно есть REPL. Иногда нужно не просто складывать и умножать, а, например, отсортировать 1000 целых чисел. Это все тоже относится к «калькулятору»;
  • В веб-стартапах важно быстро слепить прототип, налить на него трафика и посмотреть, понравилась ли идея пользователям. Если взлетит, находим 10% кода, которые тормозят по правилу 10:90, и переписываем их на Си. Смотри историю развития ВКонтакте, Facebook и прочих;
  • Не стоит также забывать, что обычных, не хайлоад проектов, в мире очень много — всяких там бложиков, небольших интернет-магазинов и форумов. Кроме того, даже в хайлоаде может быть какой-то генератор статики или веб-админка, которой одновременно никогда не пользуется больше десяти человек;
  • Кстати, по поводу хайлоада. Если Reddit подходит Python, GitHub’у подходит Ruby, а Facebook’у подходит PHP, то и вашему хайлоуду наверняка подойдет Python. Дело в том, что в типичном хайлоаде почти все время обработки запроса тратится на посылку запросов к кэшам и базам данных, и не имеет большого значения, на Python вы пишите или на чистом Си. Если перед приложением поставить Nginx или lighttpd, то вы без проблем сможете держать 10 000 одновременных TCP-соединений, а то и больше. Польза же от легковесных процессов, как в Go или Erlang, становится сомнительной, так как время выполнения запроса (десятки миллисекунд, не больше) становится сравнимым со временем переключения между легковесными потоками.
  • Во многих приложениях (игровых движках, отладчиках, текстовых редакторах, …) часто нужен какой-то встраиваемых язык, чтобы пользователь мог описать некую кастомную логику;
  • То, что исходники скриптов всегда видны, может быть важно с точки зрения безопасности. Хорошо знать, что устанавливаемое приложение не делает чего-нибудь лишнего. Ведь совсем не факт, что всякий бинарный пакет собран из тех же исходников, что лежат на GitHub;

Почему же на мой взгляд для таких задач идеально подходит Python и в меньше степени подходит Perl или, скажем, Ruby:

  • Простой, единообразный, строгий синтаксис. Код либо отформатирован правильно, либо не запускается. При работе в коллективе из нескольких человек это действительно здорово. В отношении Perl и Ruby это не так;
  • Предельно низкий порог вхождения, вполне себе боевой код можно начать писать за считанные часы;
  • Огромное сообщество пользователей, а следовательно куча книг, куча ответов на StackOverflow, и так далее;
  • Большая коллекция готовых библиотек практически на все случаи жизни. Некоторые люди отмечают существование уникальных научных библиотек, которые есть только в мире Python, таких как NumPy, SciPy, MatPlotLib и TensorFlow;
  • Строгая динамическая типизация. В Perl она нестрогая, то есть числа можно складывать со строками, что по моему опыту сильно усложняет поддержку кода;
  • Сборка мусора не тупо на подсчете ссылок, но и с нормальным GC для циклических ссылок. В Perl есть только подсчет ссылок;
  • Все не примитивные типы передаются по ссылке, как в Java, что делает код намного более читаемым. В Perl нужно отличать данные от ссылок на данные;
  • Есть подвижки в сторону JIT-компиляции — PyPy, разрабатываемый в Dropbox’е Pyston, плюс Jython для компиляции под JVM. Аналогов для Perl мне не известно. Для Ruby JIT вроде есть, но не знаю, насколько соответствующие проекты популярны;
  • Для Python существует мощная и бесплатная IDE — PyCharm. Кроме того, есть мощнейший плагин к Sublime Text — Anaconda. Для Perl, насколько я знаю, полноценной IDE до сих пор не существует. Для Ruby есть RubyMine, но она распространяется только за деньги;
  • В отличие от Perl из коробки есть поддержка работы с большими числами, есть нормальный ООП, нормальная многопоточность (с точностью до GIL в CPython; кстати, по большому счету наличие или отсутствие GIL определяет лишь то, какие приложения на языке будут работать быстрее, многопроцессные или многопоточные), нормальные исключения, нормальный boolean, контейнер set, не говоря уже о нормальной передаче аргументов функциям, REPL, генераторах (которые с ключевым словом yield), кортежах, генераторах списков, и пусть простом, но все-таки pattern matching’е;
  • Есть type hints, которые сильно упрощают чтение кода. Плюс есть реализация gradual typing в лице MyPy;
  • Есть много вакансий. Притом, есть основания полагать, что в соответствующих проектах не слишком сильный говнокод, что не справедливо в отношении Perl. Плюс есть некое разнообразие в решаемых задачах. Ruby же практически всегда означает вебчик с Ruby on Rails;
  • Код, написанный на Ruby в 2013 году, внезапно перестает работать в 2015 году. См историю с OpenVZ Web Panel. В отношении Perl и Python это не так;
  • Куча саксесс сторис (Google, Yahoo, Яндекс, Mail.ru, Dropbox, Bitbucket, Disqus, …) и известных проектов (Graphite, Mercurial, SCons, Ansible, Trac, Mailman, MoinMoin, Pelican, …);

Резюмируя вышесказанное — по сравнению с Perl язык Python по очень многим параметрам лучше тупо в техническом плане. Также с уверенностью можно сказать, что Ruby не лучше Python. При этом то, что для Python есть бесплатная IDE от JetBrains, а также что ни в каких Яндексах с Mail.ru нет открытых вакансий для программистов на Ruby, сыграли решающую роль. К тому же, за всю жизнь я не написал ни одной строчки на Ruby, а Python как-никак когда-то приходилось немного трогать. Варианты же с JavaScript, PHP, Lua, TCL и прочими я не особо рассматривал. Под Linux из коробки нет соответствующих интерпретаторов, а root на машине не всегда имеется. И вообще, в мире *nix систем на этих языках не принято писать скрипты. Нельзя полностью исключать культурный фактор!

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

В заключение хотелось бы поделиться некоторыми поделками, которые я написал в процессе изучения (скорее даже вспоминания) Python.

Генератор шоунотов (см аналоги на Go, Rust и Kotlin):

#!/usr/bin/env python3

import re
import sys
import urllib3
import threading
import queue

if len(sys.argv) < 2:
    print("Usage: " + sys.argv[0] + " input.txt [workersnum=8]")
    sys.exit(1)

fname = sys.argv[1]
http = urllib3.PoolManager()
taskq = queue.Queue()
resultq = queue.Queue()
taskdonelock = threading.Lock()
tasksdone = 0
taskstotal = 0
workersnum = 8

if len(sys.argv) >= 3:
    workersnum = int(sys.argv[2])

def worker():
    while True:
        task = taskq.get()
        if task is None:
            break
        (tasknum, line) = task
        result = (tasknum, process_line(line))
        resultq.put(result)

def process_line(line):
    global tasksdone

    m = re.search("""(?i)(https?\S+)""", line)
    url = m.group(1)

    title = "[NO TITLE]"
    try:
        res = http.request('GET', url)
        body = res.data.decode('utf-8')
        m = re.search("(?is)<title>(.*?)</title>", body)
        title = m.group(1).strip()
        title = re.sub(r'\s+', ' ', title)
    except KeyboardInterrupt:
        raise
    except Exception as e:
        title = "[FAILED: " + str(e) + "]"

    tasknum = -1
    with taskdonelock:
        tasksdone += 1
        tasknum = tasksdone

    print("Done [" + str(tasknum) + "/" + str(taskstotal) + "] " + url)
    return '<li><a href="' + url + '">' + title + '</a></li>'

with open(fname) as f:
    for line in f:
        task = (taskstotal, line)
        taskq.put(task)
        taskstotal += 1

for _ in range(0, workersnum):
    taskq.put(None)
    thread = threading.Thread(target=worker)
    thread.start()

results = list(range(0, taskstotal))
for _ in range(0, taskstotal):
    (i, res) = resultq.get()
    results[i] = res

print("\n\n<ul>")
for res in results:
    print(res)
print("</ul>")

Заливка картинок на ImageShack (аналог на Perl):

#!/usr/bin/env python3

import re
import sys
import urllib3
import random
import os.path

if len(sys.argv) < 2:
    print("Usage: " + sys.argv[0] + " <image>")
    sys.exit(1)

fname = sys.argv[1]
url = 'http://imageshack.us/upload_api.php'
http = urllib3.PoolManager()

data = None
with open(fname, "rb") as f:
    data = f.read()

basename = os.path.basename(fname)
fields = {
    "key": "015EFMNVfe7f6f7e93cb4a7b0a41e19956ce59f8",
    "Filedata": (basename, data)
}
res = http.request('POST', url, fields)
body = res.data.decode('utf-8')
m = re.search("(?is)<image_link>(.*?)</image_link>", body)
url = m.group(1).strip()
print(url)

Заливалка текстовых файлов на PasteBin (тут более подробно про requests):

#!/usr/bin/env python3

import re
import sys
import requests

if len(sys.argv) < 2:
    print("Usage: " + sys.argv[0] + " <file>")
    sys.exit(1)

fname = sys.argv[1]
base_url = 'http://pastebin.com'
post_url = base_url + '/post.php'

headers = {}
headers['user-agent'] = u'Mozilla/5.0 (compatible; MSIE 9.0; ' + \
  u'Windows NT 6.0; Trident/5.0;  Trident/5.0)'

res = requests.get(base_url, headers = headers)
body = res.text

m = re.search("""(?is)name="csrf_token_post" value="(.*?)">""", body)
token = m.group(1).strip()

cookies = {
  "__cfduid": res.cookies.get('__cfduid'),
  "PHPSESSID": res.cookies.get('PHPSESSID')
}

paste_code = None
with open(fname, "rb") as f:
    paste_code = f.read()

data = {
    "csrf_token_post": token,
    "paste_code": paste_code,
    "submit_hidden": "submit_hidden",
    "paste_format": "1",
    "paste_expire_date": "N",
    "paste_private": "0",
    "paste_name": ""
}
res = requests.post(post_url, data = data, headers = headers,
                    cookies = cookies)
print(base_url + "/raw/" + res.url.split("/")[-1])

Посылка SMS через Infobip (см также посылку SMS на Scala через Plivo):

#!/usr/bin/env python3

import sys
import json
import urllib3

# change this!
source = 'source'
login = 'login'
password = 'Pa$$w0rd'

if len(sys.argv) < 3:
    print("Usage: " + sys.argv[0] + " <phone> <text>")
    sys.exit(1)

phone = sys.argv[1]
text = sys.argv[2]

http = urllib3.PoolManager()
body = json.dumps({'from': source, 'to': phone, 'text': text})

headers = urllib3.make_headers(basic_auth = login + ':' + password)
headers['content-type'] = u'application/json'
headers['accept'] = u'application/json'

# see http://dev.infobip.com/docs/send-single-sms
res = http.urlopen('POST', 'https://api.infobip.com/sms/1/text/single',
                   headers = headers, body = body)

if res.status != 200:
    print("ERROR: HTTP status = " + str(res.status))
    sys.exit(1)

doc = json.loads(res.data.decode('utf-8'))
status = doc['messages'][0]['status']

if status['groupId'] != 1:
    print("ERROR: " + status['description'])
    sys.exit(1)

print("OK")

Скрипт mostviewed.py (аналог вот этой программы на Scala):

#!/usr/bin/env python3

import sys
import urllib3
import re

http = urllib3.PoolManager()

headers = urllib3.make_headers()
headers['user-agent'] = u'Mozilla/5.0 (compatible; MSIE 9.0; ' + \
  u'Windows NT 6.0; Trident/5.0;  Trident/5.0)'

def get_content(url):
  res = http.urlopen("GET", url, headers = headers)
  if res.status != 200:
    raise Exception("res.status == " + str(res.status) +
      " for url " + url)
  return res.data.decode("utf-8")

def get_stat(domain, date):
  stat_url = "http://www.liveinternet.ru/stat/" + domain + \
    "/pages.html?date=" + date + \
    "&period=month&total=yes&per_page=100"

  data = get_content(stat_url)

  http_home = "http://" + domain + "/"
  https_home = "http://" + domain + "/"
  re_str = """(?s)for="id_\d+"><a href="([^"]+)"[^>]*>.*?""" + \
    """<td>([\d,]+)</td>"""
  for m in re.finditer(re_str, data):
    [url, count] = [ m.group(x) for x in [1,2] ]
    if url == http_home or url == https_home:
      continue
    yield ( url, int(count.replace(",", "")) )

def get_title(url):
  data = get_content(url)
  m = re.search("<h2>(.*?)</h2>", data)
  return m.group(1)

def get_ending(count):
  rem = count % 100
  if 5 <= rem and rem <= 20:
    return "ов"
  rem = count % 10
  if rem == 1:
    return ""
  if 2 <= rem and rem <= 4:
    return "а"
  return "ов"

if len(sys.argv) < 4:
  print("Usage: " + sys.argv[0] + " <domain> <date> <number>")
  sys.exit(1)

[domain, date, number] = sys.argv[1:4]
number = int(number)

all_stat = list(get_stat(domain, date))

print("<ul>")
for (url, count) in all_stat[:number]:
  title = get_title(url)
  print('<li><a href="' + url+ '">' + title + '</a>, ' +
    str(count) + ' просмотр' + get_ending(count) +
    ' за месяц</li>')

print("</ul>")

Генерация уникальных текстов (аналоги на Perl, Erlang и Haskell):

#!/usr/bin/env python3

import re
import random

def repl(m):
    lst = m.group(1).split("|")
    return lst[int(random.random() * len(lst))]

text = "{Hi|{Hello|Good day}}, {world|people}!"
saved = ""

while(saved != text):
    saved = text
    text = re.sub("""\{([^\{\}]+)\}""", repl, text)

print(text)

Ротация логов:

#!/usr/bin/env python3

import os
import re
import time
import calendar
import argparse

def dprint(str):
    if not debug_print_enabled:
        return
    print("DEBUG: " + str)

parser = argparse.ArgumentParser(description='Rotate logs')
parser.add_argument(
    '-d', '--debug', action="store_true",
    help='Enable debug output')
parser.add_argument(
    '-n', '--ndays', metavar='N', type=int, default=30,
    help='Keep logs for N days')
parser.add_argument(
    'dir', metavar='DIR', type=str,
    help='Path to directory with log files')

args = parser.parse_args()

dir = args.dir
max_ndays = args.ndays
debug_print_enabled = args.debug
dprint("dir = " + dir + ", max_ndays = " + str(max_ndays))

now = int(time.time())
os.chdir(dir)

for fname in os.listdir("."):
    m = re.search("(\d{4}-\d{2}-\d{2})", fname)
    ftime = calendar.timegm(time.strptime(m.group(1), "%Y-%m-%d"))
    ndays = int((now - ftime) / (60 * 60 * 24))
    dprint("fname = " + fname + ", ndays = " + str(ndays))
    if ndays < 1:
        dprint("  Skipping")
    elif ndays > max_ndays:
        dprint("  Deleting")
        os.unlink(fname)
    elif fname.endswith(".gz"):
        dprint("  Already compressed")
    else:
        dprint("  Compressing")
        os.system("gzip " + fname)

Посылка e-mail (аналог на Perl, аналог на Scala):

#!/usr/bin/env python3

from smtplib import SMTP
# from smtplib import SMTP_SSL as SMTP
from email.mime.text import MIMEText
import getpass

server = 'mail.example.ru'
port = 25 # 587
login = "afiskon@example.ru"
password = getpass.getpass("SMTP Password: ")

subj = """Test message"""
from_addr = 'Aleksander Alekseev <afiskon@example.ru>'
addr_list = """
user1@example.ru
user2@example.ru
"""
.split("\n")

body = """\
Message body here
"""


for receiver in addr_list:
    receiver = receiver.strip()
    if receiver == "":
        continue

    print("Sending to: '" + receiver + "'")

    msg = MIMEText(body, 'plain')
    msg['Subject'] = subj
    msg['From'] = from_addr
    msg['To'] = receiver

    with SMTP(server, port) as conn:
        conn.starttls()
        conn.login(login, password)
        conn.sendmail(from_addr, [receiver], msg.as_string())

Резервное копирование GitHub-репозиториев:

#!/usr/bin/env python3

import subprocess
import getpass
import sys

def eprint(msg):
    print(msg, file=sys.stderr)
    sys.exit(1)

if len(sys.argv) < 2:
    eprint("Usage: {} <username>".format(sys.argv[0]))

username = sys.argv[1]
password = getpass.getpass("{}'s password: ".format(username))

page = 0

while True:
    page += 1
    cmd = ("curl -s -u '{}:{}' https://api.github.com/user/repos" +
           "?page={} | jq -r '.[].git_url' 2>/dev/null"
          ).format(username, password, page)

    replist = subprocess.check_output(cmd, shell=True)
    replist = replist.decode('utf-8').strip()

    if not replist:
        if page == 1:
            eprint("Something is wrong. Auth failed?")
        break

    for repo in replist.split("\n"):
        print("Cloning {}...".format(repo))
        cmd = "git clone {} 2>/dev/null".format(repo)
        code = subprocess.call(cmd, shell=True)
        if code != 0:
            eprint("Command '{}' failed with code {}".format(cmd, code))

print("All done!")

Еще пару интересных Python-скриптов вы можете найти в заметках Как править в CLion код любых проектов на С++, даже тех, в которых не используется CMake и Парсинг RSS/Atom лент на Python или сам себе RSS2Email.

Пожалуй, это все. Как всегда, я буду несказанно рад вашим конструктивным замечаниям и дополнениям к посту!

Метки: .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: