О событиях. Использование Google как CGI сканера. Не забываем про очевидное

Если вы.NET программист, то вы наверняка объявляли и использовали события в своем коде. Несмотря на это, не все знают, как события работают внутри и какие особенности связаны с их применением. В этой статье я попытался описать работу событий как можно более подробно, включая некоторые частные случаи, с которыми редко приходится иметь дело, но про которые важно и\или интересно знать.

Что такое событие?

Событием в языке C# называется сущность, предоставляющая две возможности: для класса - сообщать об изменениях, а для его пользователей - реагировать на них.
Пример объявления события:

Public event EventHandler Changed;
Рассмотрим, из чего состоит объявление. Сначала идут модификаторы события, затем ключевое слово event, после него - тип события, который обязательно должен быть типом-делегатом, и идентификатор события, то есть его имя. Ключевое слово event сообщает компилятору о том, что это не публичное поле, а специальным образом раскрывающаяся конструкция, скрывающая от программиста детали реализации механизма событий. Для того, чтобы понять, как работает этот механизм, необходимо изучить принципы работы делегатов.

Основа работы событий - делегаты

Можно сказать, что делегат в.NET - некий аналог ссылки на функцию в C++. Вместе с тем, такое определение неточно, т.к. каждый делегат может ссылаться не на один, а на произвольное количество методов, которые хранятся в списке вызовов делегата (invocation list). Тип делегата описывает сигнатуру метода, на который он может ссылаться, экземпляры этого типа имеют свои методы, свойства и операторы. При вызове метода Invoke() выполняется последовательный вызов каждого из методов списка. Делегат можно вызывать как функцию, компилятор транслирует такой вызов в вызов Invoke().
В C# для делегатов имеются операторы + и -, которые не существуют в среде.NET и являются синтаксическим сахаром языка, раскрываясь в вызов методов Delegate.Combine и Delegate.Remove соответственно. Эти методы позволяют добавлять и удалять методы в списке вызовов. Разумеется, форма операторов с присваиванием (+= и -=) также применима к операторам делегата, как и к определенным в среде.NET операторам + и - для других типов. Если при вычитании из делегата его список вызовов оказывается пуст, то ему присваивается null.
Рассмотрим простой пример:

Action a = () => Console.Write("A"); //Action объявлен как public delegate void Action(); Action b = a; Action c = a + b; Action d = a - b; a(); //выведет A b(); //выведет A c(); //выведет AA d(); //произойдет исключение NullReferenceException, т.к. d == null

События - реализация по умолчанию

События в языке C# могут быть определены двумя способами:
  1. Неявная реализация события (field-like event).
  2. Явная реализация события.
Уточню, что слова “явная” и “неявная” в данном случае не являются терминами, определенными в спецификации, а просто описывают способ реализации по смыслу.

Рассмотрим наиболее часто используемую реализацию событий - неявную. Пусть имеется следующий исходный код на языке C# 4 (это важно, для более ранних версий генерируется несколько иной код, о чем будет рассказано далее):

Class Class { public event EventHandler Changed; }
Эти строчки будут транслированы компилятором в код, аналогичный следующему:

Class Class { EventHandler сhanged; public event EventHandler Changed { add { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange(ref this.changed, comparand + value, comparand); } while(eventHandler != comparand); } remove { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange(ref this.changed, comparand - value, comparand); } while (eventHandler != comparand); } } }
Блок add вызывается при подписке на событие, блок remove - при отписке. Эти блоки компилируются в отдельные методы с уникальными именами. Оба этих метода принимают один параметр - делегат типа, соответствующего типу события и не имеют возвращаемого значения. Имя параметра всегда ”value”, попытка объявить локальную переменную с таким именем приведет к ошибке компиляции. Область видимости, указанная слева от ключевого слова event определяет область видимости этих методов. Также создается делегат с именем события, который всегда приватный. Именно поэтому мы не можем вызвать событие, реализованное неявным способом, из наследника класса.

Interlocked.CompareExchange выполняет сравнение первого аргумента с третьим и если они равны, заменяет первый аргумент на второй. Это действие потокобезопасно. Цикл используется для случая, когда после присвоения переменной comparand делегата события и до выполнения сравнения другой поток изменяет этот делегат. В таком случае Interlocked.CompareExchange не производит замены, граничное условие цикла не выполняется и происходит следующая попытка.

Объявление с указанием add и remove

При явной реализации события программист объявляет делегат-поле для события и вручную добавляет или удаляет подписчиков через блоки add/remove, оба из которых должны присутствовать. Такое объявление часто используется для создания своего механизма событий с сохранением удобств языка C# в работе с ними.
Например, одна из типичных реализаций заключается в отдельном хранении словаря делегатов событий, в котором присутствуют только те делегаты, на события которых была осуществлена подписка. Доступ к словарю осуществляется по ключам, которыми обычно являются статические поля типа object, используемые только для сравнения их ссылок. Это делается для того, чтобы уменьшить количество памяти, занимаемое экземпляром класса (в случае, если он содержит большое количество нестатических событий). Эта реализация применяется в WinForms.

Как происходит подписка на событие и его вызов?

Все действия по подписке и отписке (обозначаются как += и -=, можно легко спутать с операторами делегатов) компилируются в вызовы методов add и remove. Вызовы внутри класса, отличные от вышеуказанных, компилируются в простую работу с делегатом. Следует заметить, что при неявной (и при правильной явной) реализации события невозможно получить доступ к делегату извне класса, работать можно лишь с событием как с абстракцией - подписываясь и отписываясь. Так как нет способа определить, подписались ли мы на какое-либо событие (если не использовать рефлексию), то кажется логичным, что отписка от него никогда не вызовет ошибок - можно смело отписываться, даже если делегат события пуст.

Модификаторы событий

Для событий могут использоваться модификаторы области видимости (public, protected, private, internal), они могут быть перекрыты (virtual, override, sealed) или не реализованы (abstract, extern). Событие может перекрывать событие с таким же именем из базового класса (new) или быть членом класса (static). Если событие объявлено и с модификатором override и с модификатором abstract одновременно, то наследники класса должны будут переопределить его (равно как и методы или свойства с этими двумя модификаторами).

Какие типы событий бывают?

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

Как все обстоит в C# 3?

Реализация field-like события, которая описана выше, соответствует языку C# 4 (.NET 4.0). Для более ранних версий существуют весьма существенные отличия.
Неявная реализация использует lock(this) для обеспечения потокобезопасности вместо Interlocked.CompareExchange с циклом. Для статических событий используется lock(typeof(Class)). Вот код, аналогичный раскрытому компилятором неявному определению события в C# 3:

Class Class { EventHandler changed; public event EventHandler Changed { add { lock(this) { changed = changed + value; } } remove { lock(this) { changed = changed - value; } } } }
Помимо этого, работа с событием внутри класса ведется как с делегатом, т.е. += и -= вызывают Delegate.Combine и Delegate.Remove напрямую, в обход методов add/remove. Это изменение может привести к невозможности сборки проекта на языке C# 4! В C# 3 результатом += и -= был делегат, т.к. результатом присвоения переменной всегда является присвоенное значение. В C# 4 результатом является void, т.к. методы add/remove не возвращают значения.

Помимо изменений в работе на разных версиях языка есть еще несколько особенностей.

Особенность №1 - продление времени жизни подписчика

При подписке на событие мы добавляем в список вызовов делегата события ссылку на метод, который будет вызван при вызове события. Таким образом, память, занимаемая объектом, подписавшимся на событие, не будет освобождена до его отписки от события или до уничтожения объекта, заключающего в себе событие. Эта особенность является одной из часто встречаемых причин утечек памяти в приложениях.
Для исправления этого недостатка часто используются weak events, слабые события. Эта тема уже .

Особенность №2 - явная реализация интерфейса

Событие, являющееся частью интерфейса, не может быть реализовано как поле при явной реализации этого интерфейса. В таких случаях следует либо скопировать стандартную реализацию события для реализации как свойство, либо реализовывать эту часть интерфейса неявно. Также, если вам не нужна потокобезопасность этого события, можно использовать самое простое и эффективное определение:

EventHandler changed; event EventHandler ISomeInterface.Changed { add { changed += value; } remove { changed -= value; } }

Особенность №3 - безопасный вызов

События перед вызовом следует проверять на null, что следует из описанной выше работы делегатов. От этого разрастается код, для избежания чего существует как минимум два способа. Первый способ описан Джоном Скитом (Jon Skeet) в его книге C# in depth :

Public event EventHandler Changed = delegate { };
Коротко и лаконично. Мы инициализируем делегат события пустым методом, поэтому он никогда не будет null. Вычесть из делегата этот метод невозможно, т.к. он определен при инициализации делегата и у него нет ни имени, ни ссылки на него из любого места программы.

Второй способ заключается в написании метода, содержащего в себе необходимую проверку на null. Этот прием особенно хорошо работает в.NET 3.5 и выше, где доступны методы расширений (extension methods). Так как при вызове метода расширений объект, на котором он вызывается, является всего лишь параметром этого метода, то этот объект может быть пустой ссылкой, что и используется в данном случае.

Public static class EventHandlerExtensions { public static void SafeRaise(this EventHandler handler, object sender, EventArgs e) { if(handler != null) handler(sender, e); } public static void SafeRaise(this EventHandler handler, object sender, TEventArgs e) where TEventArgs: EventArgs { if(handler != null) handler(sender, e); } }
Таким образом, мы можем вызывать события как Changed.SafeRaise(this, EventArgs.Empty), что экономит нам строчки кода. Также можно определить третий вариант метода расширений для случая, когда у нас EventArgs.Empty, чтобы не передавать их явно. Тогда код сократится до Changed.SafeRaise(this), но я не буду рекомендовать такой подход, т.к. для других членов вашей команды это может быть не так явно, как передача пустого аргумента.

Тонкость №4 - что не так со стандартной реализацией?

Если у вас стоит ReSharper, то вы могли наблюдать следующее его сообщение . Команда решарпера правильно считает, что не все ваши пользователи достаточно осведомлены в работе событий\делегатов в плане отписки\вычитания, но тем не менее ваши события должны работать предсказуемо не для ваших пользователей, а с точки зрения событий в.NET, а т.к. там такая особенность есть, то и в вашем коде она должна остаться.

Бонус: попытка Microsoft сделать контравариантные события

В первой бете C# 4 Microsoft попытались добавить контравариантности событиям. Это позволяло подписываться на событие EventHandler методами, имеющими сигнатуру EventHandler и все работало до тех пор, пока в делегат события не добавлялось несколько методов с разной (но подходящей) сигнатурой. Такой код компилировался, но падал с ошибкой времени выполнения. По всей видимости, обойти это так и не смогли и в релизе C# 4 контравариантность для EventHandler была отключена.
Это не так заметно, если опускать явное создание делегата при подписке, например следующий код отлично скомпилируется:

Public class Tests { public event EventHandler Changed; public void Test() { Changed += ChangedMyEventArgs; Changed += ChangedEventArgs; } void ChangedMyEventArgs(object sender, MyEventArgs e) { } void ChangedEventArgs(object sender, EventArgs e) { } }
Это происходит потому, что компилятор сам подставит new EventHandler(...) к обеим подпискам. Если же хотя бы в одном из мест использовать new EventHandler(...), то компилятор сообщит об ошибке - невозможно сконвертировать тип EventHandler в EventHandler (здесь Events - пространство имен моего тестового проекта).

Источники

Далее приведен список источников, часть материала из которых была использована при составлении статьи. Рекомендую к прочтению книгу Джона Скита (Jon Skeet), в которой в деталях описаны не только делегаты, но и многие другие средства языка.

Используйте события , чтобы узнавать, как пользователи взаимодействуют с вашим контентом.

Событие – это взаимодействие пользователя с контентом, которое можно отслеживать независимо от просмотров страниц или экранов. Скачивания файлов, клики по мобильным объявлениям, использование гаджетов, элементов Flash и AJAX, воспроизведение видео – все эти действия можно отслеживать в качестве событий.

Содержание

Просмотр данных о событиях

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

Чтобы просмотреть отчеты о событиях, выполните следующие действия:

  1. Выберите нужное представление .
  2. Откройте Отчеты .
  3. Выберите Поведение > События .

Компоненты событий

Событие состоит из следующих компонентов. Если событие произошло, для каждого из компонентов регистрируется значение, которое появляется в отчетах:

  • Категория
  • Действие
  • Ярлык (необязательно, но рекомендуется)
  • Значение (необязательно)

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

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

Предположим, что вам также нужно отслеживать, сколько раз скачали ваше видео (см. пример выше). Можно использовать следующий набор компонентов:

В этом случае в отчетах будет только одна категория – Videos (Видеозаписи) – и вы увидите сводные показатели взаимодействия пользователей со всеми элементами этого видеообъекта.

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

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

  • Видео – фильмы
  • Видео – музыка
  • Скачивание видео

Тогда в отчете будет показано общее количество событий во всех трех категориях. Показатель Всего событий – это количество всех событий во всех категориях, указанных в реализации отслеживания. Однако вы не сможете увидеть объединенные показатели по всем видеозаписям отдельно от скачивания видео, так как данные отдельных событий объединены в соответствующих категориях.

Объектная модель отслеживания событий обеспечивает максимальную гибкость, но все же мы рекомендуем сначала продумать структуру отчетов, а затем выбирать названия категорий. Если вы будете повторно использовать название категории, не используйте другие варианты названия. Например, если для отслеживания видео вы добавите категорию Video, а затем в названии по ошибке используете множественное число (Videos), то у вас будет две разных категории для отслеживания видео. Кроме того, если вы измените название категории объекта, который уже отслеживается, данные по исходной категории не будут изменены. Поэтому в отчетах показатели одного элемента страницы будут представлены в двух разных категориях.

Действие

Обычно параметр "Действие" используется для обозначения отслеживаемого события или взаимодействия, связанного с веб-объектом. Например, с помощью этого параметра можно отслеживать в одной категории Videos (Видеозаписи) несколько событий:

  • Время завершения загрузки видео
  • Нажатия кнопки "Воспроизведение"
  • Нажатия кнопки "Стоп"
  • Нажатия кнопки "Пауза"
  • Все действия перечисляются независимо от своих родительских категорий. Это удобно для сегментирования данных событий в отчетах.
  • Уникальное событие определяется уникальным именем действия. В разных категориях можно использовать одинаковые названия действий, но это может повлиять на расчет количества уникальных событий. Подробнее об этом можно узнать в разделах "Рекомендации" и "Неявный подсчет" ниже.

Ярлыки

Ярлыки позволяют предоставить дополнительную информацию для отслеживаемых событий, например название фильма в приведенных выше примерах видеозаписей или имя файла при отслеживании скачивания.

Эти компоненты событий, подобно категориям и действиям, можно просматривать в отдельном отчете, где собраны все созданные вами ярлыки. Ярлык можно рассматривать как дополнительный параметр взаимодействия пользователя с объектами страницы. Предположим, вам нужно отслеживать взаимодействие с пятью проигрывателями видео на странице сайта. Каждый из этих проигрывателей может использовать категорию Videos (Видеозаписи) с действием Play (Воспроизведение), но для каждого можно также создать отдельный ярлык (например, с названием фильма), и они будут представлены в отчете как отдельные элементы.

Чтобы отчеты были максимально эффективны, следуйте приведенным ниже рекомендациям.

  • Названия действий должны соответствовать данным отчета. При отслеживании событий показатели для действий с одним названием из двух разных категорий объединяются. Например, если действие под названием Click (Клик) используется в категории Downloads (Скачивание) и в категории Videos (Видеозаписи), то показатели для него в отчете "Основные действия" отображаются по всем взаимодействиям с этим названием. Подробные данные для действия Click (Клик) по категориям можно просмотреть на следующем уровне отчета. Однако неосмотрительное использование действия Click (Клик) в отслеживании различных событий снижает эффективность работы с отчетами. Если вы отслеживаете большое количество событий на сайте, выбирайте для действий имена, которые отражают категории отслеживаемых данных. Например, название Click (Клик) можно использовать для взаимодействия с гаджетами, а для действий с проигрывателем – названия Play (Воспроизведение), Pause (Пауза) и Stop (Стоп).
  • Используйте глобальные названия действий, чтобы получать сводные и раздельные данные по взаимодействиям пользователей. Например, можно использовать название Play (Воспроизведение) в категории Videos (Видеозаписи) для всех видеозаписей на сайте. Тогда отчет "Основные действия" будет содержать сводные данные о действии "Воспроизведение", и вы сможете сравнить это событие с другими событиями в видеозаписях, например "Пауза" или "Стоп".

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

  • "Действие" не всегда означает действие. В качестве значения этого компонента можно использовать любую строку. В некоторых случаях название события или действия не очень информативно, поэтому для отслеживания других элементов можно использовать параметр действия. Например, если требуется отслеживать скачивание файлов на странице, в качестве параметра действия для события скачивания можно указать тип файла документа. В этом случае отчеты по категории Downloads (Скачивание) будут разбиты по типам файлов (PDF, DOC, XLS).
  • Количество уникальных событий увеличивается только за счет уникальных действий. Каждое взаимодействие пользователя с объектом, помеченным определенным названием действия, регистрируется как одно уникальное событие, связанное с этим названием. Любое дополнительное взаимодействие с тем же триггером действия в рамках одного сеанса не влияет на подсчет уникальных событий, связанных с этим действием. Даже если пользователь выполнит действие с этим названием на другом объекте, количество уникальных событий не изменится.

    Поэтому при работе с отчетами нужно учитывать два важных фактора. Во-первых, предположим, что пользователь выполняет действие Play (Воспроизведение) в двух разных проигрывателях, отнесенных к разным категориям. В отчетах "Основные действия" для Play (Воспроизведение) будет указано одно уникальное событие, хотя пользователь взаимодействовал с двумя уникальными проигрывателями. Во-вторых, в отчете по действиям из каждой категории будет указано одно уникальное действие, потому что фактически на каждую пару "категория-действие" приходится одно уникальное событие. Подробнее об этом можно узнать в разделе "Неявный подсчет" ниже.

Значения

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

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

В этом случае среднее время загрузки в отчете подсчитывается так:
  • Сеансы с событиями: 5.
  • Значение: 53.
  • Среднее значение: 10,6.

Отрицательные значения не поддерживаются.

События без взаимодействия

Термин "без взаимодействия" относится к необязательному логическому параметру, который может быть передан методу, отправляющему событие. С помощью этого параметра вы можете задать, как должен определяться показатель отказов для страниц на сайте, на которых используется отслеживание событий. Предположим, у вас на главной странице есть встроенное видео и вы хотите узнать показатель отказов для этой страницы. Что следует считать отказом? Важно ли для вас взаимодействие с роликом на главной странице? Если да, то такое взаимодействие должно учитываться при подсчете показателя отказов. Сеансы, включающие только главную страницу с нажатием на видео, не должны считаться отказами. Если же вы хотите узнать показатель отказов для главной страницы независимо от взаимодействия с видео, тогда при подсчете отказов вам нужно исключить взаимодействия с видео.

В этом случае вам понадобится необязательный параметр, позволяющий не учитывать взаимодействия. Напомним, что отказом считается сеанс, в течение которого было выполнено только одно взаимодействие. По умолчанию событие считается взаимодействием и тоже учитывается при подсчете показателя отказов. Однако если этому параметру присвоено значение true, то событие такого типа не считается взаимодействием. Этот факт можно использовать для корректировки показателя отказов на страницах с событиями. Если параметру присвоено значение true, это значит, что сеанс, содержащий единственную страницу, помеченную событиями без взаимодействий, считается отказом, даже если посетитель запустил событие. И наоборот: когда этот параметр не используется, сеанс не считается отказом, если пользователь посещает только одну страницу, на которой отслеживаются события, и запускает событие.

Неявный подсчет

При отслеживании событий каждое взаимодействие с отслеживаемым объектом страницы учитывается и связывается с сеансом пользователя. В отчетах показатель "Всего событий" рассчитывается как общее количество взаимодействий с таким объектом.

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

В таблице ниже показано, как в интерфейсе отчетов объединяются данные по определенной категории событий. В этом примере одно название категории используется в двух разных проигрывателях, каждому из которых присвоен свой ярлык. Здесь используются одинаковые действия Play (Воспроизведение) и Stop (Стоп), запрограммированные во Flash-интерфейсе проигрывателей.

Отслеживание событий для категории Videos (Видеозаписи)

Действие Ярлык: Gone With the Wind (Унесенные ветром) Ярлык: Mr Smith Goes to Washington (Мистер Смит едет в Вашингтон) Всего
Play (Воспроизведение)

22 клика в 10 сеансах с событием

7 кликов в 5 сеансах с событием

29 событий всего

15 уникальных событий Play (Воспроизведение)

Pause (Пауза)

3 клика в 2 сеансах с событием

16 кликов в 8 сеансах с событием

19 событий всего

10 уникальных событий Pause (Пауза)

Stop (Стоп)

2 клика в 2 сеансах с событием

4 клика в 3 сеансах с событием

6 событий всего

5 уникальных событий Stop (Стоп)

Всего

27 событий всего

14 уникальных событий для Gone With the Wind

27 событий всего

16 уникальных событий для Mr Smith Goes to Washington

54 события всего

30 уникальных событий для категории Videos

О чем важно помнить

Прежде чем настраивать отслеживание событий, учтите факторы, перечисленные ниже.

Влияние на показатель отказов

Обычно отказом считается сеанс, во время которого пользователь посетил только одну страницу сайта. В Google Аналитике отказом считается сеанс, в ходе которого был выполнен только один запрос GIF (например, если пользователь открыл одну страницу сайта и покинул его, не отправив других запросов к серверу Google Аналитики в рамках этого сеанса). Однако для страниц, на которых используется отслеживание событий, показатель отказов может изменяться. Это объясняется тем, что отслеживание событий, как и отслеживание страниц, классифицируется как запрос взаимодействия.

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

Поэтому для страниц с отслеживанием событий отказом считается посещение одной страницы сайта без взаимодействия с отслеживаемыми событиями.

Важно! Если взаимодействием считается загрузка страницы, то показатель отказов для этой страницы будет равен нулю.

Ограничение по количеству событий за сеанс

ga.js

Первые 10 событий, отправленных в Google Аналитику, обрабатываются сразу. Затем выполняется обработка не более одного события в секунду. За сеанс может быть обработано не более 500 событий.

analytics.js и gtag.js

Первые 20 событий, отправленных в Google Аналитику, обрабатываются сразу. Затем выполняется обработка не более двух событий в секунду. За сеанс может быть обработано не более 500 событий. Это ограничение распространяется на все обращения, за исключением типов item и transaction (данные по товарам и транзакциям).

Чтобы число обращений оставалось в пределах установленных ограничений, не рекомендуется делать следующее:

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

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

  • Заранее определите, для каких элементов вам может понадобиться отслеживать данные, даже если сейчас вас интересует только один объект на сайте. Это поможет вам разработать структуру отчетности, в рамках которой вы позднее сможете отслеживать сколько угодно событий любого типа.
  • Планируя отчеты по отслеживанию событий, обсудите все детали с теми, для кого эти отчеты предназначены. Четко понимая, что должно быть представлено в отчетах, вы сможете правильно реализовать отслеживание событий. Например, если в отчетах нужно отразить только взаимодействие с интерфейсом просмотра видео, структура категорий будет значительно отличаться от ситуации, когда требуется отслеживать другие элементы интерфейса на базе Flash, например меню, встроенные гаджеты и время загрузки. Кроме того, советуем обсудить с пользователями отчетов разные возможности отслеживания событий и выбрать наиболее подходящий вариант. Например, им может быть интересно не только взаимодействие пользователей с Flash-интерфейсом видеороликов, но и время их загрузки. Поэтому нужно заранее продумать, какие названия будут использоваться в вызовах событий.
  • Используйте точные и понятные названия: все названия категорий, действий и ярлыков, которые вы используете для отслеживания событий, отображаются в интерфейсе отчетов. Кроме того, в отчетах нельзя использовать одинаковые пары "категория-действие", поэтому заранее обдумайте, как будут рассчитываться показатели для всех объектов одной категории.

Чем отличаются события от целей-событий

Событие – это взаимодействие пользователя с вашим сайтом или приложением, которое задается и регистрируется путем изменения кода отслеживания, как описано в этой статье.

Цель-событие – это заданная вами , определяющая событие как конверсию.

Была ли эта статья полезна?

Как можно улучшить эту статью?


2.
3.
4.
5. Ожидания
6.
7. WebDriver API
8. Приложение: Часто Задаваемые Вопросы

5. Ожидания

В наши дни большинство веб-приложений используют AJAX технологии. Когда страница загружена в браузере, элементы на этой странице могут подгружаться с различными временными интервалами. Это затрудняет поиск элементов, если элемент не присутствует в DOM , возникает исключение ElementNotVisibleException. Используя ожидания, мы можем решить эту проблему. Ожидание дает некий временной интервал между произведенными действиями - поиске элемента или любой другой операции с элементом.

Selenium WebDriver предоставляет два типа ожиданий - неявное (implicit) и явное (explicit). Явное ожидание заставляет WebDriver ожидать возникновение определенного условия до произведения действий. Неявное ожидание заставляет WebDriver опрашивать DOM определенное количество времени, когда пытается найти элемент.

5.1 Явные ожидания

Явное ожидание - это код, которым вы определяете какое необходимое условие должно произойти для того, чтобы дальнейший код исполнился. Худший пример такого кода - это использование команды time.sleep(), которая устанавливает точное время ожидания. Существуют более удобные методы, которые помогут написать вам код, ожидающий ровно столько, сколько необходимо. WebDriverWait в комбинации с ExpectedCondition является одним из таких способов.

From selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Firefox() driver.get("http://somedomain/url_that_delays_loading") try: element = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement"))) finally: driver.quit()
Этот код будет ждать 10 секунд до того, как отдаст исключение TimeoutException или если найдет элемент за эти 10 секунд, то вернет его. WebDriverWait по умолчанию вызывает ExpectedCondition каждые 500 миллисекунд до тех пор, пока не получит успешный return. Успешный return для ExpectedCondition имеет тип Boolean и возвращает значение true, либо возвращает not null для всех других ExpectedCondition типов.

Ожидаемые условия
Существуют некие условия, которые часто встречаются при автоматизации веб-сайтов. Ниже перечислены реализации каждого. Связки в Selenium Python предоставляют некоторые удобные методы, так что вам не придется писать класс expected_condition самостоятельно или же создавать собственный пакет утилит.

  • title_is
  • title_contains
  • presence_of_element_located
  • visibility_of_element_located
  • visibility_of
  • presence_of_all_elements_located
  • text_to_be_present_in_element
  • text_to_be_present_in_element_value
  • frame_to_be_available_and_switch_to_it
  • invisibility_of_element_located
  • element_to_be_clickable - it is Displayed and Enabled.
  • staleness_of
  • element_to_be_selected
  • element_located_to_be_selected
  • element_selection_state_to_be
  • element_located_selection_state_to_be
  • alert_is_present
from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.ID,"someid")))
Модуль expected_conditions уже содержит набор предопределенных условий для работы с WebDriverWait.

5.2 Неявные ожидания

Неявное ожидание указывает WebDriver"у опрашивать DOM определенное количество времени, когда пытается найти элемент или элементы, которые недоступны в тот момент. Значение по умолчанию равно 0. После установки, неявное ожидание устанавливается для жизни экземпляра WebDriver объекта.

From selenium import webdriver driver = webdriver.Firefox() driver.implicitly_wait(10) # seconds driver.get("http://somedomain/url_that_delays_loading") myDynamicElement = driver.find_element_by_id("myDynamicElement")
Перейти к следующей главе.

В Hibernate работа с БД осуществляется через объект типа org.hibernate.Session .
Выдержка из документации:
The main runtime interface between a Java application and Hibernate. This is the central API class abstracting the notion of a persistence service.
The lifecycle of a Session is bounded by the beginning and end of a logical transaction. (Long transactions might span several database transactions.)
The main function of the Session is to offer create, read and delete operations for instances of mapped entity classes.

