项目

AngleSharp 常见问题解答

如何下载图片?

这与 AngleSharp 无直接关系。你可以对任何 URL 执行请求。

以下是一个示例:

var imageUrl = @"https://via.placeholder.com/150";
var localPath = @"g:\downloads\image.jpg";

using (var client = new HttpClient())
{
    using (var response = await client.GetAsync(imageUrl))
    {
        using (var source = await response.Content.ReadAsStreamAsync())
        {
            using (var target = File.OpenWrite(localPath))
            {
                await source.CopyToAsync(target);
            }
        }
    }
}

如果因为某种原因需要,例如,重用通过 AngleSharp 获取的一些 cookie,那么你可以共享 cookie 容器或使用来自 AngleSharp 的请求器。

var imageUrl = @"https://via.placeholder.com/150";
var localPath = @"g:\downloads\image.jpg";
var download = context.GetService<IDocumentLoader>().FetchAsync(new DocumentRequest(new Url(imageUrl)));

using (var response = await download.Task)
{
    using (var target = File.OpenWrite(localPath))
    {
        await response.Content.CopyToAsync(target);
    }
}

这假设有一个如下的配置/上下文:

IConfiguration config = Configuration.Default
    .WithDefaultLoader(new LoaderOptions { IsResourceLoadingEnabled = true })
    .WithCookies();
IBrowsingContext context = BrowsingContext.New(config);

是否可以在 JavaScript 和 Blazor 运行后获取 HTML?

AngleSharp 只是一个浏览器核心,尽管运行 JavaScript 是可能的(有一个实验性的插件),但它不适用于复杂的东西(例如,运行 Angular)。我不知道是否存在任何 WASM 插件,所以我想运行像 Blazor 这样的东西是不可能的,除非有人编写了 WASM 插件。

如何使用 AngleSharp 将 HTML 转换为 XML?

不幸的是,没有一种(总是有效,即银弹)方法可以将 HTML 转换为 XML——这两种格式实际上是不兼容的。当对象模型不兼容时,问题会变得更加严重,例如,使用 AngleSharp 创建 DOM 并将其转换为 XmlDocument 实例。

你需要手动进行转换,使用一个映射函数。由于格式不兼容,你将需要指定要转换的内容以及在不存在映射的情况下如何反应...

如何处理其他身份验证方法(例如,Kerberos)?

这高度依赖于认证方案。假设我们使用 Windows 认证方案( “IWA” 或有时称为 NTLM/Kerberos)。有几种实现方式(你肯定需要 CookieContainer 是激活状态,所以 WithCookies() 是必需的,但是这并不能首先让你完成认证)。你肯定会想使用 AngleSharp.Io 库中的 HttpRequester(因为它允许你重新配置它)或者自己实现一个 IRequester 的实现。

对于旧的 HttpWebRequest ,你只需设置正确的认证级别和凭据(如下所示)以进行适当的认证。

req.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequested;
req.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;

另一个非常常见的场景是标准网络凭据。这些也可以更轻松地使用基于 HttpClientAngleSharp.Io 方法提供。

var credentials = new NetworkCredential("user", "pass", "domain");
var handler = new HttpClientHandler { Credentials = credentials };
IConfiguration config = Configuration.Default
    .WithRequesters(handler)
    .WithCookies()
    .WithDefaultLoader();
IBrowsingContext context = BrowsingContext.New(config);
IDocument document = await context.OpenAsync(url);

如何使用代理与 AngleSharp 一起?

我们推荐使用 AngleSharp.Io 中基于 HttpClientIRequester 实现。这个可以被正确地重新配置。

作为一个例子,以下处理器可能来自一些提供 PortAddressproxyServerSettings

var handler = new HttpClientHandler()
{
    Proxy = new WebProxy(String.Format("{0}:{1}", proxyServerSettings.Address, proxyServerSettings.Port), false),
    PreAuthenticate = true,
    UseDefaultCredentials = false,
};

文本区域中的内容被 HTML 编码!

给定一些内容如下:

<p><textarea>one<p>two

AngleSharp 将这些部分视为:

<p><textarea>one&lt;p&gt;two</textarea></p>

这是官方 HTML 规范定义的标准行为。textarea 标签切换到一个新的解析状态,并不会自动关闭。它需要遇到一个 textarea 结束标签来关闭。这个新的解析状态基本上忽略了所有保留字符(例如,<),导致序列化表示你看到的是它们的编码值。

因此,问题不在于编码(这只是序列化的表示形式),而是 textarea 没有关闭,这将把所有(假设?)子元素作为原始输入放入 textarea 中。

不幸的是,你在这里什么都做不了——你需要关闭 textarea 。所有浏览器(因此最初的备注与规范有关)都以相同的方式看待它,所以这并不是 AngleSharp 独有的。

在 AngleSharp 中我可以 “点击” 什么?

在 AngleSharp(核心,即非 JS)中,你唯一能点击的是具有锚点的所有东西(链接会被跟随),比如 a ,或者是提交(如 button )按钮,表单将被提交。例如,如果我们有一个在 JS 中定义了点击处理器的 div ,则什么也不会出来。

如何在没有 UI 的情况下对 div 进行点击?

让我们再次回顾一下 AngleSharp 可以做什么:

  • 任何类型的请求,包括它们的修改(在请求时,但在响应之前)
  • 通用的 cookie 管理(当然还有它们的修改)
  • 查询 DOM 并执行 “简单” 的操作(例如,点击按钮、提交表单)
  • 运行简单的 JavaScript 文件

这里的 “简单” 意味着:脚本不需要超出 AngleSharp 提供的任何功能,例如,渲染树信息、高级 CSSOM 访问、... - 或者使用非 ES5 兼容解析器的脚本(例如,使用 ES6 或某些特殊的非标准能力)。

问题是,为了在页面上 “点击” 一个 div,需要运行一个脚本。这个脚本现在可以属于“简单”类别,然而,大多数情况下不是。现在你有两个选项:

  • 试一试,也许它可以工作/很好,否则...
  • 看看脚本在做什么(显然最终是某个 HTTP 请求...)并做同样的事情

后者当然可以在 C# / AngleSharp 中重新实现。所以你可以创建一个 HTTP 请求,获取数据,然后要么直接对该数据集做一些操作(可能是 JSON,已经是你想要的...)要么(如果是提供部分 HTML)重新解析它,并将其整合到真实的页面上。

如何删除所有匹配特定选择器的元素?

以下代码适用于所有 span 元素。确保根据你的问题调整选择器。

foreach (var element in document.QuerySelectorAll('span'))
{
    element.Remove();
}

DocumentUri 与 Url 有何不同?

这些属性对应于 DOM 中同名的属性。

根据 MDN 所述:

HTML 文档具有 document.URL 属性,它返回与 document.documentURI 相同的值。与 URL 不同,documentURI 在所有类型的文档上都可用。

因此,理论上只有 DocumentUri 保证总是能返回一个值。

如何从 HTML 文档中提取一组 URL?

假设 URL 总能在标准的锚链接(a)中找到。一种可能的方法是使用以下 LINQ 查询:

IEnumerable<IHtmlAnchorElement> links = document.Links
    .OfType<IHtmlAnchorElement>()
    .Select(e => e.Href)
    .Where(h => h.Contains(keyword));

根据我们的条件,我们可能会使用不同的 LINQ 语句(或至少是不同的 Where 子句)。

如何向 <input type='file'> 指定输入文件?

每个 IHtmlInputElement 都有一个 Files 属性,可用于添加文件。

var input = document.QuerySelector<IHtmlInputElement>("input[type=file][name=myInputFile]");
input?.Files.Add(file);

在之前的示例中,file 变量指的是任何 IFile 实例。AngleSharp 是一个 PCL,并没有自带实现,但一个简单的示例如下:

class FileEntry : IFile
{
    private readonly String _fileName;
    private readonly Stream _content;
    private readonly String _type;
    private readonly DateTime _modified;

    public FileEntry(String fileName, String type, Stream content)
    {
        _fileName = fileName;
        _type = type;
        _content = content;
        _modified = DateTime.Now;
    }

    public Stream Body
    {
        get { return _content; }
    }

    public Boolean IsClosed
    {
        get { return _content.CanRead == false; }
    }

    public DateTime LastModified
    {
        get { return _modified; }
    }

    public Int32 Length
    {
        get
        {
            return (Int32)_content.Length;
        }
    }

    public String Name
    {
        get { return _fileName; }
    }

    public String Type
    {
        get { return _type; }
    }

    public void Close()
    {
        _content.Close();
    }

