Вторая часть сравнения python и tcl

В предыдущей статье была описана реализация задачи сбора данных о звонках с АТС, включающее в себя получение, разбор данных и добавление в БД. Решение данной задачи представлено на двух языках программирования — python и tcl. И вот, как и обещал, представляю код (на двух языках) WEB-интерфейса, включающего в себя телефонный справочник и отчеты о звонках.

В качестве вэб-сервера используется Apache2 с модулями wsgi и rivet (соответсвенно для python и tcl). Все пакеты устанавливались из официальных репозитариев, в случае отсутствия в репах rpm-пакеты собирались самостоятельно. Базовой системой выбран Simply Linux.

Примеры кода будут приводиться в таком порядке: сперва питон потом тикль. Имена процедур и переменных старался делать одинаковыми. Местами html-код просто копировался, ну и так как проект совсем небольшой, то отделять представление (хтмл) от логики (python, tcl) не стал.

И последнее замечание — в реализации на питоне включена работа с LDAP (AD домен) т.е. в пром. эксплуатации телефонный справочник подгружается из домена, в тиклевой реализации этого нет, ввиду отсутствия у меня AD на данный момент.

Приступим.

main — основная процедура вызываемая в начале скрипта.
В зависимости от того какую ссылку (пункт меню) мы «ткнём» на страничке в main будет выделен соответсвующий параметр и сгенерирован ответ (html-код) путём вызова соответсвующих процедур.

URL выглядит вот так — «localhost/?query_type=external». query_type может принимать значения: 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 }

Начальная страница:

image

footer — процедура вывода «низа» страницы

Python + Tcl

def footer():     txtFooter = '</body></html>\n'     return txtFooter 

proc footer {} {     set txtFooter "</body></html>\n"     return $txtFooter }

Основное «тело» страницы

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 

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 }

Теперь посмотрим как выглядит справочник внутренних номеров (он же телефонный справочник) снаружи:

image

и внутри, за это (в части касающейся вывод списка номеров) отвечает процедура 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. Что там, что там соединение с БД пишется в одну строку:

def connectDB():     c = pymysql.connect(         db='ats',         user='ats',         passwd='pass',         host='x.x.x.x',         charset='utf8')     return c 

proc connectDB {} {     return [mysql::connect -host localhost -user root -db ats] }

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

image

Отчет разбит на две процедуры — это вывод формы и запрос и вывод полученных данных.
Форма ничего особенного не представляет, генерится процедуркой 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>" } 

Вот и всё.

Напоследок, хоть процедура получения данных по LDAP и выходит за рамки темы статьи но входит в проект, то будет справедливым показать и её:

LDAP Python

# Список пользователей (ФИО, телефон, почта, отдел) из LDAP (AD) def getLDAPusers(domain, group):     order = 'int_number'     if domain == 'domain1':         head = '<p>Для внешних абонентов звонить на xxxxxx + добавочный номер</p>'         org = '1'         s = Server('192.168.1.1')         c = Connection(s, user='domain1\\phone', password="pass", authentication=NTLM)         # perform the Bind operation         if not c.bind():             print('error in bind', c.result)         c.search('OU=users,dc=domain1,dc=local', '(objectclass=person)',                  attributes=['cn', 'mail', 'telephoneNumber', 'department', 'title', 'mobile'])     elif domain == 'domain2':         head = '<p>Для внешних абонентов звонить на xxxxxxx + добавочный номер</p>'         org = '2'         s = Server('192.168.2.2')         c = Connection(s, user='domain2\\phone', password="pass", authentication=NTLM)         if not c.bind():             print('error in bind', c.result)         c.search('OU=users,dc=domain2,dc=local', '(objectclass=person)',                  attributes=['cn', 'mail', 'telephoneNumber', 'department', 'title', 'mobile'])     elif domain == 'domain3':         head = '<p>Для внешних абонентов звонить на xxxxxxx + добавочный номер</p>'         org = '3'         s = Server('192.168.3.3')         c = Connection(s, user='domain3\\phone', password="pass", authentication=NTLM)         if not c.bind():             print('error in bind', c.result)         c.search('OU=users,dc=domain3,dc=local', '(objectclass=person)',                  attributes=['cn', 'mail', 'telephoneNumber', 'department', 'title', 'mobile'])      result = '<h4 align=left>Список абонентов %s</h4>\n%s' % (org, head)     listHeader = '<table class="table_dark"><tr>\n' \                  '<th>ФИО</th>\n' \                  '<th>Отдел</th><th>Должность</th>\n' \                  '<th>Телефон</th>\n' \                  '<th>Эл.почта</th><th>Мобильный телефон</th></tr>\n'     result = result + listHeader     for item in c.entries:         item = str(item)         name = re.search('(cn:)(.+?)(\n)', item)         if name:             name = name.groups()[1]         else:             name=name         department = re.search("(department:)(.+?)(\n)", item)         if department:             dep = department.groups()[1]         else:             dep = ""         title = re.search("(title:)(.+?)(\n)", item)         if title:             title = title.groups()[1]         else:             title = ""         mail = re.search("(mail:)(.+?)(\n)", item)         if mail:             mail = mail.groups()[1]         else:             mail = ""         telephone = re.search("(telephoneNumber:)(.+?)(\n)", item)         if telephone:             phone = telephone.groups()[1]         else:             phone = ""         mobile = re.search("(mobile:)(.+?)(\n)", item)         if mobile:             mobile = mobile.groups()[1]         else:             mobile = ""         rowData = "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><a href=\"mailto:%s\">%s</a></td><td>%s</td></tr>\n" % (name, dep, title, phone, mail, mail, mobile)         result = result + rowData     return (result + "</table>\n") 

Все файлы доступны на BitBucket. На этом повествование на тему сравнения python и tcl прекращаю, во всяком случае пока не набёрется материал.

FavoriteLoadingДобавить в избранное

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *