者:Matt Trilby-Bassett;Min Huang
排版:Rani Sun
開發人員在閱讀 API 參考文檔時,有時會需要或希望查看相應的源代碼。直到不久之前,.NET API 參考文檔還沒有提供指向源代碼的鏈接,這引起社區添加這一功能的呼聲。針對這一反饋,我們很高興地宣布,現在大多數流行的 .NET API 上都提供了連接文檔和源代碼的鏈接。
在這篇博文中,我們將分享將鏈接添加到文檔以及利用現有 API 來實現這一改進的詳細信息。
鏈接的實例
在介紹實施細節之前,我們想展示一下文檔的改動。對于符合我們標準(啟用了源代碼鏈接、具有可訪問的 PDB 并托管在公共存儲庫中)的 .NET API,其鏈接包含在 Definition 元數據中。以下來自 String 類的截圖演示了這個新鏈接的位置:
如果存在重載,鏈接將包含在重載標題的下面。下面的 String.IndexOf 方法截圖演示了這種情況:
參考文檔
https://learn.microsoft.com/en-us/dotnet/api/
String.IndexOf 方法
https://learn.microsoft.com/en-us/dotnet/api/system.string.indexof?view=net-8.0#system-string-indexof(system-string-system-int32-system-int32)
我們如何建立鏈接?
.NET 參考文檔管道對一組 DLL 文件和 NuGet 包進行操作。這些文件由各種工具處理,以將其內容轉換為顯示在 Microsoft Learn 上的 HTML 頁面。正確構建源代碼的鏈接需要了解源代碼、二進制文件和 GitHub 之間的關系,以及它們如何與一些現有的 .NET API 配合在一起。在與 .NET 和 Roslyn 團隊的開發人員討論我們公開源代碼鏈接的目標時,很明顯我們的要求與 Visual Studio 的Go to definition功能緊密相關。
憑借這種理解以及 @davidwengier 在 Roslyn 中針對外部源的Go to definition改進中提供的有關Go to definition的大量細節,我們能夠采用類似的方法來構建指向文檔源代碼的鏈接。
Go to definition
https://github.com/dotnet/roslyn/issues/55834
@davidwengier
https://github.com/davidwengier
源代碼鏈接
源代碼鏈接是一種技術,它使 .NET 開發人員能夠調試其應用程序引用的程序集的源代碼。盡管源代碼鏈接最初旨在用于源代碼調試,但它完全適用于我們的場景。每個啟用源代碼鏈接的 .NET 項目都會在 PDB(程序數據庫)中生成從相對文件夾路徑到 絕對存儲庫 URL 的映射。這與 @davidwengier 在 Roslyn 中針對外部源的 Go to definition 改進中所述一致。
若要查看源鏈接條目,可以使用 dotPeek 或 ILSpy 打開 DLL。以下屏幕截圖顯示了使用 dotPeek 訪問 System.Private.CoreLib 的源鏈接條目的示例,方法是導航到 Portable PDB Metadata,然后導航到 CustomDebugInformation 表:
[!NOTE] 若要了解有關源代碼鏈接的元數據定義,請轉到:PortablePdb-Metadata。
源代碼鏈接
https://github.com/dotnet/sourcelink
@davidwengier
https://github.com/davidwengier
Roslyn 中針對外部源的 Go to definition 改進
https://devblogs.microsoft.com/dotnet/go-to-definition-improvements-for-external-source-in-roslyn/#source-link
ILSpy
https://github.com/icsharpcode/ILSpy
PortablePdb-Metadata
https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#source-link-c-and-vb-compilers
建立鏈接
現在我們知道在源代碼鏈接條目中存儲了一個總體映射,下一個問題是如何為這個 DLL 中的每個類型/成員構建唯一的鏈接?
例如,我們為 String.Clone 方法構建的鏈接是:
https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs#L388C13-L388C25
此鏈接可分為 3 個部分:
第一部分 https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14 是從源代碼鏈接映射 json 解析出來的,并且與特定的存儲庫提交綁定。
第二部分 src/libraries/System.Private.CoreLib/src/System/String.cs 可以在 PDB 的文檔表中找到。
最后一部分 #L388C13-L388C25 是基于 MethodDebugInformation 表的 SequencePoints 列構建的。SequencePoints blob 會將此方法塊中的一系列 IL 指令映射回其原始源代碼行號,如下面的屏幕截圖所示。有關更多詳細信息,請轉到 SequencePoints 元數據定義。
我們使用 System.Reflection.Metadata 庫來遍歷此 DLL 中的所有類型/成員,然后匹配 MethodDebugInformation 表中的記錄以構建最終的鏈接。
var mdReader = peReader.GetMetadataReader();
foreach(var typeDefHandle in mdReader.TypeDefinitions)
{
var typeDef = mdReader.GetTypeDefinition(typeDefHandle);
string typeName = mdReader.GetString(typeDef.Name);
string ns = mdReader.GetString(typeDef.Namespace);
string fullName = String.IsOrEmpty(ns) ? typeName : $"{ns}.{typeName}";
Console.WriteLine(fullName);
foreach (var document in debugReader.FindSourceDocuments(typeDefHandle))
{
Console.WriteLine($" {document.SourceLinkUrl}");
}
}
該實現也可以在 Roslyn DocumentDebugInfoReader.cs 和 SymbolSourceDocumentFinder.cs 中找到。
SequencePoints 元數據定義
https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#sequence-points-blob
System.Reflection.Metadata
https://learn.microsoft.com/en-us/dotnet/api/system.reflection.metadata?view=net-8.0
DocumentDebugInfoReader.cs
https://github.com/dotnet/roslyn/blob/bbcac94e166e0cd87d36b41a387278e7d00d1728/src/Features/Core/Portable/PdbSourceDocument/DocumentDebugInfoReader.cs
SymbolSourceDocumentFinder.cs
https://github.com/dotnet/roslyn/blob/4262648cadff59cc703b6be8c00b9814a6b13c5a/src/Features/Core/Portable/PdbSourceDocument/SymbolSourceDocumentFinder.cs
查找 PDB 文件
因為我們知道鏈接的信息可以在 PDB 中找到,所以下一步就是找到這些 PDB 以供我們使用。
目前,指定某一個 DLL,我們會在三個地方查找相應的 PDB:
嵌入式 PDB。如果您的 csproj 中指定了<DebugType>embedded</DebugType>,則 PDB 文件將嵌入到此 DLL 中。
磁盤上的 PDB。您可以將 PDB 放在 DLL 旁邊。
Microsoft Symbol Server。有一個公共符號服務器,我們可以從中下載 DLL 的 PDB。
請參閱 Roslyn PdbFileLocatorService.cs 中的實現。
PdbFileLocatorService.cs
https://github.com/dotnet/roslyn/blob/b3d9ff7c9dc9e330b24d6087419dffe611a9dd77/src/Features/Core/Portable/PdbSourceDocument/PdbFileLocatorService.cs
查找正確的 PDB 版本
我們想進一步討論如何從 Microsoft Symbol Server 下載指定 DLL 的正確版本的 PDB。
下面是一個PDB 下載 URL的示例 ,其格式在 portable-pdb-signature 中定義。
http://msdl.microsoft.com/download/symbols/System.Private.CoreLib.pdb/8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF/System.Private.CoreLib.pdb
從 URL 模式中我們可以觀察到,我們需要提供 PDB 文件名 System.Private.CoreLib.pdb 和 GUID 8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF。那么問題是我們可以在哪里找到這些信息?
之前我們使用 dotPeek 打開 DLL 來查找源代碼鏈接條目。現在我們可以再次打開它并檢查元數據部分。
在上面的截圖中,我們可以在 Debug Directory 中找到這個 GUID,并且該條目必須是一個可移植代碼視圖條目。該條目的 Path 屬性代表 PDB 文件的路徑,我們可以從中獲取文件名。
foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.CodeView && entry.IsPortableCodeView)
{
var codeViewEntry = peReader.ReadCodeViewDebugDirectoryData(entry);
var pdbName = Path.GetFileName(codeViewEntry.Path);
var codeViewEntryGuid = $"{codeViewEntry.Guid.ToString("N").ToUpper()}FFFFFFFF";
return $"{MsftSymbolServerUrl}/{pdbName}/{codeViewEntryGuid}/{pdbName}";
}
}
查找 DLL 文件
如前所述,我們的 .NET 參考文檔管道對 DLL 文件或 NuGet 包的集合進行操作。但對于某些程序集,我們需要發揮創造力來生成指向源代碼的鏈接。以下是我們需要開發解決方案的兩種情況:
參考程序集。例如, Microsoft.NETCore.App.Ref 包中的 DLL。參考程序集沒有將 PDB 上傳到符號服務器,這阻止我們生成源代碼鏈接。我們當前的解決方案是下載Runtime 包并使用其中的程序集下載匹配的 PDB。
源代碼嵌入在 PDB 中。例如,System.Threading.AccessControl 包在構建時會將源代碼生成到 obj 文件夾中。
這并不能幫助我們鏈接到源代碼,因此我們不會使用 lib 文件夾中的 DLL,而是會在runtimes文件夾中查找同名的 DLL。
Microsoft.NETCore.App.Ref
https://www.nuget.org/packages/Microsoft.NETCore.App.Ref/8.0.0
Runtime 包
https://www.nuget.org/packages/Microsoft.NETCore.App.Runtime.linux-x64/8.0.0
System.Threading.AccessControl
https://www.nuget.org/packages/System.Threading.AccessControl/8.0.0
使用文檔管道中的鏈接
一旦我們找到正確的 DLL/PDB 文件并成功建立源代碼的鏈接,我們就會將此信息以 JSON 文件形式保存在目標文檔 GitHub 存儲庫中。
為了了解我們將如何使用這些信息,我們需要重新審視 .NET 參考文檔管道。管道為每種唯一類型創建一個 XML 文件,我們的構建系統稍后會將其轉換為顯示在 Microsoft Learn 上的 HTML 頁面。為了將 XML 中的 API 映射到 JSON 文件中找到的相應源代碼鏈接,我們使用唯一標識符 DocId。此值存在于 XML(DocId)和 JSON(DocsId)中。
例如,System.String 的 DocId 為 T:System.String。此 DocId 值將用于定位 System.Private.CoreLib.json 文件(其對應版本)中的源代碼鏈接。
"DocsId": "T:System.String",
"SourceLink": "https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs"
若要了解如何生成 DocId,請參閱 DocCommentId.cs 或 DocumentationCommentId.cs。
DocId
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d42-id-string-format
T:System.String
https://github.com/dotnet/dotnet-api-docs/blob/main/xml/System/String.xml#L4
System.Private.CoreLib.json
https://github.com/dotnet/dotnet-api-docs/blob/main/xml/SourceLinkInformation/net-8.0/System.Private.CoreLib.json
DocCommentId.cs
https://github.com/jbevain/cecil/blob/56d4409b8a0165830565c6e3f96f41bead2c418b/rocks/Mono.Cecil.Rocks/DocCommentId.cs#L303C2-L303C66
DocumentationCommentId.cs
https://github.com/dotnet/roslyn/blob/fd9a371c76d7b3440d0bf61ba2d8fe534d4a99ac/src/Compilers/Core/Portable/DocumentationCommentId.cs#L50
已知限制
在當前的實施中,我們意識到一些限制:
對于 PDB 中沒有記錄文檔信息的類型(例如枚舉或接口),在 CustomDebugInformation 表中引入了新的 GUID TypeDefinitionDocuments 來解決此問題。但是,對于某些 DLL,這些信息有時會被修剪,導致我們無法生成鏈接。
請參閱此處的錯誤詳細信息 https://github.com/dotnet/runtime/issues/100051。
對于沒有定義主體的類成員(例如 extern 或 abstract),PDB 中不包含行信息(SequencePoints)。因此,我們無法指向某個跨度范圍,而是指向整個文件。我們計劃在未來做出改進以解決此問題。
TypeDefinitionDocuments
https://www.w3.org/TR/WCAG22/#keyboard-accessible
另一個改進想法
您可能已經注意到,我們與 Go to definition共享了許多核心邏輯。事實上,我們在實現中重用了它們的幾個類。我們提出了一個準備用來的改進此過程的功能,即使用現有代碼修改 Roslyn,以生成供我們使用的類型/成員級源映射。
如果社區有同樣的需求,請評論為我們投票。謝謝!
向我們提供您的反饋
我們很樂意聽取您對使用這些鏈接的反饋,因此請告訴我們您的想法!如果您發現任何與鏈接相關的問題,請隨時使用反饋控件分享或在相關文檔存儲庫上提交 GitHub 問題。
最后,致謝
我要感謝我的同事@shiminxu 為這個項目做出的貢獻。還要感謝 .NET 團隊的 @ericstj 和 Roslyn 團隊的 @tmat 提供的技術指導。最后,感謝無數為實現這一改變做出的貢獻的人。
@shiminxu
https://github.com/jianying10202713
@ericstj
https://github.com/ericstj
@tmat
https://github.com/tmat
當前互聯網信息爆炸的時代,高效準確地抓取和處理網頁內容成為眾多領域必備技能。網絡爬蟲技術搭配強大的HTML解析庫HtmlAgilityPack,能夠自動化抽取并結構化分析網頁數據。本篇我將帶領您深入了解如何使用C#編程語言結合HtmlAgilityPack,從HTML文檔中快速且準確地獲取文章標題。
假設我們通過網絡爬蟲程序成功獲取了一個網頁的HTML源代碼:
string htmlContent = "<html><head><title>這是文章標題</title></head><body>介紹了主題和內容...</body></html>";
為了從這段HTML文本中定位并提取出文章標題,我們可以編寫如下的C#方法:
// 定義一個靜態方法用于從HTML字符串中提取文章標題
public static string ExtractTitle(string htmlContent)
{
// 創建一個HtmlDocument對象以解析加載的HTML內容
var htmlDocument = new HtmlDocument();
htmlDocument.LoadHtml(htmlContent);
// 使用XPath表達式精確匹配<head>標簽內的<title>標簽節點
var titleNode = htmlDocument.DocumentNode.SelectSingleNode("//head/title");
// 如果找到了<title>節點,則返回其內部文本內容,并進行trim操作去除多余空白;否則返回提示信息
return titleNode != null ? titleNode.InnerText.Trim() : "未找到頁面標題";
}
接下來,在主程序入口處調用此方法并顯示提取結果:
static void Main(string[] args)
{
// 假設此處的htmlContent是從網絡爬蟲抓取的實際HTML數據
string pageTitle = ExtractTitle(htmlContent);
// 輸出提取得到的文章標題
Console.WriteLine($"文章標題: {pageTitle}");
Console.WriteLine(new string('\n', 100)); // 輸出多行空白以便區分輸出內容
}
可以看到這個代碼展示了C#與HtmlAgilityPack在HTML內容解析方面的強大能力,不僅能準確無誤地從HTML文檔中摘取文章標題,而且具備極高的靈活性和擴展性。通過調整或增強XPath表達式,您可以輕松定位并抽取其他多種關鍵元素,比如正文、作者信息、發布日期等。面對日益復雜的數據抓取需求,這一技術手段將成為您的得力助手,助您從浩瀚的網頁信息海洋中篩選出有價值的精華內容。不斷實踐和優化此類解決方案,我們將在大數據時代更好地駕馭和利用網絡信息資源,實現更深層次的信息挖掘與應用。
朋友們,如果你們覺得這個案例有用,那就盡情地使用它吧!別忘了關注我,我是代碼領域的詩人XY,一個樂于分享的人。我熱愛與大家分享我的知識和經驗,幫助你們解決問題,激發你們的思考。我深信,只有通過分享和交流,我們才能不斷進步,不斷創新。若你對更多相關話題感興趣,或者在學習工作中遇到難題,請隨時留言給我,我會為你撰寫更多相關內容,并盡我所能幫助你解決難題。
在網頁中,有時需要為文字設置粗體、斜體或下劃線效果,這時就需要用到HTML中的文本格式化標簽,使文字以特殊的方式顯示。
b i s u 只有使用 沒有 強調的意思 , strong em del ins 語義更強烈
HTML中還有一種特殊的標簽——注釋標簽。如果需要在HTML文檔中添加一些便于閱讀和理解但又不需要顯示在頁面中的注釋文字,就需要使用注釋標簽。
<!-- 注釋語句 -->
*請認真填寫需求信息,我們會在24小時內與您取得聯系。