Защита от "дурака" в программах на языке Python.

Автор: Paul Evans
Перевод: Андрей Киселев

Вероятно вам не приходилось раньше писать программы на языке Python, которые обрабатывали бы ввод пользователя. Как новичок в Python, вы скорее всего предполагаете, что достаточно просто запросить ввод с клавиатуры и выдать результат...

ВНИМАНИЕ: Предыдущее предложение может вызвать у ветеранов-программистов приступ дикого хохота.
[Прим.ред. -- Угу, гомерического, доходящего до икоты. А икающий программист -- это зрелище не для слабонервных пользователей. ;-) ]

Пользователи вводят не то, что вы у них спрашиваете.

Например, вы хотите получить простой ответ -- "y" или "n". В ответ на запрос пользователь может весело и непринужденно ввести свое имя или список блюд на завтрак или может вообще ничего не ввести, а просто нажать клавишу "Enter" -- в результате ваша программа благополучно "падает". Причем делается это не умышленно, а по неграмотности, по неопытности, из-за того, что невнимательно прочитали текст вашего запроса и очень часто пытаются ввести такую чепуху, что просто диву даешься. Причем, как это ни странно, во всех своих бедах они пытаются обвинить вас -- программиста.

Чтобы избежать подобных неприятностей, любой ввод пользователя в обязательном порядке должен проверяться программой. Python имеет в своем распоряжении достаточно большое число функций, которые помогут вам в нелегкой борьбе с пользователем. Некоторые из них мы и рассмотрим в данной статье.

Кроме того, вы можете использовать возможность задания маски для полей ввода. Это означает, что поле ввода будет просто игнорировать те символы, которые не предусмотрены маской. Например, если вы установите маску для ввода числа, а пользователь нажмет клавиши 'ABC', то эти символы будут проигнорированы полем ввода, поскольку оно будет ожидать ввод только цифровых символов, десятичной точки или знак доллара. Позднее мы вернемся к этому вопросу.

Наконец, даже если ваши пользователи очень опытны и послушно вводят именно то, что программа запрашивает, вводимые последовательности символов могут отличаться по формату от того, что ожидает программа. Например, по невнимательности пользователь может ввести свое имя так: 'jOHN sMITH' (нажата клавиша caps lock) или номер телефона не в том формате '604555-1212'.

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

Запрос ввода

Если вы разрабатываете консольный вариант программы, то Python может предложить два метода приема ввода от пользователя: 'raw_input("Текст запроса")' и 'input("Текст запроса")'. (Не используйте в своих программах 'input', пояснения см. ниже.) Еще можно использовать, для этой цели, аргументы командной строки или переменные окружения.

Если вы разрабатываете программу с графическим интерфейсом, то можете воспользоваться возможностями, предоставляемыми Xdialog, Gdialog (входит в состав gnome-utils) или Kaptain.

Кроме того, в вашем распоряжении имеется набор библиотек, для работы с GUI: PyQT , TKinter, WxPython и PyGTK.

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

По этой причине вы никогда не должны давать им возможность использовать вашу программу в неблаговидных целях:

O.K. Выдыхайте! Самое страшное уже позади!

Для вызова интерпретатора откройте окно терминала и введите команду 'python'.

Обратите внимание: Большая часть примеров, приводимых в статье, требуют Python версии '2' или выше. RedHat по прежнему распространяется с Python версии 1.5x поэтому, если вы пользователь Redhat, для вызова интерпретатора вам необходимо пользоваться командой 'python2'. Для справки: версия '1.5' была выпущена еще в прошлом веке.

[Прим.ред. -- python -V вам поможет.]

Проверка содержимого строковых объектов

Языки программирования, как правило, включают в себя возможность выполнения проверок подобного рода и Python не является исключением. Рассмотрим наш первый пример: попробуем убедиться в том, что пользователь ввел число.

Одна из замечательных черт Python заключается в том, что все строковые объекты имеют встроенные методы, позволяющие выполнить проверку их содержимого без лишней головной боли. Введите следующие строки после символов приглашения к вводу '>>>':

>>>input = '9'
>>>input.isdigit()
1

Последняя команда вернет '1' (true) [прим.ред. -- или непосредственно значение True.], так что ее можно использовать в операторах проверки условий 'if'. Есть еще ряд полезных методов, выполняющих проверку, например:

s.isalnum() вернет true если все символы являются алфавитно-цифровыми, false -- в противном случае.
s.isalpha() вернет true если все символы являются алфавитными, false -- в противном случае.

