Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Без ошибок и опечаток

Мир ПК» 11/2007)

Основная идея
Первые шаги
Автоматизация пополнения словаря
Первоначальный алгоритм создания или дополнения словаря
Выявленный недостаток. Доработка алгоритма
Доработанный алгоритм создания или дополнения словаря
Дальнейшие усовершенствования
Окончательный алгоритм создания или дополнения словаря
Окончательная программа. Руководство пользователя
Регулярные выражения
1. Класс символов
2. Альтернатива
3. Указание количества повторов
4. Условия
5. Подстановка
Исправление ошибочного слитного и раздельного написания
«Почти хорошие» ошибки
Разное
Приложение

Основная идея

При редактировании текстовых файлов мне часто приходится исправлять орфографические ошибки и опечатки (далее для краткости все они называются ошибками). Естественным было попытаться облегчить себе эту работу с помощью компьютера.

За счёт чего это можно сделать? Некоторые ошибки (назовём их «хорошими») допустимо исправлять автоматической заменой ошибочного слова на правильное, например: «собераюсь» -> «собираюсь». Однако далеко не все ошибки таковы. Предположим, неправильно написано «садится» вместо «садиться» — оба эти слова есть в русском языке, поэтому автоматически заменять одно на другое нельзя. Или, скажем, «оптека» — непонятно, что же должно быть на самом деле, то ли «оптика», то ли «аптека».

Так вот, если написать программу, исправляющую хотя бы наиболее распространённые «хорошие» ошибки, то объём ручной работы заметно сократится. Ведь чтобы исправить одну ошибку вручную, нужно выполнить целую последовательность действий, например: подвести курсор к нужному месту текста, удалить неправильный или лишний символ и/или вставить правильный. Бывают ошибки и в двух символах, переставленных местами, и ещё более сложные. Особенно полезной такая программа будет при обработке текстов с большим количеством ошибок.

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

Первые шаги

Для начала я написал простейшую программу на Perl (этот язык весьма удобен для работы с текстами), которая читала исходный текстовый файл, исправляла часть ошибок с помощью замен неправильных слов на правильные и записывала результат. Для простоты я решил ограничиться обработкой таких текстовых файлов, где не используются переносы слов.

Участок программы, исправляющий ошибки, представлял собой последовательность операторов поиска-замены следующего вида:

s/(?<![[:alpha:]'-])неправильное слово(?![[:alpha:]'-])/правильное слово/g;

