从 HTML header 标签创建树 object-structure

Create a tree object-structure from HTML header tags

从 HTML header 标签创建树 object-structure 的最佳方法是什么? (我最关心的是之后的可读性)

这是 object 我想将 HTML 映射到:

public class Node
{
    public string Title { get; set; }
    public string Html { get; set; }
    public List<Node> SubNodes { get; set; }
}

这是示例 HTML:

<h1>Header 1</h1>
<p>Content under header 1</p>

<h2>H2 for header 1</h2>
<p>Content under H2 for header 1</p>

<h3>H3 for H2 under header 1</h3>
<h4>h4 for h3 under h2 and header 1<h4>
<p>Content under H4 for h3 and H2 under header 1</p>

<h2>Second H2 for header1</h2>
<p>Content under second H2 for header 1</p>

<h1>Second header 1</h1>
<p>Content under second header 1</p>

预期的结构应该是这样的(写在JSON):

[{ 
    'Title': 'Header 1',
    'Html': '<h1>Header 1</h1><p>Content under header 1</p>',
    'SubNodes': [{
        'Title': 'H2 for header 1',
        'Html': '<h2>H2 for header 1</h2><p>Content under H2 for header 1</p>',
        'SubNodes': [{        
            'Title': 'H3 for H2 under header 1',
            'Html': '<h3>H3 for H2 under header 1</h3>',
            'SubNodes': [{
                'Title': 'h4 for h3 under h2 and header 1,'
                'Html': '<h4>h4 for h3 under h2 and header 1<h4><p>Content under H4 for h3 and H2 under header 1</p>',
                'SubNodes': []
            }]
        },{
            'Title': 'Second H2 for header1',
            'Html': '<h2>Second H2 for header1</h2><p>Content under second H2 for header 1</p>',
            'SubNodes': [] 
        }]
    }]
},{
    'Title': 'Second header 1',
    'Html': '<h1>Second header 1</h1><p>Content under second header 1</p>',
    'SubNodes': [] 
}]

好吧,这一点都不漂亮,如果 HTML 的结构变化太多,它可能会中断,但它看起来很有效,也许它会让您知道从哪里开始。

首先,我会将 属性 public int Level { get; set; } 添加到您的 Node class 以简化操作。

接下来,您可能需要一种方法来判断 Level 标题有哪些。

我做了这样的东西:

bool IsHeading(string tagName, out int? level)
{
  level = null;
  if (tagName.StartsWith("h", StringComparison.OrdinalIgnoreCase) == false)
  {
    return false;
  }

  int tempLevel;
  if (int.TryParse(tagName.Substring(1), out tempLevel) == false)
  {
    return false;
  }

  level = tempLevel;
  return true;
}

算法类似于:

  • 获取第一个标题并将其设置为当前节点
  • 如果下一个元素不是标题,则将其内容附加到当前节点
  • 重复最后一步,直到下一个元素是标题。
  • 将当前节点设置为parent节点,获取下一个标题并将其设置为新的当前节点。
  • 如果下一个标题有更高级别,请将其添加到 parent。
  • 如果级别相同或更低,则找到级别更低的最后一个节点,并将其添加到该节点。
  • 如果没有下级节点,则视为"first heading"。
  • 重复

像这样:

  var nodeList = new List<Node>();
  var allNodes = new List<Node>();
  Node parentNode = null;
  Node currentNode = null;

  foreach (var htmlNode in body.ChildNodes)
  {
    int? level;

    if (IsHeading(htmlNode.Name, out level) && level.HasValue)
    {
      currentNode = new Node();
      currentNode.Title = htmlNode.InnerText;
      currentNode.Html = htmlNode.OuterHtml;
      currentNode.Level = level.Value;
      allNodes.Add(currentNode);

      if (!allNodes.Any(n => n.Level < currentNode.Level))
      {
        nodeList.Add(currentNode);
        parentNode = null;
      }

      if (parentNode != null)
      {
        if (parentNode.Level >= currentNode.Level)
        {
          parentNode = allNodes.Last(n => n.Level < currentNode.Level);
        }
        parentNode.SubNodes.Add(currentNode);
      }
      parentNode = currentNode;

      continue;
    }

    if (currentNode == null)
    {
      continue;
    }

    currentNode.Html += htmlNode.OuterHtml;
  }

再说一次,不是骄傲,而是一个开始。

编辑 1:不知道 rootNode 是什么意思。没有必要;已删除。

编辑 2:哦,即使第一个标题不是 <h1>,它也可能是为了让它起作用。修正了那个。