Покорим Ruby вместе! Капля шестая

Сегодня мы с вами создадим наше первое полноценное приложение на Руби, попутно обучаясь новым методам классов String и File и регулярными выражениями.

Наше приложение: Текстовый анализатор


Собственно программа простая: она будет считывать текстовый файл, анализировать его по некоторым паттернам, считать статистику и выводить результат. Руби замечательно подходит для анализа документов и текстов с помощью регулярных выражений и методов scan и split. В этом приложении мы сконцентрируемся на простом и быстром программировании и не будем организовывать объектно-ориентированную структуру.


Основные возможности


Вот список возможностей, которые мы должны реализовать:
  • подсчет символов
  • подсчет символов без пробелов
  • подсчет строк
  • подсчет слов
  • подсчет абзацев
  • среднее число слов в предложении
  • среднее число предложений в абзаце

Реализация


В начале разработки новой программы полезно представить неоходимы ключевые шаги. Давайте выделим основные ступени:
  • Загрузить файл, содержащий текст, который мы хотим проанализировать.
  • Так как мы загружаем файл по строками, сразу будем считать их количество для необходимой статистики.
  • Вставить весь текст в одну строку и измерить ее длину для подсчета символов.
  • Временно удалить все пробелы и посчитать длину новой строки без них.
  • Разбить строку по пробелам, чтобы узнать количество слов.
  • Разбить строку по знакам препинания, чтобы посчитатать количество предложений
  • Разбить по двойным переносам строки, чтобы узнать количество абзацев.
  • Произвести подсчеты, чтобы узнать средние величины для статистики.

Создайте новый, пустой файл исходников и сохраните его как analyzer.rb.

Ищем какой-нибудь текст


Перед тем, как начать писать код, неоходимо найти кусок текста для тестов. В нашем примере мы будем использовать первую часть рассказа Оливер Твист, которую вы можете загрузить здесь. Сохраните файл под именем text.txt там же, где находится analyzer.rb

Загрузка файла и подсчет строк


Настало время для кодинга! Первый шаг — загрузка файла. Руби предоставляет достаточный список методов для файловых манипуляций в классе File. Вот код, который откроет наш text.txt:

File.open("text.txt").each { |line| puts line }

Внесите код в analyzer.rb и запустите программу. В результате вы увидите бегущие по экрану строки текста.

Вы запрашиваете класс File открыть text.txt, а затем, как в случае с массивами, вызываете метод each прямо на файл, заставляя передавать каждую строку одну за другой во внутренний блок, где puts отпраляет их на экран. Отредактируйте код, чтобы он выглядел так:

line_count = 0
File.open("text.txt").each { |line| line_count += 1 }
puts line_count


Вы определяете переменную line_count, чтобы хранить в ней счетчик строк, затем окрываете файл и итерируете по строкам, увеличивая line_count на 1 каждый раз. В конце вы выводите результат на экран (около 127 в нашем примере). У нас есть первый кусочек для статистики!

Вы посчитали строки, однако у нас по-прежнему нет возможности посчитать слова, предложения, абзацы. Это легко исправить. Давайте немного изменим код и добавим переменную text, чтобы налету собрать в нее все строки:

text=''
line_count = 0
File.open("text.txt").each do |line| 
   line_count += 1
   text << line
end

puts "#{line_count} lines"


В отличие от предыдущего кода, этот представляет переменную text и добавляет в нее каждую строку. Когда итерация окончена, в text находится весь наш текст.

Казалось бы код максимально лаконичен и прост, однако в File также существуют другие методы, которые могут быть использованы в нашем случае. Например, мы можем переписать наш код так:

lines = File.readlines("text.txt")
line_count = lines.size
text = lines.join

puts "#{line_count} lines"


Намного проще! Метод readlines считывает весь файл в массив, строка за строкой.

Считаем символы


Так как мы собрали весь файл в text, мы можем использовать применимый для строк метод length, который вернет размер (количество символов) в строке, и, соответственно, во всем нашем тексте. Допишем код:

total_characters = text.length
puts "#{total_characters} characters"


Еще один элемент статистики, который нам необходим — это подсчёт символов без пробелов. Для этого воспользуемся методами замены символов. Вот пример:

