Журнал LinuxFormat - перейти на главную

LXF89:Mono

Материал из Linuxformat
Перейти к: навигация, поиск
C# для начинающих

Содержание

Mono: Работаем с файлами

Подогрев ваш интерес пространствами имен и объектно-ориентированным программированием, Пол Хадсон покажет, как пишется полезный код...

Наш эксперт

Пол Хадсон
полагает, что Mono – лучшая вещь со времен мультфильма Pinky and the Brain, и сейчас поддерживает два проекта на основе Mono на SourceForge.

Вы когда-нибудь смотрели мультсериал Thundercats [Громо-Коты, – прим. пер.]? В детстве я считал его гениальным: вы действительно могли ощущать «колдовство и рык звериный», глядя в телевизор, благодаря крутой анимации, симпатичным персонажам и замечательным сценариям. Сейчас, однако, я понимаю, что он был не лишен шаблонности. Среди ГромоКотов были такие персонажи, как Лева-O, Тигра и Пантро, и они боролись против обезьяноподобного индивида по имени Обезмен, шакаловидного Шаклмена и коршунообразного Коршмена. Их родная страна называлась Громада. Машина Пантро называлась Громокар. Уловили закономерность?

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

К примеру, я использую PHP уже много лет и никак не могу запомнить, принимает ли функция strpos() (поиск вхождения одной строки в другую) параметры как $иголка, $стог_сена или как $стог_сена, $иголка.

Проблема PHP в том, что функция strpos() принимает параметры как $стог_сена, $иголка, в то время как функция in_array() (она отвечает на вопрос, встречается ли элемент в массиве) принимает параметры как $иголка, $стог_сена. Вдобавок strpos() пишется в одно слово, а str_replace() содержит разделяющий две части знак подчеркивания. По своей природе, PHP не очень запоминающийся язык программирования. Программистам PHP не быть ГромоКотами.

К счастью, вы не учитесь программировать на PHP, а работаете на платформе Mono с С#, а C# – язык более новый и менее набитый ляпсусами, чем PHP – не имеет таких проблем. Собственно говоря, некоторые части C# так прямолинейны, что вы можете вставлять имена методов по догадке. На нашем втором уроке мы рассмотрим управление файлами. Вы напишете программу, которая пройдется по файловой системе и составит указатель по всем имеющимся файлам, а вы потом сможете его распечатать. Не волнуйтесь, если это звучит сложно – C#, Mono и .NET сделают всю трудную работу за вас.

Ощутите колдовство

Начнем с простейшего чтения и записи файлов. Вообще-то точнее будет сказать – простейшего ввода-вывода: в терминах программирования, «писать» означает «вывести» или «отобразить» (то, что делает команда write). Запустите MonoDevelop (который мы установили в прошлый раз) и создайте консольный проект C# (File > New Project > C# > Console Project). Это не временный проект, поэтому дайте ему такое имя, чтобы не стыдно было выложить его на SourceForge. Я выбрал имя Snarf, потому что оно означает «брать» (эта программа будет читать содержимое кучи файлов), а также хорошо сочетается с темой ГромоКотов.

(thumbnail)
Велите MonoDevelop компилировать программы в каталог вашего проекта, чтобы программа могла читать кэш файлов.

Нам понадобится кое-что из новой функциональности .NET 2.0, но в большинстве версий MonoDevelop по умолчанию используется .NET 1.1. Чтобы поправить дело, нажмите Project > Options, затем выберите Runtime Options из списка Categories в появившемся окне. Справа помещен выбор среды исполнения, варианты: 1.1 и 2.0, вот и выберите 2.0. Пока вы еще в этом окне, откройте категорию Configurations > Debug, выберите Output, затем измените Output Path, удалив раздел /bin/Debug. Теперь MonoDevelop будет сохранять исполняемый файл в корневом каталоге вашего проекта. Нажмите OK, чтобы запомнить изменения.

И чтение, и запись файлов осуществляются с помощью библиотеки System.IO (сокращение от Input/Output – ввод/вывод, т.е. чтение и запись), поэтому нужно поместить 'using System.IO' вверху вашего главного файла проекта. Измените class MainClass на class Snarf и удалите строку Console.WriteLine(), оставив метод Main() пустым.

Первое, что мы сделаем – считаем содержимое одного файла. Содержимое файлов – по крайней мере, тех, что нас интересуют – простой текст, а значит, его можно легко сохранить как данные строкового типа. Создайте файл с именем myfile.txt в корневом каталоге вашего проекта (там, где MonoDevelop будет сохранять вашу программу), и введите любой текст.

Встает вопрос: как прочесть содержимое файла в строку? А попробуйте так:

string myfile = File.ReadAllText("myfile.txt");