Здесь конструкция «(?<![[:alpha:]'-])» означает, что в тексте не должно быть буквы, символа «'» или «-» перед заменяемым словом, а конструкция «(?![[:alpha:]'-])» — что таких символов не должно быть после него. Эти условия необходимы, чтобы избежать замены в том случае, когда найденная в тексте последовательность символов, совпадающая с неправильным словом, в действительности является частью какого-то другого слова.

Чтобы программа была эффективной, она должна исправлять как можно больше ошибок. А значит, нужно было добавлять в неё операторы для исправления новых ошибок. Я это делал прямо в процессе редактирования текста. Дабы меньше отвлекаться от этой своей основной задачи, я, исправив очередную «хорошую» ошибку, добавлял оператор для её исправления, только если она встречалась достаточно часто.

Следующим шагом было некоторое упрощение добавления информации для исправления ошибок. Я изменил программу так, чтобы пары из неправильного и правильного слов содержались не непосредственно в операторах поиска-замены, а в отдельном файле-словаре в виде «неправильное слово -> правильное слово». Программа читала содержимое этого словаря и в соответствии с ним выполняла замены. Добавить новую пару в словарь было несколько проще, чем добавить новый оператор поиска-замены в программу.

Хотелось бы, однако, пополнять словарь так, чтобы вовсе не приходилось отвлекаться от редактирования текста. Например, сначала отредактировать текст, а потом, в удобное время, сравнить исходный текст с отредактированным, находя исправленные слова и добавляя в словарь «хорошие» пары слов (т.е. соответствующие «хорошим» ошибкам). Но это процесс трудоёмкий, и выполняя его вручную, легко что-нибудь пропустить.

Автоматизация пополнения словаря

К счастью, возникла идея почти полной автоматизации этого процесса. Пусть специальная программа сравнивает исходный текст с отредактированным, находя исправленные слова. Найденные пары слов (слово с ошибкой и исправленный вариант) она будет предъявлять пользователю, который и вынесет вердикт: является ли данная пара «хорошей» или нет. «Хорошие» пары программа добавит к словарю.

То есть тут человек берёт на себя именно ту часть работы, которую компьютеру выполнить затруднительно: оценивает, является ли очередная пара слов «хорошей». А всё остальное делается автоматически.

Руководствуясь вышеизложенным, предстояло разработать алгоритм и написать требуемую программу для пополнения словаря.

Ясно, что прежде всего такая программа должна уметь выделять в тексте отдельные слова. А для этого она должна знать, что такое слово. Мы-то знаем, что это такое, а вот как объяснить это компьютеру? Я принял, что словом будет считаться последовательность символов (в том числе и состоящая всего из одного знака), обладающая следующими свойствами:

Тогда слово может быть описано следующим регулярным выражением Perl (оно разбито на строки для удобства чтения и комментирования):

(?<![[:alpha:]'-]) # Условие: слева нет ни буквы, ни "'", ни "-".
(?=[[:alpha:]])    # Условие: справа есть буква.
[[:alpha:]'-]+     # Последовательность букв, "'", "-".
(?<=[[:alpha:]])   # Условие: слева есть буква.
(?![[:alpha:]'-])  # Условие: справа нет ни буквы, ни "'", ни "-".

Далее надо было решить, каким образом программа, сравнивая исходный текст с отредактированным, будет находить пары слов (слово с ошибкой и исправленное слово).

Если бы при редактировании порядок слов оставался одним и тем же, никакие слова не добавлялись бы и не удалялись, одни слова не заменялись на другие, а исключительно лишь исправлялись ошибки, то всё было бы просто: берём первое слово исходного текста и сравниваем его с первым словом отредактированного; если не совпадают — значит, эти слова и образуют искомую пару; затем точно так же сравниваем вторые слова текстов, третьи и т.д.

Но в действительности при редактировании текст может подвергаться весьма существенным изменениям, поэтому подобный способ непригоден. Тогда можно пойти другим путём: искать такие пары слов (первое слово из исходного текста, а второе из отредактированного), в которых второе слово может быть исправлением первого.

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

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

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

При редактировании текста одно предложение нередко разбивается на два, и поэтому в слове, оказавшемся в начале второго предложения, строчная буква становится прописной. Или два предложения объединяются в одно, и тогда в слове, которое было в начале второго предложения, прописная буква становится строчной. Иногда автор начинает предложение со строчной буквы, и приходится исправлять её на прописную. Пары, соответствующие таким исправлениям (например, «быстро -> Быстро»), нельзя отнести к «хорошим», так как оба слова в них являются допустимыми. Так вот, чтобы пользователь не тратил времени на просмотр подобных пар и отказ от их добавления в словарь, я решил ввести условие: пары слов, различающихся лишь регистром, считаются неподходящими и не предъявляются пользователю. Из-за этого, правда, не будут предъявлены и «хорошие» пары типа «москва -> Москва», но их можно добавить в словарь вручную.

При поиске пар я решил рассматривать лишь слова длиной не меньше минимальной (я принял её равной четырём символам). Иначе возможно предъявление слишком большого количества неподходящих пар (например, «но -> не», «но -> он», «но -> то»). Ведь для коротких незначительно отличающихся слов велика вероятность того, что они окажутся не парой из неправильного и исправленного слова, а просто разными словами. Опять-таки добавить в словарь какие-либо пары коротких слов можно вручную.

Упрощает поиск пар такая оптимизация: не рассматривать случаи, где первое слово является допустимым (ведь допустимые слова нельзя заменять автоматически на что-то, значит, эти пары всё равно будут отвергнуты). Для проверки допустимости слова можно использовать то обстоятельство, что правильными являются все слова отредактированного текста (ведь ошибки в нём уже исправлены) и вторые слова в парах словаря (по определению). То есть если проверяемое слово среди них присутствует, значит, оно допустимо. Конечно, этот способ проверки неточен — проверяемое слово может быть допустимым и в то же время отсутствовать среди известных программе допустимых слов, но это приведёт лишь к невозможности оптимизации для данного конкретного слова.

Также не имеет смысла рассматривать пары, где о первом слове уже известно, что оно неправильное и как его надо исправить (т.е. в словаре уже есть пара с таким первым словом).

И конечно, в процессе работы программы нет смысла предъявлять пользователю какую-либо пару повторно.

Первоначальный алгоритм создания или дополнения словаря

  1. Построим множество source, содержащее (без повторов) все слова достаточной длины из исходного текста. Под достаточной длиной здесь и далее понимается длина не меньше минимальной — о ней я говорил в предыдущем разделе.
  2. Построим множество edited, содержащее (без повторов) все слова достаточной длины из отредактированного текста.
  3. Построим множество good_words из слов, содержащихся в множестве edited. Это множество будет обладать тем свойством, что слова в нём не повторяются и являются допустимыми.
  4. Если словаря нет, переходим к п. 7.
  5. Заполним (без повторов) множество bad_words (изначально пустое) такими первыми словами пар из словаря, которые имеют достаточную длину. Полученное множество будет обладать тем свойством, что слова в нём не повторяются, являются неправильными и для них в словаре уже содержится информация о том, как они должны быть исправлены.
  6. Дополним (без повторов) множество good_words такими вторыми словами пар из словаря (они являются правильными словами), которые имеют достаточную длину. После этого good_words по-прежнему будет обладать свойствами, упомянутыми в п. 3.
  7. Главный цикл. Для каждого слова из source выполняем:
    а) если оно содержится в good_words, значит, это допустимое слово, на этом его обработка завершается;
    б) если оно есть в bad_words, значит, в словаре уже есть пара для исправления этого слова, на этом его обработка завершается;
    в) построим множество m, содержащее такие слова из edited, которые могут быть исправлениями текущего слова (критерий, по которому определяется, может ли одно слово быть исправлением другого, был описан ранее) и при этом отличаются от текущего слова не только регистром. Если таких слов не нашлось, то на этом обработка текущего слова завершается;
    г) по очереди предъявляем пользователю пары, где первое слово — текущее, а второе — очередное слово из множества m, до тех пор, пока либо пользователь не подтвердит какую-либо пару, либо не отвергнет все пары. В первом случае добавляем подтверждённую пару в словарь.

Замечу, что так как множества source и edited не содержат повторяющихся элементов и каждое слово из source не более одного раза сопоставляется со словами из edited, то не нужно специально следить за тем, чтобы никакая пара слов не предъявлялась пользователю повторно, — это получится само собой.

Рассмотрим работу алгоритма на простом примере.

Исходный текст:
Но, вопервых, отметим дезайн аппарата.

Отредактированный текст:
Но, во-первых, отметим великолепный дизайн аппарата.

Исходный словарь:
ешё -> ещё
дезайн -> дизайн

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

  1. Множество source будет содержать слова «вопервых», «отметим», «дезайн», «аппарата». Слово «Но», так как его длина не является достаточной, не будет добавлено к множеству.
  2. Множество edited будет содержать слова «во-первых», «отметим», «великолепный», «дизайн», «аппарата». Точно так же слово «Но» не будет добавлено к множеству.
  3. Множество good_words будет содержать слова «во-первых», «отметим», «великолепный», «дизайн», «аппарата».
  4. Так как словарь есть, к п. 7 не переходим.
  5. Множество bad_words будет содержать слово «дезайн». Слово «ешё», так как его длина не является достаточной, не будет добавлено к множеству.
  6. Дополненное множество good_words будет содержать слова «во-первых», «отметим», «великолепный», «дизайн», «аппарата», т.е. не изменится: слово «ещё» не будет добавлено к нему, так как его длина не является достаточной, а слово «дизайн» — потому что оно уже содержится в множестве.
  7. Главный цикл. Рассмотрим обработку каждого слова из source:
    • «вопервых» — слова нет в good_words и в bad_words. Построенное множество m будет состоять из единственного слова «во-первых». Предъявляем пользователю пару «вопервых -> во-первых». Допустим, он подтвердил её. Добавляем эту пару в словарь;
    • «отметим» — слово есть в good_words, на этом его обработка завершается;
    • «дезайн» — слова нет в good_words, но оно есть в bad_words, на этом его обработка завершается;
    • «аппарата» — слово есть в good_words, на этом его обработка завершается.

В итоге словарь станет таким:
ешё -> ещё
дезайн -> дизайн
вопервых -> во-первых

Выявленный недостаток. Доработка алгоритма

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

Программа была опробована в режиме создания или дополнения словаря. И что бы вы думали? Хоть я при её разработке и уделял внимание тому, чтобы по возможности пользователю не предъявлялись неподходящие пары слов, проблема оказалась именно в этом.

Например, в исходном тексте есть слово «делал», а из отредактированного текста оно вообще было убрано, но там есть слова, которые по принятому критерию могут быть его исправлениями: «дела», «делала», «делали». Тогда программа предъявит пользователю пары «делал -> дела», «делал -> делала», «делал -> делали», и пользователь потратит время, отвергая их. (Я подразумеваю, что слова «делал» нет также и среди вторых слов пар словаря, так что программа не может сделать вывод, что оно является допустимым и поэтому не надо рассматривать пары с этим первым словом.)

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

Чтобы избежать предъявления подобных неподходящих пар, я решил ввести дополнительное условие: рассматривать только такие пары, где слова находятся в одинаковом контексте (т.е. первое слово пары должно быть в исходном тексте в окружении тех же слов, что и второе слово пары в отредактированном тексте). Ведь слова, лишь случайно похожие друг на друга, почти наверняка окажутся в разном контексте.

Длина контекста (то есть количество рассматриваемых соседних слов) не должна быть как слишком малой (чтобы избежать случайного совпадения контекста), так и слишком большой (чем больше контекст, тем больше вероятность того, что он изменится при редактировании). Я выбрал длину контекста в одно слово до рассматриваемого и одно слово после. То есть слева и справа от первого слова пары в исходном тексте должны быть те же самые слова, что слева и справа от второго слова пары в отредактированном тексте.

Доработанный алгоритм создания или дополнения словаря

  1. Создадим массив source: первый элемент — пустое слово, следующие элементы — слова из исходного текста (в таком же порядке, как в тексте), и последний элемент — опять пустое слово. Эти пустые слова нужны, чтобы в дальнейшем для каждого слова исходного текста был определён контекст.
  2. Точно так же по отредактированному тексту создадим массив edited.
  3. Создадим структуру данных, состоящую из хеша by_context и дополнительных массивов, нужную для того, чтобы в дальнейшем можно было быстро узнавать, какие слова достаточной длины встречаются в edited непосредственно между двумя заданными словами. В каждом элементе этого хеша ключ — два слова из edited, перечисленные через запятую, а значение — ссылка на созданный массив слов, длина которых достаточна и которые встречаются в edited непосредственно между этими двумя словами; этот массив не должен содержать повторов. Если для каких-то двух слов соответствующий массив оказывается пуст, то и элемент хеша не создаётся.
    Создание вышеописанной структуры данных происходит так.
    Изначально хеш by_context пуст. Создадим вспомогательное множество temp. Введём определение: тройка слов — это три слова, идущие подряд. Так вот, элементами множества temp будут все различные тройки слов (слова в них разделяются запятыми) из edited, причём второе слово в тройке — достаточной длины. Затем для каждого элемента temp выполняем следующее:
    а) разбиваем его на составляющие — получаем три слова, обозначим их 1, 2, 3;
    б) если в by_context есть элемент с ключом из слов 1 и 3, то добавляем к массиву, соответствующему этому элементу, слово 2. Такого слова заведомо нет в массиве — оно могло бы там быть, только если ранее уже была обработана точно такая же тройка слов, а это исключено, потому что все обрабатываемые тройки слов различны;
    в) иначе (в by_context нет элемента с ключом из слов 1 и 3) создаём этот элемент, а его значением будет ссылка на массив (который также нужно создать), состоящий из одного элемента — слова 2.
  4. Построим множество good_words из слов достаточной длины, содержащихся в массиве edited, кроме повторов. Это множество будет обладать тем свойством, что слова в нём не повторяются и являются допустимыми.
  5. Если словаря нет, переходим к п. 8.
  6. Заполним множество bad_words (изначально пустое) первыми словами пар из словаря, обладающими достаточной длиной, без повторов. Полученное множество будет обладать такими свойствами: слова в нём не повторяются, являются неправильными и для них в словаре уже содержится информация о том, как они должны быть исправлены.
  7. Дополним множество good_words вторыми словами пар из словаря (они являются правильными словами), обладающими достаточной длиной. Добавление производится так, чтобы в good_words не возникло повторов. После этого good_words по-прежнему будет обладать свойствами, упомянутыми в п. 4.
  8. Главный цикл. Для каждого слова из source, кроме пустых слов в начале и конце, выполняем:
    а) если длина слова меньше минимальной, на этом его обработка завершается;
    б) если оно есть в good_words, значит, это допустимое слово, на этом его обработка завершается;
    в) если оно есть в bad_words, значит, в словаре уже есть пара для исправления этого слова, на этом его обработка завершается;
    г) берём контекст текущего слова — слова слева и справа от него — и составляем из них ключ (строку из этих двух слов, разделённых запятой). Если в хеше by_context нет элемента с таким ключом, значит, в отредактированном тексте нет ни одного слова достаточной длины с таким контекстом, и на этом обработка текущего слова завершается;
    д) внутренний цикл. Для каждого слова из массива, соответствующего вышеуказанному ключу в хеше by_context (обозначим это слово j, а текущее слово главного цикла — w), выполняем:
    • если j отличается от w только регистром, то на этом обработка j завершается;
    • если j не может быть исправлением w, то на этом обработка j завершается;
    • если пара w -> j есть в множестве bad_pairs (там хранятся отвергнутые пользователем пары, изначально оно пусто), то на этом обработка j завершается;
    • предъявляем пользователю пару w -> j. Если он подтвердил её, то добавляем её в словарь, добавляем w в bad_words, и внутренний цикл прерывается. А если пользователь отверг пару, то добавляем её в множество bad_pairs, и на этом обработка j завершается.

