Я собираюсь объяснить, как построить общее решение стандартным способом, используя комбинаторы синтаксического анализа.Другое представленное решение намного быстрее, но, как только вы поймете, как это сделать, вы сможете легко адаптировать его к другим задачам.
Во-первых, вы показываете сообщение электронной почты.Формат таких сообщений определен в куче RFC. RFC-822 определяет основы заголовка и тела, хотя в нем подробно описываются заголовки, но ничего не говорится о теле. RFC-1521 и 1522 говорят о MIME и сами являются пересмотрами RFC 1341 и 1342. Есть много других RFC по этому вопросу.
Интересно то, что они предоставляют грамматикиоб этом материале, так что вы можете написать парсеры для правильной декомпозиции.Давайте начнем с упрощенной версии RFC822, игнорируя все известные поля и их форматы, и просто разместим все на карте.Я делаю это потому, что грамматика довольно длинная, и те несколько строк, которые у меня есть, уже можно сравнить с теми, что в RFC.
В комбинаторах Scala Parser каждое правило отделяется ~
(вRFC, только пробелы разделяют их), и я иногда использую <~
или ~>
, чтобы отбросить неинтересную часть.Кроме того, я использовал ^^
для преобразования того, что было проанализировано, в используемую структуру данных.
import scala.util.parsing.combinator._
/** Object companion to RFC822, containing the Message class,
* and extending the trait so that it can be used as a parser
*/
object RFC822 extends RFC822 {
case class Message(header: Map[String, String], text: String)
}
/**
* Parsers `message` according to RFC-822 (http://www.w3.org/Protocols/rfc822/),
* but without breaking up the contents for each field,
* nor identifying particular fields.
*
* Also, introduces "header" to convert all fields into a map.
*/
class RFC822 extends RegexParsers {
import RFC822.Message
override def skipWhitespace = false
def message = (header <~ CRLF) ~ text ^^ {
case hd ~ txt => Message(hd, txt)
}
// this isn't part of the RFC, but we use it to generate a map
def header = field.* ^^ { _.toMap }
def field = (fieldName <~ ":") ~ fieldBody <~ CRLF ^^ { case name ~ body => name -> body }
def fieldName = """[^:\P{Graph}]+""".r
// Recursive definition needs a type
// Also, I use .+ on LWSPChar because it's specified for the lexer,
// which we are not using
def fieldBody: Parser[String] = fieldBodyContents ~ (CRLF ~> LWSPChar.+ ~> fieldBody).? ^^ {
case a ~ Some(b) => a + " " + b // reintroduces a single LWSPChar
case a ~ None => a
}
def fieldBodyContents = ".*".r
def CRLF = """\n""".r // this needs to be the regex \n pattern
def LWSPChar = " " | "\t" // these do not need to be regex
def text = "(?s).*".r // (?s) makes . match newlines
}
Теперь давайте разберемся с типом контента.Спецификация на RFC-1521 - это реализовано ниже.У меня есть слово type
между обратными чертами, потому что это зарезервированное слово в Scala.Кроме того, я делаю необязательную точку с запятой, потому что в приведенном вами примере отсутствует определение после определения char-set
.
object ContentType extends ContentType {
case class Content(`type`: String, subtype: String, parameter: Map[String, String])
}
class ContentType extends RegexParsers {
import ContentType.Content
// case-insensitive matching of type and subtype
def content = ("Content-Type" ~> ":" ~> `type` <~ "/") ~ subtype ~ parameters ^^ {
case t ~ s ~ p => Content(t, s, p)
}
// use this to generate a map
// *** SEMI-COLON IS NOT OPTIONAL ***
// I'm making it optional because the example is missing one
def parameters = (";".? ~> parameter).* ^^ (_.toMap)
// All values case-insensitive
def `type` = ( "(?i)application".r | "(?i)audio".r
| "(?i)image".r | "(?i)message".r
| "(?i)multipart".r | "(?i)text".r
| "(?i)video".r | extensionToken
)
def extensionToken = xToken | ianaToken
def ianaToken = failure("IANA token not implemented")
def xToken = """(?i)x-(?!\s)""".r ~ token ^^ { case a ~ b => a + b }
def subtype = token
def parameter = (attribute <~ "=") ~ value ^^ { case a ~ b => a -> b }
def attribute = token // case-insensitive
def value = token | quotedString
def token: Parser[String] = not(tspecials) ~> """\p{Graph}""".r ~ token.? ^^ {
case a ~ Some(b) => a + b
case a ~ None => a
}
// Must be in quoted-string,
// to use within parameter values
def tspecials = ( "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\\" | "\""
| "/" | "[" | "]" | "?" | "="
)
// These are part of RFC822
def qtext = """[^\\"\n]""".r
def quotedPair = """\\.""".r
def quotedString = "\"" ~> (qtext|quotedPair).* <~ "\"" ^^ { _.mkString }
}
Теперь мы можем использовать это для анализа текста.
object Parser {
def apply(email: String): Option[(Map[String, String], List[String])] = {
import RFC822._
parseAll (message, email) match {
case Success(result, _) =>
if (result.header get "Content-Type" nonEmpty) Some(getParts(result))
else Some(result.header -> List(result.text))
case _ => None
}
}
def getParts(message: RFC822.Message): (Map[String, String], List[String]) = {
import ContentType._
parseAll (content, "Content-Type: " + message.header("Content-Type")) match {
case Success(Content("multipart", _, parameters), _) =>
// The ^.* part eats starting characters; it doesn't seem to be
// as spec'ed, but the sample has two extra dashes at the start
// of the line
val parts = message.text split ("^.*?\\Q" + parameters("boundary") + "\\E")
val bodies = flatMap this.apply flatMap (_._2)
message.header -> bodies.toList
case _ => message.header -> List(message.text)
}
}
}
Затем вы можете использовать его как Parser(email)
.
Опять же, я не предлагаю вам использовать это решение для вашей текущей проблемы!Но изучение этого может помочь вам в будущем.