« Microsoft Wordの変更履歴の記録に使うXML要素 | トップページ | git の「ワークツリー」という用語 »

2024年9月 7日 (土)

AngleSharpでHTMLファイルを更新する

AngleSharpというと、皆さんウェブサイトのスクレイピングを目的に、専らHTMLの解析と情報の取り出しに使っているようだが、実はHTMLの書き換えや新規作成にも使える。そして、そういう使い方でも、かなり便利だ。

ということで、AngleSharpを、ローカルファイルのHTMLの更新に使う話。(AngleSharpでスクレイピングするやり方は、ウェブに説明が多数あるので、それと重複することには触れない。)

SgmlReaderとの比較

AngleSharpを紹介する記事 (のうち、比較的初期のもの) はたいてい「C#でスクレイピングをしようと思ったら、昔はSgmlReaderが定番だった」みたいに書いている。SgmlReaderはDTDを読んでそれに基づいてSGML文書を解析する機能を持っているので、HTML以外にも「XMLじゃないSGML」を何でも解析できるのが売りだったのだが、実際にはHTMLの解析にばかり使われていたようだ。

そのSgmlReaderとAngleSharp (のHtmlParser) を、HTMLファイルを読み込んで修正して (一部を書き換えて) 保存する、という使い方に関して比較すると、こんな感じだと思う。

  SgmlReader AngleSharp
入力 TextReader、string Stream、string、char[]、ReadOnlyMemory<char>
出力 Stream、TextWriter、ファイル、string TextWriter、string
オブジェクトモデル・要素 全ての要素はXElement型という同じ型になっている。個々の要素の区別はNameプロパティを文字列として参照。(Linq to XMLの仕様) それぞれの要素が専用の型になっている。IHtmlElementという共通のインターフェースもあり、要素を区別しないで扱うことも可能。
オブジェクトモデル・属性 属性を表すXattribute型があり、全ての属性は同じ型。個々の属性の区別はNameプロパティを文字列として参照。属性値は全てstring型 (整数などへの変換は容易)。(Linq to XMLの仕様) 各要素の型に、要素の属性に対応したプロパティがある。各プロパティの型は許される属性値を意識していて、stringの他にintだったりboolだったりする。ITokenList型なんてのもある。
他方、IElementには、属性名も属性値も文字列として操作するメソッドもあり、Linq to XMLのようなダックタイピング的な操作も可能。
要素ごとの型のプロパティで属性値を操作する場合と、IElementとして名前で属性を操作する場合とは連動していて、「両者を混ぜて使っていたら値がずれてしまった」みたいなことにはならない。
ドキュメント Readmeがあるだけだが、機能が小さい上に、基本的にはXmlReaderと同じAPIを提供するだけなので、これで十分使える。 書きかけの(?)ドキュメントが10ページほどあるが、AngleSharpは巨大なライブラリー (30個以上のパッケージに分かれていて、パブリックな型だけで400以上ある) なので全然足りない。ソースにはdocumentation commentが書いてあるが、当たり前のことを機械的に書いた印象が強く、Intellisensも構文以上のことはほとんど教えてくれない。なので、使おうと思ったら、ソースを調べるか、ウェブを検索しまくるしかない。
開発チームの誰かが本を書いて売る、みたいな話もあるようだが、今のところ実現していないようだ。

AngleSharpの方がずっといいよね、一見して明らか、みたいな表になる予定だったのに、そうなってない。おかしいな…。

入力のエンコーディング

これはスクレイピングにも関係するので分かっている人が多いと思うが、SgmlReaderの入力はTextReaderか文字列かの二択。ファイルやネットワークから入手したバイト列であるStreamを直接扱うことはできない。つまり、エンコーディングは、SgmlReaderを使う以前に、アプリ側で判定してStreamReaderなりを呼び出す際に指定する必要がある。これは、一般的なSGMLを処理するのであれば、こうならざるを得ない。

ところが、実際のHTMLでは、エンコーディングはHTML文書中にmetaタグを使って書いてある。HTMLをパースした後でなければエンコーデイングが分からない。スクレイピングであれば、HTTP応答を見ることも可能だが、ローカルファイルを読むときにはContent-Typeとかcharsetとかを教えてもらうことはできない。また、特定サイトをスクレイピングするのであれば、あらかじめ使われているエンコーディングを調べてハードコードしてもいいし、2024年時点ではHTMLはUTF-8で作るのが常識だからUTF-8決め打ちで問題ないという考え方もあるだろう。でも、HTMLを扱うローカルツールとしては、UTF-8専用は気持ちが悪い。

