Обучающий курс. 18. Работа со строками и символами

Источник: delphi
Еремин Андрей

Введение

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

Символы

Символ - это одна единица текста. Это буква, цифра, какой-либо знак. Кодовая таблица символов состоит из 256 позиций, т.е. каждый символ имеет свой уникальный код от 0 до 255. Символ с некоторым кодом N записывают так: #N . Прямо так символы и указываются в коде программы. Так как код символа представляет собой число не более 255, то очевидно, что в памяти символ занимает 1 байт. Как известно, менее байта размерности нет. Точнее, она есть - это бит, но работать с битами в программе мы не можем: байт - минимальная единица. Просмотреть таблицу символов и их коды можно с помощью стандартной утилиты " Таблица символов ", входящей в Windows (ярлык расположен в меню Пуск - Программы - Стандартные - Служебные ). Но совсем скоро мы и сами напишем нечто подобное.

Строки

Строка, она же текст - это набор символов, любая их последовательность. Соответственно, один символ - это тоже строка, тоже текст. Текстовая строка имеет определённую длину. Длина строки - это количество символов, которые она содержит. Если один символ занимает 1 байт, то строка из N символов занимает соответственно N байт.
Есть и другие кодовые таблицы, в которых 1 символ представлен не одним байтом, а двумя. Это Юникод (Unicode). В таблице Юникода есть символы всех языков мира. К сожалению, работа с Юникодом довольно затруднена и его поддержка пока что носит лишь локальный характер. Delphi не предоставляет возможностей для работы с Юникодом. Программная часть есть, но вот визуальные элементы - формы, кнопки и т.д. не умеют отображать текст в формате Юникода. Будем надеяться, в ближайшем будущем такая поддержка появится. 2 байта также называют словом (word). Отсюда и название соответствующего числового типа данных - Word (число, занимающее в памяти 2 байта, значения от 0 до 65535). Количество "ячеек" в таблице Юникода составляет 65536 и этого вполне достаточно для хранения всех языков мира. Если вы решили, что "1 байт - 256 значений, значит 2 байта - 2*256 = 512 значений", советую вспомнить двоичную систему и принцип хранения данных в компьютере.

Типы данных

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

Char - один символ (т.е. 1 байт);
String - строка символов, текст (N байт).

Официально строки вмещают лишь 255 символов, однако в Delphi в строку можно записать гораздо больше. Для хранения больших текстов и текстов со специальными символами существуют специальные типы данных AnsiString и WideString (последний, кстати, двухбайтовый, т.е. для Юникода).

Для задания текстовых значений в Pascal используются одинарные кавычки (не двойные!). Т.е. когда вы хотите присвоить строковой переменной какое-либо значение, следует сделать это так:

s:='text';

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

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

var s: string[10];

В скобках указывается максимальная длина строки.

Операции со строками

Основной операцией со строками является сложение. Подобно числам, строки можно складывать. И если в числах стулья с апельсинами складывать нельзя, то в строках - можно. Сложение строк - это просто их объединение. Пример:

var s: string;
...
s:='123'+'456';
//s = "123456"

Поскольку каждая строка - это последовательность символов, каждый символ имеет свой порядковый номер. В Pascal нумерация символов в строках начинается с 1. Т.е. в строке " ABC " символ " A " - первый, " B " - второй и т.д.
Порядковый номер символа в строке придуман не случайно, ведь именно по этим номерам, индексам, осуществляются действия над строками. Получить любой символ из строки можно указанием его номера в квадратных скобках рядом с именем переменной. Например:

var s: string; c: char; 
...
s:='Hello!';
c:=s[2];
//c = "e"

Чуть позже, когда мы будем изучать массивы, станет понятно, что строка - это массив символов. Отсюда следует и форма обращения к отдельным символам.

Обработка строк

Перейдём к функциям и процедурам обработки строк.

Длина строки

Длину строки можно узнать с помощью функции Length(). Функция принимает единственный параметр - строку, а возвращает её длину. Пример:

var Str: String; L: Integer; 
{ ... } 
Str:='Hello!'; 
L:=Length(Str);
//L = 6

Нахождение подстроки в строке

