はじめに
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に示します。
リスト1 Attributeクラス
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に示します。
リスト2 AttributeListクラス
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に示します。
リスト3 Parseクラス
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( "
".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に示します。
リスト4 ParseHTMLクラス
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()!=’
’ )
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に示します。
リスト5 FindLinksクラス
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+"
";
}
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解析アプリケーションにもこのパーサを使用しています。