puts "foobar".sub('bar', 'foo') #foofoo

Метод sub нашел набор символов, переданный в первом параметре, и заменил их на символы из второго. Однако sub находит и изменяет только одно, первое встретившееся вхождение символов, метод gsub производит все возможные замены за раз.

Регулярные выражения


А как насчет замены более сложных патернов? Для этого используются регулярные выражения (regular expression). Целые книги были написаны на эту тему, поэтому мы ограничимся лишь кратким обзором этого мощного инструмента для работы с текстом. В Руби регулярные выражения создаются с помощью заключения патерна между слэшами (/pattern/). И в Руби, естественно, это также объекты. Например, вы можете задать следующий патерн для того, чтобы выбрать строки, содержащие текст Perl или текст Python: /Perl|Python/. В слэшах заключен наш патерн, состоящий из двух необходимых нам слов, разделенных прямой чертой (пайпом, pipe, |). Это символ означает «или то, что слева, или то, что справа». Вы также можете использовать скобки, как в числовых выражениях: /P(erl|ython)/.

В патернах можно реализовать повторения: /ab+c/ (это не сложение, рассматриваем как a, затем b+, затем с). Такому патерну соответствует строка, содержащая вхождение a, затем одного или более b, и, наконец, с. Заменим плюс на звездочку, теперь /ab*c/ соответствует строка содержащая a, ноль или больше b и с. + и * — это так называемые квантификаторы, чьё назначение, я думаю, понятно

Мы также можем выбирать строки, содержащие определенные символы. Наиболее простой пример — это патерны классов символов, например, \s означает whitespace (это пробелы, табы, переносы строки и т. п.), под \d попадают все числа, и др. Вот сводка, взятая из учебника по Руби на Wikibooks:



Продолжаем считать символы


Итак, теперь мы знаем как убрать из строки все ненужные символы:

total_characters_nospaces = text.gsub(/\s+/, '').length
puts "#{total_characters_nospaces} characters excluding spaces"


Добавим этот код в конец нашего файла и переходим к подсчету слов.

Считаем слова


Для подсчета количества слов есть два подхода:
  1. Посчитать число групп непрерывных символов, используя метод scan
  2. Разбить текст по пробельным символам и посчитать получившиеся фрагменты с помощью split и size.

Пойдем по второму пути. По умолчанию (без параметров) split разобьет строку по пробельным символам и поместит фрагменты в массив. Нам остается только узнать длину массива. Дописываем код:

word_count = text.split.length
puts "#{word_count} words"


Считаем предложения и абзацы


Если нам понятно, как был реализован подсчет символов, то с предложениями и абзацами сложностей не будет. Единственное отличие в патерне, по которому мы разбиваем текст. Для предложения это точка, вопросительный и восклицательный знаки, для абзацев — двойной перенос строки. Вот код:

paragraph_count = text.split(/\n\n/).length
puts "#{paragraph_count} paragraphs"
sentence_count = text.split(/\.|\?|!/).length
puts "#{sentence_count} sentences"


Думаю, код понятен. Единственную сложность может составить патерн у предложений. Однако, он только выглядит страшно. Мы не можем просто ставить символы . и ? — мы их «экранируем» наклонной чертой.

Подсчет остальных значений


У нас уже есть количество слов, абзацев и предложений в переменных word_count,
paragraph_count и sentence_count соответственно, поэтому дальше работает только арифметика:

puts "#{sentence_count / paragraph_count} sentences per paragraph (average)"
puts "#{word_count / sentence_count} words per sentence (average)"


Исходный код


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

lines = File.readlines("text.txt")
line_count = lines.size
text = lines.join
word_count = text.split.length
character_count = text.length
character_count_nospaces = text.gsub(/\s+/, '').length
paragraph_count = text.split(/\n\n/).length
sentence_count = text.split(/\.|\?|!/).length

puts "#{line_count} lines"
puts "#{character_count} characters"
puts "#{character_count_nospaces} characters excluding spaces"
puts "#{word_count} words"
puts "#{paragraph_count} paragraphs"
puts "#{sentence_count} sentences"
puts "#{sentence_count / paragraph_count} sentences per paragraph (average)"
puts "#{word_count / sentence_count} words per sentence (average)"


