【问题标题】:Processing large files using Tcl使用 Tcl 处理大文件
【发布时间】:2016-04-01 07:15:38
【问题描述】:

我在两个大文件中有一些信息。
其中之一(file1.txt,大约有 400 万行)包含所有对象名称(唯一的)和类型。
而另一个(file2.txt,有大约 200 万行)一些对象名称(它们可以重复)和分配给它们的一些值。
所以,我在file1.txt 中有类似下面的内容:

objName1 objType1
objName2 objType2
objName3 objType3
...

file2.txt 我有:

objName3 val3_1
objName3 val3_2
objName4 val4
...

对于file2.txt 中的所有对象,我需要在单个文件中输出对象名称、它们的类型和分配给它们的值,如下所示:

objType3 val3_1 "objName3"
objType3 val3_2 "objName3"
objType4 val4 "objName4"
...

以前file2.txt 中的对象名称应该是唯一的,所以我实现了一些解决方案,我从两个文件中读取所有数据,将它们保存到 Tcl 数组,然后遍历更大的数组并检查具有相同名称的对象是否存在于较小的数组中,如果存在,则将我需要的信息写入单独的文件。但这运行时间太长(> 10 小时且尚未完成)。
我该如何改进我的解决方案,或者有其他方法可以做到这一点?

编辑:
实际上我没有file1.txt,我正在通过某种程序找到该数据并将其写入 Tcl 数组。我正在运行一些程序来获取对象类型并将它们保存到 Tcl 数组,然后,我正在读取 file2.txt 并将数据保存到 Tcl 数组,然后我正在迭代第一个数组中的项目,如果对象名称匹配第二个(对象值)数组中的某个对象,我正在将信息写入输出文件并从第二个数组中删除该元素。这是我正在运行的一段代码:

set outFileName "output.txt"
if [catch {open $outFileName "w"} fid ] {
   puts "ERROR: Failed to open file '$outFileName', no write permission"
   exit 1
}


# get object types
set TIME_start [clock clicks -milliseconds]
array set objTypeMap [list]
# here is some proc that fills up objTypeMap
set TIME_taken [expr [clock clicks -milliseconds] - $TIME_start]
puts "Info: Object types are found. Elapsed time $TIME_taken"

# read file2.txt
set TIME_start [clock clicks -milliseconds]
set file2 [lindex $argv 5]
if [catch { set fp [open $file2 r] } errMsg] {
    puts "ERROR: Failed to open file '$file2' for reading"
    exit 1
}

set objValData [read $fp]
close $fp
# tcl list containing lines of file2.txt
set objValData [split $objValData "\n"]
# remove last empty line
set objValData [lreplace $objValData end end]
array set objValMap [list]
foreach item $objValData {
    set objName [string range $item 0 [expr {[string first " " $item] - 1}] ]
    set objValue [string range $item [expr {[string first " " $item] + 1}] end ]
    set objValMap($instName) $objValue
}
# clear objValData
unset objValData

set TIME_taken [expr [clock clicks -milliseconds] - $TIME_start]
puts "Info: Object value data is read and processed. Elapsed time $TIME_taken"

# write to file
set TIME_start [clock clicks -milliseconds]
foreach { objName objType } [array get objTypeMap] {
    if { [array size objValMap] eq 0 } {
        break
    }
    if { [info exists objValMap($objName)] } {
        set objValue $objValMap($objName)
        puts $fid "$objType $objValue \"$objName\""
        unset objValMap($objName)
    }
}

if { [array size objValMap] neq 0 } {
    foreach { objName objVal } [array get objValMap] {
        puts "WARNING: Can not find object $objName type, skipped..."
    }
}
close $fid

set TIME_taken [expr [clock clicks -milliseconds] - $TIME_start]
puts "Info: Output is cretaed. Elapsed time $TIME_taken"

似乎最后一步(写入文件)有大约 8 * 10^12 次迭代要做,在合理的时间内完成是不现实的,因为我已经尝试做 8 * 10^12 次迭代在 for 循环中,只打印迭代索引,~850*10^6 次迭代花费了约 30 分钟(因此,整个循环将在约 11 小时内完成)。
所以,应该有另一种解决方案。

编辑: 似乎原因是 file2.txt 地图的一些不成功的散列,因为我试图在 file2.txt 中洗牌并在大约 3 分钟内得到结果。

【问题讨论】:

  • 如果没有看到你运行的代码,就很难回答这个问题。
  • 好的,我将通过添加一段代码来更新我的问题。
  • @Jackson,请在我的问题中查看更新。
  • 10 小时有点长。我在 6 小时内处理了 3200 万行。不过,对于大文件,我不使用read,而是使用whilegets 一次获取一行(因此无需拆分,也不会在少数变量中消耗大量内存) .
  • 您应该考虑将该数据导入 sqlite 数据库并使用 sql 创建结果数据。

标签: file join tcl large-data


【解决方案1】:

将数据写入file1,让外部工具完成所有繁重的工作(它肯定比自制的Tcl代码更适合该任务)

exec bash -c {join -o 0,1.2,2.2 <(sort file1.txt) <(sort file2.txt)} > result.txt

