شکسپیر تعدادی نمایشنامه و ۱۵۴ شعر (Sonnet) نوشته است که میتوانید آنها را اینجا در وبسایت گوتنبرگ پیدا کنید. در ادامه اولین شعر آمده است:
From fairest creatures we desire increase,
That thereby beauty's rose might never die,
But as the riper should by time decease,
His tender heir might bear his memory:
But thou, contracted to thine own bright eyes,
Feed'st thy light's flame with self-substantial fuel,
Making a famine where abundance lies,
Thy self thy foe, to thy sweet self too cruel:
Thou that art now the world's fresh ornament,
And only herald to the gaudy spring,
Within thine own bud buriest thy content,
And tender churl mak'st waste in niggarding:
Pity the world, or else this glutton be,
To eat the world's due, by the grave and thee.
این سناریو شامل ایجاد یک فایل برای ذخیرهی تمام شعرها بهصورت فشرده است. قالب فایلی که باید ایجاد کنید به شکل زیر است:
[دیاگرام: قالب فایل شعرهای شکسپیر]این قالب از چندین بخش تشکیل شده است:
این قالب فایل متنها را بهصورت فشرده و اعداد را بهصورت عدد صحیح ذخیره میکند. برای پیادهسازی آن به چندین بخش از رابط Java I/O نیاز دارید که میتوانید با استفاده از الگوی Decorator ترکیب کنید.
دو روش برای خواندن این فایل متنی وجود دارد. میتوانید آن را دانلود و روی ماشین خود ذخیره کنید. یا میتوانید کدی بنویسید که فایل را مستقیماً آنلاین بخواند (که البته نیاز به اتصال اینترنت دارد).
در ادامه کد خواندن آنلاین آمده است. این کد بر اساس HttpClient API ساخته شده و یک InputStream تولید میکند که در بخش بعدی آن را به یک Reader تبدیل خواهید کرد.
URI sonnetsURI = URI.create("https://www.gutenberg.org/cache/epub/1041/pg1041.txt");
HttpRequest request =
HttpRequest.newBuilder(sonnetsURI)
.GET()
.build();
HttpClient client =
HttpClient.newBuilder().build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
InputStream inputStream = response.body();
و در ادامه کد خواندن از فایل محلی با استفاده از کلاس فابریکی Files آمده است:
Path path = Path.of("files/sonnets.txt");
BufferedReader reader = Files.newBufferedReader(path);
نکته: هیچکدام از این دو قطعه کد کامل نیستند: بخش مدیریت Exception و بستن منابع در آنها وجود ندارد.
اول باید فایل متنی موجود در وبسایت گوتنبرگ را بخوانید و تحلیل کنید و متن شعرها را استخراج کنید.
متن شعرها از خط ۳۳ فایل متنی شروع میشود. ساختار فایل به صورت زیر است:
زمانی که به خط زیر برسید، یعنی به انتهای کتاب الکترونیکی رسیدهاید:
*** END OF THE PROJECT GUTENBERG EBOOK
برای حل این مشکل میتوانید کلاس BufferedReader را گسترش دهید، ویژگیهای آن را حفظ کنید و امکانات خود را اضافه کنید. سه ویژگی مختص این مشکل وجود دارد:
برای خواندن شعرها میتوانید کد زیر را بنویسید. دو بخش در آن وجود ندارد: کلاس SonnetReader و کلاس Sonnet. متغیر inputStream از مرحلهی خواندن فایل متنی یا URL با استفاده از کد HttpClient تأمین میشود.
int start = 33;
List<Sonnet> sonnets = new ArrayList<>();
try (var reader = new SonnetReader(inputStream);) {
reader.skipLines(start);
Sonnet sonnet = reader.readNextSonnet();
while (sonnet != null) {
sonnets.add(sonnet);
sonnet = reader.readNextSonnet();
}
} catch (IOException e) {
e.printStackTrace();
}
IO.println("# sonnets = " + sonnets.size());
کلاس SonnetReader از کلاس BufferedReader مشتق شده و میتواند از هر Reader یا InputStream ساخته شود. نمونهی کد این کلاس به صورت زیر است:
class SonnetReader extends BufferedReader {
public SonnetReader(Reader reader) {
super(reader);
}
public SonnetReader(InputStream inputStream) {
this(new InputStreamReader(inputStream));
}
public void skipLines(int lines) throws IOException {
for (int i = 0; i < lines; i++) {
readLine();
}
}
private String skipSonnetHeader() throws IOException {
String line = readLine();
while (line.isBlank()) {
line = readLine();
}
if (line.startsWith("*** END OF THE PROJECT GUTENBERG EBOOK")) {
return null;
}
line = readLine();
while (line.isBlank()) {
line = readLine();
}
return line;
}
public Sonnet readNextSonnet() throws IOException {
String line = skipSonnetHeader();
if (line == null) {
return null;
} else {
var sonnet = new Sonnet();
while (!line.isBlank()) {
sonnet.add(line);
line = readLine();
}
return sonnet;
}
}
}
اجرای این کد باید خروجی زیر را در کنسول نمایش دهد:
# sonnets = 154
متد skipLines() برای رد شدن از هدر فایل استفاده میشود که حاوی اطلاعات فنی و حقوقی دربارهی خود فایل است. این متد، متد readLine() تعریفشده در کلاس BufferedReader را فراخوانی میکند.
متد skipSonnetHeader() هدر هر شعر در فایل را میخواند و دور میاندازد. هر هدر شامل تعدادی خط خالی، شمارهی شعر (به اعداد رومانیایی) و تعدادی خط خالی دیگر است.
متد readNextSonnet() متن شعر را میخواند. در متن شعر خط خالی وجود ندارد. اگر با خط خالی مواجه شدید، یعنی شعر بهطور کامل خوانده شده است.
این کلاس یک نمونه از کلاس Sonnet ایجاد میکند که به صورت زیر است:
class Sonnet {
private List<String> lines = new ArrayList<>();
public void add(String line) {
lines.add(line);
}
}
این کلاس یک بستهبندی ساده روی یک List<String> با متد سادهی add(String) است. استفاده از کلاسهای سادهای از این دست، کد شما را خواناتر و قابل نگهداریتر میکند. کار با یک نمونهی Sonnet بسیار واضحتر از کار مستقیم با List<String> است.
چون SonnetReader از کلاس BufferedReader مشتق شده، میتوان آن را در یک عبارت try-with-resources استفاده کرد. بستن این کلاس توسط try-with-resources انجام میشود و نیازی به پیادهسازی متد close() نیست. متدی که توسط try-with-resources فراخوانی میشود، همان close() کلاس BufferedReader است. البته میتوانید در صورت نیاز متد close() خودتان را هم پیادهسازی کنید. در آن صورت باید متد close() کلاس والد را فراخوانی کنید تا منابع بازشده توسط این کلاس بهدرستی بسته شوند.
بیایید با نوشتن یک شعر در یک فایل فشرده شروع کنیم.
این فایل فشرده یک فایل باینری است که با GZIP فشرده شده. خوشبختانه رابط Java I/O کلاس GZIPOutputStream را در اختیار شما قرار میدهد که فشردهسازی را مدیریت میکند. چون تمام شعرهای فشردهشده در یک فایل نوشته خواهند شد، ابتدا این Stream فشرده را در یک آرایهی بایتی ذخیره میکنیم.
متد زیر را میتوانید به کلاس Sonnet اضافه کنید:
byte[] getCompressedBytes() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (GZIPOutputStream gzos = new GZIPOutputStream(bos);
PrintWriter printWriter = new PrintWriter(gzos);) {
for (String line : lines) {
printWriter.println(line);
}
}
return bos.toByteArray();
}
این متد خطوط یک شعر را در یک ByteArrayOutputStream مینویسد که توسط یک GZIPOutputStream تزئین شده و خود آن نیز توسط یک PrintWriter تزئین شده است. این PrintWriter بسیار مفید است چون متد println() مورد نیاز را در اختیار شما قرار میدهد.
حتی اگر در این متد از منبع I/O خاصی استفاده نمیشود، استفاده از عبارت try-with-resources همچنان بسیار مفید است: بافرهای داخلی PrintWriter و GZIPOutputStream را Flush میکند و مطمئن میشود تمام بایتها به آرایه نوشته شدهاند.
نوشتن تمام شعرها شامل الحاق تمام شعرهای فشردهشده به یک آرایهی بایتی و ذخیرهی offset و length هر شعر است.
وقتی تمام این اطلاعات را دارید، نوشتن بایتها با یک BufferedOutputStream ساده و نوشتن offsetها و lengthها با یک DataOutputStream انجام میشود. پس یک بار دیگر باید با الگوی Decorator برای ساخت این Stream کار کنید.
کد زیر فایل نهایی را ایجاد میکند:
int numberOfSonnets = sonnets.size();
Path sonnetsFile = Path.of("files/sonnets.bin");
try (var sonnetFile = Files.newOutputStream(sonnetsFile);
var dos = new DataOutputStream(sonnetFile);) {
List<Integer> offsets = new ArrayList<>();
List<Integer> lengths = new ArrayList<>();
byte[] encodeSonnetsBytesArray = null;
try (ByteArrayOutputStream encodedSonnets = new ByteArrayOutputStream();) {
for (Sonnet sonnet : sonnets) {
byte[] sonnetCompressedBytes = sonnet.getCompressedBytes();
offsets.add(encodedSonnets.size());
lengths.add(sonnetCompressedBytes.length);
encodedSonnets.write(sonnetCompressedBytes);
}
dos.writeInt(numberOfSonnets);
for (int index = 0; index < numberOfSonnets; index++) {
dos.writeInt(offsets.get(index));
dos.writeInt(lengths.get(index));
}
encodeSonnetsBytesArray = encodedSonnets.toByteArray();
}
sonnetFile.write(encodeSonnetsBytesArray);
} catch (IOException e) {
e.printStackTrace();
}
بخش اول این کد از تمام شعرها عبور کرده و هر کدام را به یک آرایهی بایتی فشرده تبدیل میکند. سپس offset و length هر شعر در لیستهای عدد صحیح مربوطه ذخیره شده و بایتها به encodedSonnets از نوع ByteArrayOutputStream اضافه میشوند.
در نهایت کافی است قالب فایل را رعایت کنید:
نکته: offsetها از ابتدای آرایهی حاوی شعرهای فشردهشده محاسبه شدهاند، نه از ابتدای فایل. اگر ترجیح میدهید offsetها از ابتدای فایل باشند، کافی است مقدار 4 + 2*4*numberOfSonnets را به هر offset اضافه کنید (که اندازهی هدر فایل را نمایش میدهد).
خواندن مجدد یک شعر من شامل پیدا کردن آرایهی بایتی فشردهی صحیح در فایل و رمزگشایی آن است. خواندن در واقع به اندازهی نوشتن پیچیده نیست، چون تمام اطلاعات مورد نیاز را میتوان از فایل خواند.
بیایید با کد خواندن تعداد شعرها و برای هر شعر، offset و length شروع کنیم:
Path path = Path.of("files/sonnets.bin");
try (var file = Files.newInputStream(path);
var bis = new BufferedInputStream(file);
var dos = new DataInputStream(file);) {
int numberOfSonnets = dos.readInt();
IO.println("numberOfSonnets = " + numberOfSonnets);
List<Integer> offsets = new ArrayList<>();
List<Integer> lengths = new ArrayList<>();
for(int i = 0; i < numberOfSonnets; i++) {
offsets.add(dos.readInt());
lengths.add(dos.readInt());
}
// در این نقطه، offset و length تمام شعرها در دسترس است
}
فرض کنید میخواهید شعر شماره ۷۵ را بخوانید. باید از شعرهای قبل از آن رد شوید و تعداد بایت صحیح را بخوانید.
رد شدن از تعداد مشخصی عنصر در یک I/O Stream کمی پیچیده است. باید در نظر داشته باشید که یک Stream میتواند بسیار طولانی باشد و در حافظه نگنجد. در واقع وقتی متد skip(n) را فراخوانی میکنید، ممکن است سیستم تعداد بایت صحیح را رد نکرده باشد. کد صحیح برای رد شدن از تعداد مشخصی بایت باید تعداد بایتهای واقعاً ردشده را بررسی کند و در صورت نیاز دوباره تلاش کند.
long skip(BufferedInputStream bis, int offset) throws IOException {
long skip = 0L;
while (skip < offset) {
skip += bis.skip(offset - skip);
}
return skip;
}
همین امر دربارهی خواندن تعداد ثابتی بایت هم صادق است. ممکن است تعداد بایتهای خواندهشده توسط InputStream کمتر از درخواست شما باشد. کد شما باید این موضوع را بررسی کند و مطمئن شود تمام بایتها بهدرستی خوانده شدهاند.
byte[] readBytes(BufferedInputStream bis, int length) throws IOException {
byte[] bytes = new byte[length];
byte[] buffer = new byte[length];
int read = bis.read(buffer);
int copied = 0;
while (copied < length) {
System.arraycopy(buffer, 0, bytes, copied, read);
copied += read;
read = bis.read(buffer);
}
return bytes;
}
با این دو متد، میتوانید کد زیر را پس از خواندن offsetها و lengthها اضافه کنید:
int sonnet = 75; // شعری که میخوانید
int offset = offsets.get(sonnet - 1);
int length = lengths.get(sonnet - 1);
skip(bis, offset);
byte[] bytes = readBytes(bis, length);
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
GZIPInputStream gzbais = new GZIPInputStream(bais);
InputStreamReader isr = new InputStreamReader(gzbais);
BufferedReader reader = new BufferedReader(isr);) {
List<String> sonnetLines = reader.lines().toList();
sonnetLines.forEach(IO::println);
}
این کد بایتهای شعر فشردهشده را میخواند. سپس یک ByteArrayInputStream روی این آرایه میسازد و آن را با یک GZIPInputStream تزئین میکند تا فشردهسازی انجام شود. چون خروجی مورد نظر یک لیست از خطوط است، باید این Stream باینری را با یک Stream کاراکتری یعنی InputStreamReader تزئین کنید. میتوانید متن را از همینجا بخوانید، اما استفاده از یکی از متدهای کلاس BufferedReader که امکان خواندن خط به خط را فراهم میکند، آسانتر است.
متن این شعر که باید در کنسول نمایش داده شود به صورت زیر است:
So are you to my thoughts as food to life,
Or as sweet-season'd showers are to the ground;
And for the peace of you I hold such strife
As 'twixt a miser and his wealth is found.
Now proud as an enjoyer, and anon
Doubting the filching age will steal his treasure;
Now counting best to be with you alone,
Then better'd that the world may see my pleasure:
Sometime all full with feasting on your sight,
And by and by clean starved for a look;
Possessing or pursuing no delight,
Save what is had, or must from you be took.
Thus do I pine and surfeit day by day,
Or gluttoning on all, or all away.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/