![다른 CSV의 값을 기반으로 매우 큰 CSV를 필터링합니다.](https://linux55.com/image/198558/%EB%8B%A4%EB%A5%B8%20CSV%EC%9D%98%20%EA%B0%92%EC%9D%84%20%EA%B8%B0%EB%B0%98%EC%9C%BC%EB%A1%9C%20%EB%A7%A4%EC%9A%B0%20%ED%81%B0%20CSV%EB%A5%BC%20%ED%95%84%ED%84%B0%EB%A7%81%ED%95%A9%EB%8B%88%EB%8B%A4..png)
RAM에 맞지 않는 일부 CSV 파일을 작업 중입니다.
2개의 CSV 파일의 구조는 다음과 같습니다.
첫 번째.csv
ID | 이름 | 타임스탬프 |
---|---|---|
TV 시리즈 | 스테르 | 년-월-일 시:분:초 |
두 번째.csv
ID | 이름 | 날짜 |
---|---|---|
TV 시리즈 | 스테르 | 년 월 일 |
목표는 다음에서 first.csv
행을 선택하는 것입니다 second.csv
.
name
같다timestamp
범위는 [date
-1,date
+1]입니다.
이러한 모든 행을 반복한 후 출력을 단일 출력 파일로 결합할 수 있습니다.
답변1
쉘에서 이것이 어떻게 가능한지는 모르겠지만, 작성하기 어렵고 나중에 읽기도 어려울 것이라고 생각합니다.
기본 CSV 작업(열 선택/삭제, 행 필터링)에 대해 Go와 awk를 테스트했는데 Go가 더 빠릅니다(때로는 "훨씬 더 빠릅니다").
귀하의 게시물을 위해 저는 8,640,001 라인과 약 271MB의 테스트 파일을 만든 다음 2개의 예제 프로세서를 만들었습니다. 하나는 Python을 사용하고 다른 하나는 Go를 사용하여 읽기 및 쓰기 모드를 활용하므로 중간 저장소가 없습니다( 그리고 둘 다 대용량 파일의 효율성을 향상시킬 수 있는 버퍼링된 IO를 사용합니다.
- 파이썬 스크립트: 약 70초 동안 실행되며 약 6.5MB의 메모리를 사용합니다.
- 바이너리로 변환: 약 3.5초 정도 실행되며 약 10MB의 메모리를 사용합니다.
하지만 먼저, 그것이 일을 할 수 있는가?
기본 설정
저는 개발을 위해 다음 두 개의 작은 샘플을 만들었습니다.
첫 번째.csv
id,name,timestamp
1,foo,2000-01-01 00:00:00
2,foo,2000-01-02 00:00:00
3,foo,2000-01-03 00:00:00
4,foo,2000-01-04 00:00:00
5,foo,2000-01-05 00:00:00
6,bar,2000-02-01 00:00:00
7,bar,2000-02-02 00:00:00
8,bar,2000-02-03 00:00:00
9,bar,2000-02-04 00:00:00
두 번째.csv
id,name,date
10,foo,2000-01-03
11,bar,2000-02-02
"날짜-1"과 "날짜+1"이 무엇을 의미하는지 명확하지 않으므로 "하루 더하기 또는 빼기"를 원한다고 가정합니다.
이러한 파일에 대해 Go 또는 Python 코드를 실행하면 다음과 같은 결과가 나타납니다.
2,foo,2000-01-02 00:00:00
3,foo,2000-01-03 00:00:00
4,foo,2000-01-04 00:00:00
6,bar,2000-02-01 00:00:00
7,bar,2000-02-02 00:00:00
8,bar,2000-02-03 00:00:00
귀하의 요구 사항과 의견을 해석한 결과 다음과 같을 것으로 예상됩니다.
foo 2000-01-03
그리고bar 2000-02-02
테스트 파일
나는 100일 동안 1초 간격으로 foo에 대한 레코드만 생성하는 테스트 생성기를 만들었습니다.
import csv
from datetime import datetime, timedelta
dt_start = datetime(2000, 1, 1)
with open('test.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['id', 'name', 'timestamp'])
# 1 line per second for 100 days
for i in range(86400 * 100):
plus_secs = timedelta(seconds=i + 1)
writer.writerow([i + 1, 'foo', dt_start + plus_secs])
이게 뭐야?테스트.csv좋다:
% ll test.csv
-rw-r--r-- 1 alice bob 271M Nov 19 22:19 test.csv
% wc -l test.csv
8640001 test.csv
테스트 파일을 first.csv에 연결하면 ln -fs test.csv first.csv
다음 명령을 실행할 준비가 되었습니다.
파이썬
import csv
import sys
from datetime import datetime, timedelta
DATE_FMT = f'%Y-%m-%d'
DATETIME_FMT = f'%Y-%m-%d %H:%M:%S'
# Create lookup from second
# {name: [date-1day, date+1day]}
lookup = {}
with open('second.csv', newline='') as f:
reader = csv.reader(f)
header = next(reader)
nm_col = header.index('name')
dt_col = header.index('date')
for row in reader:
name = row[nm_col]
dt_str = row[dt_col]
dt = datetime.strptime(dt_str, DATE_FMT)
min_dt = dt - timedelta(days=1)
max_dt = dt + timedelta(days=1) # - timedelta(seconds=1)
lookup[name] = [min_dt, max_dt]
# Create on-demand writer, and iterate over first, writing when we need to
writer = csv.writer(sys.stdout)
with open('first.csv', newline='') as f:
reader = csv.reader(f)
header = next(reader)
nm_col = header.index('name')
dt_col = header.index('timestamp')
writer.writerow(header)
for row in reader:
name = row[nm_col]
if name not in lookup:
continue
dt_str = row[dt_col]
dt = datetime.strptime(dt_str, DATETIME_FMT)
min_dt = lookup[name][0]
max_dt = lookup[name][1]
if dt < min_dt or dt > max_dt:
continue
writer.writerow(row)
스크립트를 실행합니다.
% time python3 main.py > result.csv
python3 main.py > result.csv 69.93s user 0.40s system 98% cpu 1:11.07 total
% head -n5 result.csv
id,name,timestamp
86400,foo,2000-01-02 00:00:00
86401,foo,2000-01-02 00:00:01
86402,foo,2000-01-02 00:00:02
86403,foo,2000-01-02 00:00:03
% tail -n5 result.csv
259196,foo,2000-01-03 23:59:56
259197,foo,2000-01-03 23:59:57
259198,foo,2000-01-03 23:59:58
259199,foo,2000-01-03 23:59:59
259200,foo,2000-01-04 00:00:00 # is this right?
제 생각에는 이 말이 맞는 것 같습니다. 조회 날짜를 중심으로 48시간 범위 내의 콘텐츠만 로그합니다. 내가 찾은 마지막 항목이 무엇인지 잘 모르겠습니다. 네 번째 순간부터였습니다. 그게 주석 처리되었습니다 - timedelta(seconds=1)
.
가다
package main
import (
"encoding/csv"
"io"
"os"
"time"
)
type LookupEntry struct {
oneDayBefore time.Time
oneDayAfter time.Time
}
const DATE_FMT = "2006-01-02"
const DATETIME_FMT = "2006-01-02 15:04:05"
var lookup = make(map[string]LookupEntry)
func main() {
makeLookupTable()
findMatchingEntries()
}
func makeLookupTable() {
f, _ := os.Open("second.csv")
defer f.Close()
r := csv.NewReader(f)
r.Read() // Discard header
for {
record, err := r.Read()
if err == io.EOF {
break
}
dt, _ := time.Parse(DATE_FMT, record[2])
oneDayBefore := dt.AddDate(0, 0, -1)
oneDayAfter := dt.AddDate(0, 0, 1) // .Add(-time.Millisecond * 1000)
lookup[record[1]] = LookupEntry{oneDayBefore, oneDayAfter}
}
}
func findMatchingEntries() {
f1, _ := os.Open("first.csv")
defer f1.Close()
w := csv.NewWriter(os.Stdout)
r := csv.NewReader(f1)
header, _ := r.Read()
w.Write(header)
for {
record, err := r.Read()
if err == io.EOF {
break
}
lookupEntry, ok := lookup[record[1]]
if !ok {
continue
}
dt, _ := time.Parse(DATETIME_FMT, record[2])
if dt.Before(lookupEntry.oneDayBefore) || dt.After(lookupEntry.oneDayAfter) {
continue
}
w.Write(record)
}
w.Flush()
}
테스트를 빌드하고 실행합니다.
% go build main.go
% time ./main > result.csv
./main > result.csv 3.53s user 0.14s system 104% cpu 3.504 total
% head -n5 result.csv
86400,foo,2000-01-02 00:00:00
86401,foo,2000-01-02 00:00:01
86402,foo,2000-01-02 00:00:02
86403,foo,2000-01-02 00:00:03
86404,foo,2000-01-02 00:00:04
% tail -n5 result.csv
259196,foo,2000-01-03 23:59:56
259197,foo,2000-01-03 23:59:57
259198,foo,2000-01-03 23:59:58
259199,foo,2000-01-03 23:59:59
259200,foo,2000-01-04 00:00:00