Apa yang dapat saya gunakan untuk konversi input daripada scanf?

125

Saya sudah sangat sering melihat orang-orang yang mencegah orang lain untuk menggunakan scanfdan mengatakan bahwa ada alternatif yang lebih baik. Namun, yang akhirnya saya lihat adalah "jangan gunakan scanf" atau "ini format string yang benar" , dan tidak pernah ada contoh "alternatif yang lebih baik" yang disebutkan.

Sebagai contoh, mari kita ambil potongan kode ini:

scanf("%c", &c);

Ini membaca spasi putih yang tersisa di aliran input setelah konversi terakhir. Solusi yang disarankan untuk ini adalah menggunakan:

scanf(" %c", &c);

atau tidak digunakan scanf.

Karena scanfburuk, apa sajakah opsi ANSI C untuk mengonversi format input yang scanfbiasanya dapat menangani (seperti bilangan bulat, angka floating-point, dan string) tanpa menggunakan scanf?

SS Anne
sumber

Jawaban:

87

Cara membaca input yang paling umum adalah:

  • menggunakan fgetsdengan ukuran tetap, yang biasanya disarankan, dan

  • menggunakan fgetc, yang mungkin berguna jika Anda hanya membaca satu char.

Untuk mengonversi input, ada berbagai fungsi yang dapat Anda gunakan:

  • strtoll, untuk mengkonversi string menjadi integer

  • strtof/d / ld, untuk mengubah string menjadi angka floating-point

  • sscanf, yang tidak sama seburuk hanya menggunakan scanf, meskipun memiliki sebagian besar downfalls yang disebutkan di bawah

  • Tidak ada cara yang baik untuk mengurai input yang dipisahkan oleh pembatas di ANSI C. Baik digunakan strtok_rdari POSIX atau strtok, yang tidak aman untuk thread. Anda juga bisa menggulung varian aman-thread menggunakan strcspndanstrspn , karena strtok_rtidak melibatkan dukungan OS khusus.

  • Mungkin berlebihan, tetapi Anda dapat menggunakan lexers dan parser (flex dan bisonmenjadi contoh paling umum).

  • Tanpa konversi, cukup gunakan string


Karena saya tidak masuk ke mengapa persis scanfburuk dalam pertanyaan saya, saya akan menguraikan:

  • Dengan penentu konversi %[...]dan %c, scanftidak memakan ruang kosong. Ini tampaknya tidak diketahui secara luas, sebagaimana dibuktikan oleh banyak duplikat dari pertanyaan ini .

  • Ada beberapa kebingungan tentang kapan harus menggunakan &operator unary ketika merujuk pada scanfargumen (khususnya dengan string).

  • Sangat mudah untuk mengabaikan nilai pengembalian dari scanf. Ini dapat dengan mudah menyebabkan perilaku tidak terdefinisi dari membaca variabel yang tidak diinisialisasi.

  • Ini sangat mudah untuk melupakan untuk mencegah buffer overflow di scanf. scanf("%s", str)sama buruknya, jika tidak lebih buruk dari gets,.

  • Anda tidak dapat mendeteksi overflow saat mengonversi bilangan bulat dengan scanf. Bahkan, overflow menyebabkan perilaku yang tidak terdefinisi dalam fungsi-fungsi ini.


SS Anne
sumber
56

Kenapa itu scanfburuk?

Masalah utama adalah yang scanftidak pernah dimaksudkan untuk berurusan dengan input pengguna. Ini dimaksudkan untuk digunakan dengan data yang diformat "dengan sempurna". Saya mengutip kata "sempurna" karena itu tidak sepenuhnya benar. Tapi itu tidak dirancang untuk mem-parsing data yang tidak dapat diandalkan seperti input pengguna. Secara alami, input pengguna tidak dapat diprediksi. Pengguna salah mengerti instruksi, membuat kesalahan ketik, secara tidak sengaja tekan enter sebelum dilakukan dll. Orang mungkin bertanya mengapa fungsi yang seharusnya tidak digunakan untuk input pengguna dibaca dari stdin. Jika Anda adalah pengguna * nix yang berpengalaman, penjelasannya tidak akan mengejutkan tetapi mungkin membingungkan pengguna Windows. Dalam sistem * nix, sangat umum untuk membangun program yang bekerja melalui perpipaan,stdoutstdindari yang kedua. Dengan cara ini, Anda dapat memastikan bahwa output dan input dapat diprediksi. Selama keadaan ini, scanfsebenarnya berfungsi dengan baik. Tetapi ketika bekerja dengan input yang tidak dapat diprediksi, Anda berisiko segala macam masalah.

