Как за полчаса написать простую читалку книг FB2 для десктопа на Java
Недавно мне для личных целей потребовалось написать читалку FB2. И сразу я столкнулась с тем, что информации по теме минимум. Палочка-выручалочка под названием ChatGPT выдал что-то невразумительное в ответ на довольно подробный запрос. К тому же, никаких готовых библиотек, чтобы по-быстренькому наваять ридер, я также не смогла обнаружить. Хотя искала долго и упорно, как Чубакка расческу.
Все это привело меня к закономерному выводу, что сначала нужно изучить формат FB2. А потом подумать, как прочитать его стандартными способами и вывести на экран. После того, как я немного разобралась со структурой FB2, начала догадываться, почему нет готовых библиотек. Дело в том, что этот формат довольно простой, и нет особой необходимости писать для него отдельную библиотеку. Можно довольно быстро наваять свой код, который будет читать практически все файлы FB2. И вы сможете убедиться в этом, если дочитаете статью до конца.
А раз все так просто, зачем я пишу эту статью? Для этого у меня есть две причины. Во-первых, это моя первая проба пера на Хабре. А во-вторых, возможно, это сэкономит кучу времени другому такому же новичку, как я. Ну, или пригодится какому-нибудь студенту, который пишет реферат.
Что из себя представляют файлы FB2
Итак, начнем с краткого описания формата. Откроем первый попавший под руку файл FB2 в блокноте, и мы увидим, что это обычный XML.

Стандартная структура файла включает три крупных блока:
Куча элементов с разной информацией о книге, такой как имя, фамилия автора, название, краткое описание, обложка и т.д. Собственно текст книги Base64 код картинки . . еще много binary элементов
Наша задача заключается в том, чтобы вытащить содержимое этих блоков. Контент из description и body можно, немного подредактировав, отобразить как html-код с помощью такого инструмента JavaFX, как WebView. А над binary-элементами придется сначала чуточку пошаманить. Дело в том, что они представляют собой бинарный код в формате Base64, что выглядит примерно так:

Иными словами, это огромный блок сплошных символов. И все это безобразие находится в конце файла FB2. Нам нужно будет не только извлечь весь этот код из FB2, но и отделить картинки друг от друга. А потом сохранить их как нормальные изображения в формате JPG, PNG или GIF.
Подготовка к работе над читалкой FB2
Итак, для работы нам понадобится среда разработки Eclipse IDE. У меня на момент написания статьи установлена версия 4.25.0. Наводим мышку на главное меню, нажимаем File, выбираем пункт New, а затем Project.

Далее в открывшемся окошке в разделе Maven выбираем Maven Project и жмем Next.

В следующем окне ставим галочку напротив пункта Create a simple project и снова жмем Next.

В окне под названием New Maven Project заполняем поля Group Id и Artifact Id примерно как на следующей картинке. После этого можно смело жать Finish.

Eclipse создаст новый проект, который появится в левой колонке под названием Package Explorer.

Потом щелкаем правой кнопкой мыши по строчке src/main/java, выбираем в выпавшем списке пункт New, а затем Package.

.Заполняем поле Name в открывшемся окне в точности как на картинке.

В левой колонке (Package Explorer) появится новая строка com.example. Клацаем по ней правой кнопкой мыши, выбираем пункт New, а затем Class. В появившемся окне заполняем поле Name как на картинке ниже. Также нужно поставить галочку в поле public static void main.

В центральной колонке откроется файл FbReader.java, в котором будем писать весь основной код. Также нам понадобится файл pom.xml. В него мы будем вставлять зависимости Maven. Они нужны для того, чтобы подключались все необходимые для работы программы плагины. Файл pom.xml также нужно открыть для редактирования. Для этого отыщите его в левой колонке (Package Explorer) и дважды щелкните по нему мышкой. Когда будете производить какие-либо изменения в этих файлах, не забывайте их сохранять (пункт верхнего меню File, затем Save All).
Чтобы программа работала корректно, нам нужно сделать еще пару настроек. В Package Explorer выберите пункт JRE System Library, а затем Properties.

Далее потребуется сменить версию JRE на JavaSE-1.8 (jdk-19.0.2). Если в списке у вас нет такого пункта, погуглите, чтобы узнать, как установить нужную версию. Мы на этом останавливаться сейчас не будем.

И последняя настройка. В Package Explorer (левая колонка) выбираем наш проект, жмем на него правой кнопкой мыши, открываем Properties, находим пункт Run/Debug Setting, затем жмем New. , выбираем Java Application и подтверждаем выбор, нажав на ОК.

В новом окне выбираем Main class (как на картинке).

Затем переходим на вкладку Arguments и заполняем поле VM Arguments так, как показано на картинке. Жмем ОК, а потом Apply and Close.