Интерфейс org.hibernate.Session является мостом между приложением и Hibernate. С помощью сессий выполняются все CRUD-операции с объектами-сущностями . Объект типа Session получают из экземпляра типа org.hibernate.SessionFactory , который должен присутствовать в приложении в виде singleton .
3). Состояния объектов
Объект-сущность может находиться в одном из 3-х состояний (статусов):
  • transient object . Объекты в данном статусе - это заполненные экземпляры классов-сущностей. Могут быть сохранены в БД. Не присоединены к сессии. Поле Id не должно быть заполнено, иначе объект имеет статус detached ;
  • persistent object . Объект в данном статусе - так называемая хранимая сущность, которая присоединена к конкретной сессии. Только в этом статусе объект взаимодействует с базой данных. При работе с объектом данного типа в рамках транзакции все изменения объекта записываются в базу;
  • detached object . Объект в данном статусе - это объект, отсоединённый от сессии, может существовать или не существовать в БД.
Любой объект-сущность можно переводить из одного статуса в другой. Для этого в интерфейсе Session существуют следующие методы:
  • persist(Object) - преобразует объект из transient в persistent , то есть присоединяет к сессии и сохраняет в БД. Однако, если мы присвоим значение полю Id объекта, то получим PersistentObjectException - Hibernate посчитает, что объект detached , т. е. существует в БД. При сохранении метод persist() сразу выполняет insert , не делая select .
  • merge(Object) - преобразует объект из transient или detached в persistent . Если из transient , то работает аналогично persist() (генерирует для объекта новый Id , даже если он задан), если из detached - загружает объект из БД, присоединяет к сессии, а при сохранении выполняет запрос update
  • replicate(Object, ReplicationMode) - преобразует объект из detached в persistent , при этом у объекта обязательно должен быть заранее установлен Id . Данный метод предназначен для сохранения в БД объекта с заданным Id , чего не позволяют сделать persist() и merge() . Если объект с данным Id уже существует в БД, то поведение определяется согласно правилу из перечисления org.hibernate.ReplicationMode :
    ReplicationMode.IGNORE - ничего не меняется в базе.
    ReplicationMode.OVERWRITE - объект сохраняется в базу вместо существующего.
    ReplicationMode.LATEST_VERSION - в базе сохраняется объект с последней версией.
    ReplicationMode.EXCEPTION - генерирует исключение.
  • delete(Object) - удаляет объект из БД, иными словами, преобразует persistent в transient . Object может быть в любом статусе, главное, чтобы был установлен Id .
  • save(Object) - сохраняет объект в БД, генерируя новый Id , даже если он установлен. Object может быть в статусе transient или detached
  • update(Object) - обновляет объект в БД, преобразуя его в persistent (Object в статусе detached )
  • saveOrUpdate(Object) - вызывает save() или update()
  • refresh(Object) - обновляет detached -объект, выполнив select к БД, и преобразует его в persistent
  • get(Object.class, id) - получает из БД объект класса-сущности с определённым Id в статусе persistent
Объект Session кэширует у себя загруженные объекты; при загрузке объекта из БД в первую очередь проверяется кэш. Для того, чтобы удалить объект из кэша и отсоединить от сессии, используется session.evict(Object) . Метод session.clear() применит evict() ко всем объектам в сессии.

А теперь обратим внимание на аннотации @OneToMany и @ManyToOne в классах-сущностях. Параметр fetch в @OneToMany обозначает, когда загружать дочерние объекты. Может иметь одно из двух значений, указанных в перечислении javax.persistence.FetchType :

FetchType.EAGER - загружать коллекцию дочерних объектов сразу же, при загрузке родительских объектов.
FetchType.LAZY - загружать коллекцию дочерних объектов при первом обращении к ней (вызове get ) - так называемая отложенная загрузка.

Параметр cascade обозначает, какие из методов интерфейса Session будут распространяться каскадно к ассоциированным сущностям. Например, в классе-сущности User для коллекции tasks укажем:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE}) public List
Тогда при выполнении session.persist(user) или session.merge(user) операции persist или merge будут применены ко всем объектам из tasks . Аналогично для остальных операций из перечисления javax.persistence.CascadeType . CascadeType.ALL применяет все операции из перечисления. Необходимо правильно настроить CascadeType , дабы не подгружать из базы кучу лишних ассоциированных объектов-сущностей.

4). Извлечение объектов из БД
Приведём простой пример:

@Autowired private SessionFactory sessionFactory public void getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); List tasksList = user.getTasks(); ... }
Вместо метода session.get() можно использовать session.load() . Метод session.load() возвращает так называемый proxy-object . Proxy-object - это объект-посредник, через который мы можем взаимодействовать с реальным объектом в БД. Он расширяет функционал объекта-сущности. Взаимодействие с proxy-object полностью аналогично взаимодействию с объектом-сущностью. Proxy-object отличается от объекта-сущности тем, что при создании proxy-object не выполняется ни одного запроса к БД, т. е. Hibernate просто верит нам, что объект с данным Id существует в БД. Однако первый вызванный get или set у proxy-object сразу инициирует запрос select , и если объекта с данным Id нет в базе, то мы получим ObjectNotFoundException . Основное предназначение proxy-object - реализация отложенной загрузки.

Вызов user.getTasks() инициирует загрузку задач юзера из БД, так как в классе User для tasks установлен FetchType.LAZY .

LazyInitializationException
С параметром FetchType.LAZY нужно быть аккуратнее. Иногда при загрузке ассоциированных сущностей мы можем поймать исключение LazyInitializationException . В вышеуказанном коде во время вызова user.getTasks() user должен быть либо в статусе persistent , либо proxy .

Также LazyInitializationException может вызвать небольшое изменение в нашем коде:

Public List getTasks(Long userId) { ... Session session = sessionFactory.openSession(); User user = (User) session.load(User.class, userId); List tasksList = user.getTasks(); session.close(); return tasksList; }
Здесь теоретически всё верно. Но при попытке обращения к tasksList мы МОЖЕМ получить LazyInitializationException . Но в дебагере данный код отрабатывает верно. Почему? Потому, что user.getTasks() только возвращает ссылку на коллекцию, но не ждёт её загрузки. Не подождав, пока загрузятся данные, мы закрыли сессию. Выход - выполнять в транзакции, т. е.:

Public List getTasks(Long userId) { ... User user = (User) session.load(User.class, userId); Session session = sessionFactory.openSession(); session.beginTransaction(); List tasksList = user.getTasks(); session.getTransaction().commit(); return tasksList; }

Выборка с условиями
А теперь приведём несколько простых примеров выборки данных с условиями. Для этого в Hibernate используются объекты типа org.hibernate.Criteria :

Public List getUser(String login) { ... Session session = sessionFactory.openSession(); Criteria userCriteria = session.createCriteria(User.class); userCriteria.add(Restrictions.eq("login", login)); user = (User) userCriteria.uniqueResult(); session.close(); ... }
Здесь понятно, что мы выполняем select * from user where login="login" . В метод add мы передаём объект типа Criterion , представляющий определённый критерий выборки. Класс org.hibernate.criterion.Restrictions предоставляет множество различных видов критериев. Параметр «login» обозначает название свойства класса-сущности, а не поля в таблице БД.
Приведём ещё пару примеров:

