title: Parse HTML table to JSON in ASP.NET C#
body: <p>I'm having a problem. I have a survey builder written with different types of questions (list, checkbox, dropdowns...) and one with a table to fill. The problem is now to store the table I have 3 different tables on my DB and to create the table later is a bit complex. I'm trying to do it, using a JavaScript editor so the user can create the table as in word. On my code behind I receive the table markup as an String and I want to store it as JSON on the DB.</p>
  
  <p>I have this code here to convert the table to a DataSet object and then from here I can convert to XML and from here to JSON using the library JSON.NET. All of this is working fine, but the problem is when the table have "colspan" &amp; "rowspan" parameters, is not working with them. Can you help me to finish this piece of code so everyone can have this as an example to parse HTML table to JSON in C#.</p>
  
  <pre><code>protected void Page_Load(object sender, EventArgs e)
  {
      string table = @"&lt;table&gt;
                  &lt;tbody&gt;&lt;tr&gt;
                      &lt;th rowspan='4'&gt;Project&lt;/th&gt;&lt;th rowspan='4'&gt;Country&lt;/th&gt;&lt;th colspan='3' rowspan='1'&gt;Header 1&lt;/th&gt;&lt;th colspan='2' rowspan='1'&gt;Header 2&lt;/th&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;th colspan='1' rowspan='1'&gt;Child Header 1&lt;/th&gt;&lt;th colspan='2' rowspan='1'&gt;Child Header 2&lt;/th&gt;&lt;th colspan='1' rowspan='3'&gt;Child Header 3&lt;/th&gt;&lt;th colspan='1' rowspan='3'&gt;Child Header 4&lt;/th&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;th colspan='1' rowspan='2'&gt;Child Child Header 1&lt;/th&gt;&lt;th colspan='1' rowspan='1'&gt;tee&lt;/th&gt;&lt;th colspan='1' rowspan='2'&gt;ssss&lt;/th&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;th colspan='1' rowspan='1'&gt;aas&lt;/th&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;td&gt;EUS&lt;/td&gt;&lt;td&gt;ES&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;td&gt;ARP&lt;/td&gt;&lt;td&gt;IE&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;td&gt;ARM&lt;/td&gt;&lt;td&gt;UK&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;td&gt;SMRT&lt;/td&gt;&lt;td&gt;US&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;
                  &lt;/tr&gt;&lt;tr&gt;
                      &lt;td&gt;CM&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;
                  &lt;/tr&gt;
              &lt;/tbody&gt;&lt;/table&gt;";
  
      DataSet dataSet = HtmlTableParser.ParseDataSet(table);
      StringWriter sw = new StringWriter();
      dataSet.WriteXml(sw, XmlWriteMode.IgnoreSchema);
      XmlDocument xd = new XmlDocument();
      xd.LoadXml(sw.ToString());
  
      string jsonText = JsonConvert.SerializeXmlNode(xd).Replace("_x0020_", " ");
  }
  
  /// &lt;summary&gt;
  /// HtmlTableParser parses the contents of an html string into a System.Data DataSet or DataTable.
  /// &lt;/summary&gt;
  public class HtmlTableParser
  {
      private const RegexOptions ExpressionOptions = RegexOptions.Singleline | RegexOptions.Multiline | RegexOptions.IgnoreCase;
      private const string CommentPattern = "&lt;!--(.*?)--&gt;";
      private const string TablePattern = "&lt;table[^&gt;]*&gt;(.*?)&lt;/table&gt;";
      private const string HeaderPattern = "&lt;th[^&gt;]*&gt;(.*?)&lt;/th&gt;";
      private const string RowPattern = "&lt;tr[^&gt;]*&gt;(.*?)&lt;/tr&gt;";
      private const string CellPattern = "&lt;td[^&gt;]*&gt;(.*?)&lt;/td&gt;";
  
      /// &lt;summary&gt;
      /// Given an HTML string containing n table tables, parse them into a DataSet containing n DataTables.
      /// &lt;/summary&gt;
      /// &lt;param name="html"&gt;An HTML string containing n HTML tables&lt;/param&gt;
      /// &lt;returns&gt;A DataSet containing a DataTable for each HTML table in the input HTML&lt;/returns&gt;
      public static DataSet ParseDataSet(string html)
      {
          DataSet dataSet = new DataSet();
          MatchCollection tableMatches = Regex.Matches(
              WithoutComments(html),
              TablePattern,
              ExpressionOptions);
  
          foreach (Match tableMatch in tableMatches)
              dataSet.Tables.Add(ParseTable(tableMatch.Value));
  
          return dataSet;
      }
  
      /// &lt;summary&gt;
      /// Given an HTML string containing a single table, parse that table to form a DataTable.
      /// &lt;/summary&gt;
      /// &lt;param name="tableHtml"&gt;An HTML string containing a single HTML table&lt;/param&gt;
      /// &lt;returns&gt;A DataTable which matches the input HTML table&lt;/returns&gt;
      public static DataTable ParseTable(string tableHtml)
      {
          string tableHtmlWithoutComments = WithoutComments(tableHtml);
  
          DataTable dataTable = new DataTable();
  
          MatchCollection rowMatches = Regex.Matches(
              tableHtmlWithoutComments,
              RowPattern,
              ExpressionOptions);
  
          dataTable.Columns.AddRange(tableHtmlWithoutComments.Contains("&lt;th")
                                         ? ParseColumns(tableHtml)
                                         : GenerateColumns(rowMatches));
  
          ParseRows(rowMatches, dataTable);
  
          return dataTable;
      }
  
      /// &lt;summary&gt;
      /// Strip comments from an HTML stirng
      /// &lt;/summary&gt;
      /// &lt;param name="html"&gt;An HTML string potentially containing comments&lt;/param&gt;
      /// &lt;returns&gt;The input HTML string with comments removed&lt;/returns&gt;
      private static string WithoutComments(string html)
      {
          return Regex.Replace(html, CommentPattern, string.Empty, ExpressionOptions);
      }
  
      /// &lt;summary&gt;
      /// Add a row to the input DataTable for each row match in the input MatchCollection
      /// &lt;/summary&gt;
      /// &lt;param name="rowMatches"&gt;A collection of all the rows to add to the DataTable&lt;/param&gt;
      /// &lt;param name="dataTable"&gt;The DataTable to which we add rows&lt;/param&gt;
      private static void ParseRows(MatchCollection rowMatches, DataTable dataTable)
      {
          foreach (Match rowMatch in rowMatches)
          {
              // if the row contains header tags don't use it - it is a header not a row
              if (!rowMatch.Value.Contains("&lt;th"))
              {
                  DataRow dataRow = dataTable.NewRow();
  
                  MatchCollection cellMatches = Regex.Matches(
                      rowMatch.Value,
                      CellPattern,
                      ExpressionOptions);
  
                  for (int columnIndex = 0; columnIndex &lt; cellMatches.Count; columnIndex++)
                      dataRow[columnIndex] = cellMatches[columnIndex].Groups[1].ToString();
  
                  dataTable.Rows.Add(dataRow);
              }
          }
      }
  
      /// &lt;summary&gt;
      /// Given a string containing an HTML table, parse the header cells to create a set of DataColumns
      /// which define the columns in a DataTable.
      /// &lt;/summary&gt;
      /// &lt;param name="tableHtml"&gt;An HTML string containing a single HTML table&lt;/param&gt;
      /// &lt;returns&gt;A set of DataColumns based on the HTML table header cells&lt;/returns&gt;
      private static DataColumn[] ParseColumns(string tableHtml)
      {
          MatchCollection headerMatches = Regex.Matches(
              tableHtml,
              HeaderPattern,
              ExpressionOptions);
  
          return (from Match headerMatch in headerMatches
                  select new DataColumn(headerMatch.Groups[1].ToString())).ToArray();
      }
  
      /// &lt;summary&gt;
      /// For tables which do not specify header cells we must generate DataColumns based on the number
      /// of cells in a row (we assume all rows have the same number of cells).
      /// &lt;/summary&gt;
      /// &lt;param name="rowMatches"&gt;A collection of all the rows in the HTML table we wish to generate columns for&lt;/param&gt;
      /// &lt;returns&gt;A set of DataColumns based on the number of celss in the first row of the input HTML table&lt;/returns&gt;
      private static DataColumn[] GenerateColumns(MatchCollection rowMatches)
      {
          int columnCount = Regex.Matches(
              rowMatches[0].ToString(),
              CellPattern,
              ExpressionOptions).Count;
  
          return (from index in Enumerable.Range(0, columnCount)
                  select new DataColumn("Column " + Convert.ToString(index))).ToArray();
      }
  }
  </code></pre>