Отображение текста в формате reStructured невозможно без установки Docutils.
Совместная работа в mercurial
#############################
Для начала вспомним, что мы уже знаем про репозитории aka системы
контроля версий.
Система контроля версий -- это средство хранения истории изменения
набора файлов.
Разных систем контроля версий существует великое множество, наиболее
интересны из них:
* svn -- самый популярный из централизованных систем
(централизованные системы имеют несколько недостатков, наиболее
значимый из которых для нас состоит в том, что такие системы контроля
версий предполагают наличие специально знающего системного
администратора, который за ним следит),
* git -- самый популярный из децентрализованных систем, в нём
разрабатывается ядро ОС Linux, и автором его является автор ядра
Linux, что и обеспечивает ему популярность; так как он предназначен
для программистов, то он предполагает весьма продвинутого
пользователя, который знает, как не делать многие стандартные ошибки и
готов иногда покопаться у него в кишках
* mercurial -- ровесник и брат-близнец git; написан на python, и
немножко проще и толерантнее к ошибкам, чем git; собственно, поэтому
мы его и разбираем
Почти всё, что я здесь рассказываю, более-менее применимо, разве что с
точностью до замены названий команд и аргументов, ко всем
децентрализованным системам контроля версий (distributed version
control system, DVCS).
Для mercurial под windows я рекомендую установить TortoiseHG -- это
графический интерфейс к нему. Все действия, о которых я буду
рассказывать, доступны в этом случае из контекстного меню в Explorer
(оно же зовётся Проводник, оно же Мой компьютер и т.п.), либо из
командной строки windows: для этого годится cmd, если вы с ним
знакомы, либо TortoiseHG предлагает в контекстном меню запрятанную
команду "запустить командную строку в этой директории".
Репозиторий состоит из рабочей директории -- это просто директория на
компьютере, в которой лежат файлы, историю которых вы хотите хранить,
-- и истории. В меркуриале история лежит в поддиректории .hg в рабочей
директории. Внутрь поддиректории .hg вам почти никогда не потребуется
заглядывать, и большинство файлов, которые там лежат, не предназначены
для человеческого глаза. Рабочую директорию со всем содержимым можно
копировать куда угодно, меркуриал нигде специально себе не отмечает,
где лежал репозиторий в прошлый раз.
Основные команды
================
hg help
-------
Самая полезная команда для работы с mercurial -- help. Посмотрите в
то, что вам выдаёт сам hg help, hg help config, hg help имя_команды.
Если это всё равно не помогает, то тогда уж гугл наверняка что сможет
подсказать.
hg init
-------
Чтобы из директории, в которой лежат наши файлы, сделать репозиторий,
нужно зайти в неё и сказать в ней hg init.
hg add
------
Mercurial не решает сам, какие из файлов в рабочей директории нам
интересны и достойны того, чтобы их историю хранить, а какие
недостойны. Поэтому мы должны ему специально сказать hg add (добавить
в репозиторий все файлы, которые ещё не в нём) или hg add file1 file2
(добавить эти два файла в репозиторий).
hg commit
---------
Кроме этого, мы должны сами говорить mercurial, какие моменты жизни
нашего проекта мы хотим сохранить для истории. (Сам по себе он не
пытается хранить все изменения, это требовало бы слишком много места,
да и искать в такой истории что-нибудь полезное было бы трудно).
Момент жизни проекта, который мы сохранили в истории, называется
версией либо коммитом. Мы должны сопроводить коммит описанием того,
что изменилось по сравнению с прошлым состоянием (и лучше бы это
описание было информативным, тогда нам самим же будет проще искать,
где находится та самая экспериментальная версия, с которой мы хотим
ещё поиграться).
Это делается командой hg commit, которая запишет все файлы и откроет
вам notepad с тем, чтобы вы в нём вписали описание изменения. Если вы
не хотите общаться с notepad'ом, можно указать описание изменения
прямо в командной строке: hg commit -m "Описание". Ещё можно
записывать изменения не во всех, а только в некоторых файлах, см
хелпы.
Когда мы записываем изменение, мы подписываемся в его авторстве. Для
этого mercurial'у нужно знать, как нас правильнее называть. Сам он до
этого, как правило, не догадывается, а если и догадывается, то вряд ли
вам понравятся его догадки. Чтобы поправить это, нам нужно настроить
mercurial. В windows можно ткнуть куда угодно в проводнике или на
рабочем столе правой кнопкой мыши, найти в Tortoise HG -> Global
Settings -> Commit (Запись изменений) -> Username. Считается приличным
в качестве имени писать "Firstname Lastname <e@mail.com>". Кроме того,
что это даст возможность людям, общающимся с вами, писать вам вопросы
про коммиты, mercurial ещё и умеет полуавтоматически присылать
изменения на почту (о чём я, впрочем, рассказывать сейчас не буду).
hg status
---------
Полезно понимать, а записалось ли то, что мы записывали. Для этого
есть команда status, которая пишет, что меркуриал думает о файлах,
которые видит: что они сохранены и совпадают с сохранённой версией (по
умолчанию он такие файлы не перечисляет), что они сохранены, но после
сохранения мы их меняли (он нарисует у них букву M), что мы попросили
их добавить, но её не создали после этого ни одного коммита (буква A)
и т.п.
В проводнике это отображается просто в иконках файлов.
hg log
------
Следующее, что мы можем делать с историей -- это смотреть на неё. Для
этого есть команда hg log с многочисленными настройками того, как на
историю можно глядеть.
В графической среде для этого есть утилита workbench, и mercurial вам
предложит её использовать.
hg diff
-------
Ещё один способ смотреть на историю -- сравнивать, чем одна версия
отличается от другой. Это делается командой hg diff -r версия1 -r
версия2. По умолчанию он сравнивает рабочую директорию с последней
сохранённой версией, а если указан только один параметр -r, то с той
версией, которую мы указали.
Номера версий для этого мы можем получить из hg log.
В workbench для того, чтобы посмотреть разницу между двумя версиями,
нужно просто их обе выделить.
hg revert
---------
Если мы хотим из прежнего состояния истории взять один файл, мы это
можем сделать с помощью команды `hg revert -r версия файл`. Если мы в
рабочей директории его перед этим успели поредактировать, и то, что
меркуриал там видит, не совпадает с последней сохранённой версией, то
тогда он сохранит поредактированную нами версию в `файл.orig` прежде,
чем переписывать её старой версией.
Главный принцип mercurial -- никогда не удалять плоды человеческого
труда, кроме разве что случаев, когда человек очень об этом попросил,
да и то не всегда он это разрешит сделать.
Ветки
=====
hg update
---------
Третий способ смотреть на историю -- вернуться просто целиком к одной
из прошлых версий. То есть заменить все файлы в рабочей директории (за
которые отвечает mercurial) на те, которые были когда-то раньше. Это
делает команда `hg update -r версия`.
При этом меркуриал запоминает, что вот именно эта версия является
родительской к нынешнему состоянию рабочей директории.
Предположим, мы редактировали наш проект, и сделали там четыре коммита::
|3*
|2
|1
|0
(* обозначает коммит, соответствующий рабочей директории).
После этого мы сказали `hg update -r 1`::
|3
|2
|1*
|0
Теперь если мы поредактируем какие-нибудь файлы и сделаем новый
коммит, новый коммит образует новую ветку в репозитории::
| 4*
||3
||2
`|1
|0
hg merge
--------
Зачем это бывает нужно? Например, мы пишем программу для соревнования
парсеров. Мы дописали её до какого-то состояния (коммиты 0, 1 в
примере), потом решили поэкспериментировать с функцией оценки
парсеров, влияет ли она на результаты, и какой её лучше сделать (на
это у нас ушли коммиты 2 и 3), при этом по ходу дела мы получили
нерабочую версию, что-то в этой функции оценки сложно, и не получается
её заставить работать, а тут вдруг пишут, что вот, дедлайн, и нужно
срочно выкатить рабочую версию. Мы откатываемся к последней рабочей
версии (1), и тут обнаруживаем, что именно на том файле, который нам
дали для соревнований, наша программа не работает. Мы срочно это чиним
(коммит 4) и отправляем, а потом доделываем нашу функцию оценки: `hg
update -r 3 ... hg commit`::
|5*
| 4
||3
||2
`|1
|0
И тут мы понимаем, что мы вообще-то хотим иметь программу, в которой
одновременно и новая функция оценки, и починены глюки чтения файлов.
Т.е. нам нужно взять изменения, которые привели нас от версии 1 до
версии 5 и взять изменения, которые привели нас от версии 1 до версии
4, и применить и те, и другие к версии 1. Вот примерно это делает
команда `hg merge`.
Как правило (если мы редактировали в разных ветках разные места
файла), она без проблем сможет объединить изменения. Но в случае,
например, если мы в обеих ветках поредактировали одну и ту же строку,
притом разным образом, возникнет "конфликт", и mercurial предложит нам
самим решить, а как из этих двух версий собрать одну рабочую, которая
их объединяет. Для этого он покажет редактор, в котором будут показаны
версии: одной объединяемой головы, другой объединяемой головы, их
общего предка, и то, что мы получаем на выходе (по умолчанию оно
совпадает с одной из голов).
После того, как мы разобрались со всеми конфликтами, картинка станет такой::
|5*
| 4*
||3
||2
`|1
|0
Т.е. мы добились версии, которая нас устраивает как объединение двух
версий, но мы не создали новый коммит. Чтобы эта версия вошла в
историю, нам её нужно собственноручно записать: `hg commit`::
/|6*
|5
| 4
||3
||2
`|1
|0
После этого мы получили новую версию в истории, у которой два предка.
В workbench это дерево будет таким образом и нарисовано, что у нас
была история, в которой версии в некоторый момент разошлись, а потом
сошлись вместе.
Обмен версиями
==============
Так вот, а основная тема рассказа -- это как с помощью mercurial
работать совместно над одной и той же программой. Всё, что мы
вспомнили выше, нам в этом безусловно пригодится.
Предположим, что у нас есть два персонажа -- Алиса и Боб, которые
пишут программу вместе. Для начала предположим, что у них есть
какой-то мистический способ обмениваться файлами через файловую
систему (ну, например, они работают за одним комеьютером, или они
сидят в комп-классе, где сисадмин настроил мистический NFS, SAMBA или
что-нибудь ещё в этом роде)
hg clone
--------
Алиса начала писать программу (hg init ... hg add ... hg commmit)
потом ещё поредактировала (... hg commit ...), потом ещё (... hg
commit). И вот Боб решил присоединиться к её проекту. В mercurial это
делается так: Боб говорит hg clone путь-к-проекту. (Вот как выглядит
путь в жизненных ситуациях, и с чем его едят -- об этом позже. В
простых случаях, если дело происходит в пределах одного компьютера, то
это именно путь к директории, где лежит репозиторий Алисы; либо если
Алиса выложила репозиторий на web, то тогда это URL вида http://... --
как обычно в браузере. Но обе эти ситуации нам вряд ли будут сильно
полезны)
После этого у Боба получилась копия репозитория Алисы.
У Алисы::
|2*
|1
|0
У Боба::
|2*
|1
|0
hg pull
-------
После этого Алиса продолжила редактировать (... `hg commit` ...), и Боб
подредактировал программу (... `hg commit` ...). Получилось:
У Алисы::
|3*
|2
|1
|0
У Боба::
|4*
|2
|1
|0
В этот момент Боб решил вытащить к себе изменения, которые сделала
Алиса. Для этого есть команда hg pull, которая вытаскивает недостающие
кусочки истории из чуждого репозитория, но при этом ничего не меняет в
рабочей директории.
У Боба получилось::
|4*
| 3
||2
`|1
|0
А такая ситуация нам уже знакома, Боб может сделать hg merge и hg
commit, чтобы объединить две ветки, либо может просто продолжить
редактировать свою ветку, смотреть в чём разница между двумя (hg
diff), переключаться между ними (hg update) и т.п.
hg push
-------
После этого Боб хочет отправить свои изменения Алисе. Для этого в
mercurial есть команда hg push, которая делает ровно обратное к hg
pull -- отправляет ту часть нашей истории в удалённый репозиторий,
которой там ещё нет. И кроме этого ничего не меняет. При этом на hg
push есть такое ограничение: когда мы отправляем какие-то изменения,
мы не имеем права создавать новые головы. Это нужно для того, чтобы
Алиса вдруг неожиданно для себя не увидела кучу голов, из которых
непонятно, какая более поздняя, и с чем их вообще едят. Поэтому всякий
раз прежде, чем отправлять изменения, нужно смерджить все головы,
которые мы успели наплодить.
bitbucket
---------
Для команд hg clone, hg push, hg pull нужно, чтобы два компьютера,
обменивающиеся изменениями, видели друг друга непосредственно по сети
в каком-нибудь смысле. В нынешней архитектуре Интернета это скорее
невозможно. Поэтому в игру вступает какой-нибудь ещё один центральный
сервер, на котором лежит репозиторий, общий для всех участников
проекта, который виден всем. Для mercurial наиболее популярным из
таких серверов является bitbucket.org. Он довольно дружественный
пользователю, так что никаких дополнительных инструкций про него я не
привожу.
SSH-ключи
=========
Основной способ общения mercurial с bitbucket -- посредством протокола
ssh, о котором имеет смысл сказать два слова. Это протокол обмена
шифрованными данными. Он допускает несколько способов авторизации,
среди которых есть авторизация с помощью пары криптографических
ключей.
В криптографии есть большой раздел на тему того, как организовать
надёжный шифрованый канал обмена данными, когда у каждого участника
обмена есть пара ключей: один секретный, который он хранит как зеницу
ока, а второй публичный, который каждый желающий может знать, и про
который единственное нужно быть достаточно уверенным, что он
принадлежит именно тому человеку, которому вы думаете, что он
принадлежит. Соответствующий раздел криптографии позволяет человеку,
имеющему чужой публичный ключ, проверить, что кто-то обладает парным к
нему приватным ключом, не получая при этом сам приватный ключ и не
нашурая его секретности.
Для работы с SSH в windows имеется пакет PuTTY. Его нужно установить
польностью (windows installer).
Нам потребуется сгенерировать пару из приватного и публичного ключа --
это делает программа puttygen. После этого, чтобы все программы имели
доступ к ключу, его нужно загрузить -- дважды щёлкнуть по нему мышкой
(при этом запустится программа pageant, которая будет выглядеть как
компьютер в шляпе в правом нижнем углу экрана в области нотификаций).
После того, как мы сгенерировали пару ключей и выложили публичный ключ
в bitbucket, мы можем использовать его как репозиторий для нашего
проекта: `hg clone ssh://hg@bitbucket.org/user/project`, `hg pull`, `hg
push ...`
Ссылки по теме:
* `Как использовать SSH в bitbucket
<https://confluence.atlassian.com/display/BITBUCKET/Set+up+SSH+for+Mercurial>`_
* https://bitbucket.org/
* `Хороший туториал по mercurial <http://hginit.com/>`_
* http://mercurial.selenic.com/ -> `tortoise hg
<http://bitbucket.org/tortoisehg/files/downloads/tortoisehg-2.6.1-hg-2.4.1-x86.msi>`_
XML
############
Есть примерно три подхода для работы с XML:
1. Работать с ним как с текстовым документом, например, с помощью
регулярных выражений выуживать из него полезные кусочки данных -- это
вы умеете.
2. Читать его последовательно парсером, который будет оповещать нас о
событиях вида "тэг открылся", "тэг закрылся", на которые мы можем
как-то реагировать. Такой тип парсеров называется SAX -- Simple API
for XML. Они очень полезны в случае крайне больших XML-документов, но
в силу подхода несколько ограничены.
3. Прочитать его парсером целиком, получить представление документа в
виде дерева, и работать с этим деревом. Такой подход называется DOM --
document object model. Он как правило наиболее гибкий, но требует
загрузки всего документа в память.
В питоне есть несколько встроенных библиотек для работы с XML, и дикое
множество различных сторонних библиотек, которые чем-то лучше, а
чем-то хуже встроенных.
Мы будем работать с библиотекой ElementTree, который реализует подходы 2 и 3.
В первую очередь, мы остановимся на третьем подходе. Как мы, наверное,
знаем, дерево -- это граф, не имеющий циклов, и, как граф оно состоит
из вершин и рёбер. Внутренняя в дереве XML-документа -- это один тэг
(всё от начала открывающего тэга до конца закрывающего), а рёбра
отображают отношение вложенности тэгов и текста.
Как прочитать файл
==================
Библиотека лежит в модуле xml.etree.ElementTree, и дабы не писать
всегда полное имя модуля, как правило используют сокращения::
import xml.etree.ElementTree as ET
root = ET.parse(file).getroot()
Функция parse принимает имя файла или объект открытого файла, и
возвращает объект класса ET.ElementTree. Сам по себе этот объект нам
не интересен, мы хотим из него получить корневую вершину дерева. Это
делает метод getroot.
Если мы уже прочитали XML и имеем его в виде текстовой строки, то мы
можем сразу её разобрать и получить корневой элемент::
root = ET.fromstring("<a><b/><c/></a>")
Что хранится в вершине
======================
Вершина дерева (XML node) представляет тэг, а значит, хранится в ней:
* название тэга: root.tag
* словарь атрибутов тэга: root.attrib
* текстовое содержимое тэга: root.text
* хвост текстового содержимого тэга: root.tail
Сам тэг при этом, если с ним работать как со списком, ведёт себя как
список вложенных в него тэгов.
Пояснение про текстовое содержимое: как правило, когда вы будете
искать что-то в xml-документе, если вы хотите получить текст, то вам
нужно найти тэг, в котором только нужный вам кусок текста лежит
внутри, и взять от него атрибут text (наверняка заодно придётся к нему
сразу вызвать strip(), т.к. как правило в нём окажутся переносы строк
и пробелы, которые автор XML-файла добавил туда для красоты). Однако
библиотека etree является универсальной и она разрабатывалась в том
числе и для того, чтобы генерировать XML-документы, и чтобы их
перерабатывать. Чтобы ничего содержательного не потерять, они
организовали распихали текстовую часть XML-документа следующим
образом: в text попадает текст от знака > нашего открывающего тэга до
следующего знака <. А в tail попадает текст от знака > нашего
закрывающего тэга до следующего знака < в документе. Таким образом
технически любой кусочек текста знает, в какой тэг и куда именно он
попадёт. Из этого не следует, что его будет так уж удобно оттуда
вытаскивать :)
Например::
>>> root = ET.fromstring("<a><b>Eeny<c>meeny</c>miny</b>moe</a>")
>>> root.tag
'a'
>>> b = root[0]
>>> b.tag
'b'
>>> b.text
'Eeny'
>>> b.tail
'moe'
Язык XPath
==========
Вся прелесть подхода DOM состоит в том, что мы можем делать запросы к
дереву с тем, чтобы находить в нём интересные части. Для этих запросов
существует язык XPath, который более-менее стандартизован, и стандарта
которого все реализации более-менее придерживаются. (Это значит, что
выучив XPath для ElementTree, вы сможете легко выучить XPath в
javascript, например, но это не значит, что там не будет
неожиданностей в духе того, что некоторых конструкций там нет, или там
есть некоторые новые конструкции).
Язык XPath устроен следующим образом:
1. Есть некоторый узел, начиная от которого мы что-то ищем в документе.
2. Из этого узла мы получаем список его прямых детей-узлов, и
говорим, что это список узлов, с которыми мы работаем.
3. Мы указываем фильтр на список узлов, с которыми мы работаем.
4. Мы можем указать разделитель "/", который скажет, что дальше мы
берём от каждого узла всех прямых узлов-детей, либо разделитель "//",
который говорит, что мы берём от каждого узла, с которым мы работаем,
список всех-всех-всех его потомков. И после этого переходим к шагу 3.
Фильтры бывают такие:
* Точка -- то, что было перед слэшом (родитель, детей которого мы
пытались пофильтровать)
* Две точки -- его родитель
* Звёздочка -- все узлы
* Имя тэга -- только узлы с данным тэгом
* [@attr] -- узлы, имеющие данный аттрибут
* [@attr=value] -- узлы, имеющие данный аттрибут и его значение равно value.
* [tag] -- узлы, имеющие потомка с тэгом tag
Например, в документе выше, если мы начинаем от корневого элемента, то:
b
находит тэг b
c
не находит ничего (c не является прямым потомком a)
b/c
находит тэг c (сначала нашли b, взяли его детей, из них
оставили только c)
*
находит тэг b
.//*
находит тэги b и c
Ещё один пример, мы хотим в русском корпусе найти все предложения,
содержащие слово с леммой аффикция, мы можем искать:
.//ana[@lex="аффикция]/../..
Работа с XPath в ElementTree
============================
У узла дерева в ElementTree есть методы:
* find(xpath) -- выдаёт первую находку по XPath или None
* findall(xpath) -- выдаёт список всех находок по XPath
Пример про русский корпус::
import xml.etree.ElementTree as ET
root = ET.parse("fiction.xml").getroot()
for sentence in root.findall('.//ana[@lex="аффикция"]/../.'):
for word in sentence.findall('w/ana'):
print word.text
.. vim: set et ts=4 sts=4 sw=4: