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;
另一个非常常见的场景是标准网络凭据。这些也可以更轻松地使用基于 HttpClient
的 AngleSharp.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 中基于 HttpClient
的 IRequester
实现。这个可以被正确地重新配置。
作为一个例子,以下处理器可能来自一些提供 Port
和 Address
的 proxyServerSettings
。
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<p>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.product
的 ChildNodes
并然后按 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,请遵循以下步骤:
- 获取 AngleSharp NuGet 包:从 VS NuGet 包管理器中获取 AngleSharp NuGet 包。
- 构建解决方案:执行构建操作(Build -> Build Solution)。
- 复制“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
访问):
- 对于解析过程中的一次性场景,可以使用
OnCreated
回调。第一个参数是IElement
实例,第二个参数是TextPosition
值。 - 若要稍后检索,可以设置
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 的标准。