【问题标题】:How can I remove the first line of a text file using bash/sed script?如何使用 bash/sed 脚本删除文本文件的第一行?
【发布时间】:2010-09-25 06:22:51
【问题描述】:

我需要使用 bash 脚本从一个巨大的文本文件中反复删除第一行。

现在我正在使用sed -i -e "1d" $FILE - 但删除操作大约需要一分钟。

有没有更有效的方法来做到这一点?

【问题讨论】:

  • -i 代表什么?
  • @cikatomo:它代表内联编辑 - 它使用您生成的任何内容来编辑文件。
  • tail 比 sed 慢得多。 tail 需要 13.5s,sed 需要 0.85s。我的文件有~1M 行,~100MB。配备 SSD 的 MacBook Air 2013。

标签: bash scripting sed


【解决方案1】:

试试tail:

tail -n +2 "$FILE"

-n x:只打印最后的x 行。 tail -n 5 会给你输入的最后 5 行。 + 符号会反转参数并使tail 打印除第一行x-1 之外的任何内容。 tail -n +1 会打印整个文件,tail -n +2 会打印除第一行以外的所有内容,等等。

GNU tailsed 快得多。 tail 在 BSD 上也可用,-n +2 标志在这两个工具中是一致的。查看FreeBSDOS X 手册页了解更多信息。

不过,BSD 版本可能比sed 慢得多。我想知道他们是如何做到的; tail 应该只是逐行读取文件,而 sed 会执行非常复杂的操作,包括解释脚本、应用正则表达式等。

注意:您可能很想使用

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

但这会给你一个空文件。原因是重定向(>)发生在 tail 被 shell 调用之前:

  1. Shell 截断文件$FILE
  2. Shell 为tail 创建一个新进程
  3. Shell 将tail 进程的标准输出重定向到$FILE
  4. tail 从现在空的 $FILE 读取

如果你想删除文件中的第一行,你应该使用:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

&& 将确保文件在出现问题时不会被覆盖。

【讨论】:

  • 根据ss64.com/bash/tail.html,当使用带有-r 选项的BSD 'tail' 时,典型的缓冲区默认为32k。也许系统中某处有缓冲区设置?或者-n 是一个 32 位有符号数?
  • @Eddie: user869097 说当 single 行为 15Mb 或更多时它不起作用。只要行更短,tail 将适用于任何文件大小。
  • 你能解释一下这些论点吗?
  • @Dreampuf - 来自手册页:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
  • 我打算同意@JonaChristopherSahnwaldt - tail 比 sed 变体慢一个数量级。我正在对一个 500,000K 行的文件(每行不超过 50 个字符)进行测试。然而,我随后意识到我使用的是 FreeBSD 版本的 tail(默认情况下随 OS X 提供)。当我切换到 GNU tail 时,tail 调用比 sed 调用(以及 GNU sed 调用)快 10 倍。如果您使用的是 GNU,AaronDigulla 在这里是正确的。
【解决方案2】:

您可以使用 -i 来更新文件,而无需使用 '>' 运算符。以下命令将从文件中删除第一行并将其保存到文件中(在幕后使用temp file)。

sed -i '1d' filename

【讨论】:

  • 我收到错误:unterminated transform source string
  • 这每次都有效,应该是最佳答案!
  • 请记住,Mac 需要在将 sed 与就地编辑一起使用时提供后缀。所以运行上面的 -i.bak
  • 请注意 - 删除多行使用sed -i '1,2d' filename
  • 这个版本确实比tail -n +2更具可读性和通用性。不知道为什么它不是最佳答案。
【解决方案3】:

对于那些使用非 GNU 的 SunOS 的人,以下代码会有所帮助:

sed '1d' test.dat > tmp.dat 

【讨论】:

  • 有趣的人口统计
【解决方案4】:

您可以通过以下方式轻松做到这一点:

cat filename | sed 1d > filename_without_first_line

在命令行上;或者要永久删除文件的第一行,请使用带有 -i 标志的 sed 就地模式:

sed -i 1d <filename>

【讨论】:

  • -i 选项在技术上接受一个参数,指定备份文件时要使用的文件后缀(例如,sed -I .bak 1d filename 使用第一行创建一个名为 filename.bak 的原始文件副本完好无损的)。虽然 GNU sed 允许您在没有参数的情况下指定 -i 以跳过备份,但在 macOS 上发现的 BSD sed 需要一个空字符串参数作为单独的 shell 字(例如 sed -i '' ...)。
【解决方案5】:

不,这与您将获得的效率差不多。您可以编写一个 C 程序,它可以更快地完成这项工作(更少的启动时间和处理参数),但随着文件变大,它可能会趋向与 sed 相同的速度(如果需要一分钟,我假设它们很大) )。

