Bắt std :: ifstream để xử lý LF, CR và CRLF?


85

Cụ thể tôi quan tâm đến istream& getline ( istream& is, string& str );. Có tùy chọn nào đối với phương thức khởi tạo ifstream để yêu cầu nó chuyển đổi tất cả các mã hóa dòng mới thành '\ n' không? Tôi muốn có thể gọi getlinevà để nó xử lý tất cả các kết thúc dòng một cách duyên dáng.

Cập nhật : Để làm rõ, tôi muốn có thể viết mã biên dịch ở hầu hết mọi nơi và sẽ lấy đầu vào từ hầu hết mọi nơi. Bao gồm các tệp hiếm có '\ r' mà không có '\ n'. Giảm thiểu sự bất tiện cho bất kỳ người dùng phần mềm.

Thật dễ dàng để giải quyết vấn đề, nhưng tôi vẫn tò mò về cách phù hợp, theo tiêu chuẩn, để xử lý linh hoạt tất cả các định dạng tệp văn bản.

getlineđọc toàn bộ dòng, tối đa '\ n', thành một chuỗi. '\ N' được sử dụng từ luồng, nhưng getline không bao gồm nó trong chuỗi. Điều đó tốt cho đến nay, nhưng có thể có '\ r' ngay trước '\ n' được đưa vào chuỗi.

ba loại kết thúc dòng được thấy trong tệp văn bản: '\ n' là kết thúc thông thường trên máy Unix, '\ r' (tôi nghĩ) được sử dụng trên hệ điều hành Mac cũ và Windows sử dụng một cặp, '\ r' theo dõi bởi '\ n'.

Vấn đề là để getlinelại '\ r' ở cuối chuỗi.

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

Chỉnh sửa Cảm ơn Neil đã chỉ ra rằng đó f.good()không phải là điều tôi muốn. !f.fail()là những gì tôi muốn.

Tôi có thể tự xóa nó theo cách thủ công (xem phần chỉnh sửa của câu hỏi này), điều này rất dễ dàng đối với các tệp văn bản Windows. Nhưng tôi lo lắng rằng ai đó sẽ cấp dữ liệu trong tệp chỉ chứa '\ r'. Trong trường hợp đó, tôi cho rằng getline sẽ tiêu thụ toàn bộ tệp, vì nghĩ rằng đó là một dòng duy nhất!

.. và điều đó thậm chí không xem xét đến Unicode :-)

.. có lẽ Boost có một cách hay để sử dụng từng dòng một từ bất kỳ loại tệp văn bản nào?

Chỉnh sửa Tôi đang sử dụng cái này, để xử lý các tệp Windows, nhưng tôi vẫn cảm thấy mình không nên làm như vậy! Và điều này sẽ không phân nhánh đối với các tệp chỉ dành cho '\ r'.

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

2
\ n có nghĩa là dòng mới theo bất kỳ cách nào được trình bày trong hệ điều hành hiện tại. Thư viện chăm sóc điều đó. Nhưng để điều đó hoạt động, một chương trình được biên dịch trong windows phải đọc các tệp văn bản từ windows, một chương trình được biên dịch trong unix, các tệp văn bản từ unix, v.v.
George Kastrinis

1
@George, mặc dù tôi đang biên dịch trên máy Linux, đôi khi tôi đang sử dụng các tệp văn bản có nguồn gốc từ máy Windows. Tôi có thể phát hành phần mềm của mình (một công cụ nhỏ để phân tích mạng) và tôi muốn có thể nói với người dùng rằng họ có thể cung cấp tệp văn bản (giống ASCII) trong hầu hết mọi thời điểm.
Aaron McDaid


1
Lưu ý rằng nếu (f.good ()) không thực hiện những gì bạn có vẻ nghĩ.

1
@JonathanMee: Nó có thể là như thế này . Có lẽ.
Các cuộc đua ánh sáng ở Orbit vào

Câu trả lời:


111

Như Neil đã chỉ ra, "thời gian chạy C ++ nên xử lý chính xác bất kỳ quy ước kết thúc dòng nào dành cho nền tảng cụ thể của bạn."

Tuy nhiên, mọi người di chuyển các tệp văn bản giữa các nền tảng khác nhau, vì vậy điều đó là chưa đủ. Đây là một hàm xử lý cả ba phần cuối dòng ("\ r", "\ n" và "\ 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;
        }
    }
}