【讨论】:

  • 感谢您的回答,但我收到如下错误:join: file 1 is not in sorted orderjoin: file 2 is not in sorted order
  • 我尝试使用-k 1b,1 选项对它们进行排序,解决了问题,但输出不正确,我只得到每行出现 3 次的对象名称。
  • 我很好奇:这需要多长时间才能完成?
  • 这大约需要 1 分钟。但似乎有些不清楚,因为我的输出文件只有 2722 行。如果 file2 有大约 200 万行,而 file1 有大约 400 万行。使用 Peter Lewerin 下面建议的其他解决方案,我的输出文件有 7250 行。
  • 它们都应该出现在结果中吗?您没有很好地说明您的要求。
【解决方案2】:

所以...file1.txt 是在描述一个映射,file2.txt 是要处理和注释的事物列表?正确的做法是将映射加载到数组或字典中,其中键是您将查找的部分,然后逐行浏览另一个文件。这样可以减少内存中的数据量,但无论如何保持整个映射都是值得的。

# We're doing many iterations, so worth doing proper bytecode compilation 
apply {{filename1 filename2 filenameOut} {
    # Load the mapping; uses memory proportional to the file size
    set f [open $filename1]
    while {[gets $f line] >= 0} {
        regexp {^(\S+)\s+(.*)} $line -> name type
        set types($name) $type
    }
    close $f

    # Now do the streaming transform; uses a small fixed amount of memory
    set fin [open $filename2]
    set fout [open $filenameOut "w"]
    while {[gets $fin line] >= 0} {
        # Assume that the mapping is probably total; if a line fails we're print it as
        # it was before. You might have a different preferred strategy here.
        catch {
            regexp {^(\S+)\s+(.*)} $line -> name info
            set line [format "%s %s \"%s\"" $types($name) $info $name]
        }
        puts $fout $line
    }
    close $fin
    close $fout

    # All memory will be collected at this point
}} "file1.txt" "file2.txt" "fileProcessed.txt"

现在,如果映射非常大,以至于它不适合内存,那么您可能最好通过构建文件索引和类似的东西来做到这一点,但坦率地说,您实际上最好熟悉一下使用 SQLite 或其他一些数据库。

【讨论】:

  • 非常感谢!这工作得很快。似乎原因是迭代较小的数据并在较大的数据中搜索名称。我已经更改了我的解决方案,以相同的方式进行迭代,并获得了几乎相同的运行时间。
  • @Donal_Fellows:请您帮忙理解一下我的猜测是否正确?
  • 我已经在file2.txt 中改组了行,并得到了几乎相同的运行时间和可能的初始解决方案。
【解决方案3】:

Glenn Jackman 代码的纯 Tcl 变体是

package require fileutil
package require struct::list

set data1 [lsort -index 0 [split [string trim [fileutil::cat file1.txt]] \n]]
set data2 [lsort -index 0 [split [string trim [fileutil::cat file2.txt]] \n]]
fileutil::writeFile result.txt [struct::list dbJoin -full 0 $data1 0 $data2]

但在这种情况下,每一行将有 列,而不是三列:file1.txt 中的两列和 file2.txt 中的两列。如果这是一个问题,将列数减少到三是微不足道的。

示例中的文件连接也是完整的,即两个文件中的所有行都将出现在结果中,如果另一个文件没有相应的数据,则用空字符串填充。为了解决 OP 的问题,内连接可能更好(只保留对应的行)。

fileutil::cat 读取文件的内容,string trim 从内容中删除前导和尾随空格,以避免开头或结尾出现空行,split ... \n 创建一个列表,其中每一行都成为一个项目,@987654335 @ 根据每个项目中的第一个单词对该列表进行排序。

该代码经验证可与 Tcl 8.6 和 fileutil 1.14.8 一起使用。 fileutil 包是 Tcl 的 Tcllib 配套库的一部分:可以通过下载 Tcl 源并将其复制到 Tcl 安装的 lib 树(@ 987654340@ 在我的情况下)。

快速安装:从here 下载fileutil.tcl(使用“下载”按钮)并将文件复制到其他来源所在的位置。在您的源代码中,调用source fileutil.tcl,然后调用package require fileutil。 (Tcl 或 cmdline 包等可能仍存在兼容性问题。阅读源代码可能会建议解决方法。)记得检查许可证条件是否存在冲突。

文档:fileutil 包、lsortpackagesetsplitstringstruct::list

【讨论】:

  • 感谢您的回答,但运行此脚本时出现invalid command name "fileutil::writeFile" 错误
  • @Heghine: fileutil::cat 可以,但fileutil::writeFile 不行吗?这真的很奇特。
  • 是的,我只在fileutil::writeFile invalid command name "fileutil::writeFile" like below while executing "::_unknown_ fileutil::writeFile result1.txt {{name 1 val1 name1 type1..." ("uplevel" body line 1) ... 上遇到错误
  • @Heghine:package require fileutil的结果是版本号。
  • @Heghine:我强烈建议你升级 1) Tcl,2) Tcllib,或者至少升级fileutil
猜你喜欢
  • 1970-01-01
  • 2013-10-23
  • 2010-09-30
  • 2016-09-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多