А).
public List getTasksByName(String name) { ... session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List tasks = criteria.add(Restrictions.like("name", name, MatchMode.ANYWHERE)).list(); ... }
Здесь мы выбираем по содержимому свойства name класса-сущности Task . MatchMode.ANYWHERE означает, что нужно искать подстроку name в любом месте свойства «name» .

Б).
А здесь мы получаем 50 строк, начиная с 20-го номера в таблице.

Public List getTasks() { ... Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Task.class); List tasks = criteria.setFirstResult(20).setMaxResults(50).list(); ... }

5). Сохранение объектов
Давайте разберём несколько способов сохранения объекта-сущности в базу данных.

А). Создаём transient-object и сохраняем в базу:

@Autowired private UserDao userDao; @Autowired private SessionFactory sessionFactory; public void saveUser(String login) { User user = userDao.getUserByLogin(login); Session session = sessionFactory.openSession(); session.openTransaction(); Task task = new Task(); task.setName("Задача 1"); task.setDefinition("Задача 1"); task.setTaskDate(new Date()); task.setUser(user); session.saveOrUpdate(task); session.flush(); session.getTransaction().commit(); return task.getTaskId(); }
Отметим несколько нюансов. Во-первых, сохранение в БД можно производить только в рамках транзакции. Вызов session.openTransaction() открывает для данной сессии новую транзакцию, а session.getTransaction().commit() её выполняет. Во-вторых, в метод task.setUser(user) мы передаём user в статусе detached . Можно передать и в статусе persistent .

Данный код выполнит (не считая получения user ) 2 запроса - и insert into task...
Вместо saveOrUpdate() можно выполнить save() , persist() , merge() - будет также 2 запроса. Вызов session.flush() применяет все изменения к БД, но, если честно, этот вызов здесь бесполезен, так как ничего не сохраняется в БД до commit() , который сам вызовет flush() .

Помним, что если мы внутри транзакции что-то изменим в загруженном из БД объекте статуса persistent или proxy-object , то выполнится запрос update . Если task должен ссылаться на нового user , то делаем так:

User user = new User(); // Создаём transient-object user.setLogin("user"); user.setPassword("user"); ... task.setUser(user); session.saveOrUpdate(task); // Сохраняем
Внимание: в классе Task для поля user должен быть установлен CascadeType.PERSIST , CascadeType.MERGE или CascadeType.ALL .

Если мы имеем на руках userId существующего в БД юзера, то нам не обязательно загружать объект User из БД, делая лишний select . Так как мы не можем присвоить ID юзера непосредственно свойству класса Task , нам нужно создать объект класса User с единственно заполненными userId . Естественно, это не может быть transient-object , поэтому здесь следует воспользоваться известным нам proxy-объектом .

Public void saveTask(Long userId, Task task) ... task.setUser((User) session.load(User.class, userId)); // Никакого запроса к БД не происходит session.saveOrUpdate(task); ...
б). Добавляем объект в коллекцию дочерних объектов:

Public Long saveUser(String login) { Session session = sessionFactory.openSession(); session.openTransaction(); user = (User) session.load(User.class, userId); Task task = new Task(); task.setName("Имя"); task.setUser(user); user.getTasks().add(task); session.getTransaction().commit(); return user.getUserId(); }
В User для свойства tasks должен стоять CascadeType.ALL . Если стоит CascadeType.MERGE , то после user.getTasks().add(task) выполнить session.merge(user) . Данный код выполнит 3 запроса - select * from user , select nextval("task_task_id_seq") и insert into task

6). Удаление объектов
а). Можно удалить, создав transient-object :

Public void deleteTask(Long taskId) { Session session = sessionFactory.openSession(); session.openTransaction(); Tasks task = new Tasks(); task.setTaskId(taskId); session.delete(task); session.getTransaction().commit(); }
Данный код удалит только task . Однако, если task - объект типа proxy , persistent или detached и в классе Task для поля user действует CascadeType.REMOVE , то из базы удалится также ассоциированный user . Если удалять юзера не нужно, выполнить что? Правильно, task.setUser(null)

Б). Можно удалить и таким способом:

Public void deleteTask(Long userId, Long taskId) { User user = (User) session.load(User.class, userId); user.getTasks().removeIf((Task task) -> { if (task.getTaskId() == taskId) { task.setUser(null); return true; } else return false; }); }
Данный код просто удаляет связь между task и user . Здесь мы применили новомодное лямбда-выражение . Объект task удалится из БД при одном условии - если изменить кое-что в классе-сущности User :

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) public List getTasks() { return tasks; } public void setTasks(List tasks) { this.tasks = tasks; }
Параметр orphanRemoval = true указывает, что все объекты Task , которые не имеют ссылки на User , должны быть удалены из БД.

7). Декларативное управление транзакциями
Для декларативного управления транзакциями мы будем использовать Spring Framework . Управление транзакциями осуществляется через менеджер транзакций. Вместо вызовов session.openTransaction() и session.commit() используется аннотация @Transactional . В конфигурации приложения должно присутствовать следующее:


Здесь мы определили бин transactionManager , к которому привязан бин sessionFactory . Класс HibernateTransactionManager является реализацией общего интерфейса org.springframework.transaction.PlatformTransactionManager для SessionFactory библиотеки Hibernate. annotation-driven указывает менеджеру транзакций обрабатывать аннотацию @Transactional .

Болтовня ничего не стоит. Покажите мне код. (Linus Torvalds)

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ObjectNotFoundException.class, ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); Tasks task = new Tasks(); task.setName("Задача 1"); task.setDefinition("Задача 1"); task.setTaskDate(new Date()); task.setUser((User) session.load(User.class, userId)); session.saveOrUpdate(task); return task.getTaskId(); }
Аннотация @Transactional указывает, что метод должен выполняться в транзакции. Менеджер транзакций открывает новую транзакцию и создаёт для неё экземпляр Session , который доступен через sessionFactory.getCurrentSession() . Все методы, которые вызываются в методе с данной аннотацией, также имеют доступ к этой транзакции, потому что экземпляр Session является переменной потока (ThreadLocal). Вызов sessionFactory.openSession() откроет совсем другую сессию, которая не связана с транзакцией.

Параметр rollbackFor указывает исключения, при выбросе которых должен быть произведён откат транзакции. Есть обратный параметр - noRollbackFor , указывающий, что все исключения, кроме перечисленных, приводят к откату транзакции.

Параметр propagation самый интересный. Он указывает принцип распространения транзакции. Может принимать любое значение из перечисления org.springframework.transaction.annotation.Propagation . Приведём пример:

