ダウンロードしたHTMLファイルからリンクを抽出するツールを作ってみました

Haskell正規表現を試してみようと思って、ダウンロードしたHTMLファイルからリンクを抽出するツールを作ってみました。
ファイル: extlink.hs

module Main (main) where

import System.Environment (getArgs, getProgName)
import System.Exit (exitWith, ExitCode(ExitSuccess))
import System.Console.GetOpt
import System.IO (readFile)
import Control.Monad (when)
import Data.List (find, isPrefixOf)
import Text.Regex (mkRegexWithOpts, matchRegexAll, Regex)

data Flag = Help | Version | Image | Prefix String
            deriving (Show, Eq)

options :: [OptDescr Flag]
options =
  [ Option ['h'] ["help"]   (NoArg Help)   
      "display this help and exit.",
    Option [] ["version"]   (NoArg Version)
      "output version information and exit.",
    Option ['i'] ["image"]  (NoArg Image)
      "number all output lines.",
    Option ['p'] ["prefix"] (ReqArg Prefix "path")
      "set url prefix."
  ]

parseOptions :: [String] -> IO ([Flag], [String])
parseOptions args =
  case getOpt Permute options args of
    (o, n, []  ) -> return (o, n)
    (_, _, errs) -> ioError (userError (concat errs))

processDefaultOptions :: String -> [Flag] -> IO ()
processDefaultOptions prog flags = do
  when (getOption (isNoArgOption Help)    flags /= Nothing) $ usage
  when (getOption (isNoArgOption Version) flags /= Nothing) $ version
  where
    usage :: IO ()
    usage = do
      putStrLn $ usageInfo ("usage: " ++ prog ++ " [OPTION...] files...") options
      exitWith ExitSuccess
    
    version :: IO ()
    version = do
      putStrLn (prog ++ " - 0.1a")
      exitWith ExitSuccess

getOption :: (Flag -> Bool) -> [Flag] -> Maybe Flag
getOption func flags = find func flags

isNoArgOption :: Flag -> Flag -> Bool
isNoArgOption flag f = f == flag

isPrefixOption :: Flag -> Bool
isPrefixOption f =
  case f of
    Prefix path -> True
    otherwise   -> False
                     
main :: IO ()
main = do 
  prog <- getProgName
  args <- getArgs  
  (flags, files) <- parseOptions args
  processDefaultOptions prog flags
  let pat = if getOption (isNoArgOption Image) flags /= Nothing
              then imgPattern
              else hrefPattern
      fil = case getOption isPrefixOption flags of
              Just (Prefix path) -> prefixFilter path
              otherwise          -> nullFilter
  if null files
    then printPatternFromStdin pat fil
    else mapM_ (printPatternFromFile pat fil) files
  where
    hrefPattern :: Regex
    hrefPattern = mkRegexWithOpts "<a[^>]+href=\"?([^\">]+)\"[^>]*>" False False

    imgPattern :: Regex
    imgPattern = mkRegexWithOpts "<img[^>]+src=\"?([^\">]+)\"[^>]*>" False False

    nullFilter :: String -> String
    nullFilter s = s

    prefixFilter :: String -> String -> String
    prefixFilter path s
      | "/" `isPrefixOf` s = path ++ s
      | otherwise = s

    printPatternFromStdin :: Regex -> (String -> String) -> IO ()
    printPatternFromStdin pat fil = do
      s <- getContents
      mapM_ putStrLn $ map fil $ (extractAllPattern pat s)

    printPatternFromFile :: Regex -> (String -> String) -> FilePath -> IO ()
    printPatternFromFile pat fil file = do
      s <- readFile file
      mapM_ putStrLn $ map fil $ (extractAllPattern pat s)
      
extractAllPattern :: Regex -> String -> [String]
extractAllPattern pat s = 
  case matchRegexAll pat s of
    Just (_, _, after, matched) -> 
      matched ++ extractAllPattern pat after
    Nothing -> []

ファイル: Makefile

TARGET  = extlink
SOURCES = extlink.hs

HC      = ghc
HCFLAGS = -W -fno-warn-unused-matches --make

all: $(TARGET)

$(TARGET): $(SOURCES)
	$(HC) $(HCFLAGS) $< -o $@

clean:
	rm -f $(TARGET) *.o *.hi *.exe

使い方。コマンドライン引数にHTMLファイルを指定します。デフォルトではa href="..."のリンクを抽出します。-iオプションを付けると、img src="..."のリンクを抽出します。-p pathオプションを付けると、相対リンクの先頭に指定した文字列を付けます(相対リンク、絶対リンクの変換用)。

$ extlink hoge.html

オプションまわりの処理が複雑になってしまいましたが、正規表現についてはたいしたことしてません。関数extractAllPatternを再帰的に呼ぶことで、指定した文字列に含まれる全てのマッチした(部分)文字列を抽出しています。Rubyでいうところのscanみたいな感じ。
あ、そうそうGHCには、正規表現のライブラリとしてText.RegexとText.Regex.Posixのふたつがあるのですが、これってどう使い分ければいいんでしょう。Text.Regex.Posixの方は返り値にIOが付いてて使うのが面倒なので、今回はText.Regexを使ってみました。