Và đây là một chương trình thử nghiệm:

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: Tôi đã cập nhật mã sau gợi ý Bo Rights stackoverflow.com/questions/9188126/… và chạy một số thử nghiệm. Mọi thứ giờ hoạt động như bình thường.
Johan Råde 27/12/12

1
@Thomas Weller: Hàm tạo và hàm hủy của nhóm giám sát được thực thi. Chúng thực hiện những việc như đồng bộ hóa luồng, bỏ qua khoảng trắng và cập nhật trạng thái luồng.
Johan Råde

1
Trong trường hợp EOF, mục đích của việc kiểm tra ttrống trước khi đặt eofbit là gì. Không nên đặt bit đó bất kể các ký tự khác đã được đọc?
Yay295

1
Yay295: Cờ eof nên được đặt, không phải khi bạn đến cuối dòng cuối cùng, mà khi bạn cố gắng đọc quá dòng cuối cùng. Việc kiểm tra đảm bảo rằng điều này xảy ra khi dòng cuối cùng không có EOL. (Hãy thử xóa kiểm tra, sau đó chạy chương trình kiểm tra trên tệp văn bản nơi dòng cuối cùng không có EOL và bạn sẽ thấy.)
Johan Råde

3
Điều này cũng đọc dòng cuối cùng trống, không phải là hành vi std::get_linemà bỏ qua dòng cuối cùng trống. Tôi sử dụng đoạn mã sau trong trường hợp eof để bắt chước các std::get_linehành vi:is.setstate(std::ios::eofbit); if (t.empty()) is.setstate(std::ios::badbit); return is;
Patrick Roocks

11

Thời gian chạy C ++ phải xử lý chính xác bất kỳ quy ước endline nào dành cho nền tảng cụ thể của bạn. Cụ thể, mã này sẽ hoạt động trên tất cả các nền tảng:

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

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

Tất nhiên, nếu bạn đang xử lý các tệp từ một nền tảng khác, tất cả các cược sẽ tắt.

Vì hai nền tảng phổ biến nhất (Linux và Windows) đều kết thúc dòng bằng ký tự dòng mới, với Windows đứng trước nó bằng ký tự xuống dòng, bạn có thể kiểm tra ký tự cuối cùng của linechuỗi trong đoạn mã trên để xem có phải không \rvà nếu có loại bỏ nó trước khi thực hiện xử lý dành riêng cho ứng dụng của bạn.

Ví dụ: bạn có thể cung cấp cho mình một hàm kiểu getline trông giống như thế này (không được kiểm tra, sử dụng chỉ mục, substr, v.v. chỉ cho mục đích sư phạm):

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
Câu hỏi là về cách xử lý các tệp từ một nền tảng khác.
Các cuộc đua ánh sáng trong quỹ đạo

4
@Neil, câu trả lời này vẫn chưa đủ. Nếu tôi chỉ muốn xử lý CRLF, tôi đã không đến với StackOverflow. Thách thức thực sự là xử lý các tệp chỉ có '\ r'. Ngày nay chúng khá hiếm, giờ MacOS đã tiến gần hơn đến Unix, nhưng tôi không muốn cho rằng chúng sẽ không bao giờ được đưa vào phần mềm của tôi.
Aaron McDaid

1
@Aaron tốt, nếu bạn muốn có thể xử lý BẤT CỨ ĐIỀU GÌ, bạn phải viết mã của riêng bạn để làm điều đó.

4
Tôi đã nói rõ trong câu hỏi của mình ngay từ đầu rằng rất dễ giải quyết vấn đề này, ngụ ý rằng tôi sẵn sàng và có thể làm như vậy. Tôi hỏi về điều này bởi vì nó có vẻ là một câu hỏi phổ biến và có rất nhiều định dạng tệp văn bản. Tôi giả định / hy vọng rằng ủy ban tiêu chuẩn C ++ đã xây dựng điều này. Đây là câu hỏi của tôi.
Aaron McDaid

1
@Neil, tôi nghĩ có một vấn đề khác mà tôi / chúng tôi đã quên. Nhưng trước tiên, tôi chấp nhận rằng việc xác định một số ít định dạng được hỗ trợ là thực tế đối với tôi. Do đó, tôi muốn mã sẽ biên dịch trên Windows và Linux và sẽ hoạt động với một trong hai định dạng. Của bạn safegetlinelà một phần quan trọng của một giải pháp. Nhưng nếu chương trình này đang được biên dịch trên Windows, thì tôi có cần mở tệp ở định dạng nhị phân không? Các trình biên dịch của Windows (ở chế độ văn bản) có cho phép '\ n' hoạt động giống như '\ r' '\ n' không? ifstream f("f.txt", ios_base :: binary | ios_base::in );
Aaron McDaid

