รับมาตรฐาน :: ifstream เพื่อจัดการ LF, CR และ CRLF หรือไม่


85

ฉันสนใจistream& getline ( istream& is, string& str );โดยเฉพาะ มีตัวเลือกสำหรับตัวสร้าง ifstream เพื่อบอกให้แปลงการเข้ารหัสบรรทัดใหม่ทั้งหมดเป็น '\ n' ภายใต้ประทุนหรือไม่ ฉันต้องการที่จะสามารถโทรgetlineและให้มันจัดการกับจุดสิ้นสุดของบรรทัดทั้งหมดได้อย่างสง่างาม

อัปเดต : เพื่อความชัดเจนฉันต้องการที่จะสามารถเขียนโค้ดที่รวบรวมได้เกือบทุกที่และจะรับข้อมูลจากเกือบทุกที่ รวมถึงไฟล์หายากที่มี "\ r" โดยไม่มี "\ n" ลดความไม่สะดวกสำหรับผู้ใช้ซอฟต์แวร์ใด ๆ

เป็นเรื่องง่ายที่จะแก้ไขปัญหานี้ แต่ฉันยังคงสงสัยเกี่ยวกับวิธีที่ถูกต้องในมาตรฐานเพื่อจัดการกับรูปแบบไฟล์ข้อความทั้งหมดได้อย่างยืดหยุ่น

getlineอ่านเป็นบรรทัดเต็มถึง '\ n' เป็นสตริง '\ n' ถูกใช้จากสตรีม แต่ getline ไม่รวมไว้ในสตริง จนถึงตอนนี้ แต่อาจมี "\ r" อยู่ก่อนหน้า "\ n" ที่รวมอยู่ในสตริง

มีสามประเภทของการสิ้นสุดบรรทัดที่เห็นในไฟล์ข้อความ: '\ n' คือการลงท้ายแบบเดิมบนเครื่อง Unix '\ r' คือ (ฉันคิดว่า) ใช้กับระบบปฏิบัติการ Mac รุ่นเก่าและ Windows ใช้คู่ '\ r' ตามด้วย "\ n"

ปัญหาคือgetlineปล่อย '\ r' ไว้ที่ท้ายสตริง

ifstream f("a_text_file_of_unknown_origin");
string line;
getline(f, line);
if(!f.fail()) { // a non-empty line was read
   // BUT, there might be an '\r' at the end now.
}

แก้ไขขอบคุณนีลที่ชี้ให้เห็นว่านั่นf.good()ไม่ใช่สิ่งที่ฉันต้องการ !f.fail()คือสิ่งที่ฉันต้องการ

ฉันสามารถลบออกได้ด้วยตนเอง (ดูการแก้ไขคำถามนี้) ซึ่งง่ายสำหรับไฟล์ข้อความของ Windows แต่ฉันกังวลว่าจะมีคนป้อนไฟล์ที่มี แต่ "\ r" ในกรณีนั้นฉันคิดว่า getline จะกินทั้งไฟล์โดยคิดว่ามันเป็นบรรทัดเดียว!

.. และนั่นยังไม่ได้พิจารณา Unicode ด้วยซ้ำ :-)

.. บางที Boost อาจมีวิธีที่ดีในการใช้ทีละบรรทัดจากไฟล์ข้อความประเภทใดก็ได้?

แก้ไขฉันใช้สิ่งนี้เพื่อจัดการไฟล์ Windows แต่ฉันยังรู้สึกว่าไม่ควรทำ! และสิ่งนี้จะไม่แยกสำหรับไฟล์ "\ r'-only

if(!line.empty() && *line.rbegin() == '\r') {
    line.erase( line.length()-1, 1);
}

2
\ n หมายถึงบรรทัดใหม่ในลักษณะใดก็ตามที่นำเสนอในระบบปฏิบัติการปัจจุบัน ห้องสมุดดูแลนั้น แต่เพื่อให้ใช้งานได้โปรแกรมที่คอมไพล์ใน windows ควรอ่านไฟล์ข้อความจาก windows โปรแกรมที่คอมไพล์ใน unix ไฟล์ข้อความจาก unix เป็นต้น
George Kastrinis