Если всё накоденное выше тебе понятно — мои поздравления! Значит мы не зря «капали» ;)

Эпилог


Еще одна крупная капля. Написана более для совсем начинающих программистов, для знающих другие ЯП — это хорошая возможность сравнить способности Руби. Предлагаю и дальше периодически часто делать подобные выпуски с разбором готовых решений. Спасибо за пример Питеру Куперу! Жду отзывов и замечаний! Ждем следущую каплю ;)

_________
Текст подготовлен в ХабраРедакторе
+19
13 января 2009, 02:32
28
MaxElc 50,6

комментарии (47)

+4
bubuq #
Цикл с "File.open("text.txt").each do |line| " предпочтительнее, чем readlines, поскольку засасывать в память файл неизвестного размера не есть рекомендуемая практика.
0
MaxElc #
Ну ведь все равно потом файл надо в память вставлять
+2
gribozavr #
Нет. Можно считать количество слов в строке для каждой строки отдельно и накапливать сумму в переменной. Подсчёт символов, символов кроме пробелов, слов — точно так же. Для подсчёта абзацев нужно посчитать количество пустых строк (содержащих только \n). Подсчитать количество предложений немного сложнее. Нужно объявить переменную, в которой будет храниться количество слов предложения, конец которого ещё не найден. Дальше, я думаю, понятно.
0
MaxElc #
Понимаю :) Ну у нас тут все незатейливо так — пока лишь демонстрация возможностей. В то же время а если я гигантский файл в одну строку выстрою? Все равно она вся в память пойдет? Или я ошибаюсь?
0
tass #
не ошибаетесь;) так что лучше читать например по 32 кило;) или по какому-либо другому заданному размеру… только необходимо учитывать что мы можем остановиться на середине слова
+1
VolCh #
А лучше вообще файл на память отобразить и пускай ОС решает, как ей удобнее читать, а м ы на это дело ни байта не потратим ;)

А по существу — неужели нет в Ruby метода, возвращающего не результат замены вхождений паттерна на строку, а количество этих вхождений?

P.S. Еще паттерн для предложений, по-моему не самым лучшим способом написан, будет большое число «фальстартов»!!! Или не будет?! :)
0
VoidEx #
Не в качестве рекламы добавлю, что в ленивых языках так сделать можно, файл полностью читаться не будет.
+9
bubuq #
Вообще хотелось бы заметить автору этой серии топиков, что приводить переводы без указания авторства, с намёком на то, что это собственный труд, несколько некрасиво, если не сказать сильнее, и противоречит правилам портала. Текст должен быть оформлен как «перевод».

Практически весь текст (а не только примеры) взят из книги: Peter Cooper, «Beginning Ruby: From Novice to Professional», 2007, ISBN-10: 1-59059-7664. Мне неизвестно, существует ли русское издание этой книги, поэтому я предполагаю перевод, а не копипейст.

Усилия по адаптации провалились: остались английские примеры, а русские зачем-то набраны транслитом, понятие «параграф» осталось американским, то есть два перевода строки, в то время как русский параграф это перевод строки плюс отступ, и так далее и тому подобное, а ответ на предыдущий комментарий свидетельствует о том, что автор не понял им же написанное.

Безотносительно вышеизложенных соображений этики и приличия, публикация глав из книги по основам языка в виде серии статей представляется идеей более чем сомнительной: комменты превращаются в соревнования ищущих ошибки в тексти и замечания, как сделать лучше.
–1
MaxElc #
Да никаких намеков на собственный труд — как я уже писал с самого начала — я совершенный непрофессионал в Руби и точно так же самостоятельно учусь, учусь по этим самым книгам и просто хочу заинтересовать других в языке. Да, представьте себе, здесь в комментариях меня поправляют, находят ошибки и делают замечания, но это и есть цель, поставленная в этих статьях — этим замечателен хабр — всегда найдутся профи и помогут.

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