但是您的问题与许多其他问题存在相同的问题,因为它预先假定了解决方案。如果您要详细告诉我们您想要做什么,而不是如何,我们或许可以提出更好的选择。

例如,如果这是其他程序 B 处理的文件 A,则一种解决方案是不删除第一行,而是修改程序 B 以不同方式处理它。

假设您的所有程序都附加到此文件 A 和程序 B 当前读取并处理第一行,然后再删除它。

您可以重新设计程序 B,使其不会尝试删除第一行,而是在文件 A 中保持一个持久的(可能是基于文件的)偏移量,以便下次运行时,它可以寻找那个偏移,处理那里的行,并更新偏移。

然后,在安静的时间(午夜?),它可以对文件 A 进行特殊处理,以删除当前处理的所有行并将偏移量设置回 0。

程序打开并查找文件肯定比打开和重写更快。当然,此讨论假设您可以控制程序 B。我不知道是否是这种情况,但如果您提供更多信息,可能还有其他可能的解决方案。

【讨论】:

  • 我认为 OP 正在努力实现让我发现这个问题的原因。我有 10 个 CSV 文件,每个文件有 500k 行。每个文件的标题行与第一行相同。我正在 cat: 将这些文件放入一个文件中,然后将它们导入数据库,让数据库从第一行创建列名。显然我不希望在文件 2-10 中重复该行。
  • @d-b 在这种情况下,awk FNR-1 *.csv 可能更快。
【解决方案6】:

sponge util 避免了处理临时文件的需要:

tail -n +2 "$FILE" | sponge "$FILE"

【讨论】:

  • sponge 确实比公认的解决方案 (tail -n +2 "$FILE" &gt; "$FILE.tmp" &amp;&amp; mv "$FILE.tmp" "$FILE") 更干净、更健壮
  • 这是唯一对我有用的解决方案来更改系统文件(在 Debian docker 映像上)。尝试写入文件时,其他解决方案因“设备或资源忙”错误而失败。
  • 但是sponge 会在内存中缓冲整个文件吗?如果它是数百 GB,那就行不通了。
  • @OrangeDog,只要文件系统可以存储它,sponge 就会吸收它,因为它使用 /tmp 文件作为中间步骤,即然后用来替换原来的。
【解决方案7】:

如果你想修改文件,你总是可以使用原来的ed而不是它的streaming后继sed

ed "$FILE" <<<$'1d\nwq\n'

ed 命令是最初的 UNIX 文本编辑器,甚至还没有全屏终端,更不用说图形工作站了。 ex 编辑器,也就是您在vi 的冒号提示符下键入时所使用的最广为人知的编辑器,是ed 的一个ex倾向版本,因此许多相同的命令都可以工作。虽然ed 旨在以交互方式使用,但它也可以通过向其发送一串命令以批处理模式使用,这就是该解决方案的作用。

&lt;&lt;&lt;$'1d\nwq\n' 序列利用现代 shell 对此处字符串 (&lt;&lt;&lt;) 和 ANSI 引号 ($'...') 的支持,将输入提供给由两个组成的 ed 命令行:1dd删除第 1 行,然后 wqw将文件写入磁盘,然后q适合编辑会话。

【讨论】:

  • 但是你必须把整个文件读入内存,如果是几百GB就不行了。
  • 在 Mac 上工作,无需任何操作 (zsh)。
【解决方案8】:

正如 Pax 所说,您可能不会比这更快。原因是几乎没有文件系统支持从文件开头截断,所以这将是一个 O(n) 操作,其中n 是文件的大小。你可以做的 much 更快,但是用相同数量的字节(可能带有空格或注释)覆盖第一行,这可能对你有用,具体取决于你正在尝试做什么(什么是顺便说一下?)。

