A Small Collection of British Fiction

Компьютерный анализ текстов, модуль 4

Автор

Константин Сатдаров

Дата публикации

2026.05.31

α’. Импорт необходимых библиотек и предобработка корпуса

Для начала импортирую необходимые библиотеки:

library(baguette)
library(discrim)
library(embed)
library(future)
library(readtext)
library(stopwords)
library(stylo)
library(textrecipes)
library(tidymodels)
library(tidytext)
library(tidyverse)
library(udpipe)

α’.0. Начало всех начал

После этого перехожу к корпусу, который я извлёк из zip-архива. На данном этапе я выполнил ручную предобработку корпуса (которая, надеюсь, оправдана в учебных целях). В частности, из произведений я удалил все оглавления и остальные элементы книги, которые не относятся к её основному тексту. Это чаще всего предисловия, но, грубо говоря, я решил оставить только главы.

В этой связи, например, особое внимание на себя обращает исходный файл с текстом «Клариссы, или Истории молодой леди» Сэмюэля Ричардсона, в котором содержание не только в начале: содердание приводится перед началом каждого тома произведения (коих 9).

Начало содержания 9 тома «Клариссы,…»

α’.1. Главы, томы, книги не попали…

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

Да, содержания можно бы было удалить тоде автоматически, но, например содержание «Клариссы,…» представляет собою больше, чем просто нумерация писем: это в том числе и достаточно развёрнутое описание писем, которое может занимать, как видно по скриншоту выше, более одной строки.

Задаю регулярное выражение, которое необходимо для отслеживания строк упомянутого типа:

pattern <- regex(
   '^\\s*(volume|chapter|book|letter)\\s+(\\d+|[IVXLCDM]+).*$',
   ignore_case = TRUE
   )

Далее ввожу новую функцию для очистки одного файла:

clear_txt <- function(input_file, output_file = NULL) {
   # прочитать строки
   lines <- read_lines(input_file, skip_empty_rows = FALSE)

   # фильтрация строк-заголовков
   cleared_lines <- lines[!str_detect(lines, pattern)]

   # задать имя выходного файла
   if (is.null(output_file)) {
     output_file <- str_replace(input_file, '\\.txt$', '_cleared.txt')
   }
 
   # запись результата
   write_lines(cleared_lines, output_file)

   invisible(cleared_lines)
}

Обработка всех файлов разом:

process_all_txts <- function(directory, output_dir) {
  files <- list.files(path = directory, pattern = '\\.txt$', full.names = TRUE)

  for (f in files) {
      out_file <- file.path(output_dir, basename(f) |>
                              str_replace('\\.txt$', '_cleared.txt'))
      clear_txt(f, out_file)
  }
}

Запускаю функцию с указанием директорий:

# исходная папка
initial_dir <- './british_fiction'
# выходная папка создаётся заранее
target_dir <- './british_fiction_cleared'

process_all_txts(initial_dir, target_dir)

α’.2. Λημματίζω τους λόγους (‘Лемматизирую слова’)

Приступаю к лемматизации за помощью udpipe. Я использую модель для английского языка. Поскольку в данном задании я работаю над коллекциею текстов, написать функцию тоже представляется целесообразно. Для начала скачиваю модель: я взял english-ewt.

# скачать модель в рабочую директорию
# udpipe_download_model(language = 'english-ewt')

# загрузить модель
english_ewt <- udpipe_load_model(file = 'english-ewt-ud-2.5-191206.udpipe')

Следующий этап - написание функции для лемматизации корпуса.

lemmatise_txt <- function(input_path, output_path) {
  
  # чтение строк из одного файла
  text_lines <- read_lines(input_path)
  text <- paste(text_lines, collapse = '\n')

  # UDPipe-аннотация
  annotation <- udpipe_annotate(english_ewt, x = text, doc_id = basename(input_path))
  annotation_df <- as.data.frame(annotation)
  
  # оставить только не-пунктуационные токены
  df_words <- annotation_df[annotation_df$upos != 'PUNCT',]
  
  # соединить леммы пробелами
  result <- paste(df_words$lemma, collapse = ' ')
  
  # запись результата за помощью write_lines()
  write_lines(result, output_path, na = '')
}

После этого я начинаю путешествие длинною в примерно полтора-два часа: лемматизация всех тектовых файлов (произведения всё-таки немаленькие) через udpipe - предприятие времязатратное. Тем не менее я уповаю на то, что лемматизация текстов (хотя английский - это не тот язык, для которого эта операция существенна) что-то да привнесёт в данный анализ…

input_dir <- './british_fiction_cleared'
output_dir <- './british_fiction_lemmatised'