    public void Dispose()
    {
        _content.Dispose();
    }

    public IBlob Slice(Int32 start = 0, Int32 end = Int32.MaxValue, String contentType = null)
    {
        var ms = new MemoryStream();
        _content.Position = start;
        var buffer = new Byte[Math.Max(0, Math.Min(end, _content.Length) - start)];
        _content.Read(buffer, 0, buffer.Length);
        ms.Write(buffer, 0, buffer.Length);
        _content.Position = 0;
        return new FileEntry(_fileName, _type, ms);
    }
}

一个更复杂的实现会自动确定 MIME 类型,并有构造函数重载以允许传入(本地)文件路径等。

如何解析匿名块中的文本?

文本被建模为 TextNode ,它是节点的一种类型,除了元素、注释节点、处理指令等。这就是为什么尝试使用的 NextElementSibling 没有包含结果中的文本,因为它本意是只返回元素,正如其名称所暗示的那样。

你可以通过遍历 div.productChildNodes 并然后按 NodeType 过滤来获取直接位于产品 div 内的文本节点,例如:

IHtmlCollection<IElement> products = document.QuerySelectorAll("div.product");

foreach (var product in products)
{
    INode productTitle = product.ChildNodes
        .First(o => o.NodeType == NodeType.Text && o.TextContent.Trim() != "");
    Console.WriteLine(productTitle.TextContent.Trim());
}

请注意,元素之间的换行符也是文本节点,所以我们需要过滤掉这些。

如何生成自闭合标签?

给定以下使用场景:

IBrowsingContext context = BrowsingContext.New();
IDocument document = await context.OpenNewAsync();

IElement tag = document.CreateElement("customTag");
tag.SetAttribute("attr", "x");
tag.AsSelfClosing();

Console.WriteLine(tag.OuterHtml);
tag.ToHtml(Console.Out, CustomHtmlMarkupFormatter.Instance);

我们将得到以下输出:

<customtag attr="x"> <customtag attr="x" /></customtag>

有两个地方你可以在其中进行一些工作以实现这样的功能。

  • readonly NodeFlags Node._flags:记住,这个字段、它的属性和宿主类都没有公开。所以你需要一些不那么干净的技巧来完成这项工作。此外,默认的格式化器 HtmlMarkupFormatter 只使用 > ,而不使用 />
  • 创建你自己的 IMarkupFormatter

这里提供一个结合上述两点的解决方案。

让我们先从一些不那么干净的技巧开始:

public static class ElementExtensions
{
    public static void AsSelfClosing(this IElement element)
    {
        const int SelfClosing = 0x1;

        var type = typeof(IElement).Assembly.GetType("AngleSharp.Dom.Node");
        var field = type.GetField("_flags", BindingFlags.Instance | BindingFlags.NonPublic);

        var flags = (uint)field.GetValue(element);
        flags |= SelfClosing;
        field.SetValue(element, Enum.ToObject(field.FieldType, flags));
    }
}

自定义标记格式化器的实现

public class CustomHtmlMarkupFormatter : IMarkupFormatter
{
    public static readonly CustomHtmlMarkupFormatter Instance = new CustomHtmlMarkupFormatter();

    public string Text(String text) => HtmlMarkupFormatter.Instance.Text(text);
    public string Comment(IComment comment) => HtmlMarkupFormatter.Instance.Comment(comment);
    public string Processing(IProcessingInstruction processing) => HtmlMarkupFormatter.Instance.Processing(processing);
    public string Doctype(IDocumentType doctype) => HtmlMarkupFormatter.Instance.Doctype(doctype);
    public string CloseTag(IElement element, Boolean selfClosing) => HtmlMarkupFormatter.Instance.CloseTag(element, selfClosing);
    public string Attribute(IAttr attribute) => HtmlMarkupFormatter.Instance.Attribute(attribute);

    public string OpenTag(IElement element, Boolean selfClosing)
    {
        var temp = new StringBuilder();
        temp.Append('<');

        if (!String.IsNullOrEmpty(element.Prefix))
        {
            temp.Append(element.Prefix).Append(':');
        }

        temp.Append(element.LocalName);

        foreach (var attribute in element.Attributes)
        {
            temp.Append(" ").Append(Instance.Attribute(attribute));
        }

        temp.Append(selfClosing ? " />" : ">");

        return temp.ToString();
    }
}

