• Перейти на CTF площадку Игры Кодебай

    Выполняйте задания по кибербезопасности в формате CTF и получайте бесценный опыт. Ознакомиться с подробным описанием можно тут.

Статья Парсинг вывода команд операционной системы и немного о получении параметров сетевых адаптеров с помощью Python

В данной статье речь пойдет о парсинге. Не о том, когда получаются данные веб-сайта. Этот парсинг, в более правильном варианте, все же называется scrapping. А о том, который подразумевает под собой парсинг данных. В данном случае текстовых. Ну и немного о получении данных о сетевых адаптерах, установленных в системе.


000.jpg


Почему я решил написать данную статью? Дело в том, что не так давно я столкнулся с тем, что многие новички, в частности присутствующие, здесь, на форуме, не умеют парсить текстовые данные. Самый первый способ, с помощью которого они пытаются вытащить данные из километровой портянки, это использование регулярных выражений. И да, я не спорю, если уметь пользоваться регулярными выражениями, найти можно что угодно. Да и получить тот же самый e-mail или веб-адрес гораздо легче. Но иногда использование регулярных выражений попросту излишне. Давайте рассмотрим, как можно распарсить вывод команды «ipconfig /all», с помощью которой можно получить данные обо всех сетевых адаптерах, установленных в системе, а также их параметрах.

Не сказать, что данный вывод парсить одно удовольствие. Дело в том, что нам нужно предусмотреть один небольшой нюанс, а именно, исключить привязку к какому-либо естественному языку. Ведь вывод данной команды, а в частности название параметров, переведено на тот язык, который используется в системе. А потому привязаться к выводу на конкретном языке можно, но нежелательно, потому, что в будущем вам может понадобиться использовать тот же скрипт на другой системе и естественно, ничего не получиться без изменения самого скрипта.

Давайте посмотрим на сам вывод. Ниже представлен скриншот командной строки.

screenshot1.png

Это виртуальная машина. Я намеренно добавил в нее несколько сетевых адаптеров, для того чтобы немного усложнить парсинг. На первый взгляд зацепиться за какой-либо элемент в виде повторяющегося паттерна здесь не особо представляется возможным. Самое простое, что мы можем сделать в данном случае, это разделить вывод на строки с помощью split("\r\n").

Для начала импортируем в код необходимые библиотеки. В нашем случае, так как сторонними библиотеками не «из коробки» мы пользоваться не будем, блок импорта будет выглядеть следующим образом:

Python:
import json
import subprocess
from ipaddress import IPv4Address, IPv6Address
from pathlib import Path
from socket import socket, AF_INET, SOCK_DGRAM


Получение текста с маркерами для последующего разбиения на блоки и строки в них

Давайте создадим функцию, которую назовем all_adapters(). И теперь напишем код, который будет выполнять команду и делить вывод на строки.

Python:
def all_adapters() -> dict:
    network_adapters = dict()

    out = subprocess.check_output("ipconfig /all", shell=True).decode("cp866")
    text = ""

    for item in out.split("\r\n"):

Как видите, код продолжается дальше, так как на этом итерация не закончена. После того, как мы получим вывод команды в переменную out, создадим текстовую переменную text, в которую будем складывать полученные и обработанные строки. Так как нам не за что особо зацепиться, чтобы разбить полученные строки на блоки относящиеся к одному адаптеру, мы добавим в формируемый текст свои маркеры, по которым уже и будем выполнять разбиение.

Для начала проверим, не является ли строка после разбиения пустой. Если да, итерируемся по списку дальше. Если строка не пуста, проверяем, не содержит ли она в себе текст к которому можно привязаться, так как, думаю, что вывод этого текста на любом языке будет одинаковым – «Ethernet». Если данный текст присутствует в строке, добавляем к переменной text «~», а также содержимое текущей строки, у которой предварительно обрежем пустые значения, если они есть в начале и конце с помощью strip(). Ну и во всех остальных случаях к переменной text добавляем значение этой переменной, «||», который будет служить разделителем между строками и текущее содержимое строки списка.

Python:
    for item in out.split("\r\n"):
        if item == "":
            continue
        elif "Ethernet" in item:
            text = f"{text.strip()}~{item.strip()}"
        else:
            text = f"{text.strip()}||{item.strip()}"

Таким образом, мы получили текстовую переменную text, в которой содержаться как символы, по которым мы можем разбить данный текст на блоки, так и символы, по которым мы можем разбить текст в блоках на строки.


Разбиение текста на блоки и строки. Получение данных и формирование словаря из значений

Дальше будем работать с переменной текст, содержащей нужные нам параметры. Для начала создадим цикл, в котором будем итерироваться по тексту разбитому на блоки с помощью добавленного в текст маркера «~».