txt_files <- list.files(input_dir, pattern = '\\.txt$', full.names = TRUE)

for (f in txt_files) {
  base_name <- tools::file_path_sans_ext(basename(f))
  out_file <- file.path(output_dir, paste0(base_name, '_lemmatised.txt'))
  lemmatise_txt(f, out_file)
}

Процесс лемматизации файлов корпуса (очень долгий процесс!)

Скриншот сделан до изменения кода выше. Выведение текста мне служило лишь сигналом, что всё работает и что процесс лемматизации не прекратился.

Ознакомиться с итоговыми файлами можно по ссылке.

β’. Извлечение лигвистических признаков

В этой работе я работаю над биграммами.

texts <- readtext('./british_fiction_lemmatised/*.txt', 
                      docvarsfrom = 'filenames', 
                      dvsep = '_')

Для дальнейшей работы сохраняю сам текст (text), а также авторов (docvar1) и краткое название произведений (docvar2). После этого делаю из них колонку doc_id.

texts_df <- texts |>
  select(docvar1, docvar2, text)

texts_df <- texts_df |>
  unite('doc_id', docvar1, docvar2, sep = '_', remove = TRUE)

Для более точного подсчёта биграмм приведу к нижнему регистру все слова в текстах:

texts_df <- texts_df |>
  mutate(text = str_to_lower(text))

head(texts_df, 5)
readtext object consisting of 5 documents and 1 docvar.
# A data frame: 5 × 3
  docvar1 docvar2 text               
* <chr>   <chr>   <chr>              
1 ABronte Agnes   "\"agnes grey\"..."
2 ABronte Tenant  "\"the tena n\"..."
3 Austen  Emma    "\"emma emma \"..."
4 Austen  Pride   "\"pride and \"..."
5 Austen  Sense   "\"sense and \"..."

Список всех биграмм лемматизированных текстов оформляю через unnest_tokens:

bigrams_df <- texts_df |>
  unnest_tokens(bigram, text, token = 'ngrams', n = 2) |>
  separate(bigram, into = c('word1', 'word2'), sep = ' ')

Чтобы получить список более-менее осмысленных биграмм, я предлагаю воспользоваться стоп-словами. Если в биграмме хотя бы одно слово является стоп-словом, её предлагается исключать из первичнго анализа.

en_stopwords <- stopwords(language = 'en', source = 'stopwords-iso')

Фильтрация датасета с биграммами по стоп-словам:

bigrams_df_filtered <- bigrams_df |>
  filter(!word1 %in% en_stopwords,
         !word2 %in% en_stopwords)

Для каждого произведения при выводе, допустим, топ-30 биграмм отображаются в первую очередь имена собственные и наименования значимых мест, что, пожалуй, достаточно ожидаемо. Тем не менее отдельные частотные коллокации тоже имеют место быть, ср. fall asleep в «Джейн Эйр» ниже.

top30_per_doc <- bigrams_df_filtered |>
  unite('bigram', word1, word2, sep = ' ', remove = TRUE) |>
  count(doc_id, bigram) |>
  group_by(doc_id) |>
  slice_max(n, n = 30, with_ties = FALSE) |>
  ungroup()

Для «Джейн Эйр» Шарлотты Бронте:

top30_per_doc |>
  filter(doc_id == 'CBronte_Jane') |>
  head(10)
# A tibble: 10 × 3
   doc_id       bigram              n
   <chr>        <chr>           <int>
 1 CBronte_Jane jane eyre          38
 2 CBronte_Jane thornfield hall    33
 3 CBronte_Jane grace poole        21
 4 CBronte_Jane helen burns        20
 5 CBronte_Jane colonel dent       16
 6 CBronte_Jane moor house         16
 7 CBronte_Jane john reed          10
 8 CBronte_Jane fall asleep         9
 9 CBronte_Jane jane jane           9
10 CBronte_Jane john rivers         8

Для «Клариссы,…» Сэмюэля Ричардсона:

top30_per_doc |>
  filter(doc_id == 'Richardson_Clarissa') |>
  head(10)
# A tibble: 10 × 3
   doc_id              bigram                n
   <chr>               <chr>             <int>
 1 Richardson_Clarissa clarissa harlowe    449
 2 Richardson_Clarissa lady betty          225
 3 Richardson_Clarissa john belford        179
 4 Richardson_Clarissa belford esq         168
 5 Richardson_Clarissa captain tomlinson   125
 6 Richardson_Clarissa cousin morden       107
 7 Richardson_Clarissa uncle antony        101
 8 Richardson_Clarissa anna howe            94
 9 Richardson_Clarissa uncle harlowe        93
10 Richardson_Clarissa lady sarah           84

Продолжение смотреть здесь.