Вы посмотрите на блог Ruby — за пять дней, которые идет этот цикл статей, были написаны 13 топиков (7, если не считать мои). Сколько было написано за предшествующие пять дней? Одна статья… Разве это не здорово, что вернулся ко вниманию такой актуальный язык? Тем более вам, как спецу (вы тоже в руби писали, как вижу)…
0
alexbaum #
О, кстати, обзор книг по руби небольшой был бы очень кстати.
0
MaxElc #
Отправляется в todo ;)
0
mholub #
будет на днях, может сегодня вечерком, я его уже начал писать, но не закончил
0
invisiblekid #
ой спасибо… жду с нетерпением
0
Kane #
Дело ещё в том, что хочется более профессиональных материалов, а в некоторых Ващих статьях, Вы опускаетесь до самых азов программирования, которые, мне кажется, здесь не уместны. bubuq совершенно справедливо замечает, что местами Вы не понимаете сути происходящего.
0
VolCh #
Автор с самого начала написал, что программировать он не умеет и изучает это искусство на базе руби. О ходе этого изучения он и пишет тут для таких же как и он, рассчитывая, что если он ошибется где-то, или что-то неправильно поймет, то его поправят «более опытные товарищи»
0
kronos #
На безрыбье… кому надо тот докопается до истины :) а хабрадядька MaxElc дает лишь толчок.
+1
alexiznot #
всё что Вы говорите — правда, но согласен с bubuq — Вы должны были указать источник всего этого добра.
0
VolCh #
Так, между прочим, понятие «американского» параграфа — это не два перевода строки, а вертикальный отступ между параграфами, вовсе необязательно равный по высоте одной строке. Два перевода строки для обозначения конца параграфа — это всего лишь формат исходных данных. Может вообще не быть в текстовом файле строк как структурных единиц, а перевод строки будет обозначать конец абзаца, это какое понятие параграфа — китайское? Кстати, а как в «русском параграфе» определить его начало? Таб? Несколько табов? Пробел? Несколько пробелов? А может еще что?

В HTML, например, параграф обозначается тегом <p> а уж как его выводить на экран, с вертикальным отступом, или горизонтальным, или и тем, и другим (модная традиция в руской веб-типографике) решает браузер. txt файл от html в этом отношении ничем не отличается, это данные, которые программы обрабатывают (и, возможно, но необязательно, выводят пользователю) согласно некоторому предполагаемому формату данных. Одна программа воспримет два подряд переноса строки как конец параграфа, а другая как просто пустую строку, а третья вообще все повторяющиеся пробельные символы будет игнорировать и оставит только по одному пробелу.
+1
bubuq #
Всё это справедливо, и лишь демонстрирует моё утверждение о том, что комментаторы как правило, разбираются в вопросе лучше автора, что превращает статью из информативного материала в материал для битья.
0
VolCh #
Если бы я разбирался в руби, то скорее сам начал бы писать что-то вроде «практическое пособие по переходу с php на ruby» :) А так пока занимаюсь своими задачами, делаю потихоньку перевод документация к одному php-фреймворку и читаю «капли», гоняя примеры и пытаясь вносить в них небольшие изменения. Глядишь к концу цикла понадобится только беглое прочтение документации и можно приступать к работе

Формат подачи материала меня полностью устраивает, включая возможность задавать «нубские» вопросы и получать ответы на них от тех, кто действительно разбирается. Можно было бы конечно взять какую-нибудь книгу, или просто доки скачать и начать портировать какой-нибудь простой, но реальный проект, но изучать сразу два предмета в таком режиме я уже отвык, да и времени нет
0
lesa80 #
комменты превращаются в соревнования ищущих ошибки в тексте и замечания, как сделать лучше
что плохого в этом? автор пишет… если кто-то видит неточность — исправляет… в споре рождается истина…

я же говорю — лучше в отдельный блог… и не будет мельтишить на главной… а те кому интересно найдут и будут следить…
0
iv_s #
Нда, теперь понятно почему статьи с такой высокой, для новичка, периодичностью выходят.
0
iDrum #
сделайте, пожалуйста, ссылки на другие «капли», как раньше делали
0
VolCh #
Наверное стоит сделать отдельный пост (лучше в личном блоге) и собирать там «оглавление». А то постов так через дцать… полстатьи линки будут занимать :)
0
iDrum #
в этой статье есть Эпилог, что значит что она заключительная наверное :)
0
VolCh #
Эпилог есть в каждой статье этого цикла, нравится, видимо, автору это слово :)
0
lesa80 #
вот вот… я ж говорю… удобнее будет =)
+1
Nakilon #
Предложения вы считали таким регекспом:
/\.|\?|!/

