Recursive AI

Parse HTML from a string, file or stream

All onehtml recipes

Load HTML into a document from a string, a file or a stream, let the parser pick the encoding from the bytes, and see the tokenizer’s parse errors when you need them.

Document.Parse has three overloads: bytes (ReadOnlyMemory<byte>), a string, and an already decoded SourceText. There is no Stream or file-path overload, because the library never reads files or the network. You read the input and pass the bytes.

Prefer bytes whenever you have them. With bytes, the parser applies the WHATWG encoding rules: byte order mark, then the transport Content-Type (ParseOptions.ContentType), then the <meta> prescan. A string is used as it is.

C#
using System.Text;
using RecursiveAI.OneHtml;
 
// From a string
Document fromString = Document.Parse("<p class=lead>Hello, <b>world</b>");
Console.WriteLine(fromString.Root.QuerySelector("p.lead").TextContent);
Console.WriteLine(fromString.SerializeNode(fromString.DocumentElement));
 
// From a file: read the bytes and let the parser sniff the encoding
Document fromFile = Document.Parse(File.ReadAllBytes("events.html"));
Console.WriteLine($"{fromFile.Source.Encoding.Name}: {fromFile.Root.QuerySelector("h1").TextContent}");
 
// From a stream: copy it into memory first; Parse takes bytes, not a Stream
using Stream stream = File.OpenRead("events.html");
using var buffer = new MemoryStream();
await stream.CopyToAsync(buffer);
Document fromStream = Document.Parse(buffer.GetBuffer().AsMemory(0, (int)buffer.Length));
Console.WriteLine($"{fromStream.NodeCount} nodes");
 
// Tokenizer errors are recorded only when asked for
var options = new ParseOptions { RecordErrors = true };
Document broken = Document.Parse("<p title=\"a\"b>Fish &chips</p>", options);
foreach (var error in broken.ParseErrors)
{
    Console.WriteLine($"{error.Error} at offset {error.Offset}");
}
Console.WriteLine(broken.SerializeNode(broken.Root.QuerySelector("p")));
Text
Hello, world
<html><head></head><body><p class="lead">Hello, <b>world</b></p></body></html>
UTF-8: Upcoming events
101 nodes
MissingWhitespaceBetweenAttributes at offset 12
<p title="a" b="">Fish &amp;chips</p>

Some things to know:

  • A string input still gets the full document: <html>, <head> and <body> are created as a browser would create them. To parse a snippet without them, see HTML fragments.
  • buffer.GetBuffer().AsMemory(0, (int)buffer.Length) hands over the stream’s bytes without a second copy. buffer.ToArray() works too.
  • Parse errors never throw. They are recorded only when ParseOptions.RecordErrors is set, and only the tokenizer’s errors are recorded (not tree construction errors). Without it, ParseErrors is empty.
  • Hostile-input limits (input size, nesting depth, attribute count) are checked before the work they guard. Going past one throws HtmlLimitExceededException. Set your own with ParseOptions.Limits.
  • SerializeNode and SerializeChildren write the HTML serialization of the tree. To write the page back with its original bytes, use ToLosslessBytes() (see Edit HTML without reformatting it).

Encoding detection in detail: Detect the encoding of an HTML page.

A longer guide with encoding detection and broken markup: Parse HTML in C#.

Fetching, encodings, robots.txt and JavaScript-built pages: Web scraping in C#.