Настоятельно рекомендую вам ознакомиться с полным списком в Python 2.1 Quick Reference. Я пользуюсь им постоянно и даже поместил текстовую версию в HNB для оперативности.

[Прим.ред. -- HNB это небольшая программа, основанная на библиотеке curses, и выполняющая функции блокнота.]

Пример выше -- это простейший случай, а что делать, если необходимо проверить ввод вещественного числа?

Например:

input = '9.9' или
input = '-9'

В обоих случаях указаны вполне допустимые числа, но input.isdigit() вернет нам '0' (false), поскольку ни символ "-" ни десятичная точка не являются цифровыми символами. Пользователь будет весьма озадачен, если в ответ на такой ввод программа вернет сообщение об ошибке.

Предположим, что мы получили то, что ожидали и попытаемся выполнить явное преобразование. Для этого воспользуемся конструкцией try/except. При возникновении ошибки Python породит исключение (exception) и мы отловим эту ошибку.

Допустим, мы ожидаем ввод целого числа, пусть это будет '-9', тогда, используя оператор 'int()', мы сможем выполнить явное преобразование типа.

try:
    someVar = int(input)
    print 'Это целое число'
except (TypeError, ValueError):
    print 'Это не целое число'

Обратите внимание на два момента. Первое -- мы проверяем возникновение двух различных исключений: TypeError и ValueError. Таким образом мы не только обработаем ввод числа с плавающей точкой (например: '9.9'), но и обработаем пустой ввод и даже ввод произвольной строки, например: 'Ham on rye'. Второе -- мы точно указали какие исключения собираемся отлавливать. В принципе, можно оставить оператор exception без аргументов, особо не обременяя себя указанием названий интересующих нас исключений, например так:

try:
    someVar = int(input)
    print 'Это целое число'
except:
    print 'Это не целое число'

ЭТО ПРИМЕР ТОГО, КАК НЕ НАДО ДЕЛАТЬ! Python допускает это, но поскольку теперь в блок exception будут попадать ВСЕ ошибки, отладка может превратиться в настоящий кошмар, если что-то пойдет не так. Уж поверьте моему опыту! Указывайте только те имена исключений, которые вы намереваетесь обрабатывать, это поможет вам сохранить свое время, силы и нервы.

Не менее полезными для вас могут оказаться операторы long() и float(). С другой стороны, str() поможет преобразовать в строку все что угодно.

Не забывайте и о проверке границ диапазона. Всегда, где это уместно, проверяйте величину числа, введенного пользователем -- это хорошая практика. Представьте себе, что в качестве дня месяца пользователь введет число '42', а программа примет это как должное... Проверку попадания числа в заданный диапазон можно выполнять с помощью операторов сравнения '>, <, >=' и т.п..

Ограничение ввода

Как уже говорилось выше, мы можем проверить ввод пользователя после того, как получим его, но не лучше ли было бы просто не допустить ввод ошибочных символов?

Поля ввода с маской.

Это подразумевает использование графического интерфейса для взаимодействия с пользователем и инструментария, который в состоянии отфильтровать недопустимые символы. Как правило, этот инструментарий имеет встроенные валидаторы (функции проверки корректности ввода прим. перев. ) для чисел, строк символов и пр. и очень прост в использовании. В настоящий момент, для создания графического интерфейса, я чаще всего использую PyQT, однако и TKinter, и WxPython, и даже Kaptain также предоставляют такую возможность. Боюсь оказаться неправым, но кажется PyGTK подобной "фишки" еще не имеет.

Если вы не найдете валидатор под свои нужды, то PyQT, к примеру, позволит создать свой собственный.

Я не буду здесь углубляться в рассмотрение каждого инструментария, а в качестве примера продемонстрирую -- как "прицепить" валидатор числа к полю ввода в PyQT. Имя поля ввода в программе -- 'self.rate', Мы "прицепим" к этому полю ввода 'QDoubleValidator' и дополнительно укажем диапазон чисел от 0.0 до 999.0, а также ограничим число знаков после запятой -- двумя:

self.rate.setValidator(QDoubleValidator(0.0, 999.0, 2, self.rate) )

Здорово? Конечно здорово! Мы не только указали тип числа, но еще и ограничили диапазон вводимых чисел!

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

Форматированный ввод

Помните пример со строкой 'jOHN sMITH'? Вот как это можно исправить:

>>>'jOHN sMITH'.title()
'John Smith'

Да, это еще один из методов строковых объектов в Python, который начинает каждое слово в строке с заглавной буквы. Метод 'capitalize()' очень похож на 'title()', но переводит в верхний регистр только первый символ:

