Czego mogę użyć do konwersji danych wejściowych zamiast scanf?

125

Bardzo często widziałem ludzi zniechęcających innych do używania scanfi twierdzących, że są lepsze alternatywy. Jednak wszystko, co widzę, to albo „nie używaj scanf”, albo „oto ciąg formatu” i nigdy nie wspomniano o „lepszych alternatywach” .

Na przykład weźmy ten fragment kodu:

scanf("%c", &c);

Odczytuje to białe znaki, które pozostały w strumieniu wejściowym po ostatniej konwersji. Zwykle sugerowanym rozwiązaniem tego jest użycie:

scanf(" %c", &c);

lub nie używać scanf.

Ponieważ scanfjest źle, jakie są niektóre opcje ANSI C do konwersji formatów wejściowych, które scanfzwykle mogą obsługiwać (takie jak liczby całkowite, liczby zmiennoprzecinkowe i ciągi) bez użycia scanf?

SS Anne
źródło

Odpowiedzi:

87

Najczęstsze sposoby odczytu danych wejściowych to:

  • używanie fgetsze stałym rozmiarem, co jest zwykle sugerowane, oraz

  • za pomocą fgetc, co może być przydatne, jeśli czytasz tylko jeden char.

Aby przekonwertować dane wejściowe, możesz użyć różnych funkcji:

  • strtoll, aby przekonwertować ciąg na liczbę całkowitą

  • strtof/ d/ ld, aby przekonwertować ciąg na liczbę zmiennoprzecinkową

  • sscanf, co nie jest tak złe jak zwykłe używanie scanf, chociaż ma większość wad wymienionych poniżej

  • Nie ma dobrych sposobów na przeanalizowanie danych wejściowych oddzielonych separatorem w zwykłym ANSI C. Albo użyj strtok_rz POSIX lub strtok, co nie jest bezpieczne dla wątków. Można także toczyć swój własny wariant wątek bezpieczny użyciu strcspni strspn, co strtok_rnie pociąga za sobą żadnego szczególnego wsparcia systemu operacyjnego.

  • Może to być przesada, ale możesz używać leksyk i parserów ( flexi bisonto najczęstsze przykłady).

  • Bez konwersji, wystarczy użyć łańcucha


Ponieważ nie zastanawiałem się dokładnie, dlaczego scanf moje pytanie jest złe, opracuję:

  • Z specyfikatorów konwersji %[...]i %c, scanfnie jeść spacje. Najwyraźniej nie jest to powszechnie znane, czego dowodem jest wiele duplikatów tego pytania .

  • Istnieje pewne zamieszanie co do tego, kiedy używać &operatora jednoargumentowego w odniesieniu do scanfargumentów (w szczególności ciągów znaków).

  • Bardzo łatwo jest zignorować wartość zwracaną z scanf. Może to łatwo spowodować niezdefiniowane zachowanie odczytywania niezainicjowanej zmiennej.

  • Bardzo łatwo jest zapomnieć o przepełnieniu bufora scanf. scanf("%s", str)jest tak źle, jak, jeśli nie gorzej niż gets.

  • Nie można wykryć przepełnienia podczas konwersji liczb całkowitych za pomocą scanf. W rzeczywistości przepełnienie powoduje niezdefiniowane zachowanie tych funkcji.


SS Anne
źródło
56

Dlaczego jest scanf źle?

Głównym problemem jest to, że scanfnigdy nie miał on na celu radzenia sobie z wkładem użytkownika. Jest przeznaczony do stosowania z „idealnie” sformatowanymi danymi. Cytuję słowo „doskonale”, ponieważ nie jest to do końca prawda. Ale nie jest przeznaczony do analizowania danych, które są tak niewiarygodne jak dane wejściowe użytkownika. Z natury wkład użytkownika nie jest przewidywalny. Użytkownicy źle rozumieją instrukcje, literówki, przypadkowo naciśnij enter przed ich wykonaniem itp. Można rozsądnie zapytać, dlaczego funkcja, której nie należy używać do wprowadzania danych przez użytkownika stdin. Jeśli jesteś doświadczonym użytkownikiem * nix, wyjaśnienie nie będzie niespodzianką, ale może dezorientować użytkowników systemu Windows. W systemach * nix bardzo często buduje się programy działające za pomocą pipingu,stdoutstdindrugiego. W ten sposób możesz upewnić się, że dane wyjściowe i dane wejściowe są przewidywalne. W tych okolicznościach scanffaktycznie działa dobrze. Ale pracując z nieprzewidywalnymi danymi wejściowymi, ryzykujesz różnego rodzaju kłopoty.

