Недавно я писал скрипт на языке «PowerShell», который на первом этапе извлекает текст из текстового файла в формате JSON и превращает его в объект. С объектом, очевидно, проще работать, чем с чистым текстом, хоть и в формате JSON. В языке «PowerShell» это легко сделать, потому что в нем существует удобный командлет ConvertFrom-Json, предназначенный для этого.

Наличие этого командлета в языке заставило меня думать, что и для случая с текстовым файлом, содержащим код на языке HTML, в «PowerShell» тоже найдется какой-нибудь подобный командлет. Поэтому я захотел попробовать написать небольшой скрипт для анализа файла с кодом на языке HTML. Однако, моё предположение оказалось неверным. В языке «PowerShell» есть командлет ConvertTo-Html, но нет командлета ConvertFrom-Html. То есть можно легко выгрузить какой-нибудь отчет, автоматически преобразовав его в HTML-страницу, но для преобразования имеющегося текстового файла с кодом на языке HTML в объект придется что-то придумывать.

Подробнее о том, что мне нужно

Как известно, код на языке HTML имеет древовидную структуру. Проблема в том, что изначально для анализа имеется простой текстовый файл с кодом на языке HTML. Программист не может сразу запустить перебор ветвей дерева HTML-документа в цикле, чтобы найти нужные HTML-элементы. Сначала в тексте следует обнаружить и выделить отдельные ветки дерева HTML-документа, то есть HTML-элементы. Для этого в тексте нужно найти HTML-теги, обозначающие границы HTML-элементов, а затем разложить отдельные HTML-элементы в отдельные переменные. Отдельными переменными могут быть элементы массива или, что еще лучше, свойства объекта. В итоге должен быть получен объект, содержащий дерево HTML-документа.

Поиск и выделение из текста отдельных веток дерева HTML-документа называют по-научному «лексическим анализом», а помещение выделенных отдельных веток из линейного текста в древовидную структуру (в нашем случае — в объект) — «синтаксическим анализом». При автоматизации этих операций лексический и синтаксический анализ исходного текста выполняет одна и та же программа, которую часто называют «парсером» (калька с английского слова «parser»).

То есть я хочу получить (на первом этапе) HTML-парсер. На входе HTML-парсер получает текстовый файл с кодом на языке HTML в кодировке UTF-8 (без метки BOM). Результатом работы HTML-парсера является объект, содержащий дерево HTML-документа. Такой объект еще называют «объектной моделью HTML-документа» (по-английски «Document Object Model» или сокращенно «DOM»).

Если вы когда-нибудь пробовали написать даже какой-нибудь простой парсер, то можете понять, что это непростая задача. А тем более это непросто для современного кода на языке HTML. Это не та задача, за которую можно взяться в одиночку. Таким образом, очевидно, что мне нужно уже готовое решение, созданное другими людьми, то есть какая-то библиотека, предназначенная для этой задачи. Хорошо, что такие библиотеки существуют и их несложно найти. Сложнее выбрать из имеющихся библиотек лучшее решение.

Специфика. Тут следует уточнить, что я пишу скрипт для работы в программах-оболочках «Windows PowerShell» версии 5.1 и «PowerShell» версии 7 в операционной системе «Windows 10». Поэтому файл со скриптом должен быть в кодировке UTF-8 с меткой BOM, если предполагается, что скрипт будет выдавать в консоль сообщения на русском языке.

Тестовый файл с кодом на языке HTML я назвал «file.html». В этот файл я записал следующий текст в кодировке UTF-8 (без метки BOM):

<html>
    <head>
        <title>Тестовая страница</title>
    </head>
    <body>
        Текст.
    </body>
</html>

Я не буду тут описывать подробно получение текста из файла в переменную со всеми нужными проверками. Само получение выполняется так (переменная $file содержит путь к нужному файлу):

$html = get-content $file -Encoding utf8

Таким образом, у нас имеется переменная $html, содержащая текст из указанного файла с указанным кодом на языке HTML.

Устаревшие методы

Сначала я приведу код двух способов, а потом дам некоторые пояснения.

Способ 1.

$dom = New-Object -ComObject "HTMLFile"
$enc = [System.Text.Encoding]::Unicode   # UTF-16LE
$dom.write($enc.GetBytes($html))

Способ 2.

Add-Type -AssemblyName System.Windows.Forms
$webBrowser = New-Object -TypeName "System.Windows.Forms.WebBrowser"
$webBrowser.DocumentText = ""            # Здесь можно присвоить любой текст
$webBrowser.Document.write($html)
$dom = $webBrowser.Document

В первом способе нужный объект создаем с помощью класса IHTMLDocument2, обращаясь к нему с помощью COM. Этот класс базируется на браузерном движке «Trident» (он же — «MSHTML») компании «Microsoft». Он появился в браузере «Internet Explorer» в 1997 году и жил в нем до последней версии этого браузера (версия 11), вышедшей в 2013 году.

Во втором способе для получения нужного объекта сначала создаем объект класса «WebBrowser». Этот класс представляет один из элементов интерфейса («контрол», по-английски «control»), входящих в библиотеку «Windows Forms» (с помощью этой библиотеки можно сконструировать оконный интерфейс для своей программы). После некоторых манипуляций из свойства «Document» объекта класса «WebBrowser» получаем объект класса «HTMLDocument».

