Русский
Русский
English
Статистика
Реклама

Туториал

Разблокируем интернет с помощью Mikrotik и VPN подробный туториал

10.07.2020 12:22:13 | Автор: admin

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

В качестве VPN я выбрал SoftEther: он настолько же прост в настройке как и RRAS и такой же быстрый. На стороне VPN сервера включил Secure NAT, других настроек не проводилось.

В качестве альтернативы рассматривал RRAS, но Mikrotik не умеет с ним работать. Соединение устанавливается, VPN работает, но поддерживать соединение без постоянных реконнектов и ошибок в логе Mikrotik не умеет.

Настройка производилась на примере RB3011UiAS-RM на прошивке версии 6.46.11.
Теперь по порядку что и зачем.

1. Устанавливаем VPN соединение


В качестве VPN решения был выбран, конечно, SoftEther, L2TP с предварительным ключом. Такого уровня безопасности достаточно кому угодно, потому что ключ знает только роутер и его владелец.

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



То же самое командой:

/interface l2tp-client
name="LD8" connect-to=45.134.254.112 user="Administrator" password="PASSWORD" profile=default-encryption use-ipsec=yes ipsec-secret="vpn"


SoftEther заработает без изменения ipsec proposals и ipsec profiles, их настройку не рассматриваем, но скриншоты своих профилей, на всякий случай, автор оставил.


Для RRAS в IPsec Proposals достаточно изменить PFS Group на none.

Теперь нужно встать за NAT этого VPN сервера. Для этого нам нужно перейти в IP > Firewall > NAT.

Тут включаем masquerade для конкретного, или всех PPP интерфейсов. Роутер автора подключен сразу к трём VPNам, поэтому сделал так:



То же самое командой:

/ip firewall nat
chain=srcnat action=masquerade out-interface=all-ppp


2. Добавляем правила в Mangle


Первым делом хочется, конечно, защитить все самое ценное и беззащитное, а именно DNS и HTTP трафик. Начнем с HTTP.

Переходим в IP Firewall Mangle и создаем новое правило.

В правиле, Chain выбираем Prerouting.

Если перед роутером стоит Smart SFP ли еще один роутер, и вы хотите к нему подключаться по веб интерфейсу, в поле Dst. Address нужно ввести его IP адрес или подсеть и поставить знак отрицания, чтобы не применять Mange в адресу или к этой подсети. У автора стоит SFP GPON ONU в режиме бриджа, таким образом автор сохранил возможность подключения к его вебморде.

По умолчанию Mangle будет применять свое правило ко всем NAT Stateам, это сделает проброс порта по вашему белому IP невозможным, поэтому в Connection NAT State ставим галочку на dstnat и знак отрицания. Это позволит нам отправлять по сети исходящий трафик через VPN, но все еще прокидывать порты через свой белый IP.


Далее на вкладке Action выбираем mark routing, обзываем New Routing Mark так, чтобы было в дальнейшем нам понятно и едем дальше.


То же самое командой:

/ip firewall mangle
add chain=prerouting action=mark-routing new-routing-mark=HTTP passthrough=no connection-nat-state=!dstnat protocol=tcp dst-address=!192.168.1.1 dst-port=80


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

Если вы пользуетесь встроенным в роутер DNS, что делает и автор, его нужно тоже защитить. Поэтому для первого правила, как и выше мы выбираем chain prerouting, для второго же нужно выбрать output.

Output это цепь которые использует сам роутер для запросов с помощью своего функционала. Тут все по аналогии с HTTP, протокол UDP, порт 53.



То же самое командами:

/ip firewall mangle
add chain=prerouting action=mark-routing new-routing-mark=DNS passthrough=no protocol=udp
add chain=output action=mark-routing new-routing-mark=DNS-Router passthrough=no protocol=udp dst-port=53


3. Строим маршрут через VPN


Переходим в IP Routes и создаем новые маршруты.

Маршрут для маршрутизации HTTP по VPN. Указываем название наших VPN интерфейсов и выбираем Routing Mark.



На этом этапе вы уже почувствовали, как ваш оператор перестал встраивать рекламу в ваш HTTP трафик.

То же самое командой:

/ip route
add dst-address=0.0.0.0/0 gateway=LD8 routing-mark=HTTP distance=2 comment=HTTP


Ровно так же будут выглядеть правила для защиты DNS, просто выбираем нужную метку:


Тут вы ощутили, как ваши DNS запросы перестали прослушивать. То же самое командами:

/ip route
add dst-address=0.0.0.0/0 gateway=LD8 routing-mark=DNS distance=1 comment=DNS
add dst-address=0.0.0.0/0 gateway=LD8 routing-mark=DNS-Router distance=1 comment=DNS-Router


Ну под конец, разблокируем Rutracker. Вся подсеть принадлежит ему, поэтому указана подсеть.


Вот настолько было просто вернуть себе интернет. Команда:

/ip route
add dst-address=195.82.146.0/24 gateway=LD8 distance=1 commant=Rutracker.Org


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

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

Подробнее..

Как написать простого бота для ВК и Телеграм

23.02.2021 18:07:11 | Автор: admin


Мы уже упоминали, как мы предоставляем бесплатные для студентов, чтобы они учились программировать. Один из наших подопечных Павел сделал простеньких телеграм и ВК ботов для FAQ. Они очень просте, тем не менее, начинающему программисту не помешают комментарии опытных ребят поэтому публикуем его рассказ Павел будет рад, если в комментариях ему дадут советы.

Я студент Новосибирского Государственного Технического Университета, не так давно мы с парочкой моих друзей реализовали во всех возможных областях научной деятельности. Мы помогаем сводить заинтересованных преподавателей и студентов всех ВУЗов Сибири, чтобы проектная научная деятельность развивалась по территории Сибири и РФ.

Студенты и преподаватели часто обращались ко мне с вопросами и я решил автоматизировать этот процесс, написав ботов для ВК и Телеграм.

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

Я использовать Python версии 3.6 просто потому, что он самый простой для меня. Кодил в PyCharm Community Edition. Весь код опубликован на. Удачи!

1. Предварительные приготовления для телеграм-бота


1.1 Получение токена от Bot Father в телеграмме


Первым делом, нам нужно зарегистрировать нашего бота в Telegram.

Для этого, в поисковике телеги ищем BotFather

далее, делаем всё также, как показано на скриншотах:



После нажимаем на команду /newbot или же прописываем вручную.



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



1.2 Переходим в любой редактор кода и создаем файл


Перед созданием данного файла, нам нужно выбрать директорию, в котором будет реализован весь функционал бота. Если вы используете PyCharm Community/Professional Edition, то предлагаю просто создать новый проект, и писать там весь функционал бота.

Если Вы используете любой другой редактор, такой как Sublime Text 3, например, то Вам самостоятельно придётся создать директорию, создать виртуальное окружение, и работать из консоли со всеми предварительными тестами. Во избежание трудностей, предлагаю скачать продукт PyCharm Community Edition от компании JetBrains, с помощью данного продукта можно обойти действия, описанные в предыдущем абзаце, так как данный продукт сделает их самостоятельно, от Вас потребуется только указать путь до интерпритатора Python в конфигурациях PyCharm, с помощью которого и будет работать Ваш бот.

В данном файле () будет храниться только токен, который нам дал BotFather, поэтому пишем:

token = "Здесь хранится Ваш токен".

1.3 Cоздаём главный файл


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

import configimport telebot # pip install telebotfrom telebot import types # pip install pyTelegramBotAPI

Далее, нам необходимо использовать наш токен:

bot = telebot.TeleBot(config.token)

Этим действиям мы устанавливаем то, что мы будем накручивать функционал именно для того бота, для которого нам и дал токен BotFather.

2. Разворачиваем функционал


Начнём с того, что для того, чтобы пользователю запустить бота, нам необходимо прописать команды для старта они могут быть разными, например, /start или /go и вообще любыми, какими Вы сочтёте нужными.

Для обработки команд нам потребуется message_handler, с помощью которого и будет реализован весь функционал обработки команд для старта и завершения, если Вы сочтёте нужным добавить завершение. Как только придёт команда /go или /start, message_handler с соответствующими командами сравнит, совпадают ли строки и если совпадают, то обработает соответствующей функцией.

Каждая функция, как и в примере сейчас, должна принимать один параметр сообщение от пользователя, которое будет обработано соответствующей функции в обёртке декоратора. А также, каждая функция (или связка функций) должна возвращать соответсвующее сообщение от бота.

Итак:

@bot.message_handler(commands=['go', 'start']) # Обработка команды для стартаdef welcome(message):sti = open(path+'stiker.tgs', 'rb')bot.send_sticker(message.chat.id, sti)markup = types.ReplyKeyboardMarkup(resize_keyboard=True)item3 = types.KeyboardButton("Приложения")item2 = types.KeyboardButton("Мероприятия")item1 = types.KeyboardButton('О нас')markup.add(item1, item2, item3)bot.send_message(message.chat.id,"Добро пожаловать, {0.first_name}!\\n\\nЯ - <b>{1.first_name}</b>, бот команды Projector в НГТУ, ""создан для того, ""чтобы помочь Вам влиться в нашу команду,""просто узнать что-то о нас или же просто пообщаться и весело провести время.\\n\\n""<i>Have a nice time</i>".format(message.from_user, bot.get_me()),parse_mode='html', reply_markup=markup)

В этой функции реализовано сразу два действия: отправка приветственного сообщения и создание встроенной клавиатуры ReplyKeyboardMarkup, которая будет открыта, пока мы не завершим выполнения бота соответсвующей командой. Об этом будет сказано ниже.

Итак, пройдёмся по строчкам:

В строках 20-21: открывается стикер по тому пути к директории, в которой я его сохранил, после чего отправляется.

Строки 22-28: создаем встроенную клавиатуру, добавляя туда три элемента.

Строки 30-37: описано создание и отправка приветственного сообщения

Как вы можете заметить, метод send_message в строке 30, позволяет использовать HTML, для форматирования текста.

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

# RUNif __name__ == "__main__":try:bot.polling(none_stop=True)except ConnectionError as e:print('Ошибка соединения: ', e)except Exception as r:print("Непридвиденная ошибка: ", r)finally:print("Здесь всё закончилось")

Сделаем первый запуск! Для этого, в PyCharm-е нажмём зеленую кнопку старт в правом верхнем углу или же, можно запустить из консоли командой: python



Результат первого запуска:



2.1 Обработка нажатия на кнопки и создание inline keyboard


Так любое сообщение это текст, то мы будем обрабатывать именно текстовые сообщения.

Сделаем следующее и аналогично разберём по строчкам:

@bot.message_handler(content_types=["text"])def go_send_messages(message):if message.chat.type == 'private':if message.text == 'Приложения':keyboard = types.InlineKeyboardMarkup(row_width=1)itemboo = types.InlineKeyboardButton(text="Тыщ на кнопку и ты уже в Google", url="<https://www.google.ru>")itemboo1 = types.InlineKeyboardButton('Рандомное число', callback_data='good2')itemboo2 = types.InlineKeyboardButton("Калькулятор", callback_data='bad2')itemboo3 = types.InlineKeyboardButton("Хочу узнать погоду в моем городе/стране", callback_data='good3')itemboo4 = types.InlineKeyboardButton("Как твои дела?", callback_data='bad4')keyboard.add(itemboo, itemboo1, itemboo2, itemboo3, itemboo4)bot.send_message(message.chat.id,"{0.first_name}, окей, смотри, что у нас есть тут:\\n".format(message.from_user),reply_markup=keyboard)elif message.text == "Мероприятия":one_markup = types.InlineKeyboardMarkup(row_width=1)ite1 = types.InlineKeyboardButton("Ближайшие мероприятия", callback_data="one")ite2 = types.InlineKeyboardButton("Проведенные мероприятия", callback_data="two")ite3 = types.InlineKeyboardButton("Волонтерство на мероприятие", callback_data="three")ite4 = types.InlineKeyboardButton("Действующие проекты в НГТУ", callback_data="fourth")ite5 = types.InlineKeyboardButton("Мероприятия Межвузовского центра", callback_data="five")one_markup.add(ite1, ite2, ite3, ite4, ite5)bot.send_message(message.chat.id, "{0.first_name}, у нас <u>ежемесячно</u> проводится множество ""мероприятий,\\nмы постарались разбить их на следующие составляющие:".format(message.from_user), parse_mode="html", reply_markup=one_markup)

Строка 339 обработчик любых текстовых сообщений

Строка 341 предназначена для того, чтобы сказать, что если данное сообщение предназначено боту, то сравни эту строку с теми, что здесь обрабатываются и отправь ответ.

Строки 344 351 создаём инлайновую клавиатуру InlineKeyboardMarkup и помещаем в эту клавиатуру 5 элементов, которые также можно будет обработать по установленной callback_data. Элементы данной клавиатуры будут расположены друг под другом, так как в строке 344, мы установили row_width = 1, что обозначает самую широкую грань одной кнопки, поэтому они и будут расположены друг под другом.

Строки 353-355 отправляют текст, вместе с нашей Inline Keyboard.

В условиях ниже представлены аналогичные представления обработки сообщений.

Итак, сделаем запуск:

2.2 Обработка InlineKeyboardButton


Как было сказано выше, каждый элемент InlineKeyboardButton имеет параметр callback_data, и именно по этим параметрам будет обрабатываться каждая кнопка. Для этого нам потребуется обработчик инлайновой клавиатуры callback_query_handler.

@bot.callback_query_handler(func=lambda call: call.data in ['one', 'two', 'three', 'fourth', 'five']) # Мероприятияdef callback_inline_one(call):try:if call.message:if call.data == 'one': # Ближайшие мероприятияbot.send_message(call.message.chat.id,"Итак,<b>ближайшие мероприятия</b>:\\n\\n" # Здесь будут ссылки ещё"Форум Байкал\\n""Конкурс Цифровой ветер\\n""PRONETI", parse_mode="html")elif call.data == 'two': # Проведённые мероприятияbot.send_message(call.message.chat.id, "Вот список <b>проведённых мероприятий</b>:\\n\\n""МНТК\\n""Семинары по проектной деятельности\\n""Встреча с представителями предприятий", parse_mode="html")elif call.data == 'three':

Итак, разберём по строчно:

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

Строки 273-278 В данном блоке if, мы просто обрабатываем сообщение и отправляем сообщение пользователю.

Строки 279-283 Делают аналогичное действие, что и в предыдущем условном блоке.

и т. д.

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

Результат:



Так просто и обрабатываются inline keyboards.

3. Завершаем работу бота


Данная функция будет аналогичной функции обработки команд для старта бота, поэтому Вы сможете легко понять её функционал:

@bot.message_handler(commands=['stop']) # Обработка команды для выходаdef bye(message):bye_Sti = open(path+'byeMorty.tgs', 'rb')hideBoard = types.ReplyKeyboardRemove()bot.send_message(message.chat.id,"Досвидания, {0.first_name}!\\nМы, команда <b>{1.first_name}</b>, надеемся, что ты хорошо провел(а) время \\n\\n""Присоединяйся к нашей команде в <a href='<https://vk.com/projector_neti>'>vk</a>\\n""Наш <a href='<https://instagram.com/projector_neti>'>inst</a>\\n\\n""Напиши Координатору проектов (<a href='<https://vk.com/nikyats>'>Никите Яцию</a>) и задай интересующие тебя вопросы по <i>проектной деятельности</i>\\n\\n""Надеемся, что тебе ответят очень скоро \\n\\n""<u>Don't be ill and have a nice day</u> \\n\\n\\n""P.S.: Если есть какие-то пожелания или вопросы по боту, то напиши <a href='<https://vk.com/setmyaddresspls>'>мне</a>".format(message.from_user, bot.get_me()), parse_mode='html', reply_markup=hideBoard)exit()

Здесь происходит следующее:

  1. Отправляется прощальный стикер.

  2. Закрывается встроенная клавиатура (строка 44).

  3. Отправляется прощальное сообщение.


Так как мы используем bot.polling, с параметром none_stop = True, то пользователь может снова вознообновить общение с ботом при помощи команды /start или /go, обработка которых показано в пункте выше.

Результат:



ВК БОТ

Перейдём к реализации бота Вконтакте, написанном на том же родном языке Python, его основное отличие от предыдущего в том, что он был написан с использованием ООП, а именно класса. Внешне, он выполняет ту же миссию. Сама библиотека для написания вк бота намного проще телеграмм бота, поэтому разобраться в ней самостоятельно не составит большого труда. Итак, начнём.

1. Предварительные подготавления


Установим следующие библиотеки по тем же технологиям:

import vk_api # pip install vk-apiimport json  # pip install jsonfrom vk_api.longpoll import VkLongPoll, VkEventType

1.1 Получение токена для сообщества Вконтакте.


  1. На главной странице сообщества найти раздел Управление
  2. Работа с API
  3. Создать ключ. Выбираете нужные для вас пункты, которые будут доступны боту.

В итоге должно получиться примерно следующее:

Берем ключ и переходим в среду разработки и делаем следующее:

vk = vk_api.VkApi(token="Ваш_токен")

Далее следующее:

longpoll = VkLongPoll(vk)

На этом, закончим подготавления.

2. Разворачиваем функционал


Первым делом создадим файл

Cоздадим прототип встроенной клавиатуры ( всё с помощью документации VkBotAPI ).

main_keyboard = {"one_time": False,"buttons": [[{"action": {"type": "text","payload": "{\\"button\\": \\"1\\"}","label": "О нас"},"color": "positive"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"2\\"}","label": "Мероприятия"},"color": "positive"},{"action": {"type": "text","payload": "{\\"button\\": \\"3\\"}","label": "Приложения"},"color": "positive"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"4\\"}","label": "Контакты"},"color": "primary"}]]}

Затем переводим её в формат json, как требуется в документации:

main_keyboard = json.dumps(main_keyboard, ensure_ascii=False).encode('utf-8')main_keyboard = str(main_keyboard.decode('utf-8'))

Пример инлайн клавиатуры:

