Аналіз англійського тексту з чашкою кави «JavaSE8»


Від автора

«Куди тільки не заведе цікавість» — саме з цих слів і почалася ця історія.

Діло було так.

Повернувся я з відрядження із США, де провів цілий місяць свого життя. Готувався я Вам скажу я до неї грунтовно і пристойно так налягав на англійську, але от не завдання, приїхавши до заморських друзям я зрозумів, що зовсім їх не розумію. Моє засмучення не було меж. Насамперед по приїзду я зустрівся з другом, який вільно говорить англійською, вилив йому душу і почув у відповідь: «… ти просто не ті слова вчив, потрібно вчити найпопулярніші… запас слів, який використовується в повсякденних розмовах не більше 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.: Від себе хочу сказати що мої друзі зацікавилися цим експериментом, і ми будемо розвивати цю тему. Було прийнято рішення розпочати створення порталу, на якому будь-який бажаючий зможе проаналізувати зацікавив його текст. За результатами таких аналізів буде збиратися статистика і формуватися Тори англійських слів.

Про всі наші проблеми і досягнення на цьому тернистому шляху я буду писати в наступних постах. Спасибі за увагу і, сподіваюся, до нових зустрічей.

Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.