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

LXF71:PHP

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

SimpleXML и XPath

Пол Хадсон (Paul Hudson) вновь пытается помочь с решением загадки Sudoku и погружается в изучение XML для дела и удовольствия.

одна из многих латинских поговорок, приписываемых юлию Цезарю, звучит так: «Beati Hispani quibus vivere bibere est». В свободном переводе это значит: «блаженны испанцы, для которых жить — значит пить». И кто с этим не согласится? Но мало кто знает, что эта фраза очень похоже прозвучит в современной Испании. там вы можете сказать «Dichosos los espaсoles, para quienes vivir es beber.» Любой, хорошо знакомый с испанским языком, может посмотреть на фразу на латыни и догадаться о её значении, благодаря тому факту что испанский язык относится к романской группе — не из-за своей романтичности, а потому что он происходит от разговорного латинского языка, на котором говорили римляне (Romans) много лет назад.

Удивительно, но в компьютерных науках наблюдается такая же степень стандартизации. Несмотря на то, что мы никак не можем договориться, сколько кнопок должно быть на мышке, мы при этом способны создавать системы для обмена данными в понятной форме. XML (the eXtensible Markup Language, расширяемый язык разметки) — это формат данных, основанный на текстовом представлении, позволяющий легко читать и сохранять данные, а так же делиться ими с другими, в том числе и через Интернет. как латынь и языки романской группы, разные XML-схемы (правила, по которым создаются XML-файлы) отличаются друг от друга, но не настолько, чтобы другие стали непонятными, если вам знакома только одна из них. В сегодняшней статье мы рассмотрим XML для сохранения кроссвордов: мы будем читать их, записывать и показывать на экране. Мы не собираемся их разгадывать — мы займёмся совершенно другой проблемой.

Зачем использовать XML

Даже теперь, когда XML присутствует везде, мы всё еще видим разнообразие форматов. Причина этого проста — XML несовершенен. он очень многословен, он не имеет строгой типизации, он понятен для человека и поэтому заметно медленнее бинарных форматов данных. Но у него есть свои достоинства. Главные из них — возможность проверить корректность файла, не имея представления о его схеме, поддержка Unicode, возможности самодокументирования и жёсткие синтаксические правила, позволяющие быстро и безошибочно читать XML-файл.

В общем, XML силён несмотря на все свои проблемы, потому что он позволяет легко обмениваться данными между различными программами, в том числе и через Интернет. Используя его для сохранения кроссвордов, мы можем быть уверены что при желании другие программы смогу легко воспользоваться ими. Если вы читали о возможностях технологии Ajax, то вы уже знаете о возможностях, которые даёт XML при программировании на стороне клиента. Если у вас нет каких-то специфических требований, для большинства проектов XML подходит лучше всего.

создание простого XML

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

Вот пример XML-файла под названием requiem.xml:

<requiem>
<line>
<latin>Confutatis maledictis</latin>
<english>When the wicked are confounded</english>
</line>
<line>
<latin>Flammis acribus addictis</latin>
<english>Doomed to flames of woe unbounded</english>
</line>
</requiem>

В этом примере корневым элементом является requiem. он содержит два элемента line, в которых в свою очередь находятся latin и english — как видите, тут нет ничего мудрёного. Мы можем разобрать этот файл и напечатать результат разбора с помощью двух строк на PHP:

<?php
$file = simplexml_load_file("requiem.xml");
var_dump($file);
?>

Функция var_dump даёт прекрасную возможность разобраться, как что-то работает в PHP, так как она позволяется наглядно увидеть данные, содержащиеся в переменной. Вот, что выводит наша программа:

object(SimpleXMLElement)#1 (1) {
["line"]=>
array(2) {
[0]=>
object(SimpleXMLElement)#2 (2) {
["latin"]=>
string(21) "Confutatis maledictis"
["english"]=>
string(30) "When the wicked are confounded"
}
[1]=>
object(SimpleXMLElement)#3 (2) {
["latin"]=>
string(25) "Flammis acribus addictis"
["english"]=>
string(33) "Doomed to flames of woe unbounded"
}
}
}

Понимание этой структуры — это ключ к использованию SimpleXML.