Вот и все – все, что требуется, чтобы прочитать файл в строку средствами Mono. Для ее вывода можете использовать метод Console.Write(), рассмотренный на прошлом уроке:

Console.Write(myfile);

Нажмите F5, программа скомпилируется и запустится, и вы должны увидеть содержимое вашего файла в панели Application Output [Вывод приложения] внизу MonoDevelop.

Алло, оператор?

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

myfile += "\nМои крылья как щит... ой, это из другого мультика.";
File.WriteAllText("myfile.txt", myfile);
(thumbnail)
Левая панель MonoDevelop показывает файл решения (по умолчанию) или онлайн-справку. Учтите: некоторые страницы помощи устарели!

В этом коде += является оператором: это такой символ, который выполняет операцию (лихо закручено, а?). Из школьной арифметики вы знаете операторы +, *, -, /, они выполняются над двумя числами, по-научному – операндами. А оператор = берет значение одного операнда и присваивает его другому. Так, выражение a=10 означает, что переменная а принимает значение 10.

Сейчас ваш труд окупится. Если а равно 10, то как прибавить к нему еще 10? Ага-ага…

a = a + 10;

Работает! Согласно принципу «бритвы Оккама» («при прочих равных условиях, лучшим объяснением будет простейшее»), этот код вообще является наилучшим. Принцип, однако, имеет малоизвестное добавление, под названием «поправка Оккама»: в простейшем решении непременно кроются недостатки. В нашем случае, недостаток тот, что для набора строки требуется 11 нажатий клавиш, а C# позволяет сделать то же самое за 8:

a += 10;

Здесь применен оператор +=, дитя любви операторов + (сложения) и = (присваивания): он прибавляет то, что справа от него, к тому, что слева. Класс! Теперь, с новообретенными знаниями, вы поймете, что код нашей программы добавляет строку (Мои крылья как щит... – ну, вы знаете, откуда это) к уже существующей строке. Языку C# хватает ума различить, когда += используется над числами (для сложения двух чисел), а когда – над строками (для конкатенации двух строк). Мы начинаем новую строку с \n, это информирует Mono о необходимости добавить символ новой строки в существующем файле.

Метод WriteAllText() относится к методу ReadAllText() как Wilykit к Wilykat: дайте ему имя файла в качестве первого параметра и текст для записи в качестве второго, а он выполнит всю работу.

Пора вдарить по газам Громокара и ввести конструкции посерьезнее: рассмотрим условные выражения и циклы. Условные выражения нужны, чтобы выполнять действия только при выполнении (истинности) определенного нами условия. Если условное выражение ложно – небо не голубое, возраст пользователя не 26, или что мы там проверяем – то код исполняться не будет. Например:

string Name = "Cheetara";
if (Name == "Snarf") {
    Console.WriteLine("Snarf snarf!");
}
Когда = ведет к ошибке

Используя одиночный знак = в условных выражениях, вы рискуете нарваться на проблемы. Например, рассмотрим код

