Добро пожаловать в Code Safari.
В другом блоге, для которого я пишу, мне было интересно узнать статистику читабельности моего письма. Как это складывается в масштабе Флеша-Кинкейда ? Как долго были мои статьи? Как я сравнил с моим соавтором? Мне нравится такая проблема — не обязательно такая интересная, но, возможно, достижимая в достаточно короткие сроки, чтобы сделать ее стоящей. Даже без полезного результата, это идеальная тренировочная задача, чтобы развить свои навыки программирования.
Процесс состоит из двух этапов: массирование данных в подходящий формат для обработки, а затем генерирование статистики из этих данных. Я занимался подобными проблемами в прошлом, поэтому имел хорошую идею, с чего начать. Это знакомство с существующими библиотеками вашего языка (ов) — один из аргументов за попытку любого сумасшедшего эксперимента, который приходит вам в голову.
Я использую nanoc для компиляции блога, и мои исходные данные были в форме файлов Markdown с преамбулой YAML. Вот образец документа:
--- title: My Blog Post created_at: 2011-04-01 10:00 --- Indubitably I am writing a blog! puts "This is code and shouldn't be included" This is the conclusion of my fascinating blog.
В Ruby есть отличная библиотека для разбора Markdown (на самом деле у нее есть несколько!), Которая называется kramdown . Чтобы иметь возможность использовать его, я сначала должен был извлечь метаданные (заголовок, созданный в) и вырезать их из документа. Возможно, это не было слишком сложно, но зачем писать свой собственный алгоритм разбора, если кто-то другой сделал это для вас? У Nanoc должен быть какой-то код, чтобы сделать это уже…
Я распаковал исходное дерево нанокомплексов и пошел в гору. Nanoc не очень часто использует YAML, поэтому я решил, что это может быть полезно для поиска.
$ gem unpack nanoc $ cd nanoc3-3.1.3 $ ack YAML lib/nanoc3/base/ordered_hash.rb 172: YAML::quick_emit(object_id, opts) {|emitter| lib/nanoc3/base/site.rb 369: @config = DEFAULT_CONFIG.merge(YAML.load_file(config_path).symbolize_keys) lib/nanoc3/cli/commands/create_site.rb 11: # Converts the given array to YAML format lib/nanoc3/data_sources/filesystem.rb 86: meta = (meta_filename && YAML.load_file(meta_filename)) || {} 233: meta = YAML.load_file(meta_filename) || {} 255: meta = YAML.load(pieces[2]) || {} lib/nanoc3/data_sources/filesystem_unified.rb 85: io.write(YAML.dump(meta).strip + "n") lib/nanoc3/data_sources/filesystem_verbose.rb 18: # or the layout's metadata, formatted as YAML. 61: File.open(meta_filename, 'w') { |io| io.write(YAML.dump(attributes.stringify_keys)) } $
YAML.load_file(meta_filename)
кажется мне хорошим кандидатом, а имя файла ( data_sources/filesystem.rb
) еще более перспективно. Взломав файл, мы находим метод, который делает именно то, что мы хотим. Это немного долго со всей проверкой ошибок, поэтому я включу только отредактированную версию здесь:
# nanoc/lib/nanoc3/data_sources/filesystem.rb # Parses the file named `filename` and returns an array with its first # element a hash with the file's metadata, and with its second element the # file content itself. def parse(content_filename, meta_filename, kind) data = File.read(content_filename) pieces = data.split(/^(-{5}|-{3})s*$/) meta = YAML.load(pieces[2]) || {} content = pieces[4..-1].join.strip [ meta, content ] end
Я скопировал весь этот метод дословно в мой сценарий со ссылкой на его исходное местоположение на случай, если мне понадобится вернуться к нему. Для быстрого скрипта-прототипа, подобного этому, моя цель — заставить его работать как можно быстрее. Любые мысли о повторном использовании кода или архитектуре могут быть временно приостановлены.
Используя этот метод, мы можем сделать первый шаг в построении нашего парсера:
require 'kramdown' files = Dir["content/articles/*.md"] files.each do |file_name| meta, content = parse(file_name, nil, nil) doc = Kramdown::Document.new(content) puts doc.inspect end
Результат проверки, хотя и плотный, дает ценные подсказки относительно следующего шага. Мы хотим исключить любые нетекстовые элементы (например, блок кода) из нашей статистики.
<KD:Document: options={:template=>"", :auto_ids=>true, :auto_id_prefix=>"", :parse_block_html=>false, :parse_span_html=>true, :html_to_native=>false, :footnote_nr=>1, :coderay_wrap=>:div, :coderay_line_numbers=>:inline, :coderay_line_number_start=>1, :coderay_tab_width=>8, :coderay_bold_every=>10, :coderay_css=>:style, :entity_output=>:as_char, :toc_levels=>[1, 2, 3, 4, 5, 6], :line_width=>72, :latex_headers=>["section", "subsection", "subsubsection", "paragraph", "subparagraph", "subparagraph"], :smart_quotes=>["lsquo", "rsquo", "ldquo", "rdquo"]} root=<kd:root nil {:encoding=>#<Encoding:UTF-8>, :abbrev_defs=>{}} [<kd:p nil [<kd:text "Indubitably I am writing a blog!" nil>]>, <kd:blank "n" nil>, <kd:codeblock "puts "This is code and shouldn't be included"n" nil>, <kd:blank "n" nil>, <kd:p nil [<kd:text "This is the conclusion of my fascinating blog." nil>]>]> warnings=[]>
Мы видим, что kramdown создал различные типы узлов для контента, и единственные, которые нас интересуют, это kd:text
. Кажется, что все узлы находятся в древовидной структуре, происходящей от kd:root
, поэтому рекурсивной функции фильтрации должно быть достаточно для извлечения всех текстовых узлов. Вы можете проконсультироваться с документацией kramdown для точного API Document
, но вы также можете проделать долгий путь, просто угадав. root
, type
и children
являются достаточно общими именами для этого типа древовидной структуры, и это не исключение.
def extract_text(elem) value = elem.type == :text ? [elem.value] : [] value + elem.children.map {|x| extract_text(x) }.flatten end extract_text(doc.root).join(' ') # => "Indubitably I am writing a blog! This is the conclusion of my fascinating blog."
Отлично. Давайте перейдем к анализу текста.
Часть вторая
Из прошлого проекта я уже знал о библиотеке Lingua .
Lingua::EN::Readability
— это модуль Ruby, который рассчитывает статистику по английскому тексту. Он может предоставить количество слов, предложений и слогов. Он также может рассчитать несколько показателей читабельности, таких как индекс тумана и уровень Флеша-Кинкейда.
Он берет свое начало со времени, предшествующего Rubygems, и предлагает установить tar.gz
для установки. Это не так сложно, но в идеале мы бы оставались в рамках нашей системы зависимости. Со многими из этих старых проектов люди разветвляли их, чтобы правильно их упаковать или заставить работать с последними версиями Ruby. GitHub — лучшее место, чтобы найти их.
Поиск по «Lingua» дает несколько результатов, из которых победителем становится лучший. Он имеет gemspec
и некоторые исправления ошибок поверх оригинальной библиотеки. Мы можем установить его так же, как и все наши другие библиотеки Ruby.
gem install lingua
Использование тривиально, и завершает наш отчет:
require 'lingua' require 'kramdown' files = Dir["content/articles/*.md"] def parse(content_filename, meta_filename, kind) # ... from above end files.each do |file_name| meta, content = parse(file_name, nil, nil) doc = Kramdown::Document.new(content) text = extract_text(doc.root).join(" ") report = Lingua::EN::Readability.new(text) puts "%s: %.2f" % [meta['title'], report.kincaid] end # My Blog Post: 7.37
Читается вашим средним семиклассником. Не слишком потрепанный! Сам этот пост оценивается в 8,11, что, я полагаю, доступно большинству аудитории.
Завершение
Попытка небольших, полупрактичных проектов, подобных этому, является отличным способом узнать о вашей программной экосистеме и улучшить ваши алгоритмические решения. Они являются программистским эквивалентом весов музыканта. Вот некоторые дополнительные проблемы, которые вы можете попробовать для большей практики:
-
extract_text
удаляет пунктуацию, что означает, что в полученном тексте сокращенияextract_text
некорректными («I’m» становится «I m»). Это хорошо для этого анализа, но как бы вы исправили это для подачи в преобразователь текста в речь? (Подсказка, если вы на Mac: попробуйтеsay hello
в командной строке) -
created_at
по-прежнему является текстовым значением в метаданных. Преобразуйте его в соответствующий форматTime
. - Если вы ведете блог самостоятельно, попробуйте выполнить приведенный выше анализ.
Дайте нам знать, как вы идете в комментариях. Настройтесь на следующую неделю для более захватывающих приключений в джунглях кода.