Неотъемлемой задачей является нахождение подстроки в строке. Т.е. задача формулируется так: есть строка S1 . Определить, начиная с какой позиции в неё входит строка S2 . Без выполнения этой операции ни одну обработку представить невозможно.
Итак, для такого нахождения существует функция Pos(). Функция принимает два параметра: первый - подстроку, которую нужно найти, второй - строку, в которой нужно выполнить поиск. Поиск осуществляется с учётом регистра символов. Если функция нашла вхождение подстроки в строку, возвращается номер позиции её первого вхождения. Если вхождение не найдено, функция даёт результат 0. Пример:

var Str1, Str2: String; P: Integer; 
{ ... } 
Str1:='Hi! How do you do?'; 
Str2:='do'; 
P:=Pos(Str2, Str1);
//P = 9

Удаление части строки

Удалить часть строки можно процедурой Delete(). Следует обратить внимание, что это именно процедура, а не функция - она производит действия непосредственно над той переменной, которая ей передана. Итак, первый параметр - переменная строкового типа, из которой удаляется фрагмент (именно переменная! конкретное значение не задаётся, т.к. процедура не возвращает результат), второй параметр - номер символа, начиная с которого нужно удалить фрагмент, третий параметр - количество символов для удаления. Пример:

var Str1: String; 
{ ... } 
Str1:='Hello, world!'; 
Delete(Str1, 6, 7);
// Str1 = "Hello!"

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

Вот пример. Допустим, требуется найти в строке первую букву " a " и удалить следующую за ней часть строки. Сделаем следующим образом: позицию буквы в строке найдём функцией Pos() , а фрагмент удалим функцией Delete() .

var Str: String; 
{ ... } 
Str:='This is a test.';
Delete(Str,Pos('a',Str),Length(Str));

Попробуем подставить значения и посмотреть, что передаётся функции Delete . Первая буква " a " в строке стоит на позиции 9. Длина всей строки - 15 символов. Значит вызов функции происходит такой: Delete(Str,9,15). Видно, что от буквы " a " до конца строки всего 7 символов... Но функция сделает своё дело, не смотря на эту разницу. Результатом, конечно, будет строка " This is ". Данный пример одновременно показал и комбинирование нескольких функций.

Копирование (извлечение) части строки

Ещё одной важной задачей является копирование части строки. Например, извлечение из текста отдельных слов. Выделить фрагмент строки можно удалением лишних частей, но этот способ неудобен. Функция Copy() позволяет скопировать из строки указанную часть. Функция принимает 3 параметра: текст (строку), откуда копировать, номер символа, начиная с которого скопировать и количество символов для копирования. Результатом работы функции и будет фрагмент строки.

Пример: пусть требуется выделить из предложения первое слово (слова разделены пробелом). На форме разместим Edit1 ( TEdit ), в который будет введено предложение. Операцию будет выполнять по нажатию на кнопку. Имеем:

procedure TForm1.Button1Click(Sender: TObject);
var s,word: string;
begin
  s:=Edit1.Text;
  word:=Copy(s,1,Pos(' ',s)-1);
  ShowMessage('Первое слово: '+word);
end;

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

Пример "посерьёзнее"

Примеры, приведённые выше, лишь демонстрируют принцип работы со строками с помощью функций Length() , Pos() , Delete() и Copy() . Теперь решим задачу посложнее, которая потребует комбинированного применения этих функций.

Задача: текст, введённый в поле Memo, разбить на слова и вывести их в ListBox по одному на строке. Слова отделяются друг от друга пробелами, точками, запятыми, восклицательными и вопросительными знаками. Помимо этого вывести общее количество слов в тексте и самое длинное из этих слов.

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

Интерфейс: Memo1 ( TMemo ), Button1 ( TButton ), ListBox1 ( TListBox ), Label1 , Label2 ( TLabel ).

Сначала перенесём введённый текст в переменную. Для того, чтобы разом взять весь текст из Memo, обратимся к свойству Lines.Text :

procedure TForm1.Button1Click(Sender: TObject);
var Text: string;
begin
  Text:=Memo1.Lines.Text;
end;