如何在 Unity 中使用 AngleSharp?

要在 Unity 解决方案中完全集成 AngleSharp,请遵循以下步骤:

  1. 获取 AngleSharp NuGet 包:从 VS NuGet 包管理器中获取 AngleSharp NuGet 包。
  2. 构建解决方案:执行构建操作(Build -> Build Solution)。
  3. 复制“netstandard2.0”文件夹:将找到的“netstandard2.0”文件夹复制到 Unity 项目的 Assets 文件夹内。路径通常为 “[你的项目]\Packages\AngleSharp.0.11.0” 。版本号可能有所不同。

此方法的原因及更多细节,可参考 StackOverflow 上的回答。目前,已知的解决方法是使用 VS 安装 NuGet 包,这会正确解析.NET Standard 2.0 依赖项。

上述解答来源于 #774 问题讨论。

如何根据字符串创建元素?

可以使用文档片段来实现。

一种方式是使用片段解析以在正确的(元素)上下文中生成节点列表:

IBrowsingContext context = BrowsingContext.New(Configuration.Default);
IDocument document = await context.OpenAsync(r => r.Content("<div id=app><div>已有部分内容...</div></div>"));
IElement app = document.QuerySelector("#app");
IHtmlParser parser = context.GetService<IHtmlParser>();
INodeList nodes = parser.ParseFragment("<div id='div1'>你好<p>世界</p></div>", app);
app.Append(nodes.ToArray());

示例展示了如何在特定元素(本例中为"#app")的上下文中创建节点,且行为不同于使用 InnerHtml ,后者会移除现有节点。

能否获取源代码中元素的位置信息?

默认情况下,AngleSharp 会丢弃与元素在源代码中位置相关联的“令牌”,主要是为了减少内存消耗。这些标签令牌不仅携带位置信息,还包括名称、标志和其他元数据以及属性。然而,这些令牌可以被保留。

当前有两种方式实现这一点(均可通过 HtmlParserOptions 访问):

  1. 对于解析过程中的一次性场景,可以使用 OnCreated 回调。第一个参数是 IElement 实例,第二个参数是 TextPosition 值。
  2. 若要稍后检索,可以设置 IsKeepingSourceReferences 选项为 true 。这样,所有由解析器创建的 IElement 实例的 SourceReference 属性将不为空。当前,被引用的 ISourceReference 仅包含一个 Position 属性。

针对第 1 种选项,代码如下:

var bodyPos = TextPosition.Empty;
var parser = new HtmlParser(new HtmlParserOptions
{
    OnCreated = (IElement element, TextPosition position) =>
    {
        if (element.TagName == "BODY")
        {
            bodyPos = position;
        }
    },
});
IDocument document = parser.ParseDocument("<!doctype html><body>");

第 2 种选项的代码如下:

var parser = new HtmlParser(new HtmlParserOptions
{
    IsKeepingSourceReferences = true,
});
IDocument document = parser.ParseDocument("<!doctype html><body>");
TextPosition bodyPos = document.Body.SourceReference.Position;

在这两种情况中,我们关心的位置都将存储在 bodyPos 中。

备注:由于 SourceReference 可能为空(例如,如果我们省略提供的选项或选择的是解析后进入的元素),建议使用 SourceReference?.Position ,这样会得到一个 Nullable<TextPosition> 。理想情况下,我们直接使用 TextPosition.Empty 作为备选,如上面代码所示:

TextPosition bodyPos = document.Body.SourceReference?.Position ?? TextPosition.Empty;

如何指定加载文档时的编码?

当你以流的形式拥有文档时,可能希望给 AngleSharp 指定一个特定的编码,就像 Web 服务器所做的那样。

实际上,AngleSharp 的虚拟请求 API 使得模拟 HTTP 功能等变得相当简单:

IBrowsingContext context = BrowsingContext.New();

IDocument document = await context.OpenAsync(req => req.Content(myStream).Header("content-type", "text/html; charset=UTF-8"));

AngleSharp 中的编码决策遵循与浏览器相同的优先级列表。实质上,这意味着字节顺序标记(BOM)始终被视为最高标准,但头部的优先级高于源代码中找到的元标签。

无论如何,还存在“猜测”与“确定”选择的复杂性。因此,即使有 BOM,它仍然会被检查,因为它符合 W3C 的标准。

在本文档中