PowerShell, HTML Agility Pack: связь с исходным HTML-файлом при его анализе

e3c6053665e3fa86c303959e2b2bbcce

В скрипте для программы-оболочки «Windows PowerShell» версии 5.1 (или «PowerShell» версии 7) в операционной системе «Windows 10» я получаю текст из файла с кодом на языке HTML для дальнейшего анализа. С помощью библиотеки «HTML Agility Pack» превращаю этот текст в объект $dom, содержащий HTML-дерево, представляющее исходный файл.

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

Эта информация должна быть получена при конвертации исходного файла в объект $dom. Сначала я хотел сам написать это, а затем догадался посмотреть в описании библиотеки «HTML Agility Pack» — в ней уже есть эта функциональность! У каждого узла есть свойства Line и LinePosition, содержащие нужные номер строки и номер столбца (позиции в строке).

Я временно изменил код обработки узла при обходе HTML-дерева, чтобы увидеть эти номера:

    #   Обработка узла (вывод в консоль)
    if (("#document" -eq $node.Name) -or
        (("#" -eq $node.Name[0]) -and ("" -eq $node.OuterHTML.Trim()))) {
        #   Корневой узел не выводим,
        #   Пустые узлы (пробелы, символы новой строки и т.п.) не выводим
    } else {
        $content = ""; if ("#" -eq $node.Name[0]) {
            $content = ", '" + $node.OuterHTML.Trim() + "'" }
        $count = 0; foreach ($ancestor in $node.Ancestors()) { $count++ }
        $count = ($count - 1) * 2   #   отступ: 2 пробела на уровень, можно менять
        (" " * $count) + $node.Name + $content + ", строка " +
            $node.Line + ", столбец " + ($node.LinePosition + 1)
    }

После этого скрипт выдал у меня такой результат (тестовый исходный файл с кодом на языке HTML можно увидеть в одной из предыдущих статей, он примитивный):

#comment, '', строка 1, столбец 1
html, строка 1, столбец 17
  head, строка 1, столбец 24
    meta, строка 1, столбец 33
    title, строка 1, столбец 58
      #text, 'Название страницы', строка 1, столбец 65
  body, строка 1, столбец 99
    #comment, '', строка 1, столбец 108
    p, строка 1, столбец 139
      #text, 'Текст.', строка 1, столбец 142

Видно, что что-то пошло не так: для всех узлов HTML-дерева указана одна и та же строка с номером 1, хотя в исходном файле узлы находятся в строках с разными номерами.

Через некоторое время я сообразил: я сам получаю текст из исходного файла в переменную $html типа System.String. То есть я сам преобразовал текст исходного файла в строку, потеряв данные о структуре исходного файла. Кроме метода LoadHtml объекта класса HtmlAgilityPack.HtmlDocument, который принимает аргумент типа System.String, в библиотеке «HTML Agility Pack» у объекта класса HtmlAgilityPack.HtmlDocument есть и другие методы для загрузки исходного кода на языке HTML.

Например, один из них называется Load и может принимать строку с путем к исходному файлу и сведения о его кодировке. Этот метод сам получит данные из исходного файла и правильно прочитает информацию о его структуре (номера строк и номера столбцов). Использование этого метода даже немного упростит мой скрипт: мне не нужно будет самому получать данные из исходного файла.

Итак, вносим изменение в скрипт (переменная $file содержит строку (System.String) с путем к исходному файлу, путь может быть как абсолютным, так и относительным):

Add-Type -Path "HtmlAgilityPack.1.11.43\lib\netstandard2.0\HtmlAgilityPack.dll"
$dom = New-Object -TypeName "HtmlAgilityPack.HtmlDocument"
#$dom.LoadHtml($html)   #   Вариант с загрузкой строки
$dom.Load($file, [System.Text.Encoding]::UTF8)

После этого изменения скрипт выдал у меня такой результат:

#comment, '', строка 1, столбец 1
html, строка 2, столбец 1
  head, строка 3, столбец 1
    meta, строка 4, столбец 3
    title, строка 5, столбец 3
      #text, 'Название страницы', строка 5, столбец 10
  body, строка 7, столбец 1
    #comment, '', строка 8, столбец 3
    p, строка 9, столбец 3
      #text, 'Текст.', строка 9, столбец 6

Теперь всё в порядке, можно проверить по исходному файлу:




  
  Название страницы


  
  

Текст.

Замечу, что свойство Line узла HTML-дерева выдает нумерацию строк, начиная с единицы, а вот свойство LinePosition выдает нумерацию позиций в строке, начиная с нуля. Поэтому в скрипте пришлось к номеру позиции в строке (номеру столбца) каждый раз прибавлять единицу (см. код выше в статье).

Заключение

Эта информация будет полезна тем, кто, как и я, начинает работать с библиотекой «HTML Agility Pack». Лично мне было сложно выбрать между методами LoadHtml и Load для загрузки исходного кода на языке HTML, так как я изначально не понимал, какая между ними может быть разница (кроме той, что эти методы принимают разные входные параметры). Выше я продемонстрировал одно из отличий. Возможно, есть и другие.

Документация по библиотеке «HTML Agility Pack» на сайте проекта очень бедна и неполна. Постоянно приходится шерстить файл «HtmlAgilityPack.xml» с описанием классов, содержащихся в библиотеке (этот файл довольно большой, я получил его вместе с библиотекой).

© Habrahabr.ru