8

Bạn đang đọc tệp ở BINARY hay ở chế độ TEXT ? Trong chế độ TEXT , cặp ký tự xuống dòng / nguồn cấp dữ liệu dòng, CRLF , được hiểu là TEXT cuối dòng hoặc ký tự cuối dòng, nhưng trong BINARY bạn chỉ tìm nạp MỘT byte tại một thời điểm, có nghĩa là một trong hai ký tự PHẢIđược bỏ qua và để lại trong bộ đệm để được tìm nạp như một byte khác! Vận chuyển trở lại có nghĩa là, trong máy đánh chữ, xe máy đánh chữ, nơi đặt cánh tay in, đã đạt đến mép bên phải của giấy và được đưa trở lại mép bên trái. Đây là một mô hình rất cơ học, của máy đánh chữ cơ học. Sau đó, bộ nạp dòng có nghĩa là cuộn giấy được xoay lên một chút để giấy ở vị trí để bắt đầu một dòng nhập khác. Như tôi nhớ, một trong những chữ số thấp trong ASCII có nghĩa là di chuyển sang phải một ký tự mà không cần nhập, ký tự chết và tất nhiên \ b có nghĩa là backspace: di chuyển ô tô trở lại một ký tự. Bằng cách đó, bạn có thể thêm các hiệu ứng đặc biệt, chẳng hạn như gạch dưới (gõ gạch dưới), gạch ngang (gõ trừ), gần đúng các dấu khác nhau, hủy bỏ (gõ X) mà không cần bàn phím mở rộng, chỉ bằng cách điều chỉnh vị trí của ô tô dọc theo dòng trước khi vào nguồn cấp dòng. Vì vậy, bạn có thể sử dụng điện áp ASCII có kích thước byte để tự động điều khiển máy đánh chữ mà không cần máy tính ở giữa. Khi máy đánh chữ tự động được giới thiệu,TỰ ĐỘNG có nghĩa là khi bạn đến mép giấy xa nhất, ô tô được trả về bên trái áp dụng nguồn cấp dòng, tức là ô tô được giả định sẽ tự động quay lại khi cuộn giấy di chuyển lên! Vì vậy, bạn không cần cả hai ký tự điều khiển, chỉ một, \ n, dòng mới hoặc nguồn cấp dữ liệu dòng.

Điều này không liên quan gì đến lập trình nhưng ASCII cũ hơn và HEY! có vẻ như một số người đã không suy nghĩ khi họ bắt đầu làm những việc bằng văn bản! Nền tảng UNIX giả định một máy đánh máy tự động chạy điện; mô hình Windows hoàn thiện hơn và cho phép điều khiển các máy cơ học, mặc dù một số ký tự điều khiển ngày càng trở nên ít hữu ích hơn trong máy tính, như ký tự chuông, 0x07 nếu tôi nhớ rõ ... Một số đoạn văn bản bị quên ban đầu phải được ghi lại bằng các ký tự điều khiển cho máy đánh chữ điều khiển bằng điện và nó duy trì mô hình ...

Trên thực tế, biến thể chính xác sẽ là chỉ bao gồm \ r, nguồn cấp dữ liệu dòng, dấu xuống dòng là không cần thiết, nghĩa là tự động, do đó:

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

sẽ là cách chính xác nhất để xử lý tất cả các loại tệp. Tuy nhiên, lưu ý rằng \ n trong chế độ TEXT thực tế là cặp byte 0x0d 0x0a, nhưng 0x0d CHỈ \ r: \ n bao gồm \ r ở chế độ TEXT nhưng không bao gồm trong BINARY , vì vậy \ n và \ r \ n là tương đương ... hoặc nên là. Đây thực sự là một sự nhầm lẫn rất cơ bản trong ngành, quán tính điển hình của ngành, như quy ước là nói về CRLF, trong TẤT CẢ các nền tảng, sau đó rơi vào các cách hiểu nhị phân khác nhau. Nói một cách chính xác, các tệp CHỈ bao gồm 0x0d (ký tự xuống dòng) là \ n (CRLF hoặc nguồn cấp dữ liệu dòng), không đúng định dạng trong TEXT(máy đánh chữ: chỉ cần trả xe và gạch ngang mọi thứ ...), và là định dạng nhị phân không định hướng dòng (hoặc \ r hoặc \ r \ n nghĩa là định hướng dòng) nên bạn không được phép đọc dưới dạng văn bản! Mã có thể bị lỗi với một số thông báo của người dùng. Điều này không chỉ phụ thuộc vào hệ điều hành mà còn phụ thuộc vào việc triển khai thư viện C, làm tăng thêm sự nhầm lẫn và các biến thể có thể xảy ra ... (đặc biệt đối với các lớp dịch UNICODE trong suốt thêm một điểm khớp nối khác cho các biến thể khó hiểu).