На этом предварительные настройки завершены. Можно переходить к написанию кода.
Пошаговое написание читалки FB2 на Java
I этап. Создание временной папки и файла для тестов
Первым делом создадим временную папку, в которую будем складывать ресурсы (картинки, временный файл с текстом и др.). Одновременно напишем метод для удаления временной папки после завершения работы программы.
Но сначала нужно найти какой-нибудь файл в формате FB2 для тестов и сохранить его в папку проекта. Сразу запишем путь к этому файлу в строковую переменную EPUB_FILE. Создадим метод для копирования файлов и воспользуемся им для переноса тестового FB2 во временную папку. Изменим расширение скопированного файла на txt.
Добавим следующий код в файл FbReader.java:
import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Base64; import java.util.Base64.Decoder; import javax.imageio.ImageIO; import org.mozilla.universalchardet.UniversalDetector; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.layout.BorderPane; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import javafx.stage.WindowEvent; public class FbReader extends Application < // Путь к тестовому файлу FB2 private static final String EPUB_FILE = "test.fb2"; public static void main(String[] args) < launch(args); >public void start(Stage stage) throws Exception < stage.setTitle("FBReader: программа для чтения книг FB2"); // Создаем временную папку в папке проекта String filePath = new File("").getAbsolutePath(); Path tempDir = Files.createTempDirectory(Paths.get(filePath), "temp"); // Каталог, в который нужно сохранить все ресурсы String outputDir = String.valueOf(tempDir) + "\\"; // Создаем копию файла и переименовываем расширение в txt try < copyFile(new File(EPUB_FILE), new File(outputDir + "temp.txt")); >catch (java.nio.file.NoSuchFileException e) < Alert alert = new Alert(AlertType.WARNING); alert.setTitle("Файл не найден"); alert.setHeaderText("Ошибка при открытии файла"); alert.setContentText("Такого файла не существует."); alert.showAndWait(); File tmpFls = new File(String.valueOf(tempDir)); deleteDir(tmpFls); >> // Метод для удаления временной папки private static void deleteDir(File tmpFls) < File[] contents = tmpFls.listFiles(); if (contents != null) < for (File f : contents) < if (! Files.isSymbolicLink(f.toPath())) < deleteDir(f); >> > tmpFls.delete(); > // Метод для копирования файла public static void copyFile(File src, File dest) throws IOException < Files.copy(src.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); >>
Если программа не обнаружит наш тестовый FB2, она выкинет окошко (Alert) с сообщением о ненайденном файле и удалит временную папку.
На данном этапе потребуется добавить следующие зависимости в файл pom.xml:
org.openjfx javafx-controls 16 org.openjfx javafx-graphics 16 org.openjfx javafx-base 16
II этап. Сохранение в переменную всего содержимого FB2
После того, как мы создали временную папку для хранения ресурсов и переместили туда тестовый файл, нам нужно будет вытащить из него абсолютно все, что в нем есть. И на этом моменте у меня возникла первая сложность. Дело в том, что по умолчанию Java работает с текстами в кодировке UTF-8. А мой тестовый файлик был совсем в другой кодировке (Windows-1251). Подозреваю, что у других книг FB2 могут встречаться всякие разные варианты кодировок. Но это не точно.
Поэтому нам нужно программно определить кодировку файла, а затем перекодировать его содержимое в UTF-8. Добавим код в наш класс FbReader.java:
// Получение всего текста из файла temp.txt String tempFilePath = outputDir + "temp.txt"; String charset = detectCharset(tempFilePath); String content = readText(tempFilePath, charset);
А еще напишем два метода, первый из которых служит для определения кодировки текстового файла. А второй — для чтения содержимого этого файла, извлечения данных, кодировки в UTF-8 и записи в переменную String.
// Метод для определения кодировки текстового файла private static String detectCharset(String filePath) < try (InputStream inputStream = new FileInputStream(filePath)) < byte[] bytes = new byte[4096]; UniversalDetector detector = new UniversalDetector(null); int nread; while ((nread = inputStream.read(bytes)) >0 && !detector.isDone()) < detector.handleData(bytes, 0, nread); >detector.dataEnd(); return detector.getDetectedCharset(); > catch (IOException e) < e.printStackTrace(); >return null; > // Метод для чтения содержимого текстового файла, извлечения данных, // кодировки в UTF-8 и записи в переменную String private static String readText(String filePath, String charset) < try (BufferedReader reader = new BufferedReader(new InputStreamReader( new FileInputStream(filePath), charset))) < StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) < builder.append(line).append("\n"); >return builder.toString(); > catch (IOException e) < e.printStackTrace(); >return null; >
Чтобы все заработало как надо, в pom.xml добавим зависимость:
com.googlecode.juniversalchardet juniversalchardet 1.0.3
III этап. Извлечение текстового контента
Теперь, когда мы вытащили все содержимое FB2 файла, перекодировали его в UTF-8 и записали в строковую переменную, можно приступать к самому интересному. А именно, к извлечению частей текста и картинок. Для получения содержимого элементов description, body и binary мы будем использовать стандартные операции Java со строковыми переменными. На этом шаге займемся исключительно description и body.
Добавим следующий код в класс FbReader.java:
// Получение текста элемента description int start = content.indexOf(""); int end = content.lastIndexOf(" "); end = end + 14; char[] dest = new char[end - start]; content.getChars(start, end, dest, 0); String description = new String(dest); description = description.replace("image l:href=\"#", "img src=\"file://" + tempDir.toString().replace("\\","/") + "/"); // Получение текста между тегами body int startBody = content.indexOf(""); int endBody = content.lastIndexOf(""); endBody = endBody + 7; char[] dst = new char[endBody - startBody]; content.getChars(startBody, endBody, dst, 0); String body = new String(dst); body = body.replace("image l:href=\"#", "img src=\"file://" + tempDir.toString().replace("\\", "/") + "/");
Таким образом, мы получили содержимое элементов description и body из файла FB2. Одновременно с этим мы немножко подредактировали теги картинок, чтобы они корректно отображались в дальнейшем. Для этого нам понадобилось заменить невалидный для отображения в формате HTML код элементов image на соответствующие теги. Также мы прописали путь к картинкам, указав временную директорию. Именно в эту папку мы на следующем этапе и будем сохранять картинки.
IV этап. Извлечение картинок и сохранение во временную папку
На данном шаге нам придется хорошо постараться. Но все усилия будут не напрасны. Сначала мы вытащим из нашего многострадального текстового файла вообще все binary элементы и сохраним их в строковую переменную. Затем создадим цикл и два массива. Мы последовательно будем проходиться по всем элементам binary. Код Base64 изображений для каждого отдельного элемента будем сохранять в один массив, а имя файла картинки — во второй.
В конце мы сохраним картинки в нашу временную директорию. Но сначала перекодируем их из формата Base64 и получим байтовый массив. Это позволит нам получить отдельные файлы картинок в привычных форматах (JPG, PNG, GIF) и сохранить их во временную папку.
Вносим следующие изменения в класс FbReader.java:
// Получение текста со всеми картинками (binary) int startBin = content.indexOf(""); if(startBin != -1 || endBin != -1) < endBin = endBin + 9; char[] dstb = new char[endBin - startBin]; content.getChars(startBin, endBin, dstb, 0); String binContent = new String(dstb); //System.out.println(binContent); // Объявляем массивы для данных о картинках ArrayListbinCode = new ArrayList<>(); ArrayList binImg = new ArrayList<>(); // Получение отдельных binary do < int nextBin = binContent.indexOf(""); if(nextBin != -1) < lastBin = lastBin + 9; // Получение первой из оставшихся binary char[] dstBin = new char[lastBin - nextBin]; binContent.getChars(nextBin, lastBin, dstBin, 0); String binaryEl = new String(dstBin); // Удаление текущей картинки из текста со всеми картинками binContent = binContent.replace(binaryEl, ""); // Удаление закрывающего тега binary из текущей картинки binaryEl = binaryEl.replace(" ", ""); // Получение строки с открывающим тегом binary int findString = binaryEl.indexOf(">"); String tag = binaryEl.substring(0,findString + 1); // Удаление открываюшего тега из картинки binaryEl = binaryEl.replace(tag, ""); // Получение названия картинки int findId = tag.indexOf("id="); findId = findId + 4; String imageName = tag.substring(findId); int newFindId = imageName.indexOf("\""); String delText = imageName.substring(newFindId); imageName = imageName.replace(delText, ""); // Кладем данные о картинках в массивы binCode.add(binaryEl); binImg.add(imageName); > else < break; >> while(binContent.indexOf(" else if(extImage.equals("png")) < File outputfile = new File(outputDir + binImg.get(i)); ImageIO.write(img, "png", outputfile); >else if(extImage.equals("gif")) < File outputfile = new File(outputDir + binImg.get(i)); ImageIO.write(img, "gif", outputfile); >> catch (javax.imageio.IIOException e) < e.printStackTrace(); >> >
Можно выдохнуть, самое трудное позади. Остался последний этап. Но расслабляться пока все еще рано. Нам предстоит собрать весь контент книги воедино и отобразить его в окне так называемой сцены программы (Scene).
V этап. Вывод контента в окно программы
В качестве завершающих штрихов мы соединим текстовые части description и body, создадим сцену (Scene), добавим на нее браузерный элемент WebView. А в самом конце добавим код для удаления временной папки при закрытии окна программы. Для этого допишем следующие строчки в класс FbReader.java:
// Помещаем весь текстовый контент в одну переменную String text = description + "\n" + body; // Создаем панель WebView для отображения HTML контента WebView webView = new WebView(); WebEngine webEngine = webView.getEngine(); webEngine.loadContent(text); // Замена встроенных стилей CSS на собственные webEngine.setUserStyleSheetLocation("data:, @font-face " + "body " + "img "); BorderPane borderPane = new BorderPane(); borderPane.setCenter(webView); // Создание сцены и отображение окна Scene scene = new Scene(borderPane); stage.setScene(scene); stage.show(); stage.setHeight(900); borderPane.setPrefHeight(5000); // Удаление временной папки при закрытии окна stage.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, event -> < File tmpFls = new File(tempDir.toString()); deleteDir(tmpFls); >);
Добавим зависимость в pom.xml для корректной работы WebView:
org.openjfx javafx-web 17
На данном этапе меня поджидал еще один сюрприз. Когда я собрала весь контент и вывела его в окно программы, то обнаружила, что читать текст довольно сложно. Он выводился слишком мелким шрифтом, с небольшими интервалами между строками, буквально слипшимся. Обложки у некоторых книг вылазили за пределы окна, а полоса вертикального скроллинга скрывала пусть небольшую, но все же часть текста. Для того, чтобы устранить проблему, я прописала собственные стили CSS для окошка WebView.
Еще мне не понравился дефолтный шрифт с засечками. Я решила поменять его на Open Sans без засечек. Текст, написанный таким шрифтом, гораздо легче читается и воспринимается. Шрифт Open Sans или любой другой понравившийся можно скачать на сайте Google Fonts. Затем нужно будет создать папку Fonts в корневой директории проекта и положить туда TTF файл со шрифтом.
Запуск программы
Чтобы проверить, как все работает, нужно запустить программу. Для этого переходим в Package Explorer (левая колонка Eclipse), жмем правой кнопкой мыши на название нашего проекта, выбираем пункт выпавшего меню Run As, далее Java Application.

Откроется окно Select Java Application. В нем выбираем наш проект и жмем ОК.

