読者です 読者をやめる 読者になる 読者になる

HTMLスクレイピング in Scala その2

Scala

前回の続き(refs: HTMLスクレイピング in Scala その1 http://d.hatena.ne.jp/noire722/20100914/1284429573)

今回はHTMLタグの除去とファイル保存を行う処理を追加しました。

実装

[追記]2010/09/14 20:18

id:t_yano さんからtwitterで「match式は値を返すので変数srcが不要」との指摘を受けました。ありがとうございます。match式の結果に対してforeachメソッドを呼ぶように修正しました。
ちなみに、

hoge match {case 〜}.foreach() // という書き方はコンパイル通りませんでした。
(hoge match {case 〜}).foreach() // これならOK

修正した点
・match式の結果に対してforeachメソッドを呼ぶようにした。
・getSourceメソッドの最後のreturnを削除した。
・parseメソッドのfor〜yield式の結果に対してfilterメソッドを呼ぶようにした。

以下、修正後のコード

import scala.io.Source
import scala.util.matching.Regex
import java.io._

object Html{
    def getSource(url: String, toParse: Boolean): List[String] = {
        var src = Source.fromURL(url, "ISO-8859-1").getLines.toList

        var charset: String = null
        val regex = new Regex("""charset[ ]*=[ ]*[0-9a-z|\-|_]+""")
        for(line <- src){
            val lower = line.toLowerCase
            if(lower.contains("content") && lower.contains("charset")){
                charset = regex.findFirstIn(lower).get
                charset = charset.split("=")(1).trim
            }
        }
        if(charset != null) src = Source.fromURL(url, charset).getLines.toList
        if(toParse) parse(src) else src
    }

    def download(url_or_srcList: AnyRef, fileName: String, toParse: Boolean) = {
        var bw: BufferedWriter = null
        try{
            bw = new BufferedWriter(new FileWriter(fileName))
            (url_or_srcList match {
                case url: String => getSource(url, toParse)
                case list: List[String] => if(toParse) parse(list) else list
                case _ => error("Invalid Parameter: The 1st argument shoud be a String or a List[String]")
            }).foreach(elem => bw.write(elem + "\r\n"))
        } finally {
            if(bw != null) bw.close
        }
    }

    def parse(src: List[String]): List[String] = {
        def removeTag(target: String): String = {
            val regex = new Regex("""<.+?>|\t""")
            regex.replaceAllIn(target, "")
        }
        (for(str <- src) yield {removeTag(str)}).filter(_.length != 0)
    }
}

以下、修正前のコード

/*** これは修正前のコード ***/
import scala.io.Source
import scala.util.matching.Regex
import java.io._

object Html{
    def getSource(url: String, toParse: Boolean): List[String] = {
        var src = Source.fromURL(url, "ISO-8859-1").getLines.toList

        var charset: String = null
        val regex = new Regex("""charset[ ]*=[ ]*[0-9a-z|\-|_]+""")
        for(line <- src){
            val lower = line.toLowerCase
            if(lower.contains("content") && lower.contains("charset")){
                charset = regex.findFirstIn(lower).get
                charset = charset.split("=")(1).trim
            }
        }
        if(charset != null) src = Source.fromURL(url, charset).getLines.toList
        if(toParse) src = parse(src)
        return src
    }

    def download(url_or_srcList: AnyRef, fileName: String, toParse: Boolean) = {
        var bw: BufferedWriter = null
        try{
            bw = new BufferedWriter(new FileWriter(fileName))
            var src: List[String] = null
            url_or_srcList match {
                case url: String => src = getSource(url, toParse)
                case list: List[String] => src = if(toParse) parse(list) else list
                case _ => error("Invalid Parameter: The 1st argument shoud be a String or a List[String]")
            }
            src.foreach(elem => bw.write(elem + "\r\n"))
        } finally {
            if(bw != null) bw.close
        }
    }

    def parse(src: List[String]): List[String] = {
        def removeTag(target: String): String = {
            val regex = new Regex("""<.+?>|\t""")
            regex.replaceAllIn(target, "")
        }

        val parsed = for(str <- src) yield {removeTag(str)}
        parsed.filter(_.length != 0)
    }
}

前回からの変更点
・getSourceメソッドの第二引数にパーズするかどうかの条件を指定できるようにした。
・取得したソースをファイル保存するdownloadメソッドを追加した。
・パーズ用にparseメソッドを追加した。


以下、Javaには無かった記述をピックアップ

(型の)パターンマッチング

url_or_srcList match {
    case url: String => src = getSource(url, toParse)
    case list: List[String] => src = if(toParse) parse(list) else list
    case _ => error("Invalid Parameter: The 1st argument shoud be a String or a List[String]")
}

if文でinstanceOfメソッドを使う記述に比べてかなり簡潔に書けます。

foreachメソッド

src.foreach(elem => bw.write(elem + "\r\n"))

クロージャ

for〜yield文、filterメソッド、ローカル関数

def parse(src: List[String]): List[String] = {
    def removeTag(target: String): String = {
        val regex = new Regex("""<.+?>|\t""")
        regex.replaceAllIn(target, "")
    }

    val parsed = for(str <- src) yield {removeTag(str)}
    parsed.filter(_.length != 0)
}

class直下に特定の処理でしか使わない細々としたメソッドをたくさん書くと可読性が落ちます。
そんなときはローカル関数を使うと便利です。(今回は2行なのでわざわざ使う必要もないですが…)

for〜yield文ではコレクションsrcの要素毎にremoveTagメソッドを呼んで整形し、その結果のコレクションをparsedに格納しています。
filterメソッドは括弧内で指定した条件式に当てはまる要素を抽出するために使っています。

おわり。


※注意
タグ除去の処理はこれでは改行を挟んだときにうまく機能しません。
また、本文に含まれる<〜>を除去してしまう危険性があります。


最新のソースはgithubへ。
http://github.com/noire722/scala_scripts/