>>> 'jOHN sMITH'.capitalize()
'John smith'

Вы можете пойти дальше и попробовать "на зуб" 'upper()', 'lower()' и 'swapcase()'. Мне кажется -- вы заранее сможете предугадать что они делают.

А что вы слышали об 'rjust(n)'? Это один из самых полезных методов, использующихся для создания отчетов. Взгляните:

>>> 'John Smith'.rjust(15)
'     John Smith'

Строка была выровнена по правому краю, а длина ее увеличилась до 15 символов. Чудесно. А теперь попробуйте угадать, что делают методы 'center(n)' и 'ljust(n)'. Можете заглянуть в Python 2.1 Quick Reference и почитать о них.

Еще один очень важный оператор в Python -- это '%' (процент). Описание этого оператора и возможных комбинаций с объектами и символами форматирования займет не одну страницу, так что я лишь приведу здесь несколько примеров, которые могут представлять интерес.

Самое простое, что может сделать этот оператор -- это подставить значения переменных, которые изменяются в процессе исполнения программы:

>>> 'This is a %s example of its %s.' % ('good', 'use')
'This is a good example of its use.'

Кроме спецификатора формата '%s', применяемого к строковым объектам, есть еще '%r' и ряд других спецификаторов, аналогичных применяемым в функции printf языка программирования 'C': c, d, i, u, o, x, X, e, E, f, g, G.

Ниже приводится пример, взятый из Python 2.1 Quick Reference:

>>> '%s has %03d quote types.' % ('Python', 2)
'Python has 002 quote types.'

Кроме того, допускается также ссылаться на имена полей объектов по их имени.

А теперь перейдем к более интересному.

Номера телефонов

Телефонные номера могут быть разной длины. Иногда они состоят из 2 - 3 цифр (для внутренних телефонных сетей небольших предприятий). Иногда они могут состоять из 15 цифр и даже больше. Они могут даже содержать символы '#', "звездочка" и "запятая". Но самое неприятное состоит в том, что пользователь может попытаться ввести номер телефона совсем не в том виде, как это предусматриваете вы.

Будет несправедливо, если вы не позволите пользователю хотя бы попытаться ввести номер телефона правильно. Поэтому ваш валидатор должен воспринимать символы #, *, 'запятая', -, ), ( так же, как и цифры, разумеется, и вынуждать пользователя правильно вводить символы, например так:

'250-(555)-12-12'

а не так:

'(250) 555-1212'

т.е. в том виде, в котором мы хотим его получить (по крайней мере такой формат записи принят в Северной Америке). Не волнуйтесь, мы найдем достаточно общее решение.

Когда мне нужно сделать нечто подобное, я отправляюсь на Google. Как правило, отрывки кода, которые я нахожу, оказываются много лучше чем то, что создаю я сам. К сожалению, на этот раз мне пришлось обратиться за помощью к самому Guido van Rossum (создатель языка Python), который пояснил, что Python не имеет встроенных средств для таких вещей, но вполне возможно использовать нечто подобное:

import string
def fmtstr(fmt, str):
    res = [];
    i = 0;
    for c in fmt:
        if c == '#':
            res.append(str[i:i+1]); i = i+1;
        else:
            res.append(c)
    res.append(str[i:])
    return string.join(res)

Трудно спорить с авторитетным мнением, но такой вариант не является универсальным, однако он, по крайней мере, лучше, чем огромное количество конструкций 'if/then'. Давайте попробуем проверить работу этой функции:

>>> fmtstr('###-####', '5551212')
'5 5 5 - 1 2 1 2 '

В действительности, в своих программах, я очень часто использую конструкции 'if/then', чтобы породить нечто похожее по своей функциональности, для ввода номеров телефонов, дат и пр.. Но мой вариант еще более далек от совершенства, к тому же приходится "платить" большим объемом кода.

O.K., попробуем двинуться дальше... Для начала отфильтруем все "лишние" символы, которые может ввести пользователь:

def filter(inStr, allowed):
    outStr = ''
    for c in inStr:
        if c in allowed:
            outStr += c
    return outStr

Результат вызова:

>>>filter('250-(555)-12-12', string.digits)
'2505551212'

Можно определить свой собственный второй аргумент, как '0123456789#*,', чтобы добавить все допустимые символы.

Теперь возьмем код, предоставленный Guido и перевернем оба входных аргумента. Благодаря этому приему достаточно будет задать одну длинную строку формата, которая может быть применена к входной строке произвольной длины. Все "лишние" символы (вышедшие за рамки строки формата) мы просто прибавим к результату, таким образом ни один символ не будет потерян.