1
@George แม้ว่าฉันจะรวบรวมบนเครื่อง Linux แต่บางครั้งฉันก็ใช้ไฟล์ข้อความที่มาจากเครื่อง Windows ฉันอาจปล่อยซอฟต์แวร์ของฉัน (เครื่องมือขนาดเล็กสำหรับการวิเคราะห์เครือข่าย) และฉันต้องการที่จะบอกผู้ใช้ว่าพวกเขาสามารถป้อนไฟล์ข้อความ (เหมือน ASCII) ได้เกือบทุกเวลา
Aaron McDaid

3
testcase เล็ก ๆ น้อย ๆ ที่แสดงให้เห็นถึงปัญหาของคุณได้
Lightness Races ใน Orbit

1
สังเกตว่าถ้า (f.good ()) ไม่ทำในสิ่งที่คุณคิดว่าทำ

1
@JonathanMee: มันอาจจะเป็นเช่นนี้ อาจจะ.
Lightness Races ใน Orbit

คำตอบ:


111

ดังที่นีลชี้ให้เห็นว่า "รันไทม์ C ++ ควรจัดการอย่างถูกต้องกับข้อตกลงการสิ้นสุดบรรทัดสำหรับแพลตฟอร์มเฉพาะของคุณ"

อย่างไรก็ตามผู้คนย้ายไฟล์ข้อความระหว่างแพลตฟอร์มที่แตกต่างกันจึงไม่ดีพอ นี่คือฟังก์ชันที่จัดการกับส่วนท้ายบรรทัดทั้งสาม ("\ r", "\ n" และ "\ r \ n"):

std::istream& safeGetline(std::istream& is, std::string& t)
{
    t.clear();

    // The characters in the stream are read one-by-one using a std::streambuf.
    // That is faster than reading them one-by-one using the std::istream.
    // Code that uses streambuf this way must be guarded by a sentry object.
    // The sentry object performs various tasks,
    // such as thread synchronization and updating the stream state.

    std::istream::sentry se(is, true);
    std::streambuf* sb = is.rdbuf();

    for(;;) {
        int c = sb->sbumpc();
        switch (c) {
        case '\n':
            return is;
        case '\r':
            if(sb->sgetc() == '\n')
                sb->sbumpc();
            return is;
        case std::streambuf::traits_type::eof():
            // Also handle the case when the last line has no line ending
            if(t.empty())
                is.setstate(std::ios::eofbit);
            return is;
        default:
            t += (char)c;
        }
    }
}

และนี่คือโปรแกรมทดสอบ:

int main()
{
    std::string path = ...  // insert path to test file here

    std::ifstream ifs(path.c_str());
    if(!ifs) {
        std::cout << "Failed to open the file." << std::endl;
        return EXIT_FAILURE;
    }

    int n = 0;
    std::string t;
    while(!safeGetline(ifs, t).eof())
        ++n;
    std::cout << "The file contains " << n << " lines." << std::endl;
    return EXIT_SUCCESS;
}

1
@Miek: ฉันได้อัปเดตโค้ดตามคำแนะนำของ Bo Persons stackoverflow.com/questions/9188126/…และทำการทดสอบบางอย่าง ทุกอย่างทำงานได้ตามที่ควรแล้ว
Johan Råde

1
@ โทมัสเวลเลอร์: ตัวสร้างและตัวทำลายสำหรับยามจะถูกดำเนินการ สิ่งเหล่านี้ทำสิ่งต่างๆเช่นการซิงโครไนซ์เธรดการข้ามช่องว่างและการอัปเดตสถานะสตรีม
Johan Råde

1
ในกรณี EOF จุดประสงค์ของการตรวจสอบว่าtว่างก่อนตั้งค่า eofbit คืออะไร ไม่ควรตั้งค่าบิตนั้นโดยไม่คำนึงถึงอักขระอื่น ๆ ที่อ่านอยู่?
Yay295