about_us_keyboard = {"inline": True,"buttons": [[{"action": {"type": "text","payload": "{\\"button\\": \\"1\\"}","label": "Основная информация"},"color": "positive"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"2\\"}","label": "Чем мы занимаемся ?"},"color": "primary"},{"action": {"type": "text","payload": "{\\"button\\": \\"3\\"}","label": "Где мы находимся ?",},"color": "positive"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"4\\"}","label": "Как попасть в команду ?",},"color": "primary"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"5\\"}","label": "Контакты",},"color": "secondary"}],[{"action": {"type": "text","payload": "{\\"button\\": \\"6\\"}","label": "Задать вопрос руководителю проекта",},"color": "negative"}]],}

Не забываем все используемые клавиатуры переводить в формат json:

about_us_keyboard = json.dumps(about_us_keyboard, ensure_ascii=False).encode('utf-8')about_us_keyboard = str(about_us_keyboard.decode('utf-8'))

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

def write_msg(user_id, message, key):vk.method('messages.send',{'user_id': user_id,'message': message,'keyboard': key,'random_id': random.randint(0, 2048)})

После создания всех прототипов, мы можем перейти к следующему шагу. ( к этому файлу мы позже вернёмся и доработаем обработку общения пользователя и нашего vk-бота )

2.1 Основной функционал (создаем файл vk_bot.py)


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

Конструктор класса:

class VkBot:def __init__(self, user_id):self.USER_ID = user_idself._USERNAME = self._get_user_name_from_vk_id(user_id)self.my_str = ""self._COMMANDS = ["привет", "погода", "время", "пока"]self._inputMes = {"основная информация": answers.about_us1,"чем мы занимаемся ?": answers.about_us2,"где мы находимся ?": answers.about_us3,"ближайшие мероприятия": answers.events1,"проведённые мероприятия": answers.events2,"волонтёрство на мероприятие": answers.events3,"действующие проекты в нгту": answers.events4,"мероприятия межвузовского центра": answers.events5}

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

(Пример кода из файла)

events1 = "Итак,ближайшие мероприятия:\\n\\n" \\"Форум Байкал\\n"\\"Конкурс Цифровой ветер\\n"\\"PRONETI"events2 = "Вот список проведенных мероприятий:\\n"\\"МНТК\\n"\\"Семинары по проектной деятельности\\n"\\"Встреча с представителями предприятий\\n"\\events3 = "По поводу этого критерия напиши Илье (<https://vk.com/ki1337ki>)\\n"\\"А также, ты можешь заполнить анкету, благодаря которой,\\n"\\"с тобой лично свяжется один из руководителей направления\\n"\\"или координатор проекта (<https://vk.com/nikyats>)"

Итак, основной метод класса это new_message, который принимает один параметр message, который обрабатывается соответствующим условным блоком и возвращает какое -то значение обратно туда, откуда был вызван.

def _get_user_name_from_vk_id(self, user_id):request = requests.get("<https://vk.com/id>" + str(user_id))bs = bs4.BeautifulSoup(request.text, "html.parser")user_name = self._clean_all_tag_from_str(bs.findAll("title")[0])return user_name.split()[0]def new_message(self, message):# self.my_str = " ".join(re.findall('[0-9]{2}', message))if message.lower() == self._COMMANDS[0]:return f"Привет, {self._USERNAME}!"elif message.lower() == self._COMMANDS[1] or message.lower() == "узнать погоду ":return self._get_weather()elif message.lower() == self._COMMANDS[2] or message.lower() == "узнать точное время ":return self._get_time()elif message.lower() == self._COMMANDS[3]:return f"До скорой встречи, {self._USERNAME}!"else:for key, value in self._inputMes.items():if message.lower() == key:return valuereturn "Не понимаю тебя "

3. Возвращаемся в и дописываем функционал


Теперь в первых строках нам необходимо проимпортить файл vk_bot. А также нам потребуется библиотека random.

import random # pip install randomfrom vk_bot import VkBot

После того, как мы объявили longpoll, дописываем основной функционал.

longpoll = VkLongPoll(vk)try:for event in longpoll.listen():if event.type == VkEventType.MESSAGE_NEW:if event.to_me:bot = VkBot(event.user_id)if event.text.lower() == "о нас":write_msg(event.user_id, "Немного о нашем проекте", about_us_keyboard)elif event.text.lower() == "мероприятия":write_msg(event.user_id, "Что ты хочешь узнать?", events_keyboard)elif event.text.lower() == "приложения":write_msg(event.user_id, "Посмотри, что есть здесь!", app_keyboard)elif event.text.lower() == "контакты":write_msg(event.user_id, "По любым вопросам можешь обращаться к:", contacts_keyboard)elif event.text.lower() == "задать вопрос руководителю проекта":write_msg(event.user_id, "У тебя есть возможность написать сообщение нашему Руководителю проекта",go_answer)elif event.text.lower() == "калькулятор":write_msg(event.user_id, "В разработке...", calc_keyboard)# elif event.text == " ".join(re.findall('\\d{2}', event.text)):#   write_msg(event.user_id, "Отлично, мы здесь", calc_keyboard)elif event.text.lower() == "как попасть в команду ?":write_msg(event.user_id, "Напиши координатору проекта - Никите\\n""или перейди на сайт проектной деятельности,\\n""найди проект номер 612 и подай заявку", in_team)else:write_msg(event.user_id, bot.new_message(event.text), main_keyboard)except Exception as e:print(e)

Как можете заметить, в условных блоках if и elif присутствует обработка тех сообщений, которые подразумевают под собой вывод инлайн или встроенной клавиатуры (в данном примере выводятся только инлайн клавиатуры). Сюда также можно добавить более сложные обработки сообщений, после которых обработка будет метаться туда сюда по блокам if и elif. Таким образом бот будет работать, пока не упадёт с ошибкой.

Другое дело обстоит с блоком else, здесь мы обращаемся как раз ко классу нашего Бота, после чего он аналогичным способом находит обработку сообщения и выводит результат. Это можно назвать вложенной обработкой, а так как вложенностей лучше избегать, то лучше использовать тот метод программирования бота, который как раз-таки реализован в телеграмм боте.

Заключение


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

Как вы можете видеть, создать его функционал очень просто, особенно, если вы знаете основы языка Python.

Весь код опубликован в моём профиле GitHub:


Подробнее..

Перевод Запуск тестов Selenium в Jenkins

01.05.2021 16:18:08 | Автор: admin
В наши дни понятие DevOps у всех на слуху. Это организационный подход, широко используемый для ускорения разработки и развёртывания приложений. Организации внедряют у себя практики DevOps, так как они обещают дать тем, кто их использует, всё лучшее, что существует в мире разработки ПО, причём на всех этапах работы от планирования и тестирования, до развёртывания и мониторинга проектов. В реализации практик DevOps важную роль играют CI/CD-инструменты вроде Jenkins. А интеграция Jenkins с Selenium значительно облегчает процесс автоматизации Selenium-тестов.



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

Что такое Jenkins?


Jenkins это опенсорсный DevOps-инструмент, который пользуется популярностью в деле организации процессов непрерывной интеграции и непрерывной доставки программного обеспечения. Это платформонезависимое приложение, написанное на Java. Оно представляет собой систему для сборки проектов. В частности, с его помощью собирают проекты из исходного кода, запускают модульные тесты, рассылают отчёты о сборках соответствующим членам команды разработчиков.

Подробнее о Jenkins можно почитать здесь.

Что такое Selenium?


Selenium это опенсорсный инструмент, который широко используется для автоматизации тестирования веб-приложений. Пользоваться им просто, существуют форумы по Selenium, куда может обратиться тот, кто столкнулся с какими-то проблемами. Благодаря этому Selenium обрёл определённую популярность в кругах тестировщиков ПО. В состав Selenium входят четыре основных компонента: Selenium IDE, Selenium RC, Selenium WebDriver и Selenium Grid. Они созданы в расчёте на решение различных задач. Selenium даёт тому, кто им пользуется, возможности по кросс-браузерному и параллельному тестированию веб-приложений. Это позволяет тестировщикам выполнять испытания приложений в разных операционных системах и браузерах, что, в частности, даёт им возможность проверить совместимость веб-проекта с различными браузерами.

Если вас интересуют подробности о Selenium посмотрите этот материал.

Теперь, после того, как я в двух словах рассказала о Jenkins и Selenium, пришло время серьёзно заняться вопросами интеграции этих инструментов. Но, прежде чем об этом говорить, надо задаться вопросом о том, установлены ли они в системе. Поэтому давайте сначала обсудим их установку и настройку.

Установка, настройка и запуск Jenkins


Шаг 1 Для того чтобы загрузить Jenkins можно воспользоваться соответствующей ссылкой на официальном сайте проекта. Нас, в частности, интересует .war-файл Jenkins.

Шаг 2 Загрузим файл jenkins.war и сохраним его в выбранную директорию.

Шаг 3 Откроем окно командной строки и перейдём в папку, в которой хранится загруженный .war-файл.

Шаг 4 Выполним команду java -jar Jenkins.war. Будет запущен сервер Jenkins.

Шаг 5 Обычно Jenkins по умолчанию запускается на порте 8080. Если этим портом уже пользуется какой-то другой сервис, можно указать при запуске Jenkins другой порт, воспользовавшись командой такого вида:

java -jar jenkins.war --httpPort=8081

В подобной команде можно указать любой свободный порт, а не только 8081. Этот порт и будет использоваться сервером Jenkins.

Настройка порта для Jenkins

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

Пароль

В консоли, после успешного завершения установки, можно будет увидеть сообщение Jenkins is fully up and running.

Шаг 6 Запустим браузер и перейдём к localhost. Как уже говорилось, по умолчанию Jenkins запускается на порте 8080. В моём случае порт был изменён на 8081, именно к этому порту localhost мне и нужно подключиться.

После этого откроется страница настроек Jenkins. Она используется для создания административной учётной записи.

Шаг 7 Введём в соответствующее поле пароль, который ранее был показан в консоли, и нажмём на кнопку Continue для продолжения настройки системы.

Окно ввода пароля

Шаг 8 После ввода пароля система предложит нам установить плагины.

Предложение об установке плагинов

Если вы не знаете точно о том, какие именно плагины вам понадобятся можете выбрать вариант Install suggested plugins. Это приведёт к установке набора наиболее часто используемых плагинов. Но если вы знаете о том, что именно вам нужно, основываясь, например, на требованиях к проекту, вы можете выбрать вариант Select plugins to install и самостоятельно выбрать плагины для установки.

После этого плагины будут загружены и установлены.

Шаг 9 После успешной установки плагинов система предложит создать учётную запись администратора, для чего нужно будет ввести имя пользователя, пароль и адрес электронной почты. После того, как эта учётная запись создана, нас перенаправят в панель управления Jenkins.

Создание учётной записи администратора

Существует множество способов интеграции Jenkins с Selenium WebDriver. Некоторые из них мы рассмотрим ниже. Разные способы интеграции этих систем применимы в различных условиях.

Первый метод интеграции Jenkins с Selenium


Шаг 1 В панели управления Jenkins щёлкнем по значку New Item для создания нового проекта. Укажем имя проекта и выберем вариант Freestyle Project. После этого нажмём OK.

Начало создания нового проекта

Настройка свойств проекта

Шаг 2 На вкладке свойств проекта General введём, в поле Description, описание проекта.

Ввод описания проекта

Шаг 3 На вкладке Source Code Management выберем None в группе параметров Source Code Management.

Выбор системы управления исходным кодом

Шаг 4 Jenkins позволяет планировать задания по сборке проектов. Время запуска заданий указывают в таком формате:

MINUTE HOUR DOM MONTH DOW

Смысл этих сокращений раскрыт в следующей таблице

Сокращённое наименование Описание
MINUTE Минуты в пределах часа (0 59)
HOUR Часы в пределах дня (0 23)
DOM День месяца (1 31)
MONTH Месяц (1 12)
DOW День недели (0 7), где воскресенье может быть представлено значениями 0 и 7

Шаг 5 Создадим проект, который будем применять для запуска тестов с использованием Selenium WebDriver и TestNG.

Вот код на Java:

package Pages;import static org.testng.Assert.assertEquals;import java.util.concurrent.TimeUnit;import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.testng.annotations.AfterTest;import org.testng.annotations.BeforeTest;import org.testng.annotations.Test;public class LoginPage {WebDriver driver;@BeforeTestpublic void setUp() {System.setProperty("webdriver.chrome.driver", "C:\\Users\\Shalini\\Downloads\\chrom86_driver\\chromedriver.exe");driver = new ChromeDriver();}public void login() {String login_url = "https://opensource-demo.orangehrmlive.com/";driver.get(login_url);driver.manage().window().maximize();driver.manage().timeouts().pageLoadTimeout(10, TimeUnit.SECONDS);driver.findElement(By.id("txtUsername")).sendKeys("Admin");driver.findElement(By.id("txtPassword")).sendKeys("admin123");System.out.println(driver.getTitle());}@Testpublic void dashboard() {driver.findElement(By.id("menu_dashboard_index")).click();String textPresent = driver.findElement(By.xpath("//*[@id=\"content\"]/div/div[1]/h1")).getText();String textToBePresent = "DashBoard";assertEquals(textPresent, textToBePresent);}@AfterTestpublic void tearDown() {driver.quit();}}

Шаг 6 Создадим файл TestNG.xml:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><suite name="TestSuite"><test name="LoginTest"><groups><run><include name="DemoTest"/></run></groups><classes><class name="Pages.LoginPage"></class></classes></test></suite>

Шаг 7 В папке проекта добавим зависимости. Создадим .bat-файл со следующим содержимым:

ava cp bin;lib/* org.testng.TestNG TestNG.xml

Тут надо указать точное имя файла TestNG.xml, созданного ранее.

Шаг 8 Выберем в панели управления Jenkins проект, который мы создали в самом начале работы. Щёлкнем по Configure. На вкладке General щёлкнем по Advanced и установим флажок Use custom workplace. Введём в поле Directory путь к папке проекта.

Ввод пути к папке проекта

Шаг 9 На вкладке Build откроем выпадающий список Add Build Step и выберем Execute Windows batch Command. Введём в него имя .bat-файла, созданного на шаге 7.

Ввод сведений о .bat-файле

Шаг 10 Щёлкнем по кнопке Apply, потом по кнопке Save для того чтобы сохранить изменения, внесённые в настройки Jenkins-проекта. Теперь щёлкнем по кнопке Build Now, после чего можно будет понаблюдать за процессом выполнения тестов.

Запуск сборки проекта

Второй метод интеграции Jenkins с Selenium


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

Что такое Maven?


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

Совместное использование Maven, Jenkins и Selenium WebDriver даёт возможность построения DevOps-модели, реализующей механизм непрерывной интеграции программных проектов.

Установка Maven


Шаг 1 Загрузим дистрибутив Maven с официального сайта проекта.

Шаг 2 Добавим в состав системных переменных MAVEN_HOME переменную, в которой содержится путь к домашней директории Maven.

Системная переменная MAVEN_HOME

Шаг 3 Внесём в переменную Path путь к директории bin, содержащейся в домашней директории Maven.


Настройка переменной Path

Шаг 4 Для того чтобы проверить правильность установки Maven откроем окно командной строки и попробуем выполнить команду mvn version.

Проверка правильности установки Maven

Шаг 5 Создадим Maven-проект и добавим в файл pom.xml соответствующие зависимости:

<project xmlns="http://personeltest.ru/away/maven.apache.org/POM/4.0.0" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://personeltest.ru/away/maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>demoProject</groupId><artifactId>demoProject</artifactId><version>0.0.1-SNAPSHOT</version><build><sourceDirectory>src</sourceDirectory><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version><configuration><sоurce>1.8</sоurce><target>1.8</target></configuration></plugin></plugins></build><dependencies><dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>3.141.59</version></dependency><dependency><groupId>org.testng</groupId><artifactId>testng</artifactId><version>7.3.0</version><scope>test</scope></dependency></dependencies></project>

Вот Java-код:

package WebDriverProject;import org.junit.Assert;import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.firefox.FirefoxDriver;public class LoginClass {WebDriver driver;@BeforeTestpublic void setup() {System.setProperty("WebDriver.gecko.driver", "C:\\Users\\shalini\\Downloads\\geckodriver-v0.26.0-win64\\geckodriver.exe");driver = new FirefoxDriver(); //Инициализация WebDriver}@Testpublic void loginTest() {driver.get("https://opensource-demo.orangehrmlive.com/"); //Определение URLString pageTitle = driver.getTitle(); //get the title of the webpageSystem.out.println("The title of this page is ===> " + pageTitle);Assert.assertEquals("OrangeHRM", pageTitle); //Проверка заголовка страницыdriver.findElement(By.id("txtUsername")).clear(); //Очистить поле перед вводом значенийdriver.findElement(By.id("txtUsername")).sendKeys("Admin"); //Ввести имя пользователяdriver.findElement(By.id("txtPassword")).clear();driver.findElement(By.id("txtPassword")).sendKeys("admin123"); //Ввести парольdriver.findElement(By.id("btnLogin")).click(); //Щёлкнуть по кнопке LoginSystem.out.println(Successfully logged in );}@AfterTestpublic void teardown() {driver.quit();}}

Вот XML-файл с настройками тестов:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><suite name="TestSuite"><test name="LoginTest"><groups><run><include name="DemoTest"/></run></groups><classes><class name=" WebDriverProject.LoginClass"></class></classes></test></suite>

Интеграция Selenium и Jenkins с помощью Maven


Только что мы говорили об интеграции Jenkins с Selenium WebDriver. То, что получилось, лучше всего использовать для автоматизации Selenium-тестов. Интеграция Jenkins с Selenium WebDriver даёт разработчику надёжную систему кросс-браузерного тестирования проектов. Здесь мы поговорим об интеграции Jenkins и Selenium с помощью Maven.

Шаг 1 Запустим сервер Jenkins.

Шаг 2 Откроем браузер и перейдём на localhost, использовав порт, на котором работает Jenkins.

Открытие панели управления Jenkins в браузере

Шаг 3 Щёлкнем по кнопке New Item в панели управления

Кнопка New Item

Шаг 4 Введём имя проекта и, в качестве типа проекта, выберем Maven project.

Ввод имени проекта и выбор его типа

Шаг 5 Нажмём на кнопку OK. После этого можно будет увидеть, как в панели инструментов появилось новое задание.

Новый проект в панели управления

Шаг 6 Выберем проект и нажмём на кнопку Configure.

Переход к настройке проекта

Шаг 7 На вкладке Build, в поле Root POM, введём полный путь к файлу pom.xml. В поле Goals and options введём clean test.

Настройка параметров сборки проекта

Шаг 8 Щёлкнем Apply, а затем Save.

Шаг 9 Нажмём на кнопку Build Now.

Запуск сборки проекта

Шаг 10 Будет начата сборка проекта, а после её завершения появится соответствующее сообщение. Для того чтобы посмотреть полный журнал нужно щёлкнуть по кнопке Console Output.

Просмотр журнала сборки

Итоги


Мы поговорили о том, почему проект Jenkins пользуется серьёзной популярностью, и о том, как интегрировать его с Selenium WebDriver ради эффективного выполнения тестов и достижения целей непрерывной интеграции программных проектов. Использование Jenkins для автоматизации запуска тестов позволяет разработчикам экономить время, а результаты выполнения тестов можно проанализировать, просмотрев соответствующие лог-файлы. Такой подход позволяет организовать полный цикл разработки ПО разработку, развёртывание, тестирование, мониторинг, выпуск в продакшн. Jenkins поддерживает множество различных плагинов, предназначенных для решения самых разных задач, встающих перед разработчиками. Система, кроме того, умеет сообщать заинтересованным лицам о том, успешно ли прошла сборка проекта.

Надеюсь, теперь вы без труда сможете интегрировать Jenkins с Selenium WebDriver.

Пользуетесь ли вы Jenkins и Selenium WebDriver?

Подробнее..

Пишем расширение для MediaWiki

12.05.2021 08:15:25 | Автор: admin

В рунете я почти не встречал материалов о том, как писать расширения для MediaWIki (платформы, на которой работает Википедия). Основной стартовой точкой при написании расширений был и остается официальный сайт платформы, но там процесс расписан не очень дружелюбно по отношению к новичкам. Попробуем же это исправить.

В этой статье я покажу, как написать простейшее расширение для Медиавики, включающее в себя новый метод API, расширение парсера и немного js/css для фронтенда. А чтобы не было скучно, приплетем сюда работу с Google Knowledge Graph.

Расширения MediaWiki

MediaWiki модульная платформа, куда можно устанавливать расширения для добавления самого разного функционала. Помимо того, что расширения могут реализовывать какой-то свой независимый функционал (например, добавлять какие-нибудь виджеты), оно также может и модифицировать функциональность платформы: например, менять принцип работы поиска или модифицировать внешний вид платформы. Посмотреть примеры расширений можно на официальном сайте платформы.

Пишутся расширения, как правило, на php+jQuery. Возможность встраиваться в код ядра MediaWiki (или в код других расширений) реализована через т.н. хуки. Хуки позволяют вызывать дополнительный код по заданным событиям. Примерами таких событий могут быть: сохранение страницы, вызов поиска по сайту, открытие страницы на редактирование и так далее.

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

Что будем писать?

Готовое расширение можно взять тут:
https://github.com/Griboedow/GoogleKnowledgeGraph

Давайте развлечемся и напишем что-нибудь бесполезное. Скажем, расширение, которое будет вытаскивать описания с Google Knowledge Graph.

Т.е. расширение будет вот это:

Код этого приложения прост и изящен как <GoogleKnowledgeGraph query="Мэльхэнанвенанхытбельхын"/>

Превращать в это:

Штука довольно бесполезная, но она послужит хорошей иллюстрацией. Еще и с графом знаний Гугла поиграемся!

Расширение сделано исключительно в учебных целях, не рекомендую его использовать на настоящих вики. Гугл предоставляет 100 000 бесплатных запросов в день. Для небольших вики это не проблема, но на серьезных сайтах ресурс будет исчерпан очень быстро.

Как оно будет работать

Примерный принцип работы расширения выглядит так:

  1. Пользователь сохраняет страницу, где в тексте присутствуют теги <GoogleKnowledgeGraph query="Ричард Докинз">.

    • MediaWIki позволяет использовать не только формат тега, но и формат функции парсера <link>: {{#GoogleKnowledgeGraph||query=Ричард Докинз}}.

  2. Расширение функции парсера превращает тег в html код <span class="googleKnowledgeGraph">Ричард Докинз</span>

  3. JS код при загрузке страницы идет по всем элементам .googleKnowledgeGraph и запрашивает через API нашего же расширения описания терминов, подставляя их в title.

  4. API нашего расширения будет максимально примитивным: он будет передавать запросы от фронтенда на Google API, чистить ответ от всего лишнего и передавать очищенное описание сущности на фронт.

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

Итого, нам потребуется:

  1. Определить манифест нашего расширения.

  2. Расширить MediaWIki API, добавив запрос на получение описания из Google Knowledge Graph

  3. Расширить парсер MediaWiki, добавив обработку нового тега.

  4. Добавить JS код, который будет выполняться по загрузке страницы

  5. Подгрузить наше расширение в MediaWiki

  6. Поделиться результатом наших трудов с сообществом.

А еще перед началом работы вам потребуется токен для работы с Google Knowledge Graph API. Сгенерировать его можно тут.

Создаем структуру расширения

Типичная иерархия файлов и папок для MediaWIki расширения выглядит так:

extensions                    <-- Папка всех расширений MediaWiki GoogleKnowledgeGraph      <-- Подпапка с нашим расширением      extension.json   <-- Манифест нашего расширения     i18n           <-- Каталог с используемыми строками для разных языков      en.json    <-- Строки на английском      qqq.json   <-- Описания строк для облегчения жизни переводчиков      ru.json    <-- Строки на русском     includes                             <-- PHP код      ApiGoogleKnowledgeGraph.php      <-- Расширение API      GoogleKnowledgeGraph.hooks.php   <-- Расширение парсера и другие хуки     modules                                <-- Папка с JS модулями          ext.GoogleKnowledgeGraph           <-- В нашем случае модуль только 1             ext.GoogleKnowledgeGraph.css   <-- CSS стили нашего модуля             ext.GoogleKnowledgeGraph.js    <-- JS код нашего модуля

Разберем содержимое всех файлов по порядку, и начнем с самого простого.

Интернационализация (i18n)

Для того, чтобы нашим расширением было удобно пользоваться на всех языках, можно воспользоваться стандартной системой интернационализации banana-i18n. Помимо облегчения интернационализации, эта система также позволяет хранить все тексты в одном месте (а не раскиданными по коду). Выглядит это примерно так:

qqq.json

{"@metadata": {"authors": [ "Developer Name" ]},"googleknowledgegraph-description": "Description of the extension, to be show in Special:Vesion.","apihelp-askgoogleknowledgegraph-summary" : "Help string for 'askgoogleknowledgegraph' API request","apihelp-askgoogleknowledgegraph-param-query": "Help string for 'query' parameter of API request 'askgoogleknowledgegraph'"}

en.json

{"@metadata": {"authors": [ "Nikolai Kochkin" ]},"googleknowledgegraph-description": "The extension gets brief description from Google Knowledge Graph","apihelp-askgoogleknowledgegraph-summary" : "API to get description from Google Knowledge Graph","apihelp-askgoogleknowledgegraph-param-query": "String to ask from Google Knowledge Graph"}

Создаем манифест расширения (extension.json)

Для начала разберемся, как нам сообщить MediaWiki, что нужно загрузить то или иное расширение. Путей на самом деле два:

  • Использоватьrequire_once( '/path/to/file.php' ). Этот метод считается устаревшим, так что мы его подробно не будем рассматривать.

  • Использовать функцию wfLoadExtension('ExtensionName'). Сейчас этот способ считается основным, так что на нем и остановимся. http://personeltest.ru/aways/habr.com/ru/company/veeam/blog/544534

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

Определяем манифест (файл extension.json):

{"name": "GoogleKnowledgeGraph","version": "0.1.0","author": ["Nikolai Kochkin"],"url": "http://personeltest.ru/aways/habr.com/ru/company/veeam/blog/544534/","descriptionmsg": "googleknowledgegraph-description","license-name": "GPL-2.0-or-later","type": "parserhook","requires": {"MediaWiki": ">= 1.29.0"},"MessagesDirs": {"GoogleKnowledgeGraph": ["i18n"]},"AutoloadClasses": {"GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php","ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php"},"APIModules": {"askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph"},"Hooks": {"OutputPageParserOutput": "GoogleKnowledgeGraphHooks::onBeforeHtmlAddedToOutput","ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup"},"ResourceFileModulePaths": {"localBasePath": "modules","remoteExtPath": "GoogleKnowledgeGraph/modules"},"ResourceModules": {"ext.GoogleKnowledgeGraph": {"localBasePath": "modules/ext.GoogleKnowledgeGraph","remoteExtPath": "GoogleKnowledgeGraph/modules/ext.GoogleKnowledgeGraph","scripts": ["ext.GoogleKnowledgeGraph.js"],"styles": ["ext.GoogleKnowledgeGraph.css"]}},"config": {"GoogleApiLanguage": {"value": "ru","path": false,"description": "In which language you want to get result from the Knowledge Graph","public": true},"GoogleApiToken": {"value": "","path": false,"description": "API token to be used with Google API","public": false}},"ConfigRegistry": {"GoogleKnowledgeGraph": "GlobalVarConfig::newInstance"},"manifest_version": 2}
Разбираем extension.json по частям

Первая часть файла определяет то, что пользователь увидит в описании расширения на странице Special:Version

"name": "GoogleKnowledgeGraph","version": "0.1.0","author": ["Nikolai Kochkin"],"url": "http://personeltest.ru/aways/habr.com/ru/company/veeam/blog/544534/","descriptionmsg": "googleknowledgegraph-description","license-name": "GPL-2.0-or-later","type": "parserhook",

Далее мы указываем зависимости нашего расширения: с какими версиями MediaWIki расширение может работать, какие версии php требуются, какие расширения должны быть уже установлены и так далее.

"requires": {"MediaWiki": ">= 1.29.0"},

Затем мы указываем, где искать файлы со строками i18n

"MessagesDirs": {"GoogleKnowledgeGraph": ["i18n"]},

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

"AutoloadClasses": {"GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php","ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php"},

Заявляем, что мы реализовываем API метод askgoogleknowledgegraph в классе ApiAskGoogleKnowledgeGraph

"APIModules": {"askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph"},

Перечисляем, какие коллбеки для каких хуков у нас реализованы

"Hooks": {"BeforePageDisplay": "GoogleKnowledgeGraphHooks::onBeforePageDisplay","ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup"},

Сообщаем, что модули наши лежат в папке modules

"ResourceFileModulePaths": {"localBasePath": "modules","remoteExtPath": "GoogleKnowledgeGraph/modules"},

И определяем наш фронтенд модуль с js и css. Когда модулей несколько, можно указать в коде зависимости между ними.

"ResourceModules": {"ext.GoogleKnowledgeGraph": {"localBasePath": "modules/ext.GoogleKnowledgeGraph","remoteExtPath": "GoogleKnowledgeGraph/modules/ext.GoogleKnowledgeGraph","scripts": ["ext.GoogleKnowledgeGraph.js"],"styles": ["ext.GoogleKnowledgeGraph.css"]}},

И, наконец, задаем дополнительные параметры конфигурации нашего расширения

"config": {"GoogleApiLanguage": {"value": "ru","path": false,"description": "In which language you want to get result from the Knowledge Graph","public": true},"GoogleApiToken": {"value": "","path": false,"description": "API token to be used with Google API","public": false}},"ConfigRegistry": {"GoogleKnowledgeGraph": "GlobalVarConfig::newInstance"},

В LocalSettings.php опции будут иметь стандартный префикс wg

$wgGoogleApiToken = 'your-google-token';$wgGoogleApiLanguage = 'ru';

И, наконец, задаем версию схемы манифеста

"manifest_version": 2

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

Расширяем API

Для начала реализуем API.

В extension.json мы заявили, что у нас будет метод askgoogleknowledgegraph, реализованный в классе ApiAskGoogleKnowledgeGraph из файла includes/ApiAskGoogleKnowledgeGraph.php:

// extension.json fragment"AutoloadClasses": {    <...>    "ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php"},"APIModules": {           "askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph"     },

Теперь реализуем наш метод. Файл includes/ApiAskGoogleKnowledgeGraph.php:

<?php/**  * Класс включает в себя реализацию и описание API метода askgoogleknowledgegraph * Для простоты я не реализую кеширование, любопытные могут подсмотреть реализацию тут:  * https://github.com/wikimedia/mediawiki-extensions-TextExtracts/blob/master/includes/ApiQueryExtracts.php */use MediaWiki\MediaWikiServices;class ApiAskGoogleKnowledgeGraph extends ApiBase {public function execute() {$params = $this->extractRequestParams();// query - обязательный параметр, так что $params['query'] всегда определен$description = ApiAskGoogleKnowledgeGraph::getGknDescription( $params['query'] );/** * Определяем результат для Get запроса.  * На самом деле Post запрос отработает с тем же успехом,  * если специально не отслеживать тип запроса \_()_/. */$this->getResult()->addValue( null, "description", $description );}/**  * Список поддерживаемых параметров метода */public function getAllowedParams() {return ['query' => [ApiBase::PARAM_TYPE => 'string',ApiBase::PARAM_REQUIRED => true,]];}/** * Получаем данные из Google Knowledge Graph,      * предполагая, что самый первый результат и есть верный. */private static function getGknDescription( $query ) {/** * Вытаскиваем параметры языка и токен. * Все параметры в LocalSettings.php имеют префикс wg, например: wgGoogleApiToken. * Здесь же мы их указываем без префикса */$config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'GoogleKnowledgeGraph' );$gkgToken = $config->get( 'GoogleApiToken' );$gkgLang = $config->get( 'GoogleApiLanguage' );$service_url = 'https://kgsearch.googleapis.com/v1/entities:search';$params = ['query' => $query ,'limit' => 1,'languages' => $gkgLang,'indent' => TRUE,'key' => $gkgToken,];$url = $service_url . '?' . http_build_query( $params );$ch = curl_init();curl_setopt( $ch, CURLOPT_URL, $url) ;curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );$response = json_decode( curl_exec( $ch ), true );curl_close( $ch );if( count( $response['itemListElement'] ) == 0 ){return "Nothing found by your request \"$query\"";}if( !isset( $response['itemListElement'][0]['result'] ) ){return "Unknown GKG result format for request \"$query\"";}if( !isset($response['itemListElement'][0]['result']['detailedDescription'] ) ){return "detailedDescription was not provided by GKG for request \"$query\"";}if( !isset( $response['itemListElement'][0]['result']['detailedDescription']['articleBody'] ) ){return "articleBody was not provided by GKG for request \"$query\"";}return $response['itemListElement'][0]['result']['detailedDescription']['articleBody'];}}

Теперь мы можем обращаться по апи к нашей вики:

Get /api.php?action=askgoogleknowledgegraph&query=Выхухоль&format=jsonResponse body:{"description": "Выхухоль, или русская выхухоль, или хохуля,  вид млекопитающих отряда насекомоядных из трибы Desmanini подсемейства Talpinae семейства кротовых. Один из двух видов трибы; вторым видом является пиренейская выхухоль."}

Расширяем парсер и используем прочие хуки

// Фрагмент файла extension.json"AutoloadClasses": {"GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php",<...>},"Hooks": {"BeforePageDisplay": "GoogleKnowledgeGraphHooks::onBeforePageDisplay","ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup"},

В extension.json мы заявили, что в классе GoogleKnowledgeGraphHooks из файла includes/GoogleKnowledgeGraph.hooks.php реализуем расширения для хуков:

  • OutputPageParserOutput в методе onBeforeHtmlAddedToOutput;

  • ParserFirstCallInit в методе onParserSetup

Немножко про используемые хуки:

  • OutputPageParserOutput позволяет выполнить какой-то код после того, как парсер закончил формировать html, но перед тем, как html был добавлен к аутпуту. Здесь мы, например, можем подгрузить фронтенд. Фронтенд мы целиком расположили в модуле ext.GoogleKnowledgeGraph, так что достаточно будет подгрузить его.

  • ParserFirstCallInit позволяет расширить парсер дополнительными методами. Мы добавим в парсер обработку тега <GoogleKnowledgeGraph>.

Итак, реализация (файл includes/GoogleKnowledgeGraph.hooks.php):

<?php/** * Хуки расширения GoogleKnowledgeGraph  */class GoogleKnowledgeGraphHooks {/** * Сработает хук после окончания работы парсера, но перед выводом html.  * Детали тут: https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput */public static function onBeforeHtmlAddedToOutput( OutputPage &$out, ParserOutput $parserOutput ) {// Добавляем подгрузку модуля фронтенда для всех страниц, его определение ищи в extension.json$out->addModules( 'ext.GoogleKnowledgeGraph' );return true;}/** * Расширяем парсер, добавляя обработку тега <GoogleKnowledgeGraphHooks> */public static function onParserSetup( Parser $parser ) {$parser->setHook( 'GoogleKnowledgeGraph', 'GoogleKnowledgeGraphHooks::processGoogleKnowledgeGraphTag' );return true;}/** * Реализация обработки тега <GoogleKnowledgeGraph>  */public static function processGoogleKnowledgeGraphTag( $input, array $args, Parser $parser, PPFrame $frame ) {// Парсим аргументы, переданные в формате <GoogleKnowledgeGraph arg1="val1" arg2="val2" ...> if( isset( $args['query'] ) ){$query = $args['query'];}else{// В тег не был передан аргумент query, так что и выводить нам нечегоreturn '';}return '<span class="googleKnowledgeGraph">' . htmlspecialchars( $query ) . '</span>';}}

Добавляем фронтенд

Фронтенд свяжет воедино все, что мы реализовали выше.

// Фрагмент файла extension.json    "ResourceModules": {"ext.GoogleKnowledgeGraph": {"localBasePath": "modules","remoteExtPath": "GoogleKnowledgeGraph/modules","scripts": ["ext.GoogleKnowledgeGraph.js"],"styles": ["ext.GoogleKnowledgeGraph.css"],"dependencies": []}},  "ResourceFileModulePaths": {"localBasePath": "modules","remoteExtPath": "GoogleKnowledgeGraph/modules"},

В extension.json мы заявили, что у нас есть один модуль ext.GoogleKnowledgeGraph, который находится в папке modules и состоит из двух файлов:

  • modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.js

  • modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.css

Загрузку модуля мы реализовали чуть раньше в методе onBeforeHtmlAddedToOutput. Определим теперь и сам код модуля.

Для начала зададим стили
(файл modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.css):

.googleKnowledgeGraph{    border-bottom: 1px dotted #000;    text-decoration: none;}

А теперь возьмемся за JS
(файл modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.js):

( function ( mw, $ ) {  /**   * Ищем все элементы с <span class="googleKnowledgeGraph">MyText</span>,   * вытаскиваем MyText и отправляем запрос   * /api.php?action=askgoogleknowledgegraph&query=MyText   * После чего добавляем результат в 'title'.   */$( ".googleKnowledgeGraph" ).each( function( index, element ) {$.ajax({type: "GET", url: mw.util.wikiScript( 'api' ),data: { action: 'askgoogleknowledgegraph', query: $( element ).text(),format: 'json',},dataType: 'json',success: function( jsondata ){$( element ).prop( 'title', jsondata.description );}});});}( mediaWiki, jQuery ) );

JS код довольно прост. jQuery нам достался даром, поскольку MediaWiki подгружает его автоматически.

Подгружаем наше расширение и радуемся

Для загрузки расширения, как мы уже обсуждали, потребуется поправить файл LocalSettings.php. Добавляем в самый конец:

// Фрагмент файла LocalSettings.php<?php<...>  wfLoadExtension( 'GoogleKnowledgeGraph' );$wgGoogleApiToken = "your-google-token";$wgGoogleApiLanguage = 'ru';

Можно пробовать! Добавим на страницу что-нибудь эдакое:

Даже <GoogleKnowledgeGraph query="прикольный флот"/> может стать отстойным.

И получим:

Делимся с сообществом

Если есть возможность поделиться расширением с общественностью, то можно создать страницу на MediaWiki с кратким описанием, что ваше расширение может сделать (не забудьте скриншоты: лучше один раз увидеть, чем сто раз прочитать). На страницы с описаниями расширений обычно добавляют шаблон Extension, поля которого хорошо задокументированы. Если же возникнут сложности, всегда можно скопировать его с другой страницы расширений и подправить отличающиеся поля.

Типичная страница с описанием расширенияТипичная страница с описанием расширения

Заключение

В статье был описан случай довольно простого расширения, но, на самом деле, такие расширения как iFrame, CategoryTree, Drawio и многие другие не очень далеко ушли по сложности.

За скобками остались такие вещи, как работа с базой, кэширование, OOUI и много-многое другое. Все ж я вас не напугать хотел, а как раз наоборот показать, что писать расширения под вики на самом деле совсем не сложно и не страшно.

Ссылки

Подробнее..

Перевод Напишем и поймем Decision Tree на Python с нуля! Часть 2. Основы Python, необходимые для генерации Decision Tree

11.09.2020 18:10:30 | Автор: admin
Привет, Хабр! Представляю вашему вниманию перевод статьи "Python02. Python".

Данная статья вторая в серии. Первую вы можете найти здесь.

2.1 Комментарии обозначаются # или ''' (три одинарные кавычки)


# Комментарийa = 1 # Комментарий ''' Это тоже комментарийb = cc = d'''

2.2 Использование динамической типизации (тип определяется автоматически)


# динамическая типизация переменных# = копирование значения справа налевоi = 1 #  целое число (int)f = 2.1 # число с плавающей запятой (float)s = "a" # строковый тип (string)b = True # логический тип (boolean)l = [0,1,2] # массив,список (array) t = (0,1,2) # кортеж (tuple)d = {"a":0, "b":1} # словарь, ассоциативный массивprint(i,f,s,b,l,t,d)# 1 2.1 a True [0, 1, 2] (0, 1, 2) {'a': 0, 'b': 1}# Когда хотим определить тип, используем typeprint(type(i)) # Вывод <class 'int'># Переменная не сохраняет, а содержит фактическое значение# Это, своего рода, переменная-ссылка, указывающая на местоположение значения# Можно получить идентификатор актуального значения через idprint(id(l)) # 00000000000000 (меняется от исполнения к исполнениюц)l2 = l # Приведу в пример копию массива, где ссылаюсь на 2 его элемента, а фактический массив - 1. print(id(l2)) # 00000000000000 (то же значение, что у вышеуказанного id(l))# Поскольку существует только один фактический массив, кажется, что он был добавлен в массив l, даже если вы добавили элемент со ссылкой на l2.l2.append(1)

2.3 Арифметические операторы (Вычисление и присвоение значений. Результат вычисления, как правило, помещается в переменную)


a = 1 # переменная а соответствует значению 1b = a # b так же соответствует 1 a = 2 # теперь а соответствует 2, но b не изменяетсяa += 1 # значение а увеличивается на 1 (на данном этапе а равно 3)a -= 1 # значение а уменьшается на 1 (на данном этапе а равно 2)a *= 2 # значение а увеличивается в два раза (на данном этапе а равно 4)a /= 3 # значение а составляет  предыдущего значения (на данном этапе а равно 1.3333333333333333)a = 1+2-3*4/5 # Вычисление в следующем порядке: умножение, деление, сложение, вычитание  a равно 0,6. (Однако, если учитывать погрешность, а равно 0.6000000000000001)

2.4 Создание группы программ через отступ


a = 0# Указываем условие выполнения группы программ, обозначенной отступом.# Далее прописываются условия, циклы, функционализация и т.д.if a == 1:    # Строка, которая смещена вправо на такую же величину в левой части строки от этой строки, составляет одну группу программ.    print(1)    print(2)    pass # Указание на конец группы с отступом    # при выполнении условия if выполняется группа программ до строки вышеprint(3) # вывод независимо от значения а, не подпадающий под условие if# вывод 3

2.4.1 Сгруппированным программам даётся контроль над выполнением операций


# Условие, позволяющее программе понять, запускаться или нетif условие :    # program group# Выполнение повторяется только для элементов массива# Элементы массива присваиваются переменной v в прямом порядкеfor v in массив    # program group# Выполнение продолжается только при соблюдении условийwhile условие:    # program group# Тайминг выполнение группы программ определяется позже# (в первую очередь нужно создать группу программ и дать им имена)def имя группы():    # program group    # После выполнения группы программ, возвращаем результат с помощью return    # Или можно вернуться к началу    return# Ниже описан пример с ошибкой# print(2) с измененным отступом будет относиться к другой группе программ# однако, program group 2 не имеет под собой никакого объясненияif a == 1:    # выполнение группы program group 1 регулируется условием if a==1:    print(1)        # в program group 2 нет регулирующей части, поэтому компилятор выдает ошибку.        print(2)

2.4.2 Формулировка условий


a = 1b = 2c = 2# переменные a и b одинаковы, но a и c разныеif a==b and a!=c:    print("отличается только с")# а больше или равно b (включает а=b) или а>c (не включает а=с)elif a>=b or a>c:    print("а больше или равно b или больше с")# не подходит под условия if и elifelse:    print("кроме этого")# выводит "кроме этого"

2.4.2.1 Другой пример написания условий


a = 1b = 2# если а=b, то v=0, если нет, то v=1v = 0 if a==b else 1print(v)# вывод 1 

2.4.3 Формирование циклов (for)


for v in [0,1,2,3]: # Следующая обработка повторяется для элементов [0,1,2,3]в массиве.    # Проходимся по массиву. Элементы массива выводятся через переменную v в прямом поряке   print(v)   pass # Обозначаем конец отступа# вывод 0 1 2 3# Используя enumerate, можно получить индекс и значение массива в итеративном процессе.for i,v in enumerate([5,3,7,8]):    # i - индекс, v - значение элемента    print("(",i,v,")")# вывод ( 0 5 ) ( 1 3 ) ( 2 7 ) ( 3 8 )# С помощью zip два массива могут быть объединены в один и обработаны итеративно.for v0,v1 in zip([1,2,3,4],[5,6,7,8]):    # v0 содержит элементы массива первого аргумента zip, а v1 содержит элементы второго аргумента.    print("(",v0,v1,")")# вывод ( 1 5 ) ( 2 6 ) ( 3 7 ) ( 4 8 )

2.4.4 Цикл while


a = 3# Пока a больше либо равно 0while a>=0:    print(a)    # Уменьшаем значение a на 1    a -= 1# вывод 3 2 1 0

2.4.5 Функции и методы


# Определение функции: Название - function_name, аргументы: param1 и param2, param2 - аргумент по умолчанию# В случае, если аргумент не установлен, при вызове функции его значение будет равно 1 (значению по умолчанию)def function_name(param1,param2=1):    print("p1:",param1,"p2",param2)    # Эта функция возращает сумму аргументов param1 и param2    return param1 + param2# вызов функции (на данном этапе впервые запускается function_name)# значение param1 = 5,  param2 не устанавливается (используется аргумент по умолчанию)v = function_name(5)# вывод (вывод с помощью оператора print в функции function_name) p1: 5 p2 1print("возвращаемое значение",v)()# вывод "возвращаемое значение 6"

2.4.5.1 Лямба-выражение, пример написания функции


# название функции = lambda (входные аргументы) : (возвращаемое значение) - по такому принципу описывается функция.f = lambda x: x*x# Вызов функции происходит так же, как и в примере с defv = f(2)print(v) # отобразится 4

2.5 Преобразование строки в число


si = "1"   #Строкаsf = "2.3" #Строкаi = 4      #Целое число# Преобразовываем целое число i в строку, происходит конкатенация строкprint(str(i)+si)# 41# Обратная ситуация - преобразовываем строку si в целое число(int), происходит арифметическое сложениеprint(i+int(si))# 5# Преобразовываем строку в число с плавающей запятой (float), опять же происходит сложение (число при выводе автоматически заменяется на float)print(i+float(sf))# 6.3

2.6 Массив, список


# Создание массиваa = [1,1,2,3,2]              # a=[1,1,2,3,2]b = [n for n in range(8)]  # b=[0, 1, 2, 3, 4, 5, 6, 7]# Ссылка# Отрицательные значения становятся индексами, которые выссчитываются с -1 в качестве последнего элементаv = a[0]  # v=1v = a[-1] # v=2# Добавлениеa += [4]     # a=[1, 1, 2, 3, 2, 4]a.append(5)  # a=[1, 1, 2, 3, 2, 4, 5]# Извлечение (перед выполнением a=[1, 1, 2, 3, 2, 4, 5])v = a.pop(0) # v=1, a=[1, 2, 3, 2, 4, 5]# Вырезание (перед выполнением a=[1, 2, 3, 2, 4, 5])# a[первый индекс: предпоследний индекс]# если индексы не прописывать, а оставить просто a[:], первоначальный индекс автоматически станет  равен 0, последний - количество элементов)c = a[1:3] # c=[2, 3]# Максимумминимум (перед выполнением а= [1, 2, 3, 2, 4, 5])mx,mi = max(a),min(a) # mx=5, mi=1# Среднее значение(mean)Медианное значение(median)Наиболее частое значение(mode)Стандартное отклонение(stdev)Дисперсия(variance)# (перед выполнением a=[1, 2, 3, 2, 4, 5])from statistics import mean,median,mode,stdev,variancev = mean(a) # v=2.8333333333333335v = median(a) # v=2.5v = mode(a) # v=2v = stdev(a) # v=1.4719601443879744v = variance(a) #v=2.1666666666666665# Удаление повторений(перед выполнением a=[1, 2, 3, 2, 4, 5])c = set(a) # c={1, 2, 3, 4, 5}# SortedСоздается новый упорядоченный массив(перед выполнением a=[1, 2, 3, 2, 4, 5])c = sorted(a) # c=[1, 2, 2, 3, 4, 5] (массив a остается без изменений)# Sortупорядочивает элементы массива(перед выполнением a=[1, 2, 3, 2, 4, 5])a.sort()      # a=[1, 2, 2, 3, 4, 5]# Одинаково преобразует все элементы массиваmapping (перед выполнением a=[1, 2, 2, 3, 4, 5])# используем лямбда-выражение x: x+2.Значение элемента x меняется на x+2.# list преобразует вывод функции map в массивc = list(map(lambda x:x+2,a)) #c=[3, 4, 4, 5, 6, 7]# Фильтр(перед выполнением a=[1, 2, 2, 3, 4, 5])# используем лямбда-выражение x: x%2==0# Ищется элемент xкоторый делится на два без остатка. Подходящие элементы образуют новый массивc = list(filter(lambda x:x%2==0,a)) #c=[2, 2, 4]# Из всех элементов массива создается одно значение по заданным условиям# x - изначальное значениеесли запускаем цикл, х - значение первого элемента массива# xi - объект текущей операцииfrom functools import reduce# задаем x+xi, получаем сумму всех элементов массиваперед выполнением a=[1, 2, 2, 3, 4, 5]c = reduce(lambda x,xi:x+xi,a) #c=17# задаем max(xi,x), получаем наибольшее значение среди элементов массиваперед выполнением a=[1, 2, 2, 3, 4, 5]c = reduce(lambda x,xi:max(x,xi),a) #c=5

2.7 Словарь (dictionary)ассоциативный массив


# Создание. Ключи - Родной язык, Математика, Английский язык, а значения - массивыd = {    "Родной язык" : [81, 60, 97, 96],    "Математика" : [80, 78, 75, 96],    "Английский язык" : [76, 85, 65, 88],}print(d)# вывод {'Родной язык': [81, 60, 97, 96], 'Математика': [80, 78, 75, 96], 'Английский язык': [76, 85, 65, 88]}# Инициализация с использованием ford1 = {name:[0,1] for name in ["Родной язык","Математика","Английский язык"]}# вывод {'Родной язык': [0, 1], 'Математика': [0, 1], 'Английский язык': [0, 1]}# Получение значений через указание ключаa = d["Математика"] # a=[80, 78, 75, 96]# Замена значений через укзание ключаПерезаписьd["Английский язык"] = [77, 61, 91, 87] # d["Английский язык"]=[77, 61, 91, 87]# Цикл данныхfor k,v in d.items():    print(k,v)# вывод Родной язык [81, 60, 97, 96] Математика [80, 78, 75, 96] Английский язык [77, 61, 91, 87]# Максимальное значениеМинимальное значениев случае с ключом Родной языкjmx,jmi = max(d["Родной язык"]),min(d["Родной язык"])#print(jmx,jmi)# Находим название предмета с наивысшим средним значениемfrom statistics import mean# В массивах типа словарь при использовании max можно в качестве ключевого аргумента указать функцию, определяющую, среди чего искать максимальное значение# В этом случае лямбда-выражение используется для вычисления среднего значения из аргумента k(значение ключа в словаре)kmx = max(d,key=lambda k:mean(d[k])) # kmx="Родной язык"

2.8 Файловая операция with open


# with, файловая операция# with автоматизирует завершение процессов, требующих запуска и завершения.# Например, файл, открытый с помощью open, необходимо закрыть с помощью вызова функции close # Если использовать with и открыть файл с помощью open, при выходе из диапазона with функция close вызовется автоматически.# mode - r: чтениеw: записьa: дополнительная запись# Возвращаемое значение, которое мы получаем с помощью open, назовем fwith open("09_memo.txt",mode="a") as f:    # Записываем в файл    f.write("\n")# Чтение файлаwith open("09_memo.txt","r") as r:    print(r.read())

2.9 Случайное число random


# Использование случайных чиселimport random# Инициализация генерации случайных чисел# Случайные числа позволяют генерировать одну и ту же последовательность случайных чисел несколько раз для экспериментальной воспроизводимости.# Если аргумент seed указать численно, всегда будет выводиться одно и то же число.random.seed(0)# Генерация случайных чисел больше или равных 0 и меньше 1.print(random.random()) # Вывод 0.8444218515250481# Создает случайные целые числа больше первого аргумента и меньше второго аргумента.print(random.randint(1,3)) # Вывод одного числа из 1,2,3# Произвольно выбирает число из массиваprint(random.choice([0,1,2])) # Вывод одного числа из 0,1,2# Извлечение нескольких данных из массива# Вторым аргументом указывается количества выводимых данныхprint(random.sample([0,1,2],2)) # Выводимый массив [1, 2]# Перемешиваем данные в массиве# Новый массив не создается - данные перемешиваются непосредственно в массиве аa = [0,1,2]random.shuffle(a)print(a) # вывод [0, 2, 1]print(random.sample(a,len(a))) # Для создания нового массива можно использовать sample


Спасибо за прочтение!

Мы будем очень рады, если вы расскажете нам, понравилась ли вам данная статья, понятен ли перевод, была ли она вам полезна?
Подробнее..

Java Core для самых маленьких. Часть 1. Подготовка и первая программа

11.02.2021 22:07:58 | Автор: admin

Вступление. Краткая история и особенности языка.

Как-то давно мы с моим товарищем и коллегой Егором готовили обучающий курс по Java Core. Но как-то не срослось и это дело не было доведено до какого-либо логического конца. И вот, спустя время, я решил, что не стоит пропадать добру и по-этому запускаю серию статей про Java Core для самых маленьких.

Начало разработки языка было положено еще в 1991 году компанией Sun Microsystems, Inc. Вначале язык был назван Oak (Дуб), но в 1995 он был переименован в Java. Публично заявили о создании языка в 1995 году. Причиной создания была потребность в независящем от платформы и архитектуры процессора языке, который можно было бы использовать для написания программ для бытовой электротехники. Но поскольку в таких устройствах применялись различные процессоры, то использование популярных на то время языков С/С++ и прочих было затруднено, поскольку написанные на них программы должны компилироваться отдельно для конкретной платформы.

Особенностью Java, которая решила эту проблему, стало то, что компилятор Java выдает не машинный исполняемый код, а байт-код - оптимизированный набор инструкций, которые выполняются в так называемой виртуальной машин Java (JVM - Java Virtual Machine). А на соответствующую платформу предварительно устанавливается JVM с необходимой реализацией, способная правильно интерпретировать один и тот же байт-код. У такого подхода есть и слабые стороны, такие программы выполняются медленнее, чем если бы они были скомпилированы в исполняемый код.

Установка программного обеспечения - JDK

В первую очередь, нам нужно установить на компьютер так называемую JDK (Java Development Kit) - это установочный комплект разработчика, который содержит в себе компилятор для этого языка и стандартные библиотеки, а виртуальную машинуJava (JVM) для вашей ОС.

Для того чтобы скачать и установить JDK открываем браузер, и в строке поиска Google вводим download JDK или переходим по этой ссылке.

Скролим ниже и находим таблицу с вариантами скачивания JDK. В зависимости от нашей операционной системы выбираем файл для скачивания.

Процесс установки для ОС Windows имеет несколько этапов. Не стоит пугаться, все очень просто и делается в несколько кликов. Вот здесь подробно описан процесс установки. Самое важное для пользователей Windows это добавить системную переменную JAVA_HOME. В этой же статье достаточно подробно расписано как это сделать (есть даже картинки).

Для пользователей MacOS также стоит добавить переменную JAVA_HOME. Делается это следующим образом. После установки .dmg файла JDK переходим в корневую папку текущего пользователя и находим файл .bash_profile. Если у вас уже стоит zsh то ищем файл .zshenv. Открываем этот файл на редактирование и добавляем следующие строки:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Homeexport PATH=${PATH}:${JAVA_HOME}

Здесь обратите внимание на версию JDK указанную в пути - jdk1.8.0_271.jdk. Могу предположить, что у вас она будет отличаться, поэтому пройдите по указанному пути и укажите свою версию. Сохраняем изменения и закрываем файл, он нам больше не понадобится.

Теперь важно проверить правильность установки JDK. Для этого открываем командную строку, в случае работы на Windows, или терминал для MacOS. Вводим следующую команду: java -version Если вы все сделали правильно, вы увидите версию установленного JDK. В ином случае вы, скорее всего, допустили где-то ошибку. Советую внимательно пройтись по всем этапам установки.

Установка IDE

Теперь нам нужно установить среду разработки, она же IDE (Integrated development environment). Что собой представляет среда разработки? На самом деле она выглядит как текстовый редактор, в котором мы можем вводить и редактировать текст. Но помимо этого, этот текстовый редактор умеет делать проверку синтаксиса языка на котором вы пишете. Делается это для того чтобы на раннем этапе подсказать вам о том, что вы допустили ошибку в своем коде.

Также среда разработки содержит в себе компилятор. Компилятор - это специальный инструмент, который будет превращать код, который вы пишете, в машинный код или близкий к машинному коду.

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

Для начала нам нужно выбрать и среду разработки. Их довольно таки много, и самыми популярными из них являются: IntelliJ IDEA, NetBeans, Eclipse. Для себя я выбираю IntelliJ IDEA. Она является самой удобной на мой взгляд, и хоть она и платная, на официальном сайте можно найти бесплатную версию которая называется Community. Этой версии будет вполне достаточно для изучения основ Java. Вообщем будем работать в IntelliJ IDEA.

Итак, открываем браузер, в поисковой строке вводим "Download IntelliJ IDEA Community" или переходим по этой ссылке. Выбираем версию ОС и качаем версию Community.

В установке IntelliJ IDEA нет ничего военного. На крайний случай на ютубе есть множество видео о том, как установить эту программу.

Первая программа

Теперь мы готовы создать нашу первую программу. В окошке запустившийся IDE нажимаем New Project.

В новом окошке в левой панели выбираем Java.

Обратите внимание! В верхнем окошке, справа, возле надписи "Project SDK:" должна находится версия Java, которую вы установили вместе с JDK. Если там пусто, то вам нужно будет указать путь к вашему JDK вручную. Для этого в выпадающем списке нажмите "Add JDK..." и укажите путь к вашему JDK, который был предварительно установлен.

Теперь можем нажать на кнопку Next. В следующем окошке, вверху, поставьте галочку Create project from template и выберите Command Line App. И снова нажимаем Next.

Дальше нам нужно указать имя программы. У меня это будет Hello World, желательно чтобы имя проекта было введено латиницей, и на английском языке.

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

После указываем путь к проекту программы.

Далее, нам нужно указать базовый пакет нашей программы. О пакетах я расскажу вам позже, обычно компании используют свое имя Интернет-домена в обратном порядке, но вы можете написать, например, свои имя и фамилию через точку в нижнем регистре (маленькими буквами), тоже латиницей. Я же использую псевдоним. Когда все поля будут заполнены - нажимаем Finish.

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

Это окно, то что вы будете видеть 80-90%, а иногда и 100% времени, работая программистом.

Для того чтобы закончить ваше первое приложение, останется добавить строчку кода System.out.print("Hello world!"); как показано на скриншоте.

Чтобы скомпилировать и запустить на выполнение вашу программу, вам нужно нажать кнопочку с зеленым треугольничком на верхней панели справа, или в меню найти пункт Run -> Run Main. И внизу на нижней панели, под окном редактора, в консоли, вы увидите результат выполнения вашей программы. Вы увидите надпись Hello World! Поздравляю, вы написали свою первую программу на Java.

Разбираем первую программу

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

Пройдемся по порядку:

В начале мы видим package com.zephyr.ventum; - это объявление пакета, и это постоянный атрибут файлов с исходным кодом в Java. Простыми словами, это локация вашего файла в проекте и любой .java файл должен начинаться с подобной строки.

Ниже, public class Main { - это стандартное объявление класса в Java, где public - это модификатор доступа который дает программисту возможность управлять видимостью членов класса, class - является ключевым словом объявляющим класс, Main - это имя класса. Все определение класса и его членов должно располагаться между фигурными скобками { }. Классы мы рассмотрим немного позже, только скажу что в Java все действия программы выполняются только в пределах класса.

Ключевое слово - это слово зарезервированное языком программирования. Например, package - это тоже ключевое слово.

Еще ниже, public static void main(String[] args) { - эта строка является объявлением метода main. Метод (или часто говорят функция) main это точка входа в любой java-программер. Именно отсюда начинается выполнение вашего кода. В проекте может быть несколько методов main, но мы должны выбрать какой-то один для запуска нашей программы. В следующих статьях мы еще вернемся к этому. Сейчас же у нас только один метод main.

Фигурные скобки {} у метода main обозначаю начало и конец тела метода, весь код метода должен располагаться между этими скобками. Аналогичные скобки есть и у класса Main.

Следующая строка является // write your code here однострочным комментарием.

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

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

/* write  your  code here */

Мы просто располагаем несколько строк между символами /* и */

System.out.print("Hello world!"); - строка которая находится внутри метода main является командой, которая выводит в консоль строку "Hello world!"

Обратите внимание что в конце стоит точка с запятой, в языке Java команды должны заканчиваться точкой с запятой.

Затем мы закрываем тело нашего метода main } а также закрываем класс Main }.

На этом статья подходит к концу. Автором конкретно этого материала является Егор и все уменьшительно ласкательные формы слов сохранились в первозданном виде.

В следующей статье мы поговорим о типах данных в Java.

Подробнее..

Java Core для самых маленьких. Часть 2. Типы данных

15.02.2021 14:04:42 | Автор: admin

Вступление

В этой статье мы не будем использовать ранее установленную IDE и JDK. Однако не беспокойтесь, ваш труд не был напрасным. Уже в следующей статье мы будет изучать переменные в Java и активно кодить в IDEA. Эта же статья является обязательным этапом. И в начале вашего обучения вы, возможно, будете не раз к ней возвращаться.

1998 - пин-код от моей кредитки является ничем иным как числом. По-крайней мере для нас - для людей. 36,5 - температура, которую показывают все термометры в разных ТРЦ. Для нас это дробное число или число с плавающей запятой. "Java Core для самых маленьких" - а это название данной серии статей, и мы воспринимает это как текст. Так к чему же я веду. А к тому, что Джаве (так правильно произносить, на тот случай если кто-то произносит "ява"), как и человеку, нужно понимать с чем она имеет дело. С каким типом данных предстоит работать.

Фанаты матрицы и, надеюсь, остальные читатели знают, что на низком уровне, вся информация в ЭВМ представлена в виде набора нулей и единиц. А вот у человеков, на более высоком уровне, есть высокоуровневые языки программирования. Они не требуют работы с нулями и единицами, предоставляя возможность писать код понятный для людей. Одним из таких языков программирование и является Java. Мало того, Java - это строго-типизированный язык программирования. А еще бывают языки с динамической типизацией данных (например Java Script). Но мы здесь учим нормальный язык программирования, поэтому не будем отвлекаться.

Что для нас означает строгая типизация? Это значит, что все данные и каждое выражение имеет конкретный тип, который строго определен. А также то, что все операции по передаче данных будут проверяться на соответствие типов. Поэтому давайте поскорее узнаем какие типы данных представлены в Java!

Примитивы

В языке Java существует 8, оскорбленных сообществом, примитивных типов данных. Их также называют простыми. И вот какие они бывают:

  • Целые числа со знаком: byte, short, int, long;

  • Числа с плавающей точкой: float, double;

  • Символы: char;

  • Логические значения: boolean.

В дальнейшем комбинируя эти самые примитивы мы сможем получать более сложные структуры. Но об этом нам еще рано беспокоиться. Сейчас же рассмотрим каждый из примитивов подробнее.

Тип byte

Является наименьшим из целочисленных. 8-разрядный тип данных c диапазоном значений от -2^7 до 2^7-1. Или простыми словами может хранить значения от -128 до 128. Используется для работы с потоками ввода-вывода данных, эта тема будет рассмотрена позже.

Тип short

16-разрядный тип данных в диапазоне от -2^15 до 2^15-1. Может хранить значения от -32768 до 32767. Самый редко применяемый тип данных.

Тип int

Наиболее часто употребляемый тип данных. Содержит 32 разряда и помещает числа в диапазоне от -2^31 до 2^31-1. Другими словами может хранить значения от -2147483648 до 2147483647.

Тип long

64-разрядный целочисленный тип данных с диапазоном от -2^63 до 2^63-1. Может хранить значения от -9223372036854775808 до 9223372036854775807. Удобен при работе с большими целыми числами.

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

Тип float

32-разрядный тип данных с плавающей точкой. Требует в два раза меньше памяти и в некоторых процессорах выполняется быстрее по сравнению с double. Но если значения слишком велики или слишком малы, то не обеспечивает требуемую точность вычислений. Используется когда нужно число с дробной частью, но без особой точности.

Тип double

На хранение требуется 64 бита. Рационально пользоваться double когда нужно сохранить точность многократно повторяющихся вычислений или манипулировать большими числами.

Тип char

16-разрядный тип данных в диапазоне от 0 до 2^16. Хранит значения от 0 до 65536. Этот тип может хранить в себе полный набор международных символов на всех известных языках мира (кодировка Unicode). То есть по сути каждый символ представляет из себя какое-то число. А тип данных char позволяет понять, что это число является символом.

Тип boolean

Может принимать только 2 значения true или false. Употребляется в условных выражениях, к примеру 1 > 10 вернет false, а 1 < 10 - true.

На этом примитивные типы данных в Java закончились. В следующей статье мы будем объявлять переменные конкретного типа данных. Поговорим о том, что такое литералы. А еще узнаем, что такое приведение типов данных. Вообщем следующая статья будет очень насыщенной и познавательной!

Подробнее..

Как мы загружали банковскую карту из iPhone в брелок

18.08.2020 14:15:47 | Автор: admin

С каждым годом всё больше компаний проявляют интерес к проектам, связанным с интернетом вещей (Internet of Things, IoT).

В статье я расскажу о созданной нами IoT платформе, о способах загрузки банковских карт в носимые устройства, об исследовании возможностей фреймворка Core NFC iOS и о возможной схеме мошенничества с использованием смартфонов с NFC.

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

Привет, Хабр!


Меня зовут Максим. Промышленной разработкой я занимаюсь с 2005 года. В Кошельке работаю с 2013 года, а с 2015 года помогаю бизнесу компании развивать новые финтех-сервисы в качестве руководителя подразделения.

В Кошельке наша команда запустила немало инновационных продуктов. Это и одна из первых в мире полностью виртуальная банковская карта в смартфоне с возможностью бесконтактной оплаты (за год до запуска Apple Pay в России и задолго до запуска Apple Card), и первая транспортная карта, и первая карта болельщика, и первая кампусная карта в смартфоне.

В прошлом году мы совместно с Mastercard запустили сервис Кошелёк Pay единственный в мире сервис, который, в отличие от аналогов, работает независимо от производителя смартфона или операционной системы. Например, Кошелёк Pay работает на смартфонах Huawei, на которых отсутствуют сервисы Google.

Благодарности


Коле Ашанину, который замотивировал меня на написание статьи и помогал по ходу её подготовки к публикации.

Саше Прыймак, который под моим руководством выполнил описанное в статье исследование.

Также большое спасибо за участие и поддержку:
Кате Туркиной, Антону Давыдову, Лёше Ершову, Даше Алексеенко.

Платформа IoT


На данный момент мы с командой работаем над запуском платформы интернета вещей, которая сможет дополнить и расширить существующий опыт использования сервиса Pay и внедрить оплату (и другие сервисы идентификации) в те вещи, которые мы обычно носим с собой так называемые носимые устройства.

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

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

Пример носимых устройств умные часы, фитнес-браслеты, кольца, брелоки.

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

Умные вещи сейчас мировой тренд. Об этом свидетельствуют собранные различными мировыми агентствами статистические данные (см. ссылки в конце статьи).

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

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

  1. Элемент безопасности, или Secure Element это полноценный компьютер, выполненный в цельном кристалле кремния размером около 5-20 квадратных миллиметров. Он имеет свою операционную систему, систему ввода-вывода, центральный процессор, несколько крипто-процессоров для реализации криптографических операций, оперативную и постоянную память. Элементы безопасности используют при производстве банковских карт, SIM-карт, а также встраивают в смартфоны и другие устройства. Элемент безопасности практически невозможно взломать и получить оттуда данные (отсюда и название).

    Как и на любой компьютер, в элемент безопасности можно установить приложения так называемые апплеты. Мы в нашей статье будем работать с платежным апплетом, благодаря установке и персонализации которого носимое устройство с элементом безопасности и имеет сервис бесконтактной оплаты.
  2. Стандарт GlobalPlatform Card Specification он описывает работу операционной системы элемента безопасности в целом, а также сценарии и протоколы безопасного управления содержимым элемента безопасности.
  3. TSM (Trusted Service Manager) сервис для управления содержимым в элементе безопасности. Он управляет жизненным циклом апплетов и их персонализацией под конкретного пользователя на конкретном элементе безопасности.
  4. Для превращения носимого устройства в платежный инструмент платежными системами применяется технология токенизации по стандарту EMV это процесс получения от платежной системы токена (суррогатного номера), связанного с номером реальной банковской карты. Для каждой банковской карты, в связке с форм-фактором устройства оплаты, токен всегда уникален, что обеспечивает дополнительную безопасность при оплате токеном.

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

Первый сценарий это взаимодействие с активными носимыми устройствами. Активными называют носимые устройства, в которых есть свой элемент питания (например, аккумулятор). Как правило, внутри вещи работает своя операционная система и имеется модуль BLE для связи со смартфоном. Производитель устройства предоставляет SDK и ключи доступа для взаимодействия с элементом безопасности.

Именно так работают все умные часы и фитнес-браслеты с функцией бесконтактной оплаты.Тут всё просто и понятно берем и делаем.

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

Проблема заключается в том, что нужно загрузить в пассивное носимое устройство свою банковскую карту из приложения на смартфоне.

Этот сценарий мы (условно) разбиваем уже по типу смартфонов:

  1. Любые смартфоны без NFC
  2. Смартфон Android c NFC
  3. iPhone c NFC

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

Реализация терминала персонализации может быть разной: может быть тот же смартфон пользователя, подключенный к внешнему считывателю смарт-карт по BLE или USB, а может быть и автономное внешнее устройство (полноценный компьютер с подключенным к нему считывателем, выходом в интернет и управляющим программным обеспечением).

Для второго типа (Android c NFC) реализация понятна. Смартфон в этом случае можно использовать в качестве терминала, запитать пассивное устройство от NFC-антенны и загрузить в него токен банковской карты.

В нашем исследовании я подробно распишу, как мы прорабатывали третий тип смартфонов (iPhone с NFC). В качестве носимых устройств мы использовали брелки от компании ISBC партнера, с которым мы запускаем пилот.

Цель исследования


Можем ли мы дать возможность пользователю Кошелька на платформе iOS загрузить свою банковскую карту в носимое устройство, приложив его к iPhone?

То есть:

  1. Пользователь в приложении приложении Кошелёк вводит данные своей банковской карты
  2. Пользователь прислоняет к задней стенке iPhone носимое устройство
  3. Банковская карта загружается в носимое устройство

Соответственно, техническая задача определить, можно ли во внешний элемент безопасности (Secure Element) загрузить банковскую карту, используя обычный iPhone и его NFC антенну, через протокол ISO/IEC 7816 (T=CL).

Дополнительные задачи:

  1. Получить ATR (Answer To Reset) чипа, не убирая его от считывателя
  2. Получить UID (Unique Identifier) чипа

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

Что имеем:

  • iPhone 8 c iOS 13.5
  • Тестовое носимое устройство брелок ISBC со встроенным чипом JCOP 3 EMV P60 и загруженными апплетами производства NXP:
    PPSE и апплет, реализующий спецификацию M/Chip Advance Card Specification Payment and Data Storage v1.1;
  • ключи от Issuer Security Domain чипа.

Решение


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

  1. Установление соединения чипа с NFC модулем iPhone
  2. Установка на чип апплетов Mastercard M/Chip Advance и PPSE
  3. Персонализация апплетов

Установление соединения


Именно здесь речь пойдет о фичах фреймворка Core NFC, добавленных в iOS 13.
Кстати, в iOS 14 никаких существенных изменений относительно предмета статьи не случилось, поэтому все описанное актуально и для нее.

Итак, в тринадцатой версии яблочной ОС стало возможным не только считывать данные с NFC меток, как это было в iOS 12 (но не раньше iOS 11, до нее взаимодействие по NFC было возможно только в рамках Apple Pay), но и записывать их, а также общаться на языке APDU-команд с любым чипом, который соответствует одному из следующих стандартов:


Для этого в Core NFC были добавлены два новых класса: NFCNDEFReaderSession и NFCTagReaderSession.

Первый используется для взаимодействия с NDEF метками, а второй для всего остального, соответственно.

В нашем случае это чип, поддерживающий спецификацию GlobalPlatform Card Specification 2.2.1 и стандарт ISO/IEC 7816, значит, будем использовать второй класс.

В документации написано, что нужно сделать (помимо написания кода, конечно), чтобы начать общение с чипом по ISO 7816:


Но ниже есть вот такое интересное ограничение:

Important
Core NFC doesn't support payment-related Application IDs.

Как раз его мы и хотим пощупать, узнав, что конкретно оно означает.

Добавляем строку, например Allow NFC connection для ключа NFCReaderUsageDescription в файле info.plist. С любым другим значением этого ключа тоже работает.


[Здесь в колонке слева не сам ключ, а его описание, XCode прячет формальные названия]

Дальше, если мы хотим взаимодействовать с чипом, как с устройством стандарта ISO/IEC 7816, то в значении ключа com.apple.developer.nfc.readersession.iso7816.select-identifiersукажем список ID всех апплетов (Application Identifier или AID), с которым будет взаимодействовать приложение.


Здесь стоит пояснить, что эти идентификаторы не просто случайный набор символов.
Это шестнадцатеричные (hex) строки, содержащие информацию о приложении, которому они присвоены.

AIDы могут быть длиной от 5 до 16 байт (два символа в строке = один байт). Они состоят из двух частей, первая определяет провайдера приложения (для Mastercard это A000000004), вторая говорит, какой именно это продукт данного провайдера (для продукта с именем Mastercard это 1010, а, например, для Maestro это 3060).

Кроме того, иногда в AID требуется поместить дополнительную информацию, например, если на чипе находятся два одинаковых приложения от одного провайдера, но для разных банков. Для этого существует поддержка Long AID (или Extended AID). До тех пор, пока длина AID не превышает 16 байт, в него можно записывать все, что угодно. Например, мы взяли Mastercard AID и в конце дописали к нему TEST, итог: A0000000041010BB5445535401.

Единственный AID, который выбивается из списка 325041592E5359532E444446303101.
На самом деле это обычная (только в hex-формате), что называется, plain-text строка 2PAY.SYS.DDF01. Это AID PPSE, который платежным апплетом, как таковым, не является. Он лишь содержит данные окружения, необходимые платежным приложениям.

Установка апплетов


Для установки апплетов на чип необходимо защищенное соединение (Secure Channel Protocol или SCP); мы сделали это за кадром с помощью обычного PC/SC считывателя и платформы Cardsmobile TSM.

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

Понадобится любая IDE с поддержкой JCOP Shell и эмулятором JavaCard, например, вот эта.

Создаем пустой проект, указываем желаемый AID (например 0000000000) и запускаем.

Дальше разбираемся по шагам:

  1. /card
    Получаем ATR, отправляем SELECT без идентификатора, чтобы был выбран Card Manager;

  2. auth
    Создаем защищенное соединение с эмулятором, иначе ничего установить не получится;

  3. ls (опционально)
    С помощью этой команды можно увидеть, какие приложения установлены на вашем девайсе/эмуляторе;

  4. install [packageAID] [appletAID] [instanceAID]
    Устанавливаем апплет:
    packageAID идентификатор пакета (Module), например, 0000000000
    appletAID идентификатор апплета (Load File), например, 000000000000
    instanceAID идентификатор, который будет присвоен вашему апплету после установки, например, A0000000041010;

  5. ls
    Проверяем, появился ли ваш апплет в списке установленных приложений:


Персонализация апплетов


На самом деле, персонализация апплета очень простая штука; всё, что требуется, это загрузить в него необходимые платежные данные. Для этого нужно выбрать апплет командой SELECT по его AID, установить защищенное соединение и отправить выбранному апплету команды STORE DATA с данными внутри.

Теперь вернемся к списку AIDов в файле info.plist зачем он нужен, и как конкретно Core NFC выбирает, с каким апплетом взаимодействовать?

Выглядит это примерно так:

  1. Программа идет по списку сверху вниз;
  2. Для каждого AID она формирует и отправляет команду SELECT;
  3. AID первого апплета, ответившего 9000 (статус успешного ответа, здесь список всех возможных ответов) записывается в поле initialSelectedAID объекта типа NFCISO7816Tag, который кладется в массив обнаруженных чипов

@available(iOS 13.0, *)public protocol NFCISO7816Tag : NFCNDEFTag, __NFCTag {/*** @property initialSelectedAID The Hex string of the application identifier (DF name) selected by the reader when the tag is discovered.*               This will match one of the entries in the com.apple.developer.nfc.readersession.iso7816.select-identifiers*               in the Info.plist.*/@available(iOS 13.0, *)var initialSelectedAID: String { get }


Дальше из массива можно выбрать любой такой объект, и с помощью метода sendCommand отправлять APDU-команды выбранному апплету.

А теперь поговорим об этом ограничении:

Core NFC doesn't support payment-related Application IDs.

То есть Core NFC не поддерживает платежные AIDы, а именно боевые, с которыми работают платежные терминалы.

Конечно, платежный AID в список info.plist добавить можно, вот только Core NFC его проигнорирует и не будет отправлять для него SELECT (кстати, здесь список всех использующихся AID'ов). Apple таким образом защищают свою технологию Apple Pay, закрывая сторонним разработчикам доступ к любым платежным функциям iPhone (и всему, что с этим связано).

Обходные пути


Первое, что приходит в голову а можно ли добавить в info.plist не AID платежного апплета, а AID Card Managerа (Card Manager это группа сервисов внутри операционной системы чипа, управляющих картой, которые отвечают за администрирование и безопасность), чтобы потом вручную послать ему команду SELECT с AID нужного апплета?

Здесь мы споткнулись о первый подводный камень Core NFC не позволяет отправлять команду SELECT, содержащую AID, который не прописан в info.plist.

Хорошо, добавили A0000000041010, но и тут неудача Core NFC не позволяет отправлять команду SELECT, содержащую платежный AID, вне зависимости от того, есть он в info.plist или нет.

Разберемся, как именно работает ограничение по идентификаторам.

В info.plist мы указали следующие AIDы:
1. A000000001510000 - GlobalPlatform Card Manager AID
2. 325041592E5359532E444446303101 - Proximity Payment System Environment (PPSE)
3. A0000000041010 - Mastercard Credit/Debit (Global)
4. A00000000401 - Mastercard PayPass
5. A00000000410101213 - Mastercard Credit
6. A00000000410101215 - Mastercard Credit
7. A00000000410101214 - Придуманный платежный AID
8. A00000000410101216 - Придуманный платежный AID
9. A0000000041010121F - Придуманный платежный AID
10. A0000000041010BB5445535401 - Придуманный платежный Long AID
11. A0000000041010BB5445535405 - Придуманный платежный Long AID
12. A000000004101FBB5445535401 - Придуманный не платежный AID
13. A000000004101F1213 - Придуманный не платежный AID
14. A00000000F1010 - Придуманный не платежный AID
15. A0000000040F - Придуманный не платежный AID

Мы установили 14 платежных апплетов с разными AID (пп. 2-11 платежные AID-ы), и попробовали отправить Card Manager команды SELECT с каждым из этих AID.

Ответили номера 12-15.

Получается, что ограничение накладывается именно на некий префикс AID, наличие которого и определяет, платежный это идентификатор или нет.

Жаль, но этот способ отпадает.

Второй способ персонализации, предусмотренный GlobalPlatform, это команда INSTALL [for personalization].


Она отправляется в Card Manager и содержит AID апплета, который нужно персонализировать.

После этого можно отправлять команды STORE DATA в Card Manager, а он будет пересылать их в целевое приложение.

Но есть одно ограничение. Для того, чтобы апплет поддерживал такой способ персонализации, он должен реализовывать интерфейс org.globalplatform.Application.

Card Manager, на команду INSTALL [for personalization] с Mastercard Credit/Debit (Global) AID, который был присвоен апплету M/Chip Advance от NXP, отвечал ошибкой 6985 (Conditions of use not satisfied),

а значит надо проверить, реализует ли он интерфейс Application.

Для этого мы написали простое приложение-пустышку, реализующее этот интерфейс. Как и ожидалось, на INSTALL [for personalization] оно ответило 9000.

Но когда Application был убран из интерфейсов, реализуемых приложением, оно стало отвечать на эту команду 6985, как и в случае с апплетом M/Chip Advance.

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

Дополнительные задачи


  1. Получение UID чипа
    Это сделать можно, очень простым способом.
    При старте сессии NFC модуль ищет чипы, AID'ы апплетов которых прописаны в info.plist, и складывает их в массив.
    После этого любой из них можно оттуда достать, и если его тип NFCISO7816Tag, то у него есть поле identifier, в котором и находится UID чипа.
    /** * @discussion The hardware UID of the tag.*/var identifier: Data { get }
    

  2. Получение ATR чипа
    А вот ATR, похоже, Core NFC получать не умеет, потому что во фреймворке для этого нет отдельных инструментов, а с помощью APDU-команд ATR получить нельзя.

Выводы


Что же в сухом остатке?

  1. Мы научились работать с чипами стандарта ISO/IEC 7816 (в том числе и получать их UID), используя новые возможности фреймворка Core NFC;
  2. Разобрались с ограничениями, наложенными Apple на свою технологию;
  3. Выяснили, что на данный момент, используя iPhone, персонализировать апплеты с платежными идентификаторами, при этом не реализующие интерфейс Application невозможно;
  4. Взяли на заметку, что с любыми другими AID все работает на ура эти знания пригодятся нам в будущем при работе с нефинансовыми сервисами.

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

В рамках поставленной задачи мы остановились на использовании внешнего BLE считывателя для загрузки токена банковской карты из iPhone в пассивное носимое устройство.

Надеемся, что в будущем Apple расширит возможности NFC Core для выполнения этой и ряда других задач, связанных с токенизацией и оплатой в сторонних приложениях. Впрочем, недавняя новость сигнализирует, что это вряд ли случится в ближайшем будущем.

Побочный результат исследования


В ходе проведения работ родилась схема потенциального мошенничества, которую нельзя воспроизвести посредством смартфонов Apple,

но вполне можно реализовать через Android-смартфон с поддержкой NFC и технологией Host Card Emulation.

Суть вкратце:

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

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

  1. Первый смартфон прикладывается к любой банковской карте;
  2. Второй прикладывается к платежному терминалу;
  3. Второй смартфон эмулирует банковскую карту, пересылая APDU-команды, посылаемые терминалом, первому смартфону;
  4. Первый смартфон транслирует эти команды приложенной банковской карте, передавая ее ответы второму смартфону;
  5. Второй смартфон передает полученные ответы терминалу;
  6. Платеж произведен.

То есть потенциально мошенник может приложить смартфон к вашему карману и оплатить покупку. Будьте осторожны!

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

Вместо заключения


Статью я старался писать простым языком, не сильно углубляясь в терминологию предметной области. В ней я описал один из инновационных проектов, над которым мы работаем, предметную область и небольшое исследование новых возможностей iOS фреймворка Core NFC.

Приветствую любую обратную связь:
вопросы, отзывы, комментарии, дополнения, конструктивную критику.

Полезные ссылки


Если вас заинтересовала тема, описанная в статье, то ниже несколько ссылок для более подробного изучения:
  1. Книга И. М. Голдовского Банковские Микропроцессорные Карты
  2. Концепция EMV Payment Tokenisation
  3. Статьи с анализом рынка IoT:

Подробнее..

OpenCV в Python. Часть 1

16.09.2020 22:19:36 | Автор: admin

Привет, Хабр! Запускаю цикл статей по библиотеке OpenCV в Python. Кому интересно, добро пожаловать под кат!


my_logo


Введение


OpenCV это open source библиотека компьютерного зрения, которая предназначена для анализа, классификации и обработки изображений. Широко используется в таких языках как C, C++, Python и Java.


Установка


Будем считать, что Python и библиотека OpenCV у вас уже установлены, если нет, то вот инструкция для установки python на windows и на ubuntu, установка OpenCV на windows и на ubuntu.


Немного про пиксели и цветовые пространства


Перед тем как перейти к практике, нам нужно разобраться немного с теорией. Каждое изображение состоит из набора пикселей. Пиксель это строительный блок изображения. Если представить изображение в виде сетки, то каждый квадрат в сетке содержит один пиксель, где точке с координатой ( 0, 0 ) соответствует верхний левый угол изображения. К примеру, представим, что у нас есть изображение с разрешением 400x300 пикселей. Это означает, что наша сетка состоит из 400 строк и 300 столбцов. В совокупности в нашем изображении есть 400*300 = 120000 пикселей.
В большинстве изображений пиксели представлены двумя способами: в оттенках серого и в цветовом пространстве RGB. В изображениях в оттенках серого каждый пиксель имеет значение между 0 и 255, где 0 соответствует чёрному, а 255 соответствует белому. А значения между 0 и 255 принимают различные оттенки серого, где значения ближе к 0 более тёмные, а значения ближе к 255 более светлые:


4850884 91136851 P7DI0Ak0 greyscalesteps0255


Цветные пиксели обычно представлены в цветовом пространстве RGB(red, green, blue красный, зелёный, синий), где одно значение для красной компоненты, одно для зелёной и одно для синей. Каждая из трёх компонент представлена целым числом в диапазоне от 0 до 255 включительно, которое указывает как много цвета содержится. Исходя из того, что каждая компонента представлена в диапазоне [0,255], то для того, чтобы представить насыщенность каждого цвета, нам будет достаточно 8-битного целого беззнакового числа. Затем мы объединяем значения всех трёх компонент в кортеж вида (красный, зеленый, синий). К примеру, чтобы получить белый цвет, каждая из компонент должна равняться 255: (255, 255, 255). Тогда, чтобы получить чёрный цвет, каждая из компонент должна быть равной 0:
(0, 0, 0). Ниже приведены распространённые цвета, представленные в виде RGB кортежей:
Снимок экрана от 2020-08-31 01-29-26


Импорт библиотеки OpenCV


Теперь перейдём к практической части. Первое, что нам необходимо сделать это импортировать библиотеку. Есть несколько путей импорта, самый распространённый это использовать выражение:


import cv2

Также можно встретить следующую конструкцию для импорта данной библиотеки:


from cv2 import cv2

Загрузка, отображение и сохранение изображения


def loading_displaying_saving():    img = cv2.imread('girl.jpg', cv2.IMREAD_GRAYSCALE)    cv2.imshow('girl', img)    cv2.waitKey(0)    cv2.imwrite('graygirl.jpg', img)

Для загрузки изображения мы используем функцию cv2.imread(), где первым аргументом указывается путь к изображению, а вторым аргументом, который является необязательным, мы указываем, в каком цветовом пространстве мы хотим считать наше изображение. Чтобы считать изображение в RGB cv2.IMREAD_COLOR, в оттенках серого cv2.IMREAD_GRAYSCALE. По умолчанию данный аргумент принимает значение cv2.IMREAD_COLOR. Данная функция возвращает 2D (для изображения в оттенках серого) либо 3D (для цветного изображения) массив NumPy. Форма массива для цветного изображения: высота x ширина x 3, где 3 это байты, по одному байту на каждую из компонент. В изображениях в оттенках серого всё немного проще: высота x ширина.
С помощью функции cv2.imshow() мы отображаем изображение на нашем экране. В качестве первого аргумента мы передаём функции название нашего окна, а вторым аргументом изображение, которое мы загрузили с диска, однако, если мы далее не укажем функцию cv2.waitKey(), то изображение моментально закроется. Данная функция останавливает выполнение программы до нажатия клавиши, которую нужно передать первым аргументом. Для того, чтобы любая клавиша была засчитана передаётся 0. Слева представлено изображение в оттенках серого, а справа в формате RGB:


concatenate_two_girl


И, наконец, с помощью функции cv2.imwrite() записываем изображение в файл в формате jpg(данная библиотека поддерживает все популярные форматы изображений:png, tiff,jpeg,bmp и т.д., поэтому можно было сохранить наше изображение в любом из этих форматов), где первым аргументом передаётся непосредственно само название и расширение, а следующим параметром изображение, которое мы хотим сохранить.


Доступ к пикселям и манипулирование ими


Для того, чтобы узнать высоту, ширину и количество каналов у изображения можно использовать атрибут shape:


print("Высота:"+str(img.shape[0]))print("Ширина:" + str(img.shape[1]))print("Количество каналов:" + str(img.shape[2]))

Важно помнить, что у изображений в оттенках серого img.shape[2] будет недоступно, так как данные изображения представлены в виде 2D массива.
Чтобы получить доступ к значению пикселя, нам просто нужно указать координаты x и y пикселя, который нас интересует. Также важно помнить, что библиотека OpenCV хранит каналы формата RGB в обратном порядке, в то время как мы думаем в терминах красного, зеленого и синего, то OpenCV хранит их в порядке синего, зеленого и красного цветов:


(b, g, r) = img[0, 0]print("Красный: {}, Зелёный: {}, Синий: {}".format(r, g, b))

Cначала мы берём пиксель, который расположен в точке (0,0). Данный пиксель, да и любой другой пиксель, представлены в виде кортежа. Заметьте, что название переменных расположены в порядке b, g и r. В следующей строке выводим значение каждого канала на экран. Как можно увидеть, доступ к значениям пикселей довольно прост, также просто можно и манипулировать значениями пикселей:


img[0, 0] = (255, 0, 0)(b, g, r) = img[0, 0] print("Красный: {}, Зелёный: {}, Синий: {}".format(r, g, b))

В первой строке мы устанавливаем значение пикселя (0, 0) равным (255, 0, 0), затем мы снова берём значение данного пикселя и выводим его на экран, в результате мне на консоль вывелось следующее:


Красный: 251, Зелёный: 43, Синий: 65Красный: 0, Зелёный: 0, Синий: 255

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

Подробнее..

OpenCV в Python. Часть 2

15.11.2020 22:16:27 | Автор: admin

Привет, Хабр! Продолжаем туториал по библиотеке opencv в python. Для тех кто не читал первую часть, сюда: Часть 1, а всем остальным увлекательного чтения!


part_2_logo


Введение


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


Изменение размера изображения


Первый метод, который мы изучим это как поменять высоту и ширину у изображения. Для этого в opencv есть такая функция как resize():


def resizing():    res_img = cv2.resize(img, (500, 900), cv2.INTER_NEAREST)

Данная функция первым аргументом принимает изображение, размер которого мы хотим изменить, вторым кортеж, который должен содержать в себе ширину и высоту для нового изображения, третьим метод интерполяции(необязательный). Интерполяция это алгоритм, который находит неизвестные промежуточные значения по имеющемуся набору известных значений. Фактически, это то, как будут заполняться новые пиксели при модификации размера изображения. К примеру, интерполяция методом ближайшего соседа (cv2.INTER_NEAREST) просто берёт для каждого пикселя итогового изображения один пиксель исходного, который наиболее близкий к его положению это самый простой и быстрый способ. Кроме этого метода в opencv существуют следующие: cv2.INTER_AREA, cv2.INTER_LINEAR( используется по умолчанию), cv2.INTER_CUBIC и cv2.INTER_LANCZOS4. Наиболее предпочтительным методом интерполяции для сжатия изображения является cv2.INTER_AREA, для увелечения cv2.INTER_LINEAR. От данного метода зависит качество конечного изображения, но как показывает практика, если мы уменьшаем/увеличиваем изображение меньше, чем в 1.5 раза, то не важно каким методом интерполяции мы воспользовались качество будет схожим. Данное утверждение можно проверить на практике. Напишем следующий код:


res_img_nearest = cv2.resize(img, (int(w / 1.4), int(h / 1.4)),                                  cv2.INTER_NEAREST)res_img_linear = cv2.resize(img, (int(w / 1.4), int(h / 1.4)),                                 cv2.INTER_LINEAR)

Слева изображение с интерполяцией методом ближайшего соседа, справа изображение с билинейной интерполяцией:


conc_girl


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


res_girl


Поэтому текущую функцию для изменения размера необходимо модифицировать:


def resizing(new_width=None, new_height=None, interp=cv2.INTER_LINEAR):    h, w = img.shape[:2]    if new_width is None and new_height is None:        return img    if new_width is None:        ratio = new_height / h        dimension = (int(w * ratio), new_height)    else:        ratio = new_width / w        dimension = (new_width, int(h * ratio))    res_img = cv2.resize(img, dimension, interpolation=interp)

Соотношение сторон мы вычисляем в переменной ratio. В зависимости от того, какой параметр не равен None, мы берём установленную нами новую высоту/ширину и делим на старую высоту/ширину. Далее, в переменной dimension мы определяем новые размеры изображения и передаём в функцию cv2.resize().


Смещение изображения вдоль осей


С помощью функции cv2.warpAffine() мы можем перемещать изображение влево и вправо, вниз и вверх, а также любую комбинацию из перечисленного:


def shifting():    h, w = img.shape[:2]    translation_matrix = np.float32([[1, 0, 200], [0, 1, 300]])    dst = cv2.warpAffine(img, translation_matrix, (w, h))    cv2.imshow('Изображение, сдвинутое вправо и вниз', dst)    cv2.waitKey(0)

Сначала, в переменной translation_matrix мы создаём матрицу преобразований как показано ниже:


1


Первая строка матрицы [1, 0, tx ], где tx количество пикселей, на которые мы будем сдвигать изображение влево или вправо. Отрицательное значения tx будет сдвигать изображение влево, положительное вправо.
Вторая строка матрицы [ 0, 1, ty], где ty количество пикселей, на которые мы будем сдвигать изображение вверх или вниз. Отрицательное значения ty будет сдвигать изображение вверх, положительное вниз. Важно помнить, что данная матрица определяется как массив с плавающей точкой.
На следующей строчке и происходит сдвиг изображения вдоль осей, с помощью, как я писал выше, функции cv2.warpAffine(), которая первым аргументом принимает изображение, вторым матрицу, третьим размеры нашего изображения. Если вы запустите данный код, то увидите следующее:


girl_right_and_down


Вырез фрагмента изображения


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


def cropping():    crop_img = img[10:450, 300:750]

В данной строчке мы предоставляем массив numpy для извлечения прямоугольной области изображения, начиная с (300, 10) и заканчивая (750, 450), где 10 это начальная координата по y, 300 начальная координата по x, 450 конечная координата по y и 750 конечная координата по x.Выполнив код выше, мы увидим, что обрезали лицо девочке:


crop_face


Поворот изображения


И, последнее, что мы на сегодня рассмотрим это как повернуть изображение на некоторый угол:


def rotation():    (h, w) = img.shape[:2]    center = (int(w / 2), int(h / 2))    rotation_matrix = cv2.getRotationMatrix2D(center, -45, 0.6)    rotated = cv2.warpAffine(img, rotation_matrix, (w, h))

Когда мы поворачиваем изображение, нам нужно указать, вокруг какой точки мы будем вращаться, именно это принимает первым аргументом функция cv2.getRotationMatrix2D(). В данном случае я указал центр изображения, однако opencv позволяет указать любую произвольную точку, вокруг которой вы захотите вращаться. Следующим аргументом данная функция принимает угол, на который мы хотим повернуть наше изображение, а последним аргументом коэффициент масштабирования. Мы используем 0.6, то есть уменьшаем изображение на 40%, для того, чтобы оно поместилось в кадр. Данная функция возвращает массив numpy, который мы передаём вторым аргументом в функцию cv2.warpAffine(). В итоге, у вас на экране должно отобразиться следующее изображение:


rotated_girl


Вот и вторая часть подошла к концу, исходный код доступен на github. Спасибо за уделённое внимание! До скорой встречи!

Подробнее..

Перевод Анимации градиентного спуска и ландшафта потерь нейронных сетей на Python

10.01.2021 14:07:00 | Автор: admin
Во время изучения различных алгоритмов машинного обучения я наткнулся на ландшафт потерь нейронных сетей с их горными территориями, хребтами и долинами. Эти ландшафты потерь сильно отличались от выпуклых и гладких ландшафтов потерь, с которыми я столкнулся при использовании линейной и логистической регрессий. Здесь мы создадим ландшафты потерь нейронных сетей и анимированного градиентного спуска с помощью датасета MNIST.


Рисунок 1 Ландшафт потерь свёрточной нейронной сети с 56 слоями (VGG-56, источник)



На приведённом выше изображении показан ландшафт нейронной сети с высокой степенью поверхностных потерь. Ландшафт потерь это визуальное представление значений, которые функция стоимости берёт на себя для заданного диапазона значений параметров с учётом наших тренировочных данных. Поскольку нашей целью является визуализация затрат в трёх измерениях, необходимо выбрать два конкретных параметра, которые будут варьироваться в наших графиках, тогда как все остальные параметры модели остаются неизменными. Стоит, однако, отметить, что существуют более продвинутые методы (например, уменьшение размерности, нормализация фильтра), которые могут использоваться для приближения ландшафтов потерь нейронной сети в подпространстве с низкой размерностью. Трёхмерное представление ландшафта потерь нейронной сети VGG с 56 слоями показано на рисунке 1. Однако это выходит за рамки данной статьи.

Искусственная нейронная сеть, с которой мы будем работать, состоит из одного входного слоя (с 784 узлами), двух скрытых слоёв (с 50 и 500 узлами соответственно) и одного выходного слоя (с 10 узлами). Мы будем повсеместно использовать сигмовидную функцию в качестве функции активации. Нейронная сеть не будет подвержена предвзятости. Обучающие данные состоят из изображений 28x28 пикселей, рукописных цифр в диапазоне от 0 до 9 из набора данных MNIST. Технически мы могли бы выбрать любой из 784*50+50*500+500*10=69,200 весов, которые мы используем в нашей нейронной сети. Я произвольно решил использовать веса w250, 5 (2) и w251,5(2), которые соединяют 250-й и 251-й узлы второго скрытого слоя с 6-м выходным нейроном соответственно. В нашей модели 6-й выходной нейрон возвращает активацию для модели, прогнозируя наличие цифры 5 на изображении. На рисунке 2 схематично показана архитектура нейронной сети, с которой мы будем работать. Из соображений ясности некоторые связи между нейронами и большая часть весовых аннотаций были намеренно опущены.


Рисунок 2 Архитектура нейронной сети

Мы импортируем MNIST в скрипт на Python. Рукописные цифры набора данных MNIST представлены в виде изображений в оттенках серого, поэтому мы можем нормализовать входные данные путём масштабирования значений пикселей из диапазона 0-255 в диапазон 0-1,2 в нашем коде, следовательно, мы делим x-значения на 255.

# Import librariesimport numpy as npimport gzipfrom sklearn.preprocessing import OneHotEncoderimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfrom scipy.special import expitimport celluloidfrom celluloid import Camerafrom matplotlib import animation # Open MNIST-files: def open_images(filename):    with gzip.open(filename, "rb") as file:             data=file.read()        return np.frombuffer(data,dtype=np.uint8, offset=16).reshape(-1,28,28).astype(np.float32) def open_labels(filename):    with gzip.open(filename,"rb") as file:        data = file.read()        return np.frombuffer(data,dtype=np.uint8, offset=8).astype(np.float32)     X_train=open_images("C:\\Users\\tobia\\train-images-idx3-ubyte.gz").reshape(-1,784).astype(np.float32) X_train=X_train/255 # rescale pixel values to 0-1y_train=open_labels("C:\\Users\\tobia\\train-labels-idx1-ubyte.gz")oh=OneHotEncoder(categories='auto') y_train_oh=oh.fit_transform(y_train.reshape(-1,1)).toarray() # one-hot-encoding of y-values

Чтобы создать ландшафты потерь, создадим график поверхности стоимости по отношению к вышеупомянутым весам w_250, 5(2) и w_251,5(2). Для этого мы определим среднеквадратичную функцию стоимости ошибки по отношению к весам w_a и w_b. Стоимости нашей модели J эквивалентны усреднённой сумме квадратичных ошибок между прогнозом модели и фактическим значением каждого из 10 выходных нейронов нашего обучающего набора данных с размером N:



С y и pred, представляющим матрицы фактических и прогнозируемых значений y соответственно. Прогнозируемые значения вычисляются путём прямого распространения входных данных через нейронную сеть на конечный слой. Вывод каждого слоя служит входными данными для следующего слоя. Входная матрица умножается на весовую матрицу соответствующего слоя. После этого сигмоидная функция применяется для получения выходных данных этого конкретного слоя. Весовые матрицы инициализируются малыми случайными числами с помощью генератора псевдослучайных чисел numpy. С помощью seed мы гарантируем воспроизводимость результатов. После этого подставляем два веса, которые могут изменяться в зависимости от аргументов функции w_a и w_b. Мы разработали функцию затрат на Python следующим образом:

hidden_0=50 # number of nodes of first hidden layerhidden_1=500 # number of nodes of second hidden layer# Set up cost function:def costs(x,y,w_a,w_b,seed_):          np.random.seed(seed_) # insert random seed         w0=np.random.randn(hidden_0,784)  # weight matrix of 1st hidden layer        w1=np.random.randn(hidden_1,hidden_0) # weight matrix of 2nd hidden layer        w2=np.random.randn(10,hidden_1) # weight matrix of output layer        w2[5][250] = w_a # set value for weight w_250,5(2)        w2[5][251] = w_b # set value for weight w_251,5(2)        a0 = expit(w0 @ x.T)  # output of 1st hidden layer        a1=expit(w1 @ a0)  # output of 2nd hidden layer        pred= expit(w2 @ a1) # output of final layer        return np.mean(np.sum((y-pred)**2,axis=0)) # costs w.r.t. w_a and w_b

Чтобы создать графики, мы определяем диапазоны значений для наших весов и строим сетку, получая таким образом все возможные комбинации значений веса для w_a и w_b соответственно. Для каждой пары w_a и w_b в нашей сетке мы намерены рассчитать соответствующие стоимости с помощью нашей функции стоимости. Далее мы можем, наконец, создать некоторые ландшафты потерь:

# Set range of values for meshgrid: m1s = np.linspace(-15, 17, 40)   m2s = np.linspace(-15, 18, 40)  M1, M2 = np.meshgrid(m1s, m2s) # create meshgrid # Determine costs for each coordinate in meshgrid: zs_100 = np.array([costs(X_train[0:100],y_train_oh[0:100].T                                 ,np.array([[mp1]]), np.array([[mp2]]),135)                         for mp1, mp2 in zip(np.ravel(M1), np.ravel(M2))])Z_100 = zs_100.reshape(M1.shape) # z-values for N=100zs_10000 = np.array([costs(X_train[0:10000],y_train_oh[0:10000].T                                 ,np.array([[mp1]]), np.array([[mp2]]),135)                         for mp1, mp2 in zip(np.ravel(M1), np.ravel(M2))])Z_10000 = zs_10000.reshape(M1.shape) # z-values for N=10,000# Plot loss landscapes: fig = plt.figure(figsize=(10,7.5)) # create figureax0 = fig.add_subplot(121, projection='3d' )ax1 = fig.add_subplot(122, projection='3d' )fontsize_=20 # set axis label fontsizelabelsize_=12 # set tick label size# Customize subplots: ax0.view_init(elev=30, azim=-20)ax0.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=9)ax0.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=-5)ax0.set_zlabel("costs", fontsize=fontsize_, labelpad=-30)ax0.tick_params(axis='x', pad=5, which='major', labelsize=labelsize_)ax0.tick_params(axis='y', pad=-5, which='major', labelsize=labelsize_)ax0.tick_params(axis='z', pad=5, which='major', labelsize=labelsize_)ax0.set_title('N:100',y=0.85,fontsize=15) # set title of subplot ax1.view_init(elev=30, azim=-30)ax1.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=9)ax1.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=-5)ax1.set_zlabel("costs", fontsize=fontsize_, labelpad=-30)ax1.tick_params(axis='y', pad=-5, which='major', labelsize=labelsize_)ax1.tick_params(axis='x', pad=5, which='major', labelsize=labelsize_)ax1.tick_params(axis='z', pad=5, which='major', labelsize=labelsize_)ax1.set_title('N:10,000',y=0.85,fontsize=15)# Surface plots of costs (= loss landscapes):  ax0.plot_surface(M1, M2, Z_100, cmap='terrain', #surface plot                             antialiased=True,cstride=1,rstride=1, alpha=0.75)ax1.plot_surface(M1, M2, Z_10000, cmap='terrain', #surface plot                             antialiased=True,cstride=1,rstride=1, alpha=0.75)plt.tight_layout()plt.show()


Рисунок 3 Ландшафты с различными размерами образцов

На рисунке 3 показаны два примерных ландшафта потерь с одинаковыми весами (w_250, 5 (2) и w_251,5(2)) и одинаковыми случайными начальными весами. Левый участок поверхности создавался с помощью первых 100 изображений набора данных MNIST, в то время как участок справа был создан с помощью первых 10 000 изображений. Если мы присмотримся к левому графику, то увидим некоторые типичные черты невыпуклых ландшафтов потерь: локальные минимумы, плато, хребты (иногда также называемые седловыми точками) и глобальный минимум. Однако термин минимум следует использовать с осторожностью, поскольку мы видим только заданный диапазон значений, вместе с тем не проводился тест первой производной.


Рисунок 4

Градиентный спуск


Эти географические барьеры резко контрастируют с гладкими и выпуклыми ландшафтами потерь, которые можно увидеть в линейной и логистической регрессиях. Считается, что эти барьеры замедляют достижение глобального минимума и даже препятствуют этому, а следовательно, негативно влияют на производительность модели [3]. Для исследования явления я решил анимировать градиентный спуск с этим конкретным ландшафтом потерь и тремя характерными начальными точками. Градиентный спуск в основном компрометирует обновление параметров модели (например, весов) в соответствии со следующим уравнением:



где J градиент нашей функции стоимости, w вес всей модели, e соответствующая эпоха и скорость обучения.

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



где w определяется как вес между j-м узлом слоя до и i-м узлом текущего слоя, который является выходным слоем в нашем случае. Вход i-го нейрона в выходном слое просто обозначается как in () и эквивалентен сумме активаций слоя до умножения на их соответствующие веса соединения, ведущие к этому узлу. Выход * i*-го нейрона в выходном слое обозначается как out () и соответствует (in ()). Решая уравнение выше, мы получаем:



с * out (), соответствующим активации j-го узла в слое, перед которым в выходном слое через w. соединен с n-м узлом. Переменная target обозначает целевой вывод для каждого из 10 выходных нейронов. Ссылаясь на рисунок 2, out () будет соответствовать активации h или h, в зависимости от того веса, от которого мы намереваемся вычислить частную производную. Отличное объяснение, включая подробный математический вывод, можно найти здесь [4].

Поскольку вывод нейронов выходного слоя эквивалентен прогнозу нейронной сети, в следующем коде воспользуемся более удобной аббревиатурой 'PRE'. Поскольку строгая фокусировка на конкретном узле может привести к путанице в коде, мы стремимся придерживаться установленного принципа использования весовых матриц и умножения матриц для обновления всех весов в выходном слое сразу. Наконец, мы обновим только два конкретных веса выходного слоя, которые собираемся обновить на самом деле. На Python мы реализуем алгоритм градиентного спуска только для двух весов вот так:

# Store values of costs and weights in lists: weights_2_5_250=[] weights_2_5_251=[] costs=[] seed_= 135 # random seedN=100 # sample size # Set up neural network: class NeuralNetwork(object):    def __init__(self, lr=0.01):        self.lr=lr        np.random.seed(seed_) # set random seed        # Intialize weight matrices:         self.w0=np.random.randn(hidden_0,784)          self.w1=np.random.randn(hidden_1,hidden_0)        self.w2=np.random.randn(10,hidden_1)        self.w2[5][250] = start_a # set starting value for w_a        self.w2[5][251] = start_b # set starting value for w_b        def train(self, X,y):        a0 = expit(self.w0 @ X.T)          a1=expit(self.w1 @ a0)          pred= expit(self.w2 @ a1)        # Partial derivatives of costs w.r.t. the weights of the output layer:         dw2= (pred - y.T)*pred*(1-pred)  @ a1.T / len(X)   # ... averaged over the sample size        # Update weights:         self.w2[5][250]=self.w2[5][250] - self.lr * dw2[5][250]         self.w2[5][251]=self.w2[5][251] - self.lr * dw2[5][251]         costs.append(self.cost(pred,y)) # append cost values to list        def cost(self, pred, y):        return np.mean(np.sum((y.T-pred)**2,axis=0))    # Initial values of w_a/w_b: starting_points = [  (-9,15),(-10.1,15),(-11,15)] for j in starting_points:    start_a,start_b=j    model=NeuralNetwork(10) # set learning rate to 10    for i in range(10000):  # 10,000 epochs                    model.train(X_train[0:N], y_train_oh[0:N])         weights_2_5_250.append(model.w2[5][250]) # append weight values to list        weights_2_5_251.append(model.w2[5][251]) # append weight values to list# Create sublists of costs and weight values for each starting point: costs = np.split(np.array(costs),3) weights_2_5_250 = np.split(np.array(weights_2_5_250),3)weights_2_5_251 = np.split(np.array(weights_2_5_251),3)

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

fig = plt.figure(figsize=(10,10)) # create figureax = fig.add_subplot(111,projection='3d' ) line_style=["dashed", "dashdot", "dotted"] #linestylesfontsize_=27 # set axis label fontsizelabelsize_=17 # set tick label fontsizeax.view_init(elev=30, azim=-10)ax.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=17)ax.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=5)ax.set_zlabel("costs", fontsize=fontsize_, labelpad=-35)ax.tick_params(axis='x', pad=12, which='major', labelsize=labelsize_)ax.tick_params(axis='y', pad=0, which='major', labelsize=labelsize_)ax.tick_params(axis='z', pad=8, which='major', labelsize=labelsize_)ax.set_zlim(4.75,4.802) # set range for z-values in the plot# Define which epochs to plot:p1=list(np.arange(0,200,20))p2=list(np.arange(200,9000,100))points_=p1+p2camera=Camera(fig) # create Camera objectfor i in points_:    # Plot the three trajectories of gradient descent...    #... each starting from its respective starting point    #... and each with a unique linestyle:    for j in range(3):         ax.plot(weights_2_5_250[j][0:i],weights_2_5_251[j][0:i],costs[j][0:i],                linestyle=line_style[j],linewidth=2,                color="black", label=str(i))        ax.scatter(weights_2_5_250[j][i],weights_2_5_251[j][i],costs[j][i],                   marker='o', s=15**2,               color="black", alpha=1.0)    # Surface plot (= loss landscape):    ax.plot_surface(M1, M2, Z_100, cmap='terrain',                              antialiased=True,cstride=1,rstride=1, alpha=0.75)    ax.legend([f'epochs: {i}'], loc=(0.25, 0.8),fontsize=17) # set position of legend    plt.tight_layout()     camera.snap() # take snapshot after each iteration    animation = camera.animate(interval = 5, # set delay between frames in milliseconds                          repeat = False,                          repeat_delay = 0)animation.save('gd_1.gif', writer = 'imagemagick', dpi=100)  # save animation   


Рисунок 5 Траектории градиентного спуска

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

fig = plt.figure(figsize=(10,10)) # create figureax0=fig.add_subplot(2, 1, 1) ax1=fig.add_subplot(2, 1, 2) # Customize subplots: ax0.set_xlabel(r'$w_a$', fontsize=25, labelpad=0)ax0.set_ylabel(r'$w_b$', fontsize=25, labelpad=-20)ax0.tick_params(axis='both', which='major', labelsize=17)ax1.set_xlabel("epochs", fontsize=22, labelpad=5)ax1.set_ylabel("costs", fontsize=25, labelpad=7)ax1.tick_params(axis='both', which='major', labelsize=17)contours_=21 # set the number of contour linespoints_=np.arange(0,9000,100) # define which epochs to plotcamera = Camera(fig) # create Camera objectfor i in points_:    cf=ax0.contour(M1, M2, Z_100,contours_, colors='black', # contour plot                     linestyles='dashed', linewidths=1)    ax0.contourf(M1, M2, Z_100, alpha=0.85,cmap='terrain') # filled contour plots         for j in range(3):        ax0.scatter(weights_2_5_250[j][i],weights_2_5_251[j][i],marker='o', s=13**2,               color="black", alpha=1.0)        ax0.plot(weights_2_5_250[j][0:i],weights_2_5_251[j][0:i],                linestyle=line_style[j],linewidth=2,                color="black", label=str(i))                ax1.plot(costs[j][0:i], color="black", linestyle=line_style[j])    plt.tight_layout()    camera.snap()    animation = camera.animate(interval = 5,                          repeat = True, repeat_delay = 0)  # create animation animation.save('gd_2.gif', writer = 'imagemagick')  # save animation as gif


Рисунок 6 Траектории градиентного спуска в 2D

Обе анимации показывают, что градиентный спуск может застревать в локальных минимумах, седловых точках или плато с невыпуклыми ландшафтами потерь. Для преодоления некоторых из этих препятствий были реализованы многочисленные варианты градиентного спуска (ADAGRAD, Adam и др.). Однако я хотел бы прояснить, что не все ландшафты потерь настолько невыпуклые в определённом диапазоне значений для w_a и w_b. Выпуклость ландшафта потерь зависит, среди прочего, от количества скрытых слоев, при этом глубокие нейронные сети приводят к сильно невыпуклым ландшафтам потерь.

Я решил создать несколько произвольных ландшафтов потерь, перебирая случайные начальные значения для генерации. На этот раз веса, которые могут изменяться, находятся на втором скрытом уровне (код). Репрезентативный образец ландшафтов потерь, с которыми я столкнулся при таком подходе, можно увидеть ниже. Размер выборки и индексы весов, представленных w_a и w_b, указаны соответственно в заголовках.


Рисунок 7 N=500, w20030(1), w20031(1) (создано автором, код)


Рисунок 8 N=1000, w55(1), w56(1) (создано автором, код)

Визуализация ландшафта потерь может быть полезна для лучшего понимания лежащей в основе теории и потенциальных недостатков различных алгоритмов оптимизации. Однако на практике актуальность локальных минимумов, плато или хребтов всё ещё обсуждается. Некоторые авторы утверждают, что локальные минимумы очень редки в многомерных пространствах и что седловые точки могут быть даже более проблематичными, чем локальные минимумы в отношении оптимизации параметров. Другие авторы даже предполагают, что минимизация затрат, связанная с локальными минимумами, является достаточной и может предотвратить переобучение [7].

Я надеюсь, что вам понравилось! Полный код Jupyter Notebook можно найти на моём GitHub.

Приложение



Представленное изображение

Ссылки
База данных MNIST

  1. Li, Hao, et al. Visualizing the loss landscape of neural nets. Advances in neural information processing systems. 2018.
  2. Как нормализовать, центрировать и стандартизировать пиксели изображения в Keras
  3. Почему сложно обучать нейронную сеть
  4. Пример пошагового обратного распространения ошибки
  5. Staib, Matthew & J. Reddi, Sashank & Kale, Satyen & Kumar, Sanjiv & Sra, Suvrit. (2019). Escaping Saddle Points with Adaptive Gradient Methods.
  6. Dauphin, Yann et al. Identifying and attacking the saddle point problem in high-dimensional non-convex optimization. NIPS (2014).
  7. Choromanska, A., Henaff, M., Mathieu, M., Arous, G. B., & LeCun, Y. (2015). The loss surfaces of multilayer networks. Journal of Machine Learning Research, 38, 192204.


image



Подробнее..

OpenCV в Python. Часть 3

26.01.2021 00:16:12 | Автор: admin

Привет, Хабр! Это продолжение туториала по библиотеке opencv в python. Для тех кто не читал первую и вторую части, сюда: Часть 1 и Часть 2, а всем остальным приятного чтения!



Введение


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


Арифметика изображений


Надеюсь, что все знают такие арифметические операции как сложение и вычитание, но при работе с изображениями мы не должны забывать о типе данных.
К примеру, у нас есть RGB изображение, пиксели которого попадают в диапазон [0,255]. Итак, что же произойдёт, если мы попытаемся к пикселю с интенсивностью 250 прибавить 30 или от 70 отнять 100? Если бы мы пользовались стандартными арифметическими правилами, то получили бы 280 и -30 соответственно. Однако, если мы работаем с RGB изображениями, где значения пикселей представлены в виде 8-битного целого беззнакового числа, то 280 и -30 не является допустимыми значениями. Для того, чтобы разобраться, что же произойдёт, давайте посмотрим на строчки кода ниже:


print("opencv addition: {}".format(cv2.add(np.uint8([250]),                                                    np.uint8([30]))))print("opencv subtract: {}".format(cv2.subtract(np.uint8([70]),                                                     np.uint8([100]))))print("numpy addition: {}".format(np.uint8([250]) + np.uint8([30])))print("numpy subtract: {}".format(np.uint8([70]) - np.uint8([71])))

Как мы видим, сложение и вычитание можно осуществить с помощью функций opencv add и subtract соответственно, а также с помощью numpy. И результаты будут отличаться:


opencv addition: 255opencv subtract: 0numpy addition: 24numpy subtract: 255

OpenCV выполняет обрезку и гарантирует, что значения пикселей никогда не выйдут за пределы диапазона [0,255]. В numpy же всё происходит немного иначе. Представьте себе обычные настенные часы, где вместо 60 находится 255. Получается, что после достижение 255 следующим числом будет идти 0, а когда мы отнимаем от меньшего числа большее, то после 0 ( против часовой стрелки) будет идти 255.


Разбиение и слияние каналов


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


image = cv2.imread('rectangles.png')b, g, r = cv2.split(image)cv2.imshow('blue', b)cv2.imshow('green', g)cv2.imshow('red', r)

Сначала мы загружаем изображение. Для наглядности работы данной функции я взял следующее изображение:



Затем разделяем изображение на три канала и показываем каждый канал по отдельности. В результате выполнения данной функции отобразится три изображения в оттенках серого:



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



Как можно увидеть, красный канал очень светлый. Это происходит потому, что оттенки красного очень сильно представлены в нашем изображении. Синий и зелёный каналы, наоборот, очень тёмные. Это случается потому, что на данном изображении очень мало данных цветов.
Для того, чтобы объединить каналы воедино, достаточно воспользоваться функцией merge(), которая принимает значения каналов:


merge_image = cv2.merge([g,b,r])cv2.imshow('merge_image', merge_image)cv2.imshow('original', image)cv2.waitKey(0)


Таким образом, мы получаем такое же изображение как оригинальное, за исключением того, что я поменял местами синий с зелёным каналом.


Размытие


Размытие это когда более резкие области на изображении теряют свою детализацию, в результате чего изображение становится менее чётким. В opencv имеются следующие основные методы размытия: averaging(усреднённое), gaussian(гауссово) и median(медианное).


Averaging


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


def averaging_blurring():    image = cv2.imread('girl.jpg')    img_blur_3 = cv2.blur(image, (3, 3))    img_blur_7 = cv2.blur(image, (7, 7))    img_blur_11 = cv2.blur(image, (11, 11))

Чем больше размер ядра, тем более размытым будет становиться изображение:



Gaussian


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



Это размытие реализуется в opencv с помощью функции GaussianBlur(), которая принимает первые два аргумента такие же как и предыдущая функция, а третьим аргументом указываем стандартное отклонение ядра Гаусса. Установив это значение в 0, мы тем самым говорим opencv автоматически вычислять его, в зависимости от размера нашего ядра:


def gaussian_blurring():    image = cv2.imread('girl.jpg')    img_blur_3 = cv2.GaussianBlur(image, (3, 3), 0)    img_blur_7 = cv2.GaussianBlur(image, (7, 7), 0)    img_blur_11 = cv2.GaussianBlur(image, (11, 11), 0)

Median


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


def median_blurring():    image = cv2.imread('girl.jpg')    img_blur_3 = cv2.medianBlur(image, 3)    img_blur_7 = cv2.medianBlur(image, 7)    img_blur_11 = cv2.medianBlur(image, 11)

В результате у нас получится следующее:



На этом данная часть подошла к концу. Код, как всегда, доступен на github. До скорой встречи:)

Подробнее..

OpenCV в Python. Часть 4

15.03.2021 22:22:10 | Автор: admin

Привет, Хабр! В этой статье я бы хотел рассказать как с помощью только OpenCV распознавать объекты, на примере игральных карт:



Введение


Допустим, у нас имеется следующее изображение с картами:



А также у нас имеются эталонные изображения каждой карты:



И теперь для того, чтобы детектировать каждую карту, нам необходимо написать три ключевые функции, которые:


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

Нахождение контуров карт


def find_contours_of_cards(image):    blurred = cv2.GaussianBlur(image, (3, 3), 0)    T, thresh_img = cv2.threshold(blurred, 215, 255,                                   cv2.THRESH_BINARY)    (_, cnts, _) = cv2.findContours(thresh_img,                                 cv2.RETR_EXTERNAL,                                cv2.CHAIN_APPROX_SIMPLE)    return cnts

Первым аргументом в данную функцию мы передаём изображение в оттенках серого, к которому применяем гауссово размытие, для того, чтобы было легче найти контуры карт. После этого с помощью функции threshold() мы преобразуем наше серое изображение в бинарное. Данная функция принимает первым параметром изображение, вторым пороговое значение, третьим это максимальное значение, которое присваивается значениям пикселей, превышающим пороговое значение. Из нашего примера следует, что любое значение пикселя, превышающее 215, устанавливается равным 255, а любое значение, которое меньше 215, устанавливается равным нулю. И последним параметром передаём метод порогового значения. Мы используем THRESH_BINARY(), который указывает на то, что значения пикселей, которые больше 215 устанавливаются в максимальное значение, которое мы передали третьим параметром. Данная функция возвращает два значения, где первое это значение, которое мы передали в данную функцию вторым аргументом, а второе чёрно-белое изображение, которое выглядит подобным образом:



Теперь мы можем найти контуры наших карт, где контур это кривая, соединяющая все непрерывные точки, которые имеют одинаковый цвет. Поиск контуров осуществляется с помощью метода findContours(), где в качестве первого аргумента эта функция принимает изображение, вторым это тип контуров, который мы хотим извлечь. Я использую cv2.RETR_EXTERNAL для извлечения только внешних контуров. К примеру, для того, чтобы извлечь все контуры используют cv2.RETR_LIST, а последний параметром мы указываем метод аппроксимации контура. Мы используем cv2.CHAIN_APPROX_SIMPLE, указывая на то, что все лишние точки будут удалены, тем самым экономя память. Например, если вы нашли контур прямой линии, то разве вам нужны все точки этой линии, чтобы представить эту линию? Нет, нам нужны только две конечные точки этой линии. Это как раз то, что и делает cv2.CHAIN_APPROX_SIMPLE.


Нахождение координат карт


def find_coordinates_of_cards(cnts, image):    cards_coordinates = {}    for i in range(0, len(cnts)):        x, y, w, h = cv2.boundingRect(cnts[i])        if w > 20 and h > 30:            img_crop = image[y - 15:y + h + 15,                             x - 15:x + w + 15]            cards_name = find_features(img_crop)            cards_coordinates[cards_name] = (x - 15,                      y - 15, x + w + 15, y + h + 15)    return cards_coordinates

Данная функция принимает контуры, которые мы нашли в предыдущей функции, а также основное изображение в оттенках серого. Первым делом мы создаём словарь, где в роли ключа будет выступать название карты, а в роли значения координаты каждой карты. Далее мы проходимся в цикле по нашим контурам, где с помощью функции boundingRect() находим ограничительные рамки каждого контура: начальные x и y координаты, за которыми следуют ширина и высота рамки. Так получилось, что функция, которая искала контура, нашла аж 31 контур, хотя карт всего 4. Это могут быть незначительные контуры, которые мы дальше сортируем в условии, исходя из размера контура.


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


Распознавание карт


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



Теперь закройте глаза и попытайтесь представить это изображение:



И вы увидите, что помните много деталей об этом образе. Причина этого в том, что на изображении много интересных областей. Теперь переходим к практике:


def find_features(img1):    correct_matches_dct = {}    directory = 'images/cards/sample/'    for image in os.listdir(directory):        img2 = cv2.imread(directory+image, 0)        orb = cv2.ORB_create()        kp1, des1 = orb.detectAndCompute(img1, None)        kp2, des2 = orb.detectAndCompute(img2, None)        bf = cv2.BFMatcher()        matches = bf.knnMatch(des1, des2, k=2)        correct_matches = []        for m, n in matches:            if m.distance < 0.75*n.distance:                correct_matches.append([m])                correct_matches_dct[image.split('.')[0]]                    = len(correct_matches)    correct_matches_dct =        dict(sorted(correct_matches_dct.items(),             key=lambda item: item[1], reverse=True))    return list(correct_matches_dct.keys())[0]

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



Затем нам необходимо сопоставить(вычислить расстояние) дискрипторы первого изображения с дискрипторами второго и взять ближайший. Для этого мы создаём BFMatcher объект с помощью вызова метода BFMatcher(). Теперь мы можем с помощью функции knnMatch() найти k лучших совпадений для каждого дескриптора, где k в нашем случае равно 2. Далее нам необходимо выбрать только хорошие совпадения, основываясь на расстоянии. Поэтому мы проходимся в цикле по нашим совпадениям и если оно удовлетворяет условию m.distance < 0.75*n.distance, то мы засчитываем это совпадение как хорошее и добавляем в список. Потом считаем количество хороших совпадений(чем больше, тем лучше) и основываясь на этом делаем вывод, что за карта. Вот какие совпадения были найдены для каждой карты с королём:



И вслед за этим рисуем прямоугольник вокруг карты с помощью функции draw_rectangle_aroud_cards():


def draw_rectangle_aroud_cards(cards_coordinates, image):    for key, value in cards_coordinates.items():        rec = cv2.rectangle(image, (value[0], value[1]),                             (value[2], value[3]),                             (255, 255, 0), 2)        cv2.putText(rec, key, (value[0], value[1] - 10),                     cv2.FONT_HERSHEY_SIMPLEX,                     0.5, (36, 255, 12), 1)    cv2.imshow('Image', image)    cv2.waitKey(0)

На этом всё. Надеюсь, было познавательно) Код и картинки можно найти на github. До новых встреч:)

Подробнее..

Из песочницы Подготовка к собеседованию QA starter pack или самая большая шпаргалка вопросов-ответов по тестированию

18.07.2020 14:05:59 | Автор: admin

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

В первую очередь хочу подчеркнуть, что этот материал от джуна джунам, от intermediate таким же и в данный момент предлагается AS IS, т.к. сорвал все предполагаемые ранее сроки, поэтому прошу прощения если вызову кровотечение из глаз у опытных коллег. Качество материала будет улучшаться по мере вычитки и вклада сообщества. Также будет добавляться новый материал по мере появления.

Здесь очень много перевода из зарубежных источников и в силу слабого английского и отсутствия коммерческого опыта там может быть откровенная ересь. Местами присутствует винегрет из русских и английских терминов. Изначально материал делался для себя, но он оказался предварительно востребованным, так что я привёл его в более-менее надлежащий вид, но и только.

Что касается источников и ресурсов, то список не полный. В первоначальном конспекте я не сохранял ссылки, так что, если увидели авторский контент, прошу не ругаться. Список полезных ресурсов я не пытался сделать всеобъемлющим, а лишь указал те, что пригодились лично мне.
Также отмечу, что и сам материал далеко не всеобъемлющий. Предполагается, что это некий гибрид ответов на вопросы и базовой теории и здесь темы раскрыты в той мере, что требуется на собеседовании. То есть ориентир и какая-то база есть, но при необходимости копаете дальше уже сами. Каждый термин, каждая тема представляется мне как трехмерный объект и не всегда можно достичь понимания, глядя в лоб (один источник). Порой требуется посмотреть под разными углами (в разных источниках). К тому же, как я уже сказал, это копание можно продолжать бесконечно и это был бы формат толстой книги, а не статьи.

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

Эта статья не заменит хорошую книгу или курс, с другой стороны, здесь больше реальности.

ВНИМАНИЕ! Для того, чтобы увидеть материал целиком, нужно открыть первую или вторую часть в файлах на гитхабе (Manual part 1 или Manual part 2).

Оказалось, что такой объем практически нереально предоставить общественности. К сожалению, Хабр пока не умеет парсить исходники больше 150кб, а это означало бы разбиение материала на 8 статей (разработчики пообещали исправить, задача уже в активных).

На гите дополнительно пришлось поделить материал на 2 файла ввиду ограничения на размер одного в 1 мб. Если есть что исправить или дополнить пишите тут/создавайте issue/форкайте и коммитьте! Призываю участвовать всех неравнодушных!

Оглавление


Manual part 1


  • HR-часть
  • Вопросы с реальных собеседований с этапа HR
  • Общее о тестировании
  • Что означает тестирование ПО?
  • Почему требуется тестирование ПО?
  • Что означает обеспечение качества (Quality Assurance QA) при тестировании ПО?
  • Что означает контроль качества (Quality Control QC) при тестировании ПО?
  • Что означает качество ПО? (Software Quality)
  • Объясните отличия в QA, QC и тестировании
  • Что означает Verification при тестировании ПО?
  • Что означает Validation в тестировании ПО?
  • Разница между Design Verification и Design Validation?
  • Принципы тестирования?
  • Что подразумевается под тестовым покрытием? (Test Coverage)
  • Что такое модель зрелости тестирования (TMM Test Maturity Model)?
  • Что такое CMM?
  • Что такое тестирование со сдвигом влево? (Shift left testing)
  • Что такое независимое тестирование? (Independent testing)
  • В чем разница между превентивным и реактивным подходами в тестировании? (Preventative and Reactive approaches)
  • Перечислите типичные возможные обязанности инженера по обеспечению качества?
  • Что такое аудит качества?
  • Почему тестирование делится на отдельные этапы?
  • Почему невозможно полностью протестировать ПО?
  • Как вы тестируете продукт, если требования еще не зафиксированы?
  • Как вы узнаете, было ли создано достаточно тестов для тестирования продукта?
  • Как вы понимаете инспекцию?
  • Какие есть роли/должности в команде?
  • Опишите жизненный цикл продукта по этапам какие участники на каждом этапе, какие у них роли? Какие артефакты на каждом этапе?
  • Кто такой SDET?
  • Что такое тестирование как сервис? (TaaS testing as a Service)
  • Что подразумевается под тестовой средой? (Test Environment/Test Bed)
  • Что подразумевается под тестовыми данными?
  • Основные фазы тестирования?
  • Подробнее про бета-тестирование?
  • Что означает пилотное тестирование? (Pilot)
  • В чем отличие build от release?
  • Что такое бизнес логика (domain)?
  • Ты единственный тестировщик на проекте. Что делать?
  • Основные инструменты тестировщика?
  • Виды тестирования
  • Какие существуют основные виды тестирования ПО?
  • Типы тестирования? (White/Black/Grey Box)
  • Что означает тестирование черного ящика?
  • Что означает тестирование белого ящика?
  • Что означает тестирование серого ящика? (Grey box)
  • Основные отличия White/grey/black box?
  • Что такое деструктивное/разрушающее/негативное тестирование? (DT Destructive testing)
  • Что такое недеструктивное/неразрушающее/позитивное тестирование? (NDT Non Destructive testing)
  • Что такое пирамида/уровни тестирования? (Testing Levels)
  • Что подразумевается под компонентным/модульным/юнит тестированием? (Component/Module/Unit testing)
  • Что подразумевается под интеграционным тестированием? (Integration testing)
  • Разница между Unit testing и Integration testing?
  • Что такое системное интеграционное тестирование? (SIT System Integration testing)
  • Что подразумевается под инкрементальным подходом? (Incremental Approach)
  • Что подразумевается под подходом снизу-вверх? (Bottom-Up Approach)
  • Что подразумевается под подходом сверху-вниз? (Top-Down Approach)
  • Что подразумевается под гибридным/сэндвич-подходом? (Sandwich Approach)
  • Что подразумевается под подходом Большого взрыва? (Big Bang Approach)
  • В чем разница между тест-драйвером и тест-заглушкой? (Test Driver and Test Stub)
  • Что подразумевается под системным тестированием?
  • Можем ли мы провести системное тестирование на любом этапе?
  • Что такое функциональное тестирование?
  • Что такое тестирование совместимости/взаимодействия? (Compatibility/Interoperability testing)
  • Что такое тестирование на соответствие? (Conformance/Compilance testing)
  • Что такое нефункциональное тестирование?
  • Основные понятия в тестировании производительности?
  • Тестирование производительности клиентской части и серверной, в чем разница?
  • В общем виде что такое тестирование производительности?
  • Что такое тестирование емкости/способностей? (Capacity)
  • Что означает тестирование масштабируемости? (Scalability)
  • Разница между тестированием ёмкости/способностей и тестированием масштабируемости? (Capacity vs Scalability)
  • Расскажите о стрессовом тестировании? (Stress testing)
  • Расскажите о нагрузочном тестировании? (Load)
  • Что такое объемное тестирование? (Volume testing)
  • Тестирование выносливости/стабильности/надежности (Soak/Endurance/Stability/Reliability testing)
  • Что такое спайк/шиповое тестирование? (Spike)
  • Что такое тестирование устойчивости? (Resilence)
  • Что такое тестирование времени отклика? (Response time testing)
  • Что такое Ramp тестирование?
  • Что такое тестирование хранилища? (Storage testing)
  • Что такое тестирование на отказ и восстановление? (Failover and Recovery testing)
  • Что вы знаете о Тестировании удобства пользования? (Usability testing)
  • Отличия тестирование на удобство пользования и тестирования доступности? (Usability Vs. Accessibility testing)
  • Что такое тестирование интерфейса? (UI testing)
  • Что такое тестирование рабочего процесса/воркфлоу? (Workflow testing)
  • Что вы знаете о пользовательском приемочном тестировании? (UAT User Acceptance testing)
  • Что такое эксплуатационное приемочное тестирование? (OAT Operational Acceptance testing)
  • Расскажите об инсталляционном тестировании?
  • Что вы знаете о тестировании безопасности? (Security and Access Control testing)
  • Что означает оценка уязвимости/защищенности? (Vulnerability Assessment)
  • Расскажите подробнее о тестировании на проникновение? (Penetration testing)
  • Отличия Vulnerability Assessment от Penetration testing?
  • Что такое Fuzz тестирование?
  • Можно ли отнести тестирование безопасности или нагрузочное тестирование к функциональным видам тестирования?
  • Что вы знаете о конфигурационном тестировании? (Configuration testing)
  • Что подразумевается под проверкой дыма/дымовым тестированием? (Smoke testing)
  • Что такое тестирование встряхиванием? (Shake out testing)
  • Что подразумевается под санитарным тестированием/согласованности/исправности? (Sanity testing)
  • Отличие санитарного тестирования от дымового? (Sanity vs Smoke testing)
  • Что вы знаете про регрессионное тестирование? (Regression testing)
  • Объясните, что такое тестирование N+1?
  • Что означает подтверждающее тестирование? (confirmation/re-testing)
  • В чем разница между повторным и регрессионным тестированием?
  • Типы регрессии по Канеру?
  • Что вы знаете о тестировании сборки? (Build Verification Test)
  • Что такое тестирование файлов cookie?
  • Что такое тестирование потоков? (Thread testing)
  • Что такое тестирование документации? (Documentation testing)
  • Какие вы знаете уровни тестирования данных?
  • Что такое подкожный тест? (Subcutaneous test)
  • Расскажите о локализации, глобализации и интернационализации? (Localization/ globalization/internationalization testing)
  • Что такое исследовательское тестирование? (Exploratory testing)
  • Что такое Свободное или Интуитивное тестирование? (Adhoc)
  • Что вы знаете о мутационном тестировании? (Mutation testing)
  • Что означает механизм тестирования по ключевым словам? (Keyword Driven testing Framework)
  • Что вы знаете о тестировании интерфейса прикладного программирования (API Application Programming Interface)?
  • Как протестировать API без документации/черным ящиком?
  • А что такое endpoint?
  • Frontend testing Vs. Backend testing?
  • Что подразумевают под эталонным тестированием? (Baseline testing)
  • В чем разница между Baseline и Benchmark testing?
  • Что такое параллельное/многопользовательское тестирование? (Concurrency/Multi-user testing)
  • Как вы думаете, что такое тестирование на переносимость?
  • Что такое тестирование графического интерфейса/визуальное тестирование? (GUI Graphical User Interface)
  • Что такое A/B тестирование?
  • Что означает сквозное тестирование? (E2E EndtoEnd)
  • В чем разница между E2E и системным тестированием?
  • Что такое параллельное тестирование? (Parallel testing)
  • Тест дизайн
  • Тест дизайн? (Test Design)
  • Перечислите известные техники тест-дизайна?
  • Что такое статическое тестирование, когда оно начинается и что оно охватывает?
  • Что такое динамическое тестирование, когда оно начинается и что оно охватывает?
  • Какие виды Review вы знаете?
  • Что вы знаете о Data Flow testing?
  • Что вы знаете о Control Flow testing?
  • Что такое Loop coverage?
  • Что такое Race coverage?
  • Тестирование пути и тестирование базового пути? (Path testing & Basis Path testing)
  • Что вы знаете о Statement coverage?
  • Что вы знаете о Decision coverage?
  • Что вы знаете о Branch coverage?
  • Что вы знаете о Condition coverage?
  • Что вы знаете о FSM coverage?
  • Что такое Function coverage?
  • Что такое Call coverage?
  • Что означает LCSAJ coverage?
  • Сравнение некоторых метрик
  • Что такое Equivalence Partitioning?
  • Что такое Boundary Value Analysis?
  • Что такое Error Guessing?
  • Что такое Cause/Effect?
  • Что такое Exhaustive testing?
  • Какие вы знаете комбинаторные техники тест-дизайна?
  • Что такое тестирование ортогональных массивов? (OAT Orthogonal Array testing)
  • Что такое Domain analysis/testing?
  • Что такое Cyclomatic Complexity в тестировании ПО?
  • Что такое State Transition testing?
  • Что такое Scenario (use case) testing?
  • Что такое Decision Table testing?
  • Что такое Random testing?
  • Что такое Syntax testing?
  • Что вы знаете о Classification tree method?
  • Как мы узнаем, что код соответствует спецификациям?
  • Что включает в себя матрица отслеживания требований? (RTM Requirement Traceability Matrix)
  • В чем разница между Test matrix и Traceability matrix?
  • Что такое анализ GAP?
  • Что такое граф причинно-следственных связей? (Cause Effect Graph)
  • В чем разница между предугадыванием ошибок и посевом? (Error guessing and error seeding)
  • Стили тестов?
  • Техники тестирования требований?
  • Что такое эвристики?
  • Тестовые артефакты и документация (Test Deliverables/TestWare/test artifacts)
  • Виды тестовой документации?
  • Отличия Test Suite от Test Scenario?
  • Какие отличия у плана тестирования и стратегии тестирования?
  • Виды тест планов?
  • Что является основой для подготовки плана приемки? (PAP Product Acceptance Plan)
  • В чем разница между тест-кейсом и чек-листом?
  • В чем разница между тест-кейсами высокого уровня и низкого уровня?
  • Чем Test case отличается от сценария тестирования?
  • Что такое тест-анализ/основа теста? (Test Analysis/Test Basis)
  • Что такое документ бизнес-требований (BRD)?
  • Что вы знаете о требованиях (уровни/виды и т. д.)?
  • Рассажите, какие есть требования к самим требованиям?

Manual part 2


  • Дефекты и ошибки
  • Что такое дефект?
  • Классы дефектов?
  • Какие есть категории дефектов?
  • Error/Mistake/Defect/Bug/Failure/Fault?
  • Каково содержание эффективного сообщения об ошибке?
  • Несколько ключевых моментов, которые следует учитывать при написании отчета об ошибке?
  • Серьезность и Приоритет Дефекта (Severity & Priority)
  • Может ли быть высокий severity и низкий priority? А наоборот?
  • Жизненный цикл дефекта?
  • Пришёл баг из продакшена, что делаем?
  • Что такое утечка дефектов и релиз бага? (Bug Leackage & Bug Release)
  • Что означает плотность дефектов при тестировании ПО?
  • Что означает процент обнаружения дефектов при тестировании ПО?
  • Что означает эффективность устранения дефектов при тестировании ПО? (DRP)
  • Что означает эффективность Test case в тестировании ПО? (TCE)
  • Возраст дефекта в тестировании ПО?
  • Что такое принцип Парето в тестировании ПО?
  • Каковы различные способы применения принципа Парето в тестировании ПО?
  • В чем основное отличие отладки от тестирования? (Debugging Vs. Testing)
  • Почему в программном обеспечении есть ошибки?
  • Что вы будете делать, если во время тестирования появится ошибка?
  • Как вы справляетесь с невоспроизводимой ошибкой?
  • Если продукт находится в производстве и один из его модулей обновляется, то необходимо ли провести повторную проверку?
  • Что такое анализ рисков?
  • В чем разница между coupling и cohesion?
  • Что такое скрытый дефект? (Latent defect)
  • Что такое маскировка ошибок, объясните примером?
  • Категории отладки? (Debugging)
  • Что такое Эффективность удаления дефектов? (DRE Defect Removal Efficiency)
  • Что такое сортировка дефектов? (Bug triage)
  • SDLC и STLC
  • Что вы знаете о жизненном цикле разработки ПО? (SDLC Software Development Lifecycle)
  • Что такое цикл/колесо Деминга? (Deming circle/cycle/wheel)
  • Модели разработки ПО?
  • Что такое Agile?
  • Что такое Scrum?
  • В чем отличие Canban от scrum?
  • Что знаете о User stories в гибких подходах к разработке?
  • Что значит жизненный цикл тестирования ПО? (STLC Software Testing Lifecycle)
  • Что вы знаете о техниках оценки теста? (Test Estimation)
  • В чем разница между SDLC и STLC?
  • Что такое быстрая разработка приложений? (RAD Rapid Application Development)
  • Что такое разработка через тестирование (TDD Test Driven Development)?
  • TDD в Agile Model Driven Development (AMDD)
  • Тестирование на основе моделей (MDD Model-driven Development)
  • Тестирование на основе данных (DDT Data Driven testing)
  • Тестирование на основе риска (RBT Risk Based Testing)
  • Что вы знаете о потоковом тестировании? (BFT BusinessFlowTesting)
  • Тестирование в разных сферах/областях (testing different domains)
  • Что такое веб-тестирование и как его производить?
  • Тестирование банковского ПО
  • Тестирование электронной коммерции (eCommerce)
  • Тестирование платежного шлюза (Payment Gateway)
  • Тестирование систем розничной торговли (POS Point Of Sale)
  • Тестирование в сфере страхования (Insurance)
  • Тестирование в сфере телекоммуникаций (Telecom)
  • Тестирование протокола: L2 и L3 OSI
  • Тестирование интернета вещей (IoT Internet of Things)
  • Что такое облачное тестирование? (Cloud testing)
  • Что такое тестирование сервис-ориентированной архитектуры? (SOA Service Oriented Architecture)
  • Что такое тестирование планирования ресурсов предприятия? (ERP Enterprise Resource Planning)
  • Тестирование качества видеосвязи WebRTC-based сервиса видеоконференций
  • Что такое тестирование ETL?
  • Мобильное тестирование
  • Каковы особенности в тестировании мобильных приложений? В чем отличия тестирования мобильного приложения от десктопного?
  • В чем отличия тестирования мобильного приложения от web?
  • Что вы знаете о симуляторах и эмуляторах?
  • Типы мобильных приложений?
  • Что основное проверить при тестировании мобильного приложения?
  • Последнее обновление Android/iOS, что нового?
  • Основные различия iOS и Android?
  • Виды жестов и т.п.?
  • Как проверить использование процессора на мобильных устройствах?
  • Объясните критические ошибки, с которыми вы сталкиваетесь при тестировании на мобильных устройствах или в приложениях?
  • Сети и около них
  • Что такое http?
  • Компоненты HTTP?
  • Методы HTTP-запроса?
  • Что такое ресурс?
  • Что такое веб-сервис? (WS Web service)
  • Отличие сервиса от сервера?
  • Отличие сервиса от веб-сайта?
  • Что такое REST, SOAP? В чем отличия?
  • Что такое JSON, XML?
  • Коды ответов/состояния сервера с примерами? (HTTP status code)
  • Почему ошибка 404 относится к 4** клиентской, если по идее должна быть 5**?
  • Какие еще бывают протоколы?
  • TCP/IP это?
  • Что такое куки (cookies)?
  • Разница между cookie и сессией/сеансом?
  • Отличие stateless и stateful?
  • Различия методов GET и POST?
  • Клиент серверная архитектура?
  • Уровни OSI?
  • Что вы подразумеваете под потоковыми медиа? (Streaming media)
  • Основные команды Linux?
  • Почему важно тестировать в разных браузерах?
  • Адаптивный веб-дизайн vs. Отзывчивый веб-дизайн, в чем разница? (Adaptive Vs. Responsive)
  • Как сервер узнаёт, с какого типа устройства/браузера/ОС/языка вы открываете веб-сайт? (Например, для Adaptive design)
  • Чем отличается авторизация от аутентификации?
  • Как работает авторизация/аутентификация? Как сайт понимает, что ты залогинен?
  • Почему важно делать подтверждение e-mail при регистрации?
  • Что такое кэш и зачем его очищать при тестировании?
  • Что такое AJAX в вебе?
  • Как работает браузер (коротко)?
  • Как работает сотовая связь?
  • Как работает подключение к Wi-Fi?
  • Базы данных
  • Может ли у ПО быть сразу несколько баз данных?
  • Что такое SQL?
  • Что вы знаете о NoSQL?
  • Что такое нормальные формы?
  • Понятие хранимой процедуры?
  • Понятие триггера?
  • Что такое индексы? (Indexes)
  • Какие шаги выполняет тестер при тестировании хранимых процедур?
  • Как бы вы узнали для тестирования базы данных, сработал триггер или нет?
  • Как тестировать загрузку данных при тестировании базы данных?
  • Основные команды SQL?
  • Подробнее о джойнах? (Join)
  • Типы данных в SQL?
  • ПРАКТИКА
  • Дана форма для регистрации. Протестируйте.
  • Определение серьезности и приоритета
  • Определение граничных значений и классов эквивалентности
  • Логические задачи
  • Еще примеры
  • Набор небольших задач по SQL
  • Тестирование чашки для кофе
  • HR: Как вы будете решать конфликты между членами вашей команды?
  • HR: Что делать, если разработчик утверждает, что найденный дефект таковым не является?
  • Вот тебе комп и работающий сайт. Сделай мне 401-ю ошибку.
  • С чего начать абсолютному новичку?
  • Путь
  • CV
  • Собеседование
  • Ошибки в работе у начинающих тестировщиков
  • Полезное
  • Youtube-каналы
  • Telegram
  • Web
  • Книги
  • Курсы
  • Источники
Подробнее..

Морской бой на Java для новичков. Level 1

17.06.2021 10:17:44 | Автор: admin

Всем привет!

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

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

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

  2. Одновременно в игре могут участвовать только два человека.

  3. В самом начале игроки "представляются" - программа "спрашивает" (предлагает пользователям ввести), какие у них имена

  4. У каждого игрока есть своё поле - квадрат 10х10 клеток

  5. Затем игроки по очереди расставляют свои корабли. Как и в "бумажной" версии - каждый может поставить 4 однопалубных корабля, 3 двухпалубных, 2 трехпалубных и 1 четырёхпалубный.

  6. Корабли можно располагать только по горизонтали или по вертикали.

  7. Игроки не видят расположение кораблей друг друга.

  8. Начинается игра. Первый игрок делает выстрел, сообщая нашему приложению координаты предполагаемой цели - номер клетки по горизонтали и номер клетки по вертикали.

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

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

    Второй вариант, если игрок не попал ни в какой корабль, то ход переходит второму игроку.

  10. Таким образом, как описано в пункте 8, передавая ход друг другу, игроки пытаются как можно раньше уничтожить корабли друг друга. Тот, кто первым, одолеет флотилию врага - победитель. Программа печатает поздравление и прекращает свою работу.

    Теперь, согласовав с бизнесом сценарий работы приложения, можно приступать к реализации игры.

Как и в любом Java приложении нам потребуется класс (не умаляя общности назовём его Main), в котором будет объявлен, я думаю уже всем известный, метод main.

public class Main {public static void main(String[] args) {  //your code will be here}}

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

Опираясь на пункты 1-3 утвержденного сценария, реализуем функционал приложения, который будет предлагать игрокам ввести свои имена. Здесь нам придётся использовать класс java.util.Scanner, который умеет считывать введенные значения в консоли.

public class Main {    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");    }}

Подробнее о коде:

В строке 2 создаем статичное свойство класса Main scanner.

Нестатический метод nextLine() класса Scanner (строки 6 и 11) обращается к консоли и возвращает строку, которую он еще не считывал.

После получения имени пользователей, программа выводит приветствие в консоль - "Hello, {username} !"

В консоли будем видеть следующее, если запустим код сейчас.

Player 1, please, input your nameEgorHello, Egor!Player 2, please, input your nameMaxHello, Max!

Поговорим о том, как мы будем отображать поле боя и заполнять его кораблями. Пожалуй, что наиболее логичным будет использование двумерного массива char[][] buttlefield. В нем мы будем отображать расположение кораблей. Договоримся, что удачное попадание в корабль противника будем отображать символом #. Неудачный выстрел будем помечать символом *. Таким образом изначально, массив будет проинициализирован дефолтовым для примитива char значением ('\u0000'), а в процессе игры будет заполняться символами # и *.

public class Main {static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);public static void main(String[] args) {    System.out.println("Player 1, please, input your name");    String player1Name = scanner.nextLine();    System.out.println("Hello, " + player1Name + "!");    System.out.println("Player 2, please, input your name");    String player2Name = scanner.nextLine();    System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];}}

Подробнее о коде:

В строке 2 мы создаем константу FIELD_LENGTH, которая будет содержать размер поля - согласно требованию 4 - проинициализируем FIELD_LENGTH значением 10.

В строках 14-18 создаем двумерные массивы char. playerFiled1 и playerField2 - массивы, в которые будем записывать расположение кораблей каждого игрока. Размеры массивов равны размерам поля - логично, ведь эти двумерные массивы фактически отображают поля.

Перейдём к написанию логике по заполнению полей игроков своими кораблями. Предлагаю создать метод fillPlayerField(playerField), который будет спрашивать у игрока позиции корабля и записывать в массив новый корабль.

public class Main {    static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];        fillPlayerField(playerField1);        fillPlayerField(playerField2);    }}private static void fillPlayerField(char[][] playerField) {// your code will be here}

Метод fillPlayerField должен быть статическим (static), так как вызываться он будет из метода main, который по определению должен быть статическим. fillPlayerField не будет возвращать никакого значения (void). В этом методе будет реализована логика по получению координат корабля от пользователя и запись в массив playerField нового корабля.

Поговорим о том, как пользователь будет задавать координаты кораблей. Будем размещать корабли по убыванию палуб, то есть первым разместим четырёхпалубный корабль, затем все трёхпалубные корабли, все двухпалубные корабли и все однопалубные. Программа будет принимать от пользователя координаты начала корабля и его положение (см. бизнес-требование 6) - по горизонтали или по вертикали.

Приведу вывод программы в консоль, который мы ожидаем увидеть после создания метода fillPlayerField:

Расставляем 4-палубный корабль. Осталось расставить: 1Input x coord: 1Input y coord: 11. Horizontal; 2. Vertical ?1

Наконец-то приступаем. На данный момент имеем:

private static void fillPlayerField(char[][] playerField) {    // your code will be here}

Нам нужно расставить корабли с палубами от 4 до 1. Здесь дело идёт к циклу. Предлагаю без лишнего пафоса использовать for. Заметим, кораблей с одинаковым числом палуб может быть несколько, поэтому нам нужно ещё как-то контролировать, чтобы пользователь мог разместить лишь определенное число кораблей с заданным количеством палуб (см. бизнес требование 5) - эта задача также эффективно решается с помощью цикла - без пафоса также используем for.

private static void fillPlayerField(char[][] playerField) {// i - счётчик количества палуб у корабля    // начинаем расстановку с корабля, которого 4 палубы, а заканчиваем кораблями с одной палубой    for (int i = 4; i >= 1; i--) {      // см. подробнее о коде под этой вставкой    for (int k = i; k <= 5 - i; k++) {          System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));        // some your code here        }    }}

Подробнее о коде:

На 5 строчке мы создаём цикл for (int k = 0; k <= 5 - i; k++). Объясню, откуда такая магия. Нам нужно как-то понимать, сколько кораблей каждого типа (с определенным количеством палуб) пользователь может поставить.

Мы можем создать еще один двумерный массив, в котором мы захардкодим что-то в духе:

int[][] shipTypeAmount = {{1, 4}, {2, 3}, {3, 2}, {4, 1}};

Мы бы в первом цикле (см. строку 4) перебирали значение количества палуб, потом искали бы в двумерном массиве, какое количество кораблей соответствует данному типу. На мой взгляд, такое решение качественное, однозначно, если бы мы писали коммерческую программу, то сделали бы именно так, но сейчас такая красота выглядит сложной - оставим это как задачу со звёздочкой, для тех, кто захочет "причесать" свой код.

Я же предлагаю отметить особенность, что сумма количества кораблей и количества палуб - величина постоянная. Действительно, 1 + 2 = 5; 2 + 3 = 5; 3 + 2 = 5; 4 + 1 = 5 . Поэтому, зная количество палуб у корабля (зная тип корабля), мы можем посчитать, сколько кораблей такого типа может быть установлено одним играком. Например, 5 - 1 = 4 - таким образом, каждый игрок может поставить 4 однопалубных корабля. В цикле for на строке 6 реализована проверка условия цикла "лайтовым" способом - на основе этого интересного свойства.

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

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка самих кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));                System.out.println("Input x coord: ");                x = scanner.nextInt();                System.out.println("Input y coord: ");                y = scanner.nextInt();                System.out.println("1 - horizontal; 2 - vertical ?");                position = scanner.nextInt();                // если корабль располагаем горизонтально              if (position == 1) {                    // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                    for (int q = 0; q < i; q++) {                        playerField[y][x + q] = '1';                    }                }                            // если корабль располагаем вертикально                if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                    for (int m = 0; m < i; m++) {                        playerField[y + m][x] = '1';                    }                }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже                printField(playerField);            }        }    }

Подробнее о коде:

Корабль помечаем символом '1' столько раз, сколько палуб он имеет - если корабль четырёхпалубный, то он займёт 4 клетки - помечаем 4 клетки значением '1'.

Нам неоднократно потребуется печатать поля игроков, либо поля их выстрелов по вражескому флоту. Для того, чтобы сделать это - создаём метод printField.

static void printField(char[][] field) {        for (char[] cells : monitor) {            for (char cell : t) {              // если значение дефолтовое (в случае char - 0), значит в данной клетке              // корабль не установлен - печатаем пустую клетку                if (cell == 0) {                    System.out.print(" |");                } else {   // если клетка непустая (значение отличается от дефолтового),                  //тогда отрисовываем сожержимое клетки (элемента массива)                    System.out.print(cell + "|");                }            }            System.out.println("");            System.out.println("--------------------");        }    }

На экране метод будет так отображать расстановку кораблей:

 | | | | | | | | | |-------------------- |1|1|1|1| | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |--------------------

Вот игроки уже заполнили свои корабли на карте, и теперь мы можем приступить к реализации пунктов 8-10 бизнес-требований заказчика.

Логику по получению от пользователя координат выстрела, обработки выстрела и передачи хода опишем в методе playGame. Дабы придерживаться (или пока только стараться) принципа single responsobility - не забываем делить логику на методы (1 функциональность - 1 метод, но тоже держим себя в руках, код, в котором 100500 однострочных методов тоже не комильфо) - примерно из этих соображений получились еще методы handleShot и isPlayerAlive. Реализация обоих приведена ниже

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {        // "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы  // удачные (#) и неудачные (*)  char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];  // вспомогательные переменные, которым будут присваиваться значения текущего игрока -   // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения  // за тавтологию        String currentPlayerName = player1Name;        char[][] currentPlayerField = playerField2;        char[][] currentPlayerBattleField = playerBattleField1;  // внутри цикла происходит смена очередности игроков, выстрел, его обработка.  // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков  // "частично" цел (ранен) ещё хотя бы один корабль        while (isPlayerAlive(playerField1) && isPlayerAlive(playerField2)) {          // принимаем от пользователя координаты выстрела            System.out.println(currentPlayerName + ", please, input x coord of shot");            int xShot = scanner.nextInt();            System.out.println(currentPlayerName + ", please, input y coord of shot");            int yShot = scanner.nextInt();          // обрабатываем выстрел и получаем возвращаемое значение метода handleShot            int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);            // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {                currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;            }        }    }/**    * Метод обрабатывает выстрел. Если выстрел удачный, то есть снаряд достиг цели -    * в клетку записывается значение '#' (отображается к в массиве игрока, так и в массиве соперника),    * а также на экран выводится сообщение 'Good shot!'. В этом случае метод возвращает значение 1.    * В случае неудачного выстрела - в массив battleField записывается значение '0' в элемент [y][x], и    * и возвращается значение 0.    * Возвращаемые значения нам нужны для того, чтобы в методе, внутри которого вызывается метод handleShot,    * мы могли понимать, успешно или неуспешно прошёл выстрел. На основе этого мы принимаем решение, * переходит ход к другому игроку или нет.    */    private static int handleShot(char[][] battleField, char[][] field, int x, int y) {        if ('1'.equals(field[y][x])) {            field[y][x] = '#';            battleField[y][x] = '#';            System.out.println("Good shot!");            return 1;        }        battleField[y][x] = '*';        System.out.println("Bad shot!");        return 0;    }/***Метод определяет, не проиграл ли еще игрок. Если у игрока остался хотя бы    * один "раненный" корабль, тогда пользователь продолжает игру.    * То есть, если на карте у игрока остался хотя бы один символ '1', которым мы отмечали    * корабли, то игра продолжается - возвращается значение true. Иначе false.*/    private static boolean isPlayerAlive(char[][] field) {        for (char[] cells : field) {            for (char cell : cells) {                if ('1' == cell) {                    return true;                }            }        }        return false;    }

Думаю, что к комментариям в коде мне добавить нечего. Единственное, обращу внимание на тонкий момент. Мы привыкли в математике к записи (x, y) - где первой идёт координат абсцисс, а второй - координата ординат. Казалось бы, чтобы обратиться к элементу двумерного массива (иногда срываюсь и называю в тексте элемент клеткой, но суть не меняется) нужно написать arr[x][y], но это будет неверно, и чтобы это доказать воспользуемся неопрвергаемым методом пристального взгляда. Для примера рассмотрим следующий двумерный массив:

int[][] arr = {{1, 2}, {7, 4}, {8, 3, 5, 9}, {1}}System.out.println(arr[0][1]); // ?System.out.println(arr[1][0]); // ?

Теперь вопрос из квиза "Программирование и мир" - что выведется на консоль в строках 3 и 4?
Вспоминаем, что двумерный массив - это не совсем таблица (нам так проще его воспринимать и детектировать его в задачах) - это "массив массивов" - вложенные массивы. Если в одномерных целочисленных массивах элементом является целое число, то в случае двумерного массива - элементом является массив (а в случае трёхмерного массива - элементом является двумерный массив). Таким образом, первый индекс указывает, какой по счёту массив мы выбираем. Второй индекс указывает, какой элемент по счёту мы выбираем в выбранном ранее массиве. Запись arr[1][2] указывает, что мы обращаемся к элементу с индексом 2 (то есть 3 по порядку) в массиве с индексом 1 (или второму по порядку). Соответсвенно, в строке 3 в консоль выведется значение 2, а в строке 4 - 7.

Постепенно подбираемся к концу. Что нам осталось реализовать?

  1. Вывод имени победителя

  2. Проверка клетки, которую пользователь указал как начало корабля

Первое кажется проще, стартанём с него. Потопали в метод playGame - как вы помните, там есть цикл while, в условии которого есть проверка - живы ли еще оба игрока. Напомню, что если игрок "мёртв", то есть у него не осталось ни одного корабля, то игра прекращается, а выживший игрок считается победителем. Собственно, единственное, что добавилось - строчка 36 - вызов метода System.out.println()

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {// "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы    // удачные (#) и неудачные (*)    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];    // вспомогательные переменные, которым будут присваиваться значения текущего игрока -     // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения    // за тавтологию    String currentPlayerName = player1Name;    char[][] currentPlayerField = playerField2;    char[][] currentPlayerBattleField = playerBattleField1;    // внутри цикла происходит смена очередности игроков, выстрел, его обработка.    // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков    // "частично" цел (ранен) ещё хотя бы один корабль    while (isPlayerAlive(playerField1) &amp;&amp; isPlayerAlive(playerField2)) {      // перед каждым выстрелом выводим в консоль отображение всех выстрелов игрока      printField(currentPlayerBattleField);        // принимаем от пользователя координаты выстрела        System.out.println(currentPlayerName + ", please, input x coord of shot");        int xShot = scanner.nextInt();        System.out.println(currentPlayerName + ", please, input y coord of shot");        int yShot = scanner.nextInt();        // обрабатываем выстрел и получаем возвращаемое значение метода handleShot        int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);        // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {            currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;        }    }  System.out.println(currentPlayerName + " is winner!");}

Переходим ко второму пункту наших "остатков" - реализуем проверку клетки, которую указал пользователь - начало корабля.

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

Окрестность с радиусом одна клетка. Красным кругом помечена стартовая клетка - начало корабля.Окрестность с радиусом одна клетка. Красным кругом помечена стартовая клетка - начало корабля.

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

Какие нюансы есть в этом алгоритме? Снова посмотрим на рисунок.

Неэффективность нашего простого алгоритма. Некоторые клетки проверяем несколько раз.Неэффективность нашего простого алгоритма. Некоторые клетки проверяем несколько раз.
private static int validateCoordForShip(char[][] field, int x, int y, int position, int shipType) {        // если пользователь хочет расположить корабль горизонтально  if (position == 1) {            for (int i = 0; i < shipType - 1; i++) {if ('1' == field[y][x + i]                                || '1' == field[y - 1][x + i]                                || '1' == field[y + 1][x + i]                                || '1' == field[y][x + i + 1]                                || '1' == field[y][x + i - 1]|| (x + i) > 9) {                    return -1;                }            }        } else if (position == 2) {          // если пользователь хочет расположить корабль вертикально            for (int i = 0; i < shipType - 1; i++) {                if ('1' == field[y][x + i]                        || '1' == field[y - 1][x + i]                        || '1' == field[y + 1][x + i]                        || '1' == field[y][x + i + 1]                        || '1' == field[y][x + i - 1]|| (y + i) > 9) {                    return -1;                }            }        }        return 0;    }

Не забываем проапгрейдить метод fillPlayerField - напомню, что в этом методе реализована логика по расположению кораблей игроками.

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));              // иницализируем переменную начальным значением              int validationResult = 1;            while (validationResult != 0) {              System.out.println("Input x coord: ");            x = scanner.nextInt();            System.out.println("Input y coord: ");            y = scanner.nextInt();            System.out.println("1 - horizontal; 2 - vertical ?");            position = scanner.nextInt();                  // если координата не прошла валидацию (проверку), то метод возвращает отрицательное// значение, конечно, оно не равно нулю, поэтому пользователю придётся ввести координаты                  // ещё раз                  validationResult = validateCoordForShip(playerField, x, y, position, i);                }            // если корабль располагаем горизонтально              if (position == 1) {                // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                for (int q = 0; q < i; q++) {                    playerField[y][x + q] = '1';                }            }                        // если корабль располагаем вертикально            if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                for (int m = 0; m < i; m++) {                    playerField[y + m][x] = '1';                }            }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже            printField(playerField);        }    }}

Вот мы и написали игру "Морской бой" - level 1. Начали с простого - простых конструкций, идей и методов. Один класс, нет коллекций - только массивы. Оказывается на циклах и массивах можно написать игру.

Мы удовлетворили все требования бизнеса. Доигрывая до конца, получили отличную оценку от заказчика, он полностью доволен приложением. Ждём, когда он опробует игру и вернётся снова за апгрейдом. А тут и будет level - 2.

Всем спасибо, всегда рад обратной связи!

Подробнее..

Как вести технический блог?

04.07.2020 12:05:52 | Автор: admin


Я работаю в коммерческой разработке с 2011 года. С конца 2012 занимаюсь разработкой под iOS. Свою первую техническую статью я написал на Хабре в начале 2017 года про подход к локализации мобильных приложений. Потом выпустил ещё несколько статей по iOS-разработке на Хабре и в конце 2017 года я перешёл в новую компанию и решил вести блог про solution architecture https://medium.com/@nvashanin, где начал описывать общие концепты, обязанности архитектора, его скилл-сет и т.д. К лету 2020 года количество просмотров моих статей перевалило за 800 тысяч, а количество времени, которое люди потратили на прочтение больше 6 млн минут, или около 12 лет чистого времени. Флагманская статья была переведена другими людьми на разные языки: например, польский или испанский.

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

Предыстория


Мне стало интересно развиваться в сторону архитектора программного обеспечения ещё в 2016 году. В начале 2017 года я побывал на первом в своей жизни трёхдневном интенсиве, где матёрый архитектор рассказывал про основные концепции. К концу интенсива мы объединялись в команды, которые должны были взять предложенную проблему, придумать под неё архитектурное решение и защитить перед другими командами. Честно признаться, тогда это вышло у меня плохо.

Я уже был тимлидом несколько лет, затем перешёл в другую компанию и узнал, что есть отдельная ветка развития в архитекторы. Для этого мне пришлось поработать на своём проекте архитектором и пройти ассессмент через комитет зрелых архитекторов. Я прошел этот комитет только со второго раза, через полгода после первой попытки. Общий процент прохождения в компании на эту позицию был около 25%.

И вот в конце 2017, когда я начал неофициально исполнять обязанности архитектора на большом проекте, я попутно завёл блог, в котором стал писать о карьерном развитии. Мне хотелось получше разобраться самому. А также помочь тем, кто находится в похожей ситуации и размышляет о том, куда идти после тимлидской позиции, кроме ветки менеджмента. Это главная мотивация, почему я начал вести блог. Давайте рассмотрим, какие ещё могут быть причины для этого.

Зачем вести технический блог


Я часто слышу вопрос: Зачем мне блог?. Вот лишь несколько причин и они актуальны не только для IT-сферы.

Прокачивать знания. Если вам интересна какая-то тема и вы хотите в ней лучше разобраться пишите статью. Так вы сможете улучшить собственные знания и разложить их по полочкам. Я завёл блог, потому что хотел структурировать свои знания в области работы архитектора ПО.

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

А ещё, грамотно развивая личный бренд, вы можете стать инфлюенсером то есть специалистом, к чьему мнению прислушиваются другие участники сообщества. Один из ярких примеров кейс ребят из Rambler. В 2016 году у них получилось зарекомендовать VIPER как лучшую архитектуру слоя презентации на iOS и альтернативу более классическим MVC и MVVM среди русского iOS-сообщества. И всё благодаря толковым выступлениям и полезным статьям. Очень много компаний и разработчиков перетащили VIPER себе, некоторые до сих пор её используют.

Продвигать компанию. Создание личного бренда всегда тесно переплетается с укреплением бренда компании. Это взаимовыгодная ситуация: вы помогаете привлекать лучшие кадры, подаёте хороший пример своим коллегам, рассказываете о продукте и полезных инсайдах рынку, и компания так или иначе вас поощряет.

Конечно, важно, чтобы руководство компании также осознавало ценность этой истории и поддерживало активность сотрудников. Мы в Cardsmobile сейчас активно работаем над укреплением HR-бренда на рынке, и, возможно, мои статьи помогут мотивировать ребят из команды делиться знаниями, писать статьи и выступать.

Заработать деньги. Большинство заводит блог, чтобы заработать. Это частая причина для трэвел или бьюти сферы, но, что парадоксально, в IT-блогинг редко идут за деньгами. Хотя возможности всё же есть: я купил себе часы на деньги, которые принёс мне блог хотя и не ставил перед собой цели заработать.

О чём писать в блоге


Вопрос, с которым сталкивается каждый, кто решил завести блог: О чём писать?. Чаще всего темы для статей подсказывает сама причина, по которой вы решили завести блог. Если вы решили прокачаться пишите статьи о том, в чём хотите разобраться. Если же укрепить бренд компании и свой собственный рассказывайте о своей работе, проектах, применяемых технологиях.

Для себя я выделил следующие направления или рубрики:

Обзоры. Увидели новую технологию попробуйте объяснить её в формате от А до Я. Узнавайте, о чём рассказывают на профильных конференциях, что сейчас находится в тренде. Почти всё новое имеет огромный потенциал для исследования и глубокого погружения. Также хорошо заходят обзоры прочитанных книг.

Кейсы. Сделали что-то интересное расскажите об этом. Частая ошибка ощущение, что вы не делаете ничего интересного. Зачастую всё наоборот. Смотрите на свою работу не изнутри, а со стороны, и боритесь с синдромом самозванца.

Работаете с высоконагруженным back-endом расскажите, как делаете хайлоад и почему именно так. Верстаете UI расскажите про интересную анимацию, которую сделали. Использовали в проекте новую технологию расскажите о своём опыте работы с ней. Ваши подводные камни и инсайды помогут другим не сидеть над проблемой часами.

Гайды. Возьмите какую-то проблему и попробуйте её решить. Решение подробно распишите получится своеобразный гайд для тех, кто столкнется со схожей проблемой.

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

Статьи с юмором. Возьмите серьёзную тему и расскажите о ней в легком ключе. Аудитории такое заходит.

Пара советов по поиску тем


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

Читайте новости и тренды. Это бесконечный поток новых тем для вашего блога. Готовя статью о какой-либо новости или тренде, вы решаете сразу две задачи: прокачиваете собственные знания и экономите время другим участникам сообществам, собирая информацию за них.

Читайте комментарии к чужим статьям. Это кладезь информации о том, что интересует и беспокоит других людей.

Помните, что уникальных и гениальных идей, как правило, мало. Всё так или иначе уже было кем-то придумано. Тем не менее это не повод не писать, ведь хороших описательных текстов мало. Мой совет: просто пишите, и со временем вы научитесь быстро находить интересные темы для блога.

Как написать хорошую статью


Делитесь уникальным контентом. Главное правило хорошего текста: он должен быть уникальным. Никому не интересно читать то, что уже где-то было опубликовано. К тому же, по правилам некоторых площадок в том числе Хабра публиковать неуникальный контент запрещено: это карается не только минусами в карму, но и баном.

Составляйте план. Интересная и понятная статья начинается с плана и структурирования основных мыслей. Набросать хороший скелет статьи мне помогают mind maps. Для этой статьи получился небольшой mind map, но иногда выходят и солидные. Ниже mind map, который я собрал, чтобы рассказать в Твиттере о своём опыте жизни в Нидерландах.

image

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

Придерживайтесь структуры. Чтобы читателю было проще ориентироваться в статье, используйте традиционную структуру, состоящую из введения, основной части и заключения.
  • Введение.
Введение должно заинтересовать читателя и мотивировать его погрузиться в текст. Расскажите, о чём будет статья и какую пользу несёт ваш текст, и чётко обозначьте аудиторию, для которой вы написали статью.
  • Основная часть.
В этой части раскрывайте основную идею и давайте ответы на поставленные во вступительной части вопросы. Позаботьтесь о читателе и упростите навигацию по статье: используйте заголовки и подзаголовки, разные шрифты (жирный и курсив для выделения заголовков и важных фрагментов текста), дробите текст на абзацы, составляйте списки. Продираться сквозь простыню текста сложно и неинтересно, а эти простые приемы помогут повысить его читаемость.
  • Выводы или заключение.
В финале статьи ёмко сформулируйте основные идеи, которые вы раскрыли в тексте, чтобы разложить всё по полочкам в голове читателя, или оставьте простор для размышлений. В конце статьи также можно добавить полезные ссылки по теме, чтобы заинтересованные читатели смогли детальнее изучить тему.

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

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

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

Отдельного внимания заслуживает заглавная картинка к статье тизер (от англ. tease дразнить). Из названия понятен механизм, по которому её следует выбирать: она должна вызывать желание открыть ссылку. Это важно, ведь человек первым делом обращает внимание именно на картинку. Симпатичный вижуал соберёт больше внимания и переходов.

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

image

image

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

Проверяйте контент. Я не публикую написанную статью сразу же. У меня есть ритуал, который я строго соблюдаю, это ревью, состоящее из нескольких этапов.

Проверка орфографии, пунктуации и стилистики текста. Советую пользоваться специальными автоматизированными сервисами: Орфограммка для орфографии, пунктуации и стилистики; Advego для стилистической проверки текста; Главредом царём сервисов, который очищает текст от словесного мусора. Для текстов на английском языке я использую платную версию сервиса Grammarly.

Проверка ревьюерами. В тех областях, о которых я пишу от iOS разработки до архитектуры у меня есть несколько хороших ревьюеров специалистов, чьему мнению я доверяю. Они хорошо разбираются в теме и могут дать комментарии по содержанию: в iOS разработчик из VK, мой хороший друг из Санкт-Петербурга, а в архитектуре архитектор из Google в Кремниевой долине. Если у вас на примете нет таких экспертов, покажите статью руководителю или коллегам. Большие компании, в которых процесс производства контента поставлен на поток, сотрудничают с внешними рецензентами.

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

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

Где публиковать статью: выбор площадки


Если у компании есть собственный блог, начать можно с него. Если же нет варианта два: уже существующие площадки или блог на своём домене.

Блог-платформ много: у русскоязычной аудитории популярен Хабр, у англоязычной Medium. Платформ для создания собственного блога также немало. У каждого из подходов есть свои плюсы и минусы.

Популярная платформа


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

+ Готовый UI, который упрощает вёрстку статей. Платформы предлагают удобные редакторы, в которые встроены разнообразные шаблоны и механики. В Medium, к примеру, есть блок с автоматическим расчётом времени, которое потребуется для прочтения статьи. Статьи также удобно читать: в редакторах по умолчанию предусмотрены воздух в тексте, вариации шрифтов, заголовки, удобная работа с картинками и многое другое.

+ Простой запуск. Работа над статьей идёт по простому сценарию: написание > публикация.

Базовая кастомизация стилистики. Если хочется сложной вёрстки или нестандартных фишек, такой вариант не подойдёт.

Необходимость принять все правила блог-платформы. Не исключены нюансы с юридической точки зрения, а ещё вас могут забанить за нарушение, что приведёт к полной потере блога. Речь и о приватности: например, нельзя публиковать частные переписки или делать полную копию статьи с другой площадки. Такие моменты кажутся очевидными, но лучше на старте всё же прочесть правила платформы.

Зависимость от востребованности площадки. Если сервис теряет популярность, то и ваш блог теряет популярность.

Блог на собственном домене


Плюсы популярных платформ это минусы блогов на своём домене. А минусы платформ плюсы собственных площадок.

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

Если вам всё же хочется иметь собственный домен, некоторые площадки разрешают настроить собственное доменное имя и публиковать статьи под ним такая опция доступна, к примеру, на Medium. Оценив все плюсы и минусы обоих вариантов, я завёл блоги на двух платформах: на Хабре и Medium.

Как продвигать статью


Если вы решили запустить блог на платформе, вам по умолчанию будет доступен трафик, который генерирует сама платформа. К продвижению блога на собственном домене есть смысл подключить SEO-специалиста.

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

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

Опубликуйте ссылку во всех социальных сетях. Даже если у вас не раскручены соцсети, посты всё равно сгенерируют некоторое количество просмотров на старте это уже хорошо. В моём случае лучше всего работает Linkedin, где у меня более 3 тысяч контактов, преимущественно англоязычное сообщество. А вот Instagram, по моим ощущениям, для IT-блогов не работает совсем.

Отправьте ссылку инфлюенсерам. Это могут быть блогеры, паблики и издания на схожую с вашей тематику. Возможно, вашу статью добавят в рассылку или опубликуют в ленте. У меня был кейс с публикацией в Телеграм-канале с парой тысяч активных читателей.

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

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

image

image

image

image
Скрины сообщений в Linkedin

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

Как монетизировать блог


Перейдём к самой провокационной теме можно ли монетизировать блог, и как это сделать.

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

Виды монетизации


Есть два направления монетизации.

Прямая. Вы получаете деньги непосредственно за написание статьи.

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

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

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

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

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

Механизмы монетизации


Я протестировал несколько механизмов монетизации. Делюсь их результатами.

Нативная монетизация блог-платформы. Делюсь опытом с Medium. Каждый читатель или автор может купить платный аккаунт за $ 5 и читать все статьи на платформе. Статьям, которые ему нравятся, читатель ставит лайк на Medium это называется хлопок. Деньги, которые читатель заплатил за подписку, распределяются между авторами понравившихся ему статей. Лучшие авторы могут получать неплохие деньги. В конце месяца Medium отправляет всем авторам рассылку, в которой раскрывает топовые цифры по доходам. В мае 2020 года цифры выглядели так: лучший автор получил $ 25000 за месяц со всех своих статей, а за лучшую статью выплатили больше $ 10000. Я получил за одну из статей $ 50 за месяц она была опубликована за 8 месяцев до того, как я протестировал инструмент.

image
Скрин экрана с информацией о том, сколько я заработал на одной из своих статей


Минус такого формата в том, что статья доступна только тем, у кого есть подписка. Меня это не устраивало, поэтому я изучил и другие варианты.

Аффилированные ссылки. Вы становитесь участником так называемой аффилированной программы и размещаете в своей статье аффилированные ссылки. Если читатель переходит по такой ссылке и покупает товар, вы получаете некий процент от продажи.

Аффилированные программы есть у многих программных продуктов и магазинов на HubSpot опубликован список доступных программ: от Coursera до Amazon. Я выбрал последний: в каждой моей статье есть пара ссылок на книги по архитектуре. Если кто-то покупает их по моей рекомендации, Amazon перечисляет мне определенный процент. Механика проста: если вы пишете полезный контент для своей аудитории, правильно подобранная аффилированная программа может вас озолотить. Ссылки в моих статьях до сих пор генерируют в среднем от $100 до $150 каждый месяц даже с учётом того, что последнюю статью я написал почти год назад (но я обещаю исправиться и снова начать писать!).

image
Мой доход в аффилированной программе Amazon за октябрь 2019 года

Недостаток такого подхода в том, что вывести деньги с Amazon на карточки российских банков невозможно. Я жил в Нидерландах, поэтому проблем с банковским счётом и выводом денег у меня не было. Вы же можете сделать счёт на PayPal и выводить деньги через него.

Аффилированные программы самый прибыльный и проверенный способ монетизации. Они могут приносить тысячи долларов ежемесячно при условии, что вы пишете постоянно. Кейсы подтверждают: один автор сделал линк-приглашение в водители Uber, когда агрегатор только начинал свою экспансию, и заработал $ 50000, а другой написал статью об Instagram-ботах на Medium и заработал $2000.

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

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

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

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

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

Возможно, если бы я писал больше статей, я бы зарабатывал на блоге пару тысяч долларов в месяц. Пока за всё время существования с 2017 года блог принёс мне около $ 5000 через разные форматы монетизации. На эти деньги я купил себе часы, о чём уже упоминал в начале статьи. Приятный бонус особенно учитывая то, что я всерьез не занимался блогом и продвижением, а лишь хотел изучить это направление.

Длина статьи, тэги и ещё три лайфхака


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

Лучше доработать статью, чем выпустить сырую. Звучит по-капитански, но иногда мне приходилось дорабатывать уже опубликованную статью. Такой подход рушит ваш авторитет в глазах читателей и приводит к уменьшению трафика. Солженицын в Круге первом описывает правило последних вершков: когда 95% работы закончено, остаётся всего ничего, но эти 5% работы самые сложные с точки зрения мотивации и самые важные с точки зрения удобства читателя. Всегда доделывайте статью до конца, даже если вам и кажется, что и так сойдет иначе зачем это всё?

Оптимальная длина одной статьи 7 минут. Такова статистика Medium. Прочтение статей с самым высоким рейтингом на платформе занимает 7 минут. Конечно, это не универсальное число. На мой взгляд, наилучший тайминг от 6 до 10 минут. В такой статье достаточно контента, чтобы полноценно раскрыть тему, но её не сложно прочитать за один раз. Многое также зависит от площадки: где-то оптимальная длина поста 3 минуты, а где-то возможно, все 30 минут.

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

Публикуйте статью в четверг или в субботу утром. По статистике в эти дни количество просмотров на 33% больше, чем в другие. Худший день для публикации понедельник.

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

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

Вместо выводов, или что дальше


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

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

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

На старте придумайте темы сразу для нескольких статей. Так вам будет проще сохранять мотивацию и не забросить блог. Постарайтесь построить долгосрочную стратегию развития вашего блога. К примеру, я сразу понимал, что хочу написать серию из 8 статей на тему карьерного развития. И чётко обозначил читателям в первой статье, что будет продолжение в первой же статье. Таким образом, я подготовил аудиторию к тому, что буду публиковать ещё контент, который им интересен, а себя к тому, что так просто соскочить не получится ведь я пообещал читателям новые статьи. Так люди могут становиться вашими постоянными читателями.

Получилась самая большая статья, которую я когда-либо писал. Я очень надеюсь, что раскрыл тему и замотивировал вас начать писать. Это долгий путь, но он того стоит. Write it right now!

Полезные ссылки


Grammarly сервис для проверки текстов на английском языке.
Главред сервис для проверки текста на русском языке.
Орфограммка сервис для проверки орфографии, пунктуации и стилистики.
Advego сервис для проверки стилистики.
Пиши, сокращай Максима Ильяхова и Людмилы Сарачевой лучшая книга на русском языке для тех, кто хочет писать понятные и ёмкие тексты.
Текст, который продает товар услугу или бренд Анны Шуст ещё одна книжная находка, которая поможет прокачать навык писательства.
Блог на миллион долларов книга известного эксперта по диджитал-маркетингу Наташи Кортни-Смит.
Подробнее..

Туториал по FASM (Windows x32 APIWin32API), Hello world!

29.05.2021 00:21:04 | Автор: admin

Коротко о FASM, ассемблере, WinAPI

  • Что такое FASM? - Это компилятор ассемблера (flat assembler).

  • Что такое ассемблер? - это машинные инструкции, то есть команды что делать процессору.

  • Что такое Windows API/WinAPI? - Это функции Windows, без них нельзя работать с Windows.

    Что дают WinAPI функции? - Очень много чего:

  • Работа с файлами.

  • Работа с окнами, отрисовка картинок, OpenGL, DirectX, GDI, и все в таком духе.

  • Взаимодействие с другими процессами.

  • Работа с портами.

  • Работа с консолью Windows

  • И еще очень много интересных функций.

Зачем нужен ассемблер?

На нем можно сделать все что угодно, от ОС до 3D игр.

Вот плюсы ассемблера:

  • Он очень быстрый.

  • На нем можно сделать любую программу.

А вот минусы ассемблера:

  • Долго делать программу. (относительно)

  • Сложен в освоении.

Что нужно для программирования на ассемблере (FASM)?

Установка компонентов (если можно так назвать)

Архив FASM-а распаковуем в C:\\FASM\ или любой другой, но потом не забудьте настроить FASMEditor.

Архив FASMEdit-a распаковуем куда-то, в моем случае C:\\FASM Editor 2.0\

Архив OlyDbg распаковуем тоже куда-то, в моем случае C:\\Users\****\Documents\FasmEditorProjects\

Настройка FASM Editor-a

Для этого его нужно запустить.

Сразу вас приветствует FASM Editor соей заставкой.

Теперь вам нужно зайти в вкладку "Сервис" (на картинке выделил синим) -> "Настройки..."

Жмем на кнопку с названием "..." и выбираем путь к файлам или папкам.

Теперь мы полностью готовы. К началу.

Пишем "Hello world!" на FASM

В Fasm Editor нужно нажать на кнопку слева сверху или "файл" -> "новый". Выбираем любое, но можно выбрать "Console"

По началу вас это может напугать, но не боимся и разбираемся.

format PE Console ; говорим компилятору FASM какой файл делатьentry start ; говорим windows-у где из этой каши стартовать программу.include 'win32a.inc' ; подключаем библиотеку FASM-а;можно и без нее но будет очень сложно.section '.data' data readable writeable ; секция данныхhello db 'hello world!',0 ; наша строка которую нужно вывестиsection '.code' code readable writeable executable ; секция кодаstart: ; метка стартаinvoke printf, hello ; вызываем функцию printf    invoke getch ; вызываем её для того чтоб программа не схлопнулась  ;то есть не закрылась сразу.    invoke ExitProcess, 0 ; говорим windows-у что у нас программа закончилась  ; то есть нужно программу закрыть (завершить)section '.idata' data import readable ; секция импорта        library kernel, 'kernel32.dll',\ ; тут немного сложней, объясню чуть позже                msvcrt, 'msvcrt.dll'    import kernel,\  ExitProcess, 'ExitProcess'            import msvcrt,\  printf, 'printf',\          getch, '_getch'

На самом деле из всей этой каши текста, команд всего 3: на 16, 18, 21 строках. (и то это не команды, а макросы. Мы к командам даже не подобрались)

Все остальное это просто подготовка программы к запуску.

Программа при запуске должна выглядеть так:

Самое интересное то что программа весит 2КБ. (Можно сократить и до 1КБ, но для упрощения и так пойдет)

Разбор: что значат этот весь текст?

На 1 строчке: "format PE Console" - это строчка говорит FASM-у какой файл скомпилировать, точнее 1 слово, все остальные слова это аргументы (можно так сказать).

PE - EXE файл, программа.

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

Но есть кроме это остальные:

  • format MZ - EXE-файл НО под MS-DOS

  • format PE - EXE-файл под Windows, аналогично format PE GUI 4.0

  • format PE64 - EXE-файл под Windows, 64 битное приложение.

  • format PE GUI 4.0 - EXE-файл под Windows, графическое приложение.

  • format PE Console - EXE-файл под Windows, консольная программа. (просто подключается заранее консоль)

  • format PE Native - драйвер

  • format PE DLL - DLL-файл Windows, поясню позднее.

  • format COFF - OBJ-файл Linux

  • format MS COFF - аналогично предыдущему

  • format ELF - OBJ-файл для gcc (Linux)

  • format ELF64 - OBJ-файл для gcc (Linux), 64-bit

Сразу за командой (для компилятора) format PE Console идет ; это значит комментарий. К сожалению он есть только однострочный.

3 строка: entry start

  • Говорим windows-у где\в каком месте стартовать. "start" это метка, но о метках чуть позже.

5 строка: include 'win32a.inc'

  • Подключает к проекту файл, в данном случае "win32a.inc" он находиться в папке INCLUDE (в папке с FASM). этот файл создает константы и создает макросы для облегчения программирования.

8 строка: section '.data' data readable writeable

  • Секция данных, то есть программа делиться на секции (части), к этим секциям мы можем дать разрешение, имя.

Флаг "data" (Флаг это бит\байт\аргумент хранившей в себе какую-то информацию) говорит то что эта секция данных.

Флаги "readable writeable" говорят то что эта секция может читаться кем-то и записываться кем-то.

Текст '.data' - имя секции

10 строка: hello db 'hello world!',0

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

db - говорит то что под каждый символ резервируем 1 байт. То есть 1 символ храниться в одном байте.

'hello world!' - наша строка в кодировке ASCII

Что значит ",0" в конце строки? - это символ с номером 0 (или просто ноль), у вас на клавиатуре нет клавиши которая имела символ с номером 0, по этому этот символ используют как показатель конца строки. То есть это значит конец строки. Просто ноль записываем в байт после строки.

12 строка: section '.code' code readable writeable executable

Флаг "code" - говорит то что это секция кода.

Флаг "executable" - говорит то что эта секция исполняема, то есть в этой секции может выполняться код.

Все остальное уже разобрали.

14 строка: start:

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

15 строка: invoke printf, hello

  • Функция printf - выводит текст\число в консоль. В данном случае текст по адресу "hello"

Это штото на подобие команды, но это и близко не команда ассемблера, а просто макрос.

Макрос - Это макро команда для компилятора, то есть вместо имени макроса подставляется что-то другое.

Например, макро команда invoke делиться на такие команды: (взят в пример команда с 15 строки)

push hellocall [printf]

Не переживайте если нечего не поняли.

17 строка: invoke getch

  • getch - функция получения нажатой кнопки, то есть просто ждет нажатия кнопки и потом возвращает нажатую кнопку.

20 строка: invoke ExitProcess, 0

  • ExitProcess - WinAPI функция, она завершает программу. Она принимает значение, с которым завершиться, то есть код ошибки, ноль это нет ошибок.

23 строка: section '.idata' data import readable

Флаг "import" - говорит то что это секция импорта библиотек.

24-25 строки:

library kernel, 'kernel32.dll',\  msvcrt, 'msvcrt.dll'
  • Макро команда "library" загружает DLL библиотеки в виртуальную память (не в ОЗУ, вам ОЗУ не хватит чтоб хранить всю виртуальную память).

Что такое DLL объясню позже.

kernel - имя которое привязывается к библиотеке, оно может быть любым.

Следующий текст после запятой: 'kernel32.dll' - это имя DLL библиотеки который вы хотите подключить.

Дальше есть знак \ это значит что текст на следующей строке нужно подставить в эту строку.

То есть код:

library kernel, 'kernel32.dll',\  msvcrt, 'msvcrt.dll'

Заменяется на:

library kernel, 'kernel32.dll', msvcrt, 'msvcrt.dll'

Это нужно потому что у ассемблера 1 строка это 1 команда.

27-28 строка:

import kernel,\  ExitProcess, 'ExitProcess'

import - Макро команда, которая загружает функции из DLL.

kernel - Имя к которой привязана DLL, может быть любым.

ExitProcess - Как будет называться функция в программе, это имя будет только в вашей программе, и по этому имени вы будете вызывать функцию. (WinAPI функция)

'ExitProcess' - Это имя функции которое будет загружено из DLL, то есть это имя функции которое прописано в DLL.

Дальше думаю не стоит объяснять, вроде все понятно.

Что такое DLL библиотека?

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

Подводим итог

На ассемблере писать можно не зная самого языка, а используя всего лишь макро команды компилятора. За всю статью я упомянул всего 2 команды ассемблера это push hello и call [printf] . Что это значит расскажу в следующей статье.

Подробнее..
Категории: Assembler , Туториал , Уроки , Winapi , Fasm , Windowsapi

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru