Reputation: 15860
I'm trying to develop a C# program that would get a list of available Windows Updates and look up KB articles to retrieve titles of each update. (Otherwise, they all look like cryptic "Update for Windows Server (KBxxxxx)")
I tried retrieving the HTML of each KB article but the title is not present in the HTML (I'm guessing they're using angular to build the page)
Here's an example: https://support.microsoft.com/en-us/kb/3102429 The title of the article as shown in the browser does not appear anywhere in the HTML when I view source
Is there a good way to do this?
Upvotes: 8
Views: 3523
Reputation: 3596
As Specified by canon in the comments of Aybe's answer, the KB pages load source via script once the page is loaded, so you cannot easily get this programmatically.
You can, however use the API link directly at e.g. https://support.microsoft.com/app/content/api/content/help/en-us/4034733
Upvotes: 0
Reputation: 21
I discovered that they are now putting some prefetch script into the initial payload that contains some useful json. (Actually : this is the json mentioned by b.mcewan in the currently accepted answer).
Since I have this all ready for consumption.... Here is a link to some code that will gather your machine's installed hotfixes and present some detail including the KB title.
Code will run in LINQPad http://share.linqpad.net/l6tdxc.linq
In case you do not use LP here are the routines. ParseTitle makes use of some autogenerated classes to deserialize the json. You will need to remove the .Dump() extension method calls and Hyperlinq class reference and present the data some other way. (EDIT: more than just the KB article Title is exposed by the ArticleInfo class.... like the details about what the hotfix does, how to get it and install it etc.)
void Main()
{
const string query = "SELECT HotFixID, InstalledOn, InstalledBy, Description, Caption, * FROM Win32_QuickFixEngineering";
var result =
(from ManagementObject quickfix in new ManagementObjectSearcher(query).Get() //.AsParallel()
orderby Convert.ToDateTime(quickfix["InstalledOn"]) descending
let web = new WebClient()
let input = quickfix["Caption"].ToString()
let id = input.Substring(35, input.Length - 35)
let url = $"{input.Replace("microsoft.com/?kbid=", "microsoft.com/en-us/help/")}/kb{id}"
let html = web.DownloadString(url)
where string.IsNullOrEmpty( html ).Equals(false)
let kbInfo = ParseInfo( url, html )
where kbInfo != null
let pub = kbInfo.Details.PublishedOn
let title = kbInfo.Details.Title
let desc = Util.OnDemand( "More....", () =>
Util.RawHtml(string.Join(Environment.NewLine,
kbInfo.Details.Body
.Select(i => $"<span class=typeglyphx>{i.Title}</span>{i.Content.Single()}")))
)
select
new
{
HotFixID = Util.RawHtml($"<span class=typeglyphx>{quickfix["HotFixID"].ToString()}</span>"),
Published = pub.Date,
InstalledOn = quickfix["InstalledOn"].ToString(),
InstallDelay = $"{Convert.ToInt16((Convert.ToDateTime(quickfix["InstalledOn"].ToString()).Date - pub.Date).TotalDays)} days",
InstalledBy = quickfix["InstalledBy"].ToString(),
Description = new Hyperlinq(quickfix["Description"].ToString()),
Title = Util.RawHtml($"<span class=typeglyphx>{title}</span>") ?? $"{url} [Could not obtain KB title]",
Body = desc,
Link = new Hyperlinq(url),
}
).Dump(1);
}
#nullable enable
string? ParseTitle ( string html )
{
var doc = new HtmlDocument();
doc.LoadHtml(html);
var meta = doc.DocumentNode
.SelectNodes("//script");
var searchToken = "microsoft.support.prefetchedArticle = (function() ";
var nuggets = meta
.Where(i => i.OuterHtml.Contains(searchToken))
.Select(i => i.OuterHtml)
.Single();
var start = nuggets.IndexOf(":") + 1;
var length = nuggets.Length - start - 28;
var json = nuggets.Substring(start, length);
string? ret = null;
try
{
var articleInfo = MSKBPreFetched.ArticleInfo.FromJson(json);
ret = articleInfo.Details.Title;
}
catch{ json.DumpTrace("could not deserialize the json for this article"); // LP only}
return ret;
}
#nullable disable
// <auto-generated />
// json2csharp
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using MSKBPreFetched;
//
// var articleInfo = ArticleInfo.FromJson(jsonString);
namespace MSKBPreFetched
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public partial class ArticleInfo
{
[JsonProperty("sideNav")]
//[JsonConverter(typeof(ParseStringConverter))]
public string SideNav { get; set; }
[JsonProperty("details")]
public Details Details { get; set; }
[JsonProperty("_ts")]
public long Ts { get; set; }
}
public partial class Details
{
[JsonProperty("subType")]
public string SubType { get; set; }
[JsonProperty("heading")]
public string Heading { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("body")]
public List<Body> Body { get; set; }
[JsonProperty("urltitle")]
public string Urltitle { get; set; }
[JsonProperty("keywords")]
public List<string> Keywords { get; set; }
[JsonProperty("keywordsLower")]
public List<string> KeywordsLower { get; set; }
[JsonProperty("os")]
public List<object> Os { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id")]
[JsonConverter(typeof(ParseStringConverter))]
public long Id { get; set; }
[JsonProperty("locale")]
public string Locale { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("titleLower")]
public string TitleLower { get; set; }
[JsonProperty("published")]
public bool Published { get; set; }
[JsonProperty("createdOn")]
public DateTimeOffset CreatedOn { get; set; }
[JsonProperty("publishedOn")]
public DateTimeOffset PublishedOn { get; set; }
[JsonProperty("version")]
public long Version { get; set; }
[JsonProperty("eolProject")]
public string EolProject { get; set; }
[JsonProperty("supportAreaPaths")]
public List<Guid> SupportAreaPaths { get; set; }
[JsonProperty("supportAreaPathNodes")]
public List<PrimarySupportAreaPath> SupportAreaPathNodes { get; set; }
[JsonProperty("disableVAPopup")]
public bool DisableVaPopup { get; set; }
[JsonProperty("primarySupportAreaPath")]
public List<PrimarySupportAreaPath> PrimarySupportAreaPath { get; set; }
[JsonProperty("isContentLocaleFallback")]
public bool IsContentLocaleFallback { get; set; }
[JsonProperty("contentLocale")]
public string ContentLocale { get; set; }
}
public partial class Body
{
[JsonProperty("meta")]
public Meta Meta { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("content")]
public List<string> Content { get; set; }
}
public partial class Meta
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("products")]
public List<object> Products { get; set; }
[JsonProperty("supportAreaPaths")]
public List<object> SupportAreaPaths { get; set; }
[JsonProperty("isInternalContent")]
public bool IsInternalContent { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
}
public partial class PrimarySupportAreaPath
{
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("parent", NullValueHandling = NullValueHandling.Ignore)]
public Guid? Parent { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("tree")]
public List<object> Tree { get; set; }
}
public partial class ArticleInfo
{
public static ArticleInfo FromJson(string json) => JsonConvert.DeserializeObject<ArticleInfo>(json, MSKBPreFetched.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this ArticleInfo self) => JsonConvert.SerializeObject(self, MSKBPreFetched.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
internal class ParseStringConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var value = serializer.Deserialize<string>(reader);
long l;
if (Int64.TryParse(value, out l))
{
return l;
}
throw new Exception("Cannot unmarshal type long");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
if (untypedValue == null)
{
serializer.Serialize(writer, null);
return;
}
var value = (long)untypedValue;
serializer.Serialize(writer, value.ToString());
return;
}
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
}
}
Upvotes: 1
Reputation: 142
For hotfixes released after August 2017, the new API link appears to be https://support.microsoft.com/app/content/api/content/help/en-us/4034733.
For hotfixes released after February 2017, the new API link appears to be https://support.microsoft.com/api/content/help/3115489.
The data on that page is JSON:
If you load that JSON data using Python, e.g., then you can find the title and other useful information under "details". In particular,
d["details"]["id"] == u'3115489'
d["details"]["title"] == u'February 7, 2017, update for Office 2013 (KB3115489)'
d["details"]["publishedOn"] == u'2017-02-07T17:05:19.000368Z'
Just for reference, when loading the URL https://support.microsoft.com/kb/3115489 in Chrome with Developer Tools running, the network activity shows an XHR transfer from api/content/help:
Upvotes: 7
Reputation: 16662
If somehow you can fetch the KB number out of a Windows Update, then the article should be accessible at the following URL:
https://support.microsoft.com/en-us/kb/YOUR_KB_NUMBER
And the id="mt5"
seems like being the title.
EDIT:
My bad, the id
do change in fact, the first child of <section>
with class="section kb-article spacer-84-top"
is the title, however this might change ... (take it as it is :)
Upvotes: 0