Рассмотрим работу алгоритма на примере.

Исходный текст:
Также, когда я взял телефон, то обратил внимание на его дисплэй.

Отредактированный текст:
Также, взяв телефон, я обратил внимание на его дисплей.

Словарь отсутствует.

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

  1. Массив source будет следующим: «», «Также», «когда», «я», «взял», «телефон», «то», «обратил», «внимание», «на», «его», «дисплэй», «».
  2. Массив edited будет следующим: «», «Также», «взяв», «телефон», «я», «обратил», «внимание», «на», «его», «дисплей», «».
  3. Множество temp будет содержать элементы «,Также,взяв», «Также,взяв,телефон», «взяв,телефон,я», «я,обратил,внимание», «обратил,внимание,на», «его,дисплей,». Элементы «телефон,я,обратил», «внимание,на,его», «на,его,дисплей» не будут добавлены к множеству, так как у них длина второго слова не является достаточной.
    Хеш by_context и дополнительные массивы будут следующими:
    Таблица 1
    Ключ Значение
    «,взяв» Ссылка на массив из одного элемента «Также»
    «Также,телефон» —/— «взяв»
    «взяв,я» —/— «телефон»
    «я,внимание» —/— «обратил»
    «обратил,на» —/— «внимание»
    «его,» —/— «дисплей»
  4. Множество good_words будет содержать слова «Также», «взяв», «телефон», «обратил», «внимание», «дисплей».
  5. Так как словаря нет, переходим к п. 8.
  6. Рассмотрим обработку каждого слова из source, кроме пустых слов в начале и конце:
    • «Также»: длина слова не меньше минимальной; оно есть в good_words — на этом его обработка завершается;
    • «когда»: длина слова не меньше минимальной; его нет в good_words; его нет в bad_words; ключ, составленный из слов слева и справа от него, будет «Также,я», в хеше by_context нет элемента с таким ключом — на этом обработка слова завершается;
    • «я»: длина слова меньше минимальной — на этом его обработка завершается;
    • «взял»: длина слова не меньше минимальной; его нет в good_words; его нет в bad_words; ключ, составленный из слов слева и справа от него, будет «я,телефон», в хеше by_context нет элемента с таким ключом — на этом обработка слова завершается;
    • «телефон»: длина слова не меньше минимальной; оно есть в good_words — на этом его обработка завершается;
    • «то»: длина слова меньше минимальной — на этом его обработка завершается;
    • «обратил»: длина слова не меньше минимальной; оно есть в good_words — на этом его обработка завершается;
    • «внимание»: длина слова не меньше минимальной; оно есть в good_words — на этом его обработка завершается;
    • «на»: длина слова меньше минимальной — на этом его обработка завершается;
    • «его»: длина слова меньше минимальной — на этом его обработка завершается;
    • «дисплэй»: длина слова не меньше минимальной; его нет в good_words; его нет в bad_words; ключ, составленный из слов слева и справа от него, будет «его,»; в хеше by_context есть элемент с таким ключом, ему соответствует массив из одного слова «дисплей»; оно отличается от «дисплэй» не только регистром; оно может быть исправлением слова «дисплэй»; пары «дисплэй -> дисплей» нет в множестве bad_pairs; предъявляем её пользователю; допустим, он подтвердил её; добавляем её в словарь, добавляем «дисплэй» в bad_words, и на этом обработка слова «дисплэй» завершается.