Dlaczego więc nie ma łatwych w użyciu standardowych funkcji wprowadzania danych przez użytkownika? Można się tylko zgadywać, ale zakładam, że starzy hakerzy C po prostu uważali, że istniejące funkcje są wystarczająco dobre, nawet jeśli są bardzo niezręczne. Ponadto, patrząc na typowe aplikacje terminalowe, bardzo rzadko odczytują one dane wejściowe użytkownika stdin. Najczęściej przekazujesz wszystkie dane wejściowe użytkownika jako argumenty wiersza poleceń. Jasne, są wyjątki, ale w przypadku większości aplikacji wkład użytkownika jest bardzo drobny.

Więc co możesz zrobić?

Mój ulubiony jest fgetsw połączeniu z sscanf. Kiedyś napisałem odpowiedź na ten temat, ale ponownie opublikuję cały kod. Oto przykład z przyzwoitym (ale nie doskonałym) sprawdzaniem i analizowaniem błędów. Jest wystarczająco dobry do celów debugowania.

Uwaga

Nie lubię szczególnie prosić użytkownika o wprowadzenie dwóch różnych rzeczy w jednym wierszu. Robię to tylko wtedy, gdy należą do siebie w naturalny sposób. Jak na przykład, printf("Enter the price in the format <dollars>.<cent>: ")a następnie użyj sscanf(buffer "%d.%d", &dollar, &cent). Nigdy bym czegoś takiego nie zrobił printf("Enter height and base of the triangle: "). Głównym celem użycia fgetsponiżej jest hermetyzacja danych wejściowych, aby upewnić się, że jedno wejście nie wpływa na następne.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Jeśli wykonasz wiele z nich, mogę polecić utworzenie opakowania, które zawsze będzie opróżniać:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Takie postępowanie wyeliminuje powszechny problem, którym jest końcowy znak nowej linii, który może zepsuć się z danymi wejściowymi gniazda. Ale ma inny problem, a mianowicie, jeśli linia jest dłuższa niż bsize. Możesz to sprawdzić za pomocą if(buffer[strlen(buffer)-1] != '\n'). Jeśli chcesz usunąć nowy wiersz, możesz to zrobić za pomocą buffer[strcspn(buffer, "\n")] = 0.

Zasadniczo radziłbym nie oczekiwać, że użytkownik wprowadzi dane wejściowe w dziwnym formacie, który należy przeanalizować z różnymi zmiennymi. Jeśli chcesz przypisać zmienne heighti width, nie pytaj o oba jednocześnie. Pozwól użytkownikowi nacisnąć klawisz Enter między nimi. Podejście to jest bardzo naturalne w pewnym sensie. Nigdy nie otrzymasz danych wejściowych, stdindopóki nie naciśniesz Enter, więc dlaczego nie zawsze przeczytać całą linię? Oczywiście może to nadal prowadzić do problemów, jeśli linia jest dłuższa niż bufor. Czy pamiętam, że wspomniałem, że dane wejściowe użytkownika są nieporadne w C? :)

Aby uniknąć problemów z liniami dłuższymi niż bufor, możesz użyć funkcji, która automatycznie przydziela bufor o odpowiednim rozmiarze, możesz użyć getline(). Wadą jest to, że będziesz musiał freepóźniej uzyskać wynik.

Przyspieszenie gry

Jeśli poważnie myślisz o tworzeniu programów w C przy pomocy danych wejściowych, polecam zajrzeć do biblioteki takiej jak ncurses. Ponieważ wtedy prawdopodobnie chcesz również tworzyć aplikacje z pewną grafiką terminali. Niestety, jeśli to zrobisz, stracisz trochę przenośności, ale daje to znacznie lepszą kontrolę nad danymi wejściowymi użytkownika. Na przykład umożliwia natychmiastowe odczytanie naciśnięcia klawisza zamiast oczekiwania na naciśnięcie klawisza Enter.