1
Yay295: ควรตั้งค่าแฟล็ก eof ไม่ใช่เมื่อคุณไปถึงจุดสิ้นสุดของบรรทัดสุดท้าย แต่เมื่อคุณพยายามอ่านเกินบรรทัดสุดท้าย ตรวจสอบให้แน่ใจว่าสิ่งนี้เกิดขึ้นเมื่อบรรทัดสุดท้ายไม่มี EOL (ลองลบเครื่องหมายถูกออกแล้วรันโปรแกรมทดสอบในไฟล์ข้อความที่บรรทัดสุดท้ายไม่มี EOL แล้วคุณจะเห็น)
Johan Råde

3
นอกจากนี้ยังอ่านบรรทัดสุดท้ายที่ว่างเปล่าซึ่งไม่ใช่ลักษณะการทำงานstd::get_lineที่ละเว้นบรรทัดสุดท้ายที่ว่างเปล่า ฉันใช้รหัสต่อไปนี้ในกรณี eof เพื่อเลียนแบบstd::get_lineพฤติกรรม:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks

11

รันไทม์ C ++ ควรจัดการอย่างถูกต้องกับข้อตกลง endline สำหรับแพลตฟอร์มเฉพาะของคุณ โดยเฉพาะรหัสนี้ควรใช้ได้กับทุกแพลตฟอร์ม:

#include <string>
#include <iostream>
using namespace std;

int main() {
    string line;
    while( getline( cin, line ) ) {
        cout << line << endl;
    }
}

แน่นอนว่าหากคุณกำลังจัดการกับไฟล์จากแพลตฟอร์มอื่นการเดิมพันทั้งหมดจะถูกปิด

ในฐานะที่เป็นสองแพลตฟอร์มที่พบบ่อยที่สุด (Linux และ Windows) ทั้งสองจะสิ้นสุดบรรทัดด้วยอักขระขึ้นบรรทัดใหม่โดยที่ Windows นำหน้าด้วยการคืนค่าขนส่งคุณสามารถตรวจสอบอักขระสุดท้ายของlineสตริงในโค้ดด้านบนเพื่อดูว่าเป็น\rเช่นนั้นหรือไม่ ลบออกก่อนดำเนินการประมวลผลเฉพาะแอปพลิเคชันของคุณ

ตัวอย่างเช่นคุณสามารถระบุฟังก์ชันสไตล์ getline ที่มีลักษณะเช่นนี้ได้ (ไม่ได้ทดสอบใช้ดัชนีตัวย่อย ฯลฯ เพื่อจุดประสงค์ในการสอนเท่านั้น):

ostream & safegetline( ostream & os, string & line ) {
    string myline;
    if ( getline( os, myline ) ) {
       if ( myline.size() && myline[myline.size()-1] == '\r' ) {
           line = myline.substr( 0, myline.size() - 1 );
       }
       else {
           line = myline;
       }
    }
    return os;
}

9
คำถามคือเกี่ยวกับวิธีจัดการกับไฟล์จากแพลตฟอร์มอื่น
Lightness Races ใน Orbit

4
@ นีลคำตอบนี้ยังไม่เพียงพอ ถ้าฉันแค่ต้องการจัดการ CRLFs ฉันคงไม่มาที่ StackOverflow ความท้าทายที่แท้จริงคือการจัดการไฟล์ที่มีเฉพาะ '\ r' ปัจจุบันนี้หายากแล้วตอนนี้ MacOS เข้าใกล้ Unix มากขึ้น แต่ฉันไม่อยากคิดว่ามันจะไม่ถูกป้อนเข้ากับซอฟต์แวร์ของฉัน
Aaron McDaid

1
@Aaron ดีถ้าคุณต้องการที่จะสามารถจัดการกับสิ่งใด ๆ คุณต้องเขียนโค้ดของคุณเองเพื่อทำมัน

4
ฉันตั้งคำถามไว้อย่างชัดเจนตั้งแต่เริ่มต้นว่ามันง่ายที่จะแก้ไขปัญหานี้โดยหมายความว่าฉันเต็มใจและสามารถทำได้ ฉันถามเกี่ยวกับเรื่องนี้เพราะดูเหมือนจะเป็นคำถามทั่วไปและมีรูปแบบไฟล์ข้อความที่หลากหลาย ฉันสันนิษฐาน / หวังว่าคณะกรรมการมาตรฐาน C ++ ได้สร้างสิ่งนี้ขึ้นมานี่คือคำถามของฉัน
Aaron McDaid

