Usuń wszystkie wiersze z pliku A, które zawierają ciągi z pliku B

15

Mam plik CSV users.csvz listą nazw użytkowników, identyfikatorów użytkowników i innych danych:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"Paul McCartny", 30923833, "left", "black"
"Ringo Starr", 77392318, "right", "blue"
"George Harrison", 72349482, "left", "green"

W innym pliku toremove.txtmam listę identyfikatorów użytkowników:

30923833
77392318

Czy istnieje sprytny i skuteczny sposób na usunięcie wszystkich wierszy z users.csvpliku zawierającego identyfikatory toremove.txt? Napisałem prostą aplikację w języku Python, aby przeanalizować dwa pliki i zapisać w nowym pliku tylko te wiersze, których nie ma toremove.txt, ale jest to wyjątkowo powolne. Być może trochę sedlub awkmagia może tu pomóc?

Jest to pożądany wynik, biorąc pod uwagę powyższe przykłady:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
dotancohen
źródło
Może powinieneś udostępnić swój skrypt Pythona. Podejrzewam, że jest tam coś nie tak, jak bycie O (N²). Chociaż jeśli przechowujesz i usuwasz miliony zapisów, magia nie pomoże zbyt wiele.
Ángel
Skrypt to tak naprawdę O (n <sup> 2 </sup>): n dla linii users.csvpliku i n dla linii toremove.txt. Nie jestem pewien, jak to zrobić z mniejszą złożonością. Istotą jest to: for u in users: if not any(toremove in u): outputfile.write(u). Mogę wysłać to do recenzji kodu.
dotancohen
1
Przeczytałbym toremove.txt, zapisując wpisy jako klucze . Iterate users.csv, drukowanie tych, których identyfikatora nie ma w dykcie. Otrzymujesz przetwarzanie O (n) zarówno dla, jak toremove.txti users.csvO (n) użycie pamięci dla toremove.txt(co jest prawdopodobnie stosunkowo niewielkie)
Ángel
@ Ángel: Tak, dokładnie tak działa skrypt!
dotancohen
1
Sprawdzanie, czy klucz istnieje w słowniku, jest równoznaczne z sprawdzaniem tablicy skrótów, czyli O (1). Z drugiej strony, jeśli trzeba iterować elementy do usunięcia, to O (m)
Ángel

Odpowiedzi:

15

Za pomocą grepmożesz:

$ grep -vwF -f toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"

Z awk:

$ awk -F'[ ,]' 'FNR==NR{a[$1];next} !($4 in a)' toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
Cuonglm
źródło
@terdon: Dang! Chciałem to powiedzieć. Zauważ jednak, że odpowiedź Gnouca (prawdopodobnie) robi to, o co pyta pytanie , ale może nie być tym, czego chce użytkownik.
Scott
awkRozwiązanie jest bardzo wrażliwa na plikach sformatowanych jest dokładnie jak pokazane w pytaniu. Najbardziej rażąco, jeśli nazwa jest tylko jednym słowem / tokenem (tzn. Nie zawiera spacji; np. "Bono") Lub zawiera więcej niż dwa tokeny (tj. Zawiera więcej niż jedną spację; np. "Sir Paul McCartney"), Przejdzie nawet, jeśli dopasowania identyfikatora użytkownika. Mniej oczywiste, to samo dzieje się, jeśli między pierwszym przecinkiem a identyfikatorem użytkownika nie ma spacji lub jeśli jest więcej niż jedna spacja (np "John Lennon", 90123412, ….).
Scott
@Scott: Tak, to jest powód, dla którego postawiłem awkza sobą rozwiązaniegrep
cuonglm,
4

Oto awkodpowiedź Gnouca , zmodyfikowana tak, aby była ślepa na kosmos:

awk -F, 'FNR==NR{a[$1];next} !(gensub("^ *","",1,$2) in a)' toremove.txt users.csv

Ponieważ używa ograniczników tylko przecinków (a nie spacji), $1jest "John Lennon", $2jest  90123412(ze spacją wiodącą) itp. Dlatego używamy gensubdo usunięcia dowolnej liczby wiodących spacji $2 przed sprawdzeniem, czy (identyfikator użytkownika) był w toremove.txtpliku.

Scott
źródło
Możesz być w stanie zrobić tutaj inne sprytne rzeczy (po prostu głośno myśleć), na przykład analizując „dokładny kawałek” łańcucha, który nie powinien się zgadzać, i porównując to z tablicą asocjacyjną, czy co.
rogerdpack
Wierzę, że to właśnie robię. Co miałeś na myśli?
Scott
Tak, jesteś. Miałem na myśli tylko, jeśli trzeba było zrobić coś bardziej Funky jak usunięcie pierwszej połowy linii lub coś podobnego (downcasing itp stackoverflow.com/a/4784647/32453 ) właśnie specjalizuje parsowania
rogerdpack
0

OK w ruby ​​sposób: jeśli masz listę ciągów w pliku i chcesz usunąć wszystkie wiersze z innego pliku, które zawierają nawet dowolny ciąg w pierwszym pliku (w tym przypadku usuwając „plik2” z „pliku1”) plik ruby :

b=File.read("file2").split # subtract this one out
remove_regex = Regexp.new(b.join('|'))
File.open("file1", "r").each_line do |line|
  if line !~ remove_regex
    puts line
  end
end

niestety przy dużym pliku „do usunięcia” wydaje się to obniżać złożoność do O (N ^ 2) (moje założenie jest takie, że regexp ma dużo pracy), ale nadal może być przydatne dla kogoś tam (jeśli ty chcesz więcej niż usuwanie pełnych linii). W niektórych przypadkach może być szybszy.

Inną opcją, jeśli dążysz do szybkości, jest użycie tego samego mechanizmu sprawdzania skrótu, ale ostrożne „przeanalizowanie” wiersza w celu dopasowania pasujących ciągów znaków, a następnie porównanie ich ze skrótem.

W rubinie może wyglądać tak:

b=File.read("file2").split # subtract this one out
hash={}
for line in b
  hash[line] = 1
end

ARGF.each_line do |line|
  ok = true
  for number in line.scan(/\d{9}/)
    if hash.key? number
      ok=false
    end
  end
  if (ok)
    puts line
  end
end

Zobacz także odpowiedź Scotta, jest podobna do proponowanych tu odpowiedzi na awk i unika złożoności O (N ^ 2) (uff).

rogerdpack
źródło