Создадим еще один цикл, в котором будем итерироваться уже внутри блока, который разобьем на словарь с помощью маркера «||». Так как не все элементы в полученном списке являются параметрами, нужно забрать из списка названия сетевых адаптеров, которые добавим в словарь и, уже в них, будем складывать параметры, получаемые из блока.

Проверяем, содержит ли текущий итерируемый элемент списка значение «Ethernet». Если да, создаем в общем словаре раздел с названием адаптера. Объявляем переменную keys и в нее присваиваем значение только что созданного словаря. Это необходимо для того, чтобы складывать получаемые далее параметры куда нужно. Вот код, где мы делаем проверку.

Python:
            if "Ethernet" in item:
                network_adapters[item.replace(":", "")] = dict()
                keys = network_adapters[item.replace(":", "")]
                continue

Если мы не присвоим значение только что созданного словаря в переменную, значение network_adapters[item.replace(":", "")] будем всегда меняться, так как item будет содержать в себе, хоть и в текущем блоке другое значение. Ну и с помощью continue переходим к следующей итерации.

Теперь нужно проверить, не содержит ли в себе параметр пустого значения. Делим элемент списка по « :» и забираем второй элемент. Если он пустой, то переходим к следующей итерации.

Python:
            try:
                if item.split(" :")[1].strip() == "":
                    continue
            except IndexError:
                continue

Так как не все сетевые адаптеры активны, а зачастую только один из них, это если брать в расчет не серверную машину, у неактивных адаптеров нет ip-адреса. А вот у активных он есть. Но, так как мы не можем привязаться к конкретному языку, чтобы выполнить проверку по названию параметра, нужно проверить, есть ли он. Более того, значение здесь выдается в не особо удобном для последующего использования виде: 10.0.2.15(Основной). То же самое и с IPv6 адресом. И вот этот вот основной нужно убрать, да еще и проверить, является ли данное значение адресом вообще. Для этого будем использовать функции IPv4Address, IPv6Address библиотеки ipaddress.

Получаем значение элемента. Делим его по « :». Забираем второй элемент, который дополнительно делим по открывающей скобке. И загоняем в функцию IPv4Address. Если элемент не является ipv4 адресом, функция выбросит исключение. Если нет, добавляем ключ и значение в словарь. То же самое проделываем и с адресом ipv6.

Для чего такие сложности? Дело в том, что некоторые значения могут иметь в своих названиях скобки. А значит просто так, не проверив адрес это или нет, мы их разделить не можем. А если разделим без проверки, получим только часть названия. Для примера, название сетевого адаптера: Intel(R) PRO/1000 MT Desktop Adapter #4.

Ну и после проверки ipv6 адреса, если он вызовет исключение, добавляем остальные значения, которые не относятся к адресам в словарь.

Python:
            try:
                IPv4Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                pass
            try:
                IPv6Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip()})

Python:
    for tx in text.split("~")[1:]:
        for item in tx.split("||"):
            if "Ethernet" in item:
                network_adapters[item.replace(":", "")] = dict()
                keys = network_adapters[item.replace(":", "")]
                continue
            try:
                if item.split(" :")[1].strip() == "":
                    continue
            except IndexError:
                continue
            try:
                IPv4Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                pass
            try:
                IPv6Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip()})

В общем-то, словарь на данном этапе уже будет готов. Но, на всякий случай, мало ли что пойдет не так, чтобы не потерять уже полученные данные, сохраним их в папку скрипта в json, после чего вернем словарь из функции.

Python:
    with open(Path.cwd() / 'ethernet_adapters.json', 'w', encoding='utf-8') as js:
        json.dump(network_adapters, js, indent=4, ensure_ascii=False)

    return network_adapters

Python:
def all_adapters():
    """
    Парсинг вывода команды ipconfig /all.
    Выполняем команду. Для того чтобы разделить вывод на блоки,
    в каждом из которых будет один адаптер, приводим строку к нужному
    виду, указав в качестве разделителя "~". Также на будущее делим все
    остальные строки с помощью "||".
    Затем итерируемся по созданному нами текстовому блоку. Делим его по
    разделителям для начала на блоки, в которых содержаться данные об
    одном адаптере. Затем, в каждом блоке делим содержимое на список из
    строк по "||". Затем итерируемся по полученному списку, забираем
    название адаптера. Создаем в словаре раздел с адаптером. Итерируемся
    дальше, пропускаем пустые строки, проверяем адреса ipv4 и ipv6, для
    того, чтобы убрать у них приставку (в русском варианте: Основной).
    Делим элементы списка и добавляем в словарь полученные значения.
    Затем записываем полученные данные в json для лучшей сохранности и
    возвращаем полученный словарь из функции.
    :return: словарь со всеми адаптерами в системе.
    """

    network_adapters = dict()

    out = subprocess.check_output("ipconfig /all", shell=True).decode("cp866")
    text = ""

    for item in out.split("\r\n"):
        if item == "":
            continue
        elif "Ethernet" in item:
            text = f"{text.strip()}~{item.strip()}"
        else:
            text = f"{text.strip()}||{item.strip()}"

    for tx in text.split("~")[1:]:
        for item in tx.split("||"):
            if "Ethernet" in item:
                network_adapters[item.replace(":", "")] = dict()
                keys = network_adapters[item.replace(":", "")]
                continue
            try:
                if item.split(" :")[1].strip() == "":
                    continue
            except IndexError:
                continue
            try:
                IPv4Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                pass
            try:
                IPv6Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip()})

    with open(Path.cwd() / 'ethernet_adapters.json', 'w', encoding='utf-8') as js:
        json.dump(network_adapters, js, indent=4, ensure_ascii=False)

    return network_adapters


