我的CSV文件带有注释,这些注释可以提供列名称,其中列在整个文件中发生变化:
#c1,c2,c3
a,b,c
d,e,f
#c4,c5
g,h
i,j
我想提供一种方法来迭代(仅)文件的数据行作为列名称映射到值(所有字符串)。所以上面会变成:
Map(c1 -> a, c2 -> b, c3 -> c)
Map(c1 -> d, c2 -> e, c3 -> f)
Map(c4 -> g, c5 -> h)
Map(c4 -> i, c5 -> j)
文件非常大,因此无法将所有内容读入内存。现在我有一个Iterator
类,在hasNext()
和next()
之间保持一些丑陋的状态;我还提供当前行号的访问器和实际的最后一行和注释读取(如果消费者关心字段顺序)。我想尝试以更实用的方式做事。
我的第一个想法是理解:我可以迭代文件的行,用过滤子句跳过注释行。我可以yield
一个包含地图,行号等的元组。问题是我需要记住最后看到的列名,以便我可以从中创建地图。对于循环可以理解,试着阻止保持状态,只允许你设置新的val
。我从this question了解到我可以更新yield
块中的成员变量,但这正是我 想要在我的情况下更新它们的时候!
我可以在迭代子句中调用一个更新状态的函数,但这看起来很脏。那么,在功能风格中执行此操作的最佳方法是什么?滥用理解?哈哈scanLeft?使用图书馆?带出parser combinator大枪?或者功能风格是不是很适合这个问题呢?
答案 0 :(得分:5)
State Monad FTW!
实际上,我在State monad吮吸。我有一段时间写这篇文章,我有一种强烈的感觉,它可以做得更好。特别是,在我看来traverse
是要走的路,但是......
// Get Scalaz on the job
import scalaz._
import Scalaz._
// Some type aliases to make stuff clearer
type Input = Stream[String]
type Header = String
type InternalState = (Input, Header)
type Output = Option[(Header, String)]
type MyState = State[InternalState, Output]
// Detect headers
def isHeader(line: String) = line(0) == '#'
// From a state, produce an output
def makeLine: (InternalState => Output) = {
case (head #:: _, _) if isHeader(head) => None
case (head #:: _, header) => Some(header -> head)
case _ => None
}
// From a state, produce the next state
def nextLine: (InternalState => InternalState) = {
case (head #:: tail, _) if isHeader(head) => tail -> head
case (_ #:: tail, header) => tail -> header
case _ => Stream.empty -> ""
}
// My state is defined by the functions producing the next state
// and the output
val myState: MyState = state(s => nextLine(s) -> makeLine(s))
// Some input to test it. I'm trimming it to avoid problems on REPL
val input = """#c1,c2,c3
a,b,c
d,e,f
#c4,c5
g,h
i,j""".lines.map(_.trim).toStream
// My State/Output Stream -- def to avoid keeping a reference to the head
def stateOutputStream = Stream.iterate(myState(input, "")){
case (s, _) => myState(s)
} takeWhile { case ((stream, _), output) => stream.nonEmpty || output.nonEmpty }
// My Output Stream -- flatMap gets rid of the None from the headers
def outputStream = stateOutputStream flatMap { case (_, output) => output }
// Now just get the map
def outputToMap: (Header, String) => Map[String, String] = {
case (header, line) =>
val keys = header substring 1 split ","
val values = line split ","
keys zip values toMap
}
// And this is the result -- note that I'm still avoiding "val" so memory
// won't leak
def result = outputStream map outputToMap.tupled
答案 1 :(得分:3)
在这里,你可以通过Iteratees做到这一点。该流表示为从Iteratee到Iteratee的函数,因此它在内存中实际上从未实现过。我使用State monad来跟踪最后遇到的标题。
import scalaz._
import Scalaz._
import IterV._
type Header = List[String]
type MyState[A] = State[Header, A]
type Out = Map[String, String]
// Detect headers
def isHeader(line: String) = line(0) == '#'
type Enumeratee[A, B, C] =
IterV[B, C] => Iteratee[MyState, A, IterV[B, C]]
// Enumerate a list. Just for demonstration.
def enumerateM[M[_]: Monad, E, A]:
(List[E], Iteratee[M, E, A]) => Iteratee[M, E, A] = {
case (Nil, i) => i
case (x :: xs, Iteratee(m)) => Iteratee(for {
v <- m
o <- v match {
case d@DoneM(_, _) => d.pure[M]
case ContM(k) => enumerateM.apply(xs, k(El(x))).value
}
} yield o)
}
def stateTrans[A]: Enumeratee[String, Map[String, String], A] =
i => Iteratee(i.fold(
done = (_, _) => DoneM(i, Empty.apply).pure[MyState],
cont = k => ContM((x: Input[String]) => x match {
case El(e) => Iteratee[MyState, String, IterV[Out, A]](for {
h <- init
o <- if (isHeader(e))
put(e substring 1 split "," toList) map (_ => Empty[Out])
else El((h zip (e split ",")).toMap).pure[MyState]
v <- stateTrans(k(o)).value
} yield v)
case Empty() => stateTrans(k(Empty.apply))
case EOF() => stateTrans(k(EOF.apply))
}).pure[MyState]
))
让我们测试一下并取出输出流的头部:
scala> (enumerateM[MyState, String, IterV[Out, Option[Out]]].apply(
| List("#c1,c2,c3","a,b,c","d,e,f"), stateTrans(head)).value ! List())
| match { case DoneM(a, _) => a match { case Done(b, _) => b } }
res0: Option[Out] = Some(Map(c1 -> a, c2 -> b, c3 -> c))
通过将这些东西抽象给辅助函数,可以做得更好。
答案 2 :(得分:2)
这是一个可能的解决方案:
首先看一下Split up a list at each element satisfying a predicate (Scala)的答案,它会给你一个groupPrefix函数。您将获得一个方法groupPrefix,它将列表拆分为列表列表,当项目满足给定谓词时拆分出现。通过这种方式,您可以拆分列表,从每个注释行开始(列定义),然后包含相应的数据
此例程将转换相应映射列表中的一个子列表(以列名开头)。
import scala.collection.immutable.ListMap
// to keep the order of the columns. If not needed, just use Map
def toNamedFields(lines: List[String]) : List[Map[String, String]] = {
val columns = lines.head.tail.split(",").toList // tail to discard the #
lines.tail.map{line => ListMap(columns.zip(line.split(",")): _*)}
}
使用它,您可以分割线条,获取每个组中的地图,获取一个地图列表列表,您可以将其变为单个列表并展平
groupPrefix(lines){_.startsWith("#")}.map(toNamedFields).flatten
答案 3 :(得分:2)
可能会更优雅,但你会得到演练:
def read(lines: Iterator[String], currentHeadings: Option[Seq[String]] = None): Stream[Option[Map[String, String]]] =
if (lines.hasNext) {
val l = lines.next
if (l.startsWith("#"))
Stream.cons(
None,
read(lines, Some(l.tail.split(","))))
else
Stream.cons(
currentHeadings.map(_.zip(l.split(",")).toMap),
read(lines, currentHeadings))
} else Stream.cons(None, Stream.Empty)
def main(args: Array[String]): Unit = {
val lines = scala.io.Source.fromFile("data.csv").getLines
println(read(lines).flatten.toList)
}
打印:
List(Map(c1 -> a, c2 -> b, c3 -> c), Map(c1 -> d, c2 -> e, c3 -> f), Map(c4 -> g, c5 -> h), Map(c4 -> i, c5 -> j))
答案 4 :(得分:2)
这就是Python ......
from collections import namedtuple
def read_shifty_csv(csv_file):
cols = None
for line in csv_file:
line = line.strip()
if line.startswith('#'):
cols = namedtuple('cols', line[1:].split(','))
else:
yield cols(*line.split(','))._asdict()
如果您宁愿使用元组而不是映射(dict),请删除_asdict()调用。 仅在内存中一次实现一行。
编辑以尝试稍微更具功能性:
from collections import namedtuple
from itertools import imap
def read_shifty_csv(csv_file):
cols = None
for line in imap(str.strip, csv_file):
if line.startswith('#'):
cols = namedtuple('cols', line[1:].split(','))
else:
yield cols(*line.split(','))._asdict()
刚刚删除了line = line.strip()
的邪恶重新分配答案 5 :(得分:1)
受@ schmichael在功能性Python解决方案上的勇敢努力的启发,这是我试图推动事情走得太远的尝试。我并没有声称它是可维护的,高效的,示范性的或可耻的,但它是有用的:
from itertools import imap, groupby, izip, chain
from collections import deque
from operator import itemgetter, methodcaller
from functools import partial
def shifty_csv_dicts(lines):
last = lambda seq: deque(seq, maxlen=1).pop()
parse_header = lambda header: header[1:-1].split(',')
parse_row = lambda row: row.rstrip('\n').split(',')
mkdict = lambda keys, vals: dict(izip(keys,vals))
headers_then_rows = imap(itemgetter(1), groupby(lines, methodcaller('startswith', '#')))
return chain.from_iterable(imap(partial(mkdict, parse_header(last(headers))), imap(parse_row, next(headers_then_rows))) for headers in headers_then_rows)
好的,让我们打开它。
基本的见解是(ab)使用itertools.groupby
来识别从标题到数据行的更改。我们使用参数评估语义来控制操作的顺序。
首先,我们告诉groupby
按行是否以'#'
开头对行进行分组:
methodcaller('startswith', '#')
创建一个函数,它接受一行并调用line.startswith('#')
(它相当于风格上更好但效率更低lambda line: line.startswith('#')
)。
因此groupby
接受lines
的传入迭代,并在返回可迭代的标题行(通常只有一个标题)和可迭代的数据行之间交替。它实际上返回(group_val, group_iter)
的元组,在这种情况下,group_val
是bool
,表示它是否是标题。因此,我们在所有元组上执行等效的(group_val, group_iter)[1]
来选择迭代器:itemgetter(1)
只是一个在你给它的任何东西上运行“[1]
”的函数(再次相当于但比lambda t: t[1]
更有效率。所以我们使用imap
在itemgetter
返回的每个元组上运行我们的groupby
函数来挑选标题/数据迭代器:
imap(itemgetter(1), groupby(lines, methodcaller('startswith', '#')))
我们首先评估该表达式并给它一个名称,因为我们稍后会使用它两次,首先是标题,然后是数据。最外面的电话:
chain.from_iterable(... for headers in headers_then_rows)
逐步完成从groupby
返回的迭代器。我们正在狡猾地调用值headers
,因为...
中的其他一些代码在我们不看时会选择rows
,在此过程中推进groupby
迭代器。这个外部生成器表达式只会产生标题(记住,它们会改变:标题,数据,标题,数据......)。诀窍是确保在行之前消耗标头,因为它们共享相同的底层迭代器。 chain.from_iterable
只是将所有数据行迭代器的结果拼接成一个迭代器以返回它们。
那么我们拼接在一起的是什么?好吧,我们需要取(最后)标题,用每行值拉上它,然后从中做出决定。这样:
last = lambda seq: deque(seq, maxlen=1).pop()
是一个有点脏但有效的黑客来从迭代器中获取最后一个项目,在本例中是我们的标题行。然后我们通过修剪前导#
和尾随换行来解析标题,然后拆分,
以获取列名列表:
parse_header = lambda header: header[1:-1].split(',')
但是,我们只希望为每个行迭代器执行一次,因为它耗尽了我们的头迭代器(我们现在不想将它复制到某个可变状态,是吗?)。我们还必须确保在行之前使用头迭代器。解决方案是制作部分应用的函数,评估和修复标题作为第一个参数,并将一行作为第二个参数:
partial(mkdict, parse_header(last(headers)))
mkdict
函数使用列名作为键,行数据作为值来制作字典:
mkdict = lambda keys, vals: dict(izip(keys,vals))
这给了我们一个冻结第一个参数(keys
)并让你传递第二个参数(vals
)的函数:正是我们用相同的键创建一堆dicts所需要的和不同的价值观。
要使用它,我们会像您期望的那样解析每一行:
parse_row = lambda row: row.rstrip('\n').split(',')
回想一下next(headers_then_rows)
将从groupby
返回数据行迭代器(因为我们已经使用了头迭代器):
imap(parse_row, next(headers_then_rows))
最后,我们将部分应用的dict-maker函数映射到解析的行:
imap(partial(...), imap(parse_row, next(headers_then_rows)))
这些都由chain.from_iterable
拼接在一起,形成一个又大又快乐,功能齐全的CSV格式。
为了记录,这可能会被简化,我仍然会以@ schmichael的方式做事。但是我学会了解决这个问题的方法,我会尝试将这些想法应用到Scala解决方案中。
答案 6 :(得分:0)
编辑:抓一点,我认为你不需要monad