1. 程式人生 > >Qt下csv的讀寫封裝

Qt下csv的讀寫封裝

概述

csv檔案作為簡單的格式化文字檔案,隨著資料探勘和python的普及突然就又火起來了,工作中發現許多資料互動由原來的xml又改為通過csv檔案進行互動,csv檔案有個最大的缺點是單個單元格里不能出現換行,如果是單純的資料互動,csv的確是最簡單的格式化方式。
csv把每個單元資料用逗號隔開,但某些情況下需要注意的是,遇到一個單元內容有包含引號"和逗號,時是需要轉義的。否則會造成格式混亂,本文的目的是封裝一個簡單好用的csv流式輸入輸出,類名SACsvStream,實現如下效果輸出csv檔案::

QFile file;
……//open file
double d = 0.111;
float
f = 0.343; int i = 1212; QString str = "這是一個文字"; SACsvStream csv(&file); csv << d << f << i << str << endl;

實現輸出:

0.111,0.343,1212,這是一個文字

關鍵點

csv的解析

其實匯出csv和匯出txt唯一的區別就是對csv的一些特殊情況需要轉義操作,如遇到逗號遇到引號的情況
在csv中,遇到逗號需要把整個內容用引號擴起來
遇到引號,使用兩個引號代表一個引號
這是一個"複合文字"文字:1,2,4,5,czyt1988

,在csv中需要顯示為"這是一個""複合文字""文字:1,2,4,5,czyt1988"

操作符<<對endl的處理

為了方便使用,過載了operator <<操作符,這裡還需要注意一點,就是處理endl
operator <<操作符中,endl其實是一個函式指標,為了能讓operator <<操作符支援endl需要指定一個函式指標的operator <<操作符過載
例如:類SACsvStream 為了支援operator <<操作符識別endl,需要定義一個函式指標:

typedef SACsvStream & (*SACsvWriterFunction)(SACsvStream &);

這個然後需要讓operator <<操作符處理此函式指標:

SACsvStream &operator<<(SACsvStream &s, SACsvWriterFunction f)
{
    return (*f)(s);
}

這樣就可以寫類似SACsvStream &endl(SACsvStream &s);這樣的函式,實現需要的功能

SACsvStream &endl(SACsvStream &s)
{
    //此處進行換行
    return s;
}

這樣你不僅可以實現類似endl,你可以實現任何名稱的過載
如:

SACsvStream &wo(SACsvStream &s)
{
    s << "我";
    return s;
}
SACsvStream &zui(SACsvStream &s)
{
    s << "最";
    return s;
}
SACsvStream &shuai(SACsvStream &s)
{
    s << "帥";
    return s;
}

然後:

SACsvStream  csv(&file);
csv << wo << zui << shuai;

輸出結果大家自己默唸

實現

標頭檔案:

#ifndef QCSVSTREAM_H
#define QCSVSTREAM_H
class QTextStream;
class QFile;
class SACsvStreamPrivate;
#include <QString>
///
/// \brief 寫csv檔案類支援
/// \author czy
/// \date 2016-08-10
///
class SACsvStream
{
public:
    SACsvStream(QTextStream* txt);
    SACsvStream(QFile* txt);
    virtual ~SACsvStream();
    //轉換為標識csv字元
    static QString toCsvString(const QString& rawStr);
    //把一行要用逗號分隔的字串轉換為一行標準csv字串
    static QString toCsvStringLine(const QStringList& sectionLine);
    //解析一行csv字元
    static QStringList fromCsvLine(const QString &lineStr);
    SACsvStream & operator << (const QString &str);
    SACsvStream & operator << (int d);
    SACsvStream & operator << (double d);
    SACsvStream & operator << (float d);
    //另起一行
    void newLine();
    //獲取輸入輸出流
    QTextStream* stream() const;
    //讀取並解析一行csv字串
    QStringList readCsvLine();
    //判斷是否到檔案末端
    bool atEnd() const;
private:
    static int advquoted(const QString &s, QString &fld, int i);
    static int advplain(const QString &s, QString &fld, int i);
private:
    QScopedPointer<SACsvStreamPrivate> d_p;
};
typedef SACsvStream & (*SACsvWriterFunction)(SACsvStream &);
inline SACsvStream &operator<<(SACsvStream &s, SACsvWriterFunction f)
{
    return (*f)(s);
}
SACsvStream &endl(SACsvStream &s);
#endif // QCSVSTREAM_H