# импортировать модуль для работы с регулярными выражениями
import re

def formatStr(inStr, fmtStr, p = '^'):
    inList = [x for x in inStr] #список строк..
    fmtList = [x for x in fmtStr]
    # перевернуть !
    inList.reverse(); fmtList.reverse()
    outList = []
    i = 0
    for c in fmtList:
        if c == p:
            try:
                outList.append(inList[i])
                i += 1
            # прервать цикл, если fmtStr длиннее, чем inStr
            except IndexError:
                break
        else:
            outList.append(c)
    # обработать остаток inStr, если она длиннее, чем fmtStr
    while i < len(inList):
        outList.append(inList[i])
        i += 1
    # результат перевернуть, чтобы получить привычное представление
            outList.reverse()
    outStr = ''.join(outList)
    # выкинуть лишние скобки и дефисы, стоящие в начале строки
    while re.match('[)|-| ]', outStr[0]):
        outStr = outStr[1:]
    # добавить парные скобки
    while outStr.count(')') > outStr.count('('):
        outStr = '(' + outStr
    return outStr

[этот листинг в виде отдельного текстового файла.]

Практически это то же самое, что предложил Guido, за исключением шаблонного символа в строке формата, теперь это символ '^' (знак вставки), поскольку символ '#' может входить в состав телефонного номера. К тому же функция допускает возможность явно указать иной шаблонный символ в качестве третьего аргумента функции.

Ниже приводится несколько примеров вызова функции:

>>> formatStr('51212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'5-1212'
>>> formatStr('045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'(04) 555-1212'
>>> formatStr('16045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 (604) 555-1212'
>>> formatStr('1011446045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 011 44 (604) 555-1212'

На практике, вызов функции можно упростить, передавая строку формата в переменной:

phone_format_str = ' ^^^ ^^ (^^^) ^^^-^^^^'

Символ пробела, находящийся в начале строки формата, заставит отделить все "лишние" символы (если длина входной строки больше строки формата) от части, задаваемой форматом, пробельным символом. В результате вызов функции может выглядеть так:

formatStr(input, phone_format_str)

... перед этим, само собой разумеется, вы должны отфильтровать в строке 'input' все лишние символы, например функцией 'filter()'.

Почтовые индексы

На всякий случай -- если вы не знакомы с почтовыми индексами Канады, они выглядят примерно так:

'V8G 4L2'

На первый взгляд ничего особенного, но это только пока вы не попытаетесь ввести его. Особенно, если вы не так много печатаете (как и я). Вы можете нажать клавишу caps lock и забыть нажать ее повторно. Или напечатаете [shift]+символ, цифра, [shift]+символ, а в результате получите 'v*g $l@'. Нет нужды говорить о том, что пользователи жуть как не любят набирать подобные последовательности и практически не смотрят на то, что вводят.

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

>>>formatStr('V8G4L2', ' ^^^ ^^^')
'V8G 4L2'

Если для вашего приложения очень важна точность представления почтовых индексов, то вам наверняка потребуется провести ряд дополнительных проверок. Но в большинстве случаев этого будет достаточно.

Что можно сказать о номерах социального страхования? Вот пример:

>>> formatStr('716555123', '^^^-^^^-^^^')
'716-555-123'

Дополнительно номер социального страхования придется проверить на корректность, поскольку в них порядок следования цифр имеет определенный смысл. То же касается и кредитных карточек.

Надеюсь, что эти примеры помогут вам сэкономить свое время при разработке интерфейса с пользователем. Мне очень хотелось бы узнать ваши мнения и предложения об этих примерах, которые вы можете направлять по адресу pevans@catholic.org. Особенно, если они будут касаться обработки ввода дат (я имею ввиду календарные даты).

Между прочим, очень важно, чтобы вы не держали свои приемы форматирования в секрете от пользователей. Разместите их описание в разделе справки, в "совете дня", в подсказках и т.п. так, чтобы пользователи могли узнать о них. Если они узнают о более простом пути через несколько месяцев жесточайшей борьбы с клавиатурой, то ваш утренний кофе потеряет свою привлекательность из-за постоянных перешептываний у вас за спиной!

Paul Evans

Paul Evans влюблен во все, что связано с электроникой и в особенности с компьютерами. Он уже не молод и помнит годы своей юности, когда "пускал слюнки" по Altair 8080A. Он и его двое детей, живут в дебрях Северной Британской Колумбии. Они не лесорубы, но у них все OK.
Copyright (C) 2002, Paul Evans. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 82 of Linux Gazette, September 2002

Вернуться на главную страницу