【讨论】:

  • Re "...几乎没有支持截断的文件系统...":这很有趣;请考虑在括号中添加命名此类文件系统的注释。
  • @agc:现在无关紧要,但我在 70 年代的第一份工作是在 Quadex,一家小型初创公司(现已消失,与现在使用该名称的两家公司无关)。他们有一个文件系统,允许在文件的开头或结尾添加 删除,主要用于通过在文件中放置窗口上方和窗口下方来实现小于 3KB 的编辑。它没有自己的名字,它只是 QMOS(Quadex 多用户操作系统)的一部分。 ('Multi' 通常是 2-3 在 LSI-11/02 上,RAM 小于 64KB,通常有几个 RX01 型 8" 软盘,每个 250KB。):-)
【解决方案9】:

可以就地编辑文件:只需使用 perl 的 -i 标志,如下所示:

perl -ni -e 'print unless $. == 1' filename.txt

如您所问,这会使第一行消失。 Perl 将需要读取和复制整个文件,但它会安排将输出保存在原始文件的名称下。

【讨论】:

    【解决方案10】:

    应该显示除第一行之外的行:

    cat textfile.txt | tail -n +2
    

    【讨论】:

    • - 你应该做“tail -n +2 textfile.txt”
    • @niglesiais 我不同意“无用的使用 cat”,因为它清楚地表明此解决方案适用于管道内容,而不仅仅是文件。
    【解决方案11】:

    可以使用 vim 来做到这一点:

    vim -u NONE +'1d' +'wq!' /tmp/test.txt
    

    这应该更快,因为 vim 在处理时不会读取整个文件。

    【讨论】:

    • 如果你的 shell 是 bash,可能需要引用 +wq!。可能不会,因为! 不是单词的开头,但是养成引用事物的习惯可能对所有方面都很好。 (如果你想通过不必要的引用来提高效率,你也不需要 1d 周围的引号。)
    • vim 是否需要读取整个文件。事实上,如果文件大于内存,正如这个 Q 中所要求的,vim 会读取整个文件并将其(或大部分)写入临时文件,并在编辑后将其全部写回(永久文件)。我不知道你认为它如何没有这个。
    【解决方案12】:

    使用 csplit 怎么样?

    man csplit
    csplit -k file 1 '{1}'
    

    【讨论】:

    • 这种语法也可以,但只生成两个输出文件而不是三个:csplit file /^.*$/1。或者更简单地说:csplit file //1。或者更简单:csplit file 2.
    【解决方案13】:

    这个班轮就可以了:

    echo "$(tail -n +2 "$FILE")" > "$FILE"
    

    它可以工作,因为tailecho 之前执行,然后文件被解锁,因此不需要临时文件。

    【讨论】:

      【解决方案14】:

      由于听起来我无法加快删除速度,我认为一个好的方法可能是像这样批量处理文件:

      While file1 not empty
        file2 = head -n1000 file1
        process file2
        sed -i -e "1000d" file1
      end
      

      这样做的缺点是,如果程序在中间被杀死(或者如果那里有一些错误的 sql - 导致“进程”部分死亡或锁定),将会有一些行被跳过,或处理两次。

      (file1 包含多行 sql 代码)

      【讨论】:

      • 第一行包含什么?你能像我在帖子中建议的那样用 sql 评论覆盖它吗?
      【解决方案15】:

      如果您希望在失败后恢复,您可以构建一个包含您目前所做的文件的文件。

      if [[ -f $tmpf ]] ; then
          rm -f $tmpf
      fi
      cat $srcf |
          while read line ; do
              # process line
              echo "$line" >> $tmpf
          done
      

      【讨论】:

        【解决方案16】:

        基于其他 3 个答案,我想出了在我的 Mac OSx bash shell 中完美运行的语法:

        line=$(head -n1 list.txt &amp;&amp; echo "$(tail -n +2 list.txt)" &gt; list.txt)

        测试用例:

        ~> printf "Line #%2d\n" {1..3} > list.txt
        ~> cat list.txt
        Line # 1
        Line # 2
        Line # 3
        ~> line=$(head -n1 list.txt && echo "$(tail -n +2 list.txt)" > list.txt)
        ~> echo $line
        Line # 1
        ~> cat list.txt
        Line # 2
        Line # 3
        

        【讨论】:

          【解决方案17】:

          是否会在 N-1 行上使用 tail 并将其定向到一个文件中,然后删除旧文件,并将新文件重命名为旧名称来完成这项工作?

          如果我以编程方式执行此操作,我将通读文件,并在读取每一行后记住文件偏移量,因此我可以回到该位置以读取其中少一行的文件。

          【讨论】:

          • 第一个解决方案与布伦特现在所做的基本相同。我不明白您的编程方法,只需要删除第一行,您只需读取并丢弃第一行并将其余部分复制到另一个文件,该文件再次与 sed 和 tail 方法相同。
          • 第二种解决方案意味着文件每次都不会被第一行缩小。程序只是简单地处理它,就好像它被缩小了一样,但每次都从下一行开始
          • 我还是不明白你的第二个解决方案是什么。
          猜你喜欢
          • 2018-10-03
          • 1970-01-01
          • 2023-03-30
          • 2018-04-20
          • 1970-01-01
          • 2013-12-20
          • 1970-01-01
          • 2016-08-17
          • 2011-07-16
          相关资源
          最近更新 更多