ウェブスクレイピングという技術があるそうです.ウェブサイトから情報を抽出する技術のこと.TeraTailにVBAからMSXMLを使用してHTMLをパースし、XPathで必要な情報を取得したいという趣旨の投稿があありました.そもそもMSXMLでは入力がXHTMLでない限り(つまりHTMLをXMLとした扱えない限り)MSXMLだと無理ですと書いたのですが、まあ否定するだけではソリューションになりません.WEBを当たってみると.NETのDLLをCOM化する、つまりVBAからも扱えるようにするという話は結構掲載されています.ならばVB.NET + XPathでHTMLを検索できるのか試してみました.
ともかくVisual Studioを立ち上げるのは年に1回あるかないかです.恥ずかしながらすべてを忘れてしまっています.それでも昔作ったVBのプログラムを参考にしながら、WEBを調べるとHTML Agility Pack(
https://www.nuget.org/packages/HtmlAgilityPack)というので、HTMLをパースできるところはわかりました.これを使って気象庁が出しているその日の気象情報から、地点と最高気温を抜き出してXMLに落とすプログラムを構想してみました.
簡単には、該当のテーブルを選んで、見出し行を除いてtrを取得し、0番目と6番目のセルの値を取得するだけです.
ところが結果は全然うまくいってくれません.HTMLを読めることは読めているようなのですが、まともにDOMに落ちていないらしく、XMLに書き込むと文字は化けるは、最高気温もデタラメと散々でした.
そこで別の方法はないものかと探したのですが、.NETのライブラリに同様にSGMLをXMLに落としてくれるオープンソースがありました.
SgmlReader - Convert (almost) any HTML to valid XML
こちらを使ってみると、なんの苦も無くほぼ一発でXMLに落とせました.そうです、HTMLというのはそもそも由緒正しきSGMLアプリケーションなので、SGMLのリーダーがあれば読めるのです.プログラムはこんな具合です.
[SgmlReaderを使ったプログラム]
Imports System.Xml
Imports System.Xml.XPath
Imports System.Text.Encoding
Imports Sgml
Module HtmlXPathModule
Sub Main()
Dim xw As XmlWriter = CreateXmlWriter("maxTemp.xml")
xw.WriteStartElement("tempDataRoot", "")
Dim sgml As SgmlReader = New SgmlReader()
sgml.DocType = "HTML"
sgml.IgnoreDtd = True
Dim htmlDoc As XDocument = XDocument.Load(sgml)
Dim nsTable As NameTable = New NameTable
Dim nsMgr As XmlNamespaceManager = New XmlNamespaceManager(nsTable)
Dim targetTrs As IEnumerable(Of XElement) = htmlDoc.XPathSelectElements("//xhtml:table[@class = 'o1']//xhtml:tr[@class != 'o1h']", nsMgr)
For Each tr As XElement In targetTrs
Dim targetTds As IEnumerable(Of XElement) = tr.Elements
Dim tdArray As XElement() = targetTds.ToArray
Dim region As XElement = tdArray(0)
Dim maxTemp As XElement = tdArray(6)
xw.WriteStartElement("tempData")
xw.WriteAttributeString("region", "", region.Value)
xw.WriteAttributeString("maxTemp", "", maxTemp.Value)
xw.WriteEndElement()
Next
xw.WriteEndElement()
xw.Close()
End Sub
Function CreateXmlWriter(outputPath As String) As XmlWriter
Dim settings As XmlWriterSettings = New XmlWriterSettings()
settings.CloseOutput = True
settings.ConformanceLevel = ConformanceLevel.Document
settings.Encoding = UTF8
settings.Indent = False
settings.NewLineChars = vbCrLf
settings.NewLineHandling = NewLineHandling.None
settings.OmitXmlDeclaration = False
settings.WriteEndDocumentOnClose = False
Dim xw As XmlWriter = XmlWriter.Create(outputPath, settings)
Return xw
End Function
End Module
[生成されたmaxTemp.xml(抜粋:最高気温の右の"]"は元からついています)]
<?xml version="1.0" encoding="UTF-8"?>
<tempDataRoot>
<tempData maxTemp="27.1]" region="札幌"/>
<tempData maxTemp="26.9]" region="稚内"/>
<tempData maxTemp="26.0]" region="北見枝幸"/>
<tempData maxTemp="28.0]" region="旭川"/>
...
<tempData maxTemp="37.1]" region="鹿児島"/>
...
<tempData maxTemp="33.0]" region="名護"/>
<tempData maxTemp="32.7]" region="久米島"/>
<tempData maxTemp="32.3]" region="南大東島"/>
<tempData maxTemp="31.8]" region="宮古島"/>
<tempData maxTemp="32.6]" region="与那国島"/>
<tempData maxTemp="31.3]" region="西表島"/>
<tempData maxTemp="33.1]" region="石垣島"/>
<tempData maxTemp="" region="昭和"/>
</tempDataRoot>
この日は暑くて鹿児島は37℃を越えています.我が家も冷房がないので、このプログラムを作るのも汗だくです.
あともう一度トライしてみましたが、HTML Agility Packでも同じように動かすことができました.どうもエンコーディングがシビアらしいです.(このサイトでは)UTF-8を明示的に指定して、HTMLをいったんstringに落としてそこからパースすると動いてくれました.
[XML Agility Packを使ったプログラム]
Imports System.Xml
Imports System.Text.Encoding
Imports System.Net
Imports HtmlAgilityPack
Module HtmlAgilityTestModule
Sub Main()
Dim xw As XmlWriter = CreateXmlWriter("maxTemp.xml")
xw.WriteStartElement("tempDataRoot", "")
Dim wc As WebClient = New WebClient()
wc.Encoding = UTF8
Dim doc As HtmlDocument = New HtmlDocument()
doc.LoadHtml(htmlSource)
Dim trs = doc.DocumentNode.SelectNodes("//table[@class = 'o1']//tr[@class != 'o1h']")
For Each tr In trs
Dim tds As IEnumerable(Of HtmlNode) = tr.Elements("td")
Dim tdArray As HtmlNode() = tds.ToArray
Dim regionTd As HtmlNode = tdArray(0)
Dim maxTempTd As HtmlNode = tdArray(6)
xw.WriteStartElement("tempData")
xw.WriteAttributeString("region", regionTd.InnerText)
xw.WriteAttributeString("maxTemp", maxTempTd.InnerText)
Debug.WriteLine("region={0} max-temp={1}", regionTd.InnerText, maxTempTd.InnerText)
xw.WriteEndElement()
Next
xw.WriteEndElement()
xw.Close()
End Sub
あと、同じことをXPathでなくLINQ とXMLでやったらどうか?ずっと疑問に思っていました.こちらも案ずるより産むが安しでStackOverflowに質問したらすぐ回答をいただけました.LINQもなかなか強力ですね.
このXPathはLINQ to XMLならどう書くのでしょうか?
という訳でお盆は暑かったのですが結構勉強になりました.普段XSLTやDITAやJavaばっかりなんですが、たまにはVisual Studioも動かすべきですね.