Получение локального ipv4-адреса

Что ж, хорошо. Параметры адаптеров, которые есть в системе, мы получили. Но, мы до сих пор не знаем, какой из адаптеров является основным, то есть, используемым по умолчанию. Нужно получить какое-то уникальное значение, по которому можно определить, да, вот это вот и есть нужный адаптер, и забрать его значения. Одним из таких значений является локальный ip-адрес, который и будет принадлежать сетевому адаптеру, который используется по умолчанию. Для получения локального ipv4 адреса используем функцию со SO. Она достаточно хорошо делает свое дело.

Python:
def local_ipv4() -> str:
    """
    Получаем локальный IP-адрес с помощью установки
    соединения на адрес 10.255.255.255. В ответ получаем
    имя сокета, которое и является адресом.

    :return: возвращает локальный IP-адрес или адрес локальной петли
    в случае возникновения исключения.
    """
    ip = None
    st = socket(AF_INET, SOCK_DGRAM)
    try:
        st.connect(('10.255.255.255', 1))
        ip = st.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        st.close()
        return ip


Получение сетевого адаптера используемого в системе по умолчанию и его параметров

Теперь, когда у нас есть словарь с параметрами сетевых адаптеров и локальный ipv4 адрес, мы можем получить сетевой адаптер по умолчанию. Создадим для этого отдельную функцию, в которую передадим полученный из предыдущей функции словарь, default_network(network_adapters: dict, loc_ip: str), также в эту же функцию мы передадим полученный ipv4 адрес.

Создадим словарь, в который будем помещать значения из словаря со всеми адаптерами по одному. То есть, запускаем цикл и итерируемся по ключам словаря. После того, как мы составим словарь из значений одного из ключей, мы будем итерироваться по созданному словарю для проверки, есть ли в нем искомый ipv4 адрес. И если есть, возвращаем уже готовый словарь с параметрами адаптера из функции. Если же нет, продолжаем итерацию.

Python:
def default_network(network_adapters: dict, loc_ip: str) -> dict:
    """
    Получаем сетевой адаптер используемый по-умолчанию.
    Итерируемся по ранее составленному словарю, составляем промежуточный
    словарь, итерируемся по нему и проверяем, есть ли в нем значение,
    которое соответствует ipv4-адресу по-умолчанию. Если да, возвращаем
    словарь из функции. Если нет, обнуляем словарь и двигаемся дальше.
    :param network_adapters: словарь со всеми сетевыми адаптерами в системе.
    :param loc_ip: локальный ipv4 адрес.
    :return: словарь с сетевым адаптером по-умолчанию.
    """
    default_adapter = dict()
    for network in network_adapters:
        default_adapter[network] = dict()
        for adapter in network_adapters[network]:
            default_adapter[network].update({adapter: network_adapters[network][adapter]})
        for loc in default_adapter[network]:
            if loc_ip == default_adapter[network][loc]:
                with open(Path.cwd() / 'ethernet_default.json', 'w', encoding='utf-8') as js:
                    json.dump(default_adapter, js, indent=4, ensure_ascii=False)
                return default_adapter
        default_adapter.clear()


Запуск функций созданных ранее

Ну и напоследок создадим функцию main(), в которой будем запускать созданные ранее функции, а также проверять полученные значения словаря. Если словарь с сетевым адаптером по умолчанию пустой, а такое может быть в случае отсутствия интернета, выводим на печать общий словарь. Если же все в порядке, выводим на печать значения обоих словарей.

Python:
def main():
    adapters = all_adapters()
    ipv4 = local_ipv4()
    default = default_network(adapters, ipv4)
    if default is None:
        if adapters is not None:
            print(adapters)
    else:
        print(adapters)
        print(default)


if __name__ == "__main__":
    main()

Python:
"""
Скрипт для получения данных о сетевых адаптерах
используемых в системе, а также их параметров, которые
доступны при выводе команды "ipconfig /all".
"""