Теперь перейдём к обработке. Первое, что нужно сделать - разобраться с символами-разделителями. Дело в том, что такие символы могут запросто идти подряд, ведь после запятых, точек и других знаков ставится пробел. Обойти эту трудность можно таким простым способом: все разделяющие символы заменим на какой-то один, например на запятую. Для этого пройдём все символы и сделаем необходимые замены. Чтобы определить, является ли символ разделителем, запишем все разделители в отдельную строковую переменную (константу), а затем будем искать в этой строке каждый символ функцией Pos() . Все эти замены будут производиться в переменной, чтобы оригинальный текст в Memo (т.е. на экране) не был затронут. Тем не менее, для проверки промежуточных результатов работы имеет смысл выводить обработанный текст куда-либо. Например, в другое поле Memo. Чтобы пройти все символы, воспользуемся циклом FOR , где переменная пройдёт порядковые номера всех символов, т.е. от 1 до длины строки текста:

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ' .,!?';
var Text: string; i: integer;
begin
  Text:=Memo1.Lines.Text;
  for i := 1 to Length(Text) do
    if Pos(Text[i],DelSym) > 0 then
      Text[i]:=',';
 
  Memo2.Text:=Text;
end;

Теперь нужно устранить помехи. Во-первых, первый символ не должен быть разделителем, т.е. если первый символ - запятая, его нужно удалить. Далее, если подряд идут несколько запятых, их нужно заменить на одну. И наконец, чтобы корректно обработать весь текст, последним символом должна быть запятая.

if Text[1] = ',' then
    Delete(Text,1,1);
while Pos(',,',Text) > 0 do
  Delete(Text,Pos(',,',Text),1);
if Text[Length(Text)] <> ',' then
  Text:=Text+',';

Здесь замена произведена следующим образом: организован цикл, в котором одна из запятых удаляется, но происходит это до тех пор, пока в тексте есть две идущие подряд запятые.

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

var Word: string;
{...}
Word:=Copy(Text,1,Pos(',',Text)-1);
Delete(Text,1,Length(Word)+1);

Теперь в переменной Word у нас слово из текста, а в переменной Text вся остальная часть текста. Вырезанное слово теперь добавляем в ListBox , вызывая ListBox.Items.Add(строка_для_добавления) .

Теперь нам нужно организовать такой цикл, который позволил бы вырезать из текста все слова, а не только первое. В данном случае подойдёт скорее REPEAT , чем WHILE . В качестве условия следует указать Length(Text) = 0 , т.е. завершить цикл тогда, когда текст станет пустым, т.е. когда мы вырежем из него все слова.

repeat
  Word:=Copy(Text,1,Pos(',',Text)-1);
  Delete(Text,1,Length(Word)+1);
  ListBox1.Items.Add(Word);
until Length(Text) = 0;

Итак, на данный момент имеем:

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ' .,!?';
var Text,Word: string; i: integer;
begin
  Text:=Memo1.Lines.Text;
  for i := 1 to Length(Text) do
    if Pos(Text[i],DelSym) > 0 then
      Text[i]:=',';
 
  if Text[1] = ',' then
    Delete(Text,1,1);
  while Pos(',,',Text) > 0 do
    Delete(Text,Pos(',,',Text),1);
	
  repeat
    Word:=Copy(Text,1,Pos(',',Text)-1);
    Delete(Text,1,Length(Word)+1);
    ListBox1.Items.Add(Word);
  until Length(Text) = 0;
end;

Если вы сейчас запустите программу, то увидите, что всё отлично работает. За исключением одного момента - в ListBox в конце появились какие-то пустые строки... Возникает вопрос: откуда же они взялись? Об этом вы узнаете в следующем разделе урока, а пока давайте реализуем требуемое до конца.

Количество слов в тексте определить очень просто - не нужно заново ничего писать. Т.к. слова у нас занесены в ListBox, достаточно просто узнать, сколько там строк - ListBox.Items.Count .

Label1.Caption:='Количество слов в тексте: '+IntToStr(ListBox1.Items.Count);

Теперь нужно найти самое длинное из всех слов. Алгоритм нахождения максимального числа таков: принимаем в качестве максимального первое из чисел. Затем проверяем все остальные числа таким образом: если число больше того, которое сейчас записано как максимальное, делаем максимальным это число. В нашем случае нужно искать максимальную длину слова. Для этого можно добавить код в цикл вырезания слов из текста или произвести поиск после добавления всех слов в ListBox. Сделаем вторым способом: организуем цикл по строкам ListBox. Следует отметить, что строки нумеруются с нуля, а не с единицы! В отдельной переменной будем хранить самое длинное слово. Казалось бы, нужно ведь ещё хранить максимальную длину слова, чтобы было с чем сравнивать... Но не нужно заводить для этого отдельную переменную, ведь мы всегда можем узнать длину слова функцией Length(). Итак, предположим, что первое слово самое длинное...

