はじめに
Web上のデータのほとんどはHTML形式で保存されています。そのため、C#アプリケーションでHTMLの構文解析ができたらと思うことも多いでしょうが、.NET FrameworkにはHTMLの構文解析を簡単に行うための方法がありません。その証拠に、どこのフォーラムでも、HTML構文の簡単な解析方法を知らないかというC#プログラマの質問をよく見かけます。
Microsoft .NET Frameworkでは、XMLが強力にサポートされています。XMLとHTMLは外見こそよく似ていますが、あまり互換性はありません。XMLとHTMLの大きな違いは次のとおりです。
- XMLには終了タグが必要である
- すべてのXML属性値は、一重引用符か二重引用符で完全に囲む必要がある
- XMLタグは正確にネストさせる必要がある
- XMLタグ名では大文字/小文字を区別する
- XMLでは属性を重複して指定できない
- XMLでは空の属性を指定できない
実際のコードでこれらの違いを確認しておきましょう。XMLでは、すべての開始タグに対して終了タグが必要になります。次のHTMLをXMLパーサで解析すると、問題が起こります。
<p>This is line 1<br> This is line 2</p>
これは多くの違いのうちのほんの1つにすぎません。もちろん、XMLとの互換性を重視した書き方もできます。たとえば、上のHTMLは次のようにも書けるでしょう。
<p>This is line 1<br/> This is line 2</p>
これを理解できないブラウザはありませんし、XMLパーサもこれなら理解できます。しかし、HTMLソースの書き方を解析側が制御することはできないので、これは有効な解決策にはなりません。必要なのは、どのソースからのHTMLでも処理できるプログラムです。そこで、このような条件を満たすHTMLパーサを自分で作成することにしました。以降では、このHTMLパーサの作成方法を示し、それを読者のアプリケーションでどう利用できるかを説明します。
HTMLパーサの作成
まず、HTMLパーサを構成する主コンポーネントを示し、最後に簡単な例を通じてその使い方をお見せしま す。今回のHTMLパーサは次の4つのクラスからできています。
Attribute
――HTMLタグの個々の属性を格納するクラスです。AttributeList
――個々のHTMLタグとその全属性を格納するクラスです。Parse
――テキスト解析の汎用ルーチンを含んでいるクラスです。ParseHTML
――インターフェイスとして使われるメインクラスです。解析したいテキストをこのParseHTML
クラスに渡します。
では、各クラスの機能とその使い方を見ていきましょう。まずはAttribute
クラスです。
Attributeクラス
Attribute
クラスは、個々のHTML属性を格納するのに使用されます。Attribute
クラスのソースコードをリスト1に示します。
using System; namespace HTML { /// <summary> /// Attribute holds one attribute, as is normally stored in an /// HTML or XML file. This includes a name, value and delimiter. /// This source code may be used freely under the /// Limited GNU Public License(LGPL). /// /// Written by Jeff Heaton (http://www.jeffheaton.com) /// </summary> public class Attribute: ICloneable { /// <summary> /// The name of this attribute /// </summary> private string m_name; /// <summary> /// The value of this attribute /// </summary> private string m_value; /// <summary> /// The delimiter for the value of this attribute(i.e. " or '). /// </summary> private char m_delim; /// <summary> /// Construct a new Attribute. The name, delim, and value /// properties can be specified here. /// </summary> /// <param name="name">The name of this attribute.</param> /// <param name="value">The value of this attribute.</param> /// <param name="delim">The delimiter character for the value. /// </param> public Attribute(string name,string value,char delim) { m_name = name; m_value = value; m_delim = delim; } /// <summary> /// The default constructor. Construct a blank attribute. /// </summary> public Attribute():this("","",(char)0) { } /// <summary> /// Construct an attribute without a delimiter. /// </summary> /// <param name="name">The name of this attribute.</param> /// <param name="value">The value of this attribute.</param> public Attribute(String name,String value):this(name,value, (char)0) { } /// <summary> /// The delimiter for this attribute. /// </summary> public char Delim { get { return m_delim; } set { m_delim = value; } } /// <summary> /// The name for this attribute. /// </summary> public string Name { get { return m_name; } set { m_name = value; } } /// <summary> /// The value for this attribute. /// </summary> public string Value { get { return m_value; } set { m_value = value; } } #region ICloneable Members public virtual object Clone() { return new Attribute(m_name,m_value,m_delim); } #endregion } }
HTMLタグの属性の例を次に示します。
<img src="picture.gif" alt="Some Picture">
このHTMLタグにはsrc
とalt
という2つの属性が含まれています。属性値はそれぞれ「picture.gif」と「Some Picture」です。
Attribute
クラスは、name
、value
、delim
という3つのプロパティから構成されています。name
プロパティは属性の名前を格納し、value
プロパティは属性の持つべき値を格納します。delim
プロパティは、値の区切りとして用いる文字を表すプロパティで、値の区切りに何を用いるかに応じて、引用符(")またはアポストロフィ(')を格納します(区切り文字を使用しない場合は何も格納しません)。
AttributeListクラス
1つのHTMLタグが複数の属性を含むことも珍しくありません。そのようなとき、属性のリストを格納する目的で用いられるのがAttributeList
クラスです。AttributeList
クラスのソースコードをリスト2に示します。
using System; using System.Collections; namespace HTML { /// <summary> /// The AttributeList class is used to store list of /// Attribute classes. /// This source code may be used freely under the /// Limited GNU Public License(LGPL). /// /// Written by Jeff Heaton (http://www.jeffheaton.com) /// </summary> /// public class AttributeList:Attribute { /// <summary> /// An internally used Vector. This vector contains /// the entire list of attributes. /// </summary> protected ArrayList m_list; /// <summary> /// Make an exact copy of this object using the cloneable /// interface. /// </summary> /// <returns>A new object that is a clone of the specified /// object.</returns> public override Object Clone() { AttributeList rtn = new AttributeList(); for ( int i=0;i<m_list.Count;i++ ) rtn.Add( (Attribute)this[i].Clone() ); return rtn; } /// <summary> /// Create a new, empty, attribute list. /// </summary> public AttributeList():base("","") { m_list = new ArrayList(); } /// <summary> /// Add the specified attribute to the list of attributes. /// </summary> /// <param name="a">An attribute to add to this /// AttributeList.</paramv public void Add(Attribute a) { m_list.Add(a); } /// <summary> /// Clear all attributes from this AttributeList and return /// it to a empty state. /// </summary> public void Clear() { m_list.Clear(); } /// <summary> /// Returns true of this AttributeList is empty, with no /// attributes. /// </summary> /// <returns>True if this AttributeList is empty, false /// otherwise.</returns> public bool IsEmpty() { return( m_list.Count<=0); } /// <summary> /// If there is already an attribute with the specified name, /// it will have its value changed to match the specified /// value. If there is no Attribute with the specified name, /// one will be created. This method is case-insensitive. /// </summary> /// <param name="name">The name of the Attribute to edit or /// create. Case-insensitive.</param> /// <param name="value">The value to be held in this /// attribute.</param> public void Set(string name,string value) { if ( name==null ) return; if ( value==null ) value=""; Attribute a = this[name]; if ( a==null ) { a = new Attribute(name,value); Add(a); } else a.Value = value; } /// <summary> /// How many attributes are in this AttributeList? /// </summary> public int Count { get { return m_list.Count; } } /// <summary> /// A list of the attributes in this AttributeList /// </summary> public ArrayList List { get { return m_list; } } /// <summary> /// Access the individual attributes /// </summary> public Attribute this[int index] { get { if ( index<m_list.Count ) return(Attribute)m_list[index]; else return null; } } /// <summary> /// Access the individual attributes by name. /// </summary> public Attribute this[string index] { get { int i=0; while ( this[i]!=null ) { if ( this[i].Name.ToLower().Equals( (index.ToLower()) )) return this[i]; i++; } return null; } } } }
AttributeList
クラスは、1つの名前といくつかの属性の集まりから構成されます。AttributeList
のname
プロパティに格納されている名前は、タグの名前を表します。パーサから返されるタグは、AttributeList
オブジェクトの形をとります。
AttributeList
クラスは、C#のインデックスを利用します。個々の属性へのアクセスには、数値インデックスと文字列インデックスの両方を使用できます。たとえば、theTag
というAttributeList
オブジェクトにsrc
属性が含まれている場合、そのsrc
属性にアクセスするには次の2種類の方法があります。
theTag[0] // assuming "src" were the first attribute theTag["src"]
どちらの方法を使っても、タグの属性にアクセスできます。
ParseクラスとParseHTMLクラス
HTMLの構文解析をするだけなら、Parse
クラスは忘れてかまいません。Parse
クラスはHTMLパーサの内部で使用され、属性/値ベースのファイル(HTML、SGML、XML、さらにはHTTPヘッダも含む)に対する低レベルのサポートを提供します。Parse
クラスのソースコードをリスト3に示します。
using System; namespace HTML { /// <summary> /// Base class for parsing tag based files, such as HTML, /// HTTP headers, or XML. /// /// This source code may be used freely under the /// Limited GNU Public License(LGPL). /// /// Written by Jeff Heaton (http://www.jeffheaton.com) /// </summary> public class Parse:AttributeList { /// <summary> /// The source text that is being parsed. /// </summary> private string m_source; /// <summary> /// The current position inside of the text that /// is being parsed. /// </summary> private int m_idx; /// <summary> /// The most recently parsed attribute delimiter. /// </summary> private char m_parseDelim; /// <summary> /// This most recently parsed attribute name. /// </summary> private string m_parseName; /// <summary> /// The most recently parsed attribute value. /// </summary> private string m_parseValue; /// <summary> /// The most recently parsed tag. /// </summary> public string m_tag; /// <summary> /// Determine if the specified character is whitespace or not. /// </summary> /// <param name="ch">A character to check</param> /// <returns>true if the character is whitespace</returns> public static bool IsWhiteSpace(char ch) { return( "\t\n\r ".IndexOf(ch) != -1 ); } /// <summary> /// Advance the index until past any whitespace. /// </summary> public void EatWhiteSpace() { while ( !Eof() ) { if ( !IsWhiteSpace(GetCurrentChar()) ) return; m_idx++; } } /// <summary> /// Determine if the end of the source text has been reached. /// </summary> /// <returns>True if the end of the source text has been /// reached.</returns> public bool Eof() { return(m_idx>=m_source.Length ); } /// <summary> /// Parse the attribute name. /// </summary> public void ParseAttributeName() { EatWhiteSpace(); // get attribute name while ( !Eof() ) { if ( IsWhiteSpace(GetCurrentChar()) || (GetCurrentChar()=='=') || (GetCurrentChar()=='>') ) break; m_parseName+=GetCurrentChar(); m_idx++; } EatWhiteSpace(); } /// <summary> /// Parse the attribute value /// </summary> public void ParseAttributeValue() { if ( m_parseDelim!=0 ) return; if ( GetCurrentChar()=='=' ) { m_idx++; EatWhiteSpace(); if ( (GetCurrentChar()=='\'') || (GetCurrentChar()=='\"') ) { m_parseDelim = GetCurrentChar(); m_idx++; while ( GetCurrentChar()!=m_parseDelim ) { m_parseValue+=GetCurrentChar(); m_idx++; } m_idx++; } else { while ( !Eof() && !IsWhiteSpace(GetCurrentChar()) && (GetCurrentChar()!='>') ) { m_parseValue+=GetCurrentChar(); m_idx++; } } EatWhiteSpace(); } } /// <summary> /// Add a parsed attribute to the collection. /// </summary> public void AddAttribute() { Attribute a = new Attribute(m_parseName, m_parseValue,m_parseDelim); Add(a); } /// <summary> /// Get the current character that is being parsed. /// </summary> /// <returns></returns> public char GetCurrentChar() { return GetCurrentChar(0); } /// <summary> /// Get a few characters ahead of the current character. /// </summary> /// <param name="peek">How many characters to peek ahead /// for.</param> /// <returns>The character that was retrieved.</returns> public char GetCurrentChar(int peek) { if( (m_idx+peek)<m_source.Length ) return m_source[m_idx+peek]; else return (char)0; } /// <summary> /// Obtain the next character and advance the index by one. /// </summary> /// <returns>The next character</returns> public char AdvanceCurrentChar() { return m_source[m_idx++]; } /// <summary> /// Move the index forward by one. /// </summary> public void Advance() { m_idx++; } /// <summary> /// The last attribute name that was encountered. /// <summary> public string ParseName { get { return m_parseName; } set { m_parseName = value; } } /// <summary> /// The last attribute value that was encountered. /// <summary> public string ParseValue { get { return m_parseValue; } set { m_parseValue = value; } } /// <summary> /// The last attribute delimeter that was encountered. /// <summary> public char ParseDelim { get { return m_parseDelim; } set { m_parseDelim = value; } } /// <summary> /// The text that is to be parsed. /// <summary> public string Source { get { return m_source; } set { m_source = value; } } } }
この記事ではParse
クラスの詳しい説明を省きますが、コードリストではすべてのメソッドにコメントを付けてあるので、興味のある方はそちらをご覧ください。
ParseHTML
クラスはParse
クラスのサブクラスであり、パーサがHTMLを扱う上で必要となるHTML固有のコードを含んでいます。ParseHTML
クラスのソースコードをリスト4に示します。
using System; namespace HTML { /// <summary> /// Summary description for ParseHTML. /// </summary> public class ParseHTML:Parse { public AttributeList GetTag() { AttributeList tag = new AttributeList(); tag.Name = m_tag; foreach(Attribute x in List) { tag.Add((Attribute)x.Clone()); } return tag; } public String BuildTag() { String buffer="<"; buffer+=m_tag; int i=0; while ( this[i]!=null ) {// has attributes buffer+=" "; if ( this[i].Value == null ) { if ( this[i].Delim!=0 ) buffer+=this[i].Delim; buffer+=this[i].Name; if ( this[i].Delim!=0 ) buffer+=this[i].Delim; } else { buffer+=this[i].Name; if ( this[i].Value!=null ) { buffer+="="; if ( this[i].Delim!=0 ) buffer+=this[i].Delim; buffer+=this[i].Value; if ( this[i].Delim!=0 ) buffer+=this[i].Delim; } } i++; } buffer+=">"; return buffer; } protected void ParseTag() { m_tag=""; Clear(); // Is it a comment? if ( (GetCurrentChar()=='!') && (GetCurrentChar(1)=='-')&& (GetCurrentChar(2)=='-') ) { while ( !Eof() ) { if ( (GetCurrentChar()=='-') && (GetCurrentChar(1)=='-')&& (GetCurrentChar(2)=='>') ) break; if ( GetCurrentChar()!='\r' ) m_tag+=GetCurrentChar(); Advance(); } m_tag+="--"; Advance(); Advance(); Advance(); ParseDelim = (char)0; return; } // Find the tag name while ( !Eof() ) { if ( IsWhiteSpace(GetCurrentChar()) || (GetCurrentChar()=='>') ) break; m_tag+=GetCurrentChar(); Advance(); } EatWhiteSpace(); // Get the attributes while ( GetCurrentChar()!='>' ) { ParseName = ""; ParseValue = ""; ParseDelim = (char)0; ParseAttributeName(); if ( GetCurrentChar()=='>' ) { AddAttribute(); break; } // Get the value(if any) ParseAttributeValue(); AddAttribute(); } Advance(); } public char Parse() { if( GetCurrentChar()=='<' ) { Advance(); char ch=char.ToUpper(GetCurrentChar()); if ( (ch>='A') && (ch<='Z') || (ch=='!') || (ch=='/') ) { ParseTag(); return (char)0; } else return(AdvanceCurrentChar()); } else return(AdvanceCurrentChar()); } } }
HTMLパーサとユーザーを結ぶメインインターフェイスとなるのがこのParseHTML
クラスですが、HTMLパーサの使い方は次項で取り上げることにします。主に使われるメソッドは次の2つです。
public char Parse() public AttributeList GetTag()
Parse()
メソッドは、呼び出されると、解析中のHTMLファイルから次の1文字を取り出し、その文字がタグの一部だと判明した場合は、値としてゼロを返します。したがって、Parse()
がゼロを返してきたときは、HTMLタグの処理が必要です。そのタグにアクセスするにはGetTag()
メソッドを呼び出します。GetTag()
メソッドはArrayList
オブジェクトを返します。このオブジェクトに、処理対象のタグとその全属性が含まれています。
このHTMLパーサの使い方
では、このHTMLパーサの使い方の一例を紹介しましょう。今回のサンプルプログラムはユーザーにURLの指定を求め、指定されたURLのHTMLファイル内部にあるすべてのハイパーリンクを表示します。このサンプルでは、HTMLデータを指すURLしか使用できないことに注意してください。画像やその他のバイナリデータはうまく扱えません。この例のソースコードをリスト5に示します。
using System; using System.Net; using System.IO; namespace HTML { /// <summary> /// FindLinks is a class that will test the HTML parser. /// This short example will prompt for a URL and then /// scan that URL for links. /// This source code may be used freely under the /// Limited GNU Public License(LGPL). /// /// Written by Jeff Heaton (http://www.jeffheaton.com) /// </summary> class FindLinks { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(string[] args) { System.Console.Write("Enter a URL address:"); string url = System.Console.ReadLine(); System.Console.WriteLine("Scanning hyperlinks at: " + url ); string page = GetPage(url); if(page==null) { System.Console.WriteLine("Can't process that type of file," + "please specify an HTML file URL." ); return; } ParseHTML parse = new ParseHTML(); parse.Source = page; while( !parse.Eof() ) { char ch = parse.Parse(); if(ch==0) { AttributeList tag = parse.GetTag(); if( tag["href"]!=null ) System.Console.WriteLine( "Found link: " + tag["href"].Value ); } } } public static string GetPage(string url) { WebResponse response = null; Stream stream = null; StreamReader reader = null; try { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); response = request.GetResponse(); stream = response.GetResponseStream(); if( !response.ContentType.ToLower().StartsWith("text/") ) return null; string buffer = "",line = ""; reader = new StreamReader(stream); while( (line = reader.ReadLine())!=null ) { buffer+=line+"\r\n"; } return buffer; } catch(WebException e) { System.Console.WriteLine("Can't download:" + e); return null; } catch(IOException e) { System.Console.WriteLine("Can't download:" + e); return null; } finally { if( reader!=null ) reader.Close(); if( stream!=null ) stream.Close(); if( response!=null ) response.Close(); } } } }
サンプルプログラムの動作を見るには、何らかのURLアドレスを入れてください。例えば、「http://www.developer.com」と入力すれば、Developer.comのホームページに含まれているすべてのハイパーリンクが表示されます。
ページの処理を行うループは次のとおりです。
ParseHTML parse = new ParseHTML(); parse.Source = page; while( !parse.Eof() ) { char ch = parse.Parse(); if(ch==0) { AttributeList tag = parse.GetTag(); if( tag["href"]!=null ) System.Console.WriteLine( "Found link: " + tag["href"].Value ); } }
ParseHTML
オブジェクトがインスタンス化され、このオブジェクトのSource
プロパティに、解析すべきHTMLページが設定されます。ページの終わりに達するまで、このループが繰り返されます。ここでは、通常文字は無視し、タグだけを探しています(ch
変数がゼロのときは、現在の文字がタグの一部であることを表します)。検出されたタグごとに、HREF属性があるかどうかを調べ、HREF属性がある場合は、そのリンクを表示します。
まとめ
ご覧いただいたとおり、これらのクラスはHTML構文解析の枠組みとしてたいへん使いやすく、どのようなMicrosoft .NETアプリケーションでも利用できます。今回のサンプルプログラムでは、リンクを表示する目的にしかこのパーサを使用していませんが、私自身は複雑なHTML解析アプリケーションにもこのパーサを使用しています。