1
@ นีลฉันคิดว่ามีปัญหาอื่นที่ฉัน / เราลืมไปแล้ว แต่ก่อนอื่นฉันยอมรับว่ามันเป็นประโยชน์สำหรับฉันที่จะระบุรูปแบบจำนวนเล็กน้อยที่จะรองรับ ดังนั้นฉันต้องการโค้ดที่จะคอมไพล์บน Windows และ Linux ซึ่งจะใช้ได้กับรูปแบบใดรูปแบบหนึ่ง ของคุณsafegetlineเป็นส่วนสำคัญของการแก้ปัญหา แต่ถ้าโปรแกรมนี้กำลังคอมไพล์บน Windows ฉันจะต้องเปิดไฟล์ในรูปแบบไบนารีด้วยหรือไม่? คอมไพเลอร์ของ Windows (ในโหมดข้อความ) อนุญาตให้ '\ n' ทำงานเหมือน '\ r' '\ n' หรือไม่ ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid

8

คุณกำลังอ่านไฟล์ในBINARYหรือในโหมดTEXT ? ในโหมดTEXTการส่งคืนค่าการขนส่งคู่ / การป้อนบรรทัดCRLFจะถูกตีความเป็นTEXT end of line หรือ end of line character แต่ในBINARYคุณดึงข้อมูลได้ครั้งละหนึ่งไบต์เท่านั้นซึ่งหมายความว่าอักขระใด ๆต้องถูกละเว้นและทิ้งไว้ในบัฟเฟอร์เพื่อดึงข้อมูลเป็นไบต์อื่น! การคืนแคร่หมายถึงในเครื่องพิมพ์ดีดว่ารถพิมพ์ดีดที่แขนพิมพ์อยู่ถึงขอบกระดาษด้านขวาและกลับไปที่ขอบด้านซ้าย นี่เป็นแบบจำลองเชิงกลของเครื่องพิมพ์ดีดเชิงกล จากนั้นการป้อนบรรทัดหมายความว่าม้วนกระดาษหมุนขึ้นเล็กน้อยเพื่อให้กระดาษอยู่ในตำแหน่งที่จะเริ่มพิมพ์บรรทัดอื่น เช่นเดียวกับที่ฉันจำตัวเลขหนึ่งในตัวเลขต่ำใน ASCII หมายถึงเลื่อนไปทางขวาหนึ่งอักขระโดยไม่ต้องพิมพ์อักขระที่ตายแล้วและแน่นอน \ b หมายถึงแบ็กสเปซ: เลื่อนรถไปข้างหลังหนึ่งอักขระ ด้วยวิธีนี้คุณสามารถเพิ่มเอฟเฟกต์พิเศษเช่นการขีดเส้นใต้ (พิมพ์ขีดล่าง) ขีดทับ (พิมพ์ลบ) โดยประมาณสำเนียงที่แตกต่างกันยกเลิก (ประเภท X) โดยไม่ต้องใช้แป้นพิมพ์เสริม เพียงแค่ปรับตำแหน่งของรถตามแนวเส้นก่อนเข้าสู่ไลน์ฟีด ดังนั้นคุณสามารถใช้แรงดันไฟฟ้า ASCII ขนาดไบต์เพื่อควบคุมเครื่องพิมพ์ดีดโดยอัตโนมัติโดยไม่มีคอมพิวเตอร์อยู่ระหว่างนั้น เมื่อมีการนำเครื่องพิมพ์ดีดอัตโนมัติมาใช้อัตโนมัติหมายความว่าเมื่อคุณไปถึงขอบกระดาษที่ไกลที่สุดรถจะกลับไปทางซ้ายและใช้การป้อนเส้นนั่นคือรถจะถูกส่งกลับโดยอัตโนมัติเมื่อม้วนเลื่อนขึ้น! ดังนั้นคุณจึงไม่จำเป็นต้องใช้อักขระควบคุมทั้งสองตัวเพียงตัวเดียว \ n บรรทัดใหม่หรือฟีดบรรทัด