Jadi mengapa tidak ada fungsi standar yang mudah digunakan untuk input pengguna? Orang hanya bisa menebak di sini, tapi saya berasumsi bahwa peretas tua hardcore C hanya berpikir bahwa fungsi yang ada cukup baik, meskipun mereka sangat kikuk. Juga, ketika Anda melihat aplikasi terminal tipikal mereka sangat jarang membaca input pengguna stdin. Paling sering Anda melewati semua input pengguna sebagai argumen baris perintah. Tentu, ada pengecualian, tetapi untuk sebagian besar aplikasi, input pengguna adalah hal yang sangat kecil.

Jadi apa yang bisa kamu lakukan?

Favorit saya adalah fgetskombinasi dengan sscanf. Saya pernah menulis jawaban tentang itu, tetapi saya akan memposting ulang kode lengkap. Berikut adalah contoh dengan pengecekan dan parsing kesalahan yang layak (tapi tidak sempurna). Cukup bagus untuk keperluan debugging.

Catatan

Saya tidak terlalu suka meminta pengguna untuk memasukkan dua hal yang berbeda pada satu baris. Saya hanya melakukan itu ketika mereka saling memiliki secara alami. Suka misalnya printf("Enter the price in the format <dollars>.<cent>: ")dan kemudian gunakan sscanf(buffer "%d.%d", &dollar, &cent). Saya tidak akan pernah melakukan hal seperti itu printf("Enter height and base of the triangle: "). Poin utama menggunakan di fgetsbawah ini adalah untuk merangkum input untuk memastikan bahwa satu input tidak mempengaruhi yang berikutnya.

#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);

