 | Уровень сложности: простой Джим Диксон, писатель, независимый писатель
22.04.2009 Данная серия статей представляет собой руководство для тех, кому нужно быстрое решение на XML и Perl. В подавляющем большинстве случаев для интеграции XML в Perl-приложение вам понадобится всего одно средство - XML::Simple. В первой части вы узнаете, где найти этот инструмент, как его использовать и куда двигаться дальше. Когда вы поймете, как здорово работать с XML и Perl, следующие две статьи данной серии помогут вам отточить навыки.
Введение
Первая статья из серии о Perl и XML посвящена инструменту
XML::Simple. Программисты на Perl, как правило, в первую очередь используют XML для извлечения параметров из конфигурационного файла. В данной статье описывается, как считывать такие параметры с помощью двух строчек кода, первая из которых сообщает Perl об использовании
XML::Simple, а вторая устанавливает значения переменных в файле. Вам даже не нужно задавать имя конфигурационного файла:
XML::Simple
сам сделает правильный выбор.
В качестве конкретного примера мы рассмотрим зоомагазин. Из данной статьи вы узнаете, как, прилагая минимум усилий, считывать XML-файлы в иерархическую структуру данных Perl (анонимные массивы и хэши). В статье показано, как лаконично Perl может преобразовывать и реструктурировать информацию, имеющуюся в исходном XML-документе, и как затем ее можно записывать обратно различными способами.
В заключение мы обсудим некоторые ограничения
XML::Simple. Из этого возникают темы следующих двух статей данной серии: более сложный парсинг, использование усовершенствованных средств для преобразования XML из одной формы в другую и технические приемы для сериализации XML из DOM и других форм в памяти.
Первоначально эта статья планировалась для программистов Perl, с незначительным представлением Perl, но она также будет полезной для XML- специалистов, интересующихся изучением более программных подходов к управлению XML- документами.
Начало работы
Прежде чем начать, вам нужно установить Perl. Если вы еще не установили его, обратитесь к ссылке в разделе
Ресурсы.
Затем вам понадобится
XML::Simple. Если вы используете UNIX или Linux, удобнее всего будет получить его из архива CPAN с помощью команды
cpan
. Вы начинаете этот процесс с установки cpan на свой компьютер, используя команды, показанные в листинге 1. Обычно его устанавливают под пользователем root, чтобы модули Perl были доступны всем пользователям.
Листинг 1. Установка cpan, получение XML::Simple
$ perl -MCPAN -e shell
cpan> ...
cpan> install XML::Simple
cpan> quit
|
Выполняя команды в первый раз, вы столкнетесь с длинным диалогом. В
листинге 1
он опущен. Возможно, вы захотите отредактировать итоговую конфигурацию; это можно сделать в файле /etc/perl/CPAN/Config.pm.
Пользователи Windows выполняют те же процедуры, используя PPM (Если у вас нет PPM, обратитесь к разделу
Ресурсы,
если у вас нет PPM). В этом случае команды для установки модуля показаны в листинге 2.
Листинг 2. Windows: использование PPM для получения
XML::Simple
$ ppm install XML::Simple
|
Во время установки и cpan и ppm проверяют зависимости и выбирают все недостающие зависимости из хранилища. Это происходит автоматически, если установить политику необходимых условий cpan на 'follow'. Модули, как правило, компилируются во время установки и создают страницы сообщений. Это не сложно, но вам придется немного подождать.
Еще одно предварительное условие
XML::Simple
преобразовывает XML-документы в ссылки на хэши и массивы хэшей. Это значит, что вы должны хорошо разбираться во взаимодействии ссылок, хэшей и массивов в Perl. Если в этом вопросе вам потребуется помощь, просмотрите отличное справочное пособие по Perl в разделе Ресурсы.
XML::Simple
Фактически инструмент XML::Simple, разработанный Грантом Маклином, имеет две функции: он преобразовывает текстовые XML-документы в структуры данных Perl, комбинацию анонимных хэшей и массивов, а также преобразовывает структуры данных обратно в текстовые XML-документы.
Эта ограниченная функциональность чрезвычайно полезна и будет показана на двух уровнях. Сначала вы узнаете, как импортировать данные из конфигурационных файлов в форму XML. Затем на более подробном примере небольшого зоомагазина вы узнаете, как считывать большие и сложные XML-файлы в память, преобразовывать их способами, которые могут быть сложны для стандартных средств XML, таких как XSLT, и перезаписывать их на диск.
Многим
XML::Simple
предоставит все необходимое для работы с XML в Perl.
Конфигурационный файл XML
У нас есть проблема, с которой каждый день сталкиваются программисты по всему миру. Нам нужно передать смешанную информацию по конфигурации своей программе, и по многим причинам это сложно сделать при помощи параметров командной строки. Поэтому мы решаем использовать конфигурационный файл. Поскольку XML, что ни говори, является стандартом для вещей подобного рода, мы будем форматировать файл способом, показанным в листинге 3. Для этого используем
XML::Simple.
Листинг 3. Конфигурационный файл, part1.xml
<config>
<user>freddy</user>
<passwd>longNails</passwd>
<books>
<book author="Steinbeck" title="Cannery Row"/>
<book author="Faulkner" title="Soldier's Pay"/>
<book author="Steinbeck" title="East of Eden"/>
</books>
</config>
|
В дополнение к конструктору
XML::Simple
имеет две подпрограммы:
XMLin()
и
XMLout(). Как вы можете предположить, первая считывает XML-файл, возвращая ссылку. Вторая подпрограмма, имея ссылку на соответствующую структуру данных, преобразовывает эту структуру в XML-документ, либо в строковом формате, либо в виде файла, в зависимости от параметров.
XML::Simple
имеет удобные значения по умолчанию, так что, например, если вы не пропишете имя входного файла, программа Perl part1.pl (см. листинг 4) прочтет файл под названием part1.xml.
Листинг 4. part1.pl
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Data::Dumper;
print Dumper (XML::Simple->new()->XMLin());
|
Выполнив part1.pl, получим результат, показанный в листинге 5.
Листинг 5. Выходные данные part1.pl
$VAR1 = {
'passwd' => 'longNails',
'user' => 'freddy',
'books' => {
'book' => [
{
'title' => 'Cannery Row',
'author' => 'Steinbeck'
},
{
'title' => 'Soldier\'s Pay',
'author' => 'Faulkner'
},
{
'title' => 'East of Eden',
'author' => 'Steinbeck'
}
]
}
};
|
XMLin()
вернул ссылку на хэш. Если бы мы присвоили эту ссылку переменной $config, то могли бы получить имя пользователя, используя
$config->{user}, и пароль, используя $config->{passwd}. Те, кто обращает внимание на экономичность кода, заметят, что для считывания конфигурационного файла и возврата одного параметра достаточно в менее чем одной строки кода:
XML::Simple->new->{user}.
Приведенный дамп указывает на то, что при работе с
XML::Simple надо быть внимательным.
- Во-первых, он не учитывает имя корневого элемента.
-
Во-вторых, он сворачивает элементы с одними и теми же именами в одну ссылку на анонимный массив. Следовательно, название первой книги -
@{$config->{books}->{book}}[0]->{title}
или 'Cannery Row'.
- В-третьих, он одинаково обрабатывает атрибуты и подэлементы.
Все эти режимы работы можно изменить при помощи опций для
XMLin(). Более подробно об опциях см. раздел Ресурсы
и приведенное ниже обсуждение.
Более сложный пример - зоомагазин
XML::Simple можно использовать не только для экономичного парсинга конфигурационных файлов. На самом деле он может работать с большими и сложными XML-файлами и преобразовывать их в регулярные структуры данных, которые хорошо подходят для преобразований, легко реализуемыми в Perl, но сложными или невозможными при использовании традиционных инструментов преобразования XML, таких как XSLT.
Предположим, что вы работаете в зоомагазине, где информация о животных хранится в XML- файле. Небольшая часть этого документа приводится ниже в листинге 6. Менеджер хочет сделать несколько изменений:
- Для экономии места поменять все подэлементы на атрибуты
- Увеличить цену на 20%
- Унифицировать формат цен, оставив два знака после запятой
- Отсортировать список
- Поменять даты рождения на возраст
Научившись работать в Perl и зная о том, что вычисления в XSLT сложны (вы когда-либо пробовали сделать сдвиг, используя XPath?), мы решаем сделать эту работу с помощью
XML::Simple
(см. листинг 6).
Листинг 6.Несколько наших животных, pets.xml
<?xml version='1.0'?>
<pets>
<cat>
<name>Madness</name>
<dob>1 February 2004</dob>
<price>150</price>
</cat>
<dog>
<name>Maggie</name>
<dob>12 October 2005</dob>
<price>75</price>
<owner>Rosie</owner>
</dog>
<cat>
<name>Little</name>
<dob>23 June 2006</dob>
<price>25</price>
</cat>
</pets>
|
Первые исследования
Наши первые попытки использования
XML::Simple показаны в листинге 7.
Листинг 7. Прекрасный новый Perl
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Data::Dumper;
my $simple = XML::Simple->new();
my $data = $simple->XMLin('pets.xml');
# DEBUG
print Dumper($data) . "\n";
# END
|
Предусмотрительности ради мы используем
Data::Dumper, чтобы посмотреть, что считалось в память, и с грустью видим там то, что показано в листинге 8.
Листинг 8. Что у нас получилось
$VAR1 = {
'cat' => {
'Little' => {
'dob' => '23 June 2006',
'price' => '25'
},
'Madness' => {
'dob' => '1 February 2004',
'price' => '150'
}
},
'dog' => {
'owner' => 'Rosie',
'dob' => '12 October 2005',
'name' => 'Maggie',
'price' => '75'
}
};
|
Результат разочаровывает. Кошки и собаки представлены абсолютно по-разному: две кошки находятся в дважды вложенном хэше с именем в качестве ключа, тогда как информация о собаке хранится в простом хэше, а ее имя рассматривается как обычныйатрибут. Вы также замечаете, что имя корневого элемента исчезло. Так что мы отправляемся читать замечательную документацию (см. Ресурсы) и узнаем о существовании опций, в частности, ForceArray=>1
и KeepRoot=>1. При помощи первой вложенные элементы представляются в качестве массивов. Вторая опция обеспечивает сохранение имени корневого элемента при вводе. При выводе, что мы увидим чуть позже, она означает, что представление данных в памяти включает имя корневого элемента. Со всеми этими изменениями мы получаем результат, показанный в листинге 9, с которым программисту значительно проще работать, хотя он и может занять больше памяти.
Листинг 9. Выходные данные
Data::Dumper
после добавления опций, немного обновленные, чтобы их было удобнее читать
$VAR1 = {
'pets' => [
{
'cat' => [
{
'dob' => [ '1 February 2004' ],
'name' => [ 'Madness' ],
'price' => [ '150' ]
},
{
'dob' => [ '23 June 2006' ],
'name' => [ 'Little' ],
'price' => [ '25' ]
}
],
'dog' => [
{
'owner' => [ 'Rosie' ],
'dob' => [ '12 October 2005' ],
'name' => [ 'Maggie' ],
'price' => [ '75' ]
}
]
}
]
};
|
Преобразование структуры данных в памяти
Сейчас у нас в памяти есть правильная структура, с которой очень просто работать программным путем. Чтобы выполнить первое задание начальника, которое заключается в том, чтобы преобразовать элементы в атрибуты, мы должны переместить ссылки в массивы, как показано в листинге 10.
Листинг 10. Замена ссылки на массив с одним элементом
После этого мы должны заменить ссылки на простые значения, как показано в листинге 11.
Листинг 11. Замена ссылки на обычное значение
После таких изменений
XML::Simple
будет выводить пару "атрибут - значение", а не подэлемент. Там, где для вывода есть несколько экземпляров одного вида (в данном случае когда у вас две кошки и одна собака) нам нужно собрать хэши в анонимный массив анонимных хэшей. В листинге 12 показано, как совершить часть этого небольшого волшебства.
Листинг 12. Свертывание массивов в хэши, преобразование элементов в атрибуты
sub makeNewHash($) {
my $hashRef = shift;
my %oldHash = %$hashRef;
my %newHash = ();
while ( my ($key, $innerRef) = each %oldHash ) {
$newHash[$key] = @$innerRef[0];
}
return \%newHash;
}
|
Имея ссылку на XML-описание отдельного животного, этот код "сворачивает" ее в хэш. Если в наличии всего одно животное определенного вида, на этом все заканчивается. Мы записываем ссылку на новый хэш обратно в
$data. Однако если у нас несколько животных определенного вида, мы записываем обратно ссылку на анонимный массив, содержащий ссылки на анонимные хэши, описывающие отдельных животных. Вы можете увидеть, как это происходит, посмотрев на
foldType()
в полном решении, см.
листинг 16.
Другие требования: прелести Perl
Другими требованиями начальника было упорядочить список, увеличить цену на 20%, записывать цены с двумя знаками после запятой и поменять даты рождения на возраст. Оказывается, первое требование - это опция по умолчанию для выходных данных для XML::Simple. Первое задание выполнил сам Perl, а второе и третье делаются в Perl шутя. К счастью, Perl обладает полиморфизмом: цены являются числами, когда мы рассчитываем увеличение цены на 20%, однако когда вы записываете их в виде строк, они остаются в том формате, в котором вы их записали. Таким образом, программа из листинга 13 делает все необходимое, преобразовывая строки в числа и обратно в строки.
Листинг 13. Переформатирование и увеличение цен
sprintf "%6.2f", $amt * (1 + $change)
|
Процесс преобразования дат рождения в возраст немного сложнее. Быстрая проверка со CPAN показала, что
Date::Calc
имеет все необходимые функции (и много больше).
Decode_Date_EU
меняет даты на европейский формат, например, 13 января 2006 на массив из трех элементов (YMD - год, месяц, день), который в этом пакете является стандартным. Имея две такие даты,
Delta_YMD($earlier, $later) затем выводит разницу (в нашем случае возраст) в том же формате. К сожалению,
Delta_YMD
может ошибаться: иногда день или месяц могут быть отрицательными! Но небольшой поиск в Google позволяет решить проблему. Программа
deltaYMD
в полном решении (в
листинге 16) показывает, как с этим справиться.
Разбираемся с кошками и собаками
Чтобы код было проще расширять, используйте таблицу переходов, как показано в листинге 14. Таблицы переходовподробно описываются в отличной книге Джейсона Доминуса (Jason Dominus)
"Perl на высоком уровне"
(см. Ресурсы).
Листинг 14. Таблица переходов
my $DISPATCHER = {
'cat' => sub { foldType(shift); },
'dog' => sub { foldType(shift); },
'hippo' => \&hippoFunc,
};
|
Таблица переходов может содержать абсолютный код, используемый для работы с конкретным элементом, в виде анонимной подпрограммы, или содержать ссылку на именованную подпрограмму, которая определяется в каком-либо другом месте. Эту конструкцию можно использовать там, где в других языках используются переключатели.
В рассматриваемом примере есть только два типа элементов - кошка и собака. Скорее всего в настоящих XML-документах их будет больше и на разных уровнях. Использование одной или нескольких таблиц переходов намного понятнее и удобнее в поддержке, чем многострочные конструкции
if ... elsif ... elsif.
Запись нашего XML на диск
Настройки вывода данных по умолчанию в
XML::Simple обычно разумны. Если не указать для
XMLout(), никаких опций, он выведет строку. Если же вы хотите записать результат в файл, добавьте опцию
OutputFile. Если вы не укажете иного, в качестве корневого элемента будет использован
<opt>. Если у структуры данных в памяти есть имя корневого элемента, добавьте опцию
KeepRoot, задав для нее значение true или, как принято в Perl, 1. Все это сделано в листинге 15.
Листинг 15. Вывод данных в XML-файл
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
|
Полное решение
Следующие 112 строк кода, представленные в
листинге 16, выполняют все задания начальника. Экономичность XML::Simple впечатляет. Восемь строк кода считывают и записывают XML. Менее половины остального кода выполняет преобразование его структуры.
Листинг 16. Окончательная версия кода
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Date::Calc qw(Add_Delta_YM Decode_Date_EU Delta_Days Delta_YMD);
use Data::Dumper;
my $simple = XML::Simple->new (ForceArray => 1, KeepRoot => 1);
my $data = $simple->XMLin('pets.xml');
my @now = (localtime(time))[5, 4, 3];
$now[0] += 1900; # Perl years start in 1900
$now[1]++; # months are zero-based
sub fixPrice($$) {
my ($amt, $change) = @_;
return sprintf "%6.2f", $amt * (1 + $change);
}
sub deltaYMD($$) {
my ($earlier, $later) = @_; # refs to YMD arrays
my @delta = Delta_YMD (@$earlier, @$later);
while ( $delta[1] < 0 or $delta[2] < 0 ) {
if ( $delta[1] < 0 ) { # negative month
$delta[0]--;
$delta[1] += 12;
}
if ( $delta[2] < 0 ) { # negative day
$delta[1]--;
$delta[2] = Delta_Days(
Add_Delta_YM (@$earlier, @delta[0,1]), @$later);
}
}
return \@delta;
}
sub dob2age($) {
my $strDOB = shift;
my @dob = Decode_Date_EU($strDOB);
my $ageRef = deltaYMD( \@dob, \@now );
my ($ageYears, $ageMonths, $ageDays) = @$ageRef;
my $age;
if ( $ageYears > 1 ) {
$age = "$ageYears years";
} elsif ($ageYears == 1) {
$age = '1 year' . ( $ageMonths > 0 ?
( ", $ageMonths month" . ($ageMonths > 1 ? 's' : '') )
: '');
} elsif ($ageMonths > 1) {
$age = "$ageMonths months";
} elsif ($ageMonths == 1) {
$age = '1 month' . ( $ageDays > 0 ?
( ", $ageDays day" . ($ageDays > 1 ? 's' : '') ) : '');
} else {
$age = "$ageDays day" . ($ageDays != 1 ? 's' : '');
}
return $age;
}
sub makeNewHash($) {
my $hashRef = shift;
my %oldHash = %$hashRef;
my %newHash = ();
while ( my ($key, $innerRef) = each %oldHash ) {
my $value = @$innerRef[0];
if ($key eq 'dob') {
$newHash{'age'} = dob2age($value);
} else {
if ($key eq 'price') {
$value = fixPrice($value, 0.20);
}
$newHash{$key} = $value;
}
}
return \%newHash;
}
sub foldType ($) {
my $arrayRef = shift;
# if single element in array, return simple hash
if (@$arrayRef == 1) {
return makeNewHash(@$arrayRef[0]);
}
# if multiple elements, return array of simple hashes
else {
my @outArray = ();
foreach my $hashRef (@$arrayRef) {
push @outArray, makeNewHash($hashRef);
}
return \@outArray;
}
}
my $dispatcher = {
'cat' => sub { foldType(shift); },
'dog' => sub { foldType(shift); },
};
my @base = @{$data->{pets}};
my %types = %{$base[0]};
my %newTypes = ();
while ( my ($petType, $arrayRef) = each %types ) {
my @petArray = @$arrayRef;
print "type $petType has " . @petArray . " representatives \n";
my $refReturned = &{$dispatcher->{$petType}}( $arrayRef );
$newTypes{$petType} = $refReturned;
}
$data->{pets} = \%newTypes; # overwrite existing data
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
|
Хотя код в Perl можно сделать еще более сжатым, данный код также показывает, как легко управлять XML в Perl. В частности, использование таблиц перехода делает возможным работу с большим количеством типов элементов с разными структурами, простым и удобным для поддержки способом.
Ограничения
К сожалению, некоторые действия в
XML::Simple невозможны. Этот вопрос будет тщательно рассмотрен в частях 2 и 3, однако у
XML::Simple есть два значительных ограничения. Во-первых, при вводе данных он считывает весь XML-файл в память. Следовательно, если файл слишком велик или вы имеете дело с потоком XML-данных, вы не можете использовать этот модуль. Во-вторых, он не может работать со смешанным XML-содержимым, где в теле элемента одновременно появляются и текст, и подэлементы, как показано в листинге 17.
Листинг 17. Смешанное содержимое
<example>of <mixed/> content</example>
|
Как узнать, что ваш файл слишком велик для обработки XML::Simple? Ориентировочно XML при считывании в память увеличивается в объеме в 10 раз. Отсюда, если на вашей рабочей станции имеется несколько сот мегабайт свободной памяти,
XML::Simple должен справляться с XML-файлами размером до нескольких десятков мегабайт.
Заключение
Формат XML применяется повсеместно и все глубже проникает в новые приложения и операционные системы. Программистам Perl необходимо хорошо разбираться в том, как его использовать. Средства, подобные
XML::Simple, облегчают процесс преобразования XML-документов в понятные структуры данных в Perl и перевод таких структур данных обратно в XML. Для каждого из этих действий, как правило, достаточно одной строки кода.
-
-
С другой стороны, специалисты по XML будут приятно удивлены тем, насколько полезным может быть Perl при преобразовании и анализе содержимого XML.
Из части 2 вы узнаете, как использовать две главные школы XML-парсинга для разработчиков Perl: древовидный парсинг и парсинг, управляемый по событиям.
Ресурсы Научиться
- Оригинал статьи
XML for Perl developers, Part 1: XML plus Perl -- simply magic (EN).
-
Пособие по использованию ссылок в Perl (EN): изучите все документы обо всех аспектах ссылок и вложенных структур данных в Perl.
-
Разработчики Perl: Пополните свой XML- инструментарий
(Паранд Даругар (Parand Darugar), developerWorks, июнь 2001 г.) (EN): обзор двух десятков основных средств и библиотек для управления XML в Perl.
-
Эффективная обработка XML с DOM и XPath в Perl
(Паранд Даругар, developerWorks, октябрь 2001 г.) (EN): узнайте, как эффективно использовать DOM.
-
Perl на высоком уровне
(Марк Джейсон Доминус (Mark Jason Dominus), 2005 г.): прочтите книгу о методах функционального программирования в Perl и о том, как создавать функции, которые могут модифицировать и формировать другие функции.
-
Сертификация IBM XML: узнайте, как стать сертифицированным разработчиком IBM (IBM-Certified Developer) в области XML и смежных технологий.
-
Технические ресурсы по XML: посетите раздел XML на developerWorks, где вы найдете множество статей, советов, учебных пособий, стандартов и справочных руководств IBM.
-
Технические мероприятия и Web-трансляции developerWorks
: будьте в курсе всех новых технологий.
Получить продукты и технологии
-
Perl: получите самую последнюю версию и испытайте ее в действии.
-
Огромная Perl-библиотека CPAN
(18 миллионов ссылок в Google!): в Comprehensive Perl Archive Network можно найти любые материалы по Perl.
-
PPM, Perl Package Manager для Windows: получите средства, которые позволят вам устанавливать, перемещать, обновлять и другими способами управлять использованием распространенных модулей Perl CPAN (таких как Tk и DBI) в ActivePerl.
-
XML::Simple Гранта Маклина: попробуйте модуль XML::Simple - простой API поверх встроенного модуля XML-парсинга.
-
Спецификация XML: изучите подробно полное описание расширяемого языка разметки (XML).
-
Спецификация Document Object Model (DOM): познакомьтесь подробнее с независимыми от языка и платформы интерфейсом, который обеспечивает программам и сценариям динамический доступ и обновление содержимого, структуры и стиля документов.
-
Введение в XML (Дуг Тидвел (Doug Tidwell), developerWorks, август 2002 г.): в качестве более плавного введения в XML ознакомьтесь с этим пособием, где описано, как появился XML, какое значение он имеет для будущего электронной торговли, а также ознакомьтесь с различными программными интерфейсами и стандартами XML и двумя примерами того, как компании решают проблемы при помощи XML.
-
XPath 1.0: получите спецификацию языка для навигации по дереву DOM.
-
Спецификация XSLT 1.0: узнайте о преобразовании одного XML-документа в другой.
-
Пробуем писать древовидный XML в Perl: работа с древовидными моделями документов
(Паранд Даругар, developerWorks, июль 2000 г.) (EN): хорошее введение в древовидный парсинг XML в Perl.
-
Ознакомительное программное обеспечение от IBM: используйте в своем следующем проекте ознакомительное программное обеспечение, которое можно загрузить прямо с developerWorks.
Обсудить
Об авторе  | |  | Джим Диксон (Jim Dixon) - независимый разработчик, недавно вернувшийся в Сан Франциско, где он консультирует начинающие компании, занимающиеся Web 2.0, по вопросам Perl и Ruby. До этого он семь лет работал ведущим техническим специалистом работающего в Великобритании и США Интернет-провайдера, а также много разрабатывал программное обеспечение на Java/J2EE. |
Выскажите мнение об этой странице
|  |