Java: dzielenie ciągu znaków oddzielonych przecinkami, ale ignorowanie przecinków w cudzysłowach


Mam sznurek trochę podobny do tego:
foo,bar,c;qual="baz,blurb",d;junk="quux,syzygy"

Chcę to oddzielić przecinkami - ale muszę zignorować przecinki w cudzysłowie. W jaki sposób mogę to zrobić? Wygląda na to, że podejście regexp nie działa; Przypuszczam, że mogę ręcznie skanować i wejść w inny tryb, gdy widzę cytat, ale byłoby miło skorzystać z bibliotek, które już istnieją. (

edit
: Myślę, że chodziło mi o biblioteki, które są już częścią JDK lub częścią powszechnie używanych bibliotek, takich jak Apache Commons).
powyższa linia powinna zostać podzielona na:
foo
bar
c;qual="baz,blurb"
d;junk="quux,syzygy"


Uwaga:

NIE jest to plik CSV, jest to jedna linia zawarta w pliku o większej ogólnej strukturze
Zaproszony:
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Próbować:
public class Main { 
public static void main(String[] args) {
String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
String[] tokens = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
for(String t : tokens) {
System.out.println("> "+t);
}
}
}

Wyjście:
> foo
> bar
> c;qual="baz,blurb"
> d;junk="quux,syzygy"

Innymi słowy:

dziel na przecinek tylko wtedy, gdy przed przecinkiem znajduje się zero lub parzysta liczba cudzysłowów

.
Lub trochę bardziej przyjazne dla oka:
public class Main { 
public static void main(String[] args) {
String line = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\""; String otherThanQuote = " [^\"] ";
String quotedString = String.format(" \" %s* \" ", otherThanQuote);
String regex = String.format("(?x) "+// enable comments, ignore white spaces
", "+// match a comma
"(?= "+// start positive look ahead
" (?: "+// start non-capturing group 1
" %s* "+// match 'otherThanQuote' zero or more times
" %s "+// match 'quotedString'
" )* "+// end group 1 and repeat it zero or more times
" %s* "+// match 'otherThanQuote'
" $ "+// match the end of the string
") ",// stop positive look ahead
otherThanQuote, quotedString, otherThanQuote); String[] tokens = line.split(regex, -1);
for(String t : tokens) {
System.out.println("> "+t);
}
}
}

co daje to samo, co w pierwszym przykładzie.

EDIT
>
Jak wspomniał @MikeFHay w komentarzach:

Wolę używać