В итоге словарь будет содержать: дисплэй -> дисплей.

Отмечу, что если бы для обработки исходных данных этого примера применялся первоначальный алгоритм, то пользователю была бы предъявлена ещё и пара «взял -> взяв», от которой он отказался бы, но всё равно потратил бы время на её рассмотрение. В данном же примере эта пара не предъявляется, ведь у слов «взял» в исходном тексте и «взяв» в отредактированном тексте — разный контекст: у первого — слова «я» и «телефон», а у второго — «Также» и «телефон».

Дальнейшие усовершенствования

Когда часть программы, отвечающая за создание или дополнение словаря, была переписана в соответствии с вышеизложенным алгоритмом, мои ожидания оправдались: программа предъявляла пользователю меньше неподходящих пар. Затем я решил расширить её возможности.

Допустим, при дополнении словаря была добавлена пара «тедефона -> телефона». Такая ошибка может встретиться и в других подобных словах: например, может быть написано «тедефоном» вместо «телефоном» и т.д. Неплохо было бы исправлять и такие ошибки. Чтобы не добавлять в словарь все возможные пары, где различаются только окончания, я решил сделать так: пусть символ «~» в конце слова означает любое окончание (в т.ч. пустое), при этом в качестве слова для замены указывается то, на что будет заменено начало слова (окончание исправленного слова остаётся каким было). Таким образом, для рассматриваемого примера вместо исходной пары можно было бы записать такую: «тедефон~ -> телефон».

Также пусть «~» в начале слова означает любое начало (в том числе пустое). Допустим, нужно исправлять ошибки в словах «кто-нибуть», «когда-нибуть» и т.п., где требуется исправить окончание «-нибуть» на «-нибудь». Тогда, чтобы не создавать отдельную пару для каждого такого слова, можно записать универсальную пару «~-нибуть -> -нибудь».

Соответственно, поставив «~» и в начале и в конце, мы получим возможность заменять середину слова, оставляя без изменений его начало и окончание. Это тоже может быть полезным. Например, нужно исправлять слова с корнем «объект», если он неправильно записан как «обьект» (примеры ошибочных слов: «обьекта», «необьективному»). Для этого можно добавить в словарь пару «~обьект~ -> объект».

Естественно, при использовании «~» надо следить, чтобы под получившуюся универсальную замену не попали какие-либо другие слова, не те, что задумывалось. Иначе при исправлении текста они окажутся искажены.

Ещё одна идея по улучшению программы была такой. Поскольку исправление производится с помощью оператора поиска-замены (о чём уже говорилось в начале статьи), а в этом операторе предусмотрено использование регулярных выражений, то естественным было бы использовать регулярные выражения и в парах словаря (далее этому посвящён отдельный раздел статьи — «Регулярные выражения»).

Так как теперь в парах словаря могут быть не только слова, но и выражения, то далее я буду вместо «первое слово пары», «второе слово пары» писать «первый член пары», «второй член пары».

Следующая идея. При использовании программы было такое неудобство. Допустим, при пополнении словаря добавилась пара «выгледит -> выглядит». Но исправление будет происходить лишь в точности для такого слова, а если оно расположено в начале предложения и, соответственно, начинается с прописной буквы, то исправления не будет. Чтобы оно осуществлялось, придётся вручную добавить в словарь пару «Выгледит -> Выглядит». И наоборот, если в словаре есть пара «Выгледит -> Выглядит», то не будет производиться исправление «выгледит -> выглядит», пока не добавишь такую пару.

И я решил: если оба члена находящейся в словаре пары начинаются с прописной буквы или оба со строчной, то при исправлении текста пусть всё происходит так, как если бы в словаре вместо этой пары было две: у одной оба члена начинаются со строчной буквы, а у другой — с прописной, чтобы не приходилось вручную добавлять пары с другим регистром первой буквы.

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

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

  1. Заполним массивы dict_bad и dict_good (изначально пустые) данными из словаря: для каждой взятой из словаря пары первый её член добавляется в dict_bad, а второй — в dict_good; вдобавок к этому если оба члена пары начинаются с прописной буквы или оба со строчной, то регистр первой буквы каждого члена пары меняется на противоположный, и члены получившейся пары точно так же добавляются в dict_bad и dict_good.
  2. Главный цикл. Для каждой пары соответствующих друг другу (т.е. имеющих одинаковый индекс) элементов dict_bad и dict_good (обозначим эти элементы bad и good) выполняем следующее:
    а) сформируем оператор поиска-замены так, что выражением для замены будет good, а регулярным выражением для поиска будет: bad, перед которым добавлено подвыражение «(?<![[:alpha:]'-])», а после bad — подвыражение «(?![[:alpha:]'-])». Но если bad начинается с «~», то этот «~» убирается и подвыражение перед bad не добавляется. Аналогично, если bad заканчивается на «~», то этот «~» убирается и подвыражение после bad не добавляется;
    б) выполним сформированный оператор поиска-замены применительно к исправляемому тексту.

