И ещё одно замечание — питонист из меня ещё тот, поэтому за качество кода пинать можно но не сильно. Примеры кода будут приводиться в таком порядке: сперва питон потом тикль, описание процедур и команд будет идти в порядке необходимом для понимания работы скриптов, а не так как они записаны в коде.
Python
import pymysql import sys, os import re import datetime # параметры соединения с СУБД db_host = 'host' db_user = 'dbuser' db_pass = 'dbpass' out_dir = '/var/log/ats'
Tcl
package require mysqltcl # параметры соединения с СУБД #set db(host) "host" #set db(user) "user" #set db(pass) "password" #set db(dbname) "ats_test" #set out_dir "~/tmp/ats"
Тут в общем всё понятно — импортируем нужные и не очень модули, и инициализируем переменные. Но в примере на tcl строки закоментированы (эти строки нужно вынести в другой файл см. под спойлер), ниже будет ясно почему.
# параметры соединения с СУБД set db(host) "host" set db(user) "user" set db(pass) "password" set db(dbname) "ats_test" set out_dir "~/tmp/ats"
Скрипт может обрабатывать данные из текстового файла и напрямую из последовательного порта, для этого добавлены ключи для запуска, соответсвенно -port — чтение из порта, -file — чтение из файла:
if __name__ == "__main__": if len(sys.argv) > 2: if sys.argv[1] == '-port': #action = 'read_port' port_name = sys.argv[2] port_data_read(port_name) if sys.argv[1] == '-file': #action = 'read_file' log_file_name = sys.argv[2] log = open(log_file_name) for line in log: parce_string(line) log.close() else: print ("\nФормат вызова:\n- для обработки файла\ \n # python data_reader.py -file TDA20013082015_12052016.lg\ \n- для чтения данных напрямую с com-порта АТС\ \n # python data_reader.py -port /dev/ttyUSB0\n") sys.exit(1)
В tcl-скрипте добавлена еще одна опция -conf, тестирование кода проводилось на рабочем сервере, и корячить туда помимо питона ещё и тикль, было уже черезчур. И по сему, пришлось собирать «экзешник» по принципу «всё включено» а чтобы обеспечить гибкость настроек и была добавлена эта опция (да и так правильнее).
# Обработка ключей командной сроки if {[llength $argv] >= 2} { if {[lindex $argv 0] == "-conf"} { source [lindex $argv 1] } else { puts "Не указан конфигурационный файл" } if {[lindex $argv 2] == "-port"} { set port_name [lindex $argv 3] PortDataRead $port_name } if {[lindex $argv 2] == "-file"} { set log_file_name [lindex $argv 3] set log [open $log_file_name "r"] # проверям наличие каталога и если его нет то создаём if {[file isdirectory $out_dir] == 0} { file mkdir $out_dir } # читаем файл построчно while {[gets $log line] >= 0} { ParceString $line } close $log } } else { puts "\nФормат вызова:\n- для обработки файла\ \n # -conf config.tcl \n # tclsh logger.tcl -conf config.tcl -file TDA20013082015_12052016.lg\ \n- для чтения данных напрямую с com-порта АТС\ \n # tclsh logger.tcl -conf config.tcl -port /dev/ttyUSB0\n" exit }
Идём дальше. Функции работы с последовательным портом:
def port_data_read(port_name): global out_dir """Чтение данных из последовательного порта""" import serial ser = serial.Serial(port_name) ser.baudrate = 9600 while True: # читаем строку из порта line = ser.readline() # декодируем строку в текстовый формат line = line.decode() # обрезаем лишние пробельные символы line = line.rstrip() # отдаём процедуре обработки строки parce_string(line)
В tcl операции работы с файлами или портами используются одни и те же. Т.е. сперва создаётся так называемый pipe (труба или канал) командой open потом уже из этой «трубы» читаются данные или записываются туда, будь то файл или последовательный порт.
# читаем данные из порта proc PortDataRead {portName} { global fh # открываем порт в режииме "только чтение" set fh [open $portName RDONLY] # настраиваем канал в неблокирующем режиме и соответсвующими параметрами порта fconfigure $fh -blocking 0 -buffering line -mode 9600,n,8,1 -translation crlf -eofchar {} # "навешиваем" событие fileevent $fh readable Read vwait forever } # читаем строку из порта и отправляем на обработку proc Read {} { global fh if {[gets $fh line] >= 0} { ParceString $line } }
Команда
fileevent $fh readable Read
позволяет навешивать на канал событие, точнее реакцию на событие, в данном случае мы указали, что при появлении каких-то данных в канале выполнять процедуру Read.
Вот и подошли к ключевому моменту — разбору строки. АТС «выбрасывает» данные ввиде строки где поля разделены пробелами, точнее, для каждого поля задан размер в символах и отсутствующие данные добиваются пробелами:
30/09/16 10:44 501 01 <I> 0'00 00:00'13 D0
Код функции на питоне:
def parce_string(line): """Получает на вход строку и раскидывает её в нужном виде и пишет в файл""" # тут проверяется строка на ненужный хлам if line[:3] == "---" or line == "" or line[3:7] == "Date": print(line) return print(line) # Создаём текстовые файлы на всякий случай, для дублирования информации now = datetime.datetime.now() out_log_name = os.path.join(out_dir, '{}_{}'.format(now.month, now.year)) out_log = open(out_log_name,"a+") out_log.write(line + '\n') out_log.close() # Разбор строки # Преобразуем дату к виду "ДД/ММ/ГГГГ" (с годом решил не мудрствовать а решить в лоб) call_date = "20{}/{}/{}".format(line[6:8],line[3:5],line[:2]) # выдёргиваем данные и обрезаем лишние пробелы call_time = line[9:14].strip() int_number = line[19:22].strip() ext_co_line = line[23:25].strip() dial_number = line[26:51].strip() ring = line[52:56].strip() call_duration = re.sub("'", ":", line[57:65].strip()) acc_code = line[66:77].strip() call_code = line[77:81].strip() # Проверяем признак входящщего звонка if dial_number == "<I>": call_direct = "Входящий" dial_number = "" elif dial_number[:3] == "EXT": call_direct = "Внутренний" dial_number = dial_number[3:] else: call_direct = "Исходящий" # отправлем в процедуру добавление в БД insert(call_date=call_date, call_time=call_time, int_number=int_number, ext_co_line=ext_co_line, dial_number=dial_number, ring=ring, call_duration=call_duration, acc_code=acc_code, call_code=call_code, call_direct=call_direct)
В строковых функциях между питом и тиклем есть некоторые различия, к примеру line[9:14] вернёт содержимое строки начиная с 9 и кончая 13 символом включительно, т.е. в качестве правой границы указывается следующий за значимым символ. В tcl, для этой цели, используется команда [string range $line 9 13].
proc ParceString {line} { global out_dir arrVariables # Получает на вход строку и раскидывает её в нужном виде и пишет в файл if {[string range $line 0 2] == "---" || $line == "" || [string range $line 3 6] == "Date"} { #puts $line return } # Создаём текстовые файлы на всякий случай, для дублирования информации # получим имя файла ввиде ММ_ГГГГ используя цепь вложенных команд clock set fName [clock format [clock scan "now" -base [clock seconds]] -format %m_%Y] set out_log_name [file join $out_dir $fName] set out_log [open $out_log_name "a+"] puts $out_log "$line" close $out_log # Разбор строки # все данные сохраняются в именованом массиве переменных # Преобразуем дату к виду "ДД/ММ/ГГГГ" set arrVariables(call_date) "20[string range $line 6 7]\/[string range $line 3 4]\/[string range $line 0 1]" set arrVariables(call_time) [string range $line 9 13] set arrVariables(int_number) [string range $line 19 21] set arrVariables(ext_co_line) [string range $line 23 24] set arrVariables(dial_number) [string range $line 26 50] set arrVariables(ring) [string range $line 52 55] set arrVariables(call_duration) [string range $line 57 66] set arrVariables(acc_code) [string range $line 66 76] set arrVariables(call_code) [string range $line 77 81] # Проверяем признак входящщего звонка if {$arrVariables(dial_number) == "<I>"} { set arrVariables(call_direct) "In" set arrVariables(dial_number) "" } elseif {[string range $arrVariables(dial_number) 0 3] == "EXT"} { set arrVariables(call_direct) "Ext" set arrVariables(dial_number) [string range $arrVariables(dial_number) 3 end] } else { set arrVariables(call_direct) "Out" } InsertData
В тикле есть такая замечательная штука, как массив переменных, в данном случае это arrVariables(), в который сохраняются все данные в соответсвующих переменных, определяемых ключом, к примеру arrVariables(call_time) — это время звонка. Можно было это всё сохранить ввиде списка списков «ключ — значение», на примере предыдущей переменной, это выглядело-бы следющим образом:
т.е. в список lstVar (точнее переменную содержащую список) добавляем список из двух значений call_time и содержимое строки $line между 9 и 13 символами включительно.lappend lstVar [list call_time [string range $line 9 13]]
А теперь добавим строку в БД, структура которой описана ниже:
CREATE TABLE `cdr` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`call_date` date DEFAULT NULL,
`call_time` time DEFAULT NULL,
`int_number` varchar(11) DEFAULT NULL,
`ext_co_line` char(2) DEFAULT NULL,
`dial_number` varchar(30) DEFAULT NULL,
`ring` varchar(5) DEFAULT NULL,
`call_duration` time DEFAULT NULL,
`acc_code` varchar(20) DEFAULT NULL,
`call_code` char(2) DEFAULT NULL,
`call_direct` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2775655 DEFAULT CHARSET=utf8 COMMENT='Call Data Records';Запрос к БД строится динамически на основе параметров переданных в функцию. Из кода, в принципе, всё понятно — форматируем строки в соответствии с требованиями к SQL-запросу, в нужных местах вставляем запятые или скобки и т.д. И так как запрос строится динамически, то в некоторых местах добавляется лишняя запятая и пробел, которые приходится удалять командой rstrip(', ') (можно, конечно, считать количество полей и добавлять нужное число запятых, но накладных расходов это не уменьшит и потому сделано так). Так как данные сыпятся ни часто, то на каждую строку данных (один запрос) выполняется одна транзакция, т.е. подключились, выполнили запрос, отключились.
И собсвенно код функции:
def insert(**kwargs): """Вставка данных в БД. В качестве параметров список полей и значений""" qwery = 'INSERT INTO `cdr` (' for key in kwargs.keys(): qwery = "{} `{}`, ".format(qwery,key) qwery = qwery.rstrip(', ') + ') VALUES(' for key in kwargs.keys(): #qwery = qwery + '"' + kwargs.get(key) +'", ' qwery = "{} \"{}\",".format(qwery,kwargs.get(key))
А теперь то же самое на тикле:
proc InsertData {} { global arrVariables db set qwery "INSERT INTO `cdr` (" # читаем тот самый массив с параметрами и генерим запрос foreach key [array names arrVariables] { set qwery "$qwery `$key`, " } set qwery "[string trimright $qwery ", "]\) VALUES\(" foreach key [array names arrVariables] { set qwery "$qwery \"[string trim $arrVariables($key)]\"," } set qwery "[string trimright $qwery ", "]\);" puts $qwery # подключаемся к БД выполняем запрос и отключаемся set conn [mysql::connect -host $db(host) -db $db(dbname) -user $db(user) -password $db(pass) -encoding utf-8] mysql::exec $conn $qwery mysql::commit $conn mysql::close $conn }
Вот на этом можно и завершить повествование. На мой взгляд никаких приемуществ, в данном конкретном случае, ни у того ни у другого языка нет (лукавлю слегка, по мне тикль красивше, но это в силу привычки). Сложностей с питоном также не возникает. Исходники тестировались в Centos и Fedore последних версий и Windows 10. Проектик (в части сбора данных) доведён до логического завершения и запущен в работу, есть еще простенькая вэб мордочка со справочником телефонов и отчетами по собранным данным, но это тема другой статьи.
Исходники доступны тут: Git репозитарий

