使用 Tcl 处理大文件

Processing large files using Tcl

我在两个大文件中有一些信息。
其中之一(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^ for 循环中的 12 次迭代并仅打印迭代索引,~850*10^6 次迭代耗时~30 分钟(因此,整个循环将在~11 小时内完成)。
所以,应该有另一种解决方案。

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

将数据写入 file1,让外部工具完成所有艰苦的工作(它肯定比自制的 Tcl 代码更优化任务)

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

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 创建一个列表,其中每行成为一个项目,lsort -index 0 根据每个项目中的第一个单词对该列表进行排序。

代码经验证可与 Tcl 8.6 和 fileutil 1.14.8 一起使用。 fileutil 包是 Tcllib Tcl 配套库的一部分:可以通过下载 Tcl 源并将其复制到 Tcl 安装的 lib 树(在我的例子中是 C:\Tcl\lib\teapot\package\tcl\teapot\tcl8.2)。

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

文档:fileutil package, lsort, package, set, split, string, struct::list

那么……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 或其他一些数据库。