「累積更新プログラム」とは、その名の通りOSを最新の状態に保つための更新プログラムが累積されたものであり、毎月1回以上配信されます。
Windows Updateから更新を適用することもできますが、社内のPC管理をされている方であれば「Microsoft Update カタログ」から必要なファイルだけダウンロードされている方も多いのではないでしょうか。
しかし、このカタログからのダウンロード作業は
“地味に面倒くさい!”
です。
サイト(http://www.catalog.update.microsoft.com/)にアクセスして、KB番号や「累積 更新 Windows-10」といったキーワードで検索して、ヒットしたファイルをクリックしてダウンロードして・・・。
毎日行う作業ではないですが、ホント地味ーーーに手間が掛かる作業です。
そこで今回は、自分が楽するために、Microsoft Update カタログから自動的に累積更新プログラムをダウンロードするスクリプトを作成してみました。
Microsoft Updateカタログの仕様を調べる
ダウンロードする方法として、まずブラウザーの操作が考えられますが、動作が重くなるのであまり使いたくはありません。
できれば更新プログラム(msu)のURLを調べて、ブラウザーを介さずにファイルをダウンロードしたいものです。
そこで、まずは手動でダウンロードする際のMicrosoft Updateカタログの仕様を調べてみることにしました。
「ダウンロード」ボタンを押したときの挙動
Updateカタログの検索結果から「ダウンロード」ボタンをクリックすると、ファイルのダウンロードURLが記載された「DownloadDialog.aspx」ページが開きます。
このときの挙動を「Fiddler」で追ってみました(下記は必要なところだけ抜粋)。
POST http://www.catalog.update.microsoft.com/DownloadDialog.aspx HTTP/1.1 Content-Type: application/x-www-form-urlencoded updateIDs=%5B%7B%22size%22%3A0%2C%22languages%22%3A%22%22%2C%22uidInfo%22%3A%22c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e%22%2C%22updateID%22%3A%22c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e%22%7D%5D&updateIDsBlockedForImport=&wsusApiPresent=&contentImport=&sku=&serverName=&ssl=&portNumber=&version=
POSTされたデータをデコードすると下記のようになります。
updateIDs=[{"size":0,"languages":"","uidInfo":"c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e","updateID":"c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e"}]&updateIDsBlockedForImport=&wsusApiPresent=&contentImport=&sku=&serverName=&ssl=&portNumber=&version=
これを見ると、重要なのは「updateIDs」パラメーターで、「updateID」という、それらしい値をJSON形式で渡しているようです。
[{ "size": 0, "languages": "", "uidInfo": "c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e", "updateID": "c51e9c8d-8757-49bd-a65f-c1fa9ebc2c6e" }]
この「updateID」さえ分かれば何とかなりそうな気がしてきました。
updateIDの取得
ソースを調べると、リンクのonclick属性の値の中にIDが記載されていました。
ただ、Webサイト = htmlから値を持ってくるのは面倒くさそうです。
そこで目を付けたのが検索結果の「RSSフィード」、XMLに値が記載されていれば、htmlの解析をするより楽に値を取得できます。
ところが、RSSフィードのリンクをクリックすると、見事にエラー発生!
世の中そう思い通りには動いてくれません・・・。
諦めきれないので、ブラウザーを替えInternet Explorerで開いてみると、
今度は問題なく表示できました。
updateIDもちゃんとguid要素の値として記載されています。
ここまでくれば、下記のような流れで処理できそうです。
- Microsoft Updateカタログで「累積 更新 Windows-10」キーワード検索した結果のRSSフィードを取得(Internet Explorer)
- 1.のRSSフィードから、対象となる更新プログラムのupdateIDを取得
- ダウンロードページに対してupdateIDを含めたパラメーターをPOST
- 結果として表示されるWebページからファイルのダウンロードURLを取得
Microsoft Update カタログから累積更新プログラムをダウンロードするVBScript
この流れを実装したのが下記のコードです。
すべての更新プログラムをダウンロードするわけにはいかないので、プラットフォームやOSのバージョン、日付で絞り込むようにしています。
日付はRSSフィードのpubDate要素でざっくりと年月を判断しているだけなので、指定月以外の更新プログラムがダウンロードされる場合がありますが、大まかには上手く動いていると思います。
'************************************************************************* ' Microsoft Update カタログから更新プログラムをダウンロードするスクリプト ' ' 2019/6/14 @kinuasa '************************************************************************* Option Explicit Call DownloadWindowsUpdatesFromCatalog() Wscript.Echo "処理が完了しました。" Private Sub DownloadWindowsUpdatesFromCatalog() 'Microsoft Update カタログから更新プログラムをダウンロード Dim fso, fol Dim nodes, node Dim res Dim yyyy, mm Dim update_title, update_id, update_url, update_filename Dim parent_folderpath, dl_folderpath, dl_filepath Dim pub_date, guid Dim flg Dim v Dim i, cnt '[累積 更新 Windows-10]を検索キーワードとしたフィードURL Const url = "http://catalog.update.microsoft.com/v7/site/Rss.aspx?q=%e7%b4%af%e7%a9%8d+%e6%9b%b4%e6%96%b0+Windows-10&lang=ja" 'title絞り込み用のキーワード(Or条件) Const keyword = "1803,1809,1903" '対象プラットフォーム Const platform = "x64" 'フォルダ設定 Set fol = CreateObject("Shell.Application") _ .BrowseForFolder(0, "ダウンロード先フォルダ選択", &H10, 0) If fol Is Nothing Then Exit Sub parent_folderpath = fol.Self.Path Set fso = CreateObject("Scripting.FileSystemObject") '年月設定 yyyy = InputBox("対象年を入力してください。", , Year(Now())) mm = InputBox("対象月を入力してください。", , Right("0" & Month(Now()), 2)) 'RSSフィード取得 With CreateObject("WinHttp.WinHttpRequest.5.1") .Open "GET", url, False .setRequestHeader "User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" .Send Select Case .Status Case 200 res = .responseText Case Else Wscript.Echo "!!RSSフィード取得失敗!! ステータスコード:" & .Status Exit Sub End Select End With cnt = 0 v = Split(keyword, ",") With CreateObject("MSXML2.DOMDocument") .async = False If .LoadXML(res) = True Then Set nodes = .SelectNodes("//item") If nodes.Length > 0 Then For Each node In nodes flg = False update_title = node.SelectSingleNode("title").Text pub_date = node.SelectSingleNode("pubDate").Text '日付(pubDate)とプラットフォーム(title)で絞り込み If (InStr(pub_date, yyyy & "-" & mm) > 0) And (InStr(update_title, platform) > 0) Then 'キーワードによる絞り込み(title) For i = LBound(v) To UBound(v) If InStr(update_title, v(i)) > 0 Then flg = True: Exit For Next End If If flg = True Then If cnt > 0 Then WScript.Sleep 10000 '負荷を考えて10秒処理待ち guid = node.SelectSingleNode("guid").Text update_id = GetUpdateID(guid) update_url = GetDownloadUrl(update_id) update_filename = GetDownloadFileName(update_url) 'ダウンロード先フォルダ作成 dl_folderpath = fso.BuildPath(parent_folderpath, update_title) If fso.FolderExists(dl_folderpath) = False Then fso.CreateFolder dl_folderpath 'ダウンロード実行 dl_filepath = fso.BuildPath(dl_folderpath, update_filename) Wscript.Echo "[ダウンロード中]" & update_title & ", " & update_url & ", " & dl_filepath DownloadFile update_url, dl_filepath cnt = cnt + 1 End If Next Wscript.Echo "----- " & cnt & "ファイルダウンロード完了 -----" End If Else Wscript.Echo "!!RSSフィード読込失敗!!" End If End With End Sub Private Sub DownloadFile(ByVal url, ByVal save_filepath) 'XMLHTTPRequest + ADODB.Streamでファイルをダウンロード Dim req Const adTypeBinary = 1 Const adSaveCreateOverWrite = 2 Set req = CreateObject("Msxml2.XMLHTTP") req.Open "GET", url, False 'キャッシュ対策 'http://vird2002.s8.xrea.com/javascript/XMLHttpRequest.html#XMLHttpRequest_Cache-Control 'http://www.atmarkit.co.jp/ait/articles/0305/10/news002.html 参考 req.setRequestHeader "Pragma", "no-cache" req.setRequestHeader "Cache-Control", "no-cache" req.setRequestHeader "If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT" req.Send Select Case req.Status Case 200 With CreateObject("ADODB.Stream") .Type = adTypeBinary .Open .Write req.responseBody .SaveToFile save_filepath, adSaveCreateOverWrite .Close End With Case Else Wscript.Echo "!!ダウンロード失敗!! ステータスコード:" & .Status Exit Sub End Select End Sub Private Function GetUpdateID(ByVal guid) 'アップデートID取得 GetUpdateID = Left(guid, InStr(guid, "#") - 1) End Function Private Function GetDownloadUrl(ByVal update_id) 'ダウンロードURL取得 Dim dat Dim html Dim matches Dim match Dim ret Const url = "http://www.catalog.update.microsoft.com/DownloadDialog.aspx" dat = "updateIDs=%5B%7B%22size%22%3A0%2C%22languages%22%3A%22%22%2C%22uidInfo%22%3A%22" & update_id & _ "%22%2C%22updateID%22%3A%22" & update_id & "%22%7D%5D" With CreateObject("WinHttp.WinHttpRequest.5.1") .Open "POST", url, False .setRequestHeader "Content-Type", "application/x-www-form-urlencoded" .Send dat Select Case .Status Case 200: html = .responseText End Select End With '[downloadInformation[0].files[0].url =...]からURL取得 With CreateObject("VBScript.RegExp") .IgnoreCase = True .Global = True .Pattern = "downloadInformation.*url.*" If .Test(html) Then Set matches = .Execute(html) 'パターンをシンプルにして後から不要な文字を削除 match = matches(0).Value match = Replace(match, vbCrLf, "") match = Replace(match, vbLf, "") match = Replace(match, vbCr, "") match = Replace(match, ";", "") match = Replace(match, ",", "") match = Replace(match, "'", "") match = Replace(match, " ", "") ret = Mid(match, InStr(match, "=") + 1) End If End With GetDownloadUrl = ret End Function Private Function GetDownloadFileName(ByVal update_url) 'ダウンロード対象のファイル名取得 Dim v v = Split(update_url, "/") GetDownloadFileName = v(UBound(v)) End Function
これで面倒な定例作業が一つ自動化できました。
ちなみに、VBScriptにした理由は単に過去のコードを流用しただけなので、適当に変更するなりPowerShellに書き換えるなり、ご自由にお使いいただければと思います。
また、上記コードは、Microsoft Updateカタログの仕様変更に伴って動作しなくなる可能性がありますので、その点はご注意ください。
この記事へのコメントはありません。