Если вы сделали все правильно, должно открыться окно программы с текстом и картинками вашей электронной книги FB2.
Над чем еще можно поработать
Если есть желание, можно разобрать элемент description, вытащить из него имя автора, наименование, краткое описание книги, название файла обложки и другие параметры. А затем разложить их по переменным для дальнейшего использования. С этой задачей помогут справиться библиотеки JAXB или Jsoup, которые как раз и предназначены для работы с XML и HTML форматами. Я не стала так заморачиваться и выбрала путь попроще, потому что для моих целей этого было достаточно.
Что хочу сказать напоследок. Я новичок, поэтому прошу проявить милосердие и не кидаться помидорами, если есть чипсы. Буду рада, если в комментариях вы подскажете, как можно улучшить программу.
Если возникнут какие-либо проблемы, например, не сможете разобраться, куда вставлять тот или иной кусок кода, вы всегда можете скачать исходник и посмотреть на весь код целиком.
Исходный код проекта вы найдете тут.
Баг или фича в Java: Вывод кириллических символов в консоль.
Есть много особенностей, про которые желательно знать, программируя на Java, даже если Вы начинающий программист. Под катом я расскажу как вывести кириллические символы в консоль Windows и наглядно это продемонстрирую.
Начнем с простого.
Базовые типы
- boolean;
- byte, char, short, int, long;
- float, double.
Char — это символьный тип данных. Переменная такого типа занимает 2 байта памяти, так как хранится в кодировке unicode.
С переменными этого типа можно производить только операции присваивания, но зато различными способами. Самый простой из них выглядит так:
c = 'b';
Символ можно представить также в виде его кода, записанного в восьмеричной системе счисления:
c = '\077';
Где 077 – это обязательно трехзначное число, не большее чем 377 (=255 в десятичной системе счисления).
Или же в шестнадцатеричной системе счисления следующим образом:
c = '\u12a4';
Кроме того, существуют специальные символы, такие как знак абзаца, разрыв страницы и др. Знак абзаца запишется, например, так:
c = '\n';
Не нужно перечислять их здесь. При необходимости всегда можно заглянуть в справочник.
Теперь внимание. Кодировкой по-умолчанию среды программирования Java является Latin-1. Однако, при выводе в поток System.out символы преобразуются в кодировку по умолчанию для операционной системы. Так для русскоязычной локализации кодировкой по-умолчанию является Windows-1251, для linux таковой будет UTF-8. Однако по задумке Microsoft решили для консоли Windows использовать кодировку Cp866.
Соответственно вывод: для корректного отображения кириллических символов в консоли нужно выводить символы в кодировке Cp866!
Это можно сделать следующим способом:
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
public class Hello
public static void main(String[] args) throws UnsupportedEncodingException < /*Может возникнуть исключение типа UnsupportedEncodingException*/
String x = "Привет, мир. "; //Это строка, которую мы будем выводить на экран
PrintStream printStream = new PrintStream(System.out, true, "cp866");
/*Создаем новый поток вывода, который будет автоматически преобразоввывать символы в кодировку Cp866*/
printStream.println(x);
Сохраним полученный код в файл Hello.java. Далее создадим Hello.bat файл следующего содержания:
javac Hello.java
java -cp . Hello
pause
И поместим его в одну папку с файлом Hello.java. Должно получиться примерно так:
Как осуществить перекодировку содержимого файлов java
Здесь я попытался собрать информацию по поводу написания русскоязычных программ на языке Java. Если Вам известно что-либо помимо того, что я тут понаписал, напишите мне, ваши добавления будут кстати.
Файлы данных, потоки, БД.
Итак, как все, надеюсь, знают, в языке Java для представления символов используется Unicode, т.е. по два байта на один символ (тип char размером в 16 бит). В набор символов входят всевозможные буквы со всякими чёрточками и припендюльками, греческие, математические и символы псевдографики. В том числе и так любимые нами символы кириллицы (диапазон значений 0x0400-0x04ff). Так что с этой стороны никакой дискриминации нет.
Если Вам интересны конкретные кода символов, для их просмотра удобно использовать программу "Таблица символов" из WinNT. Вот, например, диапазон кириллицы:
С другой стороны большинство файлов данных основано на 8-битовом представлении символов. Сюда входят также текстовые файлы и большинство баз данных (окромя наиболее продвинутых). Кроме того, что самое паршивое, одни и те же байты могут представлять разные символы (в зависимости от кодовой страницы). Налицо конфликт - как преобразовать одно в другое и наоборот, причём с наименьшими потерями для данных. Для этого был придуман довольно удобный механизм использования кодовых страниц. Для каждой кодовой страницы было создано по 2 класса перекодировки (ByteToChar и CharToByte). Классы эти лежат в пакете sun.io. Если, при перекодировке из char в byte не было найдено соответствующего символа, он заменяется на символ ?.
Кстати, эти файлы кодовых страниц в некоторых ранних версиях JDK 1.1 содержат ошибки, вызывающие ошибки перекодировок, а то и вообще исключения при выполнении. Например, это касается кодировки KOI8_R. Лучшее, что можно при этом сделать - сменить версию на более позднюю. Судя по Sun-овскому описанию, большинство этих проблем было решено в версии JDK 1.1.6.
Когда и как надлежит пользоваться этой перекодировкой? Когда пользоваться, в принципе, понятно - при любом преобразовании из byte в char и наоборот. В классе String в тех местах, где есть преобразование можно указать дополнительный параметр (String enc), задающий имя кодовой страницы. Это конструктор по массиву байтов и метод getBytes(). Однако, в реальной программе, явно указывать кодовую страницу не всегда удобно. Для этого была введена кодировка по умолчанию. По умолчанию она зависит от системы (для русских виндов принята кодировка Cp1251), и в старых JDK её можно изменить установкой системного свойства file.encoding. Вообще-то, как утверждают в Sun, это свойство отражает системную кодировку, и она не должна изменяться в командной строке (см., например, комментарии к BugID 4256423) Эта кодировка используется тогда, когда явно не указанно название страницы. Т.к. эта настройка одна на все преобразования, иногда можно наткнуться на неприятности. Например, эта же настройка используется для вывода на консольный экран, что, в случае виндов, как правило, неприемлемо - там нужно использовать страницу Cp866. Было бы здорово, если бы эти кодировки указывались независимо - например, console.encoding и т.п., но, думаю, Sun-овцам пока не до таких высоких материй.
-
Использовать вместо System.out.println свой класс, а уже в нём делать преобразование. Например:
public class Msg < static String cp = System.getProperty("console.encoding","Cp866"); public static void message(String msg) < msg += "\n"; byte[] b; try < b = msg.getBytes(cp); >catch( UnsupportedEncodingException e ) < b = msg.getBytes(); // В случае отсутствия нужной кодировки, // делаем преобразование по умолчанию >System.out.write(b); > > . Msg.message("Сообщение");
. public static void main(String[] args) < // Установка вывода консольных сообщений в нужной кодировке try < System.setOut(new CodepagePrintStream(System.out,System.getProperty("console.encoding","Cp866")) ); >catch(UnsupportedEncodingException e) < System.out.println("Unable to setup console codepage: " + e); >.
- Читать и записывать массивы байтов (byte[]), а для перекодировки использовать упомянутые методы класса String. Этот способ особенно удобен, когда в потоке могут присутствовать данные в разных кодировках.
- Использовать классы InputStreamReader и OutputStreamWriter из пакета java.io, специально предназначенные для этих целей.
- Сделать преобразование в нужную кодировку. Если вы всё сделаете корректно, то данные не потеряются, хотя, конечно, пользоваться этим желательно только в крайнем случае. Пример:
// Чтение русских букв в кодировке Cp866 через объект, // поддерживающий только Cp437 String str = o.readString(); str = new String(str.getBytes("Cp437"),"Cp866"); // Сохранение русских букв в кодировке Cp866 через объект, // поддерживающий только Cp437 str = new String(str.getBytes("Cp866"),"Cp437"); o.writeString(str);
Например, один из самых часто используемых драйверов - мост JDBC-ODBC. В версиях JDK 1.1, этот мост просто игнорировал кодировки символов, из-за чего нужно было предпринять дополнительные ухищрения, типа описанных в предыдущем пункте (это также касается и последней ихней версии, 1.1.8).
Мост из комплекта Sun Java 2 теперь можно настроить на нужную кодировку. Это делается добавлением дополнительного свойства charSet в набор параметров, передаваемых для открытия соединения с базой. По умолчанию используется file.encoding. Делается это примерно так:
// Параметры соединения с базой Properties connInfo = new Properties(); connInfo.put("user", username); connInfo.put("password", password); connInfo.put("charSet", "Cp1251"); // Устанавливаем соединение Connection db = DriverManager.getConnection(dataurl, connInfo);
Другой пример - драйвер JDBC-OCI (не pure Java - тот называется thin) от Oracle 8.0.5 под Linux. При получении данных из БД, драйвер определяет "свою" кодировку при помощи переменной окружения NLS_LANG. Если эта переменная не найдена, то он считает что кодировка - ISO88591. Весь фокус в том, что NLS_LANG должна быть именно переменной окружения, а properties (типа file.encoding) здесь "не катят". В случае использования драйвера внутри servlet engine Apache+Jserv, переменную окружения можно задать в файле jserv.properties:
wrapper.env=NLS_LANG=American_America.CL8KOI8R
Если же Вы свободны в формировании формата - тогда всё проще. Используйте формат Unicode или UTF8 - и проблем не будет.
В случае с БД, можно, конечно, использовать и какой-нибудь 16-ричный формат, но это не всегда приемлемо, т.к. Вы получите 2-х - 4-х кратный рост места на диске и потеряете возможность использовать стандартные программы работы с БД, например генераторы отчётов.
Русские буквы в исходниках Java-программ.
Как уже упоминалось, при выполнении программы используется Unicode. Исходные же файлы пишутся в обычных редакторах. Я пользуюсь Far-ом, у Вас, наверняка есть свой любимый редактор. Эти редакторы сохраняют файлы в 8-битовом формате, а значит, что к этим файлам также применимы рассуждения, аналогичные приведённым выше. Разные версии компиляторов немного по разному выполняют преобразование символов. В ранних версиях JDK 1.1.x используется настройка file.encoding, которую можно поменять при помощи нестандартной опции -J. В более новых (как сообщил Денис Кокарев - начиная с 1.1.4) был введён дополнительный параметр -encoding, при помощи которого можно указать используемую кодировку. В скомпилированных классах строки представлены в виде Unicode (точнее в модифицированном варианте формата UTF8), так что самое интересное происходит при компиляции. Поэтому, самое главное - выяснить, в какой кодировке у Вас исходники и указать правильное значение при компиляции. По умолчанию будет использован всё тот же пресловутый file.encoding. Пример вызова компилятора:
javac -encoding=KOI8_R .
Кроме использования этой настройки есть ещё один метод - указывать буквы в формате "\uxxxx", где указывается код символа. Этот метод работает со всеми версиями, а для получения этих кодов можно использовать стандартную утилиту native2ascii.
Русские буквы в файлах properties.
Для чтения файлов properties используются методы загрузки ресурсов, которые работают специфичным образом. Собственно для чтения используется метод Properties.load, который не использует file.encoding (там кодировка жёстко указана), поэтому единственный способ указать русские буквы - использовать формат "\uxxxx" и утилиту native2ascii.
Метод Properties.save работает по разному в версиях JDK 1.1 и 1.2. В версиях 1.1 он просто отбрасывал старший байт, поэтому правильно работал только с англицкими буквами. В 1.2 делается обратное преобразование в "\uxxxx", так что он работает зеркально к методу load.
Русские буквы в Servlet-ах.
Ну, для чего эти самые Servlet-ы нужны, я думаю, Вы в курсе. Если нет - то лучше сначала прочитать документацию. Здесь же рассказывается только об особенностях работы с русскими буквами.
Так в чём же особенности? Когда Servlet посылает ответ клиенту, есть два способа послать этот ответ - через OutputStream (getOutputStream()) или через PrintWriter (getWriter()). В первом случае Вы записываете массивы байтов, поэтому применимы вышеописанные методы записи в файл. В случае же PrintWriter, он использует установленную кодировку. В любом случае необходимо правильно указать используемую кодировку при вызове метода setContentType(), для того, чтобы было правильное преобразование символов на стороне сервера. Это указание должно быть сделано перед вызовом getWriter() или перед первой записью в OutputStream. Пример:
public void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException < response.setContentType("text/html; charset=windows-1251")); PrintWriter out = response.getWriter(); // Отладочный вывод названия кодировки для проверки out.println( "Encoding: " + response.getCharacterEncoding() ); . out.close();
Это по поводу отдачи ответов клиенту. Со входными параметрами, к сожалению не так просто. Входные параметры кодируются броузером побайтно в соответствии с MIME-типом "application/x-www-form-urlencoded". Как рассказал Алексей Менделев русские буквы броузеры кодируют, используя текущую установленную кодировку. Ну и, разумеется, ничего о ней не сообщают. Соотвественно, например, в JSDK 2.0 и 2.1 это никак не проверяется. Собственно для раскодирования используются методы HttpUtils.parsePostData() и HttpUtils.parseQueryString(), которые просто обнуляют старший байт. Это зарегистрированная ошибка в JSDK (4154966). К сожалению, эту ошибку закрыли как "Will not be fixed", с тем оправданием, что, дескать, раз в RFC на эту тему ничего не сказанно, то и делать мы ничего не будем. Однако, после переписки наших разработчиков в майл-листе SERVLET-INTEREST дело, похоже, сдвинулось с мёртвой точки. По крайней мере на словах было обещано включить метод установки кодировки в спецификацию JSDK 2.3.
Пока же приходится обходиться своими средствами. Оригинальный способ работы с кодировками предлагает Russian Apache - здесь расписано, как именно. Судя по отзывам, не имеет проблем с русскими и система Resin.
Своё решение проблемы так же предложил Вячеслав Педак.
Ну а самый простейший вариант извлечь таки символы - передавать в комплекте параметров имя кодировки (или, если вы уверены в текущей кодировке броузера, использовать предопределённую кодировку) и использовать метод перекодировки символов:
public void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException < // Кодировка сообщений // В связке MSIE 4.01 SP1 ->JSDK 2.0 servletrunner.exe всегда выдаёт "ISO-8859-1" String requestEnc = request.getCharacterEncoding(); // Некоторые servlet engine, не мудрствуя лукаво, возвращают null if( requestEnc==null ) requestEnc="ISO-8859-1"; String clientEnc = request.getParameter("charset"); if( clientEnc==null ) clientEnc="Cp1251"; String value = new String(request.getParameter("value").getBytes(requestEnc),clientEnc);
В общем, опыт в написании Servlet-ов у меня небольшой, так что Ваши замечания будут приветствоваться.
GUI (AWT, Swing)
Многие связывают неправильный вывод русских букв с неправильной установкой шрифта. Мне кажется, это связанно с тяжким опытом программирования на Windows 3.x, где основная причина действительно была в этом. В Java всё сложнее и редко действительно связанно со шрифтами. Я не разбирался со специфическими настройками броузеров, т.к. ещё не писал апплетов, только приложения, но думаю в последних версиях в этом плане всё нормально.
Где же действительно лежат наибольшие подводные камни? В основном это связанно с неправильной перекодировкой символов. Часть этих проблем и методы их решения описаны выше. Если у Вас все преобразования выполняются корректно, и для вывода используется шрифт Unicode, то есть очень большой шанс, что Ваша программа будет работать правильно.
Если проблемы всё же остались, тут нужно выяснить, где они возникают. Попробуйте запустить приложение под разными JVM, под разными платформами, на разных броузерах.
Если программа не работает нигде - значит проблема только в ней и в ваших руках. Внимательно перечитайте всё, что было написано выше, и ищите. Если же проблема проявляется только в конкретном окружении - значит дело, скорей всего в настройках. Где именно - зависит от того, какой графической библиотекой Вы пользуетесь. Если AWT - помочь может правильная настройка файла font.properties.ru. Пример корректного файла можно взять из Java 2. Если у Вас нет этой версии, можете скачать его с данного сайта: версия для Windows, версия для Linux. Если у Вас установлена русская версия OS - просто добавьте этот файл туда, где лежит файл font.properties. Если же это англицкая версия, то нужно, или дополнительно сменить текущий язык при помощи задания настройки -Duser.language=ru или переписать этот файл вместо font.properties. Этот файл задаёт используемые шрифты и кодовые страницы.
С библиотекой Swing всё проще - в ней всё рисуется через подсистему Java2D. Надписи в стандартных диалогах (JOptionPane, JFileChooser, JColorChooser) переделать на русский очень просто - достаточно лишь создать несколько файлов ресурсов. Я это уже проделал, так что можете просто взять готовый файл и добавить его в свой CLASSPATH. Единственная проблема, с которой я столкнулся - в версиях JDK начиная с 1.2 rc1 и по 1.3 beta, русские буквы не выводятся под Win9x при использовании стандартных шрифтов (Arial, Courier New, Times New Roman, etc.) из-за ошибки в Java2D. Ошибка весьма своеобразна - со стандартными шрифтами изображения букв отображаются не в соответствии с кодами Unicode, а по таблице Cp1251 (кодировка Ansi). Эта ошибка зарегистрирована в BugParade под номером 4192443. По умолчанию в Swing используются шрифты, задаваемые в файле font.properties.ru, так что достаточно заменить их другими - и русские буквы появляются. К сожалению, набор рабочих шрифтов небольшой - это шрифты Tahoma, Tahoma Bold и два набора шрифтов из дистрибутива JDK - Lucida Sans * и Lucida Typewriter * (пример файла font.properties.ru). Чем эти шрифты отличаются от стандартных - мне непонятно.
Начиная с версии 1.3rc1 эта проблема уже исправлена, так что можно просто обновить JDK. Так же надо учесть, что с оригинальной версией Win95 поставляются шрифты, не поддерживающие Unicode - в этой ситуации можно просто скопировать шрифты из Win98 или WinNT.
Если же вам, кровь из носу, нужно использовать стандартные шрифты, работая в JDK 1.2, то можно компенсировать этот глюк, перекодировав строки текста непосредственно перед выводом. Сделать это можно, например, так:
public static String convertToWin9x(String s) < byte[] bb; try < bb = s.getBytes("Cp1251"); >catch( java.io.UnsupportedEncodingException e ) < return s; >char[] cb = new char[bb.length]; for(int i=0; i < bb.length; i++) < cb[i] = (char)( bb[i] & 0x00FF ); >return new String(cb); >
Но только не забудьте - этот код будет работать только под Win9x и Sun JDK/JRE 1.2.
По поводу компилятора jikes. Как мне рассказали в конференции по Java (fido7.ru.java) при использовании этого компилятора русские буквы тоже появляются. Это на самом деле классический пример того, как один глюк компенсирует другой - jikes просто не учитывает кодировку исходников. Того же эффекта можно добиться, если указать javac кодировку ISO-8859-1 (Latin1) в ключике -encoding. Если при этом в исходниках русские символы записаны в кодировке Cp1251, то тем самым они вместо диапазона 0x400-0x4ff (стандартный диапазон Unicode для кириллицы) попадают в диапазон 0x80-0xff. Из-за вышеупомянутого глюка в среде Win9x кириллица в стандартных шрифтах отображается как раз в этом диапазоне и русские буквы появляются. Если же попробовать запустить программу в другой среде (например, в WinNT) - русских букв не будет, так как там этот глюк отсутствует.
Аналогично на подобную компенсацию можно нарваться, если поменять региональные настройки с русских на буржуйские. При этом, кроме всего прочего, меняется и кодировка по умолчанию (file.encoding) - вместо 1251 становится 1252. Это приводит к тому, что, если при чтении файлов кодировка не была явно указана (и при компиляции не задавался ключик -encoding), то русские буквы переезжают в диапазон 0x80-0xff и создаётся впечатление нормальной работы. Разницу можно заметить на преобразованиях регистра и сортировках через java.text.Collator - они будут выполняться неверно. А если были использованы строковые константы - то на других платформах вы увидите только кракозяблы.
Ещё один способ - скачать версию Swing для JDK 1.1 и запускать приложение из под Microsoft JVM - там всё выводится корректно. Только не забудьте обновить MS JVM - те версии, что идут в комплекте с IE 4.x не совсем корректно работают. С сервера Microsoft можно скачать свежую версию, например 5.00.3240 - с ней всё ОК.
Кстати, по поводу MS JVM. Непонятно по каким соображениям, но в ней отсутствуют все файлы кодировок русских букв, акромя Cp1251 (наверное, они таким образом пытались уменьшить размер дистрибутива). Если Вам нужны другие кодировки, например, Cp866, то нужно добавить соответствующие классы в CLASSPATH. Причём классы от последних версий Sun JDK не подходят - у Sun-а уже давно изменилась их структура, поэтому последние версии классов с Microsoft-ом не стыкуются (у MS осталась структура от JDK 1.1.4). На сервере Microsoft, в принципе, лежит полный комплект дополнительных кодировок (страница "Miscellaneous Resources", ссылка "Additional I/O libraries"), но там файл размером около 3 метров, а их сервер докачку не поддерживает :-). Мне удалось таки выкачать этот файл, я его перепаковал jar-ом, можете взять его отсюда.
I18n (вывод чисел, дат и т.п.)
Загадочная комбинация i18n расшифровывается просто - это сокращение от могучего слова Internationalization. 18 - это кол-во букв между i и n. Означает оно, в контексте Java, возможность автоматической подстройки программы под текущий язык и специфику страны. Делается это через использование класса Locale, представляющего язык и конкретную страну, и классов, которые знают, что с этим Locale делать. Большинство этих классов находятся в пакете java.text.
Основной класс, которым пользуются все остальные, - это java.util.ResourceBundle, который позволяет загружать различные виды ресурсов. Причём имя загружаемого класса или файла properties зависит от указанного Locale (или Locale по умолчанию - если ничего не указанно).
- resfile_ru_RU.class
- resfile_ru_RU.properties
- resfile_ru.class
- resfile_ru.properties
- resfile.class
- resfile.properties
Это позволяет легко добавлять описания для новых языков и стран. Большинство классов сами заботятся обо всей этой внутренней кухне, так что Вам об этом знать часто и не нужно.
Что касается дат, то само форматирование выполняется классом DateFormat. Получить формат, уже настроенный на язык и страну можно при помощи методов getDateInstance(), getTimeInstance() и getDateTimeInstance(). В качестве аргумента можно указать одну из констант для задания необходимого стиля формата. По умолчанию будет использован предпочтительный стиль для данного Locale. Допустимые константы:
| Константа | Описание | Пример |
| SHORT | Полностью цифровой, короткий вывод | 25.01.99 или 17:40 |
| MEDIUM | Вывод средней длины | 25.01.1999 |
| LONG | Длинный вывод | 25 Январь 1999 г. или 17:23:32 |
| FULL | Вся информация Понедельник, | 25 января 1999 г. или 17:23:32 GMT+03:00 |
// Вывод пользователю текущей даты DateFormat df = DateFormat.getDateInstance(); String s = df.format(new Date()); . // Вывод текущего времени без секунд DateFormat df = DateFormat.getTimeInstance(DateFormat.SHORT); String s = df.format(new Date());
Если же вы хотите сами контролировать набор выводимых полей и их разделители, то для этого можно использовать класс SimpleDateFormat.
Аналогичным образом делается и форматирование чисел. За это отвечает класс NumberFormat. Получить форматы можно при помощи методов getInstance(), getNumberInstance(), getCurrencyInstance() и getPercentInstance(). Свой формат можно сконструировать при помощи класса DecimalFormat.
Об утилите native2ascii
Эта утилита входит в состав Sun JDK и предназначена для преобразования исходных текстов к ASCII-виду. Эта утилита, при запуске без параметров, работает со стандартным входом (stdin) а не выводит подсказку по ключам, как остальные утилиты. Это приводит к тому, что многие и не догадываются о необходимости указания параметров (кроме, может быть, тех, кто нашёл в себе силы и мужество заглянуть таки в документацию :-). Между тем этой утилите для правильной работы необходимо, как минимум, указать используемую кодировку (ключик -encoding). Если этого не сделать, то будет использована кодировка по умолчанию (file.encoding), что может несколько расходится с ожидаемой. В результате, получив неверные кода букв (из-за неверной кодировки) можно потратить весьма много времени на поиск ошибок в абсолютно верном коде.
О методе перекодировки символов
Этот метод многие используют неправильно, наверное, не совсем понимая его суть и ограничения. Он предназначен для восстановления верных кодов букв, если они были неверно проинтерпретированны. Суть метода проста: из полученных неверных символов, используя соответствующую кодовую страницу, восстанавливается исходный массив байтов. Затем из этого массива байтов, используя уже корректную страницу, получаются нормальные кода символов. Пример:
String res = new String( src.getBytes("ISO-8859-1"),"Cp1251");
Проблем в использовании этого приёма может быть несколько. Например, для восстановления используется неверная страница, или же она может измениться в некоторых ситуациях. Другая проблема может быть в том, что некоторые страницы выполняют неоднозначное преобразование byte char. Смотрите, например, описание ошибки за номером 4296969.
Поэтому пользоваться этим методом стоит только в самом крайнем случае, когда уже ничто другое не помогает, и Вы чётко себе представляете, где именно происходит неверное преобразование символов.
Выполнение заданий на обработку файлов