Корневой объект SimpleXMLElement содержит только одну переменную, line. О на является массивом из двух элементов (с номерами 0 и 1), соответствующих двум элементам <line> в исходном файле. Эти элементы, в свою очередь, являются объектами SimpleXMLElement, содержащими строковые переменные latin и english с соответствующими данными из исходного XML. Итак, вызов функции simplexml_load_file вернул нам смесь из объектов и массивов, соответствующую структуре XML-файла и содержащий все его данные в обычных переменных php. Вывод var_dump познавателен, но непригоден для нормальной работы, так что давайте перепишем нашу программу так, чтобы она печатала только английский текст:

<?php
$file = simplexml_load_file("requiem.xml");
foreach($file->line as $line) {
echo $line->english, "\n";
}
?>

Обратите внимание на то, как мы используем $file->line и $line->english. Т ак как $file и $line — объекты, мы можем получить доступ к их переменным при помощи оператора ->. Ничего не мешает вам трактовать объекты как массивы. Например, использовав их внутри цикла foreach, можно перебирать все поля объекта по одному, словно это элементы массива. Но лучше всё же мысленно различать объекты и массивы, так как SimpleXML использует «массивоподобный» метод для представления атрибутов. Например, если изменить первую строчку нашего XML следующим образом:

<requiem key=”D”>

то теперь с разобранным файлом вы можете сделать следующее:

$file = simplexml_load_file("requiem.xml");
print $file["key"];

Этот код отобразит значение атрибута key корневого элемента. Синтаксис $file->key не сработает, так как он обозначает обращение ко вложенному элементу <key>, которого не существует. Да, это похоже на чёрную магию, мы привыкли к тому что операторы массивов работают по другому. Всё дело в том, что здесь приходится работать с объектами SimpleXMLElement, а не с обычными плоскими массивами.

XML на входе, объекты на выходе

Как вы только что могли убедиться, SimpleXML позволяет вам работать с XML-файлами так же, как с объектами и массивами, так что вы можете игнорировать семантику XML и сосредоточиться на правильной обработке данных. Эта простота использования простирается вплоть до возможности изменять значения переменных, поскольку раз уж у вас есть объекты и массивы php, то вы можете делать с ними всё, что пожелаете. Что более интересно, после внесения изменений вы можете экспортировать результат обратно в XML. Для демонстрации этого мы используем новый XML файл:

<park>
<squirrel name=”Squirly”>
<nuts>320</nuts>
</squirrel>
<squirrel name=”Nick”>
<nuts>0</nuts>
</squirrel>
</park>

И так, в нашем парке (park) есть две белки (squirrel) по имени Squirly и Nick. У Squirly много орехов (nuts), а у Nick’а ничего нет. Чтобы это исправить, воспользуемся умением SimpleXML изменять значения переменных на лету. Сделать это действительно просто:

<?php
$park = simplexml_load_file("squirrels.xml");
$park->squirrel[1]->nuts = 10;
print $park->asXML();
?>

Этот сценарий превращает парк с белками в обычный набор объектов и массивов, получает доступ к одной из белок (второй, так как элементы нумеруются начиная с нуля, то есть squirrel[1] — это Nick) и изменяет число её орехов. Важной частью является вызов asXML, так как он позволяет превратить наши объекты снова в XML, и это лучший XML чем тот, с которого мы начинали:

<?xml version=”1.0”?>
<park>
<squirrel name=”Squirly”>
<nuts>320</nuts>
</squirrel>
<squirrel name=”Nick”>
<nuts>10</nuts>
</squirrel>
</park>

Nick обзавёлся орехами, а весь файл — заголовком с обозначением версии, которого у нас не было раньше. Метод asXML доступен для любого объекта SimpleXMLElement, так что вы можете получить XML одного только Nick-а при помощи $park->squirrel[1]->asXML(). Обратите внимание: при использовании asXML для любого другого объекта, кроме корневого, XML-заголовок с версией не вставляется, вы получите только кусочек XML-кода. После получения текстовой строки в нужном формате воспользуйтесь функцией file_put_contents для сохранения её в файле по вашему выбору.

Для того, чтобы увеличить возможности XML, было разработано множество стандартов, самый известный из которых — Xpath. он делает XML более похожим на SQL — теперь вы можете не просто использовать его для хранения информации, но и выполнять запросы. По своей сути Xpath — это способ вытащить нужную часть XML, указав путь к ней (похожий на путь к файлу в файловой системе). В PHP это достигается возвращением массива объектов SimpleXMLElement, соответствующих вашему запросу.