@Autowired private SessionFactory sessionFactory; @Autowired private UserDao userDao; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {ConstraintViolationException.class}) public Long saveTask(Long userId) { Session session = sessionFactory.getCurrentSession(); User user = userDao.getUserByLogin("user1"); Tasks task = new Tasks(); task.setName("Задача 1"); ... task.setUser(user); session.saveOrUpdate(task); return task.getTaskId(); }
Метод UserDao.getUserByLogin() также может быть помечен аннотацией @Transactional . И здесь параметр propagation определит поведение метода UserDao.getUserByLogin() относительно транзакции метода saveTask() :

  • Propagation.REQUIRED - выполняться в существующей транзакции, если она есть, иначе создавать новую.
  • Propagation.MANDATORY - выполняться в существующей транзакции, если она есть, иначе генерировать исключение.
  • Propagation.SUPPORTS - выполняться в существующей транзакции, если она есть, иначе выполняться вне транзакции.
  • Propagation.NOT_SUPPORTED - всегда выполняться вне транзакции. Если есть существующая, то она будет остановлена.
  • Propagation.REQUIRES_NEW - всегда выполняться в новой независимой транзакции. Если есть существующая, то она будет остановлена до окончания выполнения новой транзакции.
  • Propagation.NESTED - если есть текущая транзакция, выполняться в новой, так называемой, вложенной транзакции. Если вложенная транзакция будет отменена, то это не повлияет на внешнюю транзакцию; если будет отменена внешняя транзакция, то будет отменена и вложенная. Если текущей транзакции нет, то просто создаётся новая.
  • Propagation.NEVER - всегда выполнять вне транзакции, при наличии существующей генерировать исключение.
Хороший материал о транзакциях . Следует помнить, что использование транзакций несёт дополнительные издержки в производительности.
Ну что ж, подведём итоги
В моей статье я осветил самые основные принципы работы с сессиями и транзакциями в Hibernate. Надеюсь, что начинающим Java-программистам статья будет полезна при преодолении первого порога в изучении суперклассной (не для всех, возможно) библиотеки Hibernate. Желаю всем успехов в нашей сложной и интересной программерской деятельности!

Получение частных данных не всегда означает взлом - иногда они опубликованы в общем доступе. Знание настроек Google и немного смекалки позволят найти массу интересного - от номеров кредиток до документов ФБР.

WARNING

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

К интернету сегодня подключают всё подряд, мало заботясь об ограничении доступа. Поэтому многие приватные данные становятся добычей поисковиков. Роботы-«пауки» уже не ограничиваются веб-страницами, а индексируют весь доступный в Сети контент и постоянно добавляют в свои базы не предназначенную для разглашения информацию. Узнать эти секреты просто - нужно лишь знать, как именно спросить о них.

Ищем файлы

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

Ограничить поиск по файлам определенного вида в Google можно с помощью двух операторов: filetype и ext . Первый задает формат, который поисковик определил по заголовку файла, второй - расширение файла, независимо от его внутреннего содержимого. При поиске в обоих случаях нужно указывать лишь расширение. Изначально оператор ext было удобно использовать в тех случаях, когда специфические признаки формата у файла отсутствовали (например, для поиска конфигурационных файлов ini и cfg, внутри которых может быть все что угодно). Сейчас алгоритмы Google изменились, и видимой разницы между операторами нет - результаты в большинстве случаев выходят одинаковые.


Фильтруем выдачу

По умолчанию слова и вообще любые введенные символы Google ищет по всем файлам на проиндексированных страницах. Ограничить область поиска можно по домену верхнего уровня, конкретному сайту или по месту расположения искомой последовательности в самих файлах. Для первых двух вариантов используется оператор site, после которого вводится имя домена или выбранного сайта. В третьем случае целый набор операторов позволяет искать информацию в служебных полях и метаданных. Например, allinurl отыщет заданное в теле самих ссылок, allinanchor - в тексте, снабженном тегом , allintitle - в заголовках страниц, allintext - в теле страниц.

Для каждого оператора есть облегченная версия с более коротким названием (без приставки all). Разница в том, что allinurl отыщет ссылки со всеми словами, а inurl - только с первым из них. Второе и последующие слова из запроса могут встречаться на веб-страницах где угодно. Оператор inurl тоже имеет отличия от другого схожего по смыслу - site . Первый также позволяет находить любую последовательность символов в ссылке на искомый документ (например, /cgi-bin/), что широко используется для поиска компонентов с известными уязвимостями.

Попробуем на практике. Берем фильтр allintext и делаем так, чтобы запрос выдал список номеров и проверочных кодов кредиток, срок действия которых истечет только через два года (или когда их владельцам надоест кормить всех подряд).

Allintext: card number expiration date /2017 cvv

Когда читаешь в новостях, что юный хакер «взломал серверы» Пентагона или NASA, украв секретные сведения, то в большинстве случаев речь идет именно о такой элементарной технике использования Google. Предположим, нас интересует список сотрудников NASA и их контактные данные. Наверняка такой перечень есть в электронном виде. Для удобства или по недосмотру он может лежать и на самом сайте организации. Логично, что в этом случае на него не будет ссылок, поскольку предназначен он для внутреннего использования. Какие слова могут быть в таком файле? Как минимум - поле «адрес». Проверить все эти предположения проще простого.


Inurl:nasa.gov filetype:xlsx "address"


Пользуемся бюрократией

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

Например, обязательный в канцелярии министерства обороны США штамп Distribution statement означает стандартизированные ограничения на распространение документа. Литерой A отмечаются публичные релизы, в которых нет ничего секретного; B - предназначенные только для внутреннего использования, C - строго конфиденциальные и так далее до F. Отдельно стоит литера X, которой отмечены особо ценные сведения, представляющие государственную тайну высшего уровня. Пускай такие документы ищут те, кому это положено делать по долгу службы, а мы ограничимся файлами с литерой С. Согласно директиве DoDI 5230.24, такая маркировка присваивается документам, содержащим описание критически важных технологий, попадающих под экспортный контроль. Обнаружить столь тщательно охраняемые сведения можно на сайтах в домене верхнего уровня.mil, выделенного для армии США.

"DISTRIBUTION STATEMENT C" inurl:navy.mil

Очень удобно, что в домене.mil собраны только сайты из ведомства МО США и его контрактных организаций. Поисковая выдача с ограничением по домену получается исключительно чистой, а заголовки - говорящими сами за себя. Искать подобным образом российские секреты практически бесполезно: в доменах.ru и.рф царит хаос, да и названия многих систем вооружения звучат как ботанические (ПП «Кипарис», САУ «Акация») или вовсе сказочные (ТОС «Буратино»).


Внимательно изучив любой документ с сайта в домене.mil, можно увидеть и другие маркеры для уточнения поиска. Например, отсылку к экспортным ограничениям «Sec 2751», по которой также удобно искать интересную техническую информацию. Время от времени ее изымают с официальных сайтов, где она однажды засветилась, поэтому, если в поисковой выдаче не удается перейти по интересной ссылке, воспользуйся кешем Гугла (оператор cache) или сайтом Internet Archive.

Забираемся в облака

Помимо случайно рассекреченных документов правительственных ведомств, в кеше Гугла временами всплывают ссылки на личные файлы из Dropbox и других сервисов хранения данных, которые создают «приватные» ссылки на публично опубликованные данные. С альтернативными и самодельными сервисами еще хуже. Например, следующий запрос находит данные всех клиентов Verizon, у которых на роутере установлен и активно используется FTP-сервер.

Allinurl:ftp:// verizon.net

Таких умников сейчас нашлось больше сорока тысяч, а весной 2015-го их было на порядок больше. Вместо Verizon.net можно подставить имя любого известного провайдера, и чем он будет известнее, тем крупнее может быть улов. Через встроенный FTP-сервер видно файлы на подключенном к маршрутизатору внешнем накопителе. Обычно это NAS для удаленной работы, персональное облако или какая-нибудь пиринговая качалка файлов. Все содержимое таких носителей оказывается проиндексировано Google и другими поисковиками, поэтому получить доступ к хранящимся на внешних дисках файлам можно по прямой ссылке.

