今週のサクサクパーサー

2005/07/14 13:04

※ 商品のリンクをクリックして何かを購入すると私に少額の報酬が入ることがあります【広告表示】

今週のsakusakuというページを勝手にパースしてみました。

プログラムはPython,Java,Ruby,PHPで書き、最終的にはhttpに乗せてRSSとして「本日ないしは明日のトピック」を配信するところまでをやってみようと思っています。

今回は、ネット越しにHTMLを取得して今日・明日のコンテンツを配列にするところまでです。

Python

Pythonは2.3.5を使用しました。japanese_codecsが必要かもしれません。

HTMLのパース自体は標準のクラスを用いて行うことが可能でした。

Pythonのクラスやライブラリは思想の一貫性があり非常に推測しやすいので素敵です。

  # -*- coding: UTF-8 -*-
  ###########################################################################
  # Copyright 2005 everes.net
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  ###########################################################################

  from HTMLParser import HTMLParser
  from datetime import date
  import urllib

  class SakusakuParser(HTMLParser):
      sakusaku = None

      def setCharset(self, encoding):
          self.sakusaku = SakusakuWeek(encoding)
          self.sakusaku.clearData()

      def getSakusakuWeek(self):
          return self.sakusaku

      def handle_data(self, data):
          self.sakusaku.parseData(data)

      def handle_starttag(self, tag, attrs):
          self.sakusaku.startTag(tag, attrs)

      def handle_endtag(self, tag):
          self.sakusaku.endTag(tag)

  class SakusakuWeek:
      weekContents = []
      contentsArea = -1
      divCount = 0
      today = None
      tmp_content = ""

      def __init__(self, encoding):
          self.charset = encoding
          weekContents = []

      def clearData(self):
          weekContents = []
          contentsArea = -1
          divCount = 0
          today = None
          tmp_content = ""

      def getContents(self):
          return self.content

      def getToday(self):
          self.today = date.today()
          result = self.weekContents[self.today.weekday()].split('●')
          return result[1:len(result) - 1]

      def getTomorrow(self):
          self.today = date.today()
          result = self.weekContents[self.today.weekday() + 1].split('●')
          return result[1:len(result) - 1]

      def parseData(self, data):
          if self.isContentsArea():
              data = unicode(data, self.charset).encode("UTF-8")
              data = data.strip(" ¥t¥r¥n")
              if data:
                  self.tmp_content += data

      def startTag(self, tag, attrs):
          if tag == 'div':
              for i in range(len(attrs)):
                  if attrs[i][0] == 'style':
                      if attrs[i][1] == 'margin:10px':
                          self.setContentsArea(1)
              self.div()

      def endTag(self, tag):
          if tag == 'div':
              self.endDiv()

      def isContentsArea(self):
          return self.contentsArea == 1

      def setContentsArea(self, mode):
          if mode == 1:
              self.contentsArea = 1
              self.divCount = 0
          else:
              self.weekContents.append(self.tmp_content)
              self.tmp_content = ""
              self.contentsArea = -1

      def div(self):
          if self.isContentsArea():
              self.divCount += 1

      def endDiv(self):
          if self.isContentsArea():
              self.divCount -= 1
              if self.divCount == 0:
                  self.setContentsArea(-1)

  class Sakusaku:
      sakusaku = None

      def feed(self):
          url = "http://www.tvk-yokohama.com/saku2/week.html"
          data = urllib.urlopen( url )
          charset = data.headers.getparam('charset')
          if charset == None:
              charset = 'japanese.shift_jis'
          parser = SakusakuParser()
          parser.setCharset(charset)
          parser.feed( data.read() )
          self.sakusaku = parser.getSakusakuWeek()

      def getToday(self):
          self.feed()
          return self.sakusaku.getToday()

      def getTomorrow(self):
          self.feed()
          return self.sakusaku.getTomorrow()

  def main():
      sakusaku = Sakusaku()
      try:
          content = sakusaku.getToday()
      except IndexError,e:
          content = ["None","Holiday"]

      for i in range(len(content)):
          print '  %s' % content[i]

  if __name__ == "__main__":
      main()

JAVA

Javaも標準のライブラリのみで実装が可能です(swingパッケージですが)。

