
수백만 줄의 텍스트를 구문 분석하는 가장 빠른 방법은 어떤 쉘 명령입니까? 현재 스크립트에서 GREP를 사용하고 있지만 완료하는 데 몇 시간이 걸립니다.
샘플 입력:
May 1 2014 00:00:00 Allow
May 1 2014 00:00:00 Allow
May 1 2014 01:00:00 Deny
May 1 2014 01:00:00 Deny
May 1 2014 02:00:00 Allow
May 1 2014 02:00:00 Deny
샘플 출력:
(여기서 첫 번째 줄의 "2"는 'grep -c "allow" '이고 "0"은 'grep -c "deny"'입니다)
May 1 2014 00:00:00,2,0
May 1 2014 01:00:00,0,2
May 1 2014 02:00:00,1,1
답변1
정규 표현식에서 벗어나세요. (모든 언어에서) 느리고 간단한 하위 문자열 비교에 필요한 것보다 훨씬 더 많습니다.
- 0:20의 하위 문자열을 첫 번째 키로 사용
- 두 번째 키에 대한 부울 결과로 21:22(단일 문자)의 하위 문자열을 사용합니다.
- 해당 조합의 값은 볼 때마다 증가하는 정수여야 합니다.
저는 그 아이디어를 아래 Python으로 구현했습니다.
data = {}
with open("file", "r") as f:
for line in f:
key = line[0:20]
allowed = int(line[21:22] != "A")
if not key in data:
data[key] = [0,0]
data[key][allowed] += 1
for key, val in data.items():
print('%s,%d,%d' % (key, val[0], val[1]))
성능이 어떤지는 모르겠지만 시도해 보세요. 속도가 느리다면 C++로 변환하세요(PITA에 좀 더 가깝기 때문에 Python을 사용하고 있습니다!). 그러면 데이터가 찢어질 것입니다. 어려운 프로그래밍은 아니지만 최적의 속도를 위해 필요한 것입니다.
약간의 리팩토링이 필요하며 다음과 동등한 것이 없으면 포팅하기가 더 어렵습니다 defaultdict
.
from collections import defaultdict
data = defaultdict(lambda: [0,0])
with open("file", "r") as f:
for line in f:
key = line[0:20]
allowed = int(line[21:22] != "A")
data[key][allowed] += 1
for key, val in data.items():
print('%s,%d,%d' % (key, val[0], val[1]))
그리고 steeldriver와 내 아이디어를 혼합한 Python 구현입니다. 이것은 아마도 가장 효율적인 메모리이며 정규식 추출 대신 하위 문자열 비교를 사용하므로 깔끔해야 합니다.하지만 정렬된 입력이 필요했습니다.
last = ""
score = [0,0]
with open("file", "r") as f:
for line in f:
key = line[0:20]
allowed = int(line[21:22] != "A")
if key and key != last:
print '%s,%d,%d' % (last, score[0], score[1])
score = [0,0]
last = key
score[allowed] += 1
print '%s,%d,%d' % (last, score[0], score[1])
벤치마킹
이 중 일부를 테스트하기 위해(다른 무엇보다도 내 호기심을 위해) 2400개의 개별 날짜를 포함하는 2,400,000개의 레코드 파일에 대해 약간의 벤치마킹을 수행하기로 결정했습니다.
다음 Python 스크립트를 사용하여 무작위로 허용/거부로 끝나는 큰 파일을 생성했습니다.
import itertools, datetime, random
CHOICES = ['Allow', 'Deny']
with open("file", "w") as f:
for _ in itertools.repeat(None, 2400):
epoch = random.randint(1, 1404226041)
d = datetime.datetime.fromtimestamp(epoch)
print d
dstr = d.strftime('%b %d %Y %X')
for _ in itertools.repeat(None, 1000):
f.write('%s %s\n' % (dstr, CHOICES[random.randint(0,1)]))
이는 Bash에 상응하는 것보다 약 1000배 빠르며(개정 로그 참조) 우리에게 다양한 로그 파일을 제공합니다. 정렬되지 않았기 때문에 대조 입력이 필요한 두 벤치마크(아래 3과 4)는 별도의 정렬 버전을 사용하고 있습니다( sort file > file-sorted
완료하는 데 0m4.253초 소요).
- 내 첫 번째: 0m1.544s
- 내 리팩토링
defaultdict
: 0m1.413s - Steeldriver's
awk
: 0m5.168s + 0m4.253s 정렬 - #3의 내 Python 재구현: 0m1.489s + 0m4.253s 정렬
나는 240만 개의 서로 다른 날짜로 생성을 반복했습니다(처음 두 개는 한계까지 밀어붙여야 합니다). 이 정렬에는 0m6.999s가 걸렸습니다. 또한 pypy
Python 버전에 대한 타이밍 도 추가했습니다 .
- 0m11.589초(0m7.080초
pypy
) - 0m11.307s (0m7.087s in
pypy
) - 0m8.652s + 0m6.999s
- 0m6.586s + 0m6.999s (0m1.566s in
pypy
)
분석 및 결과
- 작은 키 세트에서는 1과 2가 모두 가장 좋은 성능을 발휘합니다.
pypy
더 큰 키 세트에 도움이 됩니다. awk
4는 정규식을 사용하지 않기 때문에 3보다 빠릅니다.- 4는 가장 빠르고 가장 작은 공간을 차지하지만 데이터가 미리 정렬된 경우에만 해당됩니다.
- 데이터가 뒤죽박죽인 경우 2가 가장 빠릅니다.
- 외부 정렬은정말 느리다.
답변2
지금 하고 있는 일에 대해 충분히 자세하게 포스팅하지 않았기 때문에 더 효율적일지는 추측하기 어렵지만,데이터가 타임스탬프 순서로 정렬된 경우나는 다음과 같은 알고리즘을 제안하고 싶습니다.
- 타임스탬프가 변경될 때까지(또는 입력 끝에 도달할 때까지)
Allow
및 카운트를 누적합니다.Deny
- 결과를 인쇄하고 (입력이 끝나지 않은 경우) 카운트를 재설정합니다.
awk에서는 다음과 같이 할 수 있습니다.
awk '
FNR==1 {t = $1FS$2FS$3FS$4}
$1FS$2FS$3FS$4 == t {
a += $5=="Allow" ? 1 : 0
d += $5=="Deny" ? 1 : 0
}
$1FS$2FS$3FS$4 != t {
printf "%s,%d,%d\n",t,a,d
a = $5=="Allow" ? 1 : 0
d = $5=="Deny" ? 1 : 0
t = $1FS$2FS$3FS$4
}
END {printf "%s,%d,%d\n",t,a,d}
' input