rozdzielacz guawy
http://docs.guava-libraries.go ... .html
ponieważ ma bardziej rozsądne wartości domyślne (zobacz dyskusję powyżej na temat pustych dopasowań obciętych przez
String # split ()
, więc zrobiłem to:
Splitter.on(Pattern.compile(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"))
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Chociaż generalnie lubię wyrażenia regularne, w przypadku tego rodzaju tokenizacji zależnej od stanu, uważam, że prosty parser (który w tym przypadku jest znacznie prostszy niż mogłoby się wydawać) jest prawdopodobnie czystszym rozwiązaniem, szczególnie w odniesieniu do łatwości konserwacji. Np .:
String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
List<String> result = new ArrayList<String>();
int start = 0;
boolean inQuotes = false;
for (int current = 0; current < input.length(); current++) {
if (input.charAt(current) == '\"') inQuotes = !inQuotes;// toggle state
boolean atLastChar = (current == input.length() - 1);
if(atLastChar) result.add(input.substring(start));
else if (input.charAt(current) == ',' && !inQuotes) {
result.add(input.substring(start, current));
start = current + 1;
}
}

Jeśli nie zależy Ci na zachowaniu przecinków w cudzysłowach, możesz uprościć to podejście (bez obsługi indeksu początkowego, bez specjalnych przypadków

ostatni znak
), zastępując cytowane przecinki czymś innym, a następnie oddzielając je przecinkami:
String input = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
StringBuilder builder = new StringBuilder(input);
boolean inQuotes = false;
for (int currentIndex = 0; currentIndex < builder.length(); currentIndex++) {
char currentChar = builder.charAt(currentIndex);
if (currentChar == '\"') inQuotes = !inQuotes;// toggle state
if (currentChar == ',' && inQuotes) {
builder.setCharAt(currentIndex, ';');// or '♡', and replace later
}
}
List<String> result = Arrays.asList(builder.toString().split(","));
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

http://sourceforge.net/projects/javacsv
http://sourceforge.net/projects/javacsv/
/
https://github.com/pupi1985/JavaCSV-Reloaded
https://github.com/pupi1985/JavaCSV-Reloaded
(rozwidlenie poprzedniej biblioteki, która pozwoli wygenerowanym wynikom mieć terminatory linii Windows
\ r \ n
, gdy nie jest uruchomiony system Windows)
http://opencsv.sourceforge.net
http://opencsv.sourceforge.net/
/
CSV API dla Java
https://coderoad.ru/101100/
Czy możesz polecić bibliotekę Java do czytania (i ewentualnie pisania) plików CSV?
https://coderoad.ru/200609/
Biblioteka Java lub aplikacja do konwersji pliku CSV do pliku XML?
https://coderoad.ru/123/
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Nie radziłbym wyrażenia regularnego odpowiedzi Barta, w tym konkretnym przypadku znajduję lepsze rozwiązanie parsujące (jak zasugerował Fabian). Wypróbowałem rozwiązanie regex i własną implementację parsowania i stwierdziłem, że:
  • Parsowanie jest znacznie szybsze niż parsowanie wyrażeń regularnych z linkami zwrotnymi - ~ 20x szybsze dla krótkich ciągów, ~ 40x szybsze dla długich.
  • Regex nie może znaleźć pustego ciągu po ostatnim przecinku. Chociaż nie było to pierwotne pytanie, takie było moje żądanie.

Moje rozwiązanie i test znajdują się poniżej.
String tested = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\",";
long start = System.nanoTime();
String[] tokens = tested.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
long timeWithSplitting = System.nanoTime() - start;start = System.nanoTime();
List<String> tokensList = new ArrayList<String>();
boolean inQuotes = false;
StringBuilder b = new StringBuilder();
for (char c : tested.toCharArray()) {
switch (c) {
case ',':
if (inQuotes) {
b.append(c);
} else {
tokensList.add(b.toString());
b = new StringBuilder();
}
break;
case '\"':
inQuotes = !inQuotes;
default:
b.append(c);
break;
}
}
tokensList.add(b.toString());
long timeWithParsing = System.nanoTime() - start;System.out.println(Arrays.toString(tokens));
System.out.println(tokensList.toString());
System.out.printf("Time with splitting:\td\n",timeWithSplitting);
System.out.printf("Time with parsing:\td\n",timeWithParsing);

Oczywiście możesz zmienić przełącznik na else-ifs w tym fragmencie, jeśli nie czujesz się komfortowo z jego brzydotą. Zauważ, że po przełączniku separatora nie ma przerwy. Technologia StringBuilder została wybrana zamiast StringBuffer zgodnie z projektem, aby zwiększyć prędkość tam, gdzie bezpieczeństwo wątków nie ma znaczenia.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Próbować

Szukaj
http://www.regular-expressions ... .html
wygląda jak
(?! \ "), (?! \")
. Musi pasować do
, które nie jest otoczone przez
"
.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Znajdujesz się w tym irytującym obszarze granicznym, w którym wyrażenia regularne prawie nie działają (jak zauważył Bart, unikanie cudzysłowów utrudniłoby życie), a mimo to pełnoprawny parser wydaje się przesadą.
Jeśli prawdopodobnie wkrótce będziesz potrzebować dużej złożoności, poszukałbym biblioteki parsera. Na przykład tutaj

to
http://www.javaworld.com/javaw ... .html
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Byłem niecierpliwy i postanowiłem nie czekać na odpowiedź ... w celach informacyjnych nie wydaje się to trudne do zrobienia czegoś takiego (co działa w mojej aplikacji, nie muszę się martwić o uruchamianie cytatów, ponieważ cytowane rzeczy są ograniczone do kilku ograniczonych form):
final static private Pattern splitSearchPattern = Pattern.compile("[\",]"); 
private List<String> splitByCommasNotInQuotes(String s) {
if (s == null)
return Collections.emptyList(); List<String> list = new ArrayList<String>();
Matcher m = splitSearchPattern.matcher(s);
int pos = 0;
boolean quoteMode = false;
while (m.find())
{
String sep = m.group();
if ("\"".equals(sep))
{
quoteMode = !quoteMode;
}
else if (!quoteMode && ",".equals(sep))
{
int toPos = m.start();
list.add(s.substring(pos, toPos));
pos = m.end();
}
}
if (pos < s.length())
list.add(s.substring(pos));
return list;
}

(Ćwiczenie dla czytelnika: rozszerz swoją pracę o cudzysłowy, szukając również odwrotnych ukośników.)
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Najprostszym podejściem nie jest dopasowywanie separatorów, tj. Przecinków, ze złożoną dodatkową logiką, aby dopasować to, co jest faktycznie zamierzone (dane, które mogą być cytowane jako ciągi znaków), tylko po to, aby wyeliminować fałszywe ograniczniki, ale raczej najpierw dopasuj wyprowadzone dane.
Wzorzec ma dwie możliwości: ciąg w cudzysłowie (
"[^"] * "
lub
". *? "
) lub wszystko aż do następnego przecinka (
[ ^,] +
). aby obsługiwać puste komórki, musimy pozwolić, aby element niecytowany był pusty i użyć następującego przecinka, jeśli jest obecny, a także użyć kotwicy
\\ G
:
Pattern p = Pattern.compile("\\G\"(.*?)\",?|([^,]*),?");

Szablon zawiera również dwie grupy przechwytywania służące do pobierania zawartości ciągu znaków w cudzysłowie lub zwykłej zawartości.
Następnie w Javie 9 możemy pobrać tablicę jako
String[] a = p.matcher(input).results()
.map(m -> m.group(m.start(1)<0? 2: 1))
.toArray(String[]::new);

podczas gdy starsze wersje Java potrzebują pętli, takich jak
for(Matcher m = p.matcher(input); m.find(); ) {
String token = m.group(m.start(1)<0? 2: 1);
System.out.println("found: "+token);
}

Dodanie elementów do
List
lub tablicy pozostaje jako podatek akcyzowy dla czytelnika.
W przypadku języka Java 8 możesz użyć implementacji
results ()

ta odpowiedź
https://stackoverflow.com/a/37482157/2711488
zrobić to podobnie do rozwiązania Java 9.
W przypadku mieszanej treści z wbudowanymi ciągami znaków, jak w pytaniu, możesz po prostu użyć
Pattern p = Pattern.compile("\\G((\"(.*?)\"|[^,])*),?");

Ale wtedy ciągi znaków są przechowywane w cudzysłowach.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Zamiast używać lookahead i innych szalonych wyrażeń regularnych, po prostu najpierw wyciągnij cudzysłowy. Oznacza to, że dla każdej grupy cudzysłowów zamień tę grupę na
__IDENTIFIER_1
lub inny wskaźnik i dopasuj to zgrupowanie do ciągu, mapy ciągów.
Po oddzieleniu przecinkiem zamień wszystkie dopasowane identyfikatory na oryginalne wartości ciągu.
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

a co z jednowierszowym użyciem String.split ()?

String s = "foo,bar,c;qual=\"baz,blurb\",d;junk=\"quux,syzygy\"";
String[] split = s.split( "(?<!\".{0,255}[^\"]),|,(?![^\"].*\")" );
Anonimowy użytkownik

Anonimowy użytkownik

Potwierdzenie od:

Zrobiłbym coś takiego:
boolean foundQuote = false;if(charAtIndex(currentStringIndex) == '"')
{
foundQuote = true;
}if(foundQuote == true)
{
//do nothing
}else {
string[] split = currentString.split(',');
}

Aby odpowiedzieć na pytania, Zaloguj się lub Zarejestruj się