Андрей Колесов
© Андрей Колесов, 2001В данном случае мы будем говорить о программе, реализованной в среде Word 2000/VBA, но речь будет идти о достаточно общих вопросах разработки. Мы будем использовать весьма тривиальные конструкции с достаточно прозрачным синтаксисом.
1 августа 2000 года я получил письмо от Максима, программиста из Риги, следующего содержания:
Здравствуйте! У меня вопрос по поводу кодировок в VBA for Word: мне
нужно перенести текст (английский, русский, латышский) из
DOS'овской кодировки в Unicode!...
(Далее шла информация о
различных ситуациях на компьютерах с разными OC и различными
региональными установками.)
Отмечу сразу: кто и кому отправляет письмо — из текста было непонятно (Максим решил представиться лишь несколько писем спустя). Хорошо, что хоть прислал в приложении программный пример (проект Word 2000 и исходный текст, который надо перекодировать), который приведен в листинге 1 (повторяющиеся конструкции мною изъяты).
Как раз в эти дни у меня была очередная запарка на работе, однако вопрос был довольно интересным и принципиальным. В своем ответе на первое письмо Максима я написал:
вместо:
tmp = StrConvW(Chr(k1), vbFromUnicode) ' Ваш вариантнужно написать:
tmp = StrConv(Chr(k1), vbFromUnicode) ' мой вариант
И вообще проще записать всю эту функцию в одну строку:
Conv = StrConv(StrConv(str, vbFromUnicode), vbUnicode, reg_to)
После такого исправления тест начинает работать правильно. Но есть некоторые сомнения, что программа будет верно функционировать в более общем случае, с другим содержимым исходных файлов.
Дело в том, что весь ваш программный код мне показался слишком запутанным и неудачно оформленным. Конечно, это ваше личное дело, но именно запутанность кода сильно затрудняет отладку и локализацию ошибки.
Далее я высказал несколько более конкретных замечаний по присланной программе, чтобы Максим смог довести программу до рабочего состояния самостоятельно.
Продолжение следует — обмен мнениями
Однако мой ответ лишь положил начало весьма активной двухдневной переписке с обсуждением некоторых философских проблем программирования, на которых хотелось бы акцентировать внимание читателя. В ходе этой дискуссии по электронной почте Максим высказал следующие соображения:
В ответ на это я, в свою очередь, высказал следующее:
На пункт 4 Максим прислал мне короткий ответ — "хе-хе". Это заставило меня отложить на пару часов свои дела и написать для Максима готовое решение его задачи. Код этого варианта оказался примерно в два раза короче и работал в 25 раз быстрее. К тому же он работал правильно.
Далее я попытаюсь объяснить, в чем заключались основные ошибки программы Максима. Но пока приведу наш диалог еще по одному вопросу:
А.К.: Главный принцип программиста — ищи ошибки сначала у себя, а потом — в операционной системе и компьютере.
Максим: А я слышал о другом! Если не можешь найти ошибку, перепиши модуль заново.
Что тут скажешь? Нужно искать ошибку, поскольку переписывать программу можно всю жизнь, допуская одни и те же промахи.
Как привести программу в рабочее состояние
Итак, обратимся к присланной мне программе по перекодировке DOS'овских текстов в Word 2000 (листинг 1).
Проблема здесь заключается в следующем. В DOS'овской кодировке латинские, русские и специальные латышские буквы находятся в одной кодовой таблице 866. А в Unicode они попадают в три разные таблицы (региональные установки 1033, 1049 и 1062). Простое изменение региональных установок в ОС не позволяется преобразовывать данные для трех разных языков. Нужно же написать программу, которая правильно работает независимо от типа ОС и региональных установок конкретного компьютера.
Суть основных замечаний к присланному мне начальному варианту программы (я их изложил в первом же ответе Максиму) в следующем:
С учетом только этих замечаний можно преобразовать исходную программу Максима в более простой вариант (листинг 2). Я убрал ненужные операции преобразования данных и циклы (для частного варианта одного символа в строке), сделал явное определение переменных. Такой пример отлаживать гораздо проще.
После такого упрощения программы сразу выявляются некоторые принципиальные дефекты ее алгоритма.
Понятно, что задача перекодировки заключается в последовательном преобразовании DOS -> ANSI -> Unicode. Максим разделил эти два этапа обработки в виде последовательно выполняемых функций Convert и Conv (кроме отсутствия комментариев, отмечу также выбор крайне невыразительных имен переменных и процедур), что является принципиально неверным.
Дело в том, что в DOS русские и латышские символы имеют различные коды. Но вполне вероятно, что в ANSI-кодировке (Windows) некоторые символы имеют одинаковые значения (хотя в данном конкретном случае этого не происходит). Именно поэтому Максим вынужден в программе "жестко зашить" определение языка.
Обратите внимание, что именно поэтому необходимо изначально знать, что первая строка текстового файла — русская, вторая — английская, третья — латышская. Но если бы он объединил два этапа преобразования в одной процедуре, определение языка выполнялось бы автоматически!
И еще два важных замечания:
' Сразу формирует правильный Unicode! If kod > 127 And kod < 176 Then kod = kod + 64 + 848 ElseIf kod > 223 And kod < 240 Then kod = kod + 16 + 848 End if
Аналогично для латышского языка можно сразу заменить код ANSI на Unicode. Все это будет работать независимо от версии Word (в Word 97 нельзя указывать в функции StrConv значение региональной установки).
Радикальным решением этой проблемы является использование байтовых массивов вместо строковых переменных — в этом случае никаких преобразований при вводе/выводе не производится.
С учетом всего вышесказанного можно написать макрокоманду MyDecodeProcedure перекодировки англо-русско-латышского DOS-текста в Unicode (листинг 3). На мой взгляд, она имеет достаточно простую логическую структуру (на 90% ее можно отладить на уровне визуального изучения кода!) и самое главное — автоматически распознает язык введенного символа и гарантированно выполняет правильные преобразования независимо от версии Word и Windows, кодовых таблиц и региональных установок операционной системы.
За счет чего получено резкое повышение производительности?
Значительное повышение производительности получено благодаря:
Существует и еще один важный момент, который мы обсудим отдельно...
Варианты замены комбинаций спецсимволов
Как я уже упоминал, в одном из блоков присланной Максимом программы выполнялась замена комбинаций DOS'овских знаков некоторыми специальными символами, например, "__T" на "™". А поскольку эта задача вообще не имеет отношения к проблеме перекодировки национальных символов, данный программный код следовало бы вообще сразу убрать из текстового приложения. Однако, разобравшись с преобразованием текста на разных языках, вернемся именно к этому фрагменту, так как здесь был допущен целый ряд методических и технических ошибок, а именно:
Теперь попробуем подробнее рассмотреть допущенные в исходном варианте ошибки на следующем фрагменте кода Максима:
Dim Leng&, i&, kod&, kod2&, kod3& Leng = Len(myStr) For i = 1 To Leng If i > Leng Then Exit For kod = Asc(Mid(myStr, i, 1)) If i <= Leng - 2 Then kod2 = Asc(Mid(myStr, i + 1, 1)) kod3 = Asc(Mid(myStr, i + 2, 1)) If (kod = 95) And (kod2 = 95) And (kod3 = 79) Then myStr = Left(myStr, i - 1) + Chr(187) + Right(myStr, Leng - i - 2) Leng = Leng - 2 ElseIf (kod = 95) And (kod2 = 95) And (kod3 = 111) Then myStr = Left(myStr, i - 1) + Chr(188) + Right(myStr, Leng - i - 2) Leng = Leng - 2 ElseIf (kod = 95) And (kod2 = 95) And (kod3 = 131) Then myStr = Left(myStr, i - 1) + Chr(200) + Right(myStr, Leng - i - 2) Leng = Leng - 2 ElseIf (kod = 95) And (kod2 = 95) And (kod3 = 82) Then myStr = Left(myStr, i - 1) + "®" + Right(myStr, Leng - i - 2) Leng = Leng - 2 ElseIf (kod = 95) And (kod2 = 95) And (kod3 = 114) Then myStr = Left(myStr, i - 1) + "®" + Right(myStr, Leng - i - 2) Leng = Leng - 2 ElseIf (kod = 95) And (kod2 = 95) And (kod3 = 84) Then myStr = Left(myStr, i - 1) + "™" + Right(myStr, Leng - i - 2) Leng = Leng - 2 End If End If Next
В этом фрагменте в отличие от исходного варианта (листинг 1) заменено имя переменной str на mySrt (Str — встроенная функция VBA) и исключена проверка переменной Special (непонятно, зачем она вообще здесь была). Еще раз отмечу, что в исходном варианте никакого определения используемых переменных не было. Здесь приведен фрагмент преобразования только трехсимвольных комбинаций — в программе Максима далее следовало подобное преобразование 20 двухсимвольных комбинаций.
Отдельно следует отметить смысловую запутанность всего этого кода: совершенно непонятно, что имел в виду автор под Windows-кодами 187, 188 и 200 и DOS'овским 131. Но очевидно, что тут явно кроются будущие проблемы, хотя бы потому, что в кодовой таблице cp1251 код 200 соответствует русскому "И", а в других таблицах — еще чему-то (а ведь мы потом должны провести преобразования из ANSI в Unicode!).
И таких очевидных ошибок в блоке преобразования спецсимволов наберется еще не менее десятка.
ВНИМАНИЕ! Вы видите, что мы уже забыли о преобразовании русских и латышских символов, и долго размышляем, что хотел сделать автор с помощью такого хитроумного кода, не имеющего отношения к делу!?
Ну да ладно, если он хочет сделать такую замену кодов, пусть делает. Мы будем следовать формальному алгоритму и подумаем, как его можно модифицировать.
Что тут можно сказать? В VB/VBA 6.0 имеется готовая встроенная функция Replace, которая выполняет именно эти операции. С ее помощью вся эта конструкция будет выглядеть так:
myStr = Replace(myStr, "__O", "»")) myStr = Replace(myStr, "__o", "ј") myStr = Replace(myStr, "__" + Chr$(131), "И") myStr = Replace(myStr, "__R", "®") myStr = Replace(myStr, "__r", "®") myStr = Replace(myStr, "__T", "™")
Обратите внимание на два момента.
Правда, функция Replace появилась только в VB/VBA 6.0 (именно о новых строковых функциях много писалось в обзорах новшеств VBA, Максим работал с Word 2000). Но написать ее аналог (и оттестировать только одну процедуру!) можно довольно легко применительно к любой версии Basic (листинг 4).
Здесь явно несколько сугубо алгоритмических проблем:
i = 0 Do While i < Leng i = i +1 ... Loop
Еще лучше вообще отказаться от использования переменной Leng, ведь это просто текущая длина строки myStr! Таким образом, вместо:
Leng = Len(myStr) For i = 1 To Leng If i > Leng Then Exit For kod = Asc(Mid(myStr, i, 1)) If i <= Leng - 2 Then ... If (kod = 95) And (kod2 = 95) And (kod3 = 79) Then myStr = Left(myStr, i - 1) + Chr(187) + Right(myStr, Leng - i - 2) Leng = Leng - 2
можно написать (убрав одну строку из циклического повтора и заменив Right на Mid):
i = 0 Do While i < Len(myStr) i = i +1 kod = Asc(Mid(myStr, i, 1)) If i <= Len(MyStr) - 2 Then ... If (kod = 95) And (kod2 = 95) And (kod3 = 79) Then myStr = Left(myStr, i - 1) + Chr(187) + Mid(myStr, i + 3)
Здесь видны явные повторы кода при замене символов в строке. Конечно же, имеет смысл заменить:
If (kod = 95) And (kod2 = 95) And (kod3 = 79) Then myStr = Left(myStr, i - 1) + Chr(187) + Mid(myStr, i + 3)
на:
ii = 3 ... If (kod = 95) And (kod2 = 95) And (kod3 = 79) Then NewKod = 187: Gosub InsertStr ... InsertStr: ' можно использовать для замены фрагментов разной длины — ii myStr = Left(myStr, i - 1) + NewKod + Mid(myStr, i + ii) Return
Казалось бы, мы увеличили число строк кода, и это не очень хорошо. Однако это только так кажется, тем более, что самое главное в другом. Во-первых, мы упростили циклическую часть программы (где кроются основные проблемы с отладкой). Во-вторых, самое главное в выделении общих частей программы — повышение ее надежности и удобства отладки. Ведь, например, для замены Right на Mid нужно сделать исправление одной строки, а не 25 (а какую-то из них легко пропустить!).
If v0 And v1 Then ... If v0 And v2 Then ... If v0 And v3 Then ...должна быть заменена на:
If v0 Then If v1 Then ... If v2 Then ... If v3 Then ... End If
Второй вариант работает в 3-10 быстрее, если учесть, что в 99% случаев (а спецсимволы встречают очень редко!) v0 = False, а выражения v1-v3 нужно вычислять. Теперь понятно, почему алгоритм Максима (v1 — v25 !) работал так медленно? Вместо того чтобы проверить, не является ли текущий символ подчеркиванием, он зачем-то проверял всевозможные комбинации. Не говоря уже о том, что вместо If Then ElseIf.. лучше применять Select Case. Тогда вместо начального кода из 28 строк получится такая элегантная конструкция:
i = 0 Do While i < Len(myStr) i = i +1 kod = Asc(Mid(myStr, i, 1)) If (Kod = 95) And (i <= Len(myStr) - 2) Then If Asc(Mid(myStr, i + 1, 1)) = 95 Then ' переменная Kod2 далее не нужна ii = 3 Select Case Asc(Mid(myStr, i + 2, 1)) ' вместо Kod3 ! Case 79: NewKod = 187: Gosub InsertStr Case 111: NewKod = 188: Gosub InsertStr Case 131: NewKod = 200: Gosub InsertStr Case 82, 114: NewKod = 174: Gosub InsertStr Case 84: NewKod = 188: Gosub InsertStr End Select End If End if Loop
Самое интересное, что после выполненных преобразований изучение кода программы показывает, что мы вообще напрасно занимались модификацией исходной строки myStr и исходную процедуру Convert можно записать в таком виде (мы не показали здесь преобразование двухбайтовых спецсимволов):
Leng = Len(myStr): i = 0 Do While i < Leng i = i + 1 kod = Asc(Mid(myStr, i, 1)) Select Case Kod Case 95 ' преобразование трехбайтовых комбинаций спецсимволов If i <= Len(myStr) - 2) Then '!! выполняем модицикацию Kod и увеличиваем i = i + 2 If Asc(Mid(myStr, i + 1, 1)) = 95 Then Select Case Asc(Mid(myStr, i + 2, 1)) Case 79: Kod = 187: i = i + 2 Case 111: Kod = 188: i = i + 2 Case 131: Kod = 200: i = i + 2 Case 82, 114: Kod = 174: i = i + 2 Case 84: Kod = 188: i = i + 2 End Select End If End if Case 58 ' тут будет обработка двухбайтовых комбинаций спецсимволов ' Русские буквы Case 128 To 175: kod = kod + 64 Case 224 To 239: kod = kod + 16 ' Латышские Case 240: кod = 199 ... End Select NewStr = NewStr + Chr(Kod) Loop Convert = newStr
Таким образом, модифицируется не исходная строковая переменная, а индекс текущего байта в ней.
Сравните этот код с исходным вариантом Convert. На мой взгляд, наша процедура гораздо компактнее и понятнее. И работает в 3-5 раз быстрее (это проверено на тесте). Добавив в него еще четыре строки кода, можно увеличить скорость еще в 3-4 раза (об этом советую почитать в статье "Особенности работы со строковыми переменными в VB" в КомпьютерПресс ь 12'99). А еще лучше — перейти к использованию байтового массива и применить процедуру MyDecodeProcedure (листинг 3).
Вот такой парадокс: программа получилась короче, понятнее и гораздо быстрее и доводилась с нуля до рабочего состояния всего за пару часов.
На этом закончим разбор полетов с программкой Максима — разработчика из Риги, который думает, что знал все, что нужно для работы, еще три года назад, до поступления в университет.