|
|
Выполнение заданий на обработку файлов
Данная страница содержит подробное описание процесса решения
типового задания на обработку двоичных файлов с числовой информацией,
а также примеры решения заданий на обработку
строковых и текстовых файлов.
Двоичные файлы с числовой информацией: File48
Особенности выполнения заданий на обработку файлов рассмотрим на примере
задания File48.
File48°. Даны три файла целых чисел одинакового размера с именами SA,
SB, SC и
строка SD. Создать новый файл с именем SD, в котором чередовались бы
элементы исходных файлов с одним и тем же номером:
A1, B1, C1,
A2, B2, C2, ... .
Создание программы-заготовки и знакомство с заданием
Проект-заготовку для решения задания File48 можно создать с помощью
модуля PT4Load. При этом в редактор среды Visual Basic будет загружен
файл, содержащий процедуру Solve, в которую необходимо ввести решение задания.
В средах VB5-6 файл будет иметь имя File48.bas, а в средах VB.NET File48.vb.
Приведем содержимое этих файлов:
[VB5-6, файл File48.bas]
Option Explicit
Sub Solve()
Task "File48"
End Sub
[VB.NET, файл File48.vb]
Option Strict On
Imports System.Math
Imports PT4.VisualBasic
Module PT4Tasks
Sub Solve()
Task("File48")
End Sub
End Module
Приведем окно задачника, которое появится на экране после запуска программы.
В первой строке раздела исходных данных указаны имена трех исходных файлов
(SA, SB и SC) и одного
результирующего (SD). В трех последних строках раздела
исходных данных показано содержимое исходных файлов. Для отображения
содержимого каждого файла отводится по одной строке. Элементы файлов
отображаются бирюзовым цветом, чтобы подчеркнуть их отличие от обычных
исходных данных (желтого цвета) и комментариев (светло-серого цвета).
Поскольку размер файлов, как правило, превышает количество элементов,
которое может уместиться на одной экранной строке, предусмотрена возможность
прокрутки (листания) элементов файла с помощью мыши или клавиатуры.
Следует также заметить, что при каждом запуске программы с учебным
заданием исходные файлы создаются под новыми именами и заполняются новыми
данными, а исходные и результирующие файлы, созданные при предыдущем запуске
программы, удаляются с диска.
Вернемся к нашей программе, только что запущенной на выполнение. Так как в
ней не указаны операторы ввода-вывода, запуск программы считается
ознакомительным, проверка решения не производится, а в разделе результатов
отображается вкладка «Пример верного решения» с контрольными
данными (в нашем случае это числа, которые должны содержаться в результирующем
файле при правильном решении задания).
Ввод исходных данных
Добавим в процедуру Solve фрагмент, позволяющий ввести имена файлов и
открыть данные файлы. Поскольку мы собираемся работать с четырьмя файлами,
удобно предусмотреть массив для хранения номеров всех четырех файлов:
[VB5-6]
Sub Solve()
Task "File48"
Dim s As String, _
f(1 To 4) As Integer, _
a As Integer, i As Integer
For i = 1 To 3
GetS s
f(i) = FreeFile
Open s For Random As f(i) Len = Len(a)
Next
Close
End Sub
[VB.NET]
Sub Solve()
Task("File48")
Dim s As String, _
f(4), a, i As Integer
For i = 1 To 3
GetS(s)
f(i) = FreeFile()
FileOpen(f(i), s, _
OpenMode.Random, , , Len(a))
Next
FileClose()
End Sub
При работе с массивами с использованием традиционных средств языка Visual Basic удобно считать, что первый элемент любого массива
имеет индекс 1 (поскольку аналогичным образом индексируются и символы строки, и элементы файла).
Дополнительным преимуществом подобного подхода является то, что при этом порядковые номера элементов массивов,
упоминаемые в формулировках заданий, совпадают с индексами этих элементов.
Для того чтобы индексация элементов массива начиналась с 1, в VB5-6 достаточно при определении
размера массива указать, наряду с верхней границей индексов, также и нижнюю границу, равную 1 (например, 1 To n).
В VB.NET нижняя граница индексов всегда считается равной 0, и поэтому
ее нельзя указывать при задании размера массива, однако можно просто "пожертвовать" элементом с индексом 0,
не используя его для хранения данных.
Напомним, что функция FreeFile возвращает очередной свободный файловый номер
(т. е. номер, который еще не связан с каким-либо открытым файлом), а оператор Close
(замененный в VB.NET процедурой FileClose) обеспечивает закрытие всех файлов, открытых в программе.
Оператор открытия файла Open заменен в VB.NET на процедуру FileOpen, выполняющую те же самые действия.
Режим открытия файла в VB.NET определяется с помощью перечисления OpenMode, одним из значений которого
является Random, соответствующий, как и в VB5-6, режиму прямого доступа. Как и в случае оператора Open,
в процедуре FileOpen можно не указывать некоторые параметры, связанные с открываемым файлом; если
опущенные параметры не находятся в конце списка параметров, их необходимо явно выделять запятыми
(как это сделано в приведенном выше фрагменте программы для VB.NET).
Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным
имя результирующего файла. Считывание имен файлов производится в одну и ту же
переменную s, поскольку после открытия файла с именем s все остальные действия с
данным файлом в нашей программе будут осуществляться с использованием
файлового номера f(i), без обращения к имени файла.
Запуск нового варианта программы уже не будет считаться ознакомительным,
поскольку в программе выполняется ввод исходных данных. Так как имя
результирующего файла осталось непрочитанным, этот вариант решения будет
признан неверным и приведет к сообщению «Введены не все требуемые
исходные данные. Количество прочитанных данных: 3 (из 4)».
Создание пустого результирующего файла
Для того чтобы создать результирующий файл, в языке Visual Basic можно
использовать тот же оператор Open (процедуру FileOpen в VB.NET), что и для открытия существующего файла.
Таким образом, для чтения имени результирующего файла и создания данного файла
достаточно заменить в заголовке цикла число 3 на 4:
[VB5-6]
Sub Solve()
Task "File48"
Dim s As String, _
f(1 To 4) As Integer, _
a As Integer, i As Integer
For i = 1 To 4
GetS s
f(i) = FreeFile
Open s For Random As f(i) Len = Len(a)
Next
' Обработка файловых данных
Close
End Sub
[VB.NET]
Sub Solve()
Task("File48")
Dim s As String, _
f(4), a, i As Integer
For i = 1 To 4
GetS(s)
f(i) = FreeFile()
FileOpen(f(i), s, _
OpenMode.Random, , , Len(a))
Next
' Обработка файловых данных
FileClose()
End Sub
Комментарий «Обработка файловых данных» расположен в том месте программы, в котором можно
выполнять операции ввода-вывода для всех файлов: они уже открыты оператором Open (процедурой FileOpen в VB.NET)
и еще не закрыты оператором Close (процедурой FileClose в VB.NET).
При выполнении этого варианта программы результирующий файл будет
создан, однако останется пустым, т. е. не содержащим ни одного элемента.
Поэтому после запуска программы на информационной панели появится сообщение
«Ошибочное решение», а в строке, которая должна содержать элементы
результирующего файла, появится текст
EOF:
Особое значение EOF (End Of File «конец файла») для
указателя текущей файловой позиции в окне задачника означает, что данный файл
существует, но не содержит ни одного элемента. Таким образом, нам осталось
реализовать фрагмент алгоритма, обеспечивающий ввод и вывод файловых данных.
Использование неправильных типов для файловых элементов
Во всех ранее рассмотренных вариантах программы мы не использовали
операции ввода-вывода для файлов. Поэтому размер файловых элементов,
указываемый в операторе Open с помощью аргумента Len (или в процедуре FileOpen с помощью последнего параметра),
не играл никакой роли: в качестве этого аргумента можно было указать любое число.
Тип файловых элементов становится принципиально важным, если в программе
используются операции ввода-вывода для данного файла. Чтобы продемонстрировать
это на примере нашей программы, изменим тип переменной a на Double и заменим
комментарий «Обработка файловых данных» на следующий фрагмент:
[VB5-6]
For i = 1 To 3
Get f(i), , a
Put f(4), , a
Next
[VB.NET]
For i = 1 To 3
FileGet(f(i), a)
FilePut(f(4), a)
Next
Данный фрагмент обеспечивает считывание одного элемента для каждого из трех исходных
файлов и запись этих элементов в результирующий файл (в требуемом порядке).
Обратите внимание на то, что операторы ввода-вывода Get/Put заменены в VB.NET на
процедуры FileGet/FilePut; кроме того, изменен порядок следования параметров:
необязательный второй параметр операторов Get/Put с номером обрабатываемого элемента стал
третьим (т. е. последним) параметром процедур FileGet/FilePut,
поэтому при отсутствии этого параметра в VB.NET не требуется указывать предшествующую запятую.
Подчеркнем, что мы неправильно указали размер элементов файлов
(поскольку теперь этот размер равен размеру переменной типа Double, а не Integer);
тем не менее, компиляция программы пройдет успешно, а после ее запуска не
произойдет ошибок времени выполнения.
Результат работы программы будет неожиданным.
Вначале опишем его для VB5-6. Судя по экранной строке с содержимым результирующего файла, в него будут
записаны не три, а двенадцать элементов: по четыре начальных элемента из каждого исходного файла.
Объясняется это тем, что аргумент Len = Len(a) оператора Open устанавливает длину файловых элементов
равной 8 байтам (размер переменной типа Double), в то время как «на самом деле»,
т. е. по условию задания, элементами файлов являются целые числа (типа Integer), занимающие в памяти по 2 байта.
Поэтому считывание из файла и последующая запись в файл одного «вещественного файлового элемента»
фактически приводит к считыванию и записи блока данных длины 8 байтов, содержащего четыре последовательных
целочисленных элемента исходного файла. В VB.NET размер переменных типа
Integer равен не 2, а 4 байтам (для типа Double размер не изменился), поэтому при запуске варианта
программы для VB.NET в результирующий файл будут записаны шесть элементов: по два начальных элемента
из каждого исходного файла.
Итак, мы выяснили, что ошибки, связанные с несоответствием типов файловых
элементов, не выявляются при компиляции и не всегда приводят к ошибкам времени
выполнения. Это следует иметь в виду, и при появлении «странных»
результирующих данных начинать поиск ошибки с проверки типов файловых
элементов.
Заменив в нашей программе описание типа переменной a на Integer и
повторно запустив программу, мы получим все еще неверный, но вполне
«понятный» результат: созданный файл будет содержать три элемента,
совпадающих с начальными элементами исходных файлов.
Правильное решение, его тестирование и просмотр результатов
Приведем, наконец, верное решение задания File48:
[VB5-6]
Sub Solve()
Task "File48"
Dim s As String, f(1 To 4) As Integer, _
a As Integer, i As Integer, k As Integer
For i = 1 To 4
GetS s
f(i) = FreeFile
Open s For Random As f(i) Len = Len(a)
Next
For k = 1 To LOF(f(1)) \ Len(a)
For i = 1 To 3
Get f(i), , a
Put f(4), , a
Next
Next
Close
End Sub
[VB.NET]
Sub Solve()
Task("File48")
Dim s As String, _
f(4), a, i As Integer, _
k As Long
For i = 1 To 4
GetS(s)
f(i) = FreeFile()
FileOpen(f(i), s, _
OpenMode.Random, , , Len(a))
Next
For k = 1 to LOF(f(1)) \ Len(a)
For i = 1 To 3
FileGet(f(i), a)
FilePut(f(4), a)
Next
Next
FileClose()
End Sub
От предыдущего варианта данное решение отличается добавлением цикла по
переменной k, который обеспечивает считывание всех элементов из исходных файлов
(напомним, что по условию задания все исходные файлы имеют одинаковый размер)
и запись их в результирующий файл в нужном порядке.
После запуска этого варианта программы
мы получим сообщение «Верное решение. Тест номер 1 (из 5)», а после
пяти подобных запусков сообщение «Задание выполнено!». С
помощью модуля PT4Results
мы можем вывести на экран окно результатов, в котором будут
перечислены все наши попытки решения задания:
File48 b24/03 12:15 Ознакомительный запуск.
File48 b24/03 12:16 Введены не все требуемые исходные данные.
File48 b24/03 12:20 Ошибочное решение.--3
File48 b24/03 12:21 Задание выполнено!
Строковые и текстовые файлы: File63, Text21
В данном пункте описываются особенности выполнения заданий на обработку
строковых файлов (т. е. двоичных файлов прямого доступа, элементами которых
являются строки), а также текстовых файлов, содержащих строки различной длины,
оканчивающиеся маркерами конца строки.
Строковые файлы прямого доступа
В качестве примера задания на строковые файлы рассмотрим задание File63.
File63°. Дано целое число K (> 0) и строковый файл.
Создать два новых файла: строковый, содержащий первые K символов каждой строки исходного файла,
и символьный, содержащий K-й символ каждой строки (если длина строки меньше K,
то в строковый файл записывается вся строка, а в символьный файл записывается пробел).
При выполнении заданий на строковые файлы
следует предполагать, что все строки, содержащиеся в строковых файлах, имеют одинаковую длину,
равную 80 символам (строки меньшей длины дополняются справа пробелами). В VB5-6 этого легко добиться,
используя для записи/чтения строк переменные типа String * 80. В VB.NET отсутствует возможность
использования подобных строковых переменных фиксированного размера, поэтому перед записью строки
в строковый файл следует явным образом дополнять ее пробелами до 80 символов;
это проще всего сделать с помощью функции LSet. При обработке строк,
прочитанных из строкового файла, может потребоваться удалить завершающие пробелы;
для этого удобно использовать функцию RTrim.
Подчеркнем, что условие, согласно которому длина элементов строковых файлов равна 80 символам,
накладывается задачником Programming Taskbook.
В обычных программах, не связанных с выполнением учебных заданий,
можно создавать строковые файлы с элементами любой требуемой длины.
В задании File63 и в некоторых заданиях используются также символьные файлы:
это двоичные файлы, элементами которых являются символы.
В VB5-6 элементами символьных файлов следует считать строки единичной длины
(подобные строки описываются как String * 1).
В VB.NET также удобно использовать строковые переменные, поскольку символьный тип Char,
появившийся в VB.NET, обладает рядом особенностей, затрудняющих его совместное применение
с традиционными средствами языка Visual Basic, предусмотренными для обработки файлов.
Сразу приведем правильное решение задания File63, учитывающее отмеченные
выше особенности строковых и символьных файлов:
[VB5-6]
Sub Solve()
Task "File63"
Dim k As Integer, f1 As Integer, _
f2 As Integer, f3 As Integer, _
s1 As String, s2 As String, s3 As String, _
c As String * 1, _
s As String * 80, _
i As Long
GetN k
GetS s1
f1 = FreeFile()
Open s1 For Random As f1 Len = 80
GetS s2
f2 = FreeFile()
Open s2 For Random As f2 Len = 80
GetS s3
f3 = FreeFile()
Open s3 For Random As f3 Len = 1
For i = 1 To LOF(f1) \ 80
Get f1, , s
s = Left(s, k)
Put f2, , s
c = Mid(s, k, 1)
Put f3, , c
Next
Close
End Sub
[VB.NET]
Sub Solve()
Task("File63")
Dim k, f1, f2, f3 As Integer, _
s1, s2, s3, c, s As String, _
i As Long
GetN(k)
GetS(s1)
f1 = FreeFile()
FileOpen(f1, s1, _
OpenMode.Random, , , 80)
GetS(s2)
f2 = FreeFile()
FileOpen(f2, s2, _
OpenMode.Random, , , 80)
GetS(s3)
f3 = FreeFile()
FileOpen(f3, s3, _
OpenMode.Random, , , 1)
s = Space(80)
For i = 1 To LOF(f1) \ 80
FileGet(f1, s, , True)
s = LSet(Left(s, k), 80)
FilePut(f2, s, , True)
c = Mid(s, k, 1)
FilePut(f3, c, , True)
Next
FileClose()
End Sub
Следует обратить внимание на три особенности варианта решения для VB.NET.
Прежде всего, перед считыванием строки из исходного файла в переменную s необходимо явно задать для
переменной s размер, равный 80 символам, используя, например, функцию Space
(в варианте для VB5-6 это делать не требуется, так как переменная, описанная как String * 80,
всегда имеет длину 80). Далее, после выделения из строки s подстроки длины k необходимо
явным образом дополнить эту подстроку до 80 символов, используя, например, функцию LSet
(в варианте для VB5-6 это дополнение выполняется автоматически, в силу фиксированного
размера переменной типа String * 80). Наконец, при чтении и записи строки в файл
в процедурах FileGet и FilePut необходимо явным образом указывать четвертый (необязательный)
параметр StringIsFixedLength, положив его равным True. Если этого не сделать,
то при выполнении операций ввода-вывода будет использоваться новый формат хранения строк,
который отличается от формата, используемого электронным задачником при
формировании и проверке правильности строковых файлов (задачник использует формат хранения строк, принятый в VB5-6).
Если при выполнении заданий на символьные и строковые файлы в VB.NET не учитывать эти особенности,
то данные будут считаны или записаны неверно,
и в результате при запуске программы будет выведено сообщение об ошибке.
В частности, если в процедуре FileGet или FilePut в качестве параметра
StringIsFixedLength не указано значение True, то сообщение об ошибке будет иметь вид
«Bad record length» («Ошибочная длина записи»).
Текстовые файлы
Текстовые файлы содержат строки переменной длины, снабженные маркерами
конца строки. Для работы с текстовыми файлами следует использовать стандартные
режимы Input, Append и Output.
В качестве примера задания на текстовые файлы рассмотрим задание Text21.
Text21°. Дан текстовый файл, содержащий более трех строк. Удалить из него
последние три строки.
Поскольку текстовые файлы, в отличие от файлов прямого доступа, нельзя
открыть одновременно на чтение и на запись, для изменения текстового файла
необходимо воспользоваться вспомогательным файлом. Во вспомогательный файл
записываются необходимые результирующие данные, после чего исходный файл
удаляется с диска, а имя вспомогательного файла заменяется на имя исходного.
Далее, поскольку строки, содержащиеся в текстовом файле, могут иметь различную
длину, для определения числа строк необходимо последовательно считать из файла
все его строки. Заметим, что, в отличие от строковых файлов прямого доступа, в VB5-6 для
чтения данных из текстовых файлов вполне допустимо использовать переменные
типа String, не фиксируя их размер.
Приведем решение задания Text21, учитывающее отмеченные выше
особенности текстовых файлов:
[VB5-6]
Sub Solve()
Task "Text21"
Dim f1 As Integer, f2 As Integer, _
n As Integer, i As Integer, _
name1 As String, name2 As String, s As String
GetS name1
f1 = FreeFile()
Open name1 For Input As f1
name2 = "$T21$.tmp"
f2 = FreeFile()
Open name2 For Output As f2
Do While Not EOF(f1)
Line Input #f1, s
n = n + 1
Loop
Close f1
Open name1 For Input As f1
For i = 1 To n - 3
Line Input #f1, s
Print #f2, s
Next
Close
Kill name1
Name name2 As name1
End Sub
[VB.NET]
Sub Solve()
Task("Text21")
Dim f1, f2, n, i As Integer, _
name1, name2 As String
GetS(name1)
f1 = FreeFile()
FileOpen(f1, name1, OpenMode.Input)
name2 = "$T21$.tmp"
f2 = FreeFile()
FileOpen(f2, name2, OpenMode.Output)
Do While Not EOF(f1)
LineInput(f1)
n = n + 1
Loop
FileClose(f1)
FileOpen(f1, name1, OpenMode.Input)
For i = 1 To n - 3
PrintLine(f2, LineInput(f1))
Next
FileClose()
Kill(name1)
Rename(name2, name1)
End Sub
Обратите внимание на ряд особенностей, связанных с чтением и записью строк для текстовых файлов
для различных версий Visual Basic.
Для записи в текстовый файл строки и следующего за ней маркера конца строки в VB5-6
используется оператор Print (обратите внимание на обязательный символ #, который
указывается перед файловым идентификатором f), а в VB.NET процедура PrintLine.
Если после записи строки в файл не требуется записывать маркер конца строки, то в VB5-6
следует указать символ «;» (точка с запятой) в конце оператора Print,
а в VB.NET вместо процедуры PrintLine надо использовать процедуру Print.
Для считывания очередной строки из текстового файла в VB5-6 используется оператор Line Input,
а в VB.NET функция LineInput, возвращаемым значением которой является строка,
прочитанная из файла. В приведенном варианте решения для VB.NET
вызов функции LineInput в цикле Do While оформлен как процедура, поскольку возвращаемое значение
функции в данном случае не используется.
Обратите также внимание на то, что для переименования файла в VB5-6 предусмотрен оператор Name,
а в VB.NET процедура Rename.
Приведенный вариант решения является неэффективным, поскольку требует двух
просмотров исходного файла f1: первый для определения его размера,
который записывается в переменную n, второй для создания
вспомогательного файла f2, содержащего все строки исходного файла, кроме трех
последних.
Задание Text21 можно выполнить и за один просмотр исходного файла, если
воспользоваться следующим наблюдением: строка должна быть записана во вспомогательный файл,
если после нее в исходном файле находятся по крайней мере три
строки. Таким образом, записывать очередную строку во вспомогательный файл
следует только после считывания из исходного файла трех следующих за ней строк.
Благодаря такому упреждающему считыванию необходимость в предварительном
определении размера исходного файла отпадает. Для хранения строк, которые уже
считаны из исходного файла, но еще не записаны во вспомогательный файл, удобно
использовать массив из трех элементов.
Приведем программу, реализующую описанный выше эффективный
однопроходный алгоритм решения задания:
[VB5-6]
Sub Solve()
Task "Text21"
Dim f1 As Integer, f2 As Integer, _
n As Integer, i As Integer, _
name1 As String, name2 As String, _
s(1 To 3) As String
GetS name1
f1 = FreeFile()
Open name1 For Input As f1
name2 = "$T21$.tmp"
f2 = FreeFile()
Open name2 For Output As f2
For i = 1 To 3
Line Input #f1, s(i)
Next
n = 1
Do While Not EOF(f1)
Print #f2, s(n)
Line Input #f1, s(n)
n = n Mod 3 + 1
Loop
Close
Kill name1
Name name2 As name1
End Sub
[VB.NET]
Sub Solve()
Task("Text21")
Dim f1, f2, n, i As Integer, _
name1, name2, s(3) As String
GetS(name1)
f1 = FreeFile()
FileOpen(f1, name1, OpenMode.Input)
name2 = "$T21$.tmp"
f2 = FreeFile()
FileOpen(f2, name2, OpenMode.Output)
For i = 1 To 3
s(i) = LineInput(f1)
Next
n = 1
Do While Not EOF(f1)
PrintLine(f2, s(n))
s(n) = LineInput(f1)
n = n Mod 3 + 1
Loop
FileClose()
Kill(name1)
Rename(name2, name1)
End Sub
Вначале элементы вспомогательного массива s инициализируются первыми тремя строками из исходного файла
(по условию файл содержит не менее трех строк). Для хранения номера текущего элемента массива используется
переменная n.
На каждой итерации цикла Do While во вспомогательный файл записывается строка s(n),
т. е. значение текущего элемента массива s (в этот момент уже известно, что после данной строки
в исходном файле содержатся, по крайней мере, три строки: две из этих строк уже прочитаны в массив,
а на третьей строке располагается файловый указатель). Затем из исходного файла считывается очередная
строка, которая записывается на место только что сохраненного во вспомогательном файле
элемента массива s. Наконец, выполняется циклическое изменение номера n: 1 переходит в 2, 2 в 3,
3 в 1 (при этом используется операция Mod нахождения остатка от деления).
После завершения цикла Do While во вспомогательный файл будут записаны все строки исходного файла,
кроме последних трех, а эти три последние строки будут содержаться в массиве s.
|