А если предложения заканчиваются на троеточия или "?!"?
Тогда уж наверное лучше так:
/[\.\?!]{1,3}/
0
Kane #
Многоточие — это один знак препинания, а не три точки…
0
lesa80 #
ну это у грамотных типографов… а простые смертные ставят и 2 точки… и 3 и больше…
0
Kane #
Я, например, ставлю всегда одним символом многоточие.
0
Nakilon #
А на руби обрабатывать такие тексты пробовали?
0
Kane #
Вы имеете ввиду тексты в UTF?
0
Nakilon #
Вы, кажется, сейчас сами придумали проблему, и уже намереваетесь расказать, как она решается.
При чем тут вообще эти многобайтовые кодировки? Не об этом речь была в моем верхнем комменте.
0
Kane #
Я бы не стал делать догадок. Чем отличаются строки с многоточием от строк с тремя точками? Я не понимаю.
0
VolCh #
точки входят в ASCII, многточия нет :)
0
Kane #
В Вас я не сомневаюсь:))
Мне интересно если, Nakilon всерьёз не рассматривает эту идею, отвергая мой вопрос о кодировках, то какое у него объяснения на этот счёт…
0
Nakilon #
А предложил человеку усовершенсвовать регексп немного, не залазя в дбри кодировок, а вы зачем-то пристали ко мне со своими типографскими штучками.
Я проргаммист, автор статьи — программист. А троеточия — это куда-нить в другой блог.
0
VolCh #
Ну регэксп этот можно усовершенстовать очень долго (а потом прийти к выводу об отказе от него и разработке грамматического анализатора :) ). Например очевидный для меня недостаток — если внутри предложения будут использованы точка, вопросительный или восклицательный знак, то такое предложение посчитается за два.

В данном учебном (причем не по регэкспам) примере думаю не имеет смысла расписывать все варианты, интересны, имхо, способы использования регэкспов в руби, а не разработка самих регекспов
0
Nakilon #
В своем примере я не стал перечислять множество разных знаков (хотя это тоже было бы совершенсовованием регекспа), а добавил лишь одну фишку, т.к. она простая (короткая), но уже вносит свой плюс.
А что это за предложение такое, что внутри его вопросительный знак? ..)
Насчет вашего имхо, — согласен. Просто тема регекспов раскрыта мало, а для многих, кто сейчас эти капли читает, руби — чуть ли не первое углубление в программироваине, поэтому у многих нет опыта в регекспах, — так пусть наконец освоят ..)
0
VolCh #
Мы тут в комментах захлебнемся, если каждый будет предлагать свою фишку, а итоговое выражение станет для новичков абсолютно нечитаемое :( Я бы, например, предложил считать предложением последовательность символов максимальной длины от «большой» буквы до [.?!]+ внутри которой нет такой же последовательности. Вы бы вспомнили про то, что предложение может начинаться с цифры (я не прав? :) ) и т. д., и т. п.

А вот предыдущее предложение было примером, когда внутри встречаются и вопросительные знаки и точки :)

А регэкспы при обучении языкам (кроме перла :))) ) вообще больная тема, я даже принципа их работы не понял по описаниям в различных мануалах и гайдах, где их вскользь затрагивали, пока не прочитал толстенную книгу. Хотя то, что не понял, не мешало мне их копипастить, например из библиотеки регэкспов и даже немного модифицировать при разработке на трех языках :)
0
lesa80 #
а я неумею… вот в ворде автозаменой ставится… а там чтобы специально — нет =)
0
Kane #
Ух… Ворд вспомнили )))
Ничего тут сложного нет, вот решения:
Для Линукс — regolit.com/posts/xkb_unicode
Для Виндоус — ilyabirman.ru/typography-layout/
0
RealMonk #
Большое спасибо за наводку. Давно искал что-то подобное :)
0
MaxElc #
Угу, или так: [\.\?!]\s+
0
TiGR #
Только зачем экранировать точку и вопросительный знак внутри []?
НЛО прилетело и опубликовало эту надпись здесь

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.