От автора
«Куда только не заведёт любопытство» — именно с этих слов и началась эта история.
Дело обстояло так.
Вернулся я из командировки из США, где провел целый месяц своей жизни. Готовился я Вам скажу я к ней основательно и прилично так налегал на английский, но вот не задача, приехав к заморским друзьям я понял что совершенно их не понимаю. Моему огорчению не было предела. Первым делом по приезду я встретился с другом, который свободно говорит по английски, излил ему душу и услышал в ответ: «… ты просто не те слова учил, нужно учить самые популярные… запас слов, который используется в повседневных разговорах не более 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.: От себя хочу сказать что мои друзья заинтересовались этим экспериментом, и мы будем развивать эту тему. Было принято решение начать создание портала, на котором любой желающий сможет проанализировать интересующий его текст. По результатам таких анализов будет собираться статистика и формироваться ТОРы английских слов.
О всех наших проблемах и достижениях на этом тернистом пути я буду писать в следующих постах. Спасибо за внимание и, надеюсь, до новых встреч.
Комментарии (32)
lena_hudyakova
29.04.2016 14:16Как способ получить список самых употребляемых слов это очень интересно, но не уверена в прикладном применении этого: мне кажется, знание местоимений и предлогов (а видимо они в топе этого 501 слова) недостаточно для понимания смысла.
Здесь вполне может сработать правило 80-20: чтобы понять 80% смысла, достаточно понять 20% слов, но ключевых, наименее употребляемых.
И еще интересно, как соотносится ваше понимание этих двух серий и процент слов которые вы знаете из «топа».entony
29.04.2016 15:44Я согласен, в топ безусловно попадёт много «мусора» и «шума». Второй стадией будет фильтрация этих результатов.
Что касается применения — методом анализа большого числа текстов (думаю что выберем определённую тематику) сформируются топ слов, отфильтруется от «мусора», разобьется по частям речи. Далее предполагается сделать интерфейс, который поможет запомнить эти слова.
Две серии просто для примера были взяты
fogone
29.04.2016 16:41+3Увидеть код, который считает слова было не интересно, но он приведен. Посмотреть на список слов было интересно, но его в статье нет. Не вижу смысла ограничиваться двумя сериями. Анализировал аналогично двухлетнюю историю одного чата. Это около 200к слов на 11 человек. Интересно, что у каждого человека есть свои слова-фавориты.
entony
29.04.2016 17:02Цель этого поста была описать принцип работы. Список слов по двум сериям Вы правы выкладывать нет смысла. Вот когда мы наберем статистику хотя бы по нескольким сезонам и разных сериалов, вот тогда список будет актуальный и обязательно будет представлен. Спасибо за идею про чаты.
my_own_parasite
29.04.2016 18:07Интересно было бы взять список слов, который рекомендуют учить, как раз для цели скорого участия в беседах и посмотреть какой % бесед в сериале можно понять используя этот список.
Список можно взять такой, например: http://fluent-forever.com/wp-content/uploads/2014/05/625-List-Alphabetical.pdf
Чисто же механические списки самых популярных слов издаются книгами для разных языков: поиск на amazon
Там печатают слова по частоте употребления в разных источниках: худ. книги, новости, интернет, разговоры.
denis_g
29.04.2016 23:03+2Простите, не смог удержаться :)
Кодfrom collections import Counter import re Q = 80 words = Counter(re.findall('[a-z\']+', open('subtitle.srt', encoding='windows-1252').read().lower())) words_count = len(words) popular = words.most_common(int(words_count*Q/100)) popular_count = len(popular) print("Total:", words_count) print("Popular ({}%):".format(Q), popular_count) print("Top 10:") for x in popular[:10]: print(x)
entony
29.04.2016 23:36На данный момент всё существует на уровне — сделано первым способом как пришло в голову :-)
Согласен что это не самое лучшее решение, я и не претендую на пальму лидера. От части пост писался для получения конструктивной критики.
Кстати за пример спасибо.Randl
30.04.2016 02:07Ну задача явно для скриптовых языков больше подходит. Повод изучить какой-нибудь Python
fogone
01.05.2016 10:41Думаю, дело не в скриптовом языке. Вот как оно могло бы на джаве выглядеть.
public class WordsCounter { private static final Comparator<Map.Entry<String, Long>> WORDS_COUNT_COMPARATOR = Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed(); public static void main(String[] args) { String text = "some text text some words many strong words and single bla bla"; Map<String, Long> count = Arrays .stream(text.split("\\s+")) .map(WordsCounter::prepare) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); int wordsCount = count.size(); System.out.println(wordsCount); count .entrySet() .stream() .sorted(WORDS_COUNT_COMPARATOR) .limit(wordsCount * 80 / 100) .forEach(System.out::println); } private static String prepare(String word) { return word; // // TODO: очистить от шлака и превести к нормальной форме } }
На котлине это было бы еще в два раза короче.
semch
29.04.2016 23:30Но ведь граматика… Вспомогательные слова могут ведь в корне изменить весь смысл. А еще формы глаголов, слово вроде одно, а смысл другой немножко. Думаю роботы для вас найдется.
entony
29.04.2016 23:32Да мы над этим думаем, пока нет прямого ответа как оценивать грамматику и формы глаголов.
Randl
30.04.2016 02:12Скачал я как-то такой список самых частых слов, кажется отсюда. Оказалось, что топовую тысячу я знал тогда (~4-5 лет назад) почти полностью, за исключением 4 или 5 слов.
Проблемы в понимании всё равно были, особенно когда слушал разговор людей между собой, которые не старались упростить свою речь в угоду мне. Основная проблема для меня — именно различать слова во время быстрой речи, тем более с акцентом. Потому и фильмы смотреть могу, к сожалению, только с субтитрами. Вторая проблема — сленг.
mbait
30.04.2016 05:17Спрячьте код под кат, а то после прочтения можно получить легкий шок и забыть уже выученные английские слова.
fogone
30.04.2016 20:25Как я понял, автор еще не очень крепко освоился в джаве и это был своего рода проба пера. Вероятно проба стримов. Я позволю себе несколько замечаний по коду, которые возможно помогут задуматься о мелочах кода, из которых в итоге и состоит результат, который выдает разработчик.
1. Не стоит писать в интерфейсах public у методов, все методы интерфейса по-умолчанию public, все об этом знают, а модификаторы только загромождают код. И уж тем более не стоит писать abstract — в интерфейсе по определению все методы абстрактные (если не дефолтные :-)
2. Не надо создавать коммент (или блок комента вроде описания полей) если он не несет никакой информации, например — имя и тип и так видно в коде, а комментарии засоряют код как и любая другая информация.
3. Когда создаешь бины, лучше всего начать с неизменяемой версии (без setter-ов), изменять это только если есть действительная в том необходимость. Тоже самое с методами и конструкторами — не создавать их без необходимости. Ну и можно избавить читателей вашего кода от вызова Object.super() и определения serialVersionUID — это явно никак вам в данном случае не помогает.
4. private final — это не константы (см TextAnalyzerImp), а неизменяемые поля. Константы — private static final. И еще. ONE_WORD=1 это не просто бессмысленная константа, она еще и вредная, потому что она не только не упрощает понимание, но еще и заставляет пойти и посмотреть, какое же значение у неё, потому что не может быть, чтобы для инкремента на один была сделана отдельная константа.
5. TextAnalyzer конечно хороший интерфейс, но в вашей программе абсолютно бесполезный. Он даже тесты написать вам не поможет.
6. Совершенно непонятно, зачем в WordStats хранится ReceivedText — лучше избегать лишних зависимостей, а в данном случае бин статистики хранит в себе текст — это даже логически не очень понятно.
7. Раз уж мы говорим о java8
Map.Entry.comparingByValue( (Integer value1, Integer value2) -> ( value1.equals(value2)) ? 0 : ((value1 < value2) ? 1 : -1) ) // если я правильно понял, это Map.Entry.comparingByValue(Comparator.reverseOrder())
да и сразу за этим кодом от stream-ов мы переходим к iterator-у и сваливаемся в полный императив — выглядит странно. Суммирование, например, можно было бы написать так:
wordsMap.values().stream().mapToInt(Integer::intValue).sum()
8. Я и сам довольно безграмотный, но обычно пытаюсь проверить слово, если не уверен, как оно пишется: «процентрого соотнашения». Это кстати, заодно и подводит нас к мысли, что все, что мы пишем в коде не бесплатно, даже коменты, я уж не говорю обо всем остальном.fogone
30.04.2016 20:44Раз уж начал, то вот еще:
1.
if (wordsMap.containsKey(newWord)) { wordsMap.replace(newWord, wordsMap.get(newWord) + ONE_WORD); } else { wordsMap.put(newWord, ONE_WORD); } // можно так написать wordsMap.put(newWord, wordsMap.computeIfAbsent(newWord, key -> 0) + 1);
2.
String newWord = ""; while (matcherWord.find()) { newWord = matcherWord.group("word"); // такая оптимизация не имеет смысла, только засоряет код, лучше объявить переменную по месту while (matcherWord.find()) { String newWord = matcherWord.group("word");
3. «Поле receivedText — содержет ссылку на dto с текстом и процентом понимания.» Dto — это Data transfer object, в данном случае ничего никуда не transfer-ится, это просто бин.
entony
01.05.2016 16:05Это действительно проба пера + получить критику.
Спасибо за конструктивные замечания.
Интерфейс TextAnalyzer я использовал, так как в дальнейшем этот код будет встраиваться в проект (Spring проект)
juray
01.05.2016 14:35А по-моему, основной затык не в словах, а в их устойчивых сочетаниях — все эти «get down», «go over», «look for». Что толку знать значение отдельных кусочков, если в соединении они означают что-то не сводящееся к словосочетанию, полученному буквальным переводом. Ну и фразеологизмы всякие, переносные значения. В этих вот «самых популярных» словах множественность значений особенно чувствуется. В общем, проблема контекста во весь рост.
В итоге получается — все слова понял, а фразу целиком нет («угадал все буквы, но не смог назвать слово»).
pro_co_ru
01.05.2016 15:57В жизни то диалоги, думаю, сильно будут отличаться от сериальных.
Может быть стоит выдернуть слова из ютубовских роликов, где люди в компаниях разговаривают в естественной среде, а не на камеру.
Кстати, заодно, можно ещё расклассифицировать по типу обстановки, в которой происходят диалоги.
Rumlin
04.05.2016 08:21Было бы неплохо реализовать GUI с загрузкой TXT, PDF. Интересно бы проанализировать книги.
CepbluBoJlk
Вот у меня вопрос, я конечно все понимаю jdoc и все такое, но «Устанавливает параметр», «Констркутор по умолчанию», «Конструктор с параметрами», /* Константы */ и все такое это какая то жесть и мусор.
entony
Согласен, изначально этот код демонстрировался в образовательных целях, забыл подчистить. Спасибо