【问题标题】:Attoparsec /= version of stringCIAttoparsec /= stringCI 的版本
【发布时间】:2013-04-10 13:08:53
【问题描述】:

我正在解析 robots.txt 文件,并且我已经编写了解析器以成功解析“格式正确”的 robots.txt 文件。我已经能够调整解析器以跳过以符号开头的行(例如 # 或 / 用于 cmets),但只能使用 inClass "#/"

我一直无法解决的一个问题是,如果一行不包含我想要匹配的字符串,则跳过该行。

User-agent: *
Disallow: /includes/
Disallow: /misc/
Disallow: /modules/
Doesn't belong here
Disallow: /profiles/
Disallow: /scripts/
Disallow: /themes/

我首先尝试使用匹配:

satisfy (notInClass "DdUu") *> skipWhile (not . isEndOfLine)

并且认为这样做会否定我对特定注释行解析器的需求,因为哈希或斜杠不属于字符类。问题是这不起作用。

我也意识到它无论如何都不会起作用,因为它无法解决诸如“不允许”与“不允许”之类的匹配问题。

这是解析代码(没有注释跳过代码,这仅适用于格式良好的 robots.txt):

{-# LANGUAGE OverloadedStrings, RankNTypes #-}

import           Prelude hiding (takeWhile)
import           Control.Applicative hiding (many)
import           Data.Char
import           Data.Text as T hiding (toLower)
import           Data.Text.Encoding as E
import           Control.Monad
import           Data.Attoparsec.ByteString
import qualified Data.Attoparsec.Char8 as AC
import           Data.Array.Unboxed
import           Data.ByteString as B hiding (takeWhile)
import qualified Data.ByteString.Internal as BI
import           Data.Word (Word8)

type RuleMap = [(ByteString, ByteString)]

-- newtype for indexable ua
newtype UserAgent = UserAgent { unUA :: ByteString }
    deriving (Eq, Ord, Show)

data RuleSet = RuleSet
    { userAgent :: UserAgent,
      rules     :: RuleMap }
     deriving (Eq, Ord, Show)

main = do
    r <- B.readFile "/Users/ixmatus/Desktop/robots.txt"
    print $ parse (many1 parseUABlock) r

stripper = E.encodeUtf8 . T.strip . E.decodeUtf8

isNotEnd = not . AC.isEndOfLine

-- | Catch all character matching, basically
matchALL :: Word8 -> Bool
matchALL = inClass ":/?#[]@!$&'()*%+,;=.~a-zA-Z0-9 _-"

-- | @doParse@ Run the parser, complete the partial if the end of the stream has
-- a newline with an empty string
doParse :: ByteString -> [RuleSet]
doParse cont =
    case parse (many1 parseUABlock) cont of
        Done _ set -> set
        Partial f -> handlePartial (f B.empty)
        Fail {} -> []

-- | @handlePartial@ Handle a partial with empty string by simply
-- returning the last completion
handlePartial :: forall t a. IResult t [a] -> [a]
handlePartial (Done _ r) = r
handlePartial (Fail {})  = []

-- | @parseUABlock@ Parse a user-agent and rules block
parseUABlock = do
    ua    <- parseUACol *> uA
    rulez <- many1 parseRules

    return RuleSet { userAgent = UserAgent ua,
                     rules = rulez }

-- | @matchUACol@ Parse the UA column and value taking into account
-- possible whitespace craziness
parseUACol = AC.skipSpace
          *> AC.stringCI "User-Agent"
          <* AC.skipSpace
          *> AC.char8 ':'
          *> AC.skipSpace

uA = do
    u <- takeWhile1 isNotEnd
    return (stripper u)

-- | @parseRules@ Parse the directives row
parseRules = (,) <$> parseTransLower
             <*> directiveRule

directiveRule = do
    rule <- takeWhile1 matchALL <* many1 AC.endOfLine

    return (stripper rule)

parseTransLower = do
    res <- parseDirectives <* AC.skipSpace
    return (lowercase res)

ctypeLower = listArray (0,255) (Prelude.map (BI.c2w . toLower) ['\0'..'\255']) :: UArray Word8 Word8
lowercase = B.map (\x -> ctypeLower!x)

directives = AC.stringCI "Disallow" <|> AC.stringCI "Allow"

-- | @parseDirectives@ Parse the directive column and any possibly
-- funny whitespace
parseDirectives = AC.skipSpace
                  *> directives -- <|> AC.stringCI "Crawl-delay" <|> AC.stringCI "Sitemap")
                  <* AC.skipSpace
                  <* AC.char8 ':'

【问题讨论】:

    标签: haskell attoparsec


    【解决方案1】:

    考虑这种方法。

    定义:

    data RobotsDirective = RobotsDirective String String
    

    这表示 robots.txt 文件中的已解析指令。第一个字符串是指令(即UserAgentAllowDisallow 等),第二个字符串是冒号后面的内容。

    现在为RobotsDirective写一个解析器:

    parseRD :: Parser RobotsDirective
    

    parseRD 将查找指令名称(仅应包含字母、数字和破折号,可能还有下划线),后跟一个冒号,后跟零个或多个非换行符。适当地忽略空白。如果parseRD 找到这样的模式,它将创建并返回RobotsDirective。否则会跳过一行字符重试。

    现在您已经有了RobotsDirective 的解析器,您可以以标准方式为[RobotsDirective] 创建解析器。

    这个解析器只是跳过任何看起来不像指令的行,这将包括空行、注释行和以Don't allow... 开头的行。但是,它可以为 robots.txt 文件中无效的行返回 RobotsDirective,即:

    foo: blah
    

    将返回RobotsDirective "foo" "blah"。解析 robots.txt 文件并获得 RobotsDirective 值列表后,只需浏览该列表并忽略您不感兴趣的值。

    【讨论】:

    • 我已经这样做了;需要发生的是我必须跳过任何与特定字符串不匹配的行,所以我的假设是我也可以跳过其他/非标准指令。我已经编写了stringCI/= 版本,并且正在努力让它恰到好处。我们拭目以待。
    • 您的方法的另一个问题是指令需要按 User-agent 标头分组,因为它们在 robots.txt 文件中以“位置”方式完成(我认为这很愚蠢,但这就是每个人 - google/bing/etc ... - 处理它们的方式)。所以我有 RuleSetRuleMap 类型让行走变得如此轻松。
    • 我的意思是你应该只使用 attoparsec 来解析 robots.txt 文件的基本结构。一旦你有了[RobotsDirective],就很容易:1)忽略没有意义的指令,2)按照你喜欢的方式重新排列指令。
    • 第一个答案:别担心。毕竟,robots.txt 文件有多大?第二个答案:Haskell 的懒惰意味着列表操作很少创建单独的列表。例如,take 10 [1..1000000] 不会先创建一百万个元素的列表,然后再取前 10 个。[1..1000000] 的元素是按需生成的。第三个答案:Haskell 可以在某些情况下结合列表操作来消除中间结果:map (+1) . map (*2) xs 有效地运行就像你写了map (\x-&gt;2*x+1) xs 一样。 ...
    • 第四个答案:考虑一个更通用的 robots.txt 解析器,它将每一行标识为 1) 指令、2) 注释/空白行或 3) 垃圾行(不是 1 或 2)。这种解析器不仅可以用于从文件中提取信息,还可以用于重写文件,同时保留 cmets 和垃圾行,任何使用 robots.txt 文件的应用程序都可以使用此解析器作为起点。是的,它执行了更多的工作,但它也是“结束所有 robots.txt 解析器的robots.txt 解析器”,您再也不必编写另一个 robots.txt 解析器了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    相关资源
    最近更新 更多