Как импортировать данные CSV в Matlab только с использованием команд ввода-вывода низкого уровня

#matlab #csv #scanf #fopen #low-level-io

#matlab #csv #сканф #приоткрыть #низкоуровневый ввод-вывод

Вопрос:

Я действительно пытаюсь понять, как я могу импортировать данные CSV, которые содержат 9 столбцов и около 400 строк данных, в таблицу в рабочей области Matlab. Это было бы легко, если бы мне разрешили использовать встроенные наборы инструментов, которые предлагает Matlab, но вместо этого мне нужно попытаться выполнить задачу, используя только такие команды, как 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, чтобы пропустить строку заголовков файла и добавить их в свою собственную переменную, однако я не уверен, куда идти с этого момента при создании таблицы. По сути, я хочу получить таблицу размером 400 строк на 9 столбцов в рабочей области Matlab.

Вот пример нескольких строк того, как выглядит текстовый файл:

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

Я предполагаю, что мне придется использовать встроенные функции преобразования некоторых ячеек, что мне разрешено делать. Вероятно, я пропустил какую-то важную информацию, чтобы получить правильную помощь, но любая помощь, которую кто-либо может оказать, будет высоко оценена. Спасибо.

Комментарии:

1. readtable не требует никакого набора инструментов, но если вам нужно придерживаться только ввода-вывода низкого уровня, вы действительно можете использовать fscanf или textscan

2. Спасибо, забыл добавить, что textscan специально запрещен, однако я постараюсь работать с fscanf.

Ответ №1:

Функции самого низкого уровня для чтения файла (как описано в разделе Импорт текстовых файлов данных с помощью низкоуровневого ввода-вывода):


В вашем случае входной файл является ascii, а не двоичным, поэтому мы можем сразу отказаться от последнего варианта ( fread ).

У вас остается fgetl/fgets (используется для чтения файла построчно, а затем для анализа каждой строки) и fscanf .

У вас уже есть два ответа с использованием построчного подхода, поэтому я не буду подробно описывать этот, а скорее покажу вам, как вы можете использовать fscanf (поскольку ваши данные подходят, они действительно организованы в форматированный шаблон).

Преимущество использования fscanf заключается в том, что при условии, что вы используете правильный formatSpec параметр, функция сможет прочитать весь файл за один раз, вместо того, чтобы перебирать построчно. Это верно для всех числовых данных в вашем файле. Нам нужно будет выполнить второй проход для текстовых элементов в вашем последнем столбце.

Определение спецификаторов формата:

сначала давайте определим ваши спецификации формата. Мы будем использовать другой формат для каждого прохода. Первый проход считывает все числовые данные, но пропускает текстовые поля, в то время как второй проход делает обратное, игнорирует все числовые данные и читает только текстовые поля. '*' Символ действительно полезен для этого при определении спецификатора формата:

 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 , если этого требуют данные, и вы можете без проблем смешивать оба типа в одном и том же спецификаторе (если все они числовые). Просто используйте то, что имеет смысл для каждого столбца.

Вооружившись этим, давайте перейдем к файлу. Данные находятся в формате шаблона после заголовка, поэтому сначала нам нужно пройти строку заголовка перед вызовом fscanf . Мы сделаем это так, как вы это сделали:

 %% 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 позволяет нам сохранить положение указателя файла. После того, как мы прочитаем заголовок, указатель будет расположен прямо в начале раздела данных. Мы сохраняем его, чтобы иметь возможность перемотать указатель файла на ту же точку для второго прохода чтения.

Чтение числовых данных:

Это делается очень быстро с помощью 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
 

Обратите внимание еще раз на использование оператора transpose .' и использование 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'
 

Очевидно, что если вы выберете версию таблицы, используйте имена переменных, которые вы проанализировали из заголовка, вместо автоматически сгенерированного, который я использовал в этом примере.

Ответ №2:

fgetl() Многократное использование для извлечения строк текстового файла и разделения содержимого строк с помощью split()

Не уверен, разрешено ли использование split() функции после чтения содержимого текстового файла, но вот реализация, которая использует fgetl() для захвата содержимого текстового файла построчно с помощью цикла. После извлечения всех строк при использовании split() со вторым аргументом разделитель, установленный на запятую , , позволяет разделить содержимое на ячейки. Первый цикл for-loop извлекает содержимое текстового файла построчно и сохраняет его в вызываемой строке Lines . Второй цикл for разделяет строки, сохраненные в Lines разделителе , , позволяя сохранять ячейки в другом массиве строк, показанном ниже, с разделением содержимого. Здесь -1 указывается ложная запись, полученная / при достижении конца файла.

Содержимое массива

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

Комментарии:

1. Большое вам спасибо за оба ответа. Оба были чрезвычайно полезны, и я применил их к своей проблеме.

Ответ №3:

Хотя ответ @MichaelTr7 в полном порядке, я хотел бы предложить немного более подробный ответ, включая преобразование в типы и, в конечном итоге, возврат таблицы. Обратите внимание, что он также включает предварительное выделение переменных. Поскольку MATLAB хранит переменные в виде последовательных блоков в оперативной памяти, полезно заранее сообщить, насколько большой будет ваша переменная. (MATLAB на самом деле жалуется на переменные, которые, по-видимому, растут в цикле …)

Решение также основывается на fgetl и (позже) 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 amp; 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