Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

Microsoft C#でのHTML構文解析

HTMLパーサの作成方法と利用例

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/08/18 12:00

Web上のデータのほとんどはHTML形式で保存されていますが、.NET FrameworkにはHTMLの構文解析を簡単に行うための方法がありません。そこで本稿では、HTMLパーサの作成方法を示し、それを読者のアプリケーションでどう利用できるかを説明します。

はじめに

 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タグにはsrcaltという2つの属性が含まれています。属性値はそれぞれ「picture.gif」と「Some Picture」です。

 Attributeクラスは、namevaluedelimという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つの名前といくつかの属性の集まりから構成されます。AttributeListnameプロパティに格納されている名前は、タグの名前を表します。パーサから返されるタグは、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( "\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に示します。

リスト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()!='\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に示します。

リスト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+"\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解析アプリケーションにもこのパーサを使用しています。



  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • Jeff Heaton(Jeff Heaton)

    ライター、人工知能(AI)研究者、元大学教員。AI、仮想世界、スパイダー、ボットなどの話題を取り上げて執筆した書籍は10冊以上。Java、.Net、Silverlightを対象に、高度なニューラルネットワークおよびAIフレームワークの提供を目的とするオープンソースイニシアチブ、Encogプロジェクト...

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5