На самом деле, класс «HTMLDocument» является оберткой вокруг класса «IHTMLDocument2», использовавшегося в первом способе. То есть хоть классы «HTMLDocument» и «IHTMLDocument2» имеют некоторые отличия, но в целом они дают примерно одинаковые возможности. Можно даже сказать, что это один и тот же способ.

Если вы захотите использовать один из этих способов, следует обратить внимание на то, что метод «write» в первом и втором способе принимает параметр разного типа. В первом способе в этот метод следует передать байтовый массив (тип [byte[]]) с текстом в кодировке UTF-16LE. Поэтому было сделано перекодирование исходного текста из кодировки UTF-8 (без BOM) в кодировку UTF-16LE. Причем передается именно массив байтов, а не обычная строка типа System.String! Во втором способе в этот метод передается обычная строка типа System.String.

Во втором способе используется присвоение текста свойству «DocumentText» объекта класса «WebBrowser». Особого смысла в этом присвоении нет, поэтому присваиваемый текст может быть любым. Эта строка в коде нужна потому, что в результате этого присвоения в свойстве «Document» объекта класса «WebBrowser» создается объект класса «HTMLDocument», с которым после этого можно работать. Мне не удалось создать самостоятельный объект класса «HTMLDocument» вне контекста объекта класса «WebBrowser». Дело в том, что контролы библиотеки «Windows Forms» заточены именно для создания оконного интерфейса и не предназначены для такого использования, как в вышеприведенном скрипте. Можно сказать, что мы тут фотоаппаратом пытаемся забивать гвоздь.

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

$dom.GetType()
""
$dom.all[0].outerHTML
""
foreach ($elem in $dom.all) {
    "--$($elem.tagName)"
}

Результат 1.

IsPublic IsSerial Name                           BaseType
-------- -------- ----                           --------
True     False    __ComObject                    System.MarshalByRefObject

<HTML><HEAD><TITLE>Тестовая страница</TITLE></HEAD>
<BODY>Текст.</BODY></HTML>

--HTML
--HEAD
--TITLE
--BODY

Результат 2.

IsPublic IsSerial Name                           BaseType
-------- -------- ----                           --------
True     False    HtmlDocument                   System.Object

<HTML><HEAD><TITLE>Тестовая страница</TITLE></HEAD>
<BODY>Текст.</BODY></HTML>

--HTML
--HEAD
--TITLE
--BODY

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

Выводы

Я не собираюсь использовать эти способы в своем скрипте, так как указанные классы морально устарели (базируются на стареньком «IE11», который, насколько я понимаю, даже не поддерживает множество нововведений последних лет, внесенных в стандарт языка HTML). Просто хотел их попробовать, так как на сайте «Stack Overflow» на эти способы есть очень много ссылок. Эти способы всё еще работают, их можно использовать для каких-то простых случаев.

Я собираюсь рассмотреть для использования в своем скрипте одну из двух подходящих библиотек: «Html Agility Pack» или «AngleSharp». Еще в данном контексте интересна технология «WebDriver», но, если ее использовать, то, видимо, не из «PowerShell», потому что там задействуется браузер. Нужно ли это мне, не уверен.

Комментарии (4)


  1. Zara6502
    05.08.2022 11:12

    знакомился с PS когда потребовалось автоматизировать создание и запуск виртуалок в Hyper-V, честно прифигел от PS в плане "крутости"/"раздутости" возможностей и функционала, глубоко копать не стал, ограничился уровнем CMD/BASIC. Но в целом очень понравилось. Мне кажется излишняя сложность и уклон в ООП это скорее веяние времени чем потребность.


    1. vasilisc
      05.08.2022 13:49
      +1

      Как-то прочёл маленькую книженцию про PowerShell, там был раздел "щелчок-по-носу" мне линуксоиду: в PowerShell всё объекты, хоть на экране, естественно, мы видим текст. НО PowerShell - это не cmd/bash, где ты устраиваешь с помощью конвееров "переливашки текста", перепроверяешь скрипты при серьёзных обновлениях сервера ("не поплыл ли вывод?"), пытаешься grep'пить нужное и т.д. PowerShell действительно крутая штука и админам windows систем - обязателен к изучению.

      Более подробно и доказательно в разделе "О тексте, разборе текста и объектах"
      https://disk.yandex.ru/i/Pfe6iZZUXb4NZA


      1. DikSoft
        05.08.2022 15:48
        +1

        мне линуксоиду ...

        Welcome onboard! Как по мне, виндузятнику, язык более стройный, чем bash с его парсингом / грепаньем выходных строк. ))


    1. DikSoft
      05.08.2022 15:31
      +1

      Зачастую PowerShell является единственным инструментом для решения задач автоматизации в Windows среде. Либо лучшим выбором.
      Пример?

      PS C:\> Get-VMNetworkAdapter -VMName Redmond | Set-VMNetworkAdapterVlan -Isolated -PrimaryVlanId 10 -SecondaryVlanId 200	

      Найдёте удобную и понятную сходу замену? ))