Продолжение сравнения tcl и python. ВЭБ-интерфейс
Пришло время продолжить сравнение двух языков программирования на примере реализации ВЭБ-интерфейса к данным о звонках с АТС Panasonic KX TDA200. Попутно был реализован телефонный справочник берущий данные как из базы данных mysql так и с контроллера домена ActiveDirectory.
Для реализации задуманного нам понадобится:
В качестве вэб-сервера использован Apache2
В качестве СУБД — MySQL (mariadb)
Python3 с модулями (os, sys, pymysql, re, time ldap3)
Интерпретатор Tcl
Для включения поддержки апачем tcl-скриптов был взят Apache rivet. Так как в моем дистрибутиве (Simply Linux) данный пакет отсутствует, то пришлось собрать rpm-пакет из исходников (заодно обновив и tcl), данный процесс затруднений не вызвал и описывать его в рамках данной статьи смысла нет.
Доступ к mysql из tcl реализуется при помощи пакета MysqlTcl. Оный также был собран из исходников без всяких затруднений.
Базовой системой выбран Simply Linux. Примеры кода будут приводиться в таком порядке: сперва питон потом тикль. Имена процедур и переменных старался делать одинаковыми. Местами html-код просто копировался, ну и так как проект совсем небольшой, то отделять представление (хтмл) от логики (python, tcl) не стал. И последнее замечание — в реализации на питоне включена работа с LDAP (AD домен) т.е. в пром. эксплуатации телефонный справочник подгружается из домена, в тиклевой реализации этого нет, ввиду отсутствия у меня AD на данный момент.
Приступим. main — основная процедура вызываемая в начале скрипта. В зависимости от того какую ссылку (пункт меню) мы «ткнём» на страничке в main будет выделен соответсвующий параметр и сгенерирован ответ (html-код) путём вызова соответсвующих процедур. URL выглядит вот так — «localhost/?querytype=external». querytype может принимать значения: internal — справочник внутренних телефонов, external — список внешних линий, report — отчет по звонкам, ldap — справочник из АД.
def application(environ, start_response):
status = '200 OK'
# проверяем наличие параметров в запросе и всяко-разно реагируем
if environ['QUERY_STRING'] == '':
output = bytes((header() + body() + footer()).encode('utf8'))
elif environ['QUERY_STRING'].split('&'):
paramDict = {item.split('=')[0]: item.split('=')[1] for item in environ['QUERY_STRING'].split('&')}
if paramDict.get('query_type') == 'internal':
output = bytes((header() + body() + getInternalNumbers() + footer()).encode('utf8'))
elif paramDict.get('query_type') == 'external':
output = bytes((header() + body() + getCOline() + footer()).encode('utf8'))
elif paramDict.get('query_type') == 'report':
output = bytes((header() + body() + ReportForm() + ReportData(environ) + footer()).encode('utf8'))
elif paramDict.get('query_type') == 'ldap':
domain = paramDict.get('domain')
group = paramDict.get('group')
output = bytes((header() + body() + getLDAPusers(domain, group) + footer()).encode('utf8'))
else:
output = bytes((header() + body() + footer()).encode('utf8'))
response_headers = [('Content-type', 'text/html;charset=utf-8'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
return [output]
В обоих случаях значения параметров берутся из массива переменных: в питоне это environ[‘QUERY_STRING’] в тикле rivet::var.
proc main {} {
if { [::rivet::var exists query_type] } {
set query_type [::rivet::var get query_type]
if {$query_type == "internal"} {
set output "[header] [body] [getInternalNumbers] [footer]"
} elseif {$query_type == "external"} {
set output "[header] [body] [getCOline] [footer]"
} elseif {$query_type == "ldap"} {
set output "[header] [body] [getLDAPusers] [footer]"
} elseif {$query_type == "report"} {
set output "[header] [body] [reportForm] [reportData] [footer]"
}
} else {
set output "[header] [body] [footer]"
}
return $output
}
Т.е. чтобы было более понятно:
set output "[header] [body] [reportForm] [reportData] [footer]"
это означает, что значение переменной $output будет включать в себя результат выполнения процедур: header, body, reportForm, reportData и footer.
header — возвращает заголовок страницы. css используется один и тотже, выглядит всё одинаково.
Python + Tcl
def header():
txtHeader = '<html><head><meta charset="utf-8"><link rel="shortcut icon" href="favicon.png" >\n' \
'<link rel="icon" type="image/png" href="favicon.png" >\n' \
'<link href="css/layout.css" rel="stylesheet" type="text/css" />\n' \
'<title>Телефоны</title>\n' \
'<link href="css/menu.css" rel="stylesheet" type="text/css" /></head><body>\n'
return txtHeader
proc header {} {
set txtHeader "<html><head><meta charset=\"utf-8\"><link rel=\"shortcut icon\" href=\"favicon.png\" >\n
<link rel=\"icon\" type=\"image/png\" href=\"favicon.png\" >\n
<link href=\"css/layout.css\" rel=\"stylesheet\" type=\"text/css\" />\n
<title>Телефоны</title>\n
<link href=\"css/menu.css\" rel=\"stylesheet\" type=\"text/css\" /></head><body>\n"
return $txtHeader
}
Начальная страница:
footer — процедура вывода «низа» страницы
Python
def footer():
txtFooter = '</body></html>\n'
return txtFooter
Tcl
proc footer {} {
set txtFooter "</body></html>\n"
return $txtFooter
}
Основное «тело» страницы
Python
def body():
<spoiler title="Python + Tcl">
txtBody = '<table><tr><td><h2 align=left>ТЕЛЕФОНЫ</h2></td></tr>\n' \
'<tr><td>{}</td></tr></table>\n'.format(menu())
return txtBody
Tcl
proc body {} {
set txtBody "<table><tr><td><h2 align=left>ТЕЛЕФОНЫ</h2></td></tr>\n
<tr><td>[menu]</td></tr></table>\n"
return $txtBody
}
menu — вывод меню:
Python
def menu():
#<div class="container">
txtMenu = '<div class="container"><ul id="nav"></li>\n' \
'<li><a class="hsubs" href="#">Справочник</a><ul class="subs">\n' \
'<li><a href="?query_type=external">Городские телефоны</a></li>\n' \
'<li><a href="?query_type=ldap&domain=domain1">Список абонентов 1</a></li>\n' \
'<li><a href="?query_type=ldap&domain=domain2">Список абонентов 2</a></li>\n' \
'<li><a href="?query_type=ldap&domain=domain3">Список абонентов 3</a></li></ul></li>\n' \
'<li><a class="hsubs" href="">Отчёты</a><ul class="subs">' \
'<li><a href="?query_type=report&report_type=int">Список звонков по номеру</a></li>\n' \
'<li><a href="?query_type=report&report_type=dep">Звонки по отделам</a></li>\n' \
'</ul></div>\n'
return txtMenu
Tcl
proc menu {} {
set txtMenu "<div class=\"container\"><ul id=\"nav\"></li>
<li><a class=\"hsubs\" href=\"#\">Справочник</a><ul class=\"subs\">
<li><a href=\"?query_type=external\">Городские телефоны</a></li>
<li><a href=\"?query_type=internal\">Внутренние</a></li></li>
<li><a href=\"?query_type=ldap\">Внутренние (AD)</a></li></ul></li>
<li><a class=\"hsubs\" href=\"\">Отчёты</a><ul class=\"subs\">
<li><a href=\"?query_type=report&report_type=int\">Список звонков по номеру</a></li>
<li><a href=\"?query_type=report&report_type=dep\">Звонки по отделам</a></li>\n</ul></div>"
return $txtMenu
}
Теперь посмотрим как выглядит справочник внутренних номеров (он же телефонный справочник) снаружи:
и внутри, за это (в части касающейся вывод списка номеров) отвечает процедура getInternalNumbers:
Python
def getInternalNumbers():
conn = connectDB()
c = conn.cursor()
order = 'int_number'
result = '<h4 align=left>Список внутренних телефонов</h4>'
qwr = "SELECT * FROM int_number ORDER by " + order
c.execute(qwr)
listHeader = '<table class="table_dark"><tr><th>№ п/п</th>\n' \
'<th><a href=index.py?query_type=internal&order=int_number>Телефон</a></th>\n' \
'<th><a href=index.py?query_type=internal&order=fio>ФИО</a></th>\n' \
'<th>Описание<th>Электро-почта</th></tr>\n'
result = result + listHeader
for row in c.fetchall():
rowData = "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>\n" % (row[0], row[1], row[2], row[4], row[3])
result = result + rowData
return (result + "</table>\n")
Tcl
proc getInternalNumbers {} {
set conn [connectDB]
set order "int_number"
set result "<h4 align=left>Список внутренних телефонов</h4>"
set qr "SELECT * FROM int_number ORDER by $order"
set query [mysql::query $conn $qr]
set listHeader "<table class=\"table_dark\"><tr><th>№ п/п</th>\n
<th><a href=index.py?query_type=internal&order=int_number>Телефон</a></th>\n
<th><a href=index.py?query_type=internal&order=fio>ФИО</a></th>\n
<th>Описание<th>Эл.почта</th></tr>\n"
set result "$result $listHeader"
set rowData ""
while {[set row [mysql::fetch $query]]!=""} {
append rowData "<tr><td>[lindex $row 0]</td>
<td>[lindex $row 1]</td>
<td>[lindex $row 2]</td>
<td>[lindex $row 4]</td>
<td><a href=\"mailto:[lindex $row 3]\">[lindex $row 3]</a></td></tr>\n"
}
mysql::endquery $query
set result "$result $rowData"
disconnectDB $conn
return "$result </table>\n"
}
Для работы с СУБД для питона был взят pymysql, для тикля mysqltcl. Что там, что там соединение с БД пишется в одну строку: Pyrhon def connectDB(): c = pymysql.connect( db='ats', user='ats', passwd='pass', host='x.x.x.x', charset='utf8') return c
Tcl
proc connectDB {} {
return [mysql::connect -host localhost -user root -db ats]
}
Вывод списка внешних линий подобен справочнику внутренних номеров и приводить я его тут не буду. Посмотрим подробнее на отчёт по звонкам, ради чего, собственно, всё это и затевалось. Так он выглядит визуально:
Отчет разбит на две процедуры — это вывод формы и запрос и вывод полученных данных. Форма ничего особенного не представляет, генерится процедуркой ReportForm:
Python
def ReportForm():
txtReportForm = '<h3 align=center>Отчёт по звонкам</h3><table><tr valign=top><td>' \
'<form><input type=hidden name="query_type" value="report">' \
'<fieldset class="report">' \
'<label>Номер<input type="text" name="int_number"></label>' \
'<label>Период с<input type="date" name="date_begin"></label>' \
'<label>По<input type="date" name="date_end"></label>' \
'</fieldset>' \
'<fieldset class="report-action">' \
'<input class="btn" type="submit" name="submit" value="Выбрать">' \
'</fieldset></form></td>'
return txtReportForm
Tcl
proc reportForm {} {
set txtReportForm "<h3 align=center>Отчёт по звонкам</h3><table><tr valign=top><td>
<form><input type=hidden name=\"query_type\" value=\"report\">
<fieldset class=\"report\">
<label>Номер<input type=\"text\" name=\"int_number\"></label>
<label>Период с<input type=\"date\" name=\"date_begin\"></label>
<label>По<input type=\"date\" name=\"date_end\"></label>
</fieldset>
<fieldset class=\"report-action\">
<input class=\"btn\" type=\"submit\" name=\"submit\" value=\"Выбрать\">
</fieldset></form></td>"
return $txtReportForm
}
В процедуру ReportData включена проверка корректности ввода параметров (номер и дата), т.е. своего рода защита от SQL-иньекций и невнимательности пользователей, запрос к БД и вывод данных на экран. Конечно, это можно было разбить еще мельче но я не стал.
Python
def ReportData(environ):
paramDict = {item.split('=')[0]: item.split('=')[1] for item in environ['QUERY_STRING'].split('&')}
# проверка корректности параметров
# внутренний номер (3 цифры 100-999)
templateNumber = '(^[1-9][0-9][0-9]$)'
if re.match(templateNumber, str(paramDict.get('int_number'))) is not None:
numbers = paramDict.get('int_number')
else:
return ErrorMessage('Введите внутренний номер')
# проверка и преобразование дат в удобоваримый для субд формат
templateDate = '^(0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20)[0-9][0-9]$'
if re.match(templateDate, str(paramDict.get('date_begin'))) is not None:
date_begin = time.strftime("%Y-%m-%d", time.strptime(str(paramDict.get('date_begin')), "%d.%m.%Y"))
else:
return ErrorMessage('Неверный формат даты начала периода')
if re.match(templateDate, paramDict.get('date_end')) is not None:
date_end = time.strftime("%Y-%m-%d", time.strptime(str(paramDict.get('date_end')), "%d.%m.%Y"))
else:
return ErrorMessage('Неверный формат даты окончания периода')
conn = connectDB()
c = conn.cursor()
order = 'call_date'
result = '<td><h4 align=left>Список звонков с номера {}</h4>'.format(numbers)
qwr = "SELECT * FROM cdr where int_number=\'{}\' AND call_date BETWEEN CAST(\'{}\' AS DATE) AND CAST(\'{}\' AS DATE) LIMIT 1000".format(numbers, date_begin, date_end)
c.execute(qwr)
listHeader = '<table class="table_dark"><tr>' \
'<th>№ п/п</th><th>Дата</th><th>Время</th><th>Внут.номер</th><th>Внеш.линия</th><th>Вызываемый номер</th>' \
'<th>Ring</th><th>Длительность</th><th>АСС</th><th>Код звонка</th><th>Направление</th><tr>\n'
result = result + listHeader
for row in c.fetchall():
rowData = '<td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>'\
.format(row[0], row[1], row[2], row[3], row[4], row[5], row[5], row[7], row[9], row[9], row[10])
result = result + rowData
return (result + "</table></td></tr></table>\n" + qwr)
Tcl
proc reportData {} {
set result ""
set err ""
# Проверяем введенные параметры отчёта как на наличие так и на корректность
if { [::rivet::var exists int_number] } {
set number [::rivet::var get int_number]
if {[regexp -nocase -all -- {^([0-9][0-9][0-9])$} $number match v1]} {
append result $number
} else {
append err "<p><font color=red></b>Введите номер телефона</b></font></p>"
}
} else {
return
}
if { [::rivet::var exists date_begin] } {
if [regexp -nocase -- {^(0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20[0-9][0-9])$} [::rivet::var get date_begin] match d m y] {
set dBegin "$y-$m-$d"
} else {
append err "<p><font color=red></b>Введите дату начала</b></font></p>"
}
}
if { [::rivet::var exists date_end] } {
if [regexp -nocase -- {^(0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20[0-9][0-9])$} [::rivet::var get date_end] match d m y] {
set dEnd "$y-$m-$d"
} else {
append err "<p><font color=red></b>Введите дату окончания</b></font></p>"
}
}
if {$err ne ""} {
return "<td>$err</td></tr></table>"
}
set conn [connectDB]
set order "call_date"
set result "<td><h4 align=left>Список звонков с номера $number</h4>"
set qr "SELECT * FROM cdr where int_number=\'$number\' AND call_date BETWEEN CAST(\'$dBegin\' AS DATE) AND CAST(\'$dEnd\' AS DATE) LIMIT 1000"
set query [mysql::query $conn $qr]
set listHeader "<table class=\"table_dark\"><tr>
<th>№ п/п</th><th>Дата</th><th>Время</th><th>Внут.номер</th><th>Внеш.линия</th><th>Вызываемый номер</th>
<th>Ring</th><th>Длительность</th><th>АСС</th><th>Код звонка</th><th>Направление</th><tr>"
append result $listHeader
set rowData ""
while {[set row [mysql::fetch $query]]!=""} {
append rowData "<tr>"
for {set i 0} {$i <=10} {incr i} {
append rowData "<td>[lindex $row $i]</td>"
}
}
append result "$rowData </table></td></tr></table>"
return $result
}
Тут есть некоторые различия в алгоритме преобразования даты. Так как в mysql дата (в моём случае) хранится ввиде YYYY-MM-DD, а для нас этот формат неудобен, то и была введена конвертация даты из одного формата в другой. Для питона использовались встроенные функции:
date_begin = time.strftime("%Y-%m-%d", time.strptime(str(paramDict.get('date_begin')), "%d.%m.%Y"))
Но для tcl я поступил по другому. Ввиду того, что проверка на правильность ввода даты производится regexp-ом:
if { [regexp -nocase -- {^(0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20[0-9][0-9])$} [::rivet::var get date_begin] match d m y]}
То на выходе этой команды, дата уже разбита на составные части и её надо просто скомпоновать так как требуется:
set dBegin "$y-$m-$d"
И еще в реализации на tcl строку:
rowData = '<td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>'\
.format(row[0], row[1], row[2], row[3], row[4], row[5], row[5], row[7], row[9], row[9], row[10])
заменил циклом:
for {set i 0} {$i <=10} {incr i} {
append rowData "<td>[lindex $row $i]</td>"
}
Вот и всё.
Все файлы доступны на BitBucket. На этом повествование на тему сравнения python и tcl прекращаю, во всяком случае пока не набёрется материал.