import json
import subprocess
from ipaddress import IPv4Address, IPv6Address
from pathlib import Path
from socket import socket, AF_INET, SOCK_DGRAM


def all_adapters():
    """
    Парсинг вывода команды ipconfig /all.
    Выполняем команду. Для того чтобы разделить вывод на блоки,
    в каждом из которых будет один адаптер, приводим строку к нужному
    виду, указав в качестве разделителя "~". Также на будущее делим все
    остальные строки с помощью "||".
    Затем итерируемся по созданному нами текстовому блоку. Делим его по
    разделителям для начала на блоки, в которых содержаться данные об
    одном адаптере. Затем, в каждом блоке делим содержимое на список из
    строк по "||". Затем итерируемся по полученному списку, забираем
    название адаптера. Создаем в словаре раздел с адаптером. Итерируемся
    дальше, пропускаем пустые строки, проверяем адреса ipv4 и ipv6, для
    того, чтобы убрать у них приставку (в русском варианте: Основной).
    Делим элементы списка и добавляем в словарь полученные значения.
    Затем записываем полученные данные в json для лучшей сохранности и
    возвращаем полученный словарь из функции.
    :return: словарь со всеми адаптерами в системе.
    """

    network_adapters = dict()

    out = subprocess.check_output("ipconfig /all", shell=True).decode("cp866")
    text = ""

    for item in out.split("\r\n"):
        if item == "":
            continue
        elif "Ethernet" in item:
            text = f"{text.strip()}~{item.strip()}"
        else:
            text = f"{text.strip()}||{item.strip()}"

    for tx in text.split("~")[1:]:
        for item in tx.split("||"):
            if "Ethernet" in item:
                network_adapters[item.replace(":", "")] = dict()
                keys = network_adapters[item.replace(":", "")]
                continue
            try:
                if item.split(" :")[1].strip() == "":
                    continue
            except IndexError:
                continue
            try:
                IPv4Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                pass
            try:
                IPv6Address(item.split(" :")[1].strip().split("(")[0])
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip().split("(")[0]})
                continue
            except Exception:
                keys.update({item.split(" :")[0].replace(".", "").strip(): item.split(" :")[1].strip()})

    with open(Path.cwd() / 'ethernet_adapters.json', 'w', encoding='utf-8') as js:
        json.dump(network_adapters, js, indent=4, ensure_ascii=False)

    return network_adapters


def local_ipv4() -> str:
    """
    Получаем локальный IP-адрес с помощью установки
    соединения на адрес 10.255.255.255. В ответ получаем
    имя сокета, которое и является адресом.

    :return: возвращает локальный IP-адрес или адрес локальной петли
    в случае возникновения исключения.
    """
    ip = None
    st = socket(AF_INET, SOCK_DGRAM)
    try:
        st.connect(('10.255.255.255', 1))
        ip = st.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        st.close()
        return ip


def default_network(network_adapters: dict, loc_ip: str) -> dict:
    """
    Получаем сетевой адаптер используемый по-умолчанию.
    Итерируемся по ранее составленному словарю, составляем промежуточный
    словарь, итерируемся по нему и проверяем, есть ли в нем значение,
    которое соответствует ipv4-адресу по-умолчанию. Если да, возвращаем
    словарь из функции. Если нет, обнуляем словарь и двигаемся дальше.
    :param network_adapters: словарь со всеми сетевыми адаптерами в системе.
    :param loc_ip: локальный ipv4 адрес.
    :return: словарь с сетевым адаптером по-умолчанию.
    """
    default_adapter = dict()
    for network in network_adapters:
        default_adapter[network] = dict()
        for adapter in network_adapters[network]:
            default_adapter[network].update({adapter: network_adapters[network][adapter]})
        for loc in default_adapter[network]:
            if loc_ip == default_adapter[network][loc]:
                with open(Path.cwd() / 'ethernet_default.json', 'w', encoding='utf-8') as js:
                    json.dump(default_adapter, js, indent=4, ensure_ascii=False)
                return default_adapter
        default_adapter.clear()


def main():
    adapters = all_adapters()
    ipv4 = local_ipv4()
    default = default_network(adapters, ipv4)
    if default is None:
        if adapters is not None:
            print(adapters)
    else:
        print(adapters)
        print(default)


if __name__ == "__main__":
    main()

А это полученные json.

screenshot2.png

screenshot3.png


А на этом, пожалуй, все.

Надеюсь, что с поставленной задачей, а именно показать как парсить вывод команд операционной системы, я справился. Получение параметров сетевых адаптеров, это уже второстепенная задача.


Спасибо за внимание. Надеюсь, информация будет вам полезна
 

Вложения

  • clear.zip
    2,2 КБ · Просмотры: 44
Мы в соцсетях: