Анализ английского текста с чашкой кофе «JavaSE8» +4



От автора


«Куда только не заведёт любопытство» — именно с этих слов и началась эта история.

Дело обстояло так.

Вернулся я из командировки из США, где провел целый месяц своей жизни. Готовился я Вам скажу я к ней основательно и прилично так налегал на английский, но вот не задача, приехав к заморским друзьям я понял что совершенно их не понимаю. Моему огорчению не было предела. Первым делом по приезду я встретился с другом, который свободно говорит по английски, излил ему душу и услышал в ответ: «… ты просто не те слова учил, нужно учить самые популярные… запас слов, который используется в повседневных разговорах не более 1000 слов...»

Хм, так ли это?, возник вопрос в моей голове… И пришла мне в голову идея проанализировать разговорный текст, так сказать, определить те самые употребляемые слова.

Исходные данные


В качестве разговорного текста я решил взять сценарий одной из серий сериала друзья, заодно и проверим гипотезу — «… если смотреть сериалы на английском, то хорошо подтянешь язык ...» (сценарий без особого труда можно найти в интернете)

Используемые технологии


  • Java SE 8
  • Eclipse Mars 2

Ожидаемый результат


Результатом нашего творчества станет jar библиотека, которая будет составлять лексический минимум для текста с заданным процентом понимания. То есть мы например хотим понять 80% всего текста и библиотека, проанализировав текст выдаёт нам набор слов, которые необходимо для этого выучить.

И так, поехали.

Объекты DTO (боевые единицы)


ReceivedText.java

package ru.lexmin.lexm_core.dto;

/**
 * Класс для получения от пользователя введённой информации а виде текста (text)
 * и процента понимания (percent)
 *
 */
public class ReceivedText {

	/**
	 * Версия
	 */
	private static final long serialVersionUID = 5716001583591230233L;

	// текст, который ввёл пользователь
	private String text;

	// желаемый процент понимания текста пользователем
	private int percent;

	/**
	 * Пустой конструктор
	 */
	public ReceivedText() {
		super();
	}

	/**
	 * Конструктор с параметрами
	 * 
	 * @param text
	 *            {@link String}
	 * @param percent
	 *            int
	 */
	public ReceivedText(String text, int percent) {
		super();
		this.text = text;
		this.percent = percent;
	}

	/**
	 * @return text {@link String}
	 */
	public String getText() {
		return text;
	}

	/**
	 * Устанавливает параметр
	 * 
	 * @param text
	 *            text {@link String}
	 */
	public void setText(String text) {
		this.text = text;
	}

	/**
	 * @return percent {@link int}
	 */
	public int getPercent() {
		return percent;
	}

	/**
	 * Устанавливает параметр
	 * 
	 * @param percent
	 *            percent {@link int}
	 */
	public void setPercent(int percent) {
		this.percent = percent;
	}

}

WordStat.java

package ru.lexmin.lexm_core.dto;

import java.util.HashMap;
import java.util.Map;

/**
 * Класс для передачи рзультов обработки текста в виде: - количество слов в
 * тексте - честота употребления каждого слова.
 * 
 * Количество слов хранится в поле countOfWords (int) Частота употребления
 * хранится в поле frequencyWords (Map<String, Integer>): - ключом является
 * слово - значением частора употребления в тексте
 * 
 * Поле receivedText - содержет ссылку на dto с текстом и процентом понимания.
 *
 */
public class WordStat {

	/**
	 * Версия
	 */
	private static final long serialVersionUID = -1211530860332682161L;

	// ссылка на dto с исходным текстом и параметрами
	private ReceivedText receivedText;

	// кол-во слов в тексте, на который ссылка receivedText
	private int countOfWords;

	// статистика по часторе слов текста, на который ссылка receivedText,
	// отфильтрованная с учётом процента понимания
	private Map<String, Integer> frequencyWords;

	/**
	 * Констркутор по умолчанию
	 */
	public WordStat() {
		super();
	}

	/**
	 * Конструктор с параметрами
	 * 
	 * @param receivedText
	 * @param countOfWords
	 * @param frequencyWords
	 */
	public WordStat(ReceivedText receivedText, int countOfWords, Map<String, Integer> frequencyWords) {
		this.receivedText = receivedText;
		this.countOfWords = countOfWords;
		this.frequencyWords = frequencyWords;
	}

	/**
	 * Конструктор задаёт значение поля receivedText из передоваемого объекта.
	 * остальнве поля интциализируются значениями по умолчанию
	 * 
	 * @param receivedText
	 */
	public WordStat(ReceivedText receivedText) {
		this.receivedText = receivedText;
		// инициализация остальных полей значениями по умолчинию
		this.countOfWords = 0;
		this.frequencyWords = new HashMap<String, Integer>();
	}

	/**
	 * @return receivedText {@link ReceivedText}
	 */
	public ReceivedText getReceivedText() {
		return receivedText;
	}

	/**
	 * Устанавливает параметр
	 * 
	 * @param receivedText
	 *            receivedText {@link ReceivedText}
	 */
	public void setReceivedText(ReceivedText receivedText) {
		this.receivedText = receivedText;
	}

	/**
	 * @return countOfWords {@link int}
	 */
	public int getCountOfWords() {
		return countOfWords;
	}

	/**
	 * Устанавливает параметр
	 * 
	 * @param countOfWords
	 *            countOfWords {@link int}
	 */
	public void setCountOfWords(int countOfWords) {
		this.countOfWords = countOfWords;
	}

	/**
	 * @return frequencyWords {@link Map<String,Integer>}
	 */
	public Map<String, Integer> getFrequencyWords() {
		return frequencyWords;
	}

	/**
	 * Устанавливает параметр
	 * 
	 * @param frequencyWords
	 *            frequencyWords {@link Map<String,Integer>}
	 */
	public void setFrequencyWords(Map<String, Integer> frequencyWords) {
		this.frequencyWords = frequencyWords;
	}

}


Ну тут всё просто и понятно, думаю комментариев в коде достаточно

Интерфейс анализатора текстов (определяем функциональность)


TextAnalyzer.java

package ru.lexmin.lexm_core;

import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

/**
 * Данный интерфейс описывает основной функционал анализа получаемого от
 * пользователя текста
 *
 */
public interface TextAnalyzer {

	/**
	 * Мемод получает объект класса {@link WordStat}, заполненный данными,
	 * актуальными для передаваемого объекта {@link ReceivedText}
	 * 
	 * @param receivedText
	 *            {@link ReceivedText}
	 * @return возврашает заполненный {@link WordStat}
	 */
	public abstract WordStat getWordStat(ReceivedText receivedText);

}


Нам будет достаточно всего одного внешнего метода, который нам вернёт WordStat (DTO), из которого мы потом и вытащим слова.

Реализация анализатора текстов


TextAnalyzerImp.java

package ru.lexmin.lexm_core;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

/**
 * Этот класс является реализацией интерфейса TextAnalyzer
 *
 */
public class TextAnalyzerImp implements TextAnalyzer {

	/* Константы */
	private final int PERCENT_100 = 100;
	private final int ONE_WORD = 1;
	private final String SPACE = " ";

	// регулярное выражение: все испольуемые апострофы
	private final String ANY_APOSTROPHE = "[’]";

	// применяемый, стандартный апостроф
	private final String AVAILABLE_APOSTROPHE = "'";

	// регулярное выражение: не маленькие латинские буквы, не пробел и не
	// апостроф(')
	private final String ONLY_LATIN_CHARACTERS = "[^a-z\\s']";

	// регулярное выражение: пробелы, более двух подрят
	private final String SPACES_MORE_ONE = "\\s{2,}";

	/**
	 * Метод преобразует передаваемый текст в нижнеме регистру, производит
	 * фильтрацию текста. В тексте отсаются только латинские буквы, пробельные
	 * символы и верхний апостроф. Пробелы два и более подрят заменяются одним.
	 * 
	 * @param text
	 *            {@link String}
	 * @return отфильтрованный текст
	 */
	private String filterText(String text) {

		String resultText = text.toLowerCase().replaceAll(ANY_APOSTROPHE, AVAILABLE_APOSTROPHE)
				.replaceAll(ONLY_LATIN_CHARACTERS, SPACE).replaceAll(SPACES_MORE_ONE, SPACE);

		return resultText;
	}

	/**
	 * Метод преобразует получаемый текст в Map<{слво}, {количество}>
	 * 
	 * @param text
	 *            {@link String}
	 * @return заполненный Map
	 */
	private Map<String, Integer> getWordsMap(String text) {

		Map<String, Integer> wordsMap = new HashMap<String, Integer>();

		String newWord = "";

		Pattern patternWord = Pattern.compile("(?<word>[a-z']+)");
		Matcher matcherWord = patternWord.matcher(text);

		// поиск слов в тексте по паттерну
		while (matcherWord.find()) {

			newWord = matcherWord.group("word");

			if (wordsMap.containsKey(newWord)) {

				// если слово уже есть в Map то увеличиваеи его количество на 1
				wordsMap.replace(newWord, wordsMap.get(newWord) + ONE_WORD);

			} else {

				// если слова в Map нет то добавляем его со значением 1
				wordsMap.put(newWord, ONE_WORD);

			}
		}

		return wordsMap;
	}

	/**
	 * Метод возвращает общее количество слов, суммируя частоту употребления
	 * слов в получаемом Map
	 * 
	 * @param wordsMap
	 *            {@link Map}
	 * @return общее количество слов в тексте, по которому составлен Map
	 */
	private int getCountOfWords(Map<String, Integer> wordsMap) {

		int countOfWords = 0;

		// считаем в цикле сумму значений для всех слов в Map
		for (Integer value : wordsMap.values())
			countOfWords += value;

		return countOfWords;
	}

	/**
	 * Метод производит вычисление процентрого соотнашения аргумента
	 * numberXPercents от аргумента number100Percents
	 * 
	 * @param number100Percents
	 *            int
	 * @param numberXPercents
	 *            int
	 * @return прочентное соотношение
	 */
	private int getPercent(int number100Percents, int numberXPercents) {

		return (numberXPercents * PERCENT_100) / number100Percents;
	}

	/**
	 * Метод выполняет фильтрацию слов в массива, чтобы их количество покрывало
	 * заданный процент понимания текста
	 * 
	 * @param wordsMap
	 *            {@link Map}
	 * @param countOfWords
	 *            int
	 * @param percent
	 *            int
	 * @return возвращает отфильтрованный массив, элементы когорого
	 *         отсорвированы по убывающей
	 */
	private Map<String, Integer> filterWordsMap(Map<String, Integer> wordsMap, int countOfWords, int percent) {

		// LinkedHashMap - ассоциативный массив, который запоминает порядок
		// добавления элементов
		Map<String, Integer> resultMap = new LinkedHashMap<String, Integer>();

		int sumPercentOfWords = 0;

		// создаёт поток из Map с записями Entry<String, Integer>,
		// отсортированными по убыванию
		Stream<Entry<String, Integer>> streamWords = wordsMap.entrySet()
                            .stream().sorted(Map.Entry.comparingByValue(
				       (Integer value1, Integer value2) -> (
                                                 value1.equals(value2)) ? 0 : ((value1 < value2) ? 1 : -1)
                                       )
                             );

		// создаём итератор для обхода всех записей потока
		Iterator<Entry<String, Integer>> iterator = streamWords.iterator();

		// добавляем в resultMap каждую последующую запись из итератора, пока не
		// будет тостигнут заданный процент понимания
		while (iterator.hasNext() && (sumPercentOfWords < percent)) {

			Entry<String, Integer> wordEntry = iterator.next();

			resultMap.put(wordEntry.getKey(), wordEntry.getValue());

			sumPercentOfWords += getPercent(countOfWords, wordEntry.getValue());

		}

		return resultMap;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * ru.lexmin.lexm_core.TextAnalyzer#getWordStat(ru.lexmin.lexm_core.dto.
	 * ReceivedText)
	 */
	@Override
	public WordStat getWordStat(ReceivedText receivedText) {

		WordStat wordStat = new WordStat(receivedText);

		Map<String, Integer> wordsMap = getWordsMap(filterText(receivedText.getText()));

		wordStat.setCountOfWords(getCountOfWords(wordsMap));

		wordStat.setFrequencyWords(
                          filterWordsMap(wordsMap, wordStat.getCountOfWords(), receivedText.getPercent())
                );

		return wordStat;
	}

}

Я постарался максимально подробно закомментировать все методы.

Если кратко, то происходит следующее:
Сначала из текста вырезается всё что является латинскими буквами, апострофами или пробелами. Количество пробелов более 2х подряд заменяется одним. Делается это в методе метод filterText(String text).

Далее из подготовленного текста формируется массив слов — Map<слово, количество в тексте>. За это отвечает метод getWordsMap(String text).

Подсчитываем общее количество слов методом getCountOfWords(Map<String, Integer> wordsMap)

И наконец фильтруем нужные нам слова, для того чтобы покрыть N% текста методом filterWordsMap(Map<String, Integer> wordsMap, int countOfWords, int percent)

Ставим эксперимент (выведем в консоль список слов)


package testText;

import ru.lexmin.lexm_core.TextAnalyzer;
import ru.lexmin.lexm_core.TextAnalyzerImp;
import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

public class Main {

 public static void main(String[] args) {

	final int PERCENT = 80;

	TextAnalyzer ta = new TextAnalyzerImp();

	String friends = "There's nothing to tell!  He's .... тут текст двух серий первого сезона";

	ReceivedText receivedText = new ReceivedText(friends, PERCENT);

	WordStat wordStat = ta.getWordStat(receivedText);

	System.out.println("Количество слов в тексте: " + wordStat.getCountOfWords());
	System.out.println("Количество слов, покрывающие 80% текста: " + wordStat.getFrequencyWords().size());
	System.out.println("Список слов, покрывающих 80% текста");
	wordStat.getFrequencyWords().forEach((word, count) -> System.out.println(word));

	}

}


Результат


Количество слов в тексте: 1481
Количество слов, покрывающие 80% текста: 501
Список слов, покрывающих 80% текста: i, a, and, you, the, to, just, this, it, be, is, my, no, of, that, me, don't, with, it's, out, paul, you'r, have, her, okay, … и так далее

Заключение


В данном эксперименте мы проанализировали только две серии первого сезона и делать какие-либо выводы рано, но две серии идут около 80-90 мин и для их понимания (остальные 20% оставляем на додумывание, логику и зрительное восприятие) достаточно всего 501 слово.

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

О всех наших проблемах и достижениях на этом тернистом пути я буду писать в следующих постах. Спасибо за внимание и, надеюсь, до новых встреч.




К сожалению, не доступен сервер mySQL