Jika Anda melakukan banyak ini, saya bisa merekomendasikan membuat pembungkus yang selalu memerah:

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;
}```

Melakukan hal seperti ini akan menghilangkan masalah umum, yaitu trailing newline yang dapat mengacaukan input sarang. Tetapi memiliki masalah lain, yaitu jika garis lebih panjang dari bsize. Anda dapat memeriksanya dengan if(buffer[strlen(buffer)-1] != '\n'). Jika Anda ingin menghapus baris baru, Anda dapat melakukannya dengan buffer[strcspn(buffer, "\n")] = 0.

Secara umum, saya akan menyarankan untuk tidak mengharapkan pengguna untuk memasukkan input dalam beberapa format aneh yang harus Anda parsing ke variabel yang berbeda. Jika Anda ingin menetapkan variabel heightdan width, jangan meminta keduanya sekaligus. Izinkan pengguna menekan enter di antara mereka. Juga, pendekatan ini sangat alami di satu sisi. Anda tidak akan pernah mendapatkan input dari stdinsampai Anda menekan enter, jadi mengapa tidak selalu membaca seluruh baris? Tentu saja hal ini masih dapat menimbulkan masalah jika saluran lebih panjang dari buffer. Apakah saya ingat menyebutkan bahwa input pengguna kikuk di C? :)

Untuk menghindari masalah dengan garis yang lebih panjang dari buffer, Anda dapat menggunakan fungsi yang secara otomatis mengalokasikan buffer dengan ukuran yang sesuai, Anda bisa menggunakannya getline(). Kekurangannya adalah bahwa Anda perlu freehasil setelah itu.

Meningkatkan permainan

Jika Anda serius membuat program dalam C dengan input pengguna, saya akan merekomendasikan melihat-lihat di perpustakaan seperti ncurses. Karena dengan begitu Anda kemungkinan juga ingin membuat aplikasi dengan beberapa grafik terminal. Sayangnya, Anda akan kehilangan portabilitas jika melakukannya, tetapi ini memberi Anda kontrol input pengguna yang jauh lebih baik. Misalnya, ini memberi Anda kemampuan untuk membaca penekanan tombol secara instan alih-alih menunggu pengguna menekan enter.

klutt
sumber
Catatan yang (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2tidak mendeteksi sebagai buruk teks trailing non-numerik.
chux
1
@chux Memperbaiki% f% f. Apa maksudmu dengan yang pertama?
klutt
Dengan fgets()dari "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {tidak melaporkan sesuatu yang salah dengan masukan meskipun itu memiliki "sampah".
chux - Reinstate Monica
@ chux, Ah, sekarang saya mengerti. Yah itu disengaja.
klutt
1
scanfdimaksudkan untuk digunakan dengan data yang diformat sempurna. Tetapi itu pun tidak benar. Selain masalah dengan "sampah" seperti yang disebutkan oleh @ chux, ada juga fakta bahwa format seperti "%d %d %d"senang membaca input dari satu, dua, atau tiga baris (atau bahkan lebih, jika ada campur tangan baris kosong), bahwa tidak ada cara untuk memaksa (mengatakan) input dua baris dengan melakukan sesuatu seperti "%d\n%d %d", dll. scanfmungkin sesuai untuk input stream yang diformat , tetapi sama sekali tidak baik untuk apa pun berbasis garis.
Steve Summit
18

scanfluar biasa ketika Anda tahu input Anda selalu terstruktur dan berperilaku baik. Jika tidak...

IMO, berikut adalah masalah terbesar dengan scanf:

  • Risiko buffer overflow - jika Anda tidak menentukan lebar bidang untuk %sdan %[penentu konversi, Anda berisiko buffer overflow (mencoba membaca lebih banyak input daripada ukuran buffer yang ditahan). Sayangnya, tidak ada cara yang baik untuk menentukannya sebagai argumen (seperti halnya dengan printf) - Anda harus meng-hardcode-nya sebagai bagian dari specifier konversi atau melakukan beberapa gangguan makro.

  • Menerima input yang harus ditolak - Jika Anda membaca input dengan %dspecifier konversi dan Anda mengetik sesuatu seperti 12w4, Anda akan mengharapkan scanf untuk menolak input itu, tetapi tidak - itu berhasil mengubah dan menetapkan 12, meninggalkan w4aliran input untuk mengacaukan pembacaan selanjutnya.

Jadi, apa yang sebaiknya Anda gunakan?

Saya biasanya merekomendasikan membaca semua input interaktif sebagai teks fgets- ini memungkinkan Anda menentukan jumlah karakter maksimum untuk dibaca sekaligus, sehingga Anda dapat dengan mudah mencegah buffer overflow:

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

Satu kekhasan dari itu fgetsadalah bahwa ia akan menyimpan baris tambahan di buffer jika ada ruang, sehingga Anda dapat melakukan pemeriksaan mudah untuk melihat apakah seseorang mengetik lebih banyak input daripada yang Anda harapkan:

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

Bagaimana Anda menangani hal itu terserah Anda - Anda bisa menolak seluruh input dari tangan, dan menyeruput setiap input yang tersisa dengan getchar:

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

Atau Anda dapat memproses input yang Anda dapatkan sejauh ini dan membaca lagi. Itu tergantung pada masalah yang Anda coba selesaikan.

Untuk tokenize input (membaginya berdasarkan pada satu atau lebih pembatas), Anda dapat menggunakan strtok, tetapi berhati-hatilah - strtokmemodifikasi inputnya (itu menimpa pembatas dengan string terminator), dan Anda tidak dapat mempertahankan statusnya (yaitu, Anda dapat ' t tokenize sebagian string, kemudian mulai tokenize yang lain, lalu ambil di mana Anda tinggalkan di string asli). Ada varian,, strtok_syang mempertahankan status tokenizer, tetapi AFAIK implementasinya adalah opsional (Anda harus memeriksa yang __STDC_LIB_EXT1__ditentukan untuk melihat apakah itu tersedia).

Setelah Anda tokenized input Anda, jika Anda perlu mengkonversi string ke angka (yaitu, "1234"=> 1234), Anda memiliki opsi. strtoldan strtodakan mengonversi representasi string dari bilangan bulat dan bilangan real ke jenisnya masing-masing. Mereka juga memungkinkan Anda menangkap 12w4masalah yang saya sebutkan di atas - salah satu argumen mereka adalah penunjuk ke karakter pertama yang tidak dikonversi dalam string:

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
sumber
Jika Anda tidak menentukan lebar bidang ... - atau penindasan konversi (mis. %*[%\n], Yang berguna untuk berurusan dengan garis yang terlalu panjang nanti dalam jawabannya).
Toby Speight
Ada cara untuk mendapatkan spesifikasi run-time lebar bidang, tapi itu tidak bagus. Anda akhirnya harus membangun string format dalam kode Anda (mungkin menggunakan snprintf()),.
Toby Speight
5
Anda telah membuat kesalahan yang paling umum dengan isspace()hal itu - ia menerima karakter yang tidak ditandai yang diwakili int, jadi Anda perlu melakukan casting unsigned charuntuk menghindari UB pada platform tempat charditandatangani.
Toby Speight
9

Dalam jawaban ini saya akan berasumsi bahwa Anda membaca dan menafsirkan baris teks . Mungkin Anda mendorong pengguna, yang sedang mengetik sesuatu dan menekan RETURN. Atau mungkin Anda sedang membaca baris teks terstruktur dari beberapa file data.

Karena Anda membaca baris teks, masuk akal untuk mengatur kode Anda di sekitar fungsi perpustakaan yang membaca, yah, baris teks. Fungsi Standar adalah fgets(), meskipun ada yang lain (termasuk getline). Dan kemudian langkah selanjutnya adalah menafsirkan baris teks itu entah bagaimana.

Inilah resep dasar untuk menelepon fgetsuntuk membaca satu baris teks:

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

Ini cukup dibaca dalam satu baris teks dan mencetaknya kembali. Seperti yang tertulis itu memiliki beberapa keterbatasan, yang akan kita bahas sebentar lagi. Ini juga memiliki fitur yang sangat hebat: angka 512 yang kami berikan sebagai argumen kedua fgetsadalah ukuran array yang linekami minta fgetsuntuk dibaca. Fakta ini - yang bisa kita katakan fgetsseberapa banyak itu diperbolehkan untuk dibaca - berarti kita dapat yakin bahwa fgetstidak akan meluap array dengan membaca terlalu banyak ke dalamnya

Jadi sekarang kita tahu cara membaca satu baris teks, tetapi bagaimana jika kita benar-benar ingin membaca integer, atau angka floating-point, atau satu karakter, atau satu kata? (Artinya, bagaimana jika scanfpanggilan kita mencoba untuk memperbaiki telah menggunakan format specifier seperti %d, %f, %c, atau %s?)

Sangat mudah untuk menginterpretasikan ulang baris teks - string - sebagai salah satu dari hal-hal ini. Untuk mengonversi string menjadi integer, cara paling sederhana (meskipun tidak sempurna) untuk melakukannya adalah dengan menelepon atoi(). Untuk mengonversi ke angka floating-point, ada atof(). (Dan ada juga cara yang lebih baik, seperti yang akan kita lihat sebentar lagi.) Berikut ini contoh yang sangat sederhana:

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);

Jika Anda ingin pengguna mengetik satu karakter (mungkin yatau nsebagai jawaban ya / tidak), Anda dapat langsung mengambil karakter pertama dari baris tersebut, seperti ini:

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

(Ini tentu saja mengabaikan kemungkinan bahwa pengguna mengetik respons multi-karakter; secara diam-diam mengabaikan setiap karakter tambahan yang diketik.)

Akhirnya, jika Anda ingin pengguna mengetikkan string jelas tidak mengandung spasi, jika Anda ingin memperlakukan jalur input

hello world!

sebagai string "hello"diikuti oleh sesuatu yang lain (yang adalah scanfformat apa yang %sakan dilakukan), yah, dalam hal ini, saya berselingkuh sedikit, itu tidak begitu mudah untuk menafsirkan ulang garis dengan cara itu, setelah semua, jadi jawaban untuk itu bagian dari pertanyaan harus menunggu sebentar.

Tetapi pertama-tama saya ingin kembali ke tiga hal yang saya lewati.

(1) Kami sudah menelepon

fgets(line, 512, stdin);

untuk membaca ke dalam array line, dan di mana 512 adalah ukuran array linejadi fgetstahu untuk tidak meluapnya. Tetapi untuk memastikan bahwa 512 adalah angka yang tepat (terutama, untuk memeriksa apakah mungkin seseorang mengubah program untuk mengubah ukuran), Anda harus membaca kembali ke mana linepun dinyatakan. Itu merepotkan, jadi ada dua cara yang jauh lebih baik untuk menjaga ukuran tetap sinkron. Anda bisa, (a) menggunakan preprocessor untuk membuat nama untuk ukuran:

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

Atau, (b) gunakan sizeofoperator C :

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

(2) Masalah kedua adalah bahwa kami belum memeriksa kesalahan. Saat Anda membaca input, Anda harus selalu memeriksa kemungkinan kesalahan. Jika karena alasan apa pun fgetstidak dapat membaca baris teks yang Anda minta, itu menunjukkan ini dengan mengembalikan pointer nol. Jadi kita seharusnya melakukan hal-hal seperti

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

Akhirnya, ada masalah bahwa untuk membaca satu baris teks, fgetsmembaca karakter dan mengisinya ke dalam array Anda sampai menemukan \nkarakter yang mengakhiri baris, dan itu mengisi \nkarakter ke dalam array Anda juga . Anda dapat melihat ini jika Anda sedikit memodifikasi contoh kami sebelumnya:

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

Jika saya menjalankan ini dan ketik "Steve" ketika diminta, itu akan dicetak

you typed: "Steve
"

Itu "pada baris kedua adalah karena string yang dibacanya dan dicetak kembali sebenarnya "Steve\n".

Kadang-kadang baris baru tambahan itu tidak masalah (seperti ketika kita menelepon atoiatau atof, karena mereka berdua mengabaikan input non-numerik tambahan setelah nomor), tetapi kadang-kadang itu sangat berarti. Sering kali kita ingin menghilangkan baris baru itu. Ada beberapa cara untuk melakukan itu, yang akan saya bahas sebentar lagi. (Aku tahu aku sudah mengatakan itu banyak. Tapi aku akan kembali ke semua hal itu, aku janji.)

Pada titik ini, Anda mungkin berpikir: "Saya pikir Anda mengatakan scanf itu tidak baik, dan cara lain ini akan jauh lebih baik. Tetapi fgetsmulai terlihat seperti gangguan. Memanggil scanfitu mudah ! Tidak bisakah saya tetap menggunakannya? "

Tentu, Anda bisa terus menggunakan scanf, jika mau. (Dan untuk hal-hal yang sangat sederhana, dalam beberapa hal itu lebih sederhana.) Tapi, tolong, jangan datang menangis kepada saya ketika itu membuat Anda gagal karena salah satu dari 17 keanehan dan kelemahannya, atau masuk ke loop tak terhingga karena memasukkan Anda tidak berharap, atau ketika Anda tidak tahu cara menggunakannya untuk melakukan sesuatu yang lebih rumit. Dan mari kita lihat fgetsgangguan yang sebenarnya:

  1. Anda selalu harus menentukan ukuran array. Yah, tentu saja, itu sama sekali bukan gangguan - itu fitur, karena buffer overflow adalah Hal yang Sangat Buruk.

  2. Anda harus memeriksa nilai kembali. Sebenarnya, itu adalah pencucian, karena untuk menggunakannya scanfdengan benar, Anda harus memeriksa nilai pengembaliannya juga.

  3. Anda harus melepaskan bagian \nbelakangnya. Saya akui, ini benar-benar gangguan. Saya berharap ada fungsi standar yang bisa saya tunjukkan kepada Anda yang tidak memiliki masalah kecil ini. (Tolong tidak ada yang mengemukakan gets.) Tetapi dibandingkan dengan scanf's17 gangguan yang berbeda, saya akan mengambil gangguan yang satu ini fgetssetiap hari.

Jadi bagaimana cara Anda strip baris baru itu? Tiga jalan:

(a) Cara yang jelas:

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

(B) Cara rumit & kompak:

strtok(line, "\n");

Sayangnya yang ini tidak selalu berhasil.

(C) Cara lain kompak dan agak tidak jelas:

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

Dan sekarang setelah keluar dari jalan, kita dapat kembali ke hal lain yang saya lewatkan: ketidaksempurnaan atoi()dan atof(). Masalahnya adalah mereka tidak memberi Anda indikasi sukses atau gagal: mereka diam-diam mengabaikan input nonnumerik, dan mereka diam-diam mengembalikan 0 jika tidak ada input numerik sama sekali. Alternatif yang lebih disukai - yang juga memiliki kelebihan lain - adalah strtoldan strtod. strtoljuga memungkinkan Anda menggunakan basis selain 10, artinya Anda bisa mendapatkan efek (antara lain) %oatau %xdenganscanf. Tetapi menunjukkan bagaimana menggunakan fungsi-fungsi ini dengan benar adalah cerita itu sendiri, dan akan menjadi terlalu banyak gangguan dari apa yang sudah berubah menjadi narasi yang cukup terfragmentasi, jadi saya tidak akan mengatakan apa-apa lagi tentang mereka sekarang.

Sisa dari narasi utama menyangkut input yang mungkin Anda coba uraikan yang lebih rumit daripada hanya satu angka atau karakter. Bagaimana jika Anda ingin membaca baris yang berisi dua angka, atau beberapa kata yang dipisahkan spasi, atau tanda baca framing tertentu? Di situlah hal-hal menjadi menarik, dan di mana hal-hal itu mungkin menjadi rumit jika Anda mencoba melakukan hal-hal menggunakan scanf, dan di mana ada jauh lebih banyak opsi sekarang bahwa Anda telah membaca satu baris teks dengan bersih fgets, meskipun cerita lengkap tentang semua opsi tersebut mungkin bisa mengisi buku, jadi kita hanya akan bisa menggaruk permukaan di sini.

  1. Teknik favorit saya adalah memecah garis menjadi "kata-kata" yang dipisahkan oleh spasi, kemudian melakukan sesuatu lebih jauh dengan setiap "kata". Salah satu fungsi Standar utama untuk melakukan ini adalah strtok(yang juga memiliki masalah, dan yang juga menilai seluruh diskusi terpisah). Preferensi saya sendiri adalah fungsi khusus untuk membangun array pointer ke setiap "kata" yang terpisah, sebuah fungsi yang saya jelaskan dalam catatan kursus ini . Bagaimanapun, setelah Anda mendapatkan "kata-kata", Anda dapat memproses lebih lanjut masing-masing, mungkin dengan fungsi yang sama atoi/ atof/ strtol/ strtod kita sudah melihat.

  2. Paradoksnya, meskipun kita telah menghabiskan cukup banyak waktu dan upaya di sini untuk mencari tahu bagaimana cara menjauh scanf, cara lain yang baik untuk berurusan dengan baris teks yang baru saja kita baca fgetsadalah dengan meneruskannya sscanf. Dengan cara ini, Anda berakhir dengan sebagian besar keuntungan scanf, tetapi tanpa sebagian besar kerugian.

  3. Jika sintaks input Anda sangat rumit, mungkin perlu menggunakan pustaka "regexp" untuk menguraikannya.

  4. Terakhir, Anda dapat menggunakan solusi parsing ad hoc apa pun yang cocok untuk Anda. Anda dapat bergerak melalui garis karakter pada suatu waktu dengan char *pointer memeriksa karakter yang Anda harapkan. Atau Anda dapat mencari karakter tertentu menggunakan fungsi seperti strchratau strrchr, atau strspnatau strcspn, atau strpbrk. Atau Anda dapat mem-parsing / mengonversi dan melewati kelompok karakter digit menggunakan strtolatau strtodfungsi yang kami lewati sebelumnya.

Jelas ada banyak lagi yang bisa dikatakan, tapi mudah-mudahan pengantar ini akan membantu Anda memulai.

Steve Summit
sumber
Apakah ada alasan yang baik untuk menulis sizeof (line)daripada sekadar sizeof line? Yang pertama membuatnya terlihat seperti linenama tipe!
Toby Speight
@TobySpeight Alasan bagus? Tidak, saya meragukannya. Tanda kurung adalah kebiasaan saya, karena saya tidak dapat diganggu untuk mengingat apakah itu objek atau nama ketikkan yang mereka perlukan, tetapi banyak programmer meninggalkannya ketika mereka bisa. (Bagi saya itu masalah preferensi dan gaya pribadi, dan yang cukup kecil pada saat itu.)
Steve Summit
+1 untuk digunakan sscanfsebagai mesin konversi tetapi mengumpulkan (dan mungkin memijat) input dengan alat yang berbeda. Tapi mungkin layak disebutkan getlinedalam konteks itu.
dmckee --- ex-moderator kitten
Ketika Anda berbicara tentang " fscanfgangguan aktual", maksud Anda fgets? Dan gangguan # 3 benar-benar membuatku jengkel, terutama mengingat bahwa scanfmengembalikan pointer yang tidak berguna ke buffer daripada mengembalikan jumlah input karakter (yang akan membuat pengupasan baris baru jauh lebih bersih).
supercat
1
Terima kasih atas penjelasan sizeofgaya Anda . Bagi saya, mengingat ketika Anda melihat parens itu mudah: Saya anggap (type)seperti pemain tanpa nilai (karena kami hanya tertarik pada jenisnya). Satu hal lagi: Anda mengatakan itu strtok(line, "\n")tidak selalu berhasil, tetapi tidak jelas kapan itu mungkin tidak. Saya kira Anda berpikir tentang kasus di mana garis lebih panjang dari buffer, jadi kami tidak memiliki baris baru, dan strtok()mengembalikan nol? Sangat disayangkan fgets()tidak mengembalikan nilai yang lebih berguna sehingga kita bisa tahu apakah baris baru ada atau tidak.
Toby Speight
7

Apa yang dapat saya gunakan untuk mem-parsing input daripada memindai?

Alih-alih scanf(some_format, ...), pertimbangkan fgets()dengansscanf(buffer, some_format_and %n, ...)

Dengan menggunakan " %n", kode dapat dengan mudah mendeteksi jika semua format berhasil dipindai dan tidak ada sampah non-spasi putih di akhir.

// 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 - Pasang kembali Monica
sumber
6

Mari kita nyatakan persyaratan parsing sebagai:

  • input yang valid harus diterima (dan dikonversi ke bentuk lain)

  • input yang tidak valid harus ditolak

  • ketika input apa pun ditolak, maka perlu untuk memberikan pengguna dengan pesan deskriptif yang menjelaskan (secara jelas "mudah dimengerti oleh orang normal yang bukan pemrogram" bahasa) mengapa itu ditolak (sehingga orang dapat mencari cara untuk memperbaiki masalah)

Untuk menjaga hal-hal yang sangat sederhana, mari kita mempertimbangkan penguraian bilangan bulat desimal tunggal (yang diketik oleh pengguna) dan tidak ada yang lain. Kemungkinan alasan input pengguna untuk ditolak adalah:

  • input berisi karakter yang tidak dapat diterima
  • input mewakili angka yang lebih rendah dari minimum yang diterima
  • input mewakili angka yang lebih tinggi dari maksimum yang diterima
  • input mewakili angka yang memiliki bagian pecahan bukan nol

Mari kita juga mendefinisikan "input berisi karakter yang tidak dapat diterima" dengan benar; dan katakan itu:

  • spasi putih dan spasi spasial terkemuka akan diabaikan (mis. "
    5" akan diperlakukan sebagai "5")
  • nol atau satu titik desimal diizinkan (mis. "1234." dan "1234.000" keduanya diperlakukan sama dengan "1234")
  • minimal harus ada satu digit (mis. "." ditolak)
  • tidak lebih dari satu titik desimal diizinkan (mis. "1.2.3" ditolak)
  • koma yang tidak di antara digit akan ditolak (mis. ", 1234" ditolak)
  • koma yang setelah titik desimal akan ditolak (mis. "1234.000.000" ditolak)
  • koma yang setelah koma lain ditolak (mis. "1,, 234" ditolak)
  • semua koma lainnya akan diabaikan (mis. "1.234" akan diperlakukan sebagai "1234")
  • tanda minus yang bukan karakter non-spasi pertama ditolak
  • tanda positif yang bukan karakter non-spasi pertama ditolak

Dari ini kita dapat menentukan bahwa pesan kesalahan berikut diperlukan:

  • "Karakter tidak dikenal pada awal input"
  • "Karakter tidak dikenal di akhir input"
  • "Karakter tidak dikenal di tengah input"
  • "Jumlahnya terlalu rendah (minimum adalah ....)"
  • "Jumlahnya terlalu tinggi (maksimum adalah ....)"
  • "Angka bukan bilangan bulat"
  • "Terlalu banyak titik desimal"
  • "Tidak ada angka desimal"
  • "Koma buruk di awal nomor"
  • "Koma buruk di akhir angka"
  • "Koma buruk di tengah angka"
  • "Koma buruk setelah titik desimal"

Dari titik ini kita dapat melihat bahwa fungsi yang cocok untuk mengubah string menjadi integer perlu membedakan antara jenis kesalahan yang sangat berbeda; dan sesuatu seperti " scanf()" atau " atoi()" atau "strtoll() " sama sekali dan sama sekali tidak berharga karena mereka gagal memberi Anda indikasi apa pun yang salah dengan input (dan menggunakan definisi yang benar-benar tidak relevan dan tidak tepat tentang apa yang / tidak "valid" memasukkan").

Sebagai gantinya, mari mulai menulis sesuatu yang tidak berguna:

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;
}

Untuk memenuhi persyaratan yang dinyatakan; ini convertStringToInteger()fungsi kemungkinan akan berakhir menjadi beberapa ratus baris kode dengan sendirinya.

Sekarang, ini hanya "parsing bilangan bulat desimal tunggal". Bayangkan jika Anda ingin menguraikan sesuatu yang kompleks; seperti daftar struktur "nama, alamat jalan, nomor telepon, alamat email"; atau mungkin seperti bahasa pemrograman. Untuk kasus ini, Anda mungkin perlu menulis ribuan baris kode untuk membuat parse yang bukan lelucon lumpuh.

Dengan kata lain...

Apa yang dapat saya gunakan untuk mem-parsing input daripada memindai?

Tulis sendiri (berpotensi ribuan baris) kode, sesuai dengan kebutuhan Anda.

Brendan
sumber
5

Berikut adalah contoh penggunaan flexuntuk memindai input sederhana, dalam hal ini file angka floating point ASCII yang mungkin dalam format US ( n,nnn.dd) atau Eropa ( n.nnn,dd). Ini hanya disalin dari program yang jauh lebih besar, jadi mungkin ada beberapa referensi yang tidak terselesaikan:

/* 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
sumber
-5

Jawaban lain memberikan perincian tingkat rendah yang tepat, jadi saya akan membatasi diri ke tingkat yang lebih tinggi: Pertama, analisis seperti apa tampilan setiap garis input. Cobalah untuk mendeskripsikan input dengan sintaks formal - jika beruntung, Anda akan menemukannya dapat dijelaskan menggunakan tata bahasa biasa , atau setidaknya tata bahasa bebas konteks . Jika tata bahasa biasa sudah mencukupi, maka Anda dapat membuat kode a finite-stateyang mengenali dan menafsirkan setiap karakter baris perintah satu per satu. Kode Anda kemudian akan membaca baris (seperti yang dijelaskan dalam balasan lain), kemudian memindai karakter di buffer melalui mesin negara. Di negara bagian tertentu Anda berhenti dan mengonversi media yang dipindai sejauh ini ke angka atau apa pun. Anda mungkin dapat 'roll your own' jika ini sederhana; jika Anda membutuhkan tata bahasa bebas konteks lengkap, Anda lebih baik mencari tahu cara menggunakan alat parsing yang ada (ulang: lexdan yaccatau variannya).

PMar
sumber
Mesin keadaan terbatas mungkin berlebihan; cara yang lebih mudah untuk mendeteksi overflow dalam konversi (seperti memeriksa jika errno == EOVERFLOWsetelah menggunakan strtoll) dimungkinkan.
SS Anne
1
Mengapa Anda membuat kode mesin negara terbatas Anda sendiri, ketika flex membuat penulisan mereka menjadi sangat mudah?
jamesqf