Подсматриваем конфиги

До повальной миграции в облака в качестве удаленных хранилищ рулили простые FTP-серверы, в которых тоже хватало уязвимостей. Многие из них актуальны до сих пор. Например, у популярной программы WS_FTP Professional данные о конфигурации, пользовательских аккаунтах и паролях хранятся в файле ws_ftp.ini . Его просто найти и прочитать, поскольку все записи сохраняются в текстовом формате, а пароли шифруются алгоритмом Triple DES после минимальной обфускации. В большинстве версий достаточно просто отбросить первый байт.

Расшифровать такие пароли легко с помощью утилиты WS_FTP Password Decryptor или бесплатного веб-сервиса .

Говоря о взломе произвольного сайта, обычно подразумевают получение пароля из логов и бэкапов конфигурационных файлов CMS или приложений для электронной коммерции. Если знаешь их типовую структуру, то легко сможешь указать ключевые слова. Строки, подобные встречающимся в ws_ftp.ini , крайне распространены. Например, в Drupal и PrestaShop обязательно есть идентификатор пользователя (UID) и соответствующий ему пароль (pwd), а хранится вся информация в файлах с расширением.inc. Искать их можно следующим образом:

"pwd=" "UID=" ext:inc

Раскрываем пароли от СУБД

В конфигурационных файлах SQL-серверов имена и адреса электронной почты пользователей хранятся в открытом виде, а вместо паролей записаны их хеши MD5. Расшифровать их, строго говоря, невозможно, однако можно найти соответствие среди известных пар хеш - пароль.

До сих пор встречаются СУБД, в которых не используется даже хеширование паролей. Конфигурационные файлы любой из них можно просто посмотреть в браузере.

Intext:DB_PASSWORD filetype:env

С появлением на серверах Windows место конфигурационных файлов отчасти занял реестр. Искать по его веткам можно точно таким же образом, используя reg в качестве типа файла. Например, вот так:

Filetype:reg HKEY_CURRENT_USER "Password"=

Не забываем про очевидное

Иногда добраться до закрытой информации удается с помощью случайно открытых и попавших в поле зрения Google данных. Идеальный вариант - найти список паролей в каком-нибудь распространенном формате. Хранить сведения аккаунтов в текстовом файле, документе Word или электронной таблице Excel могут только отчаянные люди, но как раз их всегда хватает.

Filetype:xls inurl:password

С одной стороны, есть масса средств для предотвращения подобных инцидентов. Необходимо указывать адекватные права доступа в htaccess, патчить CMS, не использовать левые скрипты и закрывать прочие дыры. Существует также файл со списком исключений robots.txt, запрещающий поисковикам индексировать указанные в нем файлы и каталоги. С другой стороны, если структура robots.txt на каком-то сервере отличается от стандартной, то сразу становится видно, что на нем пытаются скрыть.

Список каталогов и файлов на любом сайте предваряется стандартной надписью index of. Поскольку для служебных целей она должна встречаться в заголовке, то имеет смысл ограничить ее поиск оператором intitle . Интересные вещи находятся в каталогах /admin/, /personal/, /etc/ и даже /secret/.

Следим за обновлениями

Актуальность тут крайне важна: старые уязвимости закрывают очень медленно, но Google и его поисковая выдача меняются постоянно. Есть разница даже между фильтром «за последнюю секунду» (&tbs=qdr:s в конце урла запроса) и «в реальном времени» (&tbs=qdr:1).

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

По виду адресной строки можно догадаться только о способе ограничить вывод результатов с помощью конструкции &tbs=qdr: . Буква y после нее задает лимит в один год (&tbs=qdr:y), m показывает результаты за последний месяц, w - за неделю, d - за прошедший день, h - за последний час, n - за минуту, а s - за секунду. Самые свежие результаты, только что ставшие известными Google, находится при помощи фильтра &tbs=qdr:1 .

Если требуется написать хитрый скрипт, то будет полезно знать, что диапазон дат задается в Google в юлианском формате через оператор daterange . Например, вот так можно найти список документов PDF со словом confidential, загруженных c 1 января по 1 июля 2015 года.

Confidential filetype:pdf daterange:2457024-2457205

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

Таргетируемся и снова фильтруем

Помимо указания дополнительных операторов в поисковом запросе их можно отправлять прямо в теле ссылки. Например, уточнению filetype:pdf соответствует конструкция as_filetype=pdf . Таким образом удобно задавать любые уточнения. Допустим, выдача результатов только из Республики Гондурас задается добавлением в поисковый URL конструкции cr=countryHN , а только из города Бобруйск - gcs=Bobruisk . В разделе для разработчиков можно найти полный список .

Средства автоматизации Google призваны облегчить жизнь, но часто добавляют проблем. Например, по IP пользователя через WHOIS определяется его город. На основании этой информации в Google не только балансируется нагрузка между серверами, но и меняются результаты поисковой выдачи. В зависимости от региона при одном и том же запросе на первую страницу попадут разные результаты, а часть из них может вовсе оказаться скрытой. Почувствовать себя космополитом и искать информацию из любой страны поможет ее двухбуквенный код после директивы gl=country . Например, код Нидерландов - NL, а Ватикану и Северной Корее в Google свой код не положен.

Часто поисковая выдача оказывается замусоренной даже после использования нескольких продвинутых фильтров. В таком случае легко уточнить запрос, добавив к нему несколько слов-исключений (перед каждым из них ставится знак минус). Например, со словом Personal часто употребляются banking , names и tutorial . Поэтому более чистые поисковые результаты покажет не хрестоматийный пример запроса, а уточненный:

Intitle:"Index of /Personal/" -names -tutorial -banking

Пример напоследок

Искушенный хакер отличается тем, что обеспечивает себя всем необходимым самостоятельно. Например, VPN - штука удобная, но либо дорогая, либо временная и с ограничениями. Оформлять подписку для себя одного слишком накладно. Хорошо, что есть групповые подписки, а с помощью Google легко стать частью какой-нибудь группы. Для этого достаточно найти файл конфигурации Cisco VPN, у которого довольно нестандартное расширение PCF и узнаваемый путь: Program Files\Cisco Systems\VPN Client\Profiles . Один запрос, и ты вливаешься, к примеру, в дружный коллектив Боннского университета.

Filetype:pcf vpn OR Group

INFO

Google находит конфигурационные файлы с паролями, но многие из них записаны в зашифрованном виде или заменены хешами. Если видишь строки фиксированной длины, то сразу ищи сервис расшифровки.

Пароли хранятся в зашифрованном виде, но Морис Массар уже написал программу для их расшифровки и предоставляет ее бесплатно через thecampusgeeks.com .

При помощи Google выполняются сотни разных типов атак и тестов на проникновение. Есть множество вариантов, затрагивающих популярные программы, основные форматы баз данных, многочисленные уязвимости PHP, облаков и так далее. Если точно представлять то, что ищешь, это сильно упростит получение нужной информации (особенно той, которую не планировали делать всеобщим достоянием). Не Shodan единый питает интересными идеями, но всякая база проиндексированных сетевых ресурсов!