Алгоритм создания или дополнения словаря также претерпел изменения. Он приведён в следующем разделе.

Окончательный алгоритм создания или дополнения словаря

Первые 5 пунктов — такие же, как в предыдущем варианте этого алгоритма. Далее идут следующие пункты.

  1. Заполним массивы dict_bad и dict_good (изначально пустые) данными из словаря: для каждой взятой из словаря пары первый её член добавляется в dict_bad, а второй — в dict_good; вдобавок к этому если оба члена пары начинаются с прописной буквы или оба со строчной, то регистр первой буквы каждого члена пары меняется на противоположный и члены получившейся пары точно так же добавляются в dict_bad и dict_good.
  2. Заполним множество bad_words и массив bad_expr (изначально пустые), а также дополним множество good_words. Для всего этого выполняем следующие действия с каждой парой соответствующих друг другу (т.е. имеющих одинаковый индекс) элементов dict_bad и dict_good:
    a) если первый член пары — просто слово (не выражение), то при условии, что это слово достаточной длины, добавляем его в bad_words (если его там нет).
    Иначе (первый член пары — выражение) создаём на его основе регулярное выражение (оно нужно, чтобы впоследствии можно было проверять, описывается ли некоторое слово исходным выражением) путём следующих изменений:
    • если оно не начинается с «~», добавляем в начале «^»;
    • если оно не заканчивается на «~», добавляем в конце «$»;
    • если оно начинается на «~», удаляем этот «~»;
    • если оно заканчивается на «~», удаляем этот «~».
    Сформированное регулярное выражение добавляем в bad_expr. Замечу, что нет требования, чтобы исходное выражение было достаточной длины (так как выражение может описывать слова большей длины, чем длина его самого);
    б) если оба члена пары — просто слова (а не выражения), то считаем второй член пары допустимым словом и, если его длина достаточна, помещаем его в good_words (если его там нет). Одного лишь факта, что второй член пары подходит под определение слова, ещё недостаточно для утверждения, что это допустимое слово. Это может быть часть слова, например, в паре «безшумн~ -> бесшумн». Поэтому и нужна проверка обоих членов пары.
    Полученное множество bad_words будет обладать тем свойством, что слова в нём не повторяются, являются неправильными и для них в словаре уже содержится информация о том, как они должны быть исправлены. Полученный массив bad_expr будет обладать тем свойством, что слова, соответствующие находящимся в нём регулярным выражениям, являются неправильными и для них в словаре уже содержится информация о том, как они должны быть исправлены. Множество good_words после дополнения по-прежнему будет обладать свойствами, упомянутыми в п. 4.
  3. Главный цикл. Для каждого слова из source, кроме пустых слов в начале и конце, выполняем:
    а) если длина слова меньше минимальной — на этом его обработка завершается;
    б) если оно есть в good_words — значит, это допустимое слово, на этом его обработка завершается;
    в) если оно есть в bad_words либо подходит к какому-либо выражению из bad_expr — значит, в словаре уже есть пара для исправления этого слова, на этом его обработка завершается;
    г) берём контекст текущего слова — слова слева и справа от него — и составляем из них ключ (строку из этих двух слов, разделённых запятой). Если в хеше by_context нет элемента с таким ключом, значит, в отредактированном тексте нет ни одного слова достаточной длины с таким контекстом, и на этом обработка текущего слова завершается;
    д) внутренний цикл. Для каждого слова из массива, соответствующего вышеуказанному ключу в хеше by_context (обозначим это слово j, а текущее слово главного цикла — w), выполняем:
    • если j отличается от w только регистром, то на этом обработка j завершается;
    • если j не может быть исправлением w, то на этом обработка j завершается;
    • если пара w -> j есть в множестве bad_pairs (там хранятся отвергнутые пользователем пары, изначально оно пусто), то на этом обработка j завершается;
    • предъявляем пользователю пару w -> j. Если он подтвердил её, то добавляем её в словарь, добавляем w в bad_words (а если оба слова пары начинаются с букв одного регистра, то дополнительно добавляем в bad_words первое слово с изменённым регистром первой буквы), и внутренний цикл прерывается. Если же пользователь отверг пару, то добавляем её в множество bad_pairs (а если оба слова пары начинаются с букв одного регистра, то добавляем и пару, где у каждого слова регистр первой буквы изменён на противоположный), и на этом обработка j завершается.

Рассмотрим работу алгоритма на простом примере.

Исходный текст:
Цена вполне соответсвует его качетсву.

Отредактированный текст:
Цена вполне соответствует его качеству.