SgmlReaderとは違い、AngleSharpは、入力をStreamで渡せば、metaに書いてあるエンコーデイングに従ってよろしく処理してくれる。素晴らしい。

ただし、AngleSharpでも出力はTextWriterなので、アプリ側でエンコーディングを指定してStreamWriterなどを用意しておく必要がある。書き出すときには解析済みのHTMLがあるので、metaを参照するのは簡単だが手間ではある。この手間をはぶくと、.NETのStreamWriterはデフォルトでUTF-8なので、元のHTMLがUTF-8以外のcharsetを使っているとおかしなことになってしまうので注意が必要。

出力のシリアライズ

SgmlReaderを使う場合、更新したHTMLを書き出すのは難しい。(SgmlReaderという名前なので、この点を責めるのはどうかとも思うが。)

SgmlReaderには対になるSgmlWriterのようなものは存在しない。なので、SgmlReaderで読んだHTMLを普通に書き出そうと思うと、XElement.Saveを使ったり、XElement.ToStringしてからFile.WriteAllTextを使ったりすることになると思うが、これは内部的に、XmlWriterというXMLをシリアライズする機能が使われるので、正しいHTMLにならない (*1)。

<img src="...">」が「<img src="..." />」になるのはxhtmlでもそうなので許してもらえることが多いと思うが、空のscript要素、つまり「<script src="..."></script>」が「<script src="..." />」になってしまうのは非常に困る。

というわけで、書き出したい場合は、SgmlReaderは適さない。

AngleSharpは、もともとHTML用で、自分自身の出力機能を備えているので、問題なく書き出せる。

更新方法と書き出し方

  • ドキュメントの更新は、要素に関しては、いわゆる普通のDOMのAPIがそろっており、clone、add、append、remove、replaceといったメソッドで操作できる。拡張メソッドだがInsertAfterとかInsertBeforeなどもある。
  • 新しい要素の作成は、IDocumentのCreateElementを使う。(IDocumentは、IHtmlDocumentの親インターフェースの一つ。)
  • ただし、要素をいくつも作って組み立てるのは面倒なので、パターンが決まっているなら、IElementのInnerHtmlプロパティを書き換えるのが簡単。これはstring型で、JavaScriptのInnerHTMLと同じように機能する。
  • 既存の要素の属性値を書き換えるのは、対応するプロパティに値をセットするだけでいい。
  • 属性自体を削除したい場合は、名前を指定してIElement.RemoveAttribute(string)を呼ぶ。

更新が終わって書き出すときは、

  • IHtmlDocument html のときに、html.ToHtml() とするとHTML文書全体が一つの文字列になる。
  • また、TextWriter writer のときに、html.ToHtml(writer) とすると、ファイル等に書き出せる。

*1 実は、XmlWriterのインスタンスを作るときに使うXmlReaderSettingsというクラスにはXmlOutputMethodというプロパティがあり、値としてXmlOutputMethod.Htmlを取ることができる。マイクロソフトによる、このプロパティの説明には、こう書いてある:

出力は、HTMLの規則やXML 1.0の規則などによってシリアライズできます。
この設定は、XSLTプロセッサが設定したり、Visual Studioが内部的に使用したりします。

なるほど、これはXSLTの <xsl:output method="html" ... /> を実現するためのものなのね、これでいいじゃん、と思って、プロパティを設定しようとすると、このプロパティは読み取り専用 (getのみでsetがない) ということに気づいて驚く。実際には、セッターが存在しないのではなくて、internal なだけなので、リフレクションを使えば好きな値を設定できる。そうやってXmlWriter.Createで取得したインスタンスは、正しいHTMLを作ることができる。

できるのだが、これはかなり「ウラワザ」的なので、お勧めできない。どうしてもやりたければ、リフレクションに頼るのではなく、XslTransformを使った方がいいように思う。大げさになってしまうが、正式な公開APIだけでHTML出力を実現できる。(XSLTプロセッサが作ったHTML出力のXmlWriterが動くわけだ。たぶん。)

« Microsoft Wordの変更履歴の記録に使うXML要素 | トップページ | git の「ワークツリー」という用語 »

コメント

コメントを書く

(ウェブ上には掲載しません)

« Microsoft Wordの変更履歴の記録に使うXML要素 | トップページ | git の「ワークツリー」という用語 »