klutt
źródło
Zauważ, że (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2nie wykrywa tak źle końcowego tekstu nieliczbowego.
chux - Przywróć Monikę
1
@chux Naprawiono% f% f. Co masz na myśli mówiąc pierwszy?
klutt
Wraz fgets()z "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {nie zgłasza nic złego wejścia mimo że ma „śmieci”.
chux - Przywróć Monikę
@chux Ah, teraz widzę. Cóż, to było zamierzone.
klutt
1
scanfjest przeznaczony do użycia z idealnie sformatowanymi danymi Ale nawet to nieprawda. Oprócz problemu z „śmieciami”, o którym wspomniał @chux, istnieje również fakt, że format podobny "%d %d %d"chętnie odczytuje dane wejściowe z jednego, dwóch lub trzech wierszy (lub nawet więcej, jeśli występują przeszkadzające puste wiersze), że nie ma sposób wymuszenia (powiedzmy) wejścia dwuwierszowego poprzez wykonanie czegoś podobnego "%d\n%d %d"itp. scanfmoże być odpowiedni dla sformatowanego wejścia strumieniowego , ale w ogóle nie jest dobry dla niczego opartego na linii.
Steve Summit
18

scanfjest niesamowity, gdy wiesz , że Twój wkład jest zawsze dobrze skonstruowany i dobrze wychowany. Inaczej...

IMO, oto największe problemy z scanf:

  • Ryzyko przepełnienia bufora - jeśli nie określisz szerokości pola dla specyfikatorów %si %[specyfikatorów konwersji, ryzykujesz przepełnieniem bufora (próba odczytania większej ilości danych wejściowych, niż rozmiar bufora ma pomieścić). Niestety, nie ma dobrego sposobu na określenie tego jako argumentu (tak jak w przypadku printf) - musisz albo zakodować go na stałe w ramach specyfikatora konwersji, albo wykonać kilka makr shenaniganów.

  • Akceptuje dane wejściowe, które powinny zostać odrzucone - jeśli czytasz dane wejściowe za pomocą %dspecyfikatora konwersji i wpisujesz coś w stylu 12w4, można oczekiwać, scanf że dane wejściowe zostaną odrzucone, ale tak nie jest - pomyślnie konwertuje i przypisuje 12, pozostawiając w4w strumieniu wejściowym zepsuć następny odczyt.

Czego więc powinieneś użyć?

Zwykle zalecam czytanie wszystkich interaktywnych danych wejściowych jako tekstu fgets- pozwala określić maksymalną liczbę znaków do odczytania na raz, dzięki czemu można łatwo zapobiec przepełnieniu bufora:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Jedną z dziwnych fgetsrzeczy jest to, że zapisze końcowy znak nowej linii w buforze, jeśli jest miejsce, dzięki czemu możesz łatwo sprawdzić, czy ktoś wpisał więcej danych wejściowych niż się spodziewałeś:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Sposób, w jaki sobie z tym poradzisz, zależy od ciebie - możesz odrzucić cały wkład z ręki i przykuć wszelkie pozostałe dane za pomocą getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

Lub możesz przetworzyć dane wejściowe, które masz do tej pory i przeczytać ponownie. To zależy od problemu, który próbujesz rozwiązać.

Aby tokenizować dane wejściowe (podzielić je na podstawie jednego lub więcej ograniczników), możesz użyć strtok, ale uważaj - strtokmodyfikuje dane wejściowe (zastępuje ograniczniki ciągiem znaków) i nie możesz zachować ich stanu (tzn. Możesz „ t częściowo tokenizuj jeden ciąg, a następnie zacznij tokenizować inny ciąg, a następnie wybierz miejsce, w którym przerwałeś oryginalny ciąg). Istnieje wariant, strtok_sktóry zachowuje stan tokenizera, ale AFAIK jego implementacja jest opcjonalna (musisz sprawdzić, czy __STDC_LIB_EXT1__jest zdefiniowany, aby zobaczyć, czy jest dostępny).

Po tokenizowaniu danych wejściowych, jeśli chcesz przekonwertować ciągi na liczby (tj. "1234"=> 1234), Masz opcje. strtoli strtodprzekonwertuje ciąg znaków liczb całkowitych i liczb rzeczywistych na odpowiadające im typy. Pozwalają również uchwycić 12w4problem, o którym wspomniałem powyżej - jednym z ich argumentów jest wskaźnik do pierwszego znaku nie przekonwertowanego w ciągu:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;
John Bode
źródło
Jeśli nie określisz szerokości pola ... - lub tłumienia konwersji (np. %*[%\n]Co jest przydatne w przypadku długich linii w dalszej części odpowiedzi).
Toby Speight
Jest sposób na uzyskanie specyfikacji szerokości pola w czasie wykonywania, ale to nie jest miłe. W końcu musisz zbudować ciąg formatu w swoim kodzie (być może używając snprintf()),.
Toby Speight
5
Popełniłeś tam najczęstszy błąd isspace()- akceptuje znaki niepodpisane reprezentowane jako int, więc musisz rzucić, unsigned charaby uniknąć UB na platformach, na których charjest podpisany.
Toby Speight
9

W tej odpowiedzi założę, że czytasz i interpretujesz linie tekstu . Być może monitujesz użytkownika, który coś pisze i naciska klawisz RETURN. A może czytasz wiersze tekstu strukturalnego z jakiegoś pliku danych.

Ponieważ czytasz wiersze tekstu, warto uporządkować kod wokół funkcji bibliotecznej, która odczytuje, no cóż, wiersz tekstu. Standardowa funkcja jest fgets(), chociaż istnieją inne (w tym getline). A następnie następnym krokiem jest jakoś zinterpretować ten wiersz tekstu.

Oto podstawowy przepis na dzwonienie w fgetscelu odczytania wiersza tekstu:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

To po prostu czyta jeden wiersz tekstu i drukuje go z powrotem. Jak napisano, ma kilka ograniczeń, do których dojdziemy za chwilę. Ma także bardzo dobrą funkcję: liczba 512, którą przekazaliśmy jako drugi argument, fgetsto rozmiar tablicy line, w fgetsktórej czytamy. Ten fakt - że możemy powiedzieć, fgetsile można odczytać - oznacza, że ​​możemy być pewni, że fgetsnie przepełni tablicy, wczytując w nią zbyt wiele.

Teraz wiemy, jak odczytać wiersz tekstu, ale co, jeśli naprawdę chcielibyśmy odczytać liczbę całkowitą, liczbę zmiennoprzecinkową, pojedynczy znak lub pojedyncze słowo? (To znaczy, co jeśli scanfwezwanie staramy się poprawić używał formacie specyfikator jak %d, %f, %c, lub %s?)

Łatwo jest zinterpretować wiersz tekstu - ciąg znaków - jak dowolną z tych rzeczy. Aby przekonwertować ciąg na liczbę całkowitą, najprostszym (choć niedoskonałym) sposobem jest wywołanie atoi(). Aby przekonwertować na liczbę zmiennoprzecinkową, istnieje atof(). (I są też lepsze sposoby, jak zobaczymy za chwilę.) Oto bardzo prosty przykład:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Jeśli chcesz, aby użytkownik wpisał pojedynczy znak (być może ylub njako odpowiedź tak / nie), możesz dosłownie złapać pierwszy znak linii, na przykład:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(To oczywiście ignoruje możliwość wpisania przez użytkownika odpowiedzi wieloznakowej; po cichu ignoruje wszelkie dodatkowe znaki, które zostały wpisane).

Wreszcie, jeśli chcesz, aby użytkownik wpisał ciąg znaków zdecydowanie nie zawierający białych znaków, jeśli chcesz traktować wiersz wejściowy

hello world!

ponieważ po łańcuchu "hello"następuje coś innego (co zrobiłby scanfformat %s), cóż, w takim przypadku trochę sfałszowałem, w końcu nie jest tak łatwo ponownie zinterpretować linię w ten sposób, więc odpowiedź na to część pytania będzie musiała chwilę poczekać.

Ale najpierw chcę wrócić do trzech rzeczy, które pominąłem.

(1) Dzwoniliśmy

fgets(line, 512, stdin);

czytać do tablicy line, a gdzie 512 jest rozmiarem tablicy, linewięc fgetswie, żeby jej nie przepełnić. Ale aby upewnić się, że 512 jest prawidłową liczbą (szczególnie, aby sprawdzić, czy ktoś nie poprawił programu, aby zmienić rozmiar), musisz przeczytać wszystko, gdziekolwiek linezadeklarowano. Jest to uciążliwe, więc istnieją dwa znacznie lepsze sposoby synchronizacji rozmiarów. Możesz (a) użyć preprocesora, aby utworzyć nazwę dla rozmiaru:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Lub (b) użyj sizeofoperatora C :

fgets(line, sizeof(line), stdin);

(2) Drugi problem polega na tym, że nie sprawdzaliśmy błędów. Podczas odczytywania danych wejściowych należy zawsze sprawdzać możliwość wystąpienia błędu. Jeśli z jakiegokolwiek powodu fgetsnie może odczytać wiersza tekstu, o który go prosiłeś, oznacza to, zwracając wskaźnik zerowy. Więc powinniśmy robić takie rzeczy

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Wreszcie istnieje problem polegający na tym, że aby odczytać wiersz tekstu, fgetsodczytuje znaki i wypełnia je do tablicy, dopóki nie znajdzie \nznaku kończącego linię, a także wypełnia \nznak w tablicy . Możesz to zobaczyć, jeśli nieznacznie zmodyfikujesz nasz wcześniejszy przykład:

printf("you typed: \"%s\"\n", line);

Jeśli uruchomię to i po wyświetleniu monitu napiszę „Steve”, zostanie wydrukowane

you typed: "Steve
"

To "w drugiej linii jest dlatego, że napis, który odczytał i wydrukował, był w rzeczywistości "Steve\n".

Czasami ta dodatkowa nowa linia nie ma znaczenia (na przykład, kiedy zadzwoniliśmy atoilub atof, ponieważ oba ignorują wszelkie dodatkowe dane nienumeryczne po liczbie), ale czasami ma to duże znaczenie. Tak często będziemy chcieli usunąć tę nową linię. Można to zrobić na kilka sposobów, które omówię za chwilę. (Wiem, że dużo to mówiłem. Ale wrócę do tych wszystkich rzeczy, obiecuję.)

W tym momencie możesz myśleć: „Myślałem, że powiedziałeś, że scanf to nie jest dobre, a ten inny sposób byłby o wiele lepszy. Ale fgetszaczyna wyglądać jak uciążliwość. Dzwonienie scanfbyło takie proste ! Czy mogę nadal tego używać? „

Jasne, możesz nadal używać scanf, jeśli chcesz. (I w przypadku naprawdę prostych rzeczy, pod pewnymi względami jest to prostsze.) Ale proszę, nie przychodź do mnie płakać, gdy zawodzi cię z powodu jednego z 17 dziwactw i słabości, lub przechodzi w nieskończoną pętlę z powodu wejścia nie spodziewałem się lub gdy nie możesz dowiedzieć się, jak go użyć, aby zrobić coś bardziej skomplikowanego. I spójrzmy na fgetsrzeczywiste niedogodności:

  1. Zawsze musisz określić rozmiar tablicy. Cóż, oczywiście, wcale nie jest to uciążliwe - jest to cecha, ponieważ przepełnienie bufora jest naprawdę złą rzeczą.

  2. Musisz sprawdzić wartość zwracaną. W rzeczywistości jest to pranie, ponieważ aby używać scanfpoprawnie, musisz również sprawdzić jego wartość zwrotną.

  3. Musisz rozebrać \nplecy. Przyznaję, że to prawdziwa uciążliwość. Chciałbym, żeby istniała standardowa funkcja, na którą mógłbym cię wskazać, która nie miała tak małego problemu. (Proszę, niech nikt nie porusza gets.) Ale w porównaniu do scanf's17 różnych niedogodności, wezmę tę jedną niedogodność fgetskażdego dnia.

Więc jak nie masz paska, który przełamane? Trzy drogi:

(a) Oczywisty sposób:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Tricky i kompaktowy sposób:

strtok(line, "\n");

Niestety ten nie zawsze działa.

(c) Kolejny zwarty i nieco niejasny sposób:

line[strcspn(line, "\n")] = '\0';

A teraz, gdy to już nie przeszkadza, możemy wrócić do innej rzeczy, którą pominąłem: niedoskonałości atoi()i atof(). Problem polega na tym, że nie dają one żadnej użytecznej wskazówki na sukces lub porażkę: po cichu ignorują końcowe dane nieliczbowe i po cichu zwracają 0, jeśli w ogóle nie ma danych numerycznych. Preferowane alternatywy - które mają również pewne inne zalety - to strtoli strtod. strtolpozwala również użyć bazy innej niż 10, co oznacza, że ​​możesz uzyskać efekt (między innymi) %olub za %xpomocąscanf. Ale pokazanie, jak prawidłowo korzystać z tych funkcji, jest historią samą w sobie i byłoby zbyt dużym rozproszeniem od tego, co już zamienia się w dość rozdrobnioną narrację, więc nie powiem już więcej o nich.

Reszta głównej narracji dotyczy danych wejściowych, które możesz próbować przetworzyć, które są bardziej skomplikowane niż tylko pojedyncza liczba lub znak. Co jeśli chcesz odczytać wiersz zawierający dwie cyfry lub wiele słów oddzielonych spacjami lub określoną interpunkcję ramkową? To właśnie tam rzeczy stają się interesujące i gdzie prawdopodobnie komplikują się, jeśli próbujesz robić rzeczy za pomocą scanf, i gdzie jest znacznie więcej opcji teraz, gdy czytasz jedną linię tekstu za pomocą fgets, chociaż pełna historia wszystkich tych opcji prawdopodobnie może wypełnić książkę, więc będziemy mogli jedynie zarysować powierzchnię tutaj.

  1. Moją ulubioną techniką jest podzielenie linii na „słowa” oddzielone spacjami, a następnie zrobienie czegoś z każdym „słowem”. Jedną z głównych standardowych funkcji służących do tego jest strtok(która ma również swoje problemy i która ocenia całą osobną dyskusję). Moje własne preferencje to dedykowana funkcja do konstruowania tablicy wskaźników dla każdego rozbitego „słowa”, funkcja opisana w tych notatkach kursowych . W każdym razie, gdy masz już „słowa”, możesz dalej przetwarzać każde, być może przy użyciu tych samych funkcji atoi/ atof/ strtol/ strtod , które już sprawdziliśmy.

  2. Paradoksalnie, mimo że spędziliśmy tutaj sporo czasu i wysiłku, zastanawiając się, jak się odejść scanf, innym dobrym sposobem radzenia sobie z wierszem tekstu, który właśnie czytaliśmy, fgetsjest przekazanie go sscanf. W ten sposób uzyskujesz większość zalet scanf, ale bez większości wad.

  3. Jeśli twoja składnia wejściowa jest szczególnie skomplikowana, może być właściwe użycie biblioteki „regexp” do jej przeanalizowania.

  4. Wreszcie możesz użyć dowolnych rozwiązań analizy ad hoc, które Ci odpowiadają. Możesz poruszać się po linii po znaku za pomocą char *wskaźnika sprawdzającego znaki, których oczekujesz. Możesz także wyszukiwać określone znaki za pomocą funkcji takich jak strchrlub strrchr, lub strspnlub strcspnlub strpbrk. Lub możesz parsować / konwertować i pomijać grupy znaków cyfrowych za pomocą funkcji strtollub strtod, które pomijaliśmy wcześniej.

Można oczywiście powiedzieć o wiele więcej, ale mam nadzieję, że wprowadzenie to sprawi, że zaczniesz.

Steve Summit
źródło
Czy istnieje dobry powód do pisania, sizeof (line)a nie po prostu sizeof line? Ten pierwszy sprawia, że ​​wygląda jak linenazwa typu!
Toby Speight
@TobySpeight Dobry powód? Nie, wątpię w to. Nawiasy są moim nawykiem, ponieważ nie mogę sobie przypomnieć, czy są to nazwy obiektów lub typów, ale wielu programistów je pomija. (Dla mnie jest to kwestia osobistych preferencji i stylu, a także dość niewielkiego).
Steve Summit
+1 za użycie sscanfjako silnika konwersji, ale zbieranie (i ewentualnie masowanie) danych wejściowych za pomocą innego narzędzia. Ale może warto wspomniećgetline w kontekście taht.
dmckee --- były moderator kotek
Kiedy mówisz o „fscanf prawdziwych niedogodnościach”, masz na myśli fgets? Irytujące # 3 naprawdę mnie denerwuje, szczególnie biorąc pod uwagę, że scanfzwraca niepotrzebny wskaźnik do bufora zamiast zwracać liczbę wprowadzanych znaków (co sprawiłoby, że usunięcie nowej linii byłoby znacznie czystsze).
supercat
1
Dziękujemy za wyjaśnienie twojego sizeofstylu. Dla mnie pamiętanie, kiedy jesteś w domu, jest łatwe: myślę, że jestem (type)jak obsada bez wartości (ponieważ interesuje nas tylko ten typ). Jeszcze jedno: mówisz, że strtok(line, "\n")to nie zawsze działa, ale nie jest oczywiste, kiedy nie. Zgaduję, że myślisz o przypadku, w którym linia była dłuższa niż bufor, więc nie mamy nowej linii i strtok()zwraca wartość null? Szkoda, fgets()że nie zwraca bardziej użytecznej wartości, więc możemy wiedzieć, czy nowa linia jest, czy nie.
Toby Speight
7

Czego mogę użyć do parsowania danych wejściowych zamiast scanf?

Zamiast scanf(some_format, ...)rozważyć fgets()zsscanf(buffer, some_format_and %n, ...)

Za pomocą tego " %n"kodu można po prostu wykryć, czy cały format został pomyślnie zeskanowany i czy na końcu nie było żadnych niepotrzebnych śmieci.

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }
chux - Przywróć Monikę
źródło
6

Określmy wymagania dotyczące analizowania jako:

  • prawidłowe dane wejściowe muszą zostać zaakceptowane (i przekonwertowane na inną formę)

  • nieprawidłowe dane wejściowe należy odrzucić

  • gdy jakiekolwiek dane wejściowe zostaną odrzucone, konieczne jest przekazanie użytkownikowi opisowego komunikatu wyjaśniającego (w jasnym języku „zrozumiałym dla zwykłych ludzi, którzy nie są programistami”), dlaczego został odrzucony (aby ludzie mogli dowiedzieć się, jak to naprawić problem)

Aby wszystko było bardzo proste, rozważmy przeanalizowanie pojedynczej prostej liczby całkowitej dziesiętnej (wpisanej przez użytkownika) i nic więcej. Możliwe przyczyny odrzucenia danych przez użytkownika to:

  • dane wejściowe zawierały niedopuszczalne znaki
  • dane wejściowe oznaczają liczbę niższą niż przyjęte minimum
  • dane wejściowe reprezentują liczbę wyższą niż zaakceptowane maksimum
  • dane wejściowe oznaczają liczbę, która ma niezerową część ułamkową

Zdefiniujmy również poprawnie „dane wejściowe zawierały niedopuszczalne znaki”; i powiedz, że:

  • wiodące białe znaki i końcowe białe znaki będą ignorowane (np. „
    5” będzie traktowane jak „5”)
  • zero lub jeden przecinek dziesiętny jest dozwolony (np. „1234.” i „1234.000” są traktowane tak samo jak „1234”)
  • musi być co najmniej jedna cyfra (np. „.” jest odrzucane)
  • dozwolony jest nie więcej niż jeden przecinek dziesiętny (np. „1.2.3” jest odrzucany)
  • przecinki, które nie występują między cyframi, zostaną odrzucone (np. „1234” zostanie odrzucony)
  • przecinki po przecinku zostaną odrzucone (np. „1234 000 000” zostanie odrzucone)
  • przecinki, które są po innym przecinku są odrzucane (np. „1, 234” jest odrzucany)
  • wszystkie pozostałe przecinki zostaną zignorowane (np. „1234” będzie traktowane jak „1234”)
  • znak minus, który nie jest pierwszym znakiem spacji, jest odrzucany
  • znak dodatni, który nie jest pierwszą spacją, jest odrzucany

Na podstawie tego możemy ustalić, że potrzebne są następujące komunikaty o błędach:

  • „Nieznany znak na początku wprowadzania”
  • „Nieznany znak na końcu wprowadzania”
  • „Nieznany znak w środku wprowadzania”
  • „Liczba jest za niska (minimum to ....)”
  • „Liczba jest za wysoka (maksymalna to ....)”
  • „Liczba nie jest liczbą całkowitą”
  • „Zbyt wiele miejsc po przecinku”
  • „Bez cyfr dziesiętnych”
  • „Zły przecinek na początku numeru”
  • „Zły przecinek na końcu numeru”
  • „Zły przecinek na środku liczby”
  • „Zły przecinek po przecinku”

Z tego punktu widać, że odpowiednia funkcja do konwersji łańcucha na liczbę całkowitą musiałaby rozróżniać bardzo różne rodzaje błędów; i że coś takiego jak „ scanf()” lub „ atoi()” lub „ strtoll()” jest całkowicie i całkowicie bezwartościowe, ponieważ nie dają one żadnego wskazania, co było nie tak z danymi wejściowymi (i używają całkowicie nieistotnej i niewłaściwej definicji tego, co jest / nie jest ”prawidłowe Wejście").

Zamiast tego zacznijmy pisać coś, co nie jest bezużyteczne:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Aby spełnić podane wymagania; convertStringToInteger()prawdopodobnie ta funkcja sama w sobie będzie zawierała kilkaset wierszy kodu.

Teraz było to po prostu „analizowanie pojedynczej prostej liczby całkowitej dziesiętnej”. Wyobraź sobie, że chcesz przeanalizować coś złożonego; jak lista struktur „imię i nazwisko, adres, numer telefonu, adres e-mail”; a może jak język programowania. W takich przypadkach może być konieczne napisanie tysięcy wierszy kodu, aby utworzyć analizę składniową, która nie jest kalekim żartem.

Innymi słowy...

Czego mogę użyć do parsowania danych wejściowych zamiast scanf?

Napisz (potencjalnie tysiące wierszy) kod, aby dopasować go do swoich wymagań.

Brendan
źródło
5

Oto przykład użycia flex do skanowania prostego wejścia, w tym przypadku pliku liczb zmiennoprzecinkowych ASCII, który może być w formacie US ( n,nnn.dd) lub European ( n.nnn,dd). Jest to po prostu skopiowane z dużo większego programu, więc mogą istnieć pewne nierozwiązane odwołania:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}
jamesqf
źródło
-5

Inne odpowiedzi podają właściwe szczegóły niskiego poziomu, więc ograniczę się do wyższego poziomu: Najpierw przeanalizuj, jak chcesz wyglądać każda linia wejściowa. Spróbuj opisać dane wejściowe formalną składnią - przy odrobinie szczęścia można je opisać za pomocą zwykłej gramatyki lub przynajmniej gramatyki bezkontekstowej . Jeśli wystarczająca jest zwykła gramatyka, możesz zakodować maszynę o skończonym staniektóry rozpoznaje i interpretuje każdy wiersz polecenia po jednym znaku na raz. Twój kod następnie odczyta wiersz (jak wyjaśniono w innych odpowiedziach), a następnie zeskanuje znaki w buforze przez maszynę stanu. W niektórych stanach zatrzymujesz i konwertujesz skanowany do tej pory podciąg na liczbę lub cokolwiek innego. Prawdopodobnie możesz „rzucić własnym”, jeśli jest to takie proste; jeśli uznasz, że potrzebujesz pełnej gramatyki bezkontekstowej, lepiej jest dowiedzieć się, jak korzystać z istniejących narzędzi do analizowania (re: lexi / yacclub ich wariantów).

PMar
źródło
Maszyna stanu skończonego może być przesada; możliwe są łatwiejsze sposoby wykrywania przepełnienia konwersji (takie jak sprawdzenie, czy errno == EOVERFLOWpo użyciu strtoll).
SS Anne
1
Po co kodować własną maszynę skończoną, skoro flex sprawia, że ​​pisanie jej jest banalnie proste?
jamesqf