Vấn đề với đoạn mã trước (máy đánh chữ cơ học) là nó rất kém hiệu quả nếu không có \ n ký tự nào sau \ r (văn bản máy đánh chữ tự động). Sau đó, nó cũng giả định chế độ BINARY trong đó thư viện C buộc phải bỏ qua các diễn giải văn bản (ngôn ngữ) và cho đi các byte tuyệt đối. Không nên có sự khác biệt về các ký tự văn bản thực giữa cả hai chế độ, chỉ ở các ký tự điều khiển, vì vậy nói chung đọc BINARY tốt hơn chế độ TEXT . Giải pháp này hiệu quả đối với BINARYchế độ các tệp văn bản điển hình của Hệ điều hành Windows độc lập với các biến thể thư viện C và không hiệu quả đối với các định dạng văn bản nền tảng khác (bao gồm cả bản dịch web thành văn bản). Nếu bạn quan tâm đến hiệu quả, cách thực hiện là sử dụng con trỏ hàm, thực hiện kiểm tra các điều khiển dòng \ r so với \ r \ n theo cách bạn muốn, sau đó chọn mã người dùng getline tốt nhất vào con trỏ và gọi nó từ nó.

Tình cờ tôi nhớ rằng tôi cũng tìm thấy một số tệp văn bản \ r \ r \ n ... dịch thành văn bản dòng đôi giống như yêu cầu của một số người tiêu dùng văn bản in.


+1 cho "ios :: binary" - đôi khi, bạn thực sự muốn đọc tệp như nó vốn có (ví dụ: để tính toán tổng kiểm tra, v.v.) mà thời gian chạy không thay đổi kết thúc dòng.
Matthias

2

Một giải pháp trước tiên sẽ là tìm kiếm và thay thế tất cả các phần cuối dòng thành '\ n' - giống như Git làm theo mặc định.


1

Ngoài việc viết trình xử lý tùy chỉnh của riêng bạn hoặc sử dụng thư viện bên ngoài, bạn không gặp may. Điều dễ dàng nhất để làm là kiểm tra để đảm bảo line[line.length() - 1]không phải là '\ r'. Trên Linux, điều này là thừa vì hầu hết các dòng sẽ kết thúc bằng '\ n', có nghĩa là bạn sẽ mất một chút thời gian nếu điều này lặp lại. Trên Windows, điều này cũng không cần thiết. Tuy nhiên, còn các tệp Mac cổ điển kết thúc bằng '\ r' thì sao? std :: getline sẽ không hoạt động với những tệp đó trên Linux hoặc Windows vì '\ n' và '\ r' '\ n' đều kết thúc bằng '\ n', loại bỏ sự cần thiết phải kiểm tra '\ r'. Rõ ràng là một tác vụ như vậy hoạt động với các tệp đó sẽ không hoạt động tốt. Tất nhiên, sau đó tồn tại rất nhiều hệ thống EBCDIC, điều mà hầu hết các thư viện sẽ không dám giải quyết.

Kiểm tra '\ r' có lẽ là giải pháp tốt nhất cho vấn đề của bạn. Đọc ở chế độ nhị phân sẽ cho phép bạn kiểm tra tất cả ba phần cuối dòng chung ('\ r', '\ r \ n' và '\ n'). Nếu bạn chỉ quan tâm đến Linux và Windows vì phần cuối dòng Mac kiểu cũ sẽ không còn tồn tại lâu hơn nữa, hãy chỉ kiểm tra '\ n' và xóa ký tự ở cuối '\ r'.


0

Nếu biết mỗi dòng có bao nhiêu mục / số, người ta có thể đọc một dòng với 4 số, ví dụ:

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

Điều này cũng hoạt động với các kết thúc dòng khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.