LLな言語ではないのですが、普段仕事で使用しているのでつい作ってしまいました。

仕事で使っていてこのコードは恥ずかしいのですが、もうJavaはおもしろくはないので手抜きです。

  /**************************************************************************
    Copyright 2005 everes.net
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
  **************************************************************************/

  package net.everes.junk;

  import java.io.InputStreamReader;
  import java.net.HttpURLConnection;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.Date;
  import java.util.Enumeration;
  import java.util.List;

  import javax.swing.text.html.HTML;
  import javax.swing.text.html.HTMLEditorKit;
  import javax.swing.text.html.parser.DTD;
  import javax.swing.text.html.parser.DocumentParser;
  import javax.swing.text.html.parser.TagElement;
  import javax.swing.text.html.parser.ParserDelegator;
  import javax.swing.text.MutableAttributeSet;
  public class SakusakuCallback extends HTMLEditorKit.ParserCallback {
      List weekContents;
      boolean contentsArea;
      int today;
      int tomorrow;
      String tmpContent;
      int divCount = 0;

      public SakusakuCallback() {
          super();
          weekContents = new ArrayList();
          contentsArea = false;
          Calendar calendar = Calendar.getInstance();
          calendar.setTime(new Date());
          today = calendar.get(Calendar.DAY_OF_WEEK) - 2;
          tomorrow = calendar.get(Calendar.DAY_OF_WEEK) - 1;
          tmpContent = "";
      }

      public void handleText(char[] data, int pos) {
          if(contentsArea) {
              tmpContent += new String(data);
          }
      }

      public void handleStartTag(HTML.Tag tag, MutableAttributeSet attrs, int pos) {
          if(tag.equals(HTML.Tag.DIV)) {
              if(!contentsArea) {
                  Enumeration enumeration = attrs.getAttributeNames();
                  while(enumeration.hasMoreElements()) {
                      Object obj = enumeration.nextElement();
                      String name = "" + obj;
                      if(name.equals("style")) {
                          Object value = attrs.getAttribute(obj);
                          if(value.toString().equals("margin:10px")) {
                              contentsArea = true;
                              divCount = 0;
                          }
                      }
                  }
              }
              findDiv();
          }
      }

      public void handleEndTag(HTML.Tag tag, int pos) {
          if(tag.equals(HTML.Tag.DIV)) {
              findEndDiv();
          }
      }

      public String[] getToday() throws ArrayIndexOutOfBoundsException {
          String tmp = weekContents.get(today).replaceAll(" ¥t¥r¥n","");
          String[] result = tmp.split("●");
          String[] contents = new String[result.length];
          for(int i = 0;i < result.length;i++) {
              contents[i] = result[i];
          }
          return contents;
      }

      private void findDiv() {
          if(contentsArea) {
              divCount += 1;
          }
      }

      private void findEndDiv() {
          if(contentsArea) {
              divCount -= 1;
          }
          if(contentsArea) {
              if(divCount == 0) {
                  contentsArea = false;
                  weekContents.add(tmpContent);
                  tmpContent = "";
              }
          }
      }

      public List getContents() {
          return this.weekContents;
      }

      public static void main(String[] args) {
          try {
              URL url = new URL("http", "www.tvk-yokohama.com", "/saku2/week.html");
              HttpURLConnection request = (HttpURLConnection) url.openConnection();
              ParserDelegator parser = new ParserDelegator();
              HTMLEditorKit.ParserCallback sakusaku = new SakusakuCallback();

              parser.parse(new InputStreamReader(request.getInputStream()), sakusaku, true);

              String[] result = ((SakusakuCallback)sakusaku).getToday();

              for(String day: result) {
                  System.out.println(day);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  }

RUBY

現場の若いのにやるように進めながら先に作ってしまいました(シャアサクくんは見ないでね)。

残念ながらRAAというライブラリ群にあったHTMLパーサでは「今週のサクサク」はパースできませんでした。アトリビュートがクォートされていないとこけてしまいます。

いい感じに動くHTMLパーサがありましたので、それを使ってやってみました。

#ただし、パーサはストリームを求めているのにHTTPでストリームを返すやり方が発見できず。。。ま、仕方ないですが。

Rubyでおもしろかったのは、無名クラスの作り方です。

下記のコードではパーサのクラスを継承して作っていますが、Ruby的にはクラスをインスタンス化してからインスタンスのメソッドを書き換えることができるようです。

実際には恐ろしくて使えませんが、Rubyはやばそうだと思わせるに十分でした。

###########################################################################
# Copyright 2005 everes.net
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###########################################################################
require 'simplehtmlparse' #http://arika.org/archive/?C=M;O=A
require 'net/http'
require 'date'
require 'nkf'

class Sakusaku < SimpleHtmlParse
  weekContents = Array.new
  contentsArea = 0
  divCount = 0
  tmpString = ""

  def initialize(io)
        super(io)
        @weekContents = Array.new
        @divCount = 0
        @tmpString = ""
  end

  def begin_tag(name, attribute, orig_text)
    if(name == "DIV")
          if(attribute.has_key? "STYLE")
            if(attribute["STYLE"] == "margin:10px")
                  @contentsArea = 1
                  @divCount = 0
                end
      end
          if(@contentsArea == 1)
            @divCount += 1
          end
        end
  end

  def end_tag(name, attribute)
    if(@contentsArea == 1)
          if(name == "DIV")
            @divCount -= 1
                if(@divCount == 0)
                  @weekContents << @tmpString
                  @tmpString = ""
                  @contentsArea = 0
                end
          end
        end
  end

  def text(orig_text)
    if(@contentsArea == 1)
        tmp = orig_text.chomp.gsub(/¥t¥s /,'')
        if(tmp != '')
            @tmpString << orig_text.gsub(/¥t¥s/,'')
        end
    end
  end

  def getWeekContents()
    return @weekContents
  end

  def getToday()
    dt = Date::today
    today = dt.wday - 1
  return  getContents(today)
  end

  def getTomorrow()
    dt = Date::today
    tomorrow = dt.wday
  return getContents(tomorrow)
  end

  def getContents(dayofweek)
      tmp = @weekContents[dayofweek].split('●')
      tmp.delete_at(0)
    return tmp
  end

end

Net::HTTP.version_1_2
body = Net::HTTP.get('www.tvk-yokohama.com', '/saku2/week.html', 80)
f = open("week_tmp.html","w+")
f.puts(NKF.nkf('-w',body))
f.close()

f = open("week_tmp.html")

sakusaku = Sakusaku.new(f)
sakusaku.parse

f.close()
begin
  result = sakusaku.getTomorrow
  result.each {|x| puts x}
rescue
  puts "holiday"
end

PHP

phpは挫折しました。

標準のXML関数はHTMLのパースができません(XHTMLならできる)。

仕方なくごちゃごちゃなPEARも見てみましたが、標準と同様でした(そのあたりの思想の欠如もphpが好きでない理由です)。

世の中にライブラリも見あたりません。

phpのHTMLパーサは、知人が作っている(まだ未公開)ので作りたくありません。

知人はあっという間にphpでサクサクパーサを作りました。

結論としてphp(というか言語を取り巻く環境)は嫌いです。

以下、書き途中でがっかりしたコード。

  // php 挫折(;o;) phpのXML関数ではXHTML以外はパースできず、他に良さそうなパーサも見つからず。。。

  function beginTag($psr, $name, $attributes) {
      print "start" . $name . "¥n";
  }

  function endTag($psr, $name) {
     print "end:" . $name . "¥n";
  }

  function text($psr, $data) {
      print $data;
  }

  function getSakusakuData() {
      $URL = "http://www.tvk-yokohama.com/saku2/week.html";

      $buff = "";
      $fp = fopen($URL,"r");
      while ( !feof($fp) ) {
      $buff .= fgets($fp,4096);
      }
      fclose($fp);
      $buff = mb_convert_encoding($buff, "UTF-8", "Shift_JIS");
      return $buff;
  }

  $data = getSakusakuData();
  $parser = xml_parser_create();
  xml_set_element_handler($parser, "begintag", "endTag");
  xml_set_character_data_handler($parser, "text");
  xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,1);
  xml_parse($parser, $data);
  xml_parser_free($parser);

Prev Entry

Next Entry