用命令结果替换列

用命令结果替换列

我正在尝试找到一种方法来快速有效地用命令的输出替换文件中每一行的列值。我每天需要处理大约 500,000 行的多个文件,因此我正在寻找可以尽快完成任务的东西。

我需要将逗号分隔行的第八列作为输入,运行命令,并将该列替换为命令的输出。

这是我已经尝试过的,虽然它有效,但速度非常慢:

awk -F "," 'NR > 1 {
    cmd = "cdrtoip " $8
    cmd | getline ip
    close(cmd)
    $8=ip
    print
}' $1.csv >> $1.csv.tmp

我更愿意坚持使用 Bash 或其他可以在 Linux 服务器上预安装的 Linux 程序。

编辑:我很抱歉,我应该包括 cdrtoip 是什么。

# Convert CISCO format (signed integer) to Hex
# Capitalize or else conversion from hex to decimal doesn't work later
HEXIP=$(printf '%x\n' $1 | tr '[:lower:]' '[:upper:]')

# Negative numbers will get 8 'f' in front of them
# Trim that part off
if [[ ${#HEXIP} -eq 16 ]]; then
    HEXIP=${HEXIP:8:8}
fi

# Convert hex to decimal, separate into octets, put in order
OCTETS[0]=$(echo "ibase=16; ${HEXIP:6:2}" | bc)
OCTETS[1]=$(echo "ibase=16; ${HEXIP:4:2}" | bc)
OCTETS[2]=$(echo "ibase=16; ${HEXIP:2:2}" | bc)
OCTETS[3]=$(echo "ibase=16; ${HEXIP:0:2}" | bc)

# Print the IP
echo ${OCTETS[0]}.${OCTETS[1]}.${OCTETS[2]}.${OCTETS[3]}

cdrip 上的运行时间给出:

    0.23s real     0.00s user     0.02s system

答案1

我知道你说过你想坚持使用本机应用程序,但是GNU 并行将允许您并行执行单独的进程,这将允许您更快地运行此操作:

sudo apt-get update
sudo apt-get install parallel
awk -F',' '{print $8}' file.csv | parallel -j+0 cdrtoip {}

调用 的方法有很多种parallel,但上述方法将从 .csv 文件的第 8 列获取输出,并cdrtoip在系统的每一行上同时为每个核心执行一个进程。因此,基本上,如果您运行 4 个核心,则可以用正常执行时间的 25% 来完成这项工作。

它的优点parallel是它跟踪输出并按顺序生成它,就好像它只是一个正在运行的作业一样。

安装后,man parallel了解更多执行方式(或查看链接中的文档)。很抱歉,如果这不是您正在寻找的东西,但它过去曾多次为我提供帮助。

编辑:如果要将输出添加回 .csv 以替换第 8 列,请使用下面的示例将要工作,以及经测试。在双核 Macbook Pro 上执行 5,000 行 .csv 文件大约需要 3.25 分钟。

设置:

$ cat file.tmp
blah1,blah2,blah3,blah4,blah5,blah6,blah7,1175063050,blah9,blah10,blah11

$ for i in {1..5000}; do cat file.tmp; done > file.csv

$ wc -l < file.csv
5000

脚本(使用cdrtoip您提供的):

$ cat csvjob.sh
#!/bin/bash

fragment1="$(cut -d, -f1-7 file.csv | tr ',' "\t")"
fragment2="$(cut -d, -f8 file.csv | parallel -j+0 cdrtoip {})"
fragment3="$(cut -d',' -f9- file.csv | tr ',' "\t")"

paste <(echo "$fragment1") <(echo "$fragment2") <(echo "$fragment3") | sed "s/\t/,/g" > newfile.csv

结果:

$ time ./csvjob.sh
real    3m23.092s
user    1m22.245s
sys     2m57.794s

$ head -3 newfile.csv
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11
blah1,blah2,blah3,blah4,blah5,blah6,blah7,10.10.10.70,blah9,blah10,blah11

另一个编辑: 以下是在四核 Mac Mini 上执行的(还运行其他东西):

$ time ./csvjob.sh
real    2m12.171s
user    2m59.816s
sys     2m15.787s

我还刚刚意识到你说的是 500,000 行而不是 5,000 行。对于它的价值,请参阅下面cdrtoip连续执行 5,000 次的统计数据:

$ time for i in {1..5000}; do cdrtoip 1175063050; done > /dev/null
real    2m32.487s
user    1m26.537s
sys     1m8.270s

最终编辑: 以下是在四核 Mac Mini 上运行的 500,000 行文件,如前所述,该文件已经在运行多个应用程序:

$ time ./csvjob.sh

real    216m22.780s
user    301m40.694s
sys     239m44.404s

我完全明白你的意思,OP。

即使并行运行,这也需要相当长的时间才能执行。

我看到OP找到了更好的解决方案。每个文件 126 秒是无与伦比的。再次强调一下,下面是在 8 核 Debian VM 上使用(我意识到 OP 无法安装)运行最初提供的cdrtoip500,000 行 .csv的统计数据:parallel

$ time ./csvjob.sh
real    14m7.467s
user    6m3.883s
sys     4m18.556s

答案2

以下内容应该适用于awk支持用户定义函数以及内置函数sprintf()rshift()函数的任何版本。这包括 GNU awk。

我从这里借用了十进制并将其改编为点分四组 IP 地址算法:

https://stackoverflow.com/questions/29025177/how-can-i-convert-a-hex-ip-address-to-dotted-decimal-notation

正如我的评论中提到的,将cdrtoip外部脚本重写为 awk 函数将避免调用外部脚本超过 500,000 次。

awk -F, '
function cdrtoip(addr) {
  return sprintf ("%d.%d.%d.%d",
           rshift(and(addr,0xff000000),24),
           rshift(and(addr,0x00ff0000),16),
           rshift(and(addr,0x0000ff00),08),
           rshift(and(addr,0x000000ff),00))
};

NR > 1 {
    $8 = cdrtoip($8);
    print
}' "$1.csv" >> "$1.csv.tmp"

我在一个包含 500,000 行的测试文件上运行了这个,它在 2 秒内完成:

$ wc -l input.csv 
500000 input.csv
$ time ./michael.sh < input.csv > output.csv

real 0m1.956s   user 0m1.935s   sys 0m0.018s

答案3

cdrtoip确实相当慢,它看起来像一个有用的实用程序脚本,但可能不打算在循环中调用数百次。我假设它是其他脚本或用户使用的常用工具,并且您希望继续使用它但使其速度更快。

只需调用 1 次bc(而不是 4 次)即可使脚本运行时间缩短约 1/3。使用 shell 转换代替bc可以使脚本运行时间缩短约 1/5。

我制作了一个简短的框架来生成一堆示例输入(大约 500 行),然后运行两个脚本orig.sh(原始版本cdrtoip)和new.sh修改版本,并对它们进行计时并比较它们的输出。看起来像:

INPUT_SIZE=500
SAMPLE_FILE=in.txt

rm -f $SAMPLE_FILE orig.out new.out

x=0
while [[ $((x++)) -le $INPUT_SIZE ]]; do
    tr -cd '[:digit:]' < /dev/urandom | head -c 10 | sed s/^0/1/ >> $SAMPLE_FILE
    echo >> $SAMPLE_FILE
    if [[ $((x%10)) -eq 0 ]]; then echo -n .; fi
    if [[ $((x%20)) -eq 0 ]]; then echo -n '-' >> $SAMPLE_FILE; fi # next num is negative
done
echo

echo new cdrtoip:
time while read line; do ./new.sh $line >> new.out; done < $SAMPLE_FILE

echo original cdrtoip:
time while read line; do ./orig.sh $line >> orig.out; done < $SAMPLE_FILE

diff -q orig.out new.out || echo "Output was different!"

一次调用的输出bc

$ ./generate.sh 
..................................................
new cdrtoip:

real    0m1.431s
user    0m0.036s
sys     0m0.072s
original cdrtoip:

real    0m4.381s
user    0m0.040s
sys     0m0.084s

这是我的new.sh。如果您想要更快的版本,请注释掉该bc行并取消注释掉其下面的转换(大约 0.85 秒),并且您还可以摆脱大写${HEXIP^^}。如果你保留,${HEXIP^^}你可能应该包含一个bashshebang,因为它不能在所有 shell 中工作(特别是,它会在 dash 中失败)。

#!/bin/bash
# Convert CISCO format (signed integer) to Hex
# Capitalize or else conversion from hex to decimal doesn't work later
HEXIP=$(printf '%x' $1)
HEXIP=${HEXIP^^}

# Negative numbers will get 8 'f' in front of them
# Trim that part off
if [[ ${#HEXIP} -eq 16 ]]; then
    HEXIP=${HEXIP:8:8}
fi

# Convert hex to decimal, separate into octets, put in order
bc <<< "ibase=16; ${HEXIP:6:2}; ${HEXIP:4:2}; ${HEXIP:2:2}; ${HEXIP:0:2}" | tr '\n' . | sed 's/[\.]$/\n/'

# Convert hex to decimal, separate into octets, put in order
# using just bash: doesn't require hex characters to be upper case
#o0=$((16#${HEXIP:6:2}))
#o1=$((16#${HEXIP:4:2}))
#o2=$((16#${HEXIP:2:2}))
#o3=$((16#${HEXIP:0:2}))

# Print the IP
#echo $o0.$o1.$o2.$o3

答案4

正如 John1024 指出的那样,速度缓慢的最大嫌疑是 cdrtoip 的调用 500,000 次。

编辑:基于提供的 cdrtoip 脚本,整个实现都是用 Python 编写的。由于无需调用外部脚本,因此速度要快得多。

我建议你看看Python。对于此类任务,Python 的性能非常好,而且标准 Python 库中包含一个现有的模块来处理 csv 文件。

以下是 Python 中的示例实现。该示例像 awk 脚本一样读取和写入 stdin/stdout,但可以轻松修改它以打开文件。编辑:更好地清理和处理转换错误。在处理结束时向 stderr 提供摘要。

#!/usr/bin/python
import sys,csv

# Convert CISCO format (signed integer) to Hex
# Based on original cdrtoip script in bash
# Note that a ValueError is raised if conversion cannot be done.
def cdrtoip(addrfield):
  intaddr=int(addrfield)    # ValueError if not a valid int

  # Range-check the integer, make it unsigned
  # If out of range, raise a ValueError
  if intaddr < 0: intaddr=intaddr+0x100000000
  if intaddr < 0: raise ValueError
  if intaddr > 0xffffffff : raise ValueError

  return ".".join( [ str(intaddr >> i & 0xff) for i in (24,16,8,0) ] )

# There are other options, depending on the exact file format
# you want. See: https://docs.python.org/2/library/csv.html
indata=csv.reader(sys.stdin)
outdata=csv.writer(sys.stdout)
header=True
no_convert=0
invalid_row=0
row_converted=0
blank_row=0
for row in indata:
   # Write the first line unchanged...
   if header:
      header=False
   else:
      # Note that columns are numbered from 0
      if len(row) == 0:
         blank_row=blank_row+1
         continue
      elif len(row) > 7:
         try:
            row[7]=cdrtoip(row[7])
            row_converted=row_converted+1
         except ValueError:
            # if conversion fails, we count and leave the field unchanged.
            no_convert=no_convert+1
      else:
         # if there is no column 8 we count as invalid row.
         invalid_row=invalid_row+1

   outdata.writerow(row)

# Print a summary of work done (to stderr).
print >> sys.stderr,"%d values converted." % row_converted
if no_convert > 0:
   print >> sys.stderr,"%d values not converted." % no_convert
if invalid_row > 0:
   print >> sys.stderr,"%d rows not valid." % invalid_row
if blank_row > 0:
   print >> sys.stderr,"%d blank rows removed." % blank_row

相关内容