var LongestWord: string;
{...}
  LongestWord:=ListBox1.Items[0];
  for i := 1 to ListBox1.Items.Count-1 do
    if Length(ListBox1.Items[i]) > Length(LongestWord) then
      LongestWord:=ListBox1.Items[i];
 
  Label2.Caption:='Самое длинное слово: '+LongestWord+' ('+IntToStr(Length(LongestWord))+' букв)';

Почему цикл до ListBox.Items.Count-1 , а не просто до Count , разберитесь самостоятельно :-)

Вот теперь всё готово!

procedure TForm1.Button1Click(Sender: TObject);
const DelSym = ' .,!?';
var Text,Word,LongestWord: string; i: integer;
begin
  Text:=Memo1.Lines.Text;
  for i := 1 to Length(Text) do
    if Pos(Text[i],DelSym) > 0 then
      Text[i]:=',';
 
  if Text[1] = ',' then
    Delete(Text,1,1);
  while Pos(',,',Text) > 0 do
    Delete(Text,Pos(',,',Text),1);
 
  Text:=AnsiReplaceText(Text,Chr(13),'');
  Text:=AnsiReplaceText(Text,Chr(10),'');
 
  repeat
    Word:=Copy(Text,1,Pos(',',Text)-1);
    Delete(Text,1,Length(Word)+1);
    ListBox1.Items.Add(Word);
  until Length(Text) = 0;
 
  Label1.Caption:='Количество слов в тексте: '+IntToStr(ListBox1.Items.Count);
 
  LongestWord:=ListBox1.Items[0];
  for i := 1 to ListBox1.Items.Count-1 do
    if Length(ListBox1.Items[i]) > Length(LongestWord) then
      LongestWord:=ListBox1.Items[i];
 
  Label2.Caption:='Самое длинное слово: '+LongestWord+' ('+IntToStr(Length(LongestWord))+' букв)';
end;

Работа с символами

Собственно, работа с символами сводится к использованию двух основных функций - Ord() и Chr(). С ними мы уже встречались. Функция Ord() возвращает код указанного символа, а функция Chr() - наоборот, возвращает символ с указанным кодом.

Помните " Таблицу символов "? Давайте сделаем её сами!

Вывод осуществим в TStringGrid . Этот компонент представляет собой таблицу, где в каждой ячейке записано текстовое значение. Компонент расположен на вкладке Additional (по умолчанию следует прямо за Standard). Перво-наперво настроим нашу табличку. Нам нужны всего две колонки: в одной будем отображать код символа, а в другой - сам символ. Количество колонок задаётся в свойстве с логичным названием ColCount . Устанавливаем его равным 2. По умолчанию у StringGrid задан один фиксированный столбец и одна фиксированная строка (они отображаются серым цветом). Столбец нам не нужен, а вот строка очень кстати, поэтому ставим FixedCols = 0, а FixedRows оставляем = 1.

Заполнение осуществим прямо при запуске программы, т.е. не будем ставить никаких кнопок. Итак, создаём обработчик события OnCreate() формы.

Количество символов в кодовой таблице 256, плюс заголовок - итого 257. Зададим число строк программно (хотя можно задать и в Инспекторе Объекта):

procedure TForm1.FormCreate(Sender: TObject);
begin
  StringGrid1.RowCount:=257;
end;

Вывод делается крайне просто - с помощью цикла. Просто проходим числа от 0 до 255 и выводим соответствующий символ. Также выводим надписи в заголовок. Доступ к ячейкам StringGrid осуществляется с помощью свойства Cells : Cells[номер_столбца,номер_строки] . В квадратных скобках указываются номера столбца и строки (начинаются с нуля). Значения текстовые.

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  StringGrid1.RowCount:=257;
  StringGrid1.Cells[0,0]:='Код';
  StringGrid1.Cells[1,0]:='Символ';
  for i := 0 to 255 do
  begin
    StringGrid1.Cells[0,i+1]:=IntToStr(i);
    StringGrid1.Cells[1,i+1]:=Chr(i);
  end;
end;

Запускаем, смотрим.

Специальные символы