สิ่งนี้ไม่เกี่ยวข้องกับการเขียนโปรแกรม แต่ ASCII เก่ากว่าและเฮ้! ดูเหมือนบางคนไม่ได้คิดเมื่อพวกเขาเริ่มทำข้อความ! แพลตฟอร์ม UNIX ถือว่าเป็นเครื่องพิมพ์ดีดอัตโนมัติไฟฟ้า โมเดล Windows สมบูรณ์กว่าและช่วยให้สามารถควบคุมเครื่องจักรกลแม้ว่าอักขระควบคุมบางตัวจะมีประโยชน์น้อยลงเรื่อย ๆ ในคอมพิวเตอร์เช่นอักขระกระดิ่ง 0x07 ถ้าฉันจำได้ดี ... สำหรับเครื่องพิมพ์ดีดที่ควบคุมด้วยระบบไฟฟ้าและทำให้รุ่นต่อเนื่อง ...

จริงๆแล้วรูปแบบที่ถูกต้องจะเป็นเพียงแค่ใส่ \ r, line feed, การส่งคืนแคร่โดยไม่จำเป็นนั่นคืออัตโนมัติดังนั้น:

char c;
ifstream is;
is.open("",ios::binary);
...
is.getline(buffer, bufsize, '\r');

//ignore following \n or restore the buffer data
if ((c=is.get())!='\n') is.rdbuf()->sputbackc(c);
...

จะเป็นวิธีที่ถูกต้องที่สุดในการจัดการไฟล์ทุกประเภท อย่างไรก็ตามโปรดทราบว่า \ n ในโหมดTEXTเป็นคู่ไบต์ 0x0d 0x0a แต่ 0x0d เป็นเพียง \ r: \ n รวม \ r ในโหมดTEXTแต่ไม่อยู่ในBINARYดังนั้น \ n และ \ r \ n จะเท่ากัน ... หรือ ควรจะเป็น. นี่เป็นความสับสนของอุตสาหกรรมขั้นพื้นฐานที่จริงแล้วความเฉื่อยของอุตสาหกรรมโดยทั่วไปเนื่องจากการประชุมจะพูดถึง CRLF ในทุกแพลตฟอร์มจากนั้นจึงตกอยู่ในการตีความไบนารีที่แตกต่างกัน พูดอย่างเคร่งครัดไฟล์ที่รวมเฉพาะ 0x0d (การส่งคืนรถ) ที่เป็น \ n (CRLF หรือฟีดบรรทัด) มีรูปแบบไม่ถูกต้องในTEXTโหมด (เครื่องพิมพ์ดีด: เพียงแค่คืนรถและขีดฆ่าทุกอย่าง ... ) และเป็นรูปแบบไบนารีที่ไม่เน้นบรรทัด (ไม่ว่าจะเป็น \ r หรือ \ r \ n หมายถึงบรรทัดที่มุ่งเน้น) ดังนั้นคุณไม่ควรอ่านเป็นข้อความ! รหัสควรจะล้มเหลวโดยอาจมีข้อความของผู้ใช้ สิ่งนี้ไม่ได้ขึ้นอยู่กับระบบปฏิบัติการเท่านั้น แต่ยังรวมถึงการใช้งานไลบรารี C ด้วยการเพิ่มความสับสนและรูปแบบที่เป็นไปได้ ...

