
У сучасному мережевому адмініструванні знання Python стає незамінним інструментом, а вміння працювати з файлами — критично важливим навиком. Збереження конфігурацій, обробка логів, автоматизація обміну даними — усе це неможливо без грамотної взаємодії зі структурованими та неструктурованими файлами. У цій статті ви дізнаєтеся, як ефективно використовувати можливості мови для роботи з файлами у щоденній практиці мережевого спеціаліста. Цей матеріал створено спеціально для тих, хто прагне поєднати гнучкість Python із реальними задачами автоматизації в інфраструктурі.
У практичній роботі, щоб повною мірою застосовувати знання, отримані в попередніх розділах, необхідно добре розумітися на роботі з файлами. Під час взаємодії з мережевим обладнанням, а також у багатьох інших випадках, доводиться мати справу з різними типами файлів. Наприклад, конфігураційні файли можуть бути простими текстовими документами без складної структури. Їх обробка є однією з тем цього розділу. Окремо існують шаблони конфігурацій, для яких зазвичай використовується спеціальний формат і розглядається застосування шаблонізатора Jinja2 для їх створення.
Ще один поширений тип — файли, що містять параметри підключення; вони часто мають структуру у вигляді YAML, JSON або CSV, і методи їх обробки описуються у відповідному розділі про серіалізацію. Існує також необхідність у роботі з іншими скриптами Python, де важливо вміти грамотно взаємодіяти з модулями. У даному розділі головний акцент зроблено на взаємодії з простими текстовими файлами, такими як конфігурації пристроїв Cisco. Основні дії, які детально пояснюються, охоплюють відкриття, читання, запис і закриття файлів. Тут подано лише базову інформацію, необхідну для впевненої роботи з файлами в Python, а для поглибленого вивчення завжди можна звернутися до офіційної документації.
Для початку роботи з файлом його треба відкрити. Для відкриття файлів найчастіше використовується функція open
:
file = open('file_name.txt', 'r')
У функції open():
Функція open()
у Python дозволяє відкривати файли для подальшої роботи з ними. Першим аргументом передається ім’я або шлях до файлу (це може бути як абсолютний, так і відносний шлях), другим — режим, у якому файл буде відкрито. Наприклад, символ 'r'
означає відкриття файлу тільки для читання, і це є режимом за замовчуванням. Якщо потрібно не лише читати, а й змінювати вміст, використовується режим 'r+'
.
Для створення або повного перезапису файлу використовується режим 'w'
. У цьому випадку, якщо файл вже існує, його вміст повністю стирається, а якщо не існує — створюється новий. Аналогічно діє режим 'w+'
, але з можливістю читання. Якщо мета — додати нові дані в кінець наявного файлу, не зачіпаючи вже існуючу інформацію, застосовується режим 'a'
. Для читання і дописування одночасно використовують 'a+'
.
Після відкриття файлу з допомогою open()
створюється файловий об’єкт, до якого можна застосовувати методи читання, запису чи закриття в залежності від обраного режиму.
r – read;
a – append;
w – write.
У Python є кілька методів читання файлу:
read
– Зчитує вміст файлу в рядок
readline
– зчитує файл рядково
readlines
– зчитує рядки файлу та створює список із рядків
Подивимося як зчитувати вміст файлів на прикладі файлу r1.txt:
! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
read
Метод read
– зчитує весь файл в один рядок.
Приклад використання методу read
:
In [1]: f = open('r1.txt') In [2]: f.read() Out[2]: '!\nservice timestamps debug datetime msec localtime show-timezone year\nservice timestamps log datetime msec localtime show-timezone year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain lookup\n!\nip ssh version 2\n!\n' In [3]: f.read() Out[3]: ''
При повторному читанні файлу в 3 рядку, відображається порожній рядок. Так відбувається через те, що при викликі методу зчитується read
весь файл.seek.
readline
Файл можна вважати за допомогою методу readline
:
In [4]: f = open('r1.txt') In [5]: f.readline() Out[5]: '!\n' In [6]: f.readline() Out[6]: 'service timestamps debug datetime msec localtime show-timezone year\n'
Але найчастіше простіше пройтися по об’єкту file в циклі, не використовуючи методи read...
In [7]: f = open('r1.txt') In [8]: for line in f: ...: print(line) ...: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
readlines
Ще один корисний метод – readlines
. Він зчитує рядки файлу до списку:
In [9]: f = open('r1.txt') In [10]: f.readlines() Out[10]: ['!\n', 'service timestamps debug datetime msec localtime show-timezone year\n', 'service timestamps log datetime msec localtime show-timezone year\n', 'service password-encryption\n', 'service sequence-numbers\n', '!\n', 'no ip domain lookup\n', '!\n', 'ip ssh version 2\n', '!\n']
Якщо потрібно отримати рядки файлу, але без перекладу рядка в кінці, можна скористатися методом split
і як роздільник вказати символ \n
:
In [11]: f = open('r1.txt') In [12]: f.read().split('\n') Out[12]: ['!', 'service timestamps debug datetime msec localtime show-timezone year', 'service timestamps log datetime msec localtime show-timezone year', 'service password-encryption', 'service sequence-numbers', '!', 'no ip domain lookup', '!', 'ip ssh version 2', '!', '']
Зауважте, що останній елемент списку – порожній рядок.
Якщо перед виконанням split
, скористатися методом rstrip
, список буде без порожнього рядка наприкінці:
In [13]: f = open('r1.txt') In [14]: f.read().rstrip().split('\n') Out[14]: ['!', 'service timestamps debug datetime msec localtime show-timezone year', 'service timestamps log datetime msec localtime show-timezone year', 'service password-encryption', 'service sequence-numbers', '!', 'no ip domain lookup', '!', 'ip ssh version 2', '!']
seek
Досі файл щоразу доводилося відкривати заново, щоб знову його рахувати. Так відбувається через те, що після методів читання курсор знаходиться в кінці файлу. І повторне читання повертає порожній рядок. Щоб ще раз рахувати інформацію з файлу, потрібно скористатися методом seek
, який переміщує курсор у потрібне положення.
Приклад відкриття файлу та зчитування вмісту:
In [15]: f = open('r1.txt') In [16]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Якщо ще раз викликати метод read
, повертається порожній рядок:
In [17]: print(f.read())
Але за допомогою методу seek
можна перейти на початок файлу (0 означає початок файлу):
In [18]: f.seek(0)
Після того як за допомогою seek
курсора було переведено на початок файлу, можна знову зчитувати вміст:
In [19]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
При записі дуже важливо визначитися з режимом відкриття файлу, щоб випадково його не видалити:
w
– Відкрити файл для запису. Якщо файл існує, його вміст видаляється
a
– Відкрити файл для доповнення запису. Дані додаються до кінця файлу
При цьому обидва режими створюють файл, якщо він не існує.
Для запису у файл використовуються такі методи:
write
– записати у файл один рядок
writelines
– дозволяє передавати як аргумент список рядків
write
Метод write
чекає рядок для запису.
Наприклад, візьмемо список рядків із конфігурацією:
In [1]: cfg_lines = ['!', ...: 'service timestamps debug datetime msec localtime show-timezone year', ...: 'service timestamps log datetime msec localtime show-timezone year', ...: 'service password-encryption', ...: 'service sequence-numbers', ...: '!', ...: 'no ip domain lookup', ...: '!', ...: 'ip ssh version 2', ...: '!']
Відкриття файлу r2.txt в режимі запису:
In [2]: f = open('r2.txt', 'w')
Перетворимо список команд в один великий рядок за допомогою join
:
In [3]: cfg_lines_as_string = '\n'.join(cfg_lines) In [4]: cfg_lines_as_string Out[4]: '!\nservice timestamps debug datetime msec localtime show-timezone year\nservice timestamps log datetime msec localtime show-timezone year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain lookup\n!\nip ssh version 2\n!'
Запис рядка у файл:
In [5]: f.write(cfg_lines_as_string)
Аналогічно можна додати рядок вручну:
In [6]: f.write('\nhostname r2')
Після завершення роботи з файлом його необхідно закрити:
In [7]: f.close()
Оскільки ipython підтримує команду cat, можна легко переглянути вміст файлу:
In [8]: cat r2.txt ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 ! hostname r2
writelines
Метод writelines
чекає на список рядків, як аргумент.
Запис списку рядків cfg_lines у файл:
In [1]: cfg_lines = ['!', ...: 'service timestamps debug datetime msec localtime show-timezone year', ...: 'service timestamps log datetime msec localtime show-timezone year', ...: 'service password-encryption', ...: 'service sequence-numbers', ...: '!', ...: 'no ip domain lookup', ...: '!', ...: 'ip ssh version 2', ...: '!'] In [9]: f = open('r2.txt', 'w') In [10]: f.writelines(cfg_lines) In [11]: f.close() In [12]: cat r2.txt !service timestamps debug datetime msec localtime show-timezone yearservice timestamps log datetime msec localtime show-timezone yearservice password-encryptionservice sequence-numbers!no ip domain lookup!ip ssh version 2!
В результаті всі рядки зі списку записалися в один рядок файлу, тому що в кінці рядків не було символу \n
.
Додати переклад рядка можна по-різному. Наприклад, можна просто обробити список у циклі:
In [13]: cfg_lines2 = [] In [14]: for line in cfg_lines: ....: cfg_lines2.append(line + '\n') ....: In [15]: cfg_lines2 Out[15]: ['!\n', 'service timestamps debug datetime msec localtime show-timezone year\n', 'service timestamps log datetime msec localtime show-timezone year\n', 'service password-encryption\n', 'service sequence-numbers\n', '!\n', 'no ip domain lookup\n', '!\n', 'ip ssh version 2\n',
Якщо записати отриманий список назад у файл, він уже міститиме переклади рядків:
In [18]: f = open('r2.txt', 'w') In [19]: f.writelines(cfg_lines2) In [20]: f.close() In [21]: cat r2.txt ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
У реальному житті для закриття файлів найчастіше використовується конструкція with
. Її набагато зручніше використовувати, ніж закривати файл явно. Але, оскільки в житті можна зустріти і метод close
, у цьому розділі розглядається як його використати.
Після завершення роботи з файлом його потрібно закрити. У деяких випадках Python може самостійно закрити файл. Але краще не розраховувати і закривати файл явно.
close
Метод close зустрічався у розділі запис файлів. Там він був потрібний для того, щоб вміст файлу був записаний на диск.
Для цього в Python є окремий метод flush
. Але так як у прикладі із записом файлів, не потрібно було виконувати жодних операцій, файл можна було закрити.
Відкриємо файл r1.txt:
In [1]: f = open('r1.txt', 'r')
Тепер можна вважати вміст:
In [2]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Об’єкт file має спеціальний атрибут closed
, який дозволяє перевірити, закритий файл чи ні. Якщо файл відкритий, він повертає False
:
In [3]: f.closed Out[3]: False
Тепер закриваємо файл і знову перевіряємо closed
:
In [4]: f.close() In [5]: f.closed Out[5]: True
Якщо спробувати прочитати файл, виникне виняток:
In [6]: print(f.read()) ------------------------------------------------------------------ ValueError Traceback (most recent call last) <ipython-input-53-2c962247edc5> in <module>() ----> 1 print(f.read()) ValueError: I/O operation on closed file
Конструкція з називається менеджер контексту. У Python існує більш зручний спосіб роботи з файлами, ніж ті, які використовувалися досі – конструкція with
:
In [1]: with open('r1.txt', 'r') as f: ....: for line in f: ....: print(line) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Крім того, конструкція with
гарантує закриття файлу автоматично.
Зверніть увагу на те, як зчитуються рядки файлу:
for line in f: print(line)
Коли з файлом потрібно працювати рядково, краще використати такий варіант.
У попередньому висновку між рядками файлу були зайві порожні рядки, так як print додає ще один переклад рядка.
Щоб позбавитися цього, можна використовувати метод rstrip
:
In [2]: with open('r1.txt', 'r') as f: ....: for line in f: ....: print(line.rstrip()) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 ! In [3]: f.closed Out[3]: True
І звичайно ж, з конструкцією with
можна використовувати не тільки такий рядковий варіант зчитування, всі методи, що розглядалися до цього, також працюють:
In [4]: with open('r1.txt', 'r') as f: ....: print(f.read()) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Іноді потрібно працювати одночасно із двома файлами. Наприклад, треба записати деякі рядки з одного файлу, до іншого.
У такому випадку, у блоці with можна відкривати два файли таким чином:
In [5]: with open('r1.txt') as src, open('result.txt', 'w') as dest: ...: for line in src: ...: if line.startswith('service'): ...: dest.write(line) ...: In [6]: cat result.txt service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers
Це рівнозначно таким двом блокам with:
In [7]: with open('r1.txt') as src: ...: with open('result.txt', 'w') as dest: ...: for line in src: ...: if line.startswith('service'): ...: dest.write(line) ...:
У цьому підрозділі розглядається робота з файлами та поєднуються теми: файли, цикли та умови.
При обробці виведення команд чи конфігурації часто потрібно буде записати підсумкові дані у словник. Не завжди очевидно, як обробляти виведення команд і як загалом підходити до розбору виведення на частини. У цьому підрозділі розглядаються кілька прикладів, із зростаючим рівнем складності.
У цьому прикладі розбиратиметься висновок команди sh ip int br. З виведення команди нам треба отримати відповідність ім’я інтерфейсу – IP-адресу. Тобто ім’я інтерфейсу – це ключ словника, а IP-адреса – значення. При цьому, відповідність треба робити тільки для тих інтерфейсів, у яких призначено IP-адресу.
Приклад виведення команди sh ip int br (файл sh_ip_int_br.txt):
R1#show ip interface brief Interface IP-Address OK? Method Status Protocol FastEthernet0/0 15.0.15.1 YES manual up up FastEthernet0/1 10.0.12.1 YES manual up up FastEthernet0/2 10.0.13.1 YES manual up up FastEthernet0/3 unassigned YES unset up down Loopback0 10.1.1.1 YES manual up up Loopback100 100.0.0.1 YES manual up up
Файл working_with_dict_example_1.py:
result = {} with open('sh_ip_int_br.txt') as f: for line in f: line_list = line.split() if line_list and line_list[1][0].isdigit(): interface = line_list[0] address = line_list[1] result[interface] = address print(result)
Команда sh ip int br відображає виведення стовпцями. Отже, потрібні поля знаходяться в одному рядку. Скрипт обробляє висновок рядково і кожен рядок розбиває методом split.
Отриманий список містить стовпці виведення. Так як з усього висновку потрібні тільки інтерфейси, на яких налаштована IP-адреса, виконується перевірка першого символу другого стовпця: якщо перший символ число, значить на інтерфейсі призначена адреса і цей рядок треба обробляти.
Так як для кожного рядка є пара ключ і значення, вони надаються в словник: .result[interface] = address
Результатом виконання скрипта буде такий словник (тут він розбитий на пари ключ-значення для зручності, у реальному висновку скрипта словник відображатиметься в один рядок):
{'FastEthernet0/0': '15.0.15.1', 'FastEthernet0/1': '10.0.12.1', 'FastEthernet0/2': '10.0.13.1', 'Loopback0': '10.1.1.1', 'Loopback100': '100.0.0.1'}
Дуже часто виведення команд виглядає таким чином, що ключ та значення знаходяться у різних рядках. І треба придумати, яким чином обробляти висновок, щоб отримати потрібну відповідність.
Наприклад, з виведення команди треба отримати відповідність ім’я інтерфейсу – MTU (файл sh_ip_interface.txt):sh ip interface:
Ethernet0/0 is up, line protocol is up Internet address is 192.168.100.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/1 is up, line protocol is up Internet address is 192.168.200.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/2 is up, line protocol is up Internet address is 19.1.1.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ...
Ім’я інтерфейсу знаходиться у рядку виду , а MTU у рядку виду .Ethernet0/0 is up, line protocol is up
MTU is 1500 bytes
Наприклад, спробуємо запам’ятовувати щоразу інтерфейс та виводити його значення, коли зустрічається MTU, разом із значенням MTU:
In [2]: with open('sh_ip_interface.txt') as f: ...: for line in f: ...: if 'line protocol' in line: ...: interface = line.split()[0] ...: elif 'MTU is' in line: ...: mtu = line.split()[-2] ...: print('{:15}{}'.format(interface, mtu)) ...: Ethernet0/0 1500 Ethernet0/1 1500 Ethernet0/2 1500 Ethernet0/3 1500 Loopback0 1514
Висновок організований таким чином, що спочатку йде рядок з інтерфейсом, а потім через кілька рядків – рядок з MTU. Якщо запам’ятовувати ім’я інтерфейсу кожного разу, коли воно зустрічається, то на момент, коли зустрінеться рядок з MTU, останній запам’ятований інтерфейс – це той, до якого належить MTU.
Тепер, якщо необхідно створити словник з відповідністю інтерфейсу – MTU, достатньо записати значення на момент, коли був знайдений MTU.
Файл working_with_dict_example_2.py:
result = {} with open('sh_ip_interface.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] elif 'MTU is' in line: mtu = line.split()[-2] result[interface] = mtu print(result)
Результатом виконання скрипта буде такий словник (тут він розбитий на пари ключ-значення для зручності, у реальному висновку скрипта словник відображатиметься в один рядок):
{'Ethernet0/0': '1500', 'Ethernet0/1': '1500', 'Ethernet0/2': '1500', 'Ethernet0/3': '1500', 'Loopback0': '1514'}
Цей прийом буде досить часто корисним, оскільки виведення команд, загалом, організовано дуже схожим чином.
Якщо з виведення команди треба отримати кілька параметрів, дуже зручно використовувати словник із вкладеним словником.
Наприклад, з висновку `sh ip interface` потрібно отримати два параметри: IP-адресу та MTU. Для початку, виведення інформації:
Ethernet0/0 is up, line protocol is up Internet address is 192.168.100.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/1 is up, line protocol is up Internet address is 192.168.200.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/2 is up, line protocol is up Internet address is 19.1.1.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ...
На першому етапі кожне значення запам’ятовується в змінну, а потім виводяться всі три значення. Значення виводяться, коли зустрівся рядок з MTU, тому що він йде останнім:
In [2]: with open('sh_ip_interface.txt') as f: ...: for line in f: ...: if 'line protocol' in line: ...: interface = line.split()[0] ...: elif 'Internet address' in line: ...: ip_address = line.split()[-1] ...: elif 'MTU' in line: ...: mtu = line.split()[-2] ...: print('{:15}{:17}{}'.format(interface, ip_address, mtu)) ...: Ethernet0/0 192.168.100.1/24 1500 Ethernet0/1 192.168.200.1/24 1500 Ethernet0/2 19.1.1.1/24 1500 Ethernet0/3 192.168.230.1/24 1500 Loopback0 4.4.4.4/32 1514
Тут використовується такий самий прийом, як у попередньому прикладі, але додається ще одна вкладеність словника:
result = {} with open('sh_ip_interface.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] result[interface] = {} elif 'Internet address' in line: ip_address = line.split()[-1] result[interface]['ip'] = ip_address elif 'MTU' in line: mtu = line.split()[-2] result[interface]['mtu'] = mtu print(result)
Щоразу, коли зустрічається інтерфейс, у словнику ‘result’ створюється ключ з ім’ям інтерфейсу, якому відповідає порожній словник. Ця заготівля потрібна для того, щоб на момент, коли зустрінеться IP-адреса або MTU, можна було записати параметр у вкладений словник відповідного інтерфейсу.
Результатом виконання скрипта буде такий словник (тут він розбитий на пари ключ-значення для зручності, у реальному висновку скрипта словник відображатиметься в один рядок):
{'Ethernet0/0': {'ip': '192.168.100.1/24', 'mtu': '1500'}, 'Ethernet0/1': {'ip': '192.168.200.1/24', 'mtu': '1500'}, 'Ethernet0/2': {'ip': '19.1.1.1/24', 'mtu': '1500'}, 'Ethernet0/3': {'ip': '192.168.230.1/24', 'mtu': '1500'}, 'Loopback0': {'ip': '4.4.4.4/32', 'mtu': '1514'}}
Іноді, у висновку траплятимуться секції з порожніми значеннями. Наприклад, у випадку висновку , можуть траплятися інтерфейси, які виглядають так:`sh ip interface`.
Ethernet0/1 is up, line protocol is up Internet protocol processing disabled Ethernet0/2 is administratively down, line protocol is down Internet protocol processing disabled Ethernet0/3 is administratively down, line protocol is down Internet protocol processing disabled
Відповідно, тут немає MTU або IP-адреси.
І якщо виконати попередній скрипт для файлу з такими інтерфейсами, результат буде таким (висновок для файлу sh_ip_interface2.txt):
{'Ethernet0/0': {'ip': '192.168.100.2/24', 'mtu': '1500'}, 'Ethernet0/1': {}, 'Ethernet0/2': {}, 'Ethernet0/3': {}, 'Loopback0': {'ip': '2.2.2.2/32', 'mtu': '1514'}}
Якщо необхідно додавати інтерфейси до словника лише, коли на інтерфейсі призначено IP-адресу, треба перенести створення ключа з ім’ям інтерфейсу на момент, коли зустрічається рядок з IP-адресою (файл working_with_dict_example_4.py):
result = {} with open('sh_ip_interface2.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] elif 'Internet address' in line: ip_address = line.split()[-1] result[interface] = {} result[interface]['ip'] = ip_address elif 'MTU' in line: mtu = line.split()[-2] result[interface]['mtu'] = mtu print(result)
У цьому випадку результатом буде такий словник:
{'Ethernet0/0': {'ip': '192.168.100.2/24', 'mtu': '1500'}, 'Loopback0': {'ip': '2.2.2.2/32', 'mtu': '1514'}}
Розділ дає базові, але фундаментальні знання для впевненої роботи з текстовими файлами в Python. Через практичні приклади пояснюється, як зчитувати, записувати й обробляти конфігураційні дані з реальних мережевих сценаріїв, що є необхідним для ефективної автоматизації та аналізу мереж.