Если вы внимательно посмотрите на нашу таблицу, то увидите, что многие символы отображаются в виде квадратиков. Нет, это не значки. Так отображаются символы, не имеющие визуального отображения. Т.е. символ, например, с кодом 13 существует, но он невидим. Эти символы используются в дополнительных целях. К примеру, символ #0 (т.е. символ с кодом 0) часто применяется для указания отсутствия символа. Существуют также строки, называемые null-terminated - это строки, заканчивающиеся символом #0. Такие строки используются в языке Си.
По кодам можно опознавать нажатия клавиш. К примеру, клавиша Enter имеет код 13, Escape - 27, пробел - 32, Tab - 9 и т.д.
Давайте добавим в нашу программу возможность узнать код любой клавиши. Для этого обработаем событие формы OnKeyPress() . Чтобы этот механизм работал, необходимо установить у формы KeyPreview = True .

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
  ShowMessage('Код нажатой клавиши: '+IntToStr(Ord(Key)));
end;

Здесь мы выводим окошко с текстом. У события есть переменная Key , в которой хранится символ, соответствующий нажатой клавише. С помощью функции Ord() узнаём код этого символа, а затем функцией IntToStr() преобразуем это число в строку.

Пример "посерьёзнее" - продолжение

Вернёмся к нашему примеру. Пришло время выяснить, откуда в ListBox берутся пустые строки. Дело в том, что они не совсем пустые. Да, визуально они пусты, но на самом деле в каждой из них по 2 специальных символа. Это символы с кодами 13 и 10 (т.е. строка #13#10). В Windows такая последовательность этих двух невизуальных символов означает конец текущей строки и начало новой строки. Т.е. в любом файле и вообще где угодно переносы строк - это два символа. А весь текст, соответственно, остаётся непрерывной последовательностью символов. Эти символы можно (и даже нужно) использовать в случаях, когда требуется вставить перенос строки. Подробнее об этом можно прочитать в статье Что такое #13#10?. Знаний, полученных во всех предыдущих уроках, и в этом в том числе, вполне достаточно для понимания этой статьи - она совсем небольшая.

Доведём нашу программу по поиску слов до логического конца. Итак, чтобы избавиться от пустых строк, нам нужно удалить из текста символы #13 и #10. Сделать это можно с помощью цикла, по аналогии с тем, как мы делали замену двух запятых на одну:

while Pos(Chr(13),Text) > 0 do
  Delete(Text,Pos(Chr(13),Text),1);
 
while Pos(Chr(10),Text) > 0 do
  Delete(Text,Pos(Chr(10),Text),1);

Ну вот - теперь программа полностью работоспособна!

Дополнительные функции для работы со строками - модуль StrUtils

Дополнительный модуль StrUtils.pas содержит дополнительные функции для работы со строками. Среди этих функций множество полезных. Более подробно некоторые из функций рассмотрены в статье Работа со строковыми типами данных. А вот краткое описание часто используемых функций:

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

AnsiReplaceStr, AnsiReplaceText ( строка , текст_1 , текст_2 ) - функции выполняют замену в строке строка строки текст_1 на текст_2 . Функции отличаются только тем, что первая ведёт замену с учётом регистра символов, а вторая - без него.
В нашей программе можно использовать эти функции для вырезания из строки символов #13 и #10 - для этого в качестве текста для замены следует указать пустую строку. Вот решение в одну строку кода:

Text:=AnsiReplaceText(AnsiReplaceText(Text,Chr(13),''),Chr(10),'');

DupeString( строка , число_повторений ) - формирует строку, состоящую из строки строка путём повторения её заданное количество раз.

ReverseString( строка ) - инвертирует строку (" 123 " -> " 321 ").

Также следует упомянуть у функциях преобразования регистра.

UpperCase( строка ) - преобразует строку в верхний регистр; LowerCase( строка ) - преобразует строку в нижний регистр.

Для преобразования отдельных символов следует использовать эти же функции.

Подробную информацию о каждой функции можно получить, введя её название в любом месте редактора кода, установив курсор на это название (или выделив его) и нажав F1.

Скриншоты программ, описанных в статье

Программа, извлекающая слова из текста
Программа извлечения слов из текста
Программа, выводящая таблицу символов
Таблица символов

Заключение

Длинный получился урок... И как раз афоризм оказался в тему. Что удивительно, афоризм был подобран мной в самом начале, а сам урок написан позже :-)

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=22532