Данная страница содержит подробное описание процесса решения типовой задачи на обработку двоичных файлов с числовой информацией, а также примеры решения задач на обработку строковых и текстовых файлов.
Двоичные файлы с числовой информацией: File48
Особенности выполнения заданий на обработку файлов рассмотрим на примере задания File48.
File48°. Даны три файла целых чисел одинакового размера с именами SA, SB, SC и строка SD. Создать новый файл с именем SD, в котором чередовались бы элементы исходных файлов с одним и тем же номером: A1, B1, C1, A2, B2, C2, . .
Создание программы-заготовки и знакомство с заданием
Напомним, что программу-заготовку для решения задания можно создать с помощью модуля PT4Load, используя ярлык PT4Load, находящийся в рабочем каталоге. В созданный проект будет входить файл File48.rb. Приведем текст функции solve из данного файла (именно в эту функцию требуется ввести решение задачи):
def solve() task "File48" end
function solve() task("File48") end
После запуска программы на экране появится окно задачника. На рисунке приведены два варианта представления окна в режиме с динамической и с фиксированной компоновкой:


В первой строке раздела исходных данных указаны имена трех исходных файлов (SA, SB и SC) и одного результирующего (SD). В последующих строках раздела исходных данных показано содержимое исходных файлов. Элементы файлов отображаются бирюзовым цветом, чтобы подчеркнуть их отличие от обычных исходных данных (желтого цвета) и комментариев (светло-серого цвета).
В режиме с фиксированной компоновкой для отображения содержимого каждого двоичного файла отводится по одной строке. Поскольку размер файлов, как правило, превышает количество элементов, которое может уместиться на одной экранной строке, в режиме с фиксированной компоновкой предусмотрена возможность прокрутки (листания) элементов файла с помощью мыши или клавиатуры.
В режиме с динамической компоновкой на экране отображаются все элементы двоичных файлов, даже если для этого требуется использовать более одной экранной строки; при этом в начале каждой строки указывается порядковый номер первого элемента файла, приведенного в данной строке (элементы нумеруются от 1).
Следует заметить, что при каждом запуске программы с учебным заданием исходные файлы создаются под новыми именами и заполняются новыми данными, а исходные и результирующие файлы, созданные при предыдущем запуске программы, удаляются с диска.
Вернемся к нашей программе, только что запущенной на выполнение. Так как в ней не указаны операторы ввода-вывода, запуск программы считается ознакомительным, проверка решения не производится, а на экране отображается пример верного решения (в нашем случае это числа, которые должны содержаться в результирующем файле при правильном решении задачи).
Ввод исходных данных
Добавим в программу фрагмент, позволяющий ввести имена файлов и связать с этими файлами соответствующие файловые переменные. Поскольку мы собираемся работать с четырьмя файлами одного типа, удобно предусмотреть массив для хранения всех файловых переменных:
def solve() task "File48" f = [] for i in 0..2 f.push(File.open(get_s, "rb")) end # f.each <|e| e.close>end
function solve() task("File48") f = Vector(undef, 3) for i in 1:length(f) f[i] = open(get(), "r") end # for e in f close(e) end end
Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным имя результирующего файла. Прочитанное имя файла сразу передается функции open (методу класса File в случае языка Ruby), которая открывает указанный файл в требуемом режиме и возвращает его дескриптор. Для языка Ruby этот дескриптор добавляется к массиву f с помощью метода push, а для языка Julia он просто записывается в элемент массива с соответствующим индексом, поскольку при описании массива f для него уже была выделена память для хранения трех элементов (обратите внимание на то, что в языке Julia, в отличие от большинства современных языков, индексирование всех коллекций выполняется не от 0, а от 1). Режим открытия файла в виде текстовой строки указывается во втором параметре функции open. Для Ruby строка содержит две буквы: r режим чтения файла (read) и b указание на то, что файл является двоичным (бинарным, binary), для Julia особое указание бинарного режима работы с файлом не требуется. В конце программы мы добавили оператор, в котором закрываются все открытые файлы. В Ruby мы для этого воспользовались итератором each, вызвав его для массива f и передав ему блок, в котором для каждого элемента массива e указали требуемое действие (закрытие файла методом close). В Julia то же самое действие мы реализовали с помощью цикла по элементам массива f.
Заметим, что в языке Ruby функцию open можно вызывать, не указывая перед ней имя класса File, а в языке Jylia в заголовке цикла for по числовому диапазону можно вместо слова in использовать символ =, например, for i = 1:length(f) .
Комментарий # расположен в том месте программы, в котором можно выполнять операции ввода-вывода для файлов: они уже открыты функцией open и еще не закрыты функцией close.
Запуск нового варианта программы уже не будет считаться ознакомительным, поскольку в программе выполняется ввод исходных данных. Так как имя результирующего файла осталось непрочитанным, этот вариант решения будет признан неверным и приведет к сообщению «Введены не все требуемые исходные данные. Количество прочитанных данных: 3 (из 4)».
Пример программы, приводящей к ошибке времени выполнения
Изменим программу, увеличив размер обрабатываемого массива f до 4 элементов. Для этого в случае языка Ruby достаточно заменить в заголовке цикла число 2 на 3, а в случае языка Julia заменить в описании вектора f число 3 на 4. После запуска программы окно задачника будет содержать сообщение об ошибке. В случае Ruby сообщение будет иметь вид «Error Errno: ENOENT: (Errno 2) No such file or directory: 'dp1u.tst'», в случае Julia «Error SystemError: opening file "dp1u.tst": No such file or directory» (имя файла, разумеется, может отличаться от приведенного).
Ошибка произошла из-за того, что на четвертой, последней итерации цикла, программа попыталась открыть в режиме чтения файл с именем dp1u.tst, который отсутствует на диске (это именно тот файл, который должна создать наша программа).
Создание пустого результирующего файла
Для того чтобы избежать ошибки времени выполнения, отсутствующий файл результатов следует создать, после чего открыть в режиме записи (w). Для Ruby по-прежнему надо указывать и символ b, поскольку создаваемый файл также должен обрабатываться как двоичный. Добавим в программу тернарный оператор, определяющий по номеру параметра цикла i режим открытия файла:
def solve() task "File48" f = [] for i in 0..3 f.push(File.open(get_s, i
function solve() task("File48") f = Vector(undef, 3) for i in 1:length(f) f[i] = open(get(), i < 4 ? "r" : "w") end # for e in f close(e) end end
Запуск нового варианта программы не приведет к ошибке времени выполнения; более того, результирующий файл будет создан. Однако созданный файл останется пустым, т. е. не содержащим ни одного элемента. Начиная с версии 4.15, в этом случае на информационной панели выводится сообщение (на светло-синем фоне): «Запуск с правильным вводом данных: все требуемые данные введены, результирующий файл является пустым» (в предыдущих версиях в этой ситуации на информационной панели выводилось сообщение «Ошибочное решение», а в строке, которая должна содержать элементы результирующего файла, отображался текст EOF «конец файла», End Of File).
Таким образом, нам осталось реализовать фрагмент алгоритма, обеспечивающий ввод и вывод файловых данных.
Чтение и запись данных из двоичных файлов
Вначале обсудим действия для языка Ruby. Поскольку мы собираемся читать двоичные данные, нам необходимо использовать функцию read с параметром целым числом, определяющим, какое число байт следует прочесть из исходного файла (в отличие от большинства языков программирования, в Ruby не предусмотрено возможности непосредственного чтения числовых данных из двоичных файлов). Поскольку одно целое число в двоичном файле кодируется четырьмя байтами, нам необходимо использовать функцию read с параметром 4. Результат, возвращенный этой функцией (двоичная строка), можно сразу передавать в качестве параметра функции write для записи в создаваемый двоичный файл.
Таким образом, для считывания одного целого числа из каждого исходного файла и записи его в результирующий файл, нам достаточно добавить в раздел программы, помеченный комментарием #, следующий фрагмент:
for i in 0..2 f[3].write(f[i].read(4)) end
Язык Julia содержит средства, позволяющие считывать из двоичных файлов числовые данные требуемого типа. В нашем случае надо использовать тип Int32, так как во всех заданиях на обработку файлов целых чисел предполагается, что числа имеют размер 4 байта (32 бита):
for i in 1:3 write(f[4], read(f[i], Int32)) end
Запустив исправленную программу, мы получим все еще неверный, но ожидаемый результат: созданный файл будет содержать три элемента, совпадающих с начальными элементами исходных файлов.
Правильное решение, его тестирование и просмотр результатов
Для получения правильного решения нам необходимо повторить несколько раз приведенный ранее фрагмент программы, обеспечивающий считывание одного числа из каждого исходного файла и его запись в результирующий файл. В языке Ruby для определения размера файла в байтах достаточно воспользоваться функцией size класса File, передав ей в качестве параметра имя файла (имя, в свою очередь, можно получить из дескриптора файла, вызвав для него метод path). В языке Julia для тех же целей предусмотрена функция filesize, параметром которой может быть сам дескриптор файла. При организации цикла необходимо учесть, что количество итераций должно быть равно len / 4, где len размер файла в байтах (поскольку каждый элемент файла занимает 4 байта). Получаем один из вариантов правильного решения:
def solve() task "File48" f = [] for i in 0..3 f.push(File.open(get_s, i < 3 ? "rb" : "wb")) end len = File.size f[0].path (len / 4).times do for i in 0..2 f[3].write(f[i].read(4)) end end f.each <|e| e.close>end
function solve() task("File48") f = Vector(undef, 4) for i in 1:length(f) f[i] = open(get(), i < 4 ? "r" : "w") end for j in 1:filesize(f[1]) ÷ 4 for i in 1:3 write(f[4], read(f[i], Int32)) end end for e in f close(e) end end
Следует обратить внимание на то, что в языке Julia для операции деления нацело используется особый символ ÷, отсутствующий на клавиатуре. Для его ввода в программу надо набрать текст \div и сразу после этого нажать клавишу Tab. Заметим, что текст \div соответствует символу ÷ в системе LaTeX, предназначенной для подготовки математических текстов. Аналогичными действиями в редакторе VS Code можно вводить в программу на языке Julia и другие специальные символы.
В данном варианте решения мы учли, что по условию задачи все исходные файлы имеют одинаковый размер. После запуска этого варианта программы и успешного прохождения 5 тестов мы получим сообщение «Задание выполнено!». Нажав клавишу [F2], мы можем вывести на экран окно результатов, в котором будут перечислены все наши попытки решения задачи:
File48 r07/09 11:19 Ознакомительный запуск. File48 r07/09 11:22 Введены не все требуемые исходные данные. File48 r07/09 11:24 Error Errno::ENOENT. File48 r07/09 11:26 Запуск с правильным вводом данных. File48 r07/09 11:28 Ошибочное решение. File48 r07/09 12:33 Задание выполнено!
Символ «r» означает, что программа выполнялась на языке Ruby. Для языка Julia используется символ «u» (поскольку символ «j» зарезервирован для языка Java). При выходе из среды NetBeans можно убедиться в том, что из рабочего каталога удалены все исходные и результирующие файлы, которые создавались и обрабатывались при выполнении задания.
Примечание. Задачу можно решить и не используя функцию File.size (для языка Ruby) или функцию filesize (для языка Julia), если воспользоваться полезной функцией, возвращающей значение true, если достигнут конец файла (англ. End of File, EOF). Такая функция имеется и в языке Ruby, и в языке Julia. В языке Ruby она имеет имя eof? и вызывается как метод файлового дескриптора f: f.eof? . В языке Julia это обычная функция eof(f) .
def solve() task "File48" f = [] for i in 0..3 f.push(File.open(get_s, i < 3 ? "rb" : "wb")) end while not f[0].eof? do for i in 0..2 f[3].write(f[i].read(4)) end end f.each <|e| e.close>end
function solve() task("File48") f = Vector(undef, 4) for i in 1:length(f) f[i] = open(get(), i < 4 ? "r" : "w") end while !eof(f[1]) for i in 1:3 write(f[4], read(f[i], Int32)) end end for e in f close(e) end end
Заметим, что вместо конструкции while not в языке Ruby предпочтительнее использовать более кратний синоним until .
Получение числовых значений из двоичных файлов, преобразование файла: File25
В предыдущем пункте нам удалось выполнить задание на языке Ruby, не «расшифровывая» содержимое двоичных файлов: нам было достаточно знать, что размер каждого элемента файла равен 4 байтам. Однако во многих ситуациях приходится обрабатывать числовые значения, полученные из двоичных файлов, поэтому необходимо уметь «раскодировать» двоичные числовые форматы, в которых хранятся числовые данные в двоичных файлах, а также «кодировать» числа для последующей записи их в двоичные файлы.
Для кодирования/декодирования двоичной информации в языке Ruby предусмотрены специальные методы pack(fmt) и unpack(fmt). Метод unpack определен для двоичных строк (возвращаемых, в частности, методом read); он преобразует двоичную строку в массив данных в соответствии с форматом fmt, указанным в качестве параметра метода. Для 4-байтного знакового числа целого типа предусмотрен формат "i" , а для 8-байтного вещественного числа формат "d" (от слова double вещественное число двойной точности). Если строка содержит несколько двоичных числовых данных, то в качестве формата можно указать строку вида "i*" или "d*" . Метод pack определен для массивов и обеспечивает преобразование их данных в двоичную строку с учетом указанного формата. Для этого метода также можно использовать форматы "i" , "i*" "d" и "d*" , которые имеют тот же смысл, что и для метода unpack. Следует учитывать, что метод pack позволяет кодировать произвольное количество данных указанного формата (представленных в виде массива), возвращая строку, подготовленную к записи в двоичный файл, а метод unpack всегда обрабатывает единственную двоичную строку, однако возвращает массив декодированных значений, поскольку допускается, чтобы двоичная строка содержала несколько закодированных данных указанного формата.
Познакомимся с использованием этих функций, выполняя задание File25 первое из заданий группы File, связанное с преобразованием исходного файла:
File25°. Дан файл вещественных чисел. Заменить в нем все элементы на их квадраты.
Преобразовать файл можно двумя способами: либо открыть файл одновременно на чтение и запись и сразу записывать в него ранее считанные и преобразованные значения, либо воспользоваться вспомогательным файлом (в этом случае исходный файл открывается только на чтение, а преобразованные значения записываются во вспомогательный файл). При использовании вспомогательного файла дополнительно потребуется выполнить два завершающих действия: удалить исходный файл и переименовать вспомогательный файл, присвоив ему имя исходного. Второй способ является более универсальным, поскольку может использоваться для самых разных видов преобразований файла, в том числе и таких, которые связаны с удалением или добавлением элементов. Однако в простых случаях можно обойтись без вспомогательного файла, хотя при этом придется использовать прямой доступ к элементам файла (с помощью функции seek) и учитывать ряд особенностей, связанных с одновременным доступом к файлу и на чтение, и на запись.
Вначале приведем пример решения, не использующего вспомогательный файл:
def solve() task "File25" f = File.open get_s, "r+b" until f.eof? do s = f.read 8 x = s.unpack "d" f.pos -= 8 f.write([x[0]**2].pack "d") end f.close end
В данной программе исходный двоичный файл открывается одновременно и на чтение, и на запись; это обеспечивается указанием специального режима открытия файла "r+b" . Здесь r, как обычно, означает доступ на чтение (в частности, это значит, что файл должен существовать), а символ + «позволяет» выполнять для данного файла и операции записи. Символ b по-прежнему указывает на то, что файл является двоичным.
Для перебора всех элементов файла используется цикл while, который завершится в тот момент, когда после обработки очередного набора данных будет обнаружен конец файла. Набор байт, считанный из файла на каждой итерации, распаковывается функцией unpack. Ее результатом является массив x, состоящий из единственного элемента x[0] (поскольку мы считываем данные порциями по 8 байт). Перед записью числа x[0]**2 (т. е. исходного значения, возведенного в квадрат), необходимо выполнить два действия. Во-первых, надо «вернуться» в файле на 8 байт назад, чтобы квадрат числа был записан поверх его исходного значения (если этого не сделать, то квадрат будет записан поверх следующего файлового элемента). Это действие выполняется с помощью свойства pos файлового дескриптора, которое доступно для чтения и для записи и содержит текущую позицию файлового указателя в байтах, отсчитанных от начала файла. Во-вторых, необходимо перевести полученное число в кодирующий его набор байт; это действие выполняется методом pack, который применяется к массиву, состоящему из единственного элемента.
Заметим, что вместо свойства pos для перехода к требуемой файловой позиции можно также использовать метод seek c двумя параметрами: первый задает номер n новой позиции, а второй определяет точку отсчета и может принимать три значения: IO::SEEK_SET (отсчет от начала файла, номер n должен быть неотрицательным), IO::SEEK_END (отсчет от конца файла, номер n должен быть неположительным), IO::SEEK_CUR (отсчет от текущей позиции файла).
Аналогичным образом можно решить задачу и на языке Julia. Решение будет более простым, так как в этом языке мы можем сразу считывать данные из двоичных файлов в числовые переменные (в данном случае необходимо использовать вещественный тип Float64). Функция seek в языке Julia позволяет указывать позицию только от начала файла, однако имеются также функции seekstart(f) и seekend(f), перемещающие файловый указатель на начало и конец файла, и функция skip(f, offset), перемещающая файловый указатель на offset байт относительно его текущей позиции. Есть в этом языке и функция position(f), которая возвращает текущую позицию файлового указателя.
function solve() task("File25") f = open(get(), "r+") while !eof(f) x = read(f, Float64) skip(f, -8) write(f, x^2) end close(f) end
Теперь приведем второй вариант решения задачи, использующий вспомогательный файл. Для языка Ruby в этом варианте по-прежнему будут использоваться методы pack и unpack, однако файлы будут открываться либо только на чтение, либо только на запись, и поэтому не будет необходимости в прямом доступе к элементам файла.
task "File25" f = File.open get_s, "rb" f1 = File.open "f25.tmp", "wb" until f.eof? do s = f.read 8 x = s.unpack "d" f1.write([x[0]**2].pack "d") end f.close f1.close File.delete f.path File.rename f1.path, f.path
В данной программе вспомогательный файл связывается с файловой переменной f1 и открывается только на запись, тогда как исходный файл открывается только на чтение. Элементы исходного файла последовательно считываются в переменную s, после чего они декодируются, возводятся в квадрат, кодируются и записываются во вспомогательный файл. В конце программы с помощью методов delete и rename класса File выполняется удаление исходного файла f (в качестве параметра функции указывается имя файла, которое остается доступным для переменной f даже после закрытия файла), а затем выполняется переименование вспомогательного файла f1.
Приведем аналогичный вариант решения для языка Julia. Поскольку в этом языке по дескриптору файла нельзя определить его имя, а функции для удаления (rm) и переименования (mv), как и в языке Ruby, требуют указания имен файлов, в программе имена файлов предварительно сохраняются во вспомогательных переменных.
function solve() task("File25") s, s1 = get(), "f25.tmp" f = open(s, "r") f1 = open(s1, "w") while !eof(f) write(f1, read(f, Float64)^2) end close(f) close(f1) rm(s) mv(s1, s) end
Строковые и текстовые файлы: File67, Text21
В данном пункте описываются особенности выполнения заданий на обработку строковых файлов (т. е. двоичных типизированных файлов, элементами которых являются строки), а также текстовых файлов, содержащих строки различной длины, оканчивающиеся маркерами конца строки.
Двоичные строковые файлы
В качестве примера задания на строковые файлы рассмотрим задание File67.
File67°. Дан строковый файл, содержащий даты в формате «день/месяц/год», причем под день и месяц отводится по две позиции, а под год четыре (например, «16/04/2001»). Создать два файла целых чисел, первый из которых содержит значения дней, а второй значения месяцев для дат из исходного строкового файла (в том же порядке).
Двоичные строковые файлы отличаются от стандартных текстовых файлов тем, что в них не используются специальные маркеры конца строки, а все строки файловые элементы имеют одинаковый размер. Это позволяет использовать прямой доступ к любому файловому элементу, а также дает возможность изменять отдельные элементы-строки, не затрагивая их соседей. Размер строки может быть выбран произвольным образом; при выполнении заданий на языке Ruby с использованием задачника Programming Taskbook элементы строковых файлов имеют размер 80 символов. Таким образом, для чтения этих элементов надо использовать функцию read(80), а перед записью элементов в такой файл необходимо обеспечить их нужный размер (80 символов), дополняя их справа требуемым числом пробелов.
Для обработки строковых файлов описанного выше формата нет необходимости выполнять дополнительную перекодировку двоичных данных. Однако в задании File67 требуется сформировать два двоичных файла с числовыми данными, для которых подобная перекодировка необходима. Поэтому в программе на языке Ruby потребуется использовать метод pack. Для выделения из строки фрагмента, изображающего целое число, надо использовать операцию среза для строки, к результату которой достаточно применить функцию to_i преобразования строки в целое число. Таким образом, решение задачи File67 примет следующий вид:
def solve() task "File67" f = File.open get_s, "rb" f1 = File.open get_s, "wb" f2 = File.open get_s, "wb" until f.eof? do s = f.read 80 f1.write([s[0,2].to_i].pack "i") f2.write([s[3,2].to_i].pack 'i') end f.close f1.close f2.close end
Решение на языке Julia выглядит аналогичным образом. Приведем его текст, а затем прокомментируем его.
function solve() task("File67") f = open(get(), "r") f1 = open(get(), "w") f2 = open(get(), "w") b = Vector(undef, 80) while !eof(f) readbytes!(f, b) s = decode(b, "UTF-8") write(f1, parse(Int32, s[1:2])) write(f2, parse(Int32, s[4:5])) end close(f) close(f1) close(f2) end
В данном случае приходится использовать функцию readbytes!, которая выполняет те же действия, что и функция read для языка Ruby, а именно, читает из файла указанное количество байт. Однако для использования этой функции необходимо заранее создать массив байт (то есть элементов типа UInt8) требуемого размера и передать его в качестве второго параметра функции readbytes! (восклицательный знак в конце ее имени указывает на то, что при ее выполнении происходит изменение некоторых ее параметров).
Кроме того, полученный массив байт необходимо преобразовать в строку. Для этого предназначена функция decode, вторым параметром которой надо указать требуемую кодировку символов. В нашем случае строка содержит только символы из таблицы ASCII, поэтому можно указать, например, кодировку "UTF-8". Однако если бы строка содержала русские буквы, то было бы необходимо указать кодировку "CP1251", так как задачник использует файлы именно в такой кодировке. Заметим, что эта функция определена в дополнительном модуле StringEncodings, который автоматически подключается к программе с решением, так как соответствующая директива имеется в файле PT.jl. Парной к функции decode является функция encode, которая преобразует текстовую строку (которая в языке Julia всегда имеет кодировку UTF-8) в набор байт, соответствующий кодировке, указанной вторым параметром функции encode. Эта функция может оказаться полезной при записи данных с двоичные строковые (а также текстовые) файлы.
Наконец, после получения требуемой строки, мы извлекаем из нее подстроки, используя срезы s[1:2] и s[4:5] (в срезах указывается начальный и конечный индекс; напомним, что в языке Julia индексирование начинается от 1), и преобразуем их в числа типа Int32, используя функцию parse.
Текстовые файлы
В качестве примера задания на текстовые файлы рассмотрим задание Text21.
Text21°. Дан текстовый файл, содержащий более трех строк. Удалить из него последние три строки.
Строки в текстовом файле имеют разную длину, и, таким образом, нельзя изменить одну из них, не «затрагивая» соседние. Поэтому преобразование текстовых файлов, как правило, выполняется с помощью вспомогательного текстового файла. Во вспомогательный файл записываются необходимые результирующие данные, после чего исходный файл удаляется с диска, а имя вспомогательного файла заменяется на имя исходного. Различная длина строк в текстовом файле также вынуждает организовывать их считывание последовательно, от первой до последней строки; при этом до завершения считывания файла невозможно определить количество содержащихся в нем строк. В языке Ruby для текстовых файлов при указании режима открытия не следует указывать символ b (признак двоичного файла). В программах на этом языке (а также на языке Julia) текстовые файлы обычно открываются в одном из трех режимов: "r" чтение, "w" запись, приводящая к полному обновлению содержимого файла, и "a" режим дополнения (append), при котором новые данные добавляются в конец существующего файла. При этом для чтения отдельной текстовой строки должна использоваться функция readline, которая для языка Ruby возвращает не только содержимое строки, но и завершающий ее маркер в виде символа "\n", а для языка Julia может как возвращать, так и отбрасывать завершающий маркер (по умолчанию маркер отбрасывается). Для контроля того, достигнут или нет конец файла, можно, как обычно, использовать метод eof? в Ruby и функцию eof в Julia.
Приведем решение задачи Text21, учитывающее отмеченные выше особенности текстовых файлов:
def solve() task "Text21" f = File.open get_s, "r" n = 0 until f.eof? do f.readline n += 1 end f.close f = File.open f.path, "r" f1 = File.open "t21.tmp", "w" (n-3).times do f1.write f.readline end f.close f1.close File.delete f.path File.rename f1.path, f.path end
function solve() task("Text21") s, s1 = get(), "t21.tmp" f = open(s, "r") n = 0 while !eof(f) s2 = readline(f) n += 1 end close(f) f = open(s, "r") f1 = open(s1, "w") for i in 1:n-3 write(f1, readline(f) * "\n") end close(f) close(f1) rm(s) mv(s1, s) end
Напомним, что операция сцепления строк в языке Julia обозначается символом *.
Этот вариант решения является неэффективным, поскольку требует двух просмотров исходного файла f: первый для определения его размера, который записывается в переменную n, второй для создания вспомогательного файла f1, содержащего все строки исходного файла, кроме трех последних.
Приведем еще один способ решения, который, хотя и требует единственного просмотра исходного файла, также является неэффективным, так как сохраняет в памяти всё содержимое файла:
def solve() task "Text21" f = File.open get_s, "r" s = f.readlines f.close f = File.open f.path, "w" for e in s[0..-4] do f.write(e) end f.close end
function solve() task("Text21") name = get() f = open(name, "r") s = readlines(f) close(f) f = open(name, "w") for e in s[1:end-3] write(f, e * "\n") end close(f) end
В этой программе используется функция readlines, считывающая все строки файла и возвращающая их в виде массива. Для получения всего массива s, за исключением последних трех элементов, проще всего воспользоваться операцией среза. В программе на языке Ruby мы использовали срез со вторым отрицательным аргументом: s[0..-4] (отрицательный аргумент означает, что требуемая позиция отсчитывается от конца списка; при этом значение 1 соответствует последнему элементу). В программе на языке Julia вместо отрицательных аргументов используется ключевое слово end, означающее индекс последнего элемента массива.
Тем не менее, задание Text21 можно выполнить и за один просмотр исходного файла, причем не сохраняя в памяти все содержимое файла, если воспользоваться следующим наблюдением: строка должна быть записана во вспомогательный файл, если после нее в исходном файле находятся по крайней мере три строки. Таким образом, записывать очередную строку во вспомогательный файл следует только после считывания из исходного файла трех следующих за ней строк. Благодаря такому упреждающему считыванию необходимость в предварительном определении размера исходного файла отпадает. Для хранения строк, которые уже считаны из исходного файла, но еще не записаны во вспомогательный файл, удобно использовать список из трех элементов.
Приведем программу, реализующую описанный выше эффективный однопроходный алгоритм решения задачи:
def solve() task "Text21" f = File.open get_s, "r" f1 = File.open "t21.tmp", "w" a = [] 3.times until f.eof? f1.write a.shift a.push f.readline end f.close f1.close File.delete f.path File.rename f1.path, f.path end
function solve() task("Text21") s, s1 = get(), "t21.tmp" f = open(s, "r") f1 = open(s1, "w") a = [] for i in 1:3 push!(a, readline(f)) end while !eof(f) write(f1, popfirst!(a) * "\n") push!(a, readline(f)) end close(f) close(f1) rm(s) mv(s1, s) end
Прокомментируем полученную программу. Вначале список a заполняется первыми тремя строками из исходного файла (по условию файл содержит не менее трех строк). На каждой итерации цикла while во вспомогательный файл записывается первая строка из списка а (в этот момент уже известно, что после данной строки в исходном файле содержатся, по крайней мере, три строки: две из этих строк уже хранятся в списке, а непустая третья строка является текущей строкой файла). Записанная строка немедленно удаляется из массива (это действие обеспечивается в языке Ruby методом shift, а в языке Julia функцией popfirst!). Затем в конец списка добавляется очередная строка из исходного файла (это действие, как и добавление трех начальных строк, обеспечивается методом push для языка Ruby и функцией push! для языка Julia).
После завершения цикла while во вспомогательный файл будут записаны все строки исходного файла, кроме последних трех, а эти три последние строки будут содержаться в списке a.
Примечание. Несмотря на то что приведенные программы правильно решают поставленную задачу, они имеют один недостаток, который проявляется, если обрабатываемый файл содержит текст на русском языке. Этот недостаток обусловлен тем, что исходный файл имеет однобайтную кодировку Windows-1251, тогда как по умолчанию в языках Ruby и Julia предполагается, что содержимое обрабатываемых файлов имеет кодировку UTF-8. Поэтому при считывании данных из файлов в другой кодировке мы получим строки с неверным, с точки зрения программы, содержимым (хотя при их записи в другой файл с той же кодировкой ничего плохого не произойдет, что мы и увидели при выполнении задания). Проблемы возникнут, если мы захотим обработать прочитанные строки в самой программе. Например, если просто попытаться вывести строку с русским текстом, прочитанную из файла с кодировкой Windows-1251, в раздел отладки (используя функцию show), то и в программе на языке Ruby, и в программе на языке Julia мы получим текст из вопросительных знаков, означающий, что данную строку нельзя интерпретировать как строку в кодировке UTF-8.
Для решения отмеченной проблемы в языке Ruby достаточно указать правильную кодировку при открытии файла; это делается путем добавления названия кодировки к строке, определяющей режим доступа, например, "r:CP1251" или "w:UTF-8" (хотя кодировку UTF-8 указывать не обязательно, так как она используется по умолчанию). В результате при считывании строки из файла (или ее записи в файл) будет автоматически выполняться ее перекодировка, благодаря чему строка, полученная в программе, всегда будет иметь кодировку UTF-8.
В языке Julia, к сожалению, отсутствуют столь же простые средства для работы с файлами в различных кодировках. Однако требуемую перекодировку можно выполнить уже после считывания строки из файла (или непосредственно перед записью строки в файл), используя уже упоминавшиеся ранее функции decode и encode из модуля StringEncodings. Например, для получения из файла f1 с однобайтной кодировкой cp1251 очередной строки и ее немедленного преобразования в строку s2, имеющую кодировку UTF-8, достаточно выполнить оператор s2 = decode(Vector(readline(f1)), "CP1251") , а для выполнения обратного действия, т. е. записи в файл f1 с однобайтной кодировкой cp1251 строки s2, содержащей текст в кодировке UTF-8, достаточно выполнить оператор write(f1, encode(s2 * "\n", "CP1251")) .


Разработка сайта:
М. Э. Абрамян, В. Н. Брагилевский