如何仅使用低级 I/O 命令将 CSV 数据导入 Matlab

How do I import CSV data in to Matlab only using low level I/O commands

我真的很想弄清楚如何将包含 9 列和大约 400 行数据的 CSV 数据导入到 Matlab 工作区的 table 中。如果我被允许使用 Matlab 必须提供的 built-in 工具箱,这将很容易,但相反,我需要尝试仅使用 fscanf、fopen 等命令来完成任务。数据本身是混合的每列的格式。小数、浮点数、字符串等。我也被允许使用 CSVread,但我还没有设法让它工作,因为据我了解,CSVread 仅适用于数值。

这是我的代码:

>> filename = 'datatext.csv'; %The file in CSV format
>> fid = fopen(filename); 
>> headers = fgetl(fid); %using fgetl to remove the headers and put them in a variable

我已经使用 fgetl 跳过文件的 headers 行并将它们添加到它们自己的变量中,但是我不确定从这一点开始创建 table。基本上我想在 Matlab 的工作区中实现 400 行乘 9 列 table。

下面是文本文件的几行示例:

18,8,318,150,3436,11,70,1,sampletext
16,8,304,150,3433,12,70,1,sampletext2

我假设我将不得不使用某些单元格的 built-in 转换函数,这是我被允许的。为了获得正确的帮助,我可能错过了一些重要信息,但是任何人提供的任何帮助都将不胜感激。谢谢。

使用fgetl()重复检索文本文件行并使用split()

分隔行内容

不确定是否允许在读取文本文件内容后使用 split() 函数,但这里有一个使用 fgetl() 通过循环逐行抓取文本文件内容的实现。在使用 split() 和第二个参数检索所有行后,分隔符设置为逗号 , 允许将内容拆分为单元格。第一个 for 循环逐行检索文本文件的内容并将其存储在名为 Lines 的字符串中。第二个 for 循环通过分隔符 , 拆分存储在 Lines 中的字符串,允许将单元格存储在另一个字符串数组中,如下所示,内容分开。这里 -1 表示错误输入 retrieved/when 已到达文件末尾。

Sample.txt

18,8,318,150,3436,11,70,1,sampletext
16,8,304,150,3433,12,70,1,sampletext2

脚本:

Text = "Start";

% open file (read only)
fileID = fopen('Sample.txt', 'r');
%Running for loop till end of file termination "-1"%
Line_Index = 1;
while(Text ~= "-1")
    % read line/row
    Text = string(fgetl(fileID));
    % stopping criterion
    if (Text ~= "-1")
        Lines(Line_Index,1) = Text;
    end
    % update row index
    Line_Index = Line_Index + 1;
end
% close file
fclose(fileID);

[Number_Of_Lines,~] = size(Lines);
Output_Array = strings(Number_Of_Lines,9);


for Row_Index = 1: Number_Of_Lines
    Line = split(Lines(Row_Index,:),',');
    Line = Line';
    Output_Array(Row_Index,:) = string(Line);
end

运行 使用 MATLAB R2019b

虽然@MichaelTr7 的回答非常好,但我想建议一个更详细的答案,包括转换为类型并最终返回 table。 请注意,它还包括 预分配 变量。因为 MATLAB 将变量存储在 RAM 中的一致块中,所以最好事先告诉它你的变量有多大。 (MATLAB 实际上抱怨似乎在循环中增长的变量...)

该解决方案也建立在 fgetl and (later) split + cell2table 的基础上(这绝对不再是低级函数,但在您的情况下这可能没问题,因为它不再处理读取)

% USER-INPUT
FileName = 'Sample.csv';
strType = "%l,%f,%d,%f,%f,%f,%f,%f,%s";
delimiter = ",";



% allocate strings
Data = strings(100,1);

% open file (read only)
fileID = fopen(FileName, 'r');
%Running for loop till end of file termination "-1"%
Line_idx = 1;
while true
    % read line/row
    Line_text = string(fgetl(fileID));
    % stopping criterion
    if (Line_text == "-1")
        break
    end
    
    Data(Line_idx,1) = Line_text;
    % update row index
    Line_idx = Line_idx + 1;
    
    % extend allocation
    if Line_idx > size(Data,1)
        Data = [Data;strings(100,1)]; %#ok<AGROW>
    end
end
% close file
fclose(fileID);
% crop variable/rows
Data = Data(1:Line_idx-1,:);



strType_splt = split(strType,    delimiter);
Num_strType = length( strType_splt );
Num_Data    = length( split(Data(1,:),  delimiter) );

% check number of conversion types
assert( Num_strType == Num_Data, strcat("Conversion format 'strType' has not the same number of values as the file ",FileName,"."))



% allocate cell
C = cell(size(Data,1),Num_strType);

% loop over rows & columns + convert the elements
for r = 1:size(Data,1) % loop over rows
    line = Data(r);
    % split into individual strings
    line_splt = split(line,  delimiter);
    
    for c = 1:Num_strType % loop over columns
        element_str = line_splt(c);
        type = strType_splt(c);
        C{r,c} = convertStr( type, element_str );
    end
end
% create table
T = cell2table(C);




function element = convertStr(type,str)

    switch type
        case "%f" % float = single | convert to double and cast to single
            element = single(str2double(str));
        case "%l" % long float
            element = str2double(str);
        case "%d" % convert to double and cast to integer
            element = int32(str2double(str));
        case "%s"
            element = string(str);
        case "%D" % non-standard: datetime
            element = datetime(str);
        otherwise
            element = {str};
    end
