Анализ ввода необработанных файлов с использованием регулярных выражений
Следующая функция представляет собой пример анализа ввода необработанных файлов с регулярными выражениями.
Группы захвата регулярных выражений зациклены для создания записей. Это шаблон многократного использования, который можно применять во многих случаях. Подробнее о том, как это работает, можно узнать в разделе «Группировка в составных регулярных выражениях».
Функция отфильтрует записи, соответствующие значениям параметра. Оставляя им значения по умолчанию, функция возвращает все строки данных.
def get_dev_data(file_path, timestamp=None, iface_num=None, idx_num=None,
syntax=None, counter=None):
timestamp = timestamp or r'\d+'
iface_num = iface_num or r'(?:\.\d+)+'
idx_num = idx_num or r'\d+'
syntax = syntax or r'\w+'
counter = counter or r'\w+'
expr = rf"""
TIMESTAMP:\s({timestamp}) # date - TimeStamp
| # ** OR **
({iface_num}) # num - InterfaceNum
\.({idx_num})\s=\s # num2 - IndexNum
({syntax}):\s # syntax - SyntaxName
({counter}) # counter - Counter
"""
expr = re.compile(expr, re.VERBOSE)
rows = []
keys = ['TimeStamp', 'InterfaceNum', 'IndexNum', 'SyntaxName', 'Counter']
cols = {k: [] for k in keys}
with open(file_path, 'r') as data_file:
for line in data_file:
try:
find_data = expr.findall(line)[0]
vals = [tstamp, num, num2, sntx, ctr] = list(find_data)
if tstamp:
cur_tstamp = tstamp
elif num:
vals[0] = cur_tstamp
rows.append(vals)
for k, v in zip(keys, vals):
cols[k].append(v)
except IndexError:
# expr.findall(line)[0] indexes an empty list when no match.
pass
return rows, cols
Возвращается кортеж. Первый элемент, rows
, представляет собой список строк данных в простом формате; второй элемент, cols
, представляет собой словарь, снабженный ключом по имени столбца со списком данных строк на ключ. Оба содержат одни и те же данные и могут быть усвоены Pandas с pd.DataFrame.from_records()
или pd.DataFrame.from_dict()
соответственно.
пример фильтрации
Это показывает, как записи могут быть отфильтрованы используя параметры функции. Я думаю, что последний, result_4
, соответствует описанию в вопросе. Предположим, что для iface_num
установлены ваши const_num
, а для syntax
- ваши const_syntax
значения. Будут возвращены только совпадающие записи.
if __name__ == '__main__':
file = r"/test/inputdata.txt"
result_1 = get_dev_data(file)[0]
result_2 = get_dev_data(file, counter='Eth2')[0]
result_3 = get_dev_data(file, counter='Eth2|AA1')[0]
result_4 = get_dev_data(file,
iface_num='.1.3.4.1.2.1.1.1.1.1.1', syntax='STRING')[0]
for var_name, var_val in zip(['result_1', 'result_2', 'result_3', 'result_4'],
[ result_1, result_2, result_3, result_4]):
print(f"{var_name} = {var_val}")
Вывод
result_1 = [['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '128', 'STRING', 'AA1'], ['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '129', 'STRING', 'Eth1'], ['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '130', 'STRING', 'Eth2']]
result_2 = [['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '130', 'STRING', 'Eth2']]
result_3 = [['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '128', 'STRING', 'AA1'], ['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '130', 'STRING', 'Eth2']]
result_4 = [['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '128', 'STRING', 'AA1'], ['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '129', 'STRING', 'Eth1'], ['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '130', 'STRING', 'Eth2']]
Используя первый возвращенный элемент кортежа, доступ к данным столбца можно получить из возвращенные записи, используя их смещения. Например, TimeStamp
будет доступен как first_item[0][0]
- первая строка, первый столбец. Или строки могут быть преобразованы в фрейм данных и доступны таким образом.
Входной файл /test/inputdata.txt
TIMESTAMP: 1579051725 20100114-202845
.1.2.3.4.5.6.7.8.9 = 234567890
ifTb: name-nam-na
.1.3.4.1.2.1.1.1.1.1.1.128 = STRING: AA1
.1.3.4.1.2.1.1.1.1.1.1.129 = STRING: Eth1
.1.3.4.1.2.1.1.1.1.1.1.130 = STRING: Eth2
Преобразование данных строки в Pandas dataframe
Первым элементом кортежа в выходных данных функции будут строки данных, соответствующие столбцам, которые мы определили. Этот формат можно преобразовать в Pandas фрейм данных, используя pd.DataFrame.from_records()
:
>>> row_data = [['1579051725', '.1.3.4.1.2.1.1.1.1.1.1', '128', 'STRING', 'AA1']]]
>>>
>>> column_names = ['TimeStamp', 'InterfaceNum', 'IndexNum',
... 'SyntaxName', 'Counter']
>>>
>>> pd.DataFrame.from_records(row_data, columns=column_names)
TimeStamp InterfaceNum IndexNum SyntaxName Counter
0 1579051725 .1.3.4.1.2.1.1.1.1.1.1 128 STRING AA1
>>>
Преобразовать данные столбца в Pandas фрейм данных
Функция также производит словарь в качестве второго элемента возвращаемого кортежа, содержащего те же данные, который также может создавать тот же кадр данных, используя pd.DataFrame.from_dict()
.
>>> col_data = {'TimeStamp': ['1579051725'],
... 'InterfaceNum': ['.1.3.4.1.2.1.1.1.1.1.1'],
... 'IndexNum': ['128'], 'SyntaxName': ['STRING'],
... 'Counter': ['AA1']}
>>>
>>> pd.DataFrame.from_dict(col_data)
TimeStamp InterfaceNum IndexNum SyntaxName Counter
0 1579051725 .1.3.4.1.2.1.1.1.1.1.1 128 STRING AA1
>>>
Пример словаря
Вот несколько примеров фильтрации данных файла, инициализация постоянного словаря. Затем выполняется фильтрация дополнительных данных и добавление их в постоянный словарь. Я думаю, что это также близко к тому, что описано в вопросе.
const_num = '.1.3.4.1.2.1.1.1.1.1.1'
const_syntax = 'STRING'
result_5 = get_dev_data(file, iface_num=const_num, syntax=const_syntax)
# Use the results of the first dict retrieved to initialize the master
# dictionary.
master_dict = result_5[1]
print(f"master_dict = {master_dict}")
result_6 = get_dev_data(file, counter='Eth2|AA1')
# Add more records to the master dictionary.
for k, v in result_6[1].items():
master_dict[k].extend(v)
print(f"master_dict = {master_dict}")
df = pandas.DataFrame.from_dict(master_dict)
print(f"\n{df}")
Вывод
master_dict = {'TimeStamp': ['1579051725', '1579051725', '1579051725'], 'InterfaceNum': ['.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1'], 'IndexNum': ['128', '129', '130'], 'SyntaxName': ['STRING', 'STRING', 'STRING'], 'Counter': ['AA1', 'Eth1', 'Eth2']}
master_dict = {'TimeStamp': ['1579051725', '1579051725', '1579051725', '1579051725', '1579051725'], 'InterfaceNum': ['.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1', '.1.3.4.1.2.1.1.1.1.1.1'], 'IndexNum': ['128', '129', '130', '128', '130'], 'SyntaxName': ['STRING', 'STRING', 'STRING', 'STRING', 'STRING'], 'Counter': ['AA1', 'Eth1', 'Eth2', 'AA1', 'Eth2']}
TimeStamp InterfaceNum IndexNum SyntaxName Counter
0 1579051725 .1.3.4.1.2.1.1.1.1.1.1 128 STRING AA1
1 1579051725 .1.3.4.1.2.1.1.1.1.1.1 129 STRING Eth1
2 1579051725 .1.3.4.1.2.1.1.1.1.1.1 130 STRING Eth2
3 1579051725 .1.3.4.1.2.1.1.1.1.1.1 128 STRING AA1
4 1579051725 .1.3.4.1.2.1.1.1.1.1.1 130 STRING Eth2
Если все столбцы данных словаря не нужны, ключи от него можно обойтись с помощью <dict>.pop(<key>)
. Или вы можете удалить столбцы из любого фрейма данных, созданного на основе данных.
Группировка в составных регулярных выражениях
Это выражение показывает выражение, которое вычисляется в функции, когда все его параметры имеют значения по умолчанию.
expr = r"""
TIMESTAMP:\s(\d+) # date - TimeStamp
| # ** OR **
((?:\.\d+)+) # num - InterfaceNum
\.(\d+)\s=\s # num2 - IndexNum
(\w+):\s # syntax - SyntaxName
(\w+) # counter - Counter
"""
В приведенном выше регулярном выражении есть два альтернативных оператора, разделенных оператором OR, |
. Эти альтернативы соответствуют либо строке данных временной метки, либо данным устройства. И в этих подвыражениях есть группировки для захвата определенных c фрагментов строковых данных. Группы соответствия создаются путем помещения круглых скобок (...)
вокруг подвыражения. Синтаксис для не группирующих скобок: (?:...)
.
. Независимо от того, какие альтернативные подвыражения совпадают, все равно будет одинаковое количество групп совпадений, возвращаемых за успешный вызов re.findall()
. Может быть, немного нелогично, но так оно и есть.
Однако эта функция облегчает написание кода для извлечения того, какие поля совпадения вы захватили, поскольку вы знаете позиции, в которых должны быть группы, независимо от сопоставленного подвыражения:
[<tstamp>, <num>, <num2>, <syntax>, <counter>]
# ^expr1^ ^.............expr2..............^
И так как у нас есть предсказуемое количество групп совпадений, независимо от того, какие подвыражения совпадают, это позволяет использовать циклический шаблон, который можно применять во многих сценариях ios. Проверяя, являются ли одиночные группы совпадений пустыми или нет, мы знаем, какую ветвь в l oop нужно использовать для обработки данных для любого подвыражения, получившего попадание.
if tstamp:
# First expression hit.
elif num:
# Second alt expression hit.
Когда выражение совпадает со строкой текста, который имеет временную метку, попадет первое подвыражение, и его группы будут заполнены.
>>> re.findall(expr, "TIMESTAMP: 1579051725 20100114-202845", re.VERBOSE)
[('1579051725', '', '', '', '')]
Здесь первая группировка из выражения заполнена, а остальные группы пусты. Другие группировки принадлежат другому подвыражению.
Теперь, когда выражение совпадает с первой строкой данных устройства, второе подвыражение получает удар, и его группы заполняются. Группы временных отметок не заполнены.
>>> re.findall(expr, ".1.3.4.1.2.1.1.1.1.1.1.128 = STRING: AA1", re.VERBOSE)
[('', '.1.3.4.1.2.1.1.1.1.1.1', '128', 'STRING', 'AA1')]
И, наконец, когда ни одно из подвыражений не совпадает, тогда все выражение не получает попадания. В этом случае мы получаем пустой список.
>>> re.findall(expr, "ifTb: name-nam-na", re.VERBOSE)
[]
>>>
Для контраста, вот выражение без подробного синтаксиса и документации:
expr = r"TIMESTAMP:\s(\d+)|((?:\.\d+)+)\.(\d+)\s=\s(\w+):\s(\w+)"