Исходный словарь:
воообще -> вообще, соответсв~ -> соответств, вс(ё|е)таки -> вс$1-таки.

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

  1. Массив source будет следующим: «», «Цена», «вполне», «соответсвует», «его», «качетсву», «».
  2. Массив edited будет следующим: «», «Цена», «вполне», «соответствует», «его», «качеству», «».
  3. Множество temp будет содержать элементы «,Цена,вполне», «Цена,вполне,соответствует», «вполне,соответствует,его», «его,качеству,». Элемент «соответствует,его,качеству» не будет добавлен к множеству, так как у него длина второго слова не является достаточной.
    Хеш by_context и дополнительные массивы будут следующими:
    Таблица 2
    Ключ Значение
    «,вполне» Ссылка на массив из одного элемента «Цена»
    «Цена,соответствует» —/— «вполне»
    «вполне,его» —/— «соответствует»
    «его,» —/— «качеству»
  4. Множество good_words будет содержать слова «Цена», «вполне», «соответствует», «качеству».
  5. Так как словарь есть, к п. 8 не переходим.
  6. Массивы dict_bad и dict_good будут следующими:
    Таблица 3
    Индекс dict_bad dict_good
    0 воообще вообще
    1 Воообще Вообще
    2 соответсв~ соответств
    3 Соответсв~ Соответств
    4 вс(ё|е)таки вс$1-таки
    5 Вс(ё|е)таки Вс$1-таки
  7. Рассмотрим обработку каждой пары из dict_bad и dict_good:
    • «воообще» и «вообще»: первый член пары — «воообще» — просто слово; оно достаточной длины; в bad_words его нет; добавляем его в bad_words. Оба члена пары — просто слова; длина второго члена пары — «вообще» — достаточна; в good_words его нет; добавляем его в good_words;
    • «Воообще» и «Вообще»: первый член пары — «Воообще» — просто слово; оно достаточной длины; в bad_words его нет; добавляем его в bad_words. Оба члена пары — просто слова; длина второго члена пары — «Вообще» — достаточна; в good_words его нет; добавляем его в good_words;
    • «соответсв~» и «соответств»: первый член пары — «соответсв~» — выражение; создаём на его основе регулярное выражение «^соответсв», которое добавляем в bad_expr. Неверно, что оба члена пары — просто слова; на этом обработка пары заканчивается;
    • «Соответсв~» и «Соответств»: первый член пары — «Соответсв~» — выражение; создаём на его основе регулярное выражение «^Соответсв», которое добавляем в bad_expr. Неверно, что оба члена пары — просто слова; на этом обработка пары заканчивается;
    • «вс(ё|е)таки» и «вс$1-таки»: первый член пары — «вс(ё|е)таки» — выражение; создаём на его основе регулярное выражение «^вс(ё|е)таки$», которое добавляем в bad_expr. Неверно, что оба члена пары — просто слова; на этом обработка пары заканчивается;
    • «Вс(ё|е)таки» и «Вс$1-таки»: первый член пары — «Вс(ё|е)таки» — выражение; создаём на его основе регулярное выражение «^Вс(ё|е)таки$», которое добавляем в bad_expr. Неверно, что оба члена пары — просто слова; на этом обработка пары заканчивается.
    В итоге множество bad_words будет содержать слова «воообще», «Воообще»; массив bad_expr будет содержать выражения «^соответсв», «^Соответсв», «^вс(ё|е)таки$», «^Вс(ё|е)таки$»; множество good_words будет содержать слова «Цена», «вполне», «соответствует», «качеству», «вообще», «Вообще».
  8. Рассмотрим обработку каждого слова из source, кроме пустых слов в начале и конце:
    • «Цена»: длина слова не меньше минимальной; оно есть в good_words, на этом его обработка завершается;
    • «вполне»: длина слова не меньше минимальной; оно есть в good_words, на этом его обработка завершается;
    • «соответсвует»: длина слова не меньше минимальной; его нет в good_words; его нет в bad_words, но оно подходит к выражению «^соответсв» из bad_expr, на этом его обработка завершается;
    • «его»: длина слова меньше минимальной, на этом его обработка завершается;
    • «качетсву»: длина слова не меньше минимальной; его нет в good_words; его нет в bad_words, и оно не подходит ни к какому выражению из bad_expr; ключ, составленный из слов слева и справа от него, будет «его,»; в хеше by_context есть элемент с таким ключом, ему соответствует массив из одного слова «качеству»; оно отличается от «качетсву» не только регистром; оно может быть исправлением слова «качетсву»; пары «качетсву -> качеству» нет в множестве bad_pairs; предъявляем её пользователю; допустим, он подтвердил её; добавляем её в словарь, добавляем «качетсву» в bad_words; так как оба слова пары начинаются с букв одного регистра, то дополнительно добавляем в bad_words слово «Качетсву»; на этом обработка слова «качетсву» завершается.

В итоге словарь станет таким: воообще -> вообще, соответсв~ -> соответств, вс(ё|е)таки -> вс$1-таки, качетсву -> качеству.

Окончательная программа. Руководство пользователя