ปัญหาเกี่ยวกับข้อมูลโค้ดก่อนหน้านี้ (เครื่องพิมพ์ดีดเชิงกล) คือไม่มีประสิทธิภาพมากหากไม่มีอักขระ \ n หลัง \ r (ข้อความเครื่องพิมพ์ดีดอัตโนมัติ) จากนั้นจะถือว่าโหมดไบนารีที่ไลบรารี C ถูกบังคับให้ละเว้นการตีความข้อความ (โลแคล) และแจกไบต์ที่แท้จริง ไม่ควรมีความแตกต่างในอักขระข้อความจริงระหว่างทั้งสองโหมดเฉพาะในอักขระควบคุมดังนั้นโดยทั่วไปการอ่านBINARYจะดีกว่าโหมดTEXT โซลูชันนี้มีประสิทธิภาพสำหรับBINARYโหมดไฟล์ข้อความ Windows OS ทั่วไปโดยไม่ขึ้นกับรูปแบบไลบรารี C และไม่มีประสิทธิภาพสำหรับรูปแบบข้อความแพลตฟอร์มอื่น ๆ (รวมถึงการแปลเว็บเป็นข้อความ) หากคุณสนใจเกี่ยวกับประสิทธิภาพวิธีที่จะไปคือใช้ตัวชี้ฟังก์ชันทำการทดสอบการควบคุมบรรทัด \ r vs \ r \ n ตามที่คุณต้องการจากนั้นเลือกรหัสผู้ใช้ getline ที่ดีที่สุดลงในตัวชี้และเรียกใช้จาก มัน.

บังเอิญฉันจำได้ว่าฉันพบไฟล์ข้อความ \ r \ r \ n ด้วย ... ซึ่งแปลเป็นข้อความบรรทัดคู่เช่นเดียวกับที่ผู้บริโภคข้อความพิมพ์บางรายต้องการ


+1 สำหรับ "ios :: binary" - บางครั้งคุณต้องการอ่านไฟล์ตามความเป็นจริง (เช่นการคำนวณการตรวจสอบ ฯลฯ ) โดยที่รันไทม์ไม่เปลี่ยนการสิ้นสุดบรรทัด
Matthias

2

ทางออกหนึ่งคือค้นหาก่อนและแทนที่ส่วนท้ายบรรทัดทั้งหมดเป็น '\ n' - เช่นเดียวกับเช่น Git ทำตามค่าเริ่มต้น


1

นอกเหนือจากการเขียนตัวจัดการแบบกำหนดเองของคุณเองหรือใช้ไลบรารีภายนอกแล้วคุณก็โชคไม่ดี วิธีที่ง่ายที่สุดคือตรวจสอบให้แน่ใจว่าline[line.length() - 1]ไม่ใช่ "\ r" บน Linux สิ่งนี้ไม่จำเป็นเนื่องจากบรรทัดส่วนใหญ่จะลงท้ายด้วย '\ n' ซึ่งหมายความว่าคุณจะเสียเวลาพอสมควรหากเป็นแบบวนซ้ำ ใน Windows สิ่งนี้ยังไม่จำเป็น อย่างไรก็ตามไฟล์ Mac แบบคลาสสิกที่ลงท้ายด้วย '\ r' ล่ะ? std :: getline จะใช้ไม่ได้กับไฟล์เหล่านั้นบน Linux หรือ Windows เนื่องจาก '\ n' และ '\ r' '\ n' ทั้งสองลงท้ายด้วย '\ n' ทำให้ไม่จำเป็นต้องตรวจสอบ '\ r' เห็นได้ชัดว่างานที่ทำงานกับไฟล์เหล่านั้นจะทำงานได้ไม่ดี แน่นอนว่ามีระบบ EBCDIC จำนวนมากซึ่งเป็นสิ่งที่ห้องสมุดส่วนใหญ่ไม่กล้าจัดการ

การตรวจสอบ "\ r" น่าจะเป็นวิธีแก้ปัญหาที่ดีที่สุด การอ่านในโหมดไบนารีจะช่วยให้คุณตรวจสอบการลงท้ายบรรทัดทั่วไปทั้งสาม ('\ r', '\ r \ n' และ '\ n') หากคุณสนใจเฉพาะ Linux และ Windows เนื่องจากส่วนท้ายบรรทัด Mac แบบเก่าไม่ควรอยู่นานกว่านั้นให้เลือก "\ n" เท่านั้นและลบอักขระ "\ r" ต่อท้าย


0

ถ้าทราบว่าแต่ละบรรทัดมีกี่รายการ / ตัวเลขก็สามารถอ่านได้ 1 บรรทัดโดยมีเช่น 4 ตัวเลขเป็น

string num;
is >> num >> num >> num >> num;

นอกจากนี้ยังใช้ได้กับส่วนท้ายบรรทัดอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.