if (Name = "Snarf") {

Этот код на самом деле означает «присвоить "Snarf" переменной Name, и если это произойдет удачно, то выполнить код внутри фигурных скобок». Такое присваивание, естественно, проходит всегда, и в качестве возвращаемого значения вернется true и будет выведено сообщение. Если вы часто пишете =, вместо ==, то можете решить проблему, просто поменяв местами параметры. То есть, следующие два выражения делают одно и тоже:

if (Name == "Snarf") {
// или...
if ("Snarf" == Name) {

Разница состоит в том, что если вы наберете = вместо == во втором варианте, то MonoDevelop откажется компилировать программу, потому что нельзя присвоить строке переменную – "Snarf" всегда останется "Snarf", и никак иначе.

Данный код ничего не напечатает: хотя переменная Name существует, ее содержимое не Snarf, а Cheetara. Заметим, что == просто еще один оператор, означающий «равняется». Он отличается от оператора присваивания = (см. врезку «Когда = ведет к ошибке»).

Знакомство с циклом

Циклы позволяют выполнять определенный блок кода несколько раз. Например:

Console.Write("Громо... ");
Console.Write("Громо... ");
Console.Write("Громо... ");
Console.Write("Громокот! Хо!\n");

Строка «Громо» не раз повторяется, поэтому для ее многократного вывода на экран мы можем запихать ее вовнутрь так называемого «цикла for»:

for (int i = 1; i <= 3; ++i) {
    Console.WriteLine("Громо... ");
}
Console.WriteLine("Громокот! Хо!\n");

Согласен, в этом примере оба варианта кода имеют те же четыре строчки; ну, а если пришлось бы выполнить операцию 100 раз? Или 100000? Заметим, что ++ это сокращение C# для выражения +=1 – оно просто прибавляет единицу к выражению. C# предусматривает несколько разных циклов, и for – один из них.

Идем дальше

Сейчас мы расширим нашу программу таким образом, что она будет считывать все файлы в каталоге, и если у файла расширение txt – распечатывать его содержимое. Тут нужны и цикл, и условное выражение – о меч Завета, помоги мне постичь непостижимое!

string[] files = Directory.GetFiles("/home/paul");
foreach(string file in files) {
    if (file.EndsWith(".txt")) {
        Console.Write(File.ReadAllText(file));
    }
}
(thumbnail)
Так будет выглядеть наш кэш файлов: в каждой строке по одному имени txt-файла.

Здесь показано аж 5 нововведений, поэтому позвольте мне разбить все по этапам:

  • Если мы передадим Directory.GetFiles() каталог в качестве единственного параметра, то он вернет нам массив строк (string[], помните?), содержащий все имена файлов этого каталога.
  • Элементы почти всех массивов можно перебрать с помощью цикла foreach: он извлекает каждый элемент массива, а мы присваиваем значение элемента переменной. В нашем примере, мы заставляем Mono присваивать каждое имя файла строке 'file'.
  • У каждой строки есть метод EndsWith(), который возвращает true, если строка заканчивается подстрокой, которую мы передали ей в качестве параметра. Если метод возвращает true, то мы выполняем код внутри фигурных скобок (Console.Write(...)).
  • Вместо того, чтобы присвоить возвращаемое значение File.ReadAllText() другой строке, мы сразу же передаем его в метод Console.Write. Так делать можно, и это помогает сделать код чуть короче.
  • Отметим, что C# различает File (особый класс, позволяющий читать и записывать файлы) и file, строковую переменную, которую мы создали. Все переменные в C# чувствительны к регистру.
Скорая помощь

Если хотите красиво обработать несколько параметров командной строки, перестаньте использовать конструкцию args[0]. Вместо этого попробуйте использовать цикл foreach для извлечения каждого параметра командной строки и его последующей обработки.

Говорите в скобках!

Как мы видели в прошлый раз, C# широко использует фигурные скобки { и } как маркеры блоков – они отмечают начало и конец блоков кода. Хотя они необходимы для пространств имен, классов и методов, после условных выражений и циклов их использовать не обязательно. В данном случае скобки описывают, какой код исполнит C#, если условное выражение будет истинно или пока условие цикла истинно. При отсутствии скобок выполнится только одно выражение; если оно и вправду одно, зачем писать скобки? Вот, например:

if (Name == "Snarf") {
    Console.WriteLine("Snarf snarf!");
}

С тем же результатом можно использовать следующий код:

if (Name == "Snarf") Console.WriteLine("Snarf snarf!");

В этом коде метод WriteLine() выполнится только тогда, когда Name будет содержать строку «Snarf». А все последующие выражения будут выполнены несмотря ни на что – даже если они расположены на той же строке, что вызов WriteLine(). Использование фигурных скобок рекомендуется при обучении: можно наглядно видеть, какой код относится к циклам и условным выражениям.

Замените содержимое метода Main() новым кодом, подставьте вместо /home/paul ваш собственный каталог с текстовыми .txt-файлами. Нажмите F5: вы увидите, что все работает, но это скорее код Громомальчика, чем Громомужа.

Вы когда-либо слышали фразу «быстро, качественно, дешево – выбери любые два»? Что ж, раз Linux связан с Open Source, «дешевизна» присутствует по определению. Но вот чудо: Mono позволяет еще и сделать «быстро» и «качественно»! Немного поколдуем, чтоб сделать наш код более быстрым и более функциональным.

Колдовство сидит в методе Directory.GetFiles(). Сейчас мы передаем ему один параметр, то есть каталог, где хотим искать файлы. Но в C# методы могут выполнять разные действия в зависимости от числа передаваемых параметров. Мы можем ускорить работу нашего кода, задав второй параметр метода GetFiles(), который позволит нам определить фильтр поиска для наших файлов. А именно, вместо того, чтобы использовать file.EndsWith(".txt"), используем второй параметр метода Directory.GetFiles, то есть *.txt. Тогда массив строк files будет содержать только файлы, заканчивающиеся на .txt. Можно заставить наш код делать даже больше, определив еще один параметр метода Directory.GetFiles, а именно SearchOption.AllDirectories. Параметр заставляет Mono искать не только в указанном каталоге, но и во всех его подкаталогах.

Поэтому новый супер-пупер метод Main() будет выглядеть следующим образом:

string[] files = Directory.GetFiles("/home/paul", "*.txt", SearchOption.AllDirectories);
foreach (string file in files) {
    Console.Write(File.ReadAllText(file));
}

Эффектный финал

Места в статье уже не хватает – поэтому пора просить древних духов C# превратить этот загнивший код во что-то действительно работающее! Мы уже видели, как можно получить все имена файлов в данном каталоге (и его подкаталогах), и знаем, как читать и записывать файлы. Теперь нам необходимо, чтобы программа выполняла две вещи:

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

Наша программа будет делать нечто очень похожее на работу Linux-команды updatedb, используемую для генерации кэша поиска для locate. Но updatedb не выводит содержимого найденных файлов, значит, наша программа хоть чуть-чуть, да получше!

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

if (args.Length == 0) {
    string[] files = Directory.GetFiles("/home/paul", "*.txt", SearchOption.AllDirectories);
    File.WriteAllLines("filecache.snarf", files);
} else {
    string[] cache = File.ReadAllLines("filecache.snarf");
    foreach(string file in cache) {
        if (file.Contains(args[0])) {
            Console.Write(File.ReadAllText(file));
        }
    }
}

Этот код содержит нечто совершенно новое: args.Length. В прошлый раз мы видели, что C# передает аргументы в метод Main() через массив строк 'args' (помните, что массив – это группа объектов одного типа). args.Length используется, чтобы узнать размер массива (т.е. сколько параметров было передано). Если он равен 0, то есть параметры не передавались, то нам необходимо создать кэш имен файлов.

Скорая помощь

Наш код генерирует filecache.snarf, когда запускается без параметров, но вдруг пользователь сначала захочет запустить ее с параметром? В данной ситуации программа даст сбой, так как файла не было создано. Решение – использовать метод File.Exists(): если файл существует, то запускаем поиск. Если нет, то сначала создаем его и потом запускаем поиск.

Метод WriteAllLines() очень похож на метод WriteAllText(), за исключением того, что он принимает в качестве второго параметра не одну строку, а массив строк. Нам это очень полезно, так как Directory.GetFiles() возвращает массив строк, поэтому мы можем просто передать его в WriteAllLines(), нам не понадобится осуществлять построчную запись.

Состояние .NET

На данный момент Microsoft выпустила три версии .NET: 1.0, 1.1 и 2.0. Mono – свободная реализация .NET – поддерживает почти всю реализацию 1.0 и 1.1, и в большинстве случае вы можете на это рассчитывать. В новые версии Mono добавляются многие новые возможности .NET 2.0, включая новые виджеты Windows.Forms (строительные блоки пользовательского интерфейса в Windows), а также новые методы, например, ReadAllText() и WriteAllText(), которые мы использовали на этом уроке.

Хотя .NET 2.0 была выпущена только в ноябре 2005, Microsoft уже готова выпустить .NET 3.0, капитальное изменение платформы. Не беспокойтесь: внутренности по большей части останутся прежними. Видите ли, .NET 3.0 был изначально известен как WinFX, и был спроектирован на основе .NET 2.0 с добавлением новых библиотек для поддержки нового пользовательского интерфейса, коммуникаций и тому подобных вещей. Фактически, .NET 3.0 – это .NET 2.0 плюс новый набор библиотек, и Mono не обязательно их реализовывать, если они не требуются.

И опять мы встречаем нечто новое: else. Вы уже знакомы с if, условным оператором, выполняющим код при истинности условия. А что если условие ложно? Здесь приходит на помощь else. Например, следующий код выведет «Ты Громокот!»:

string Home = "Третья планета";
if (Home == "Средиземье") {
    Console.WriteLine("Ты хоббит!");
} else {
    Console.WriteLine("Ты Громокот!");
}

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

Наконец, мы переходим к главному блоку кода: мы просматриваем каждую строку в кэше файлов и проверяем ее на соответствие передаваемому параметру. Соответствие осуществляется с помощью специального метода Contains(), выполняемого над строками: передаем в качестве параметра подстроку, а метод возвращает true, если она содержится в строке. Переменная args[0], как вы узнали из прошлого раза, содержит первый параметр, переданный в командной строке. Если имя файла соответствует параметру, то мы считываем текст файла и выводим его на экран. Между прочим, кроме EndsWith() и Contains(), к строкам применимы методы Replace() (заменить одну подстроку другой), ToUpper() (преобразовать строку в верхний регистр) и Trim() (чтобы удалить пробелы, символы табуляции и символы новой строки, расположенные в начале и в конце строки).

Вот и все: наш проект закончен. Snarf роется в файловой системе, выводит на экран все файлы, соответствующие запросу, а Третья планета в очередной раз спасена от Mымм-Ра – и все благодаря Mono! На диске к журналу вы найдете полный код Snarf, а также дополнительные материалы. Обратите внимание на сообщения, которые выводятся при создании кэша файлов; numfilesfound – это переменная, которая увеличивается каждый раз при нахождении подходящего файла, чтоб мы могли вывести сообщение, если ни одного файла не обнаружится; а также, в случае успеха будет напечатаны имена всех файлов.


Mono: Читаем RSS-ленты

На минутку оторвались от новостей на Slashdot, и уже боитесь, что отстали от жизни? Пол Хадсон говорит: прочтите обо всем с Mono – это проще, чем сосчитать до трех...

Наш эксперт

Пол Хадсон
полагает, что Mono – лучшая вещь со времен мультфильма Pinky and the Brain, и сейчас поддерживает два проекта на основе Mono на SourceForge.

(thumbnail)
http://BBC.co.uk применяет таблицу стилей XSL к своей ленте RSS – для ее просмотра в неформатированном виде используйте пункт «Просмотреть исходный код» в контекстном меню браузера.

Интернет завален хламом – блогами людей, страдающих от своих детских страхов, бесчисленными «разборками» фанатов и миллионами часов видео про котят на YouTube. Возникает проблема: как пробраться через этот хлам к вещам действительно интересным? Поможет RSS: эта технология позволяет людям подписаться на интересующие их сайты и затем получать обновления по мере появления изменений. Например, если вы любитель новостей с сайта BBC News, но вам неохота что ни час лазать на него в поисках свежатинки, то RSS – для вас.

В прошлый раз мы разобрались, как работать с файлами, а теперь зайдем дальше: поработаем с XML. Формат XML очень похож на HTML: здесь тоже особым образом помечаются блоки текста. Но, в отличие от HTML, XML определяет не внешний вид текста, а его содержание, и это делает XML идеальным средством для пересылки данных. XML используется во множестве форматов, в том числе и RSS: Really Simple Syndication [очень простое приобретение информации]. RSS определяет способ хранения ленты новостей, обновляемой при каждом изменении на сайте; вы можете скомандовать компьютеру обновлять RSS-ленту каждые десять минут и читать заголовки новостей, не запуская web-браузер.

Mono поставляется с массой инструментов работы с XML, и нам не придется беспокоиться о том, как прочесть файл. Значит, можно сконцентрироваться на том, что мы хотим сделать, а именно:

  1. Создать программу для скачивания и красивого вывода RSS-лент.
  2. Заставить программу отслеживать содержимое ленты и отображать изменения, произошедшие с момента последнего просмотра.
  3. Заставить программу сохранять выбранные пользователем ленты новостей.

Как и в прошлый раз, постараемся создать действительно полезную программу. Вперед!

Охота на RSS

Во-первых, необходимо ясно понять, как выглядит RSS. Ниже представлена RSS-лента, и вы можете видеть, что у канала (channel, новостная лента) есть заголовок, описание и ссылка. Но это скорее метаинформация – если вас интересуют только новости, можете ее игнорировать. Вы также видите два элемента <item>, хотя их может быть сотня и более, в зависимости от ленты RSS. Эти элементы и есть пункты новостей, содержащие заголовок, описание и ссылки, на сей раз к конкретным новостям. Вот пример:

<?xml version="1.0" ?>
<rss version="2.0">
    <channel>
        <title>Мой превосходный сайт</title>
        <description>Здесь представлено много интересного – не забудьте подписаться!</description>
        <link>http://www.example.com</link>
        <item>
            <title>Mono рулит!</title>
            <description>Бесплатная реализация .NET покоряет мир</description>
            <link>http://www.example.com/news/mono</link>
            <guid>http://www.example.com/news/mono</guid>
        </item>
        <item>
            <title>Mono побеждает PHP</title>
            <description>Согласованные функции – это круто</description>
            <link>http://www.example.com/news/monovsphp</link>
            <guid>http://www.example.com/news/monovsphp</guid>
        </item>
    </channel>
</rss>

Вы заметите, что у каждого пункта <item> есть элементы <link> и <guid>. 'GUID' (сокращение от Globally Unique Id, уникальный глобальный идентификатор) – любое значение, уникальное для данной новости во всей Сети. Этот пункт обязателен для лент RSS, так как именно по нему программы RSS определяют, видели они новость или нет. Будьте внимательны: GUID обязан быть уникальным не только на своем сайте, но и на других. Простейший (и наиболее распространенный) способ выбора GUID – сделать его ссылкой на новость: уникальность гарантируется.

Рассмотрим реальный пример. Начните новый консольный проект в MonoDevelop и назовите его по своему вкусу. Наверху, где находятся строки 'using', добавьте следующую:

using System.XML;
(thumbnail)
Среда MonoDevelop превосходна для кодирования, но нам придется использовать CLI, чтобы вставить собственные параметры. Обратите внимание на ссылку System.XML на левой панели!

Далее, на панели Solution (слева в окне MonoDevelop), правой кнопкой щелкните на References и в появившемся меню выберите Edit References. Убедитесь, что там выбрано System.XML, и нажмите OK. Теперь приведите метод Main() к следующему виду:

XmlDocument doc = new XmlDocument();
doc.Load("http://tinyurl.com/8mwkm");
Console.Write(doc.InnerXml);

Ссылка TinyURL вставлена для экономии места – если угодно, укажите полный URL: http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml.

Этот код использует класс XmlDocument для чтения URL и вывода его на экран. Пока мы ничего особенно не делаем: просто выводим документ на консоль. Нажмите F5, чтобы скомпилировать и запустить программу. Вы увидите в окне Application Output в MonoDevelop большой кусок текста. Это наша RSS-лента – не беда, что она выглядит довольно зверообразно: мы ее приручим!

Просто заголовки

Для дрессировки RSS есть отличные методы .NET: SelectSingleNode() и SelectNodes(). Они позволяют осуществлять поиск нужных данных в XML-документе и извлекать либо самый первый XML-узел (т.е. XML-элемент <item>, прочитанный нашей программой), либо все подходящие узлы.

Итак, мы хотим, чтобы версия номер два нашей программы считывала все новости и для каждой выводила ее заголовок и описание. Вот мой рецепт для Hudson RSS Reader v2:

  1. Пропустить RSS через XmlDocument.load().
  2. Отделить интересующие нас элементы <item>.
  3. Просеять элементы с предыдущего шага и при необходимости откинуть их данные на Console.Write().
  4. Подсолить и подать на стол. Или – по-простому, в синтаксисе C#...
XmlDocument doc = new XmlDocument();
doc.Load("http://tinyurl.com/8mwkm");
XmlNodeList items = doc.SelectNodes("//item");
foreach (XmlNode item in items) {
    Console.WriteLine(item.SelectSingleNode("title").InnerText);
    Console.WriteLine(" " + item.SelectSingleNode("description").InnerText);
    Console.WriteLine("");
}

Параметр, передаваемый в SelectNodes//item – известен как Xpath. Это особый способ поиска элементов внутри XML, и в нашем случае он требует 'брать из XML-документа все элементы <item>'. Двойной слэш // именно и означает 'взять любое вхождение'. Взгляните на следующий XML-документ:

<stuff>
    <clothing>
        <item>Брюки</item>
        <item>Носки</item>
    </clothing>
    <news>
        <item>Релиз Wii состоялся</item>
        <item>Xbox 360 - отстой!</item>
    </news>
</stuff>

Извлекая из него элементы <item> при помощи Xpath //item, вы довольны не останетесь: ваш запрос свалит в кучу и новости, и штаны с носками! Вместо использования // 'найти любого', уточним запрос: скажем, что нам нужны элементы <item>, являющиеся частью элементов <news>. В XPath это выглядит как /news/item.

Впрочем, RSS-лента использует элементы <item> только для обозначения новостей, поэтому использовать //item вполне безопасно. Этот поиск возвращает переменную, известную как XmlNodeList. Если XML-узел (XML node) содержит один XML-элемент, то в XmlNodeList, видимо, находится несколько XML-элементов, верно? Верно. Я просто хотел убедиться, что вы не запутались при обсуждении Xpath!

Раз уж список новостей получен, остается только их распечатать. В прошлый раз я познакомил вас с циклом foreach, теперь он снова вернулся – и будет работать с XmlNodes, а не просто со строками. Этот цикл перебирает каждую новость, которую возвращает метод SelectNodes(), и присваивает ее переменной item, которую мы можем прочесть. Каждый пункт <item> в нашем XML-документе содержит несколько интересных подпунктов: заголовок новости, описание, ссылку и т.д. Чтобы извлечь любой из них, необходимо применить к нашему пункту метод SelectSingleNode, который возвращает XmlNode. Например, чтобы получить заголовок новости, необходимо применить item.SelectSingleNode("title"). Однако метод вернет просто XML-узел, являющийся .NET-представлением XML-элемента <item>, а не содержание XML-узла. Тут пригодится InnerText: он возвращает текстовое содержимое объекта XmlNode.

Держа все это в голове, посмотрим снова на эту строчку:

Console.WriteLine(item.SelectSingleNode("title").InnerText);

Она работает так: берет текущий узел, затем его заголовок и текстовое содержимое, и выводит на консоль.

Когда все это выведено, вызывается Console.WriteLine() для вывода пустой строки между новостями.

Вот и все – скомпилируйте и запустите вашу программу с помощью F5 и наслаждайтесь: ваши кулинарные навыки превратили сырые ингредиенты RSS-ленты в вывод на вашем экране!

Что нового, малыш?

У нашей программы есть проблема: RSS-ленты бывают очень большими, а людям подавай только свежие новости, появившиеся после последнего опроса ленты. Это действительно проблема: как уследить, что некоторые новости уже прочитаны, и показать только те, которые вы еще не видели? Что ж, возвратитесь назад на 1000 слов, к уникальным глобальным идентификаторам. Вот что я говорил: «Этот пункт обязателен для лент RSS, так как именно по нему программы RSS определяют, видели они новость или нет.» Каждой RSS-новости необходим GUID, однозначно определяющий ее в сети, и его можно использовать для решения проблемы.

Вот алгоритм:

  1. Получить ленту RSS.
  2. Сохранить все GUID в файле, по одному на строку.
  3. В следующий раз, когда будет загружена RSS-лента, будут показаны только новые новости, отсутствующие в кэше GUID.

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

Скорая помощь

Для создания файла есть метод File.Create(), но мы обойдемся без него – метод File.AppendAllText() автоматически создаст файл, если его не существует.

XmlDocument doc = new XmlDocument();
doc.Load("http://tinyurl.com/8mwkm");
// Этот массив строк будет хранить содержимое кэш-файла
string[] guidcache;
if (File.Exists("guidcache.txt")) {
    // У нас есть кэш-файл – так прочтем его!
    guidcache = File.ReadAllLines("guidcache.txt");
} else {
    // У нас нет кэш-файла – создадим новый массив строк из 0 элементов (то есть пустой)
    guidcache = new string[0];
}
// Получить все новости…
XmlNodeList items = doc.SelectNodes("//item");
foreach (XmlNode item in items) {
    // Положим, что мы собираемся показать эту новость по умолчанию
    bool showthisitem = true;
    // Теперь переберем каждый GUID в кэше…
    foreach (string guid in guidcache) {
        // … и сравним его с GUID этой новости.
        if (guid == item.SelectSingleNode("guid").InnerText) {
            // Если у нас есть совпадение – то не показываем эту новость!
            showthisitem = false;
            // Следующее выражение говорит C# выйти из цикла – мы уже нашли GUID,
            // поэтому не надо проверять остальной кэш GUID.
            break;
        }
    }
    if (showthisitem) {
        // Сюда попадаем только если GUID не находится в нашем кэше – выводим его на экран!
        Console.WriteLine(item.SelectSingleNode("title").InnerText);
        Console.WriteLine(" " + item.SelectSingleNode("description").InnerText);
        Console.WriteLine("");
        // … теперь добавим GUID в конец кэш-файла.
        File.AppendAllText("guidcache.txt", item.SelectSingleNode("guid").InnerText + "\n");
    }
}

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

string thisguid = item.SelectSingleNode("guid").InnerText;

Вместо того, чтобы вызывать SelectSingleNode() для каждого GUID в кэше и для каждой новости, этот код кэширует GUID в строке символов, и вы можете использовать ее вместо вызовов SelectSingleNode().

Подпишись сегодня!

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

Вот какой я ее вижу:

  1. Когда программа запускается с параметром sub, за которым следует URL, она должна подписаться на эту ленту.
  2. Когда программа запускается с параметром unsub, за которым следует URL, она должна отписаться от данной ленты.
  3. Когда она запускается без параметров, то должна обновить все RSS-ленты и показать все новые записи.
  4. Когда программа запускается с параметром reset, она должна очистить список GUID и обновить ленты, показав все записи во всех лентах.

В общем, не очень далеко от уже готового кода, но есть одна особенность: пунктам 2 и 3 необходимо выводить на экран RSS-ленты. Самый тупой способ это сделать – взять кусок кода с печатью ленты, скопировать его и вставить второй раз, но куда лучше создать отдельный метод: его можно вызывать откуда угодно, и код будет сосредоточен в одном месте.

Но сначала надо написать код для подписки на новостные ленты и отказа от нее. Здесь вы можете изучить новую классную штуку – блок switch/case. Вам уже попадалось условное выражение (вспомните: if/else), однако его трудновато использовать при проверке множества условий. Блок switch/case позволяет проверять значение переменной, не засоряя код. Например, простейший код для проверки того, что должна делать программа, выглядит так:

Скорая помощь

Очень важно проверять, сколько аргументов передается в массив args, прежде чем работать с ним, потому что Mono вылетит, если вы попытаетесь прочитать элемент, которого не существует. Будьте внимательны!

switch (args.Length) {
    case 0:
        // обновить ленты!
        break;
    case 1:
        // осуществить сброс лент!
        break;
    case 2:
        // подписаться или отписаться на новостные ленты!
        break;
}

Это код проверяет значение переменной args.Length на равенство 0, 1 или 2. Значение args.Length автоматически устанавливается равным числу параметров, передаваемых программе из командной строки, поэтому фактически оно определяет «Как эта программа была запущена?» Если параметры отсутствуют, значит, достаточно обновить ленты новостей. Если имеется один параметр, можно сразу выполнить сброс лент новостей, даже не проверяя, что это за параметр: единственное действие, описываемое одиночным параметром – перегрузка новостных лент [в «настоящей» программе не помешало бы сделать проверку, что этот единственный параметр – 'reset'; никто не запрещает пользователю запустить программу с единственным параметром 'sub' или даже 'asdfghj'! В последних двух случаях его следует известить об ошибке – например, распечатать список допустимых опций командной строки, – прим. ред.]. Наконец, если передается два параметра, то необходимо проверить, sub это или unsub, а затем предпринять соответствующее действие.

Принимаем решение

(thumbnail)
Готовый продукт: Программа чтения RSS-лент, помнящая, что вы читали.

Когда switch натыкается на совпадение, то выполняет код, начиная с найденного места, пока не упрется в другую строку case или инструкцию break. Строка 'break' просто сообщает: «С совпадением покончено, переходи в конец блока switch/case.» В предшественнике C# – C++, если вы не ставили инструкцию break, программа продолжала выполнение следующего выражения case, вне зависимости от того, произошло совпадение или нет. В C# этого не происходит, но break все равно является обязательным.

Сначала разберемся с подпиской и отказом от лент новостей. Для этого требуется проверить, что аргумент – либо sub, либо unsub, затем добавить ленту в список подписки. Вот как это происходит:

case 2:
    // Подпишись или откажись!
    if (args[1] == "") return;
    if (args[0] == "sub") {
        // Добавить сайт в существующий список
        File.AppendAllText("sitelist.txt", args[1] + "\n");
    } else {
        if (File.Exists("sitelist.txt")) {
            // Удалить сайт из списка
            string[] sitelist = File.ReadAllLines("sitelist.txt");
            File.Delete("sitelist.txt");
            foreach (string site in sitelist) {
                if (site == args[1]) {
                    // Да! именно этот сайт надо удалить; игнорируем его
                } else {
                    File.AppendAllText("sitelist.txt", site + "\n");
                }
            }
        }
    }
    break;

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

Остальные два случая проще и выглядят так:

Скорая помощь

Мы используем параметр reset, чтобы очистить GUID'ы для всех наших RSS-лент – тогда наша программа скачает все новости из новостных лент. Если вы хотите разрешить людям предоставлять параметр URL для reset, чтобы сбросить только конкретную ленту, то самый простой способ это сделать – хранить GUID в отдельных файлах: вместо guidcache.txt у вас может быть guid-news.bbc.co.uk.txt. Чтобы очистить кэш GUID для одного сайта, просто удалите соответствующий ему файл.

switch (args.Length) {
    case 0:
        // Обновить!
        ReadFeeds();
        break;
    case 1:
        // Сброс!
        File.Delete("guidcache.txt");
        ReadFeeds();
        break;

Метод ReadFeeds() – то самое повторное использование кода, о котором я говорил: можно было вставить весь код для чтения новостей прямо в case-выражение, но гораздо лучше создать собственный метод, ReadFeeds(). Итак, если ваша программа выполняется без параметров, сразу же вызывается метод ReadFeeds(); а если с параметром, мы очищаем кэш GUID, а уж потом вызываем ReadFeeds().

Метод ReadFeeds() в принципе такой же, что и старый код для чтения RSS, но я боюсь, что его придется модифицировать, если вы захотите читать ленты с нескольких сайтов. Для этого необходимо в списке подписки перебрать в цикле все сайты, а в каждом сайте – все его новости. Вот главная часть:

string[] sitelist;
if (File.Exists("sitelist.txt")) {
    sitelist = File.ReadAllLines("sitelist.txt");
} else {
    sitelist = new string[0];
}
foreach (string site in sitelist) {
    XmlDocument doc = new XmlDocument();
    doc.Load(site);

Здесь нет ничего нового, но этот код идеально завершает нашу программу. Нажмите F8, чтобы скомпилировать ее, затем откройте терминал и перейдите туда, где расположен проект MonoDevelop. Далее зайдите в каталог bin/Debug, там находится исполняемый файл. Запустите его – и, я думаю, вы согласитесь, что программа и вправду полезна!

Мы прошли немалый путь: вы больше не боитесь XML, вы поняли, что создавать свои методы – это хороший стиль, и изучили блок switch/case, благодаря которому условные выражения выглядят опрятно. А главное, вы создали свое второе полезное рабочее приложение на C# с помощью Mono – молодцы!

Персональные инструменты
купить
подписаться
Яндекс.Метрика