первые шаги по Xpath

Представляю вам новый XML, books.xml:

<books>
<author nationality=”British” name=”Jane Austen”>
<book>Pride And Prejudice</book>
<book>Sense And Sensibility</book>
</author>
<author nationality=”Colombian” name=”Gabriel Garcia Marquez”>
<book>Cien anos de soledad</book>
<book>El coronel no tiene quien le escriba</book>
</author>
<author nationality=”British” name=”David Baddiel”>
<book>Time For Bed</book>
<book>The Secret Purposes</book>
</author>
</books>

он содержит трёх авторов (<author>), из которых двое англичане, и для всех троих указаны их книги. С помощью Xpath мы можем запросить список всех книг:

<?php
$authors = simplexml_load_file("books.xml");
$books = $authors->XPath("//book");
foreach($books as $book) {
echo $book, "\n";
}
?>

Запрос «//book» ищет элемент <book> в любом месте дерева XML. Часть запроса // значит «искать везде», что для функции xpath значит «найти все корневые элементы <book>, найти все <book> внутри элемента, содержащего <author>, найти все <book> внутри элементов <author>», то есть найти <book> вообще везде, и вернуть все найденные элементы в одном массиве. Затем мы передаём полученный массив в цикл foreach и печатаем его. Есть один любопытный момент: если вместо print использовать var_dump($book), вы увидите, что в переменной на самом деле хранится SimpleXMLElement. Это часть той же магии, которая позволяет работать с объектами SimpleXML как с массивами. В нашем случае это магия функции __tostring(), позволяющая использовать объект в операторе print как обычную строку.

Если вы хотите добыть конкретную книгу, Xpath поможет вам и в этом. Например, добавьте следующий код к books.xml сразу перед строкой </books>:

<library name=”British Library”>
<book>
<title>The Peloponnesian War</title>
<author>Donald Kagan</title>
</book>
<book>
<title>The Peloponnesian War</title>
<author>Thucidydes</author>
</book>
</library>

теперь элементов <book> у нас стало больше, но они разные — элементы внутри <author> обозначают, какие книги этими авторами были написаны, а <book> внутри < library> — это список книг, имеющихся в библиотеке. Если по-прежнему использовать строку поиска //book, то мы получим их все, вне зависимости от того, есть эта книга в библиотеке или нет.

Для выполнения более точного поиска можно воспользоваться иерархией XML:

$books = $authors->XPath("/books/library/book");

Этот запрос вернёт все книги в библиотеке, не затронув книги авторов. Помните, что он по прежнему возвращает массив из SimpleXMLElement, то есть вы получите объекты со свойствами title и author. Чтобы получить только заголовки, можно воспользоваться запросом /books/library/book/title, но если нам надо и заголовки и авторов, то лучше запросить именно книги, а затем воспользоваться объектно-ориентированным синтаксисом:

<?php
$all_books = simplexml_load_file("books.xml");
$library_books = $all_books->XPath("/books/library/book");
foreach($library_books as $book) {
echo "{$book->title} was written by {$book->author}\n";
}
?>

XML нельзя назвать быстрым, но XPath — уже можно, так как в момент его использования XML преобразован во внутреннее представление, поиск по которому происходит сравнительно легко.

разделяй и запрашивай

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

<?php
$all_books = simplexml_load_file("books.xml");
$british_authors = $all_books->XPath('/books/author[@nationality="British"]');
foreach($british_authors as $author) {
echo "{$author["name"]} is British.\n";
}
?>

ключевая часть запроса заключена в квадратные скобки. Мы указываем, что хотим получить элемент author, вложенный в books, а в квадратных скобках содержится фильтр, которому должны удовлетворять наши элементы. @nationality обоозначает «Выбрать только такие элементы, у которых параметр nationality…» и далее написано условие «=British». Это условие позволяет ограничить поиск только английскими авторами. Символ @ имеет важное значение, без него условие применялось бы к вложенным элементам, а не к атрибутам. Например, запрос books/author[book="Cien anos de soledad"] вернёт «Gabriel Garcia Marquez».

кроме проверки на равенство вы можете использовать стандартные условия <, >, <=, >= и !=, а так же соединять несколько условий при помощи or или and.