Текст окончательной версии программы, подробно прокомментированный, содержится в файле autocorr.pl, который вы можете найти в приложении к данной статье. Программа написана на Perl (я использовал ActivePerl 5.8.6.811 для Windows — http://www.ActiveState.com/ActivePerl/).

К программе также прилагается пример файла со словарём (default.dic), где содержится ряд используемых мной замен. Если они вам подходят, то можете пользоваться; конечно, можете и изменить этот словарь — что-то убрать, что-то добавить.

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

После «/a» указываются следующие аргументы:

После «/c» указываются:

Если программа запущена без аргументов либо с неверными аргументами, то она выведет краткую справку по синтаксису её вызова (это можно использовать, чтобы вспомнить, какие аргументы и в каком порядке надо указывать: просто запускаем программу без аргументов и читаем выведенную справку).

При нормальном завершении программы код выхода будет нулевым, а при ошибке — нет.

Все файлы, обрабатываемые программой (тексты, словарь), должны быть в кодировке Windows-1251.

Когда программа работает в режиме дополнения словаря, она выводит пары слов, которые могут быть добавлены в словарь, и для каждой пары ждёт решения пользователя. Если пара действительно должна быть добавлена (то есть соответствующая замена всегда должна выполняться) — надо нажать «y» и затем Enter; иначе — «n» и Enter. На рис. 1 показано, как выглядит экран при работе программы в этом режиме.

Рис. 1

После того как программа окончила работу, можно в текстовом редакторе просмотреть дополненный словарь (добавленные пары находятся в его конце и отделены пустой строкой от предшествующего содержимого) и, если нужно, добавить комментарии, внести изменения. Например, в словарь была добавлена пара «телеофна -> телефона», а можно изменить её на «телеофн~ -> телефон» — тогда окончание ошибочного слова может быть любым, в результате больше ошибок будет исправлено.

Теперь рассмотрим работу программы в режиме исправления текста. Она выводит информацию о том, какие замены сколько раз были произведены (в виде «пара: количество замен»), а в конце — общее количество произведённых замен, то есть исправленных слов (рис. 2).

Рис. 2

Если эта информация важна для вас, но не умещается на экране или же вы просто хотите иметь возможность просмотреть её в любое удобное время, то можно перенаправить вывод программы в файл. Для этого при запуске программы добавьте в конце командной строки символ «>» и сразу за ним — имя этого файла. А если вместо «>» указать «>>», то информация будет дописана к этому файлу — это может быть полезным, если программа запускается несколько раз и надо сохранить всю статистику в одном файле.

Замечу, что программу нельзя использовать для исправления текста, в котором есть переносы слов: конец перенесённого слова может быть принят программой за неправильное слово и заменён, что лишь навредит; также, если неправильное слово разбито переносом, то программа его «не узнает» и, соответственно, не исправит.

О формате файла со словарём. Это обычный текстовый файл в кодировке Windows-1251, его имя и расширение могут быть произвольными (я использую расширение «dic», от «dictionary» — «словарь»). Каждая строка этого файла может состоять из трёх частей, расположенных в следующем порядке:

Каждая из этих частей является необязательной. Таким образом, в файл со словарём могут быть помещены, например, пустые строки (скажем, чтобы отделять друг от друга какие-то группы пар) и строки, состоящие только из комментариев. Вы можете увидеть это в прилагаемом файле default.dic.

Регулярные выражения

Кратко опишу основные конструкции, применяемые в регулярных выражениях Perl, показывая на примерах их использование в парах словаря.

1. Класс символов

Это конструкция вида «[abc]» (то есть заключённая в квадратные скобки последовательность символов). Она соответствует любому из этих символов.

Можно указывать диапазоны: например, «[а-гп]» эквивалентно «[абвгп]». То есть символ «-» используется в специальных целях — для обозначения диапазона между символами слева и справа от него. Чтобы «-» интерпретировался как обычный символ, надо поместить его в самом начале или конце класса.

Замечу, что в моей программе используется кодировка Windows-1251, поэтому диапазон «а—я» не содержит букву «ё» (так как её код в этой кодировке не входит в диапазон от кода буквы «а» до кода буквы «я»). Точно так же и диапазон «А—Я» не содержит «Ё».

Символ «^» сразу после открывающей квадратной скобки означает отрицание. Например, «[^аб]» — любой символ, кроме «а» и «б».

Пример использования. Пусть надо исправлять ошибочные написания «лудше» и «лутше» на правильное «лучше». Тогда вместо двух пар:

лудше -> лучше
лутше -> лучше

можно использовать одну, первый член которой будет регулярным выражением, соответствующим словам «лудше» и «лутше». Это выражение может быть таким: «лу[дт]ше». В итоге получится такая пара:

лу[дт]ше -> лучше.

2. Альтернатива

Это конструкция вида «(A|B|C)», то есть заключённая в скобки последовательность подвыражений, разделённых символом «|». Подвыражение может быть, в частности, пустым. Такая конструкция соответствует участку текста, если ему соответствует какое-либо из указанных подвыражений (они перебираются слева направо).

Пример использования. Пусть надо исправлять ошибочные написания «отсутсвие» и «отсуствие» на правильное «отсутствие». Тогда вместо двух пар:

отсутсвие -> отсутствие
отсуствие -> отсутствие

можно использовать одну, первый член которой будет регулярным выражением, соответствующим словам «отсутсвие» и «отсуствие». Это выражение может быть таким: «отсу(тс|ст)вие». В итоге получится такая пара:

отсу(тс|ст)вие -> отсутствие.

Между прочим, если написать так:

(отсутсвие|отсуствие) -> отсутствие,

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

3. Указание количества повторов

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

Таблица 4
Конструкция Количество повторов
* 0 или более
+ 1 или более
? 0 или 1
{n} n
{n,} n или более
{n,m} от n до m включительно

Пример использования. Пусть надо исправлять «видео-плеер» и «видео-плейер» на «видеоплеер». Тогда вместо двух пар:

видео-плеер -> видеоплеер
видео-плейер -> видеоплеер

можно использовать одну, первый член которой будет регулярным выражением, соответствующим словам «видео-плеер» и «видео-плейер». Это выражение может быть таким: «видео-плей?ер» («?» после «й» указывает на количество повторов 0 или 1, то есть эта буква может быть или не быть). В итоге получится такая пара:

видео-плей?ер -> видеоплеер.

4. Условия

Это конструкции следующего вида:
(?<=A) — «то, что слева, соответствует подвыражению A»;
(?<!A) — «то, что слева, не соответствует подвыражению A»;
(?=A) — «то, что справа, соответствует подвыражению A»;
(?!A) — «то, что справа, не соответствует подвыражению A».

Пример использования. Допустим, слова «пиктограмма», «пиктограммы» и т.д. могут быть записаны с ошибкой — с одной «м» вместо двух, и надо создать пару для исправления этого. Тут нельзя просто записать:

пиктограм~ -> пиктограмм,

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

пиктограм(?!м)~ -> пиктограмм.

5. Подстановка

Если регулярное выражение содержит подвыражения, заключённые в скобки, то при его успешном сопоставлении некоторому участку текста происходит запоминание во встроенных переменных $1, $2 и т.д. фрагментов этого текста, соответствующих этим подвыражениям. Участок текста, соответствующий всему выражению, запоминается в переменной $&. Выражение для замены, в свою очередь, может содержать «$1», «$2» и т.д., «$&», и при выполнении замены будет произведена подстановка значений этих переменных.

Пример использования. Допустим, слово «выбранный» и его изменения по числам, родам, падежам могут быть записаны с ошибкой — с одной «н» вместо двух, и надо создать пару для исправления этого. Если по аналогии с предыдущим примером использовать пару «выбран(?!н)~ -> выбранн», это не подойдёт, так как хотя ошибочные слова и будут исправлены, но будет добавляться лишняя «н» в слова «выбран», «выбрана»…

Пойдём другим путём — перечислим все возможные окончания исправляемого слова с помощью конструкции «альтернатива»: «(ый|ого|ому|ым|ом|ая|ой|ую|ое|ые|ых|ыми)». Данная конструкция является подвыражением в скобках, и больше никаких подвыражений в скобках мы не используем; таким образом, окончание исправляемого слова будет запоминаться в переменной $1. А чтобы в исправленном слове было то же окончание, что и в исходном, ставим во втором члене пары (то есть в выражении для замены) «$1» вместо этого окончания. В итоге получится такая пара:

выбран(ый|ого|ому|ым|ом|ая|ой|ую|ое|ые|ых|ыми) -> выбранн$1.

Подробнее узнать о регулярных выражениях Perl можно в документации к Perl, а также на русском языке — например, по следующим ссылкам:

http://www.opennet.ru/docs/RUS/perlre_man/
http://www.pereplet.ru/nauka/perl/regex.shtml

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

Попробуем составить выражение, соответствующее ошибочным написаниям. Вместо двух «к» может быть одно — то есть может быть либо одно, либо два «к», это может быть записано как «кк?». Вместо первой «у» может быть «о» или «а» — то есть эта буква может быть одной из «о», «а», «у», это может быть записано как «[оау]». Вместо одного «л» может быть два — то есть может быть либо одно, либо два «л», это может быть записано как «лл?». Итого, все ошибочные написания начала слова могут быть описаны выражением «акк?[оау]мулл?ятор». Так как окончание слова может быть любым, добавляем «~» в конце: «акк?[оау]мулл?ятор~».

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

Чтобы выражение перестало соответствовать правильному написанию, добавим в конце (но перед «~», поскольку «~» должно быть в самом конце) соответствующее условие. Получится выражение «акк?[оау]мулл?ятор(?<![Аа]ккумулятор)~». В итоге будет такая пара:

акк?[оау]мулл?ятор(?<![Аа]ккумулятор)~ -> аккумулятор.

Почему в условии написано «[Аа]ккумулятор», а не просто «аккумулятор»? Дело в том, что оба члена пары начинаются с буквы в одном регистре, а значит, исправление текста будет происходить так, как если бы в словаре вместо этой пары было две: у одной оба члена начинаются со строчной буквы, а у другой — с прописной. То есть первая буква исправляемого слова может быть и «а», и «А», что и отражено в условии.

Исправление ошибочного слитного и раздельного написания

В программе не предусмотрено, чтобы при дополнении словаря находились и предъявлялись пользователю пары для исправления таких ошибок, как слитное написание вместо раздельного, и наоборот (к слитному написанию я здесь, для простоты классификации, отношу и написание через дефис). Однако такие пары могут быть добавлены в словарь вручную, например:

небыло -> не было
несчем -> не с чем
в замен -> взамен
во первых -> во-первых

При этом есть одно обстоятельство, относящееся к случаю, когда ошибочное раздельное написание заменяется на слитное. Для примера возьмём пару «в замен -> взамен». Соответствующая замена будет осуществляться, только если в обрабатываемом тексте между «в» и «замен» находится строго один пробел.

А если слова в тексте могут быть разделены несколькими пробелами и (или) табуляциями? Как сделать, чтобы и в этом случае происходила замена? Вспомним о регулярных выражениях. В первом члене пары вместо пробела запишем выражение «[ \t]+» («\t» — обозначение табуляции). Получится пара «в[ \t]+замен -> взамен». Теперь замена произойдёт, если между «в» и «замен» в тексте находится 1 или более пробелов и (или) табуляций.

Если же слова в тексте всегда разделяются либо пробелом, либо переводом строки, то аналогичным образом используем выражение «[ \n]» («\n» — обозначение перевода строки), получится пара «в[ \n]замен -> взамен». А если слова могут разделяться либо последовательностью пробелов и (или) табуляций, либо переводом строки, используем выражение «([ \t]+|\n)» — получится пара «в([ \t]+|\n)замен -> взамен».

«Почти хорошие» ошибки

Бывают такие ошибки (назовём их «почти хорошими»), которые не являются «хорошими», но их исправление с помощью автозамены лишь в редких случаях приведёт к появлению новых ошибок, а чаще всего будут исправлены настоящие ошибки (это, конечно, зависит от количества и характера ошибок в обрабатываемом тексте). Допустим, в тексте часто встречается ошибочное написание «что бы» вместо «чтобы», и редко встречается такое сочетание «что бы», которое является правильным; тогда автозамена «что бы -> чтобы» как раз и приведёт к вышеуказанным последствиям.

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

Можно сделать так. Пусть при автоисправлении таких ошибок добавляется специальный символ в том месте, где произошло исправление. Этот символ не должен встречаться в тексте и должен выглядеть так, чтобы привлекать внимание при просмотре текста. Я использую символ с кодом 30 — в Dos Navigator, где я редактирую тексты, он отображается в виде треугольника. Из-за возможных проблем с его отображением в этой статье, я буду вместо него использовать символ «^». Вы можете выбрать и другой символ (только не «#», потому что он в строке словаря означает начало комментария). Таким образом, для исправления ошибки, взятой в качестве примера, в словарь должна быть добавлена пара «что бы -> что^бы».

Что дальше? После запуска программы, при проверке обработанного текста надо обращать внимание на появления этого символа. Встретили «что^бы» — значит, тут была произведена автозамена «что бы -> что^бы», и надо подумать, действительно ли в результате оказалась исправлена ошибка. Если нет, то надо исправить обратно: «что^бы» на «что^ бы» («^» убирать не обязательно). А когда весь текст проверен, остаётся только автоматически удалить из него все символы «^» (например, просто в текстовом редакторе заменить «^» на пустую строку).

Разное

В словарь можно добавлять и пары вида «несколько слов -> несколько слов», например:

не разу -> ни разу.

При этом если в обрабатываемом тексте слова разделяются не всегда в точности одним пробелом, то, чтобы замена выполнялась, в первом члене пары нужно вместо пробелов использовать соответствующие регулярные выражения (см. «Исправление ошибочного слитного и раздельного написания»).

* * *

Чтобы обеспечить написание некоторого слова с прописной буквы, удобно использовать такую пару:

слово -> \u$&.

При выполнении замены вместо «$&» подставится заменяемое слово, а «\u» означает «преобразовать в верхний регистр следующий символ» (если он является буквой). Поэтому первая буква слова и станет прописной.

Вместо какого-то конкретного слова может быть и регулярное выражение, например:

москв(а|ы|е|у|ой) -> \u$&.

* * *

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

(?<!")слово(?!") -> "$&".

Здесь «(?<!")» и «(?!")» — условия: соответственно «слева от слова нет кавычки» и «справа от слова нет кавычки».

Заключать в кавычки можно и не только какое-то одно конкретное слово, например:

(?<!")Мир(а|у|ом|е|) ПК(?!") -> "$&".

* * *

Если имеется набор некоторых текстов и набор соответствующих отредактированных текстов и вы хотите использовать их для дополнения словаря, то можно не запускать каждый раз программу для очередной пары «исходный текст, отредактированный текст», а поступить проще. Сформируем файл со склеенными исходными текстами (например, с помощью команды copy) и файл со склеенными отредактированными текстами (которые могут даже следовать в другом порядке, нежели исходные), после чего используем эти два файла для дополнения словаря. Потребуется лишь один запуск программы. Причём так можно делать, даже если не для каждого исходного текста есть соответствующий ему отредактированный и наоборот.

* * *

Программу можно использовать не только для исправления ошибок, но и просто для замены одних слов на другие. Также можно применять её для анализа текстов, используя тот факт, что она, будучи запущена в режиме исправления ошибок, выводит статистику замен. Пусть, например, надо узнать, сколько раз встречается в некотором тексте слово «Windows», а сколько раз слово «Linux». Создаём словарь, где эти слова заменялись бы на что-то (пусть сами на себя):

Windows -> $&
Linux -> $&

Затем запускаем программу в режиме исправления ошибок для этого текста с этим словарём (файл, куда будет записан исправленный текст, нам не понадобится, поэтому вместо его имени указываем «nul») и смотрим на выведенную статистику.

Приложение

Скачать описанную программу и пример файла со словарём (19 КБ ZIP)
Страница Ивана Рощина > Статьи >