end

这假定一个文件 Sample.csv,例如内容如下:

18,8,318,150,3436,11,70,1,sampletext
16,8,304,150,3433,12,70,1,sampletext2

读取文件的最低级别函数是, (如 Import Text Data Files with Low-Level I/O 中所述):


在你的情况下,输入文件是 ascii,而不是二进制文件,所以我们可以立即删除最后一个选项 (fread)。

剩下 fgetl/fgets(用于逐行读取文件然后解析每一行)和 fscanf.

你已经有了两个使用逐行方法的答案,所以我不会详细说明这个,而是向你展示如何使用 fscanf(因为你的数据是 suitable,它们确实以 格式化模式 ).

组织

使用fscanf的好处是,如果你使用正确的formatSpec参数,函数将能够一次读取整个文件,而不是逐行迭代。 对于文件中的所有 numeric 数据都是如此。我们将不得不对您最后一列中的文本元素进行第二次传递。

定义格式说明符:

首先让我们定义您的格式规范。我们将为每个通道使用不同的格式。第一遍将读取所有数字数据,但将 跳过 文本字段,而第二遍将执行相反的操作,忽略所有数字数据并仅读取文本字段。 '*' 字符在定义格式说明符时非常有用:

DataFormatSpec = repmat('%d,',1,8) ;
DataFormatSpec = [DataFormatSpec '%*s'] ; % yield:  '%d,%d,%d,%d,%d,%d,%d,%d,%*s'

TextFormatSpec = repmat('%*d,',1,8) ;
TextFormatSpec = [TextFormatSpec '%s'] ; % yield:  '%*d,%*d,%*d,%*d,%*d,%*d,%*d,%*d,%s'

我对所有列都使用了 %d,因为在您的示例数据中我没有看到您提到的各种数字类型。如果数据需要,您可以轻松地将它替换为 %f,并且您可以在同一个说明符中毫无问题地混合这两种类型(只要它们都是数字)。只需使用对每一列有意义的内容。

有了这些,让我们开始处理文件。数据在 header 行之后的格式化模式中,因此我们首先需要在调用 fscanf 之前通过 header 行。我们会像您一样这样做:

%% Open file and retrieve header line
fid = fopen( filein,'r') ;
hdr = fgetl(fid) ;              % Retrieve the header line
DataStartIndex = ftell(fid) ;   % save the starting index of data section

ftell的调用允许我们保存文件指针的位置。在我们读取 header 之后,指针正好位于数据段的开头。我们保存它是为了能够 倒回 文件指针指向第二次读取的同一点。

正在读取数值数据:

fscanf 只需一次简单的调用即可非常快速地完成此操作:

%% First pass, read the "numeric values"
dataArray = fscanf( fid , DataFormatSpec , [8 Inf]).' ;

注意行尾的转置运算符 .'。这是因为 fscanf 填充它按列主要顺序读取的值,但文本文件中的数据是按行主要顺序读取的。最后的转置操作只是让输出数组与文本文件中的维度相同。

现在 dataArray 包含您所有的数字数据:

>> dataArray
dataArray =
          18           8         318         150        3436          11          70           1
          16           8         304         150        3433          12          70           1

读取文本数据:

这是它变得稍微复杂一些。 fscanf 自动将文本字符转换为其 ascii 值。很容易将其转换回实际字符​​(使用函数 char())。最大的障碍是,如果我们一次读取所有文本字段,它们将全部显示为一连串数字,但无法知道每个字符串在哪里结束以及下一个字符串从哪里开始。为了克服这个问题,我们将逐行阅读,但仍然使用 fscanf:

%% Second pass, read the "text" values
fseek(fid,DataStartIndex,'bof') ;   % Rewind file pointer to the start of data section
nRows = size(dataArray,1) ;         % How many lines we'll need to read
textArray = cell(nRows,1) ;         % pre allocate a cell array to receive the text column elements

for iline=1:nRows
    textArray{iline,1} = char( fscanf(fid,TextFormatSpec,1).' ) ;
end

fclose(fid) ;   % Close file

再次注意转置运算符 .' 的使用,以及 char() 的使用。现在 textArray 是一个包含所有文本字段的元胞数组:

>> textArray
textArray = 
    'sampletext'
    'sampletext2'

重组数据集:

就个人而言,我会将这两个数组分开,因为它们是每种数据类型的最优化容器(double 数组用于数字数据,cell 数组用于字符串数组) .但是,如果您需要将它们重新组合到一个数据结构中,您可以使用元胞数组:

%% Optional, merge data into cell array
FullArray = [num2cell(dataArray) textArray]
FullArray = 
    [18]    [8]    [318]    [150]    [3436]    [11]    [70]    [1]    'sampletext' 
    [16]    [8]    [304]    [150]    [3433]    [12]    [70]    [1]    'sampletext2'

或者您可以使用 table:

%% Optional, merge data into a table
T = array2table(dataArray) ;
T.text = textArray ;
T.Properties.VariableNames = [cellstr(reshape(sprintf('v%d',1:8),2,[]).') ; {'text'}] ;

给出:

T = 
    v1    v2    v3     v4      v5     v6    v7    v8        text     
    __    __    ___    ___    ____    __    __    __    _____________
    18    8     318    150    3436    11    70    1     'sampletext' 
    16    8     304    150    3433    12    70    1     'sampletext2'

显然,如果您选择 table 版本,请使用您从 header 解析的变量名称,而不是我在本例中使用的自动生成的名称。