$club_1830_eligible = $holidaymakers->XPath('/books/author[@age>=18 or @age<=30]');

кроме того, условия можно заключать в скобки, примерно так: /books/author[@age>=18 and (@name="Jim" or @name="Bob")].

кроме операторов сравнения, в условиях XPath доступны простые математические действия. В их число входят +, -, *, div и mod. Вот несколько примеров Xpath-запросов с фильтрованием:

$blessed = $people->XPath('//person[nationality="Spanish"]');
$meaning_of_life = $earth->XPath('//monkeys[@favouritenumber = 7 * 6]');
$a_grade_freshmen = $university->XPath('/students/student[@year = 1 and grades > 80.0'];
$dangerous = $people->XPath('//adults[@iq = @shoesize]'); // обратите внимание, мы можем сравнивать значение атрибутов между собой
$squirrels_with_comedy _oversized_tails = $animals->XPath('//squirrels[@tail > @body-length * 6]');
$offenders = $people->XPath('//people/[@outstanding_penalty = true()]'); //возвращает записи, имеющие параметр outstanding_penalty, пустой или с любым значением
$good_wines = $wines->XPath('/drinks/alcoholic/wines/wine[@year mod 2 = 1 and (@country="Australia" or @country="France")]');

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

Кроссворды

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

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

<crossword>
<title>My Excellent Crossword</title>
<grid>
-l--t-
-linux
-a--l-
-m--i-
-agape
------
</grid>
</crossword>

Чтобы загрузить такой кроссворд, надо получить текст элемента grid и затем разобрать его посимвольно. Но это не очень хорошее решение. как хранить номера слов в кроссворде? как добавить вопросы? как сохранять состояние решения? Для решения всех этих проблем нам потребуется «правильный» XML, в котором каждый квадратик описывается отдельным элементом. Эти квадратные элементы могут иметь следующие атрибуты:

  • type — тип квадратика, black(чёрный) или white (белый).
  • number — номер слова (если есть)
  • direction — направление слова, down (вниз), across (вправо) или both (оба), если есть.
  • downclue — вопрос для слова, идущего вниз, если есть.
  • acrossclue — вопрос для слова, идущего вправо, если есть.
  • correctanswer — буква, которая должна быть тут.
  • currentanswer — буква, которую сюда написал отгадчик.
  • guessedanswer — буква, в которой отгадчик не уверен.

Мы отсортируем элементы так, что сначала укажем первую строчку, потом вторую и так далее. Элемент <grid> будет хранить свойства — автора, сложность (номер от 1 до 4, 4 — самый сложный), и размер сетки (6 значит шесть квадратиков вправо и вниз).

Чтобы сэкономить пространство в журнале, я приведу пример кроссворда 3x3:

<grid author=”Paul Hudson” difficulty=”1” size=”3”>
<square type=”white” number=”1” direction=”both” downclue=”Water stopper” acrossclue=”Four-legged cathater” correct=”d” current=”” guessed=”” />
<square type=”white” correct=”o” current=”” guessed=”” />
<square type=”white” number=”2” direction=”down” downclue=”Water stopper” correct=”g” current=”” guessed=”” />
<square type=”white” correct=”a” current=”” guessed=”” />
<square type=”black” />
<square type=”white” correct=”o” current=”” guessed=”” />
<square type=”white” number=”3” direction=”across” acrossclue=”Crazily annoyed” correct=”m” current=”” guessed=”” />
<square type=”white” correct=”a” current=”” guessed=”” />
<square type=”white” correct=”d” current=”” guessed=”” />
</grid>

Сохранив этот код в файле crossword.xml, мы можем использовать следующий PHP-код для печати всех правильных ответов:

<?php
$crossword = simplexml_load_file("crossword.xml");
$i = 0; // square counter
foreach($crossword->square as $square) {
if ($square["type"] == "white") {
print $square["correct"];
} else {
print " ";
}
++$i;
if ($i % $crossword['size'] == 0) print "\n";
}
?>

Вложенный в цикл второй оператор if отвечает за правильную расстановку символов перевода строки. Элементы XML загружаются в порядке очереди, так что нам нужно прервать строку после символа номер $crossword["size"] или любого, кратного ему.

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

До встречи!

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