實現檔案:

#include "SACsvStream.h"
#include <QTextStream>
#include <QFile>
class SACsvStreamPrivate
{
    SACsvStream* Parent;
    QTextStream* m_txt;
    QScopedPointer<QTextStream> m_pfile;
    bool m_isStartLine;
public:
    SACsvStreamPrivate(SACsvStream* p):Parent(p)
      ,m_txt(nullptr)
      ,m_pfile(nullptr)
      ,m_isStartLine(true)
    {
    }
    void setTextStream(QTextStream* st)
    {
        m_txt = st;
    }
    QTextStream* stream()
    {
        return m_txt;
    }
    QTextStream& streamRef()
    {
        return (*m_txt);
    }
    bool isStartLine() const
    {
        return m_isStartLine;
    }
    void setStartLine(bool on)
    {
        m_isStartLine = on;
    }
    void setFile(QFile *txt)
    {
        m_pfile.reset(new QTextStream(txt));
        m_txt = m_pfile.data();
    }
    QTextStream& formatTextStream()
    {
        if(!isStartLine())
        {
            streamRef()<<',';
        }
        else
        {
            setStartLine(false);
        }
        return streamRef();
    }
};
SACsvStream::SACsvStream(QTextStream* txt)
    :d_p(new SACsvStreamPrivate(this))
{
    d_p->setTextStream(txt);
}
SACsvStream::SACsvStream(QFile *txt)
    :d_p(new SACsvStreamPrivate(this))
{
    d_p->setFile(txt);
}
SACsvStream::~SACsvStream()
{
}
///
/// \brief 把字串裝換為標準csv一個單元得字串,對應字串原有的逗號會進行裝換
///
/// csv的原則是:
///
/// - 如果字串有逗號,把整個字串前後用引號括起來
/// - 如果字串有引號",引號要用兩個引號表示轉義""
/// \param rawStr 原有資料
/// \return 標準的csv單元字串
///
QString SACsvStream::toCsvString(const QString &rawStr)
{
    //首先查詢有沒有引號,如果有,則把引號替換為兩個引號
    QString res = rawStr;
    res.replace("\"","\"\"");
    if(res.contains(','))
    {
        //如果有逗號,在前後把整個句子用引號包含
        res = ('\"' + res + '\"');
    }
    return res;
}
///
/// \brief 把一行要用逗號分隔的字串轉換為一行標準csv字串
/// \param sectionLine 如:xxx,xxxx,xxxxx 傳入{'xxx','xxxx','xxxxx'}
/// \return 標準的csv字串不帶換行符
///
QString SACsvStream::toCsvStringLine(const QStringList &sectionLine)
{
    QString res;
    int size = sectionLine.size();
    for(int i=0;i<size;++i)
    {
        if(0 == i)
        {
            res += SACsvStream::toCsvString(sectionLine[i]);
        }
        else
        {
            res += ("," + SACsvStream::toCsvString(sectionLine[i]));
        }
    }
    return res;
}
///
/// \brief 把一句csv格式的內容解析
/// \param lineStr
/// \return
///
QStringList SACsvStream::fromCsvLine(const QString &lineStr)
{
    if(lineStr.isEmpty())
    {
        return QStringList();
    }
    QStringList res;
    QString str;
    int i, j=0;
    int col = 0;
    i = 0;
    do {
        if (i < lineStr.length() && lineStr[i] == '\"')
            j = advquoted(lineStr, str, ++i); // skip quote
        else
            j = advplain(lineStr, str, i);
        res.push_back (str);
        ++col;
        i = j + 1;
    } while (j < lineStr.length());
    return res;
}
///
/// \brief 寫csv檔案內容,字元會自動轉義為csv檔案支援的字串,不需要轉義
///
/// 例如csv檔案:
/// \par
/// 12,txt,23,34
/// 45,num,56,56
/// 寫入的方法為:
/// \code
/// .....
/// QCsvWriter csv(&textStream);
/// csv<<"12"<<"txt"<<"23";
/// csv.endLine("34");
/// csv<<"45"<<"num"<<"56";
/// csv.endLine("56");
/// \endcode
///
/// \param str 需要寫入的csv檔案一個單元得字串
/// \return
///
SACsvStream &SACsvStream::operator <<(const QString &str)
{
    d_p->formatTextStream()<<toCsvString(str);
    return *this;
}
SACsvStream &SACsvStream::operator <<(int d)
{
    d_p->formatTextStream()<<d;
    return *this;
}
SACsvStream &SACsvStream::operator <<(double d)
{
    d_p->formatTextStream()<<d;
    return *this;
}
SACsvStream &SACsvStream::operator <<(float d)
{
    d_p->formatTextStream()<<d;
    return *this;
}
///
/// \brief 換行
///
void SACsvStream::newLine()
{
    d_p->setStartLine(true);
    d_p->streamRef()<<endl;
}
///
/// \brief 獲取輸入輸出流
/// \return
///
QTextStream *SACsvStream::stream() const
{
    return d_p->stream();
}
///
/// \brief 讀取一行csv檔案
/// \return
///
QStringList SACsvStream::readCsvLine()
{
    return fromCsvLine(stream()->readLine());
}
bool SACsvStream::atEnd() const
{
    return stream()->atEnd();
}
int SACsvStream::advquoted(const QString &s, QString &fld, int i)
{
    int j;
    fld = "";
    for (j = i; j < s.length()-1; j++)
    {
        if (s[j] == '\"' && s[++j] != '\"')
        {
            int k = s.indexOf (',', j);
            if (k < 0 ) // 沒找到逗號
                k = s.length();
            for (k -= j; k-- > 0; )
                fld += s[j++];
            break;
        }
        fld += s[j];
    }
    return j;
}
int SACsvStream::advplain(const QString &s, QString &fld, int i)
{
    int j;
    j = s.indexOf(',', i); // 查詢,
    if (j < 0) // 沒找到
        j = s.length();
    fld = s.mid (i,j-i);//提取內容
    return j;
}
///
/// \brief endl
/// \param s
/// \return
///
SACsvStream &endl(SACsvStream &s)
{
    s.newLine();
    return s;
}

示例

寫入測試:

    QFile file("SACsvStreamTest.csv");
    if(!file.open(QIODevice::WriteOnly))
    {
        return;
    }
    double d = 0.111;
    float f = 0.343;
    int i = 1212;
    QString str1 = QStringLiteral("這是一個文字");
    QString str2 = QStringLiteral("這是一個帶,號文字");
    QString str3 = QStringLiteral("這是一個帶\"文字");
    QString str4 = QStringLiteral("這是一個\"複合文字\"文字:1,2,4,5,czyt1988");
    SACsvStream csv(&file);
    csv << d << f << i << str1 << str2 << str3 << str4 << endl;
    csv << str4 << str3 << str2 << str1 << i << f << d << endl;
    file.close();

結果:

0.111,0.343,1212,這是一個文字,"這是一個帶,號文字",這是一個帶""文字,"這是一個""複合文字""文字:1,2,4,5,czyt1988"
"這是一個""複合文字""文字:1,2,4,5,czyt1988",這是一個帶""文字,"這是一個帶,號文字",這是一個文字,1212,0.343,0.111

讀取示例:

    QFile file("SACsvStreamTest.csv");
    if(!file.open(QIODevice::ReadOnly|QIODevice::Text))
    {
        return -1;
    }
    SACsvStream csv(&file);
    while(!csv.atEnd())
    {
        QStringList res = csv.readCsvLine();
        qDebug().noquote() << res;
    }

輸出:

(0.111, 0.343, 1212, 這是一個文字, 這是一個帶,號文字, 這是一個帶""文字, 這是一個"複合文字"文字:1,2,4,5,czyt1988, )
(這是一個"複合文字"文字:1,2,4,5,czyt1988, 這是一個帶""文字, 這是一個帶,號文字, 這是一個文字, 1212, 0.343, 0.111)

支援