Статьи

Code Safari: лингвистический анализ с Lingua

Добро пожаловать в 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 &nbsp; 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"] &nbsp; def parse(content_filename, meta_filename, kind) # ... from above end &nbsp; 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) &nbsp; puts "%s: %.2f" % [meta['title'], report.kincaid] end &nbsp; # My Blog Post: 7.37 

Читается вашим средним семиклассником. Не слишком потрепанный! Сам этот пост оценивается в 8,11, что, я полагаю, доступно большинству аудитории.

Завершение

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

  • extract_text удаляет пунктуацию, что означает, что в полученном тексте сокращения extract_text некорректными («I’m» становится «I m»). Это хорошо для этого анализа, но как бы вы исправили это для подачи в преобразователь текста в речь? (Подсказка, если вы на Mac: попробуйте say hello в командной строке)
  • created_at по-прежнему является текстовым значением в метаданных. Преобразуйте его в соответствующий формат Time .
  • Если вы ведете блог самостоятельно, попробуйте выполнить приведенный выше анализ.

Дайте нам знать, как вы идете в комментариях. Настройтесь на следующую неделю для более захватывающих приключений в джунглях кода.