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

Python

Перевод Python 18 задач на вывод символов по заданному шаблону

11.04.2021 16:04:19 | Автор: admin
Подготовка к техническому собеседованию по Python нелёгкая задача. На таком собеседовании вам, вполне возможно, встретятся задачи на вывод символов по заданным шаблонам. Если вы хотите научиться решать такие задачи вам может пригодиться подборка способов их решения, приведённая в этом материале.



Здесь продемонстрировано 18 примеров кода. Начинающие программисты вполне могут проработать всё по порядку, а опытные могут разобраться именно с тем, что им нужно. Главное понять, как устроен тот или иной пример. Отсутствие чёткого понимания того, что происходит в программах, способно сыграть злую шутку с тем, кто, например, заучив фрагмент кода и воспроизведя его на собеседовании, попытается объяснить то, как именно этот код работает. А тех, кто проводит собеседование, часто интересуют именно такие вот разъяснения.

1. Простой числовой треугольник


Желаемый результат:

12 23 3 34 4 4 45 5 5 5 5

Код:

rows = 6for num in range(rows):for i in range(num):print(num, end=" ") # вывод числа# вывод пустой строки после каждой строки с числами для правильного отображения шаблонаprint(" ")

2. Обратный числовой треугольник


Желаемый результат:

1 1 1 1 1 2 2 2 2 3 3 3 4 4 5

Код:

rows = 5b = 0for i in range(rows, 0, -1):b += 1for j in range(1, i + 1):print(b, end=' ')print('\r')

3. Полупирамида из чисел


Желаемый результат:

11 21 2 31 2 3 41 2 3 4 5

Код:

rows = 5for row in range(1, rows+1):for column in range(1, row + 1):print(column, end=' ')print("")

4. Обратная пирамида из уменьшающихся чисел


Желаемый результат:

5 5 5 5 5 4 4 4 4 3 3 3 2 2 1

Код:

rows = 5for i in range(rows, 0, -1):num = ifor j in range(0, i):print(num, end=' ')print("\r")

5. Обратная пирамида, все элементы которой представлены одним и тем же числом


Желаемый результат:

5 5 5 5 5 5 5 5 5 5 5 5 5 5 5

Код:

rows = 5num = rowsfor i in range(rows, 0, -1):for j in range(0, i):print(num, end=' ')print('\r')

6. Пирамида из чисел, расположенных в обратном порядке


Желаемый результат:

12 13 2 14 3 2 15 4 3 2 1

Код:

rows = 6for row in range(1, rows):for column in range(row, 0, -1):print(column, end=' ')print("")

7. Обратная полупирамида из чисел


Желаемый результат:

0 1 2 3 4 5 0 1 2 3 4 0 1 2 3 0 1 2 0 1

Код:

rows = 5for i in range(rows, 0, -1):for j in range(0, i + 1):print(j, end=' ')print('\r')

8. Пирамида из натуральных чисел меньше 10


Желаемый результат:

12 3 45 6 7 8 9

Код:

currentNumber = 1stop = 2rows = 3 # Количество строк, из которых состоит пирамидаfor i in range(rows):for column in range(1, stop):print(currentNumber, end=' ')currentNumber += 1print("")stop += 2

9. Пирамида из чисел от 10, расположенных в обратном порядке


Желаемый результат:

13 26 5 410 9 8 7

Код:

start = 1stop = 2currentNumber = stopfor row in range(2, 6):for col in range(start, stop):currentNumber -= 1print(currentNumber, end=' ')print("")start = stopstop += rowcurrentNumber = stop

10. Пирамида из определённых наборов цифр


Желаемый результат:

11 2 11 2 3 2 11 2 3 4 3 2 11 2 3 4 5 4 3 2 1

Код:

rows = 6for i in range(1, rows + 1):for j in range(1, i - 1):print(j, end=" ")for j in range(i - 1, 0, -1):print(j, end=" ")print()

11. Обратная пирамида из связанных чисел


Желаемый результат:

5 4 3 2 1 1 2 3 4 55 4 3 2 2 3 4 55 4 3 3 4 55 4 4 55 5

Код:

rows = 6for i in range(0, rows):for j in range(rows - 1, i, -1):print(j, '', end='')for l in range(i):print('', end='')for k in range(i + 1, rows):print(k, '', end='')print('\n')

12. Пирамида из чётных чисел


Желаемый результат:

10 10 8 10 8 6 10 8 6 4 10 8 6 4 2

Код:

rows = 5LastEvenNumber = 2 * rowsevenNumber = LastEvenNumberfor i in range(1, rows+1):evenNumber = LastEvenNumberfor j in range(i):print(evenNumber, end=' ')evenNumber -= 2print("\r")

13. Пирамида из наборов чисел


Желаемый результат:

00 10 2 40 3 6 90 4 8 12 160 5 10 15 20 250 6 12 18 24 30 36

Код:

rows = 7for i in range(0, rows):for j in range(0, i + 1):print(i * j, end=' ')print()

14. Пирамида, в каждой строке которой выводятся разные числа


Желаемый результат:

13 35 5 57 7 7 79 9 9 9 9

Код:

rows = 5i = 1while i <= rows:j = 1while j <= i:print((i * 2 - 1), end=" ")j = j + 1i = i + 1print()

15. Зеркально отражённая пирамида из чисел (прямоугольный числовой треугольник)


Желаемый результат:

11 21 2 31 2 3 41 2 3 4 5

Код:

rows = 6for row in range(1, rows):num = 1for j in range(rows, 0, -1):if j > row:print(" ", end=' ')else:print(num, end=' ')num += 1print("")

16. Равносторонний треугольник из символов *


Желаемый результат:

** ** * ** * * ** * * * ** * * * * ** * * * * * *

Код:

size = 7m = (2 * size) - 2for i in range(0, size):for j in range(0, m):print(end=" ")m = m - 1 # уменьшение m после каждого прохода циклаfor j in range(0, i + 1):# вывод пирамиды из звёздочекprint("*", end=' ')print(" ")

17. Перевёрнутый треугольник из символов *


Желаемый результат:

* * * * * ** * * * ** * * ** * ** **

Код:

rows = 5k = 2 * rows - 2for i in range(rows, -1, -1):for j in range(k, 0, -1):print(end=" ")k = k + 1for j in range(0, i + 1):print("*", end=" ")print("")

18. Пирамида из символов *


Желаемый результат:

** ** * ** * * ** * * * *

Код:

rows = 5for i in range(0, rows):for j in range(0, i + 1):print("*", end=' ')print("\r")

Какие задачи вы посоветовали бы прорешать тем, кто готовится к собеседованию по Python?

Подробнее..

Сравнение ассортимента блюд трёх ресторанов Санкт-Петербурга

07.04.2021 20:14:16 | Автор: admin

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

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

Сайты для сбора данных были подобраны по принципу нет блокировщика парсеров и из анализа этих данных может выйти что-то интересное. Поэтому выбор пал на ассортимент блюд на доставку трёх ресторанов Санкт-Петербурга - Токио City, Евразия и 2 Берега. У них приблизительно одна направленность кухни и похожий ассортимент, поэтому явно найдется, что сравнить.

Поделюсь самим парсером для одного из ресторанов.

import requestsfrom bs4 import BeautifulSoupimport pandas as pdimport datetimeprint("Начало парсинга Токио Сити: " + str(datetime.datetime.now()))#все страницы с информацией о менюurllist = ['https://www.tokyo-city.ru/spisok-product/goryachie-blyuda1.html',           'https://www.tokyo-city.ru/spisok-product/sushi.html',           'https://www.tokyo-city.ru/spisok-product/rolly.html',           'https://www.tokyo-city.ru/spisok-product/nabory.html',           'https://www.tokyo-city.ru/spisok-product/new_lunches.html',           'https://www.tokyo-city.ru/spisok-product/pitctca.html',           'https://www.tokyo-city.ru/spisok-product/salaty.html',           'https://www.tokyo-city.ru/spisok-product/-supy-.html',           'https://www.tokyo-city.ru/spisok-product/goryachie-zakuski1.html',           'https://www.tokyo-city.ru/spisok-product/wok.html',           'https://www.tokyo-city.ru/spisok-product/pasta.html',           'https://www.tokyo-city.ru/spisok-product/gamburgery-i-shaverma.html',           'https://www.tokyo-city.ru/spisok-product/Tokio-FIT.html',           'https://www.tokyo-city.ru/spisok-product/deserty.html',           'https://www.tokyo-city.ru/spisok-product/childrensmenu.html',           'https://www.tokyo-city.ru/spisok-product/napitki1.html',           'https://www.tokyo-city.ru/new/',           'https://www.tokyo-city.ru/spisok-product/postnoe-menyu.html',           'https://www.tokyo-city.ru/hit/',           'https://www.tokyo-city.ru/vegetarian/',           'https://www.tokyo-city.ru/hot/',           'https://www.tokyo-city.ru/offers/',           'https://www.tokyo-city.ru/spisok-product/sauces.html',           'https://www.tokyo-city.ru/spisok-product/Pirogi-torty.html']#создаем пустые списки для записи всех данныхnames_all = []descriptions_all = []prices_all = []categories_all = []url_all = []weight_all = []nutr_all = []#собираем данныеfor url in urllist:    response = requests.get(url).text    soup = BeautifulSoup(response, features="html.parser")    items = soup.find_all('a', class_='item__name')    itemsURL = []    n = 0    for n, i in enumerate(items, start=n):        itemnotfullURL = i.get('href')        itemURL = 'https://www.tokyo-city.ru' + itemnotfullURL        itemsURL.extend({itemURL})        m = 0        namesList = []        descriptionsList = []        pricesList = []        weightList = []        nutrList = []        itemResponse = requests.get(itemURL).text        itemsSoup = BeautifulSoup(itemResponse, features="html.parser")        itemsInfo = itemsSoup.find_all('div', class_='item__full-info')        for m, u in enumerate(itemsInfo, start=m):            if (u.find('h1', class_='item__name') == None):                itemName = 'No data'            else:                itemName = u.find('h1', class_='item__name').text.strip()            if (u.find('p', class_='item__desc') == None):                itemDescription = 'No data'            else:                itemDescription = u.find('p', class_='item__desc').text.strip()            if (u.find('span', class_='item__price-value') == None):                itemPrice = '0'            else:                itemPrice = u.find('span', class_='item__price-value').text            if (u.find('div', class_='nutr-value') == None):                itemNutr = 'No data'            else:                itemNutr = u.find('div', class_='nutr-value').text.strip()            if (u.find('div', class_='item__weight') == None):                itemWeight = '0'            else:                itemWeight = u.find('div', class_='item__weight').text.strip()            namesList.extend({itemName})            descriptionsList.extend({itemDescription})            pricesList.extend({itemPrice})            weightList.extend({itemWeight})            nutrList.extend({itemNutr})        df = pd.DataFrame((            {'Name': namesList,             'Description': descriptionsList,             'Price': pricesList,             'Weight': weightList,             'NutrInfo': nutrList             }))        names_all.extend(df['Name'])        descriptions_all.extend(df['Description'])        prices_all.extend(df['Price'])        weight_all.extend(df['Weight'])        nutr_all.extend(df['NutrInfo'])        df['Category'] = soup.find('div', class_='title__container').text.strip()        categories_all.extend(df['Category'])result = pd.DataFrame((    {'Name': names_all,     'Description': descriptions_all,     'Price': prices_all,     'Category': categories_all,     'NutrInfo': nutr_all,     'Weight': weight_all,     }))print("Парсинг Токио Сити окончен: " + str(datetime.datetime.now()))

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

А теперь к самому интересному - анализу полученной информации.

Начальные данные:

Наименование каждого блюда, его состав, цена, вес, калорийность, БЖУ и категория, к которой это блюдо относится.

Кусочек готовой к анализу таблицы с ассортиментом:

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

  • Токио City

Меню ресторана Токио City представлено 19 уникальными категориями и 5 дублирующимися, куда попадают блюда из других категорий, соответствующие определённому признаку (например, акционные блюда или подходящие вегетарианцам). Общее количество уникальных блюд - 351.

  • Евразия

Ассортимент блюд в Евразии несколько меньше - 13 категорий, 301 уникальное блюдо. Несмотря на то, что само название Токио City намекает на большое разнообразие японских блюд, этот ресторан предлагает почти на 40% меньше суши и роллов, чем, казалось бы, более универсальная кухня Евразии.

  • 2 Берега

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

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

Вопрос 1: какую долю занимает фастфуд от всего меню уникальных блюд каждого ресторана?

К фастфуду относятся бургеры, пицца и разного рода стритфуд вроде шавермы.

В данном случае важно правильно определить, от каких категорий нужно считать процент, потому что некоторые категории не относятся к полноценным блюдам, а другие являются их дублированием. Поэтому из расчёта этого показателя убраны такие категории, как Напитки, Детское меню, Соусы, Наборы, Ланчи и Десерты + всевозможные вариации данных категорий.

Итог:

Выходит, 2 Берега - 1 по разнообразию пиццы в ассортименте. Это подтверждается, даже если просто сравнить количество блюд в категории Пицца во всех ресторанах (Токио City - 20, Евразия - 17 и 2 Берега - 51).

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

Вопрос 2: в каком из трёх ресторанов самые выгодные и сытные порции?

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

Посчитав цену за 100 грамм блюда в каждом ресторане, получаем следующие результаты:

У 2 Берега нет такой категории, как Горячие блюда. Есть ВОКи и паста, но традиционных горячих блюд вида гарнир + мясо нет. Поэтому в категории Горячие блюда сравниваются только Токио City и Евразия.

По всем категориям Токио City является безусловным лидером по соотношению цены и веса блюда. 2 Берега занимает почётное 2 место. Евразия оказывается в хвосте рейтинга. Даже если вычесть из средней цены за 100 грамм блюда в Евразии 30% (это максимальная скидка, которую предоставляет ресторан по картам лояльности), ресторан все равно ни в одной категории не сможет обогнать Токио City по выгоде.

Теперь изучим размер порций, которые могут предложить данные рестораны:

Евразия снова по всем категориям не смогла обогнать другие рестораны. Средний недовес порции составляет 30% относительно двух других ресторанов.

Зато 2 Берега отличился лучшим весом супов, салатов и детских блюд. Кстати, такой большой средний вес в категории Детские блюда у этого ресторана объясняется тем, что там представлены только наборы, в составе которых 2 блюда + напиток (вес напитка здесь не учитывается). Но даже с учётом этого факта можно похвалить этот ресторан за щедрые порции детского меню.

А Токио City предлагает отличные порции горячих блюд.

Вопрос 3: какова средняя калорийность блюда в каждом из ресторанов?

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

Калорийность половины блюд в Токио City не превышает 205 калорий в 100 граммах, поэтому присуждаем ресторану одного толстого кота из трёх. Это достаточно позитивный показатель для тех, кто следит за своим весом. А вот у блюд ресторана 2 Берега этот показатель на 35% выше, поэтому он получает максимальное количество толстых котов. Впрочем, в этом нет ничего удивительного, если вспомнить, какую долю от всего меню этого ресторана занимает пицца.

Последний вопрос: насколько сбалансированное питание может предложить каждый из ресторанов?

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

Несмотря на самую высокую калорийность на 100 грамм и большое количество фастфуда 2 Берега предлагает достаточно сбалансированное меню, тогда как у того же Токио City можно заметить явный перекос в сторону углеводов.

БЖУ Евразии какое-то слишком равномерное, практически без выбросов, поэтому вызывает подозрения.

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


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

Подробнее..

Ontol подборка видео-лекций и каналов для продвинутых программистов

14.04.2021 12:16:12 | Автор: admin
image

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

В перерывах между полетами на реактивном ранце и переводами материалов Y Combinator, я делаю проект Ontol такое место в сети, где максимальная концентрация полезного, апгрейдящего мировоззрение материала (ценного на горизонте 10+ лет, например, такого), которым можно делиться бесплатно в 1 клик. (канал в телеграм: t.me/ontol)

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



Simple Made Easy 2012 (Rich Hickey)


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



The Mess We're In (Joe Armstrong)


Джо Армстронг один из создателей Erlang. Он работал в лаборатории компьютерных наук Эрикссон в 1986 году и был частью команды, которая разработала и внедрила первую версию Erlang. Он написал несколько книг про Erlang, в том числе Programming Erlang Software for a Concurrent World. Джо имеет докторскую степень в области компьютерных наук Королевского технологического института в Стокгольме, Швеция.



The Unreasonable Effectiveness of Multiple Dispatch (Karpinski)


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

  1. Совместное использование базовых типов в большом количестве пакетов, обеспечивающих разную функциональность;
  2. Совместное использование общих алгоритмов, которые работают с различными реализациями общих абстракций.

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



Low Level JavaScript


image

Low Level JavaScript это канал YouTube, который глубоко погружается в суть низкоуровневого программирования возится с единицами и нулями но все это делается на чистом JavaScript.

Пример лекции:



David Beazley


Дэвид Бизли автор книг Python Cookbook и Python Essential Reference. Вот его канал.

Пример лекции:



Jacob Sorber


Якоб Сорбер освещает темы, полезные как для новичков, так и для продвинутых: network programming, threads, processes, operating systems, embedded systems и других.

Пример лекции:



Computerphile


Канал Computerphile младший брат Numberphile. Про всякие компьютерные штуки.

Пример лекции:



Category Theory (Bartosz Milewski)


Серия Теория категорий Бартоша Милевски открывает новый взгляд на программирование в целом.

Пример лекции:



Build a 65c02-based computer from scratch (Ben Eater)


Разбираемся, как работают компьютеры. В этих лекциях Ben Eater создет и программирет базовый компьютер с классическим микропроцессором 6502.

Пример лекции:



Building an 8-bit breadboard computer! (Ben Eater)


Попытка построить еще один 8-битный компьютер с нуля.

Пример лекции:



How to Become a Good Backend Engineer (Hussein Nasser)


Прокачиваем Backend.

Пример лекции:



Semicolon&Sons


Хардкорные скринкасты для программистов, которые создают собственный бизнес.

Пример лекции:



Andrew Kelley


Эндрю создает язык системного программирования Zig. Но он также останавливается на других общих проблемах системного программирования, которые не зависят от языка. Перееехал с YouTube на Vimeo из-за бесящей рекламы.

Пример видео:



Jon Gjengset


Мы создаем библиотеки и инструменты на языке программирования Rust. У Джона Дженгсета лучший канал про Rust для учеников среднего и продвинутого уровней. Также он соавтор/создатель Missing Semester

Пример лекции:



George Hotz | Programming | Livecoding SLAM | twitchslam | Part1


8-часовой прямой эфир. Изучение контрактов на блокчейн и обнаружение ошибки безопасности в одном из них.



Jordan Harrod


Аспирантка Гарварда и Массачусетского технологического института, изучает интерфейсы мозг-машина и машинное обучение для медицины (анестезия) и рассказывает про взаимодействие человека с ИИ и алгоритмами.

Пример видео:



ACM SIGPLAN


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

Пример видео:



Fun Fun Function


Канал, где можно узнать и про софт скилы и про трансдьюсеры.

Пример лекции:



GOTO Conferences


Канал от сообщества GOTO

Пример видео:



Javidx9


Для тех, кто занимается программированием игр. Видео от Javidx9 четкие, лаконичные и насыщенные примерами.

Пример видео:



TechLead


Уволенный из Google и Facebook техлид за стаканчиком кофе делится мудростью.



TheCherno


Канал от бывшего разработчика из EA. Видео в основном посвящены C ++ и разработке игровых движков.

Пример видео:



DefogTech


Темы канала: Java concurrency, distributed systems, system design, microservice.

Пример видео:



Simons Institute video archive

.
Больше теории, чем программирования, но много концепций SOTA.

Пример видео:



C Weekly With Jason Turner


Советы и новости про C++. И живое программирование.

Пример видео:



CppCon 2020


CppCon это ежегодная недельная встреча всего сообщества C ++. Канал конференции.

Пример видео:



NDC London 2020


Канал крупнейшей в Европе конфы по .NET & Agile development

Пример видео:



Подробнее..

Что не так с вашей консольной программой?

14.04.2021 12:16:12 | Автор: admin

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

Но как часто мы обсуждаем наши повседневные инструменты с точки зрения читабельности, хотя пишем под web и каждый день используем консольные утилиты? Сегодня Андрей Светлов расскажет, что со всем этим делать, и чем он пользуется для консолей. Помимо того, что Андрей CPython Core developer и понемногу развивает Python, в свободное от работы время он эксперт по asyncio, со-автор aiohttp, yarl, multidict и прочим популярным библиотекам.

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

Информативность

Это самая первая и очевидная проблема. Например, у всем известного Dockera вывод это простыня ровного, скучного и не выделяемого текста. В нем просто трудно ориентироваться:

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

Размер шрифта и экрана

Снова тот же Docker. Если экран не очень широкий, если шрифт большой или вертикальный экран, то Docker перестает помещаться и выводится на две строки:

И это еще не худший случай, тут можно о чем-то догадаться. А если у вас широченная таблица, которая начинает схлопываться в 3, 4 и более строчек, то разобраться в ней в таком виде решительно невозможно.

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

Scrolling & Pager

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

Светлый/темный цвет фона

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

Что из этого следует? Возвращаемся к примеру от Github. Вот вывод команды (неважно какой, сейчас это статус моих pull requests) на темном фоне:

И он же со светлой схемой:

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

ЦВЕТА И СТИЛИ

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

На этом выбор цветов для нас закончен! Конечно, такая очень узкая палитра не позволяет использовать полноцветные терминалы и точно перенести всё, что ваш UX дизайнер нарисовал в Фотошопе. Но зато у нее есть одно чудесное свойство: если приглядеться, то видно, что, например, желтый это не классический желтый, а немного золотистый, чтобы выглядело хорошо. И все цвета подобраны программой терминала так же. Если меняется тема, то программа снова подскажет, как правильно, хорошо, контрастно и красиво нарисовать этот цвет для терминала.

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

Обычно мы, конечно, выбираем понятные всем цвета:

  • Зеленый все хорошо;

  • Красный плохо;

  • Желтый warning.

Но если хотим большего, у нас есть подсказка для команды LS есть соглашение, каким цветом мы будем выводить файлы и папки. Это обеспечивается двумя переменными среды. Первая исторически появилась в виде LSCOLORS и рассказывает, как рисовать файлы и папки: по две буквы на одну позицию. Позиция это папка (нормальный файл, сокет, что-то еще). В документации или в интернете это всё есть. Первая буква отвечает за цвет шрифта (букв), вторая за цвет фона. Я попытался раскрасить так, как у меня закодировано на моей рабочей станции

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

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

Смайлики

Смайлики нельзя недооценивать, это очень хорошая вещь простенький значок, но позволяет быстро сориентироваться на экране:

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

Поэтому выбирайте простые символы, смотрите, как они рендерятся в разных режимах и проверяйте это на всех платформах (Windows, MAC, Ubuntu). И математические символы в том числе. Везде есть хитрые смайлики, с которыми могут быть проблемы.

Shell

Еще нужно помнить, что терминал существует не сам по себе. Консольные программы, в нем запускаются под разными shell: sh, bash, zsh, fish, cmd.exe, powershell или еще какие-то. Программа должна работать с выбранным shell без проблем, в том числе на Windows. Но на практике мы видим разницу в том, как shell авто-дополняет ввод и как (и в каком терминале) выводятся символы. Поэтому проверяйте и на своем shell, и на тех, которые будут у пользователей.

TTY навсегда?

Помимо shell и интерактивного режима, на который в консолях тратится большая часть усилий по пользовательскому дизайну, программы у нас могут запускаться и без терминала. И когда мы, например, перенаправляем вывод через py в grep, чтобы что-нибудь там поискать, или записываем в файл, или запускаем из-под cron, HTTP-сервера или еще чего-нибудь функция os.isatty() будет возвращать false:

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

Windows, любовь моя

К сожалению, мир консольных программ делится не только на темный и светлый фон, а еще на Windows и всех остальных. Если на posix системах (тех же MAC и Linux) всё весьма похоже, то на Windows есть много отличий, например:

  • less more. Стандартная прокрутка less отсутствует, вместо нее есть куда более гадкое и неудобное more.

  • \n \r\n. Возврат каретки другой.

  • dim / gray. Серого цвета нет (но на MAC, кстати, тоже бардак по поводу цвета, поэтому и надо всё проверять).

  • ANSI escape символы, которые как раз делают расцветку и прочие полезные вещи, по умолчанию выключены, но это легко поправить.

  • -\|/. Многие символы, как я говорил, не работают. Например, здесь наш специалист по UX создал дизайнерский спиннер у нас он должен крутиться треугольниками, а не палочками, как у всех остальных. Почему бы и нет? Но на Windows он крутится одинаковыми квадратиками, то есть не работает.

Инструменты

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

CLICK

Я очень рекомендую Click от славного парня Армена Ронахена. Это инструмент со своими особенностями, но он гораздо лучше и мощнее, чем встроенный в Python argparse. Если вы сомневаетесь, используйте Click.

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

Кроме того, у Click есть маленькая, но очень приятная и удобная фича он автоматически убирает стили для не-терминала (non-TTY). Click сам понимает, когда вывод идет не на полноценный терминал, а, например, куда-нибудь в файл он автоматически снимает все стили и делает click.unstyle. Конечно, вы можете сделать unstyling сами, вместо использования click. Но в любом случае избегайте перенаправления в файл покореженного текста с кучей непонятных значков.

PYTHON PROMPT TOOLKIT

Второй инструмент чудесная штука, которая используется, например, в IPython, BPython, в других shell это инструмент для создания полноценных приложений. Но нас сейчас интересуют вопросы ввода-вывода и здесь Prompt Toolkit решает все вопросы.

Сначала мне показалось, что Prompt Toolkit избыточен потому что для работы хватает и Click. Например, если нужен progressbar, есть всем известный tqdm. Великолепная библиотека, которая решает ровно одну задачу, но делает это хорошо. А еще есть и click.progressbar().

Prompt Toolkit же позволяет легко и просто создавать различные варианты ввода-вывода, используя стили, шапки и прочие штуки. Например, есть обновляемый виджет для progressbar. Вроде бы ничего особого, но у Prompt Toolkit это не один виджет для progressbar.

Из Prompt Toolkit можно собирать очень сложные вещи, используя layout, виджеты, компоновку. А если чего-то нет из коробки, это можно написать.

Благодаря слоям, в Python Prompt Toolkit можно легко отрисовать несколько progressbar-ов по одному на слой загружаемого образа таких же, как например, делает Docker pool:

ВСЁ ПРОПАЛО, ШЕФ! ИЛИ "ГДЕ МОЙ КУРСОР?"

Мелочь, которая в свое время попортила мне немало крови.

Распространенная тема: есть консольная программа, которая рисует чудесные виджеты, рассказывает, как Docker Image тянется на много потоков, даже не моргает и отрисовывает всё гладко. Но если ее внезапно закрыть, может, например, пропасть курсор потому что в последнем режиме курсор спрятали, а обратно не вернули. Бывает, что вы в терминале печатаете, а курсор не мигает на экране. Есть и более сложные способы испортить консоль, загнав ее в какой-нибудь режим, который не предназначен для интерактивного вывода.

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

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

ASYNCIO + CLICK

Я не могу не рассказать про asyncio!

Но Click из коробки не работает с asyncio от слова совсем! Он это не умеет, он написан немного раньше, и они совсем не дружат. Поэтому самым простым решением будет написать AsyncioRunner(), который будет не функцией, а классом, а run можно вызывать несколько раз. Это бывает удобно, например, когда запускаем асинхронный код для проверки типов входных параметров и вдобавок что-то еще run запускаются друг за другом в одном и том же контексте:

И что важно AsyncioRunner() работает при этом как асинхронный контекстный менеджер, то есть по завершению работы чистит за собой.

Мы у себя используем простое правило: неблокирующий код (тот, который выполняется мгновенно) может быть синхронным, пока Click не читает файлы, не лезет в интернет или еще что-нибудь такое не делает. Но как только нам нужно запускать асинхронный код, мы пользуемся AsyncioRunner(). Легко создается какой-нибудь декоратор, который внутри async-команд сделает все, что нам надо:

ASYNCIO + PROMPT_TOOLKIT

А вот Asyncio + Prompt_Toolkit работают вместе великолепно даже из коробки. Prompt_Toolkit знает об asyncio, а Prompt_async это стандартная штука Prompt_Toolkit, которая и запускает основную программу. Детали читайте в документации:

WINDOWS не отпускает

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

Давно известный проект Colorama работает почти со всеми escape-последовательностями, подменяя собой stdout и stderr. Он парсит то, что печатается, находит там escape-последовательности и убирает их. Вместо этого вызываются разные Windows-функции для того, чтобы поменять тот же самый цвет букв или цвет фона. Но Colorama работает только с подмножеством ANSI-символов.

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

Но, к счастью, сейчас наступила эпоха Windows 10. К счастью, потому что в ней можно перевести экран в режим, который обрабатывает escape-последовательности (по умолчанию он не включен). Этот режим позволяют включить две простые функции, вызвать их из Python при помощи ctypes это упражнение на пару минут:

АВТОЗАПОЛНЕНИЕ

В Click оно есть из коробки, но не для Windows. Так уж получилось. Может быть, в следующих версиях будет по-другому. Для Unix-мира оно есть, и это уже хорошо.

Здесь декоратор принимает click.argument от autocompletion то есть функция вызывается тогда, когда в процессе вывода мы нажимаем табуляцию, как обычный Bash, а еще лучше Zsh как это делают shell для большого количества команд.

ВАЛИДАЦИЯ

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

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

Подробнее..

Перевод Как сделать полнотекстовую поисковую машину на 150 строках кода Python

07.04.2021 20:14:16 | Автор: admin

Полнотекстовый поиск неотъемлемая часть нашей жизни. Разыскать нужные материалы в сервисе облачного хранения документов, найти фильм в Netflix, купить туалетную бумагу на Ozon или отыскать с помощью сервисов Google интересующую информацию в Интернете наверняка вы сегодня уже не раз отправляли похожие запросы на поиск нужной информации в невообразимых объёмах неструктурированных данных. И что удивительнее всего несмотря на то что вы осуществляли поиск среди миллионов (или даже миллиардов) записей, вы получали ответ за считанные миллисекунды. Специально к старту нового потока курса Fullstack-разработчик наPython в данной статье мы рассмотрим основные компоненты полнотекстовой поисковой машины и попытаемся создать систему, которая сможет за миллисекунды находить информацию в миллионах документов и ранжировать результаты по релевантности, причём всю систему можно воплотить всего в 150 строках кода на Python!


Данные

Весь код, использованный в данной статье, можно найти на Github. Здесь я приведу ссылки на фрагменты кода, и вы сами сможете попробовать с ним поработать. Весь пример можно запустить, установив файл requirements (pip install -r requirements.txt) и запустив на выполнение файл python run.py. Данная команда загрузит все данные и запустит пример поискового запроса с ранжированием и без ранжирования.

Перед тем как начать создание поисковой машины, нужно подготовить полнотекстовые неструктурированные данные, в которых будет осуществляться поиск. Искать будем в аннотациях статей из английской Википедии. Это заархивированный с помощью gzip утилиты сжатия и восстановления файлов XML-файл размером около 785 Мбайт, содержащий приблизительно 6,27 миллионов аннотаций [1]. Для загрузки архивированного XML-файла я написал простую функцию, но вы можете поступить проще загрузить его вручную.

Подготовка данных

Все аннотации хранятся в одном большом XML-файле. Аннотации в таком файле отделяются друг от друга элементом <doc> и выглядят примерно так (элементы, которые нас не интересуют, я опустил):

<doc>    <title>Wikipedia: London Beer Flood</title>    <url>https://en.wikipedia.org/wiki/London_Beer_Flood</url>    <abstract>The London Beer Flood was an accident at Meux & Co's Horse Shoe Brewery, London, on 17 October 1814. It took place when one of the  wooden vats of fermenting porter burst.</abstract>    ...</doc>

Интерес для нас представляют следующие разделы: title, url abstract (сам текст аннотации). Чтобы было удобнее обращаться к данным, представим документы как класс данных Python. Добавим свойство, конкатенирующее заголовок и содержание аннотации. Код можно взять здесь.

from dataclasses import dataclass@dataclassclass Abstract:    """Wikipedia abstract"""    ID: int    title: str    abstract: str    url: str    @property    def fulltext(self):        return ' '.join([self.title, self.abstract])

Затем нужно извлечь данные из XML-файла, осуществить их синтаксический разбор и создать экземпляры нашего объекта Abstract. Весь заархивированный XML-файл загружать в память не нужно, будем работать с потоком данных [2]. Каждому документу присвоим собственный идентификатор (ID) согласно порядку загрузки (то есть первому документу присваивается ID=1, второму ID=2 и т. д.). Код можно взять здесь.

import gzipfrom lxml import etreefrom search.documents import Abstractdef load_documents():    # open a filehandle to the gzipped Wikipedia dump    with gzip.open('data/enwiki.latest-abstract.xml.gz', 'rb') as f:        doc_id = 1        # iterparse will yield the entire `doc` element once it finds the        # closing `</doc>` tag        for _, element in etree.iterparse(f, events=('end',), tag='doc'):            title = element.findtext('./title')            url = element.findtext('./url')            abstract = element.findtext('./abstract')            yield Abstract(ID=doc_id, title=title, url=url, abstract=abstract)            doc_id += 1            # the `element.clear()` call will explicitly free up the memory            # used to store the element            element.clear()

Индексирование

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

Так выглядит книжный алфавитный указательТак выглядит книжный алфавитный указатель

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

{    ...    "london": [5245250, 2623812, 133455, 3672401, ...],    "beer": [1921376, 4411744, 684389, 2019685, ...],    "flood": [3772355, 2895814, 3461065, 5132238, ...],    ...}

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

Выделение лексемВыделение лексем

Анализ

Применим простейший способ выделения лексем: разобьём текст в местах, в которых встречаются пробелы. Затем к каждой лексеме применим пару фильтров: переведём текст лексемы в нижний регистр, удалим любые знаки препинания, исключим 25 наиболее распространённых английских слов (а также слово википедия, так как оно встречается во всех заголовках всех аннотаций) и к каждому слову применим фильтр выделения основы слова (после этой операции разные формы слова, например, красный и краснота будут соответствовать одной и той же основе красн [3]).

Функции выделения лексем и перевода в нижний регистр довольно просты:

import StemmerSTEMMER = Stemmer.Stemmer('english')def tokenize(text):    return text.split()def lowercase_filter(text):    return [token.lower() for token in tokens]def stem_filter(tokens):    return STEMMER.stemWords(tokens)

От знаков пунктуации избавиться также просто к набору пунктуационных знаков применяется регулярное выражение:

import reimport stringPUNCTUATION = re.compile('[%s]' % re.escape(string.punctuation))def punctuation_filter(tokens):    return [PUNCTUATION.sub('', token) for token in tokens]

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

# top 25 most common words in English and "wikipedia":# https://en.wikipedia.org/wiki/Most_common_words_in_EnglishSTOPWORDS = set(['the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have',                 'I', 'it', 'for', 'not', 'on', 'with', 'he', 'as', 'you',                 'do', 'at', 'this', 'but', 'his', 'by', 'from', 'wikipedia'])def stopword_filter(tokens):    return [token for token in tokens if token not in STOPWORDS]

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

def analyze(text):    tokens = tokenize(text)    tokens = lowercase_filter(tokens)    tokens = punctuation_filter(tokens)    tokens = stopword_filter(tokens)    tokens = stem_filter(tokens)    return [token for token in tokens if token]

Индексирование набора документов

Создадим класс Index, в котором будет храниться указатель (index) и документы (documents). В словаре documents будут храниться классы данных по идентификатору (ID), а ключи указателя index будут представлять собой лексемы со значениями идентификаторов документов, в которых встречаются лексемы:

class Index:    def __init__(self):        self.index = {}        self.documents = {}    def index_document(self, document):        if document.ID not in self.documents:            self.documents[document.ID] = document        for token in analyze(document.fulltext):            if token not in self.index:                self.index[token] = set()            self.index[token].add(document.ID)

Поиск

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

def _results(self, analyzed_query):    return [self.index.get(token, set()) for token in analyzed_query]def search(self, query):    """    Boolean search; this will return documents that contain all words from the    query, but not rank them (sets are fast, but unordered).    """    analyzed_query = analyze(query)    results = self._results(analyzed_query)    documents = [self.documents[doc_id] for doc_id in set.intersection(*results)]    return documentsIn [1]: index.search('London Beer Flood')search took 0.16307830810546875 millisecondsOut[1]:[Abstract(ID=1501027, title='Wikipedia: Horse Shoe Brewery', abstract='The Horse Shoe Brewery was an English brewery in the City of Westminster that was established in 1764 and became a major producer of porter, from 1809 as Henry Meux & Co. It was the site of the London Beer Flood in 1814, which killed eight people after a porter vat burst.', url='https://en.wikipedia.org/wiki/Horse_Shoe_Brewery'), Abstract(ID=1828015, title='Wikipedia: London Beer Flood', abstract="The London Beer Flood was an accident at Meux & Co's Horse Shoe Brewery, London, on 17 October 1814. It took place when one of the  wooden vats of fermenting porter burst.", url='https://en.wikipedia.org/wiki/London_Beer_Flood')]

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

def search(self, query, search_type='AND'):    """    Still boolean search; this will return documents that contain either all words    from the query or just one of them, depending on the search_type specified.    We are still not ranking the results (sets are fast, but unordered).    """    if search_type not in ('AND', 'OR'):        return []    analyzed_query = analyze(query)    results = self._results(analyzed_query)    if search_type == 'AND':        # all tokens must be in the document        documents = [self.documents[doc_id] for doc_id in set.intersection(*results)]    if search_type == 'OR':        # only one token has to be in the document        documents = [self.documents[doc_id] for doc_id in set.union(*results)]    return documentsIn [2]: index.search('London Beer Flood', search_type='OR')search took 0.02816295623779297 secondsOut[2]:[Abstract(ID=5505026, title='Wikipedia: Addie Pryor', abstract='| birth_place    = London, England', url='https://en.wikipedia.org/wiki/Addie_Pryor'), Abstract(ID=1572868, title='Wikipedia: Tim Steward', abstract='|birth_place         = London, United Kingdom', url='https://en.wikipedia.org/wiki/Tim_Steward'), Abstract(ID=5111814, title='Wikipedia: 1877 Birthday Honours', abstract='The 1877 Birthday Honours were appointments by Queen Victoria to various orders and honours to reward and highlight good works by citizens of the British Empire. The appointments were made to celebrate the official birthday of the Queen, and were published in The London Gazette on 30 May and 2 June 1877.', url='https://en.wikipedia.org/wiki/1877_Birthday_Honours'), ...In [3]: len(index.search('London Beer Flood', search_type='OR'))search took 0.029065370559692383 secondsOut[3]: 49627

Релевантность

С помощью элементарных команд Python мы создали довольно быстро работающую поисковую систему, но остался один аспект, которого явно не хватает в нашей маленькой поисковой машине, а именно не учтён принцип релевантности. Сейчас программа поиска составлена таким образом, что пользователь получает неупорядоченный список документов и должен сам выбирать из полученного списка действительно нужные ему документы. Но, если результатов очень много, справиться с такой задачей не под силу никакому пользователю (в нашем примере OR программа выдала почти 50000 результатов).

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

Частота вхождения терминов

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

# in documents.pyfrom collections import Counterfrom .analysis import analyze@dataclassclass Abstract:    # snip    def analyze(self):        # Counter will create a dictionary counting the unique values in an array:        # {'london': 12, 'beer': 3, ...}        self.term_frequencies = Counter(analyze(self.fulltext))    def term_frequency(self, term):        return self.term_frequencies.get(term, 0)

При индексировании должен осуществляется подсчёт частот вхождения терминов:

# in index.py we add `document.analyze()def index_document(self, document):    if document.ID not in self.documents:        self.documents[document.ID] = document        document.analyze()

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

def search(self, query, search_type='AND', rank=True):    # snip    if rank:        return self.rank(analyzed_query, documents)    return documentsdef rank(self, analyzed_query, documents):    results = []    if not documents:        return results    for document in documents:        score = sum([document.term_frequency(token) for token in analyzed_query])        results.append((document, score))    return sorted(results, key=lambda doc: doc[1], reverse=True)

Обратная частота документов

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

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

Обратная частота документов для термина определяется делением количества документов (N) в указателе на количество документов, содержащих термин, и взятием логарифма полученного частного.

Обратная частота документов (IDF); формула взята из https://moz.com/blog/inverse-document-frequency-and-the-importance-of-uniquenessОбратная частота документов (IDF); формула взята из https://moz.com/blog/inverse-document-frequency-and-the-importance-of-uniqueness

При ранжировании значение частоты термина умножается на значение обратной частоты документа, и, следовательно, документы, в которых присутствуют термины, редко встречающиеся в наборе документов, будут более релевантными [5]. Значение обратной частоты документов можно легко рассчитать по указателю:

# index.pyimport mathdef document_frequency(self, token):    return len(self.index.get(token, set()))def inverse_document_frequency(self, token):    # Manning, Hinrich and Schtze use log10, so we do too, even though it    # doesn't really matter which log we use anyway    # https://nlp.stanford.edu/IR-book/html/htmledition/inverse-document-frequency-1.html    return math.log10(len(self.documents) / self.document_frequency(token))def rank(self, analyzed_query, documents):    results = []    if not documents:        return results    for document in documents:        score = 0.0        for token in analyzed_query:            tf = document.term_frequency(token)            idf = self.inverse_document_frequency(token)            score += tf * idf        results.append((document, score))    return sorted(results, key=lambda doc: doc[1], reverse=True)

Будущая работа

Мы создали элементарную информационно-поисковую систему всего из нескольких десятков строчек кода Python! Код целиком приведён на Github. Также я написал вспомогательную функцию, загружающую аннотации статей Википедии и создающую указатель. Установите файл requirements, запустите его в выбранной вами консоли Python и получайте удовольствие от работы со структурами данных и операциями поиска.

Данная статья написана с единственной целью привести пример реализации концепции поиска и продемонстрировать, что поисковые запросы (даже с ранжированием) могут выполняться очень быстро (например, на своём ноутбуке с медленным Python я могу осуществлять поиск и ранжирование среди 6,27 миллионов документов). Статью ни в коем случае не следует рассматривать как инструкцию по созданию программного обеспечения промышленного уровня. Моя поисковая машина запускается полностью в памяти ноутбука, но такие библиотеки, как Lucene, используют сверхпроизводительные структуры данных и даже оптимизируют дисковые операции, а такие программные комплексы, как Elasticsearch и Solr, распространяют библиотеку Lucene на сотни, а иногда и тысячи машин.

Но даже наш простенький алгоритм можно существенно улучшить. Например, мы молчаливо предполагали, что каждое поле документа вносит в релевантность одинаковый вклад, но, если поразмыслить, так быть не должно ведь термин, присутствующий в заголовке, очевидно, должен иметь больший вес, чем термин, встречающийся в содержании аннотации. Другой перспективной идеей может стать более продвинутый синтаксический анализ запроса должны ли совпадать все термины? только один термин? или несколько? Зная ответы на эти вопросы, можно повысить качество работы поисковых запросов. Также почему бы не исключить из запроса определённые термины? почему бы не применить операции AND и OR к отдельным терминам? Можно ли сохранить указатель на диск и вывести его, таким образом, за пределы оперативной памяти ноутбука?

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

Узнайте, как прокачаться в других инженерных специальностях или освоить их с нуля:

Другие профессии и курсы
  1. Аннотация это, как правило, первый абзац или первая пара предложений статьи в Википедии. Полный объём заархивированного XML-файла составляет приблизительно 796 Мбайт. Если вы захотите самостоятельно поэкспериментировать с кодом, можно воспользоваться доступными архивами меньшего размера (с ограниченным количеством аннотаций); синтаксический разбор и индексирование XML-файла займут довольно большое время и потребуют значительных объёмов памяти.

  2. Весь набор данных и указатель будут храниться в памяти, поэтому нет нужды также хранить в памяти необработанные данные.

  3. Много ли преимуществ даёт фильтр выделения основы слова? Это тема для отдельного обсуждения. Применение такого фильтра позволит уменьшить общий размер указателя (то есть уменьшить количество уникальных слов), однако операция выделения основы слова базируется на эвристике, и мы, сами того не желая, можем отсеять потенциально ценную информацию. Например, если слова университет, универсальный, университеты и универсиада усечь до основы универс, мы потеряем способность различать значения этих слов, а это отрицательно скажется на релевантности. Более подробная информация о выделении основы слов (и лемматизации) приведена в этой отличной статье.

  4. При решении задачи мы используем оперативную память ноутбука. Но на практике применяется другой способ, не связанный с хранением указателя в памяти. Поисковая система Elasticsearch хранит данные в обычном текстовом формате JSON на диске, в самой библиотеке Lucene (базовой библиотеке поиска и индексирования) хранятся только индексированные данные. Многие другие поисковые системы просто возвращают упорядоченный список идентификаторов документов, который затем используется для извлечения и выдачи пользователям данных из базы данных или другого сервиса. Особенно это актуально для крупных корпораций, в которых полное реиндексирование всех данных обходится недёшево. Как правило, в поисковой системе хранятся только данные, связанные с обеспечением релевантности (а не атрибуты, используемые только для презентационных целей).

  5. Для более глубокого понимания алгоритма рекомендую ознакомиться со следующими публикациями: What is TF-IDF? и Term frequency and weighting

Подробнее..

Как установить ROS NOETIC на UBUNTU 20.04

12.04.2021 12:21:22 | Автор: admin

Почему я решил написать этот пост?

Вы конечно же можете посмотреть оригинальную инструкцию по установке ROS Noetic на сайте ROS Wiki , однако там все на английском и объясняется весьма туманно, я же постараюсь объяснить вам как можно понятнее. Итак, начнем.

Что такое ROS?

ROS Операционная система для роботов, остов для программирования роботов, предоставляющий функциональность для распределённой работы. ROS был первоначально разработан в 2007 году под названием switchyard в Лаборатории Искусственного Интеллекта Стэнфордского Университета.

Можно ли установить ROS Noetic на другие версии Ubuntu?

Правильного ответа на этот вопрос нет. Отдельные пакеты Noetic вроде как можно установить на другие версии операционной системы, однако стабильность их работы не гарантируется. Поэтому скорее все нет. Итак, давайте все-таки перейдем к самой установке.

Что делать если я использую другую версию Ububntu

ROS разрабатывался в разных версиях, которые устанавливаются на разные версии Ubuntu.

Версия Ubuntu

Версия ROS

Ubuntu 15.04 & 16.04

ROS Kenetic

Ubuntu 18.04

ROS Melodic

Ubuntu 20.04

ROS Noetik

Как установить ROS если вы использует другую версию Ubuntu? Легко! Просто во всех примерах кода заменяйте слово noetic на название той версии, которая удовлетворяет вашем характеристикам например вместо ros-noetic, вы можете написать ros-melodic

Подготовка к установке. Настройка репозиториев

Начнем с того, что существуют четыре основных репозитория:

  • Main- бесплатное программное обеспечение с открытым исходным кодом, поддерживаемое Canonical.

  • Universe- бесплатное программное обеспечение с открытым исходным кодом, поддерживаемое сообществом.

  • Restricted- проприетарные драйверы для устройств.

  • Multiverse- Программное обеспечение, ограниченное авторским правом или юридическими вопросами.

Итак, начинаем настройку. Сначала открываем вкладку "Программное обеспечение Ubuntu" и в настройках "Ubuntu Software" ставим галочки так, как показано на картинке

Далее переходим во вкладку "Other sowtware" и ставим галочки на пунктах Canonical Parthners и Canonical Parthners(source code)

Установка этих параметров поможет корректно установить пакеты ROS

Установка ROS Noetic

Шаг 1

После настройки репозиториев можем приступать к самой установке. Для начала настраиваем систему на прием пакетов программного обеспечения с packages.ros.org . Вставляем в терминал следующую команду:

sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $ (lsb_release -sc) main"> /etc/apt/sources.list.d/ros-latest.list'

После этого настраиваем свои ключи. После добавление репозитория вводи одну из следующих команд:

sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

В качестве альтернативы мы можем использовать curl вместо команды apt-key, что может быть полезно, если вы находитесь за прокси-сервером:

curl -sSL 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xC1CF6E31E6BADE8868B172B4F42ED6FBAB17C654' | sudo apt-key add -

Шаг 2

Во-первых обновляем пакеты с помощью команды

sudo apt-get update

Шаг 3

Начинаем установку. Для этого надо решить, какой ROS вы хотите себе установить:

  1. Desktop Full - та сборка, которую я вам рекомендую ставить. Эта версия устанавливает все, что только можно: 2D/3D симуляторы и программы восприятия. Для того, чтобы установить эту версию пишем в терминале:

sudo apt install ros-noetic-desktop-full

2. Desktop Instal - этот пакет содержит в себе все компоненты на базе ROS, а также и такие элементы как rqtиrviz. Для установки этой версии пишем в терминале следующую команду

sudo apt install ros-noetic-desktop

3. ROS-Base: (Bare Bones) - это просто голые библиотеки и пакеты ROS. Это тот вариант, который подойдет для продвинутых специалистов. Для установки этого варианта пишем в терминале:

sudo apt install ros-noetic-ros-base

Также вы можете установить какой-либо конкретный пакет ROS воспользовавшись командой:

sudo apt install ros-noetic-ИМЯ_ПАКЕТА

Шаг 4

Устанавливаем пакет bash, в котором мы будем использовать ROS. пишем в терминале:

source /opt/ros/noetic/setup.bash

Шаг 5

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

sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential

Шаг 6

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

sudo apt установить python3-rosdep

Шаг 7

Для инициализации rosdep пишем в терминале:

sudo rosdep init

И потом пишем следующее:

rosdep update

ИТОГ

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

Подробнее..

За что вы так меня не любите? (с) Python

12.04.2021 16:18:43 | Автор: admin

Python уже на протяжении нескольких лет держится в топе языков программирования. А любая популярная вещь так или иначе обречена на шквал критики разной степени конструктивности. IT-сообщество разделилось на три основных лагеря:

  1. те, кто Python боготворят;

  2. те, кто делают вид, как будто его не существует, или о нём практически не слышали;

  3. те, кто ненавидит Питон всем сердцем.

И под влиянием общепринятого человечеством правила, когда на негативное мы обращаем гораздо больше внимания, в этой статье (скорее небольшом эссе из категории Мысли на тему) я решил обсудить пять основных причин, за которые язык Python ненавидят и критикуют.

Скорость

Наверное, каждый слышал, что Python медленный и уступает в скорости другим языкам по типу С++, С# и т.д.

Отрицать это глупо, однако дьявол, как обычно, в деталях. Python интерпретируемый язык, он по определению не заточен под разработку проектов, специфика которых требует межгалактической скорости. Если используют для других целей и проектов. Там, где это нужно, все узкие, критические для производительности места и библиотеки (например, Numpy) обычно пишут на С или Fortran и оборачивают в питон-код. А в большинстве оставшихся мест это скорость-то особо не нужна или не сильно критична. Кстати, если всё-таки кровь из носу нужно повысить производительность, никто не запрещает использовать другие реализации интерпретатора, например, PyPy, Stackless, Numba и т.д.

К слову, Python далеко не самый медленный язык по сравнению с конкурентами в своей весовой категории, взять те же PHP и Ruby.

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

Ниша Python это веб, скриптинг и Data Science. Он хорош именно в этих сферах. Если тебе нужны сложные вычисления, CPU-bound задачи, то тебе надо смотреть в сторону свеженького Rust или С/С++. Никто не заставляет писать highload на Питоне. Поэтому попытки ругать Python за скорость схожи с ситуацией, когда ты покупаешь кроссовки на два размера меньше и ругаешь потом фирму за то, что в кроссовках их производства неудобно ходить. Или пытаешься гонять в футбол в свадебных туфлях с носами. Ну вы поняли.

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

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

Проблемы с многопоточностью и GIL (запрещенная на территории РФ организация)

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

Если тебе обязательно нужно миллион виртуальных потоков, то просто используй Elixir, Erlang или Go. Они с прицелом на это и создавались. Зачем ты ругаешь дуршлаг за то, что вода из него утекает? Ругать надо себя, если ты, будучи крутым прогером, не способен подобрать подходящий инструмент или перестроиться под особенности задачи.

Допустим, в процессе обработки запроса будет сделано несколько обращений, к API, к базе данных или кэшу. Сервера могут быть сколь угодно быстрыми, но всё упрётся в скорость передачи данных по сети и ожидание ответов от сервисов. В I/O-bound задачах (на которых Python и применят чаще всего) потоки питона отлично работают, для CPU-bound Python и не предназначен. Кстати, в альтернативу потокам без каких-либо ограничений можно плодить процессы. Да, они заметно тяжеловеснее, однако при грамотном менеджменте и работе с ресурсами можно добиться довольно эффективной работы. Просто надо немного подумать, это вам не просто библиотеки импортировать)

Низкий порог вхождения

Не сказал бы, что это ужасный минус. Но большинство людей говорят, что Python плодит низкоквалифицированных разработчиков, которые способны только импортить библиотеки, где всё уже сделано за них и всё готово. Знаете, еду из ресторанов, пакетиков, Макдоналдса и другого фастфуда готовить тоже не надо. Всё уже собрано, разложено и пожарено. Надо лишь заказать, сделать импорт, если вам угодно. Только вот почему-то не все люди используют готовую пищу. Многие готовят самостоятельно, зачастую сильно заморачиваются в поисках качественных продуктов, способа приготовления, стараются питаться правильно и экологично. Однако они могут позволить себе съесть фастфуд или заказные блюда в редких случаях, зачастую под какой-то повод или, например, вынуждено перекусить на ходу.

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

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

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

Отдельно можно выделить большое количество расплодившихся быдло-кодеров на YouTube (неловко оглядывается в зеркало) и других площадка, которые использует в качестве основы Python. Но поймите, дело тут не в питоне. Python довольно простой язык, поэтому привлекает большое количество народа, и если вдруг, в ближайшее время появится что-то более простое и удобное, эта контингентная прослойка моментально бросит питон. То есть дело, как обычно в самих людях.

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

Скриншот сделан 12.04.2021Скриншот сделан 12.04.2021

Синтаксис

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

Давайте сравним, как я пишу код на С++:

И как его пишут дети:

Теперь как я пишу код на Python:

И как его пишут дети:

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

А вообще, объективно говоря, на вкус и цвет товарищи разные. Думаю, про синтаксис мы закончили.

Типизация

Это одновременно и плюс, и минус. С одно стороны, безусловно, динамическая типизация тянет проблемы в очень крупных проектах и затрудняет тестирование. С другой же, проще даётся новикам, что обеспечивает приток новой крови в сообщество. Также динамическая типизация дает и несколько других бонусов в виде упрощения метапрограммирования, плюшек интроспекции и рефлексии. Также в Python 3.5 появились аннотации типов type hinting. Да, это не панацея уровня TypeScript, но указание типов стало довольно приятным нововведением, значительно облегчившим жизнь, и программисты очень часто стали им пользоваться.

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

Заключение

Подытожить всё выше сказанное хочется фразой Страуструпа, создателя С++:

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

И, как я сказал в начале текста:

Python уже несколько лет держится в топе языков программирования.

Подробнее..

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

12.04.2021 16:18:43 | Автор: admin

Те, кто знает Python, могут делать поистине удивительные вещи, например, создавать арт-объекты и игры и красивые карты, полнотекстовую поисковую машину и систему распознавания лиц. Применим Python и для вполне бытовых задач. Сегодня, специально к старту новых потоков курсов Python для веб-разработки и Fullstack-разработчик на Python поделимся с вами туториалом, как считывать, добавлять и удалять EXIF метаданные из фотографий с помощью Python.


Немного очевидного для тех кто не в теме метаданные в фото

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

  1. У большого количества людей есть камера, которая обычно находится в пределах досягаемости.

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

Помимо данных изображения фотографии, сделанные с помощью смартфонов и современных цифровых камер, содержат метаданные, которые являются дополнительной информацией о фотографии. Эти метаданные хранятся в формате EXIF (сокращение от EXchangeable Image File format), который является постоянно развивающимся стандартом для информации, добавляемой к цифровым изображениям и звукозаписям.

На фотографиях EXIF может включать такую информацию, как:

  • Размеры и плотность пикселей фото.

  • Марка и модель устройства, на котором был сделан снимок.

  • Масштабирование, диафрагма, вспышка и другие настройки камеры при съёмке фотографии.

  • Ориентация устройства при фотографировании.

  • Когда было сделано фото.

  • Где было сделано фото.

  • В каком направлении была обращена камера.

  • Высота, на которой был сделан снимок.

Эти метаданные полезны для сортировки, каталогизации и поиска фотографий, поэтому был определён стандарт EXIF. Однако при этом возникают проблемы с конфиденциальностью и безопасностью, которые многие пользователи не принимают во внимание.

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

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

Я подозреваю, что вы как разработчики, заботящиеся о безопасности, вероятно, задаёте себе следующие вопросы:

  • Как я могу программно обнаружить и прочитать EXIF-метаданные с фотографии?

  • Как я могу программно изменять, добавлять или удалять EXIF-метаданные?

Если на вашем компьютере установлен Python 3.6 или более поздней версии, вы можете узнать это, выполнив приведённые ниже инструкции. Они охватывают пару пакетов Python, которые вы можете включить в свои приложения для извлечения, добавления, изменения или удаления EXIF-данных с фотографий. Попутно вы не только будете использовать свои навыки программирования, но и поработаете детективом!

Модуль exif

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

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

pip install exif

Если при вводе этой команды выдаётся сообщение об ошибке, попробуйте вместо этого использовать команду pip3 install exif. pip3 это версия pip, менеджера пакетов Python, специально предназначенная для Python 3.

Если вы не поддерживаете какой-либо код на Python 2, но вам нужно использовать pip3 для установки пакетов Python, то вам следует подумать об обновлении Python.

Загрузка фотографий и проверка их на наличие данных EXIF

Давайте проверим exif в работе. Рассмотрим эти две фотографии, palmtree1.jpg и palmtree2.jpg:

Предположим, вам задали следующие вопросы:

  1. Были ли эти фотографии сделаны на одно устройство или на разные?

  2. Какое фото было сделано первым?

  3. Где были сделаны эти фотографии?

Чтобы ответить на эти вопросы, мы загрузим данные из этих фотографий в объекты exif-Image, а затем будем использовать эти объекты для проверки EXIF-метаданных:

from exif import Imagewith open("./images/palm tree 1.jpg", "rb") as palm_1_file:    palm_1_image = Image(palm_1_file)    with open("./images/palm tree 2.jpg", "rb") as palm_2_file:    palm_2_image = Image(palm_2_file)    images = [palm_1_image, palm_2_image]

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

Давайте выполним нашу первую операцию: убедимся, что фото действительно содержат EXIF-данные. Мы сделаем это, проверив свойство has_exif каждого объекта Image. Для каждого изображения, содержащего EXIF-данные, мы будем использовать свойство изображения exif_version, чтобы отобразить версию EXIF:

for index, image in enumerate(images):    if image.has_exif:        status = f"contains EXIF (version {image.exif_version}) information."    else:        status = "does not contain any EXIF information."    print(f"Image {index} {status}")

При запуске этот код даёт следующие результаты:

Image 0 contains EXIF (version 0220) information.Image 1 contains EXIF (version 0232) information.

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

Какие метаданные EXIF доступны на каждой фотографии?

Думайте о EXIF как о структуре данных ключ-значение, подобной словарю Python. Пары ключ-значение EXIF называются тегами, и каждый тег может содержать строковое или числовое значение. В текущем стандарте EXIF (версия 2.32) есть десятки тегов, и каждый от производителей смартфонов и камер до фотографов может добавлять свои собственные теги.

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

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

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

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

Объекты Image, предоставляемые модулем exif, представляют теги EXIF как свойства этого объекта. Это означает, что вы можете использовать встроенную функцию Python dir() для объекта Image, чтобы увидеть, какие теги EXIF у него есть.

Следующий код отображает список тегов каждого объекта Image в нашем списке изображений:

image_members = []for image in images:    image_members.append(dir(image))for index, image_member_list in enumerate(image_members):    print(f"Image {index} contains {len(image_member_list)} members:")    print(f"{image_member_list}\n")

Вы увидите следующий результат:

Image 0 contains 53 members:['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'color_space', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'digital_zoom_ratio', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_datestamp', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'light_source', 'make', 'max_aperture_value', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']Image 1 contains 68 members:['<unknown EXIF tag 316>', '<unknown EXIF tag 322>', '<unknown EXIF tag 323>', '<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_dest_bearing', 'gps_dest_bearing_ref', 'gps_horizontal_positioning_error', 'gps_img_direction', 'gps_img_direction_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'gps_speed', 'gps_speed_ref', 'has_exif', 'lens_make', 'lens_model', 'lens_specification', 'make', 'maker_note', 'metering_mode', 'model', 'offset_time', 'offset_time_digitized', 'offset_time_original', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'scene_type', 'sensing_method', 'shutter_speed_value', 'software', 'subject_area', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']

Как вы можете видеть, в то время как оба объекта Image имеют много общих тегов, изображение 1 содержит немного больше, чем изображение 0. Это означает, что изображение 1 имеет несколько больше тегов EXIF, чем изображение 0. Это сильный индикатор того, что изображение 0 и изображение изображение 1 было снято на разные устройства.

Вы можете использовать стандартный метод set() для определения общих элементов изображения 0 и изображения 1:

common_members = set(image_members[0]).intersection(set(image_members[1]))common_members_sorted = sorted(list(common_members))print("Image 0 and Image 1 have these members in common:")print(f"{common_members_sorted}")

Запуск этого кода даёт следующий результат:

Image 0 and Image 1 have these members in common:['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'make', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']

Если вы смотрели EXIF-теги в документации (либо список стандартных тегов EXIF, либо расширенный список), вы могли заметить, что имена тегов EXIF находятся в PascalCase, а свойства EXIF в объектах exif-Image в snake_case. Это связано с тем, что авторы модуля exif стремятся следовать руководству по стилю Python и разработали Image для преобразования имён тегов EXIF в имена свойств Python.

Члены класса Image exif, не являющиеся тегами EXIF

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


Члены класса

Описание

delete(attribute)

Удаляет из изображения тег EXIF, указанный в строковом атрибуте.

delete_all

Удаляет все теги EXIF из изображения.

get(attribute, default=None)

Возвращает значение тега EXIF, заданное строковым атрибутом. Если тег недоступен или содержит значение, он возвращает значение, указанное в аргументе ключевого слова по умолчанию.

get_file

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

get_thumbnail

Возвращает двоичные данные для эскиза изображения.

has_exif

Логическое значение, которое возвращает True, если изображение в настоящее время содержит метаданные EXIF.

set(attribute, value)

Установить значение для тега EXIF, заданное строковым атрибутом. Если тег недоступен или содержит значение, он возвращает значение, указанное в аргументе ключевого слова по умолчанию.

В этой статье мы рассмотрим большинство из этих свойств и методов.

Получение основной информации о фотографиях

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

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

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

for index, image in enumerate(images):    print(f"Device information - Image {index}")    print("----------------------------")    print(f"Make: {image.make}")    print(f"Model: {image.model}\n")

Вот результат кода выше:

Device information - Image 0----------------------------Make: motorolaModel: motorola one hyperDevice information - Image 1----------------------------Make: AppleModel: iPhone 12 Pro

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

Дополнительная информация об устройствах

Давайте соберём дополнительную информацию об устройствах, которые используются для фотосъёмки, а именно об их объективах и версиях операционной системы.

Не все устройства сообщают тип линзы в своих EXIF-метаданных, поэтому мы будем использовать метод Image get(), который аналогичен методу get(), используемому в Python. Подобно методу get() в Python, метод get(), предоставляемый объектом exif Image, изящно обрабатывает случай, когда данный ключ не существует.

В приведённом ниже коде используется get(), чтобы попытаться получить доступ к версиям объектива и операционной системы, используемым при съёмке фотографий. Если определённого свойства не существует, его значение будет отображаться как Неизвестно:

for index, image in enumerate(images):    print(f"Lens and OS - Image {index}")    print("---------------------")    print(f"Lens make: {image.get('lens_make', 'Unknown')}")    print(f"Lens model: {image.get('lens_model', 'Unknown')}")    print(f"Lens specification: {image.get('lens_specification', 'Unknown')}")    print(f"OS version: {image.get('software', 'Unknown')}\n")

Вот его результат:

Lens and OS - Image 0---------------------Lens make: UnknownLens model: UnknownLens specification: UnknownOS version: UnknownLens and OS - Image 1---------------------Lens make: AppleLens model: iPhone 12 Pro back triple camera 4.2mm f/1.6Lens specification: (1.5399999618512084, 6.0, 1.6, 2.4)OS version: 14.3

Обратите внимание, что телефон, используемый для съёмки изображения 0 (Motorola One Hyper), не предоставляет свойства lens_make, lens_model, lens_specification или software. Если бы мы попытались получить к ним доступ напрямую (например, image.lens_make), результатом была бы ошибка. Метод get() позволил нам предоставить альтернативное значение для этих несуществующих свойств.

Дата и время, когда была сделана фотография

Следующий вопрос: какое фото было сделано первым?

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

Некоторые телефоны также записывают свойство offset_time, которое мы можем использовать для определения смещения datetime относительно UTC.

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

for index, image in enumerate(images):    print(f"Date/time taken - Image {index}")    print("-------------------------")    print(f"{image.datetime_original}.{image.subsec_time_original} {image.get('offset_time', '')}\n")

Вот результаты:

Date/time taken - Image 0-------------------------2021:01:22 15:08:46.327211 Date/time taken - Image 1-------------------------2021:01:22 15:08:59.383 -05:00

Как видно из вывода, изображение 0 было снято первым, а изображение 1 было снято через 13 секунд.

Определение места, где была сделана фотография

В этом разделе мы рассмотрим доступ к GPS-координатам в метаданных фотографии, форматирование этих координат и упрощение понимания этих координат путём их размещения на карте и преобразования их в названия страны, региона и города, где фото было сделано.

Получение GPS-координат фотографии

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

for index, image in enumerate(images):    print(f"Coordinates - Image {index}")    print("---------------------")    print(f"Latitude: {image.gps_latitude} {image.gps_latitude_ref}")    print(f"Longitude: {image.gps_longitude} {image.gps_longitude_ref}\n")

Вот результат кода выше:

Coordinates - Image 0---------------------Latitude: (28.0, 0.0, 1.56) NLongitude: (82.0, 26.0, 59.04) WCoordinates - Image 1---------------------Latitude: (28.0, 0.0, 1.54) NLongitude: (82.0, 26.0, 58.75) W

Обратите внимание, что свойства gps_latitude и gps_longitude возвращают широту и долготу в виде кортежа из трёх значений:

  1. Градусы.

  2. Минуты (1/60 градуса).

  3. Секунды (1/60 минуты или 1/3600 градуса).

Широта определяет угловое расстояние от экватора, которое может быть северным или южным. Свойство gps_latitude_ref указывает это направление, которое может быть N или S.

Долгота определяет угловое расстояние от меридиана, которое может быть восточным или западным. Свойство gps_longitude_ref указывает это направление: E или W.

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

Форматирование широты и долготы

Давайте определим пару функций для форматирования информации о широте и долготе, возвращаемой Image, в стандартные форматы:

  • Градусы, минуты и секунды. В этом формате широта изображения 0 будет записана как 28.0 0.0 '1.56 "N.

  • Десятичные градусы. В этом формате широта изображения 0 будет записана как 28,000433333333334. Северные широты и восточные долготы представлены положительными значениями, в то время как южные широты и западные долготы представлены отрицательными значениями.

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

def format_dms_coordinates(coordinates):    return f"{coordinates[0]} {coordinates[1]}\' {coordinates[2]}\""def dms_coordinates_to_dd_coordinates(coordinates, coordinates_ref):    decimal_degrees = coordinates[0] + \                      coordinates[1] / 60 + \                      coordinates[2] / 3600        if coordinates_ref == "S" or coordinates_ref == "W":        decimal_degrees = -decimal_degrees        return decimal_degreesfor index, image in enumerate(images):    print(f"Coordinates - Image {index}")    print("---------------------")    print(f"Latitude (DMS): {format_dms_coordinates(image.gps_latitude)} {image.gps_latitude_ref}")    print(f"Longitude (DMS): {format_dms_coordinates(image.gps_longitude)} {image.gps_longitude_ref}\n")    print(f"Latitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)}")    print(f"Longitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)}\n")

Вот результат:

Coordinates - Image 0---------------------Latitude (DMS): 28.0 0.0' 1.56" NLongitude (DMS): 82.0 26.0' 59.04" WLatitude (DD): 28.000433333333334Longitude (DD): -82.44973333333334Coordinates - Image 1---------------------Latitude (DMS): 28.0 0.0' 1.54" NLongitude (DMS): 82.0 26.0' 58.75" WLatitude (DD): 28.000427777777777Longitude (DD): -82.44965277777779

Отображение местоположения фотографий на карте

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

Один из способов использовать встроенный в Python модуль webbrowser для открытия новой вкладки браузера для каждой фотографии, используя десятичную версию EXIF-координат каждой фотографии в качестве параметров URL-адреса Google Maps. Мы создадим служебную функцию с именем draw_map_for_location(), которая сделает следующее:

def draw_map_for_location(latitude, latitude_ref, longitude, longitude_ref):    import webbrowser        decimal_latitude = dms_coordinates_to_dd_coordinates(latitude, latitude_ref)    decimal_longitude = dms_coordinates_to_dd_coordinates(longitude, longitude_ref)    url = f"https://www.google.com/maps?q={decimal_latitude},{decimal_longitude}"    webbrowser.open_new_tab(url)for index, image in enumerate(images):    draw_map_for_location(image.gps_latitude,                           image.gps_latitude_ref,                           image.gps_longitude,                          image.gps_longitude_ref)

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

Отображение города, региона и страны, где был сделан снимок

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

  • reverse_geocoder простой автономный обратный геокодер, который использует внутренние таблицы для преобразования набора координат в набор названий городов и штатов/провинций и кодов стран. Установите его, введя pip install reverse_geocoder в командной строке.

  • pycountry утилита поиска стран, которую мы будем использовать для преобразования кодов стран в их соответствующие названия. Установите это, введя pip install pycountry в командной строке.

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

import reverse_geocoder as rgimport pycountryfor index, image in enumerate(images):    print(f"Location info - Image {index}")    print("-----------------------")    decimal_latitude = dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)    decimal_longitude = dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)    coordinates = (decimal_latitude, decimal_longitude)    location_info = rg.search(coordinates)[0]    location_info['country'] = pycountry.countries.get(alpha_2=location_info['cc'])    print(f"{location_info}\n")

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

  • Название города, посёлка или деревни.

  • Главный административный регион, который обычно представляет собой штат или провинцию.

  • Незначительный административный район, который обычно является округом или районом.

  • Код страны.

Затем он использует метод get() из pycountry для преобразования кода страны, предоставленного reverse_geocoder, в кортеж, содержащий соответствующие общие и официальные названия стран.

Вот его результат:

Location info - Image 0-----------------------{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}Location info - Image 1-----------------------{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}

Другая полезная информация о датчиках

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

В каком направлении была обращена камера?

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

Давайте определим, в каком направлении я смотрел, когда делал каждую из этих фотографий:


Мы будем использовать следующие свойства exif-Image, чтобы определить направление, в котором была направлена камера:

  • gps_img_direction: направление по компасу, то есть направление, в котором смотрела камера, когда был сделан снимок, выраженный в десятичных градусах. 0 север, 90 восток, 180 юг и 270 запад.

  • gps_img_direction_ref: контрольная точка для gps_img_direction. Это может быть либо T, что означает, что 0 относится к истинному или географическому северу, либо M, что означает, что 0 относится к магнитному северу. В большинстве случаев используется истинный север.

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

  • degrees_to_direction(): эта функция преобразует направления по компасу в основные направления (например, N, NE, NNE и т. д.);

  • format_direction_ref(): эта функция превращает значение в gps_img_direction_ref понятную для человека строку.

def degrees_to_direction(degrees):    COMPASS_DIRECTIONS = [        "N",        "NNE",        "NE",        "ENE",        "E",         "ESE",         "SE",         "SSE",        "S",         "SSW",         "SW",         "WSW",         "W",         "WNW",         "NW",         "NNW"    ]        compass_directions_count = len(COMPASS_DIRECTIONS)    compass_direction_arc = 360 / compass_directions_count    return COMPASS_DIRECTIONS[int(degrees / compass_direction_arc) % compass_directions_count]def format_direction_ref(direction_ref):    direction_ref_text = "(true or magnetic north not specified)"    if direction_ref == "T":        direction_ref_text = "True north"    elif direction_ref == "M":        direction_ref_text = "Magnetic north"    return direction_ref_text# Import imageslake_images = []for i in range(1, 5):    filename = f"lake {i}.jpg"    with open(f"./images/{filename}", "rb") as current_file:        lake_images.append(Image(current_file))# Display camera direction for each imagefor index, image in enumerate(lake_images):    print(f"Image direction - Image {index}")    print("-------------------------")    print(f"Image direction: {degrees_to_direction(image.gps_img_direction)} ({image.gps_img_direction})")    print(f"Image direction ref: {format_direction_ref(image.gps_img_direction_ref)}\n")

Когда вы запустите код, вы увидите следующий результат:

Image direction - Image 0-------------------------Image direction: ENE (78.416259765625)Image direction ref: True northImage direction - Image 1-------------------------Image direction: N (1.174224853515625)Image direction ref: True northImage direction - Image 2-------------------------Image direction: SSE (178.46739196870607)Image direction ref: True northImage direction - Image 3-------------------------Image direction: W (273.8248136315229)Image direction ref: True north

На какой высоте был сделан снимок?

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

Давайте выясним, на каких высотах были сделаны эти фотографии:

Мы воспользуемся этими свойствами объекта Exif Image для определения высоты:

  • gps_altitude: высота в метрах;

  • gps_altitude_ref: контрольная точка для gps_altitude. Это значение равно 0, что означает, что значение в gps_altitude относится к метрам над уровнем моря, или 1, что означает, что значение в gps_altitude относится к метрам ниже уровня моря.

Следующий код отображает высоту, сообщённую телефоном в момент съёмки каждой из фотографий. Он использует одну служебную функцию, format_altitude(), которая определяет, находится ли заданная высота над или под уровнем моря:

def format_altitude(altitude, altitude_ref):    altitude_ref_text = "(above or below sea level not specified)"    if altitude_ref == 0:        altitude_ref_text = "above sea level"    elif altitude_ref == 1:        altitude_ref_text = "below sea level"    return f"{altitude} meters {altitude_ref_text}"# Import imagesaltitude_images = []for i in range(1, 3):    filename = f"altitude {i}.jpg"    with open(f"./images/{filename}", "rb") as current_file:        altitude_images.append(Image(current_file))        # Display camera altitude for each imagefor index, image in enumerate(altitude_images):    print(f"Altitude - Image {index}")    print( "------------------")    print(f"{format_altitude(image.gps_altitude, image.gps_altitude_ref)}\n")

Вот результат:

Altitude - Image 0------------------14.025835763206075 meters above sea levelAltitude - Image 1------------------359.13079847908745 meters above sea level

Я мог бы сказать вам, где я сделал эти фотографии, но вы уже знаете, как это выяснить.

Двигался ли фотограф?

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

Мой Motorola One Hyper не записывает метаданные, связанные со скоростью, но мой iPhone это делает. Доступ к этим данным можно получить с помощью этих двух методов объекта exif Image:

  • gps_speed: скорость, выраженная в виде числа;

  • gps_speed_ref: единицы скорости, используемые для значения в gps_speed. Это значение может быть: K для километров в час; M для миль в час; N для морских миль в час, или узлов.

Рассмотрим следующие фото:

Вот код, который выводит записанную скорость во время съёмки каждой фотографии. Он включает функцию универсальности, format_speed_ref(), которая определяет единицы сообщаемой скорости:

def format_speed_ref(speed_ref):    speed_ref_text = "(speed units not specified)"    if speed_ref == "K":        speed_ref_text = "km/h"    elif speed_ref == "M":        speed_ref_text = "mph"    elif speed_ref == "N":        speed_ref_text = "knots"    return speed_ref_text# Import imagesspeed_images = []for i in range(1, 4):    filename = f"speed {i}.jpg"    with open(f"./images/speed {i}.jpg", "rb") as current_file:        speed_images.append(Image(current_file))    for index, image in enumerate(speed_images):    print(f"Speed - Image {index}")    print("---------------")    print(f"Speed: {image.gps_speed} {format_speed_ref(image.gps_speed_ref)}\n")

Вот результат:

Speed - Image 0---------------Speed: 0.0 km/hSpeed - Image 1---------------Speed: 20.19736291335287 km/hSpeed - Image 2---------------Speed: 5.520932607215793 km/h

Редактирование EXIF данных и их сохранение

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

Редактирование координат фотографии

Начнём с этой фотографии:

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

Вот код, который это сделает. Он использует служебную функцию draw_map_for_location(), которую мы определили ранее:

with open(f"./images/hotel original.jpg", "rb") as hotel_file:    hotel_image = Image(hotel_file)    # Read the GPS dataprint("Original coordinates")print("--------------------")print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")# Open a Google Map showing the location represented by these coordinatesdraw_map_for_location(hotel_image.gps_latitude,                      hotel_image.gps_latitude_ref,                      hotel_image.gps_longitude,                      hotel_image.gps_longitude_ref

Он выводит следующее:

Original coordinates--------------------Latitude: (28.0, 21.0, 58.44) NLongitude: (81.0, 33.0, 34.29) W

Кроме того, он открывает новую вкладку браузера, показывающую карту Google, которая отображает отели Swan и Dolphin, которые находятся в нескольких минутах ходьбы от Walt Disney World во Флориде.

Давайте сделаем вещи немного интереснее, изменив координаты, встроенные в данные EXIF фотографии, чтобы они сообщали, что она была сделана в Зоне 51. Если вы не слышали об этом месте, это военный объект в Неваде, в котором, как считают теоретики теории заговора, правительство США хранит тела пришельцев и космический корабль, захваченные в 1950-х годах. Его координаты: 37,0 14 '3,6 "северной широты, 115 48' 23,99" западной долготы.

Вы видели, что чтение значения тега EXIF с использованием объекта Exif Image это просто чтение значения в соответствующем свойстве. Точно так же редактирование его значения это просто вопрос присвоения нового значения этому свойству. В этом случае мы присвоим значения, определяющие координаты Зоны 51, свойствам изображения gps_latitude, gps_latitude_ref, gps_longitude и gps_longitude_ref:

# Boring. Let's change those coordinates to Area 51!hotel_image.gps_latitude = (37.0, 14, 3.6)hotel_image.gps_latitude_ref = 'N'hotel_image.gps_longitude = (115, 48, 23.99)hotel_image.gps_longitude_ref = 'W'# Read the revised GPS dataprint("Revised coordinates")print("-------------------")print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")# Open a Google Map showing the location represented by the revised coordinatesdraw_map_for_location(hotel_image.gps_latitude,                      hotel_image.gps_latitude_ref,                      hotel_image.gps_longitude,                      hotel_image.gps_longitude_ref

Выполнение кода приведёт к следующему результату

Revised coordinates-------------------Latitude: (37.0, 14.0, 3.6) NLongitude: (115.0, 48.0, 23.99) W

... и он откроет новую вкладку с картой Google, в которой отображается Зона 51.

Заполнение неиспользуемых тегов EXIF

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

Примечание: модуль exif записывает только теги в спецификации EXIF, но не дополнительные теги, включённые поставщиками. Официальные теги в спецификации EXIF подчёркнуты.

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

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

  • ImageDescription: описание фотографии. В объектах Exif Image это отображается как свойство image_description.

  • Copyright: уведомление об авторских правах на фотографию. В объектах Exif Image это свойство copyright.

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

hotel_image.image_description = "The Dolphin Hotel in Orlando, viewed at sunset from the Swan Hotel"hotel_image.copyright = "Copyright 2021 (Your name here)"print(f"Description: {hotel_image.image_description}")print(f"Copyright: {hotel_image.copyright}")

Выполнение кода даёт следующий результат:

Description: The Dolphin Hotel in Orlando, viewed at sunset from the Swan HotelCopyright: Copyright 2021 (Your name here)

Сохранение фото с обновленными данными

Теперь, когда мы отредактировали данные EXIF на картинке, давайте сохраним их как новый файл с названием hotel_updated.jpg.

with open('./images/hotel updated.jpg', 'wb') as updated_hotel_file:    updated_hotel_file.write(hotel_image.get_file())

Этот код создаёт файловый объект updated_hotel_file для записи двоичных данных в файл с именем hotel_updated.jpg. Затем он использует метод Image get_file() для получения данных об изображении отеля в сериализуемой форме и записывает эти данные в файл.

Теперь у вас должен быть новый файл: hotel_updated.jpg. Загрузим его и убедимся, что изменённые и добавленные данные сохранены:

with open(f"./images/hotel updated.jpg", "rb") as hotel_file:    hotel_image = Image(hotel_file)    print("Coordinates")print("-----------")print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")print("Other info")print("----------")print(f"Description: {hotel_image.image_description}")print(f"Copyright: {hotel_image.copyright}")# Open a Google Map showing the location represented by these coordinatesdraw_map_for_location(hotel_image.gps_latitude,                      hotel_image.gps_latitude_ref,                      hotel_image.gps_longitude,                      hotel_image.gps_longitude_ref

Код выдаст вам этот результат

Coordinates-----------Latitude: (37.0, 14.0, 3.6) NLongitude: (115.0, 48.0, 23.99) WOther info----------Description: The Dolphin Hotel in Orlando, viewed at sunset from the Swan HotelCopyright: Copyright 2021 (Your name here)

... и откроется ещё одна вкладка браузера, отображающая Зону 51 на карте.

Удаление данных EXIF и сохранение очищенной фотографии

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

Первый способ использовать метод delete(), предоставляемый объектом Exif-Image. Давайте используем его, чтобы удалить данные о широте:

hotel_image.delete('gps_latitude')hotel_image.delete('gps_latitude_ref')print("Latitude data")print("-------------")print(f"gps_latitude: {hotel_image.get('gps_latitude', 'Not found')}")print(f"gps_latitude_ref: {hotel_image.get('gps_latitude_ref', 'Not found')}")

Вот результат кода выше:

Latitude data-------------gps_latitude: Not foundgps_latitude_ref: Not found

Второй способ использовать оператор Python del, который удаляет объекты из памяти. В этом случае мы будем использовать его для удаления атрибутов hotel_image, в которых хранятся данные о долготе:

del hotel_image.gps_longitudedel hotel_image.gps_longitude_refprint("Longitude data")print("--------------")print(f"gps_longitude: {hotel_image.get('gps_longitude', 'Not found')}")print(f"gps_longitude_ref: {hotel_image.get('gps_longitude_ref', 'Not found')}")

Вот результат кода выше:

Longitude data--------------gps_longitude: Not foundgps_longitude_ref: Not found

Теперь, когда мы удалили данные о местоположении с фотографии, давайте сохраним её под новым именем, hotel_without_location_data.jpg:

with open('./images/hotel without location data.jpg', 'wb') as updated_hotel_file:    updated_hotel_file.write(hotel_image.get_file())

Наконец, если вы хотите просто удалить все данные EXIF с фотографии, вы можете использовать метод delete_all() объекта exif Image:

hotel_image.delete_all()dir(hotel_image)
['<unknown EXIF tag 322>', '<unknown EXIF tag 323>', '_segments', 'delete', 'delete_all', 'get', 'get_file', 'get_thumbnail', 'has_exif']

Опять же, как только вы использовали delete_all() для удаления всех тегов EXIF, вам нужно сохранить эти изменения. Приведённый ниже код сохраняет наше изображение со всеми удалёнными тегами как hotel_without_tags.jpg:

with open('./images/hotel without tags.jpg', 'wb') as updated_hotel_file:    updated_hotel_file.write(hotel_image.get_file())

Практические и технические соображения

Причины удаления метаданных EXIF

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

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

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

Некоторые фотографы больше озабочены секретностью, чем конфиденциальностью, когда речь идёт о метаданных их фотографий. Они не хотят разглашать настройки камеры, которые использовали для фотосъёмки.

Причины редактирования или добавления метаданных EXIF

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

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

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

Другие вещи, которые следует учитывать

Как мы видели в различных тегах, записанных устройствами iPhone и Android, используемыми для фотосъёмки в этой статье, разные камеры записывают разные наборы тегов EXIF. Программное обеспечение для редактирования фотографий часто записывает информацию в разные теги или добавляет свои собственные теги. Эти дополнительные метаданные часто могут указывать на то, что фотография была отредактирована.

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

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

Знание Python это полезный hard skill не только для программиста, но и для аналитика и даже маркетолога. С ним вы можете сами написать простой парсер, написать небольшое приложение под свои задачи и даже увеличить свой доход, выполняя заказы на фрилансе. Если вы хотите дополнительно прокачать себя и освоить этот язык под чутким руководством наших опытных менторов обратите внимание на курс Fullstack-разработчик на Python и Python для веб-разработки.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Шифрование сообщений в Python. От простого к сложному. Шифр Цезаря

13.04.2021 22:15:19 | Автор: admin

Немного о проекте

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


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

  • Шифр Цезаря

  • Шифр Виженера

  • Шифр замены

  • Омофонический шифр

  • RSA шифрование

Шифр Цезаря

Итак, после небольшого введения в цикл, я предлагаю все-таки перейти к основной теме сегодняшней статьи, а именно к Шифру Цезаря.

Что это такое?

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

Какими особенностями он обладает?

У Шифра Цезаря, как у алгоритма шифрования, я могу выделить две основные особенности. Первая особенность - это простота и доступность метода шифрования, который, возможно поможет вам погрузится в эту тему, вторая особенность - это, собственно говоря, сам метод шифрования.

Программная реализация

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

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

alfavit =  'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯ'# Создаем алфавит

Далее, нам нужно обозначить программе шаг, то есть смещение при шифровании. Так, например, если мы напишем букву "а" в сообщении, тот при шаге "2", программа выведет нам букву "в".

Итак, создаем переменную smeshenie, которая будет вручную задаваться пользователем, и message, куда будет помещаться наше сообщение, и, с помощью метода upper(), возводим все символы в нашем сообщении в верхний регистр, чтобы у нас не было ошибок. Потом создаем просто пустую переменную itog, куда мы буем выводить зашифрованное сообщение. Для этого пишем следующее:

smeshenie = int(input('Шаг шифровки: '))    #Создаем переменную с шагом шифровкиmessage = input("Сообщение для шифровки: ").upper()    #создаем переменнную, куда запишем наше сообщениеitog = ''    #создаем переменную для вывода итогового сообщения

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

for i in message:    mesto = alfavit.find(i)    #Вычисляем места символов в списке    new_mesto = mesto + smeshenie    #Сдвигаем символы на указанный в переменной smeshenie шаг

Далее, мы создаем внутри нашего цикла условие if , в нем мы записываем в список itog мы записываем наше сообщение уже в зашифрованном виде и выводим его:

if i in alfavit:        itog += alfavit[new_mesto] # здесь мы прибавляем значение правого операнда к левому    else: # и присваиваем эту сумму левому операнду.        itog += iprint (itog)

Итоговый вид программы

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

alfavit =  'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯ'smeshenie = int(input('Шаг шифровки: '))message = input("Сообщение для ДЕшифровки: ").upper()itog = ''for i in message:    mesto = alfavit.find(i)    new_mesto = mesto + smeshenie    if i in alfavit:        itog += alfavit[new_mesto]    else:        itog += iprint (itog)

Дешифровка сообщения

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

По сути, дешифровка - это алгоритм обратный шифровке. Давайте немного переделаем наш код (итоговый вид вы можете увидеть выше).

Для начала, я предлагаю сделать "косметическую" часть нашей переделки. Для этого перемещаемся в самое начало кода:

alfavit =  'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯ'smeshenie = int(input('Шаг шифровки: '))message = input("Сообщение для ДЕшифровки: ").upper()    #заменяем слово шифровка, на дешифровкаitog = ''

Остальное можно оставить так же, но если у вас есть желание, то можете поменять названия переменных.

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

Итог

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

Подробнее..

Детектирование пользовательских объектов

14.04.2021 08:08:47 | Автор: admin

Код вы можете скачать на странице GitHub (ссылка)

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

Я использую python3.7 и названия всех модулей с версиями хранятся в файле requirements.txt.

Мы будем делать это используя предварительно обученную модель и добавим в нее пользовательский объект обнаружения.

Для обучения нужно пройти следующие шаги:

  • Соберите по крайней мере 500 изображений, содержащих ваш объект абсолютный минимум будет около 100, в идеале больше 1000 или больше, но чем больше изображений у вас есть, тем более утомительным будет Шаг 2.

  • Разделите эти данные на обучающие/тестовые образцы. Обучающие данные должны составлять около 80%, а тестовые-около 20%.

  • Генерируйте записи TF для этих изображений.

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

  • Тренируйте вашу модель.

  • Экспорт графа вывода из новой обученной модели.

  • Обнаружение пользовательских объектов.

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

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

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

А вот тут начинается самое интересное. Когда все фотографии собраны, пришло время пометить нужные объекты на каждой картинке. LabelImg-это отличный инструмент для маркировки изображений, и на его странице GitHub есть очень четкие инструкции о том, как его установить и использовать.

LabelImg ссылка на GitHub (ссылка)

LabelImg ссылка для скачивания (ссылка)

Загрузите и установите LabelImg, при запуске этого приложения вы должны получить окно GUI. Отсюда выберите пункт Открыть каталог dir и выберите каталог, в который вы сохранили все свои изображения. Теперь вы можете начать аннотировать изображения с помощью кнопки create rectbox. Нарисуйте свою коробку, добавьте имя и нажмите кнопку ОК. Сохраните, нажмите на следующее изображение и повторите! Вы можете нажать клавишу w, чтобы нарисовать поле, и сделать ctrl+s, чтобы сохранить его быстрее. Для меня это заняло в среднем 1 час на 100 изображений, это зависит от количества объектов, которые у вас есть на изображении. Имейте в виду, это займет некоторое время!

LabelImg сохраняет xml-файл, содержащий данные метки для каждого изображения. Эти xml-файлы будут использоваться для создания TFRecords, которые являются одним из входных данных для тренера TensorFlow. После того, как вы пометили и сохранили каждое изображение, для каждого изображения в каталогах \test и \train будет создан один xml-файл.

Как только вы пометите свои изображения, мы разделим их на обучающие и тестовые группы. Чтобы сделать это, просто скопируйте около 20% ваших фотографий и их аннотаций XML-файлов в новый каталог под названием test, а затем скопируйте оставшиеся в новый каталог под названием train.

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

Во-первых, данные image .xml будут использоваться для создания csv-файлов, содержащих все данные для тренировочных и тестовых изображений. Из главной папки, если вы используете ту же структуру файлов выполните в командной строке следующую команду: python xml_to_csv.py.

Это создает файл train_labels.csv и test_labels.csv в папке CSGO_images. Чтобы избежать использования cmd, я создал короткий скрипт .bat под названием xml_to_csv.bat.

Затем откройте generate_tfrecord.py файл в текстовом редакторе. Замените карту меток своей собственной картой меток, где каждому объекту присваивается идентификационный номер. Это же присвоение номера будет использоваться при настройке файла labelmap.pbtxt.

Например, если вы обучаете свой собственный классификатор, вы замените следующий код в generate_tfrecord.py:

# TO-DO замените это на label mapdef class_text_to_int(row_label):    if row_label == 'table':        return 1    else:        return None

Затем сгенерируйте файлы TFRecord, запустив мой созданный файл generate_tfrecord.bat.

Эти строки генерируют файлы train.record и test.record в папке training. Они будут использоваться для обучения нового классификатора обнаружения объектов.

Последнее, что нужно сделать перед обучением это создать карту меток и отредактировать файл конфигурации обучения. Карта меток сообщает тренеру, что представляет собой каждый объект, определяя сопоставление имен классов с идентификационными номерами классов. С помощью текстового редактора создал новый файл и сохранил его как labelmap.pbtxt в папке CSGO_training. В текстовом редакторе создал карту меток. Идентификационные номера карт меток должны быть такими же, как определено в generate_tfrecord.py.

item { id: 1 name: 'table'}

Настройка обучения:

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

Перешел в каталог TensorFlow research\ object_detection\ samples\ configs и скопировал файл faster_rcnn_ inception_v2_ coco.config в каталог CSGO_training. Затем открыл файл с помощью текстового редактора. В этот файл .config необходимо внести несколько изменений, в основном изменив количество классов, примеров и добавив пути к файлам для обучающих данных. Строка 10. изменил num_classes на количество различных объектов, которые должен обнаружить классификатор. Для моего случая было так:

num_classes : 1строка 107. изменил fine_tune_checkpoint на:fine_tune_checkpoint : "faster_rcnn_inception_v2_coco_2018_01_28 / model.ckpt"Строки 122 и 124. в разделе train_input_reader изменил input_path и label_map_path на:input_path: "CSGO_images / train. record"label_map_path: "CSGO_training / labelmap.pbtxt"Линия 128. Изменил num_examples на количество изображений, имеющихся в каталоге CSGO_images\test. У меня есть 113 изображений, поэтому я меняю их на:num_examples: 113(Загружать все не стал)Строки 136 и 138. в разделе eval_input_reader измените input_path и label_map_path на:input_path: "CSGO_images / test. record"label_map_path: "CSGO_training / labelmap.pbtxt"

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

Запустите тренировку:

Осталось запустить обучение, запустив файл train.bat.

На рисунке выше каждый шаг обучения сообщает о потере. Он будет начинаться высоко и становиться все ниже и ниже по мере тренировки. Для моего обучения он начинался примерно с 1.5 . Я рекомендую позволить вашей модели тренироваться до тех пор, пока потеря последовательно не упадет ниже 0,05, что может занять довольно большое количество шагов или около нескольких часов (в зависимости от того, насколько мощен ваш процессор или графический процессор). При использовании другой модели, цифры потерь могут быть разными. Кроме того, это зависит от объектов, которые вы хотите обнаружить.

Теперь нужно экспортировать график вывода и обнаруживать наши собственные пользовательские объекты.

Экспорт Графика Вывода:

Теперь, когда обучение завершено, последний шаг это создание замороженного графика вывода (наша модель обнаружения). В папке graph лежит файл export_inference_graph.py, затем из командной строки выполните следующую команду, где XXXX в model.ckpt-XXXX должен быть заменен на самый высокий номер файла .ckpt в папке обучения:

python export_inference_graph.py --input_type image_tensor --pipeline_config_path CSGO_training/faster_rcnn_inception_v2_coco.config --trained_checkpoint_prefix CSGO_training/model.ckpt-XXXX --output_directory CSGO_inference_graph

Используйте наш обученный пользовательский классификатор обнаружения объектов:

Приведенная выше строка создает файл frozen_inference_graph.pb в папке /coco_v3/ CSGO_inference_ graph. Файл .pb содержит классификатор обнаружения объектов. Переименуйте его в frozen_inference_graph.pb . В папке coco_v3 возьмите файл predict.py Изменил строку 39 на мой замороженный файл графика вывода.

PATH_TO_FROZEN_GRAPH = 'graph/frozen_inference_graph.pb'

Изменена строка 41 в мой файл labelmap.

PATH_TO_LABELS = 'graph/labelmap.pbtxt'

И, наконец, перед запуском скриптов Python вам нужно изменить переменную NUM_CLASSES в скрипте, чтобы она равнялась количеству классов, которые мы хотим обнаружить. Я использую только 1 класс, поэтому я изменил его на 1:
NUM_CLASSES = 1

В 65 строчке вам нужно задать картинку, на которой будет происходить детектирование.

После запуска вы увидите окно и распознанную табличку.

На этом все, спасибо за внимание.

Подробнее..

Перевод Регулярные выражения Python для новичков что это, зачем и для чего

14.04.2021 14:22:42 | Автор: admin
image

За последние несколько лет машинное обучение, data science и связанные с этими направлениями отрасли очень сильно шагнули вперед. Все больше компаний и просто разработчиков используют Python и JavaScript для работы с данными.

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

Кстати, свои советы по некоторым функциям добавил Алексей Некрасов лидер направления Python в МТС, программный директор направления Python в Skillbox. Чтобы было понятно, где перевод, а где комментарии, последние мы выделим цитатой.

Зачем нужны регулярные выражения?


Они помогают быстро решить самые разные задачи при работе с данными:
  • Определить нужный формат данных, включая телефонный номер или e-mail адрес.
  • Разбивать строки на подстроки.
  • Искать, извлекать и заменять символы.
  • Быстро выполнять нетривиальные операции.

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

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

А что там с регулярными выражениями в Python?


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

import re

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

  • re.match()
  • re.search()
  • re.findall()
  • re.split()
  • re.sub()
  • re.compile()

Давайте рассмотрим каждый из них.

re.match(pattern, string)

Метод предназначен для поиска по заданному шаблону в начале строки. Так, если вызвать метод match() на строке AV Analytics AV с шаблоном AV, то его получится успешно завершить.

import reresult = re.match(r'AV', 'AV Analytics Vidhya AV')print(result) Результат:<_sre.SRE_Match object at 0x0000000009BE4370>

Здесь мы нашли искомую подстроку. Для вывода ее содержимого используется метод group(). При этом используется r перед строкой шаблона, чтобы показать, что это raw-строка в Python.

result = re.match(r'AV', 'AV Analytics Vidhya AV')print(result.group(0)) Результат:AV

Окей, теперь давайте попробуем найти Analythics в этой же строке. У нас ничего не получится, поскольку строка начинается на AV, метод возвращает none:

result = re.match(r'Analytics', 'AV Analytics Vidhya AV')print(result) Результат:None

Методы start() и end() используются для того, чтобы узнать начальную и конечную позицию найденной строки.

result = re.match(r'AV', 'AV Analytics Vidhya AV')print(result.start())print(result.end()) Результат:02

Все эти методы крайне полезны в ходе работы со строками.

re.search(pattern, string)

Этот метод похож на match(), но его отличие в том, что ищет он не только в начале строки. Так, search() возвращает объект, если мы пробуем найти Analythics.

result = re.search(r'Analytics', 'AV Analytics Vidhya AV')print(result.group(0)) Результат:Analytics

Что касается метода search (), то он ищет по всей строке, возвращая, впрочем, лишь первое найденное совпадение.

re.findall(pattern, string)

Здесь у нас возврат всех найденных совпадений. Так, у метода findall() нет никаких ограничений на поиск в начале или конце строки. Например, если искать AV в строке, то мы получим возврат всех вхождений AV. Для поиска рекомендуется использовать как раз этот метод, поскольку он умеет работать как re.search(), так и как re.match().

result = re.findall(r'AV', 'AV Analytics Vidhya AV')print(result) Результат:['AV', 'AV']

re.split(pattern, string, [maxsplit=0])

Этот метод разделяет строку по заданному шаблону.

result = re.split(r'y', 'Analytics')print(result) Результат:['Anal', 'tics']

В указанном примере слово Analythics разделено по букве y. Метод split() здесь принимает и аргумент maxsplit со значением по умолчанию, равным 0. Таким образом он разделяет строку столько раз, сколько это возможно. Правда, если указать этот аргумент, то разделение не может быть выполнено более указанного количества раз. Вот несколько примеров:

result = re.split(r'i', 'Analytics Vidhya')print(result) Результат:['Analyt', 'cs V', 'dhya'] # все возможные участки. result = re.split(r'i', 'Analytics Vidhya', maxsplit=1)print(result) Результат:['Analyt', 'cs Vidhya']

Здесь параметр maxsplit установлен равным 1, в результате чего строка разделена на две части вместо трех.

re.sub(pattern, repl, string)

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

result = re.sub(r'India', 'the World', 'AV is largest Analytics community of India')print(result) Результат:'AV is largest Analytics community of the World'

re.compile(pattern, repl, string)

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

pattern = re.compile('AV')result = pattern.findall('AV Analytics Vidhya AV')print(result)result2 = pattern.findall('AV is largest analytics community of India')print(result2) Результат:['AV', 'AV']['AV']

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

  • . Один любой символ, кроме новой строки \n.
  • ? 0 или 1 вхождение шаблона слева
  • + 1 и более вхождений шаблона слева
  • * 0 и более вхождений шаблона слева
  • \w Любая цифра или буква (\W все, кроме буквы или цифры)
  • \d Любая цифра [0-9] (\D все, кроме цифры)
  • \s Любой пробельный символ (\S любой непробельный символ)
  • \b Граница слова
  • [..] Один из символов в скобках ([^..] любой символ, кроме тех, что в скобках)
  • \ Экранирование специальных символов (\. означает точку или \+ знак плюс)
  • ^ и $ Начало и конец строки соответственно
  • {n,m} От n до m вхождений ({,m} от 0 до m)
  • a|b Соответствует a или b
  • () Группирует выражение и возвращает найденный текст
  • \t, \n, \r Символ табуляции, новой строки и возврата каретки соответственно

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

Несколько примеров использования регулярных выражений


Пример 1. Возвращение первого слова из строки

Давайте сначала попробуем получить каждый символ с использованием (.)

result = re.findall(r'.', 'AV is largest Analytics community of India')print(result) Результат:['A', 'V', ' ', 'i', 's', ' ', 'l', 'a', 'r', 'g', 'e', 's', 't', ' ', 'A', 'n', 'a', 'l', 'y', 't', 'i', 'c', 's', ' ', 'c', 'o', 'm', 'm', 'u', 'n', 'i', 't', 'y', ' ', 'o', 'f', ' ', 'I', 'n', 'd', 'i', 'a']


Теперь сделаем то же самое, но чтобы в конечный результат не попал пробел, используем \w вместо (.)

result = re.findall(r'\w', 'AV is largest Analytics community of India')print(result) Результат:['A', 'V', 'i', 's', 'l', 'a', 'r', 'g', 'e', 's', 't', 'A', 'n', 'a', 'l', 'y', 't', 'i', 'c', 's', 'c', 'o', 'm', 'm', 'u', 'n', 'i', 't', 'y', 'o', 'f', 'I', 'n', 'd', 'i', 'a']

Ну а теперь проделаем аналогичную операцию с каждым словом. Используем при этом * или +.

result = re.findall(r'\w*', 'AV is largest Analytics community of India')print(result) Результат:['AV', '', 'is', '', 'largest', '', 'Analytics', '', 'community', '', 'of', '', 'India', '']

Но и здесь в результате оказались пробелы. Причина * означает ноль или более символов. "+" поможет нам их убрать.

result = re.findall(r'\w+', 'AV is largest Analytics community of India')print(result)Результат:['AV', 'is', 'largest', 'Analytics', 'community', 'of', 'India']

Теперь давайте извлечем первое слово с использованием
^:

result = re.findall(r'^\w+', 'AV is largest Analytics community of India')print(result) Результат:['AV']

А вот если использовать $ вместо ^, то получаем последнее слово, а не первое:

result = re.findall(r'\w+$', 'AV is largest Analytics community of India')print(result) Результат:[India] 

Пример 2. Возвращаем два символа каждого слова

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

result = re.findall(r'\w\w', 'AV is largest Analytics community of India')print(result) Результат:['AV', 'is', 'la', 'rg', 'es', 'An', 'al', 'yt', 'ic', 'co', 'mm', 'un', 'it', 'of', 'In', 'di']


Теперь пробуем извлечь два последовательных символа с использованием символа границы слова (\b):

result = re.findall(r'\b\w.', 'AV is largest Analytics community of India')print(result) Результат:['AV', 'is', 'la', 'An', 'co', 'of', 'In']

Пример 3. Возвращение доменов из списка адресов электронной почты.

На первом этапе возвращаем все символы после @:

result = re.findall(r'@\w+', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')print(result) Результат:['@gmail', '@test', '@analyticsvidhya', '@rest']

В итоге части .com, .in и т. д. не попадают в результат. Чтобы исправить это, нужно поменять код:

result = re.findall(r'@\w+.\w+', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')print(result) Результат:['@gmail.com', '@test.in', '@analyticsvidhya.com', '@rest.biz']

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

result = re.findall(r'@\w+.(\w+)', 'abc.test@gmail.com, xyz@test.in, test.first@analyticsvidhya.com, first.test@rest.biz')print(result) Результат:['com', 'in', 'com', 'biz']

Пример 4. Получение даты из строки

Для этого необходимо использовать \d

result = re.findall(r'\d{2}-\d{2}-\d{4}', 'Amit 34-3456 12-05-2007, XYZ 56-4532 11-11-2011, ABC 67-8945 12-01-2009')print(result) Результат:['12-05-2007', '11-11-2011', '12-01-2009']

Для того, чтобы извлечь только год, помогают скобки:

result = re.findall(r'\d{2}-\d{2}-(\d{4})', 'Amit 34-3456 12-05-2007, XYZ 56-4532 11-11-2011, ABC 67-8945 12-01-2009')print(result) Результат:['2007', '2011', '2009']

Пример 5. Извлечение слов, начинающихся на гласную

На первом этапе нужно вернуть все слова:

result = re.findall(r'\w+', 'AV is largest Analytics community of India')print(result) Результат:['AV', 'is', 'largest', 'Analytics', 'community', 'of', 'India']

После этого лишь те, что начинаются на определенные буквы, с использованием "[]":
result = re.findall(r'[aeiouAEIOU]\w+', 'AV is largest Analytics community of India')print(result) Результат:['AV', 'is', 'argest', 'Analytics', 'ommunity', 'of', 'India']

В полученном примере есть два укороченные слова, это argest и ommunity. Для того, чтобы убрать их, нужно воспользоваться \b, что необходимо для обозначения границы слова:
result = re.findall(r'\b[aeiouAEIOU]\w+', 'AV is largest Analytics community of India')print(result) Результат:['AV', 'is', 'Analytics', 'of', 'India']


Кроме того, можно использовать и ^ внутри квадратных скобок, что помогает инвертировать группы:

result = re.findall(r'\b[^aeiouAEIOU]\w+', 'AV is largest Analytics community of India')print(result) Результат:[' is', ' largest', ' Analytics', ' community', ' of', ' India']

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

result = re.findall(r'\b[^aeiouAEIOU ]\w+', 'AV is largest Analytics community of India')print(result) Результат:['largest', 'community']

Пример 6. Проверка формата телефонного номера

В нашем примере длина номера 10 знаков, начинается он с 8 или 9. Для проверки списка телефонных номеров используем:

li = ['9999999999', '999999-999', '99999x9999'] for val in li:    if re.match(r'[8-9]{1}[0-9]{9}', val) and len(val) == 10:            print('yes')    else:            print('no') Результат:yesnono

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

Здесь у нас несколько вариантов решения. Вот первое:

line = 'asdf fjdk;afed,fjek,asdf,foo' # String has multiple delimiters (";",","," ").result = re.split(r'[;,\s]', line)print(result) Результат:['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

Кроме того, можно использовать метод re.sub() для замены всех разделителей пробелами:

line = 'asdf fjdk;afed,fjek,asdf,foo'result = re.sub(r'[;,\s]', ' ', line)print(result) Результат:asdf fjdk afed fjek asdf foo

Пример 8. Извлекаем данные из html-файла

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

Пример файла









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

result=re.findall(r'<td>\w+</td>\s<td>(\w+)</td>\s<td>(\w+)</td>',str)print(result)Output:[('Noah', 'Emma'), ('Liam', 'Olivia'), ('Mason', 'Sophia'), ('Jacob', 'Isabella'), ('William', 'Ava'), ('Ethan', 'Mia'), ('Michael', 'Emily')]


Комментарий Алексея

При написании любых regex в коде придерживаться следующих правил:

  • Используйте re.compile для любых более менее сложных и длинных регулярных выражениях. А также избегайте многократного вызова re.compile на один и тот же regex.
  • Пишите подробные регулярные выражения используя дополнительный аргумент re.VERBOSE. При re.compile используйте флаг re.VERBOSE пишите regex в несколько строк с комментариями о том что происходит. Смотрите документацию по ссылкам тут и тут.

Пример:
компактный вид

pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'re.search(pattern, 'MDLV')


Подробный вид

pattern = """    ^                   # beginning of string    M{0,3}              # thousands - 0 to 3 Ms    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),                        #            or 500-800 (D, followed by 0 to 3 Cs)    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),                        #        or 50-80 (L, followed by 0 to 3 Xs)    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),                        #        or 5-8 (V, followed by 0 to 3 Is)    $                   # end of string    """re.search(pattern, 'M, re.VERBOSE)

Используйте named capture group для всех capture group, если их больше чем одна (?P...). (даже если одна capture, тоже лучше использовать).
regex101.com отличный сайт для дебага и проверки regex

При разработке регулярного выражения, нужно не забывать и про его сложность выполнения иначе можно наступить на те же грабли, что и относительно недавно наступила Cloudflare.
1 Noah Emma 2 Liam Olivia 3 Mason Sophia 4 Jacob Isabella 5 William Ava 6 Ethan Mia 7 Michael Emily
Подробнее..

Конструирование эпидемиологических моделей

09.04.2021 20:23:06 | Автор: admin

Эпидемиология из-за некоторого стечения обстоятельств стала очень популярной за последний год. Интерес к моделированию эпидемий стал возникать у многих и уже всё больше людей знают о вездесущей SIR модели. Но есть ли другие подобные модели? Насколько сложно из вообще создавать и модифицировать? Но обо всём по порядку.

За несколько месяцев до появления первых новостей о COVID-19 я для себя, так скажем, в образовательных целях программирования, начал писать программу визуализации эпидемий. В этой простой программе кружочки разных цветов двигались по полю и заражали друг друга. Через полгода, каким-то образом это уже стало темой моей дипломной работы (специальность биоинженерия и биоинформатика) и пришлось по-настоящему вникать в математическое моделирование эпидемий для написания статей. Благо интернет вдруг стал наполняться эпидемиологическими материалами. Я это виду к тому, что работать я начал за некоторое время до того, как эпидемиология стала мейнстримом.

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

Просто о SIR модели

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

Я понимаю, что медиапространство заполнено всевозможными описаниями данной модели, поэтому я не побоюсь и добавлю ещё одно.

SIR модель. Всю популяцию, которая подвержена эпидемии, разделили на три группы: S (susceptible восприимчивые, то есть здоровые, не заразные, но могут заразиться, так как не имеют иммунитета), I (infected инфицированные, то есть болеют и заразны) и R (recovered выздоровевшие, то есть здоровые, не заразные и не могут заразиться, так как имеют иммунитет). Очевидно, что до начала эпидемии 100 % индивидов находятся в группе S (восприимчивые) и по нулям в остальных группах. Для удобства будем считать, популяция в 100 человек, соответственно S = 100, I = 0, R = 0. В таких условиях эпидемия, конечно, не пойдёт, так как чтобы она началась, должен быть хотя бы один больной. Поэтому рассмотри другую ситуацию: S = 99, I = 1, R = 0. Вот теперь начнётся эпидемия и её моделирование заключается в последовательном высчитывания состояния популяции на следующем шаге.

Дальше чуть сложнее, чтобы понимать сколько людей заразиться на каждом шаге, надо понимать наличие двух вероятностей: вероятность контакта между двумя индивидами и вероятность заразить при контакте инфицированного с восприимчивым (). Часто в модели для воплощения первой вероятности используют просто 1/N (N объём популяции), подразумевая, что в каждый момент времени каждый индивид контактирует с одним случайным индивидом в популяции. А вторая вероятность (), обеспечивает собственно биологический показатель заразности конкретного патогена (со всеми влияющим факторами: температура, наличие маски и т.п.).

Один инфицированный встретит и заразит в конкретный момент времени конкретного восприимчивого с вероятностью:

Тогда всего он заразит восприимчивых индивидов:

А все инфицированные вместе заразят восприимчивых

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

Ещё инфицированные выздоравливают, тоже с какой-то вероятностью (), которую часто рассматривают как число обратное времени болезни. Имеется в виду, что если болезнь длится 10 дней, то больной индивид в конкретный день выздоровеет с вероятностью = 1/10. Получается количество выздоравливающих на каждом шаге будет равно:

Количество инфицированных на втором шаге, помимо того, что увеличится на 0.99* ещё уменьшится на 1*. Количество выздоровевших увеличится на 1*. Относительного полученного состояния будет высчитываться следующий шаг модели.

Таким образом модель формулируется следующими уравнениями:

Модификация SEIR

О SIR модели слышали теперь довольно многие. О существовании других моделей слышало уже меньше людей. Часто всё-таки вспоминают SEIR модель, которую рассматривают как модификацию SIR.

SEIR модель учитывает инкубационный период (E exposed, индивиды болеют, но не заразны и со временем полностью заболеют). В такой модели заражение восприимчивых происходит таким же способом как в модели SIR, но попадают такие особи не в группу I, а в группу E. А из E с определённой вероятностью (, число обратное длительности инкубационного периода) происходит переход уже в I.

Компартментальные модели в целом

Существует ещё много модификаций SIR моделей. Все они, включая саму SIR, являются представителями целого класса моделей, которые называют компартментальными эпидемиологическими моделями. Упоминаемое выше разделение популяции на группы или компартменты (отсеки) как раз и обуславливает такое название моделей.

Сложность компартментальных моделей не ограничена тремя или четырьмя группами. Такие модели могут учитывать самые различные сценарии: введение карантинных мер (SIQR, добавляется группа Q quarantine), потеря иммунитета (SIRS, переход с некоторой вероятностью из R обратно в S), группы риска у восприимчивых (несколько групп S: S1, S2, , каждая из которых со своей вероятностью заражается), различные варианты течения болезни (несколько групп I: I1, I2, , в каждую из которых своя вероятность попадания восприимчивых особей, а также у каждой допустим своя заразность) и т.д. Ограничением здесь является только фантазия исследователя.

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

Схема распространения туберкулёзаСхема распространения туберкулёза

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

DEMMo (конструктор моделей)

В этом и заключается назначение разрабатываемой мной программы DEMMo. DEMMo Designer of Epidemic Math Models (конструктор эпидемиологических математических моделей). Конструированные модели организуется в объектно-ориентированном виде. Есть класс стадии (аналог компартмента), класс потока (обеспечивает переход индивидов между стадиями) и внешнего потока (прибавление/вычитание индивидов к/из стадии). Подробную инструкцию по использованию программы попытался написать в документации.

Результаты различных моделей, реализованных с использованием программыРезультаты различных моделей, реализованных с использованием программы

Программу писал на python. Интерфейс на PyQt5. Выложил исходный код программы на github (с git работал впервые) и архив с готовой версией для windows на гугл диск. Будем считать, что начинаю бета тестирование программы. Надеюсь, я хоть немного понятно объяснил и для тех, кому интересно, буду очень рад если программу жестоко поэксплуатируют. Вроде бы настроил систему создания отчётов об ошибках. Это мой первый крупный проект после задачек на ряды Фибоначчи и т.п.

Код очень плохо задокументирован, причём часть комментариев на русском, а часть на английском. Знаю, что плохо, каюсь. Надеюсь займусь этим. Если будут какие-то конкретные советы от матёрых программистов, буду очень рад.
Контактная почта: demmo.development@gmail.com

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

Подробнее..

Обработка и анализ текстов на Python и Spark NLP

08.04.2021 22:07:23 | Автор: admin

В наше время без анализа и обработки текстов,не обходится ни один проект, и так уж сложилось что Python обладает широким спектром библиотек и фреймворков для задач NLP. Задачи могут быть как тривиальные: анализ тональности(sentiment) текста, настроение, распознавание сущностей(NER) так и более интересные как боты, сравнение диалогов в саппорт-чатах - мониторить следует ли ваша тех.поддержка или сейлз текстовым скриптам, или постобработка текста после SpeechToText.

Для решения задач NLP имеется огромное количество инструментов. Вот короткий список таковых:

Речь как вы понимаете пойдет о последнем, так как он включает в себя практически все что умеют выше перечисленные библиотеки. Существуют как бесплатные pre-trained модели так и платные, узкоспециализированные например для healthcare.

Для работы Spark NLP понадобится Java 8 - она нужна для фреймворка Apache Spark с помощью которого и работает Spark NLP. Для экспериментов на сервере или на локальной машине потребуется минимум 16Гб ОЗУ. Устанавливать лучше на каком нибудь дистрибутиве Linux (На macOS могут возникнуть трудности), лично я выбрал Ubuntu инстанс на AWS.

apt-get -qy install openjdk-8

Также нужно установить Python3 и сопутствующие библиотеки

apt-get -qy install build-essential python3 python3-pip python3-dev gnupg2

pip install nlu==1.1.3

pip install pyspark==2.4.7

pip install spark-nlp==2.7.4

Экспериментировать можно так же на colab. Работает Spark NLP по принципу конвейеров (pipeline), ваш текст проходит некоторое количество стадий которые вы описали в pipe-лайне, и каждая стадия производит описанные манипуляции, к примеру: пайплайн для получения именованных сущностей. Ниже на картинке схема наиболее часто встречающихся стадий которые будет проходить ваш входной текст, каждая стадия конвейера добавляет свою колонку с данными после ее выполнения.

Пример конвейера в Spark NLPПример конвейера в Spark NLP

Пример создания стадий для пайплайна. (весь код примера по ссылке на colab)

documentAssembler = DocumentAssembler() \    .setInputCol('text') \    .setOutputCol('document')tokenizer = Tokenizer() \    .setInputCols(['document']) \    .setOutputCol('token')embeddings = BertEmbeddings.pretrained(name='bert_base_cased', lang='en') \        .setInputCols(['document', 'token']) \        .setOutputCol('embeddings')ner_model = NerDLModel.pretrained('ner_dl_bert', 'en') \    .setInputCols(['document', 'token', 'embeddings']) \    .setOutputCol('ner')ner_converter = NerConverter() \    .setInputCols(['document', 'token', 'ner']) \    .setOutputCol('ner_chunk')nlp_pipeline = Pipeline(stages=[    documentAssembler,     tokenizer,    embeddings,    ner_model,    ner_converter])
  1. documentAssembler - создает аннотацию типаDocument,которая может использоваться аннотаторами в будущем

  2. tokenizer - разбивает текст и пунктуацию на массив строк

  3. embeddings -создает векторные представления для слов

  4. ner_model - распознаватель именованных сущностей. к примеру: October 28, 1955 = DATE

  5. ner_converter - добавляет колонку с отдельными распознанными сущностями October 28, 1955

И все конечно хорошо, но приходится как-то много кода писать - описывая стадии пайплайна и сам пайплайн, не говоря про подключение библиотек и инициализацию Spark NLP, поэтому разработчики SparkNLP (johnsnowlabs) сделали более высокоуровневую библиотеку или синтаксический сахар над SparkNLP - называйте как хотите, но когда мы попробуем повторить вышеприведенный пример:

import nlupipeline = nlu.load('ner')result = pipeline.predict(  text, output_level='document').to_dict(orient='records')

мы получим все те же самые NER, но написав на порядок меньше кода.

Хотелось бы еще отметить что оба варианта получения именованных сущностей, требуют некоторое время на инициализацию Apache Spark, предзагрузку моделей и установку связи интерпретатора Python c Spark через pyspark. Потому вам не особо захочется по 10-100 раз перезапускать скрипт с кодом выше, нужно предусмотреть предзагрузку и просто обрабатывать текст посредством вызова predict, в моем случае я сделал инициализацию нужных мне конвейеров во время инициализации Сelery воркеров.

# паттерн Реестрpipeline_registry = PipelineRegistry()def get_pipeline_registry():    pipeline_registry.register('sentiment', nlu.load('en.sentiment'))    pipeline_registry.register('ner', nlu.load('ner'))    pipeline_registry.register('stopwords', nlu.load('stopwords'))    pipeline_registry.register('stemmer', nlu.load('stemm'))    pipeline_registry.register('emotion', nlu.load('emotion'))    return pipeline_registry@worker_process_init.connectdef init_worker(**kwargs):    logging.info("Initializing pipeline_factory...")    get_pipeline_registry()

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

Подробнее..
Категории: Python , Nlp , Spark , Nlu

HMM ловим мошеннические транзакции

09.04.2021 14:13:02 | Автор: admin

Три года я проработал в Сербии iOS-евангелистом - было два профильный проекта и один Machine Learning-овый.

Если вам стало интересно - добро пожаловать в мир HMM.

Постановка задачи

Австрийский банк. У него много клиентов, у клиентов открыт счет в этом банке. В течении года клиент тратит средства со своего счета. Ходит в магазины, гасит коммунальные платежи и пр. Каждое списание денег со счета назовем транзакцией. Дана последовательность транзакций за определенное время (скажем год). Надо обучить машину, чтобы она начала проверять новые транзакции как достоверные или подозрительные. И выдавала предупреждение в последнем случае. Для решения задачи надо использовать Hidden Markov Model.

Введение в HMM

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

Представим эту последовательность из 365 символов в виде массива. h означает здоров, l - болен.

days{365} = {hhhhhhhhhhllllllllllhhhhhhhhhhhhhhhhhhhhhhhh...hhhhh}

Вопрос - Какова вероятность, что я сегодня болен?

\frac{10}{365}= 3 процента

Если вам понятен ответ, то читайте дальше, через 15 минут вы станете экспертом по HMM. Если нет - ставьте плюс и идите ужинать.

Изменим вопрос - Какова вероятность, что я завтра буду больным?

Вы резонно уточните: -А сегодня я болен или здоров?

Если болен (смотрите на последовательность - таких больных дней всего 10), то с вероятностью \frac{9}{10} = 90 процентов завтра я продолжу болеть и с вероятностью 10 процентов стану здоров.

А если я сегодня здоров? - то с вероятностью

\frac{1}{355}= 0.3 процента я завтра заболею и с вероятностью 99.7% буду здоров.

Если вы сегодня больны, то с вероятностью 10% завтра вы будете здоровы и 90% продолжите болеть.

Получили 4 числа, которые красиво вписываются в матрицу 2 на 2 - вуаля! эта матрица и есть Марковский слой. Уберем проценты, оставим чистые вероятности в диапазоне от 0 до 1, как того требует наука.

здоров

болен

здоров

0.997

0.003

болен

0.10

0.90

Еще раз взглянем на матрицу и прочитаем слева направо, что если ты сегодня здоров, завтра с вероятностью 0.997 ты останешься здоров, а с вероятностью 0.003 заболеешь и так далее.

Формализуем транзакции

Чем отличается последовательность транзакций от череды болен/здоров? Ничем.

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

27.10.2020 00:00 GAZPROMNEFT AZS 219    2507,43 118 753,95 28.10.2020 / 298380 Автомобиль 26.10.2020 14:45 SPAR 77                319,73 121 261,38 27.10.2020 / 220146 Супермаркеты 26.10.2020 14:38 ATM 60006475           4800,00 121 581,11 26.10.2020 / 213074 Выдача наличных 25.10.2020 17:52 EUROSPAR 18            970,02 126 381,11 26.10.2020 / 259110 Супермаркеты 25.10.2020 00:00 Tinkoff Card2Card      20000,00 127 351,13 26.10.2020 / 253237 Перевод с карты 22.10.2020 14:22 SBOL перевод 4276      7000,00 147 351,13 22.10.2020 / 276951 Перевод с карты 22.10.2020 12:18 STOLOVAYA              185,00 154 351,13 23.10.2020 / 279502 Рестораны и кафе 21.10.2020 16:46 MEGAFON R9290499831    500,00 154 536,13 21.10.2020 / 224592 Комунальные платежи, связь, интернет. 21.10.2020 14:17 SPAR 77                987,03 155 036,13 22.10.2020 / 219015 Супермаркеты 21.10.2020 13:42 PYATEROCHKA 646        289,93 156 023,16 22.10.2020 / 294539 Супермаркеты 21.10.2020 00:00 MEBEL                  75,00 156 313,09 22.10.2020 / 279935 Прочие расходы 19.10.2020 14:54 SPAR 77                552,92 132 044,80 20.10.2020 / 208987 Супермаркеты 19.10.2020 00:00 MOBILE FEE             60,00 132 597,72 20.10.2020 / - Прочие операции 16.10.2020 14:19 SPAR 77                579,39 132 657,72 17.10.2020 / 229627 Супермаркеты 12.10.2020 13:33 STOLOVAYA              185,00 133 237,11 13.10.2020 / 261374 Рестораны и кафе 12.10.2020 00:00 OOO MASTERHOST         1000,00 133 422,11 13.10.2020 / 268065 Прочие расходы 11.10.2020 12:09 SPAR 77                782,87 134 422,11 12.10.2020 / 275816 Супермаркеты 10.10.2020 14:52 SBOL перевод           400,00 135 204,98 10.10.2020 / 276925 Перевод с карты 09.10.2020 13:29 SBOL перевод 5484*     1000,00 135 604,98 09.10.2020 / 229184 Перевод с карты 09.10.2020 11:55 MAGNIT MK KRYUCHYA     274,00 136 604,98 10.10.2020 / 209914 Супермаркеты

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

def readtrans():    with open ("assets/trans.txt", "r") as file:        grades = file.read()    pattern = '(\d{2,5}),\d\d'    result = re.findall(pattern, grades)    r = list(map(int, result[0::2]))    return rdata = readtrans()t = list(range(len(data)))df = pd.DataFrame({'number':t, 'amount':data})ax1 = df.plot.bar(x='number', y='amount', rot=0, width=1.5)

Упростим картину - дешевые транзакции (менее 10$) обозначим буквой l, дорогие свыше 100$ буквой h, остальные - буквой m.

Наша последовательность стала выглядеть подозрительно знакомо

print(observations[:20])trans[] = ['m', 'm', 'm', 'l', 'm', 'm', 'h', 'm', 'l', 'l', 'm', 'l', 'l', 'l', 'l', 'l', 'l', 'm', 'l', 'l']

Теперь напишем Марковский слой для транзакций. Это будет матрица 3 на 3, поскольку у нас 3 возможных типа транзакций = {l,m,h}

[[0.5 0.3 0.2] [0.6 0.3 0.1] [0.7 0.3 0.0]]

Расшифровываем последнюю строку матрицы - если последняя транзакция была дорогая, то с вероятностью 0.7 последующая операция будет дешевой, с вероятностью 0.3 - средней и никогда снова дорогой.

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

-Так просто?! - воскликнет читатель. Да, но за построение такой простой модели банк много денег не заплатит. Надо ввести еще один Марковский слой (невидимый), и алгоритм сразу станет красивее, сложнее и наукоёмчее. И потянет на кандидатскую диссертацию.

Скрытый Марковский слой

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

То есть существует набор периодических однотипных событий, число которых мы не знаем и Марковский слой для которых невозможно построить. Как построишь, если ничего не знаешь конкретно?! Вот именно. Но мы знаем, что эти события происходят и их примерно 4-6 штук. Поход в магазин. Столовая. Еще что-то. И так далее. И что мы точно знаем, что после похода в столовую мы никогда не опустошаем свой кошелек на сумму более 300 рублей.

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

[[a1 a2 a3 a4 a5] [b1 b2 b3 b4 b5] [c1 c2 c3 c4 c5] [x1 x2 x3 x4 x5] [y1 y2 y3 y4 y5]]

Именно 20, а не 25 (отвечать будет Ягуар). Это точно такой же Марковский слой, что мы строили раньше, но основанный на 5 событиях.

И чтобы связать два слоя (видимый с платежами и невидимый с событиями) мы заведем матрицу перехода размера 5 на 3.

В чем смысл матрицы перехода? В том, что после невидимого события a (поход в столовую)

у нас произойдет с разной степенью вероятности либо l-платеж, либо m-платеж, либо h-платеж. Скорее всего после похода в столовую эта строка будет такая

[0.96 0.04 0.0]

Что означает невозможность потратить более 100 долларов в нашей технопарковской столовой. Даже с приглашенной девушкой.

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

И мы можем найти эти 20+10 неизвестных величин, потому что у нас есть история платежей!

Это прекрасно и это называется обучить систему!

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

Обучение

В питоне есть библиотека hmm, в которой существует метод обучения - ему надо подать ваши три матрицы, и серию последовательностей из транзакций. Алгоритм такой, что вы разбиваете историю ваших транзакций порциями штук по 15-20 и скармливаете ее, пока система не обучится.

Процесс сходимости хорошо виден на графике.

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

Питон против Сишарпа

Работодатель обязал меня использовать библиотек Accord и язык C#

using Accord.MachineLearning;using Accord.Statistics.Models.Markov;using Accord.Statistics.Models.Markov.Learning;using Accord.Statistics.Models.Markov.Topology;using Comtrade.FMS.Common;

Согласно контракту я не могу разглашать код, поэтому написал аналог на Питоне (я не знаю оба языка) и частично смог повторить результат. Но по скорости обучения Питон сильно проигрывает Си-шарпу. Правда, я run-ил программу на разного класса компьютерах)) В Словении это был сервер работодателя. Питон для Хабра пыхтел на 2010 года макбуке.

Приведу одну строку кода, в которой зашифрован метод обучения.

var teacher = new BaumWelchLearning(hmm)

Детали метода Баум-Уэлша вы поймете, прочитав соответствующую литературу и настроив свои мозги на стат. процессы.

Успехов и хорошей карьеры в банковских IT структурах!

Подробнее..

Развертывание приложений Django

11.04.2021 20:17:45 | Автор: admin

Введение

После того, как мы закончили разработку веб-приложения, оно должно быть размещено на хосте, чтобы общественность могла получить доступ к нему из любого места. Мы посмотрим, как развернуть и разместить приложение на экземпляре AWS EC2, используя Nginx в качестве веб-сервера и Gunicorn в качестве WSGI.

AWS EC2

Amazon Elastic Compute Cloud (Amazon EC2) - это веб-сервис, обеспечивающий масштабируемость вычислительных мощностей в облаке. Мы устанавливаем и размещаем наши веб-приложения на экземпляре EC2 после выбора AMI (OS) по нашему усмотрению. Подробнее об этом мы поговорим в следующих разделах.

NGINX

Nginx - это веб-сервер с открытым исходным кодом. Мы будем использовать Nginx для сервера наших веб-страниц по мере необходимости.

GUNICORN

Gunicorn - это серверная реализация интерфейса шлюза Web Server Gateway Interface (WSGI), который обычно используется для запуска веб-приложений Python.

WSGI - используется для переадресации запроса с веб-сервера на Python бэкэнд.

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

Развертывание приложения

Мы запустим EC2 экземпляр на AWS, для этого войдите в консоль aws.

  • Выберите EC2 из всех сервисов

  • Выберите запуск New instance и выберите Ubuntu из списка.

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

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

Подключение к Экземпляру

Мы можем подключиться к экземпляру, используя опцию 'connect' в консоли (или с помощью putty или любого другого подобного инструмента ). После подключения запустите следующие команды

sudo apt-get update

Установите python , pip и django

sudo apt install pythonsudo apt install python3-pippip3 install django

Теперь, когда мы установили наши зависимости, мы можем создать папку, в которую мы скопируем наше приложение django.

cd  /home/ubuntu/  mkdir Projectcd Projectmkdir ProjectNamecd ProjectName

Теперь мы поместим наш код по следующему пути.
/home/ubuntu/Project/ProjectName

GitHub

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

  • Перейдите в только что созданную папку (/home/ubuntu/Project/ProjectName/)

  • git clone <repository-url>

Это клонирует репозиторий в папку, и в следующий раз мы сможем просто вытащить изменения с помощью git pull.

Settings.py Файл.

Мы должны внести некоторые изменения в settings.py в нашем проекте.

  • Вставьте свои секретные ключи и пароли в переменные окружения

  • Установить Debug = False

  • Добавте Ваш домейн в ALLOWED_HOSTS

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))STATIC_ROOT = os.path.join(BASE_DIR, static)

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

manage.py makemigrationsmanage.py migratemanage.py collectstatic

Установка Nginx

Для установки Nginx выполните команду

 sudo apt install nginx

Есть конфигурационный файл с именем по умолчанию в /etc/nginx/sites-enabled/, который имеет базовую настройку для NGINX, мы отредактируем этот файл.

sudo vi default

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

мы добавим proxy_pass http://0.0.0.0:9000 и укажем путь к нашей статической папке, добавив путь внутри каталога /static/, как указано выше. Убедитесь, что вы собрали все статические файлы в общую папку, запустив команду

manage.py collectstatic

Теперь запустите сервер nginx

sudo service nginx start             #to start nginxsudo service nginx stop              #to stop nginxsudo service nginx restart           #to restart nginx

Установка Gunicorn

pip install gunicorn

Убедитесь, что Вы находитесь в папке проекта, например: /home/ubuntu/Project, и запустите следующую команду, чтобы запустить gunicorn

gunicorn ProjectName.wsgi:application- -bind 0.0.0.0:9000

Теперь, когда мы установили и настроили nginx и gunicorn, к нашему приложению можно получить доступ через DNS экземпляра ec2.

Подробнее..
Категории: Python , Nginx , Python3 , Django , Aws , Gunicorn

Книга Современный скрапинг веб-сайтов с помощью Python. 2-е межд. издание

12.04.2021 16:18:43 | Автор: admin
image Привет, Хаброжители! Если программирование напоминает волшебство, то веб-скрапинг это очень сильное колдунство. Написав простую автоматизированную программу, можно отправлять запросы на веб-серверы, запрашивать с них данные, а затем анализировать их и извлекать необходимую информацию. Новое расширенное издание книги знакомит не только с веб-скрапингом, но и поможет собрать любого вида данные в современном Интернете. В части I основное внимание уделено механике веб-скрапинга: как с помощью Python запрашивать информацию с веб-сервера, производить базовую обработку серверного отклика и организовать автоматизированное взаимодействие с сайтами. В части II исследованы более специфичные инструменты и приложения, которые пригодятся при любом сценарии веб-скрапинга. Разбирайте сложные HTML-страницы. Разрабатывайте поисковые роботы с помощью фреймворка Scrapy. Изучайте методы хранения данных, полученных с помощью скрапинга. Считывайте и извлекайте данные из документов. Очищайте и нормализуйте плохо отформатированные данные. Читайте и пишите информацию на естественных языках. Освойте поиск по формам и логинам. Изучите скрапинг JavaScript и работу с API. Используйте и пишите программы для преобразования изображений в текст. Учитесь обходить скрапинговые ловушки и блокаторы ботов. Протестируйте собственный сайт с помощью скрапинга.

Веб-краулинг с помощью API


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

По мере распространения методов генерации и загрузки контента с помощью JavaScript и Ajax описанная выше ситуация становится все менее привычной. В главе 11 мы рассмотрели один из способов решения указанной проблемы: использование Selenium для автоматизации браузера и извлечения данных. Это легко сделать. Это работает почти всегда.

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

В текущей главе вы узнаете о том, как, минуя весь этот JavaScript (не выполняя и даже не загружая его!), получить прямой доступ к источнику данных к интерфейсам API, которые генерируют эти данные.

Краткое введение в API

Существует бесчисленное количество книг, докладов и руководств о нюансах API REST, GraphQL21, JSON и XML, однако все они основаны на одной простой концепции. API определяет стандартизованный синтаксис, который позволяет одной программе взаимодействовать с другой, даже если они написаны на разных языках или имеют разную структуру.

Данный раздел посвящен веб-API (в особенности позволяющим веб-серверу взаимодействовать с браузером), и здесь мы будем понимать под API именно этот тип интерфейсов. Но вы можете учесть, что в других контекстах API также является обобщенным термином, который может обозначать, например, интерфейс, позволяющий программе на Java взаимодействовать с программой на Python, работающей на том же компьютере. API не всегда означает интерфейс через Интернет и не обязательно должен включать в себя какие-либо веб-технологии.

Веб-API чаще всего используются разработчиками для взаимодействия с широко разрекламированными и хорошо документированными открытыми сервисами. Например, американский кабельный спортивный телевизионный канал ESPN предоставляет API (http://personeltest.ru/away/www.espn.com/apis/devcenter/docs/) для получения информации о спортсменах, счетах в играх и др. У Google в разделе для разработчиков (http://personeltest.ru/aways/console.developers.google.com) есть десятки API для языковых переводов, аналитики и геолокации.

В документации всех этих API обычно описываются маршруты или конечные точки в виде URL, которые можно запрашивать, с изменяемыми параметрами, либо входящими в состав этих URL, либо выступающими в роли GET-параметров.

Например, в следующем URL pathparam является параметром пути:

example.com/the-api-route/pathparam

А здесь pathparam является значением параметра param1:

example.com/the-api-route?param1=pathparam

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

Ответ на API-запрос обычно возвращается в формате JSON или XML. В настоящее время JSON гораздо популярнее, чем XML, но последний иногда тоже встречается. Многие API позволяют выбирать тип ответа, обычно с помощью еще одного параметра, определяющего, какой тип ответа вы хотите получить.

Вот пример ответа на API-запрос в формате JSON:

{"user":{"id": 123, "name": "Ryan Mitchell", "city": "Boston"}}


А вот ответ на API-запрос в формате XML:

<user><id>123</id><name>Ryan Mitchell</name><city>Boston</city></user>


Сайт ip-api.com (http://personeltest.ru/away/ip-api.com/) имеет понятный и удобный API, который преобразует IP-адреса в реальные физические адреса. Вы можете попробовать выполнить простой запрос API, введя в браузере следующее

ip-api.com/json/50.78.253.58

В результате вы получите примерно такой ответ:

{"ip":"50.78.253.58","country_code":"US","country_name":"United States","region_code":"MA","region_name":"Massachusetts","city":"Boston","zip_code":"02116","time_zone":"America/New_York","latitude":42.3496,"longitude":-71.0746,"metro_code":506}

Обратите внимание: в запросе есть параметр пути json. Чтобы получить ответ в формате XML или CSV, нужно заменить его на соответствующий формат:

ip-api.com/xml/50.78.253.58
ip-api.com/csv/50.78.253.58


API и HTTP-методы

В предыдущем разделе мы рассмотрели API, отправляющие на сервер GET-запрос для получения информации. Существует четыре основных способа (или метода) запроса информации с веб-сервера через HTTP:

GET;
POST;
PUT;
DELETE.

Технически типов запросов больше четырех (например, еще есть HEAD, OPTIONS и CONNECT), но они редко используются в API и маловероятно, что когда-либо встретятся вам. Подавляющее большинство API ограничиваются этими четырьмя методами, а иногда даже какой-то их частью. Постоянно встречаются API, которые используют только GET или только GET и POST.

GET тот запрос, который вы используете, когда посещаете сайт, введя его адрес в адресной строке браузера. Обращаясь по адресу ip-api.com/json/50.78.253.58, вы применяете именно метод GET. Такой запрос можно представить как команду: Эй, веб-сервер, будь добр, выдай мне эту информацию.

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

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

Запрос PUT при взаимодействии с сайтами используется реже, но время от времени встречается в API. Этот запрос применяется для изменения объекта или информации. Например, в API можно задействовать запрос POST для создания пользователя и запрос PUT для изменения его адреса электронной почты.

Запросы DELETE, как нетрудно догадаться, служит для удаления объекта. Например, если отправить запрос DELETE по адресу myapi.com/user/23, то будет удален пользователь с идентификатором 23. Методы DELETE нечасто встречаются в открытых API, поскольку те в основном создаются для распространения информации или чтобы позволить пользователям создавать или публиковать информацию, но не удалять ее из баз данных.

В отличие от GET запросы POST, PUT и DELETE позволяют передавать информацию в теле запроса, в дополнение к URL или маршруту, с которого запрашиваются данные.
Как и ответ, получаемый от веб-сервера, эти данные в теле запроса обычно представляются в формате JSON или реже в формате XML. Конкретный формат данных определяется синтаксисом API. Например, при использовании API, который добавляет комментарии к сообщениям в блоге, можно создать следующий PUT-запрос:

example.com/comments?post=123

с таким телом запроса:

{"title": "Great post about APIs!", "body": "Very informative. Really helped me out with a tricky technical challenge I was facing. Thanks for taking the time to write such a detailed blog post about PUT requests!", "author": {"name": "Ryan Mitchell", "website": "http://pythonscraping.com", "company": "O'Reilly Media"}}


Обратите внимание: идентификатор сообщения в блоге (123) передается в качестве параметра в URL, а контент создаваемого нами комментария в теле запроса. Параметры и данные могут передаваться и в параметре, и в теле запроса. Какие параметры обязательны и где передаются опять-таки определяется синтаксисом API.

Подробнее об ответах на API-запросы

Как мы видели в примере с сайтом ip-api.com в начале данной главы, важной особенностью API является то, что эти интерфейсы возвращают хорошо отформатированные ответы. Наиболее распространенные форматы ответов XML (eXtensible Markup Language расширяемый язык разметки) и JSON (JavaScript Object Notation нотация объектов JavaScript).

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

<user><firstname>Ryan</firstname><lastname>Mitchell</lastname><username>Kludgist</username></user>

А теперь посмотрите на те же данные в формате JSON:

{"user":{"firstname":"Ryan","lastname":"Mitchell","username":"Kludgist"}}


Это всего 73 символа, на целых 36 % меньше, чем те же данные в формате XML.
Конечно, вероятен аргумент, что XML можно отформатировать так:

<user firstname="ryan" lastname="mitchell" username="Kludgist"></user>


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

Другая причина, по которой JSON так быстро становится более популярным, чем XML, связана с изменением веб-технологий. Раньше получателями API были по большей части серверные скрипты на PHP или .NET. Сейчас вполне может оказаться, что получать и отправлять вызовы API будет фреймворк наподобие Angular или Backbone. Серверным технологиям до определенной степени безразлично, в какой форме к ним поступают данные. Однако библиотекам JavaScript, таким как Backbone, проще обрабатывать JSON.

Принято считать, что API возвращают ответ либо в формате XML, либо в формате JSON, однако возможен любой другой вариант. Тип ответа API ограничен только воображением программиста, создавшего этот интерфейс. Еще один типичный формат ответа CSV (как видно из примера с ip-api.com). Отдельные API даже позволяют создавать файлы. Можно отправить на сервер запрос, по которому будет сгенерировано изображение с наложенным на него заданным текстом, или же запросить определенный файл XLSX или PDF.

Некоторые API вообще не возвращают ответа. Например, если отправить на сервер запрос для создания комментария к записи в блоге, то он может вернуть только HTTP-код ответа 200, что означает: Я опубликовал комментарий; все в порядке! Другие запросы могут возвращать минимальный ответ наподобие такого:

{"success": true}


В случае ошибки вы можете получить такой ответ:

{"error": {"message": "Something super bad happened"}}


Или же, если API не очень хорошо сконфигурирован, вы можете получить не поддающуюся анализу трассировку стека или некий текст на английском. Отправляя запрос к API, как правило, имеет смысл сначала убедиться, что получаемый ответ действительно имеет формат JSON (или XML, или CSV, или любой другой формат, который вы ожидаете получить).

Об авторе

Райан Митчелл (Ryan Mitchell) старший инженер-программист в бостонской компании HedgeServ, в которой она разрабатывает API и инструменты для анализа данных. Райан окончила Инженерно-технический колледж им. Франклина В. Олина, имеет степень магистра в области разработки программного обеспечения и сертификат по анализу и обработке данных, полученный на курсах повышения квалификации при Гарвардском университете. До прихода в HedgeServ Райан трудилась в компании Abine, где разрабатывала веб-скраперы и средства автоматизации на Python. Регулярно выступает консультантом проектов по веб-скрапингу для розничной торговли, сферы финансов и фармацевтики. По совместительству работает консультантом и внештатным преподавателем в Северо-Восточном университете и Инженерно-техническом колледже им. Франклина В. Олина.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Python

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Телеграмм-бот на Python

12.04.2021 20:16:10 | Автор: admin

Недавно я попал на стажировку в новую для себя IT-компанию и наш (моей команды) проект был - бот для телеграмма, который автоматизирует часть работы hr-менеджеров. Первую неделю нам дали на самостоятельное изучение всего, что мы посчитаем нужным (а я убежден, что лучший способ что-то изучить - это практика), так что я начал действовать. Язык программирования был выбран python (наверное понятно из обложки почему), так что в этой статьи я разберу пример именно с ним.

BotFather

Чтобы создать телеграмм-бота, достаточно написать пользователю @BotFather команду /newbot. Он запросит название и @username для будущего бота. Тут ничего сложного - он все подсказывает (главное, чтобы @username был не занят и заканчивался на "bot"). BotFather пришлет HTTP API токен, который мы и будем использовать для работы с ботом.

Создание ботаСоздание бота

Telebot и сила python

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

Сила python заключается в его популярности. А, как следствие, в появлении огромного количества сторонних библиотек практически под все нужды. Именно это сделало возможным написание примитивного бота (который просто отвечает однотипно на сообщения) в 6 (ШЕСТЬ!) строчек кода. А именно:

import telebotbot = telebot.TeleBot('1111105161:AAHIjyAKY4fj62whM5vEAfotuixC5syA-j8')@bot.message_handler(commands=['start'])def start_command(message):    bot.send_message(message.chat.id, "Hello!")bot.polling()
Первое сообщениеПервое сообщение

На самом деле бот будет отвечать только на команду /start, но для начала неплохо. Здесь все довольно просто: в первой строчке импортируется библиотека telebot (для ее работы необходимо установить пакет pyTelegramBotAPI командой pip install pyTelegramBotAPI (НЕ pip install telebot!), далее создаем объекта бот, используя токен, который нам прислал BotFather. Третья строчка проверяет, что присылают пользователи (в данном случае это только команда /start), и, если проверка пройдена, то бот отправляет ответ с текстом Hello!. Последняя строчка, наверное, самая сложная для понимания, и в следующих разделах я ее подробно разберу. Сейчас же я только скажу о ее предназначении - она заставляет бота работать, то есть "реагировать" на полученные сообщения.

Flask & Requests

Telebot, конечно, круто, но есть одно важное НО. По предположению нашего проекта, у hr-ов должен быть сервис (сайт), где они будут работать и через него отправлять/получать информацию пользователям/от них. Соответственно, нам нужно самим контролировать сервер и обрабатывать запросы. На мой взгляд самый простой способ создания сервера на python - фреймворк flask. Так выглядит простейший сервер, запускаемый локально на 5000-ом порту (http://localhost:5000/):

from flask import Flask app = Flask(__name__)@app.route("/", methods=["GET"])def index():    return "Hello, World!"  if __name__ == "__main__":    app.run()

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

from flask import Flask, requestimport requestsapp = Flask(__name__)def send_message(chat_id, text):    method = "sendMessage"    token = "1111105161:AAHIjyAKY4fj62whM5vEAfotuixC5syA-j8"    url = f"https://api.telegram.org/bot{token}/{method}"    data = {"chat_id": chat_id, "text": text}    requests.post(url, data=data)@app.route("/", methods=["POST"])def receive_update():    chat_id = request.json["message"]["chat"]["id"]    send_message(chat_id, "Hello!")    return "ok"if __name__ == "__main__":    app.run()

К сожалению, в таком варианте программа работать не будет. Точнее будет, но не сразу. Проблема заключается в том, что телеграмм пока что не знает, куда посылать информацию о полученных сообщениях. Для ее решения у telegram API есть метод setWebhook. Суть метода заключается в том, что мы просто отправляем телеграмму url, по которому мы хотим получать информацию о новых обращениях к боту (в нашем случае это http://localhost:5000/). Однако, мы не можем просто сказать телеграмму: "Посылай запросы на localhost", ведь для каждого сервера localhost будет разным. Еще одна проблема заключается в том, что метод setWebhook поддерживает только https url-ы. Для решения этих проблем можно воспользоваться программой ngrok, которая строит туннель до локального хоста. Скачать ее можно по ссылке ngrok, а для запуска туннеля достаточно ввести команду ngrok http 5000. Должно получиться так:

ngrokngrok

Теперь можно задействовать метод setWebhook, например, через postman. Нужно отправить post запрос на https://api.telegram.org/bot<ТОКЕН>/setWebhook с указанием в теле нужного url. Должно получиться аналогично:

setWebhooksetWebhook

Соединение

Чем больше я работал с библиотекой telebot, тем больше она мне нравилась. Хотелось бы, используя приложение на flaske, не терять эту возможность. Но как это сделать? Во-первых, мы можем вместо нашей функции send_message использовать готовую из библиотеки. Это будет выглядеть так:

from flask import Flask, requestimport telebotapp = Flask(__name__) bot = telebot.TeleBot('1111105161:AAHIjyAKY4fj62whM5vEAfotuixC5syA-j8')@app.route("/", methods=["POST"])def receive_update():    chat_id = request.json["message"]["chat"]["id"]    bot.send_message(chat_id, "Hello!")    return "ok"if __name__ == "__main__":    app.run()

Но, если присмотреться, можно заметить, что мы потеряли часть функционала, а именно @bot.message_handler - декораторы, которые отслеживают тип введенного боту сообщения (картинка, документ, текст, команда и т. д.). Получается, что если мы используем в качестве сервера наше flask приложение, то мы теряем некоторый функционал библиотеки telebot. Если же мы используем bot.polling(), то мы не можем обращаться к серверу со стороны. Конечно, хотелось бы как-то все соединить без потерь. Для этого я нашел немного костыльный способ, однако рабочий:

from flask import Flask, requestimport telebotbot = telebot.TeleBot('1111105161:AAHIjyAKY4fj62whM5vEAfotuixC5syA-j8')bot.set_webhook(url="http://personeltest.ru/aways/8c6f687b75c9.ngrok.io")app = Flask(__name__)@app.route('/', methods=["POST"])def webhook():    bot.process_new_updates(        [telebot.types.Update.de_json(request.stream.read().decode("utf-8"))]    )    return "ok"@bot.message_handler(commands=['start'])def start_command(message):    bot.send_message(message.chat.id, 'Hello!')if __name__ == "__main__":    app.run()

Здесь мы пользуемся методом set_webhook, аналогично тому, как мы делали это ранее через postman, а на пустом роуте прописываем "немного магии", чтобы успешно получать обновления бота. Конечно, это не очень хороший способ, и в дальнейшем лучше самостоятельно прописывать функционал для обработки входящих сообщений. Но для начала, я считаю, это лучшее решение.

Заключение

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

Подробнее..
Категории: Python , Bot , Telegram , Flask

ModulationPy цифровые схемы модуляции на языке Python

14.04.2021 10:21:24 | Автор: admin

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

Сегодня хочу поделиться своим небольшим домашним проектом:

ModulationPy (GiHub)

- модуль для моделирования цифровых схем модуляции (это которые PSK, QAM и т.п.). Проект был вдохновлен другой питоновской библиотекой: CommPy; однако, в рассмотренном классе задач с ней удалось даже немного посоревноваться!

Сигнальное созвездие 16-QAM сгенерированное и отрисованное с помощью ModulationPyСигнальное созвездие 16-QAM сгенерированное и отрисованное с помощью ModulationPy

На данный момент доступны два класса схем модуляции:

  • M-PSK: Phase Shift Keying (фазовая цифровая модуляция)

  • M-QAM: Quadratured Amplitude Modulation (квадратурная амплитудная модуляция)

    где M - это порядок модуляции.

Интересен модуль может быть, скорее всего, в разрезе образовательных целей в сфере беспроводной связи (подбор модуляций исходил именно из нее), однако, вдруг кому-то пригодится и для научных изысканий. Не MatLab'ом насущным едины!

Примеры использования

Итак, для начала скачиваем библиотеку с PyPI:

$ pip install ModulationPy

Либо устанавливаем из исходников, но на PyPI на момент написания статьи все-таки актуальная версия.

Зависимости две:

  • numpy>=1.7.1

  • matplotlib>=2.2.2 (для построения сигнальных созвездий)

Продемонстрируем на деле.

Итак, например, мы хотим использовать QPSK с поворотом фазы pi/4, двоичным входом и наложением по Грею (Gray mapping). Импортируем и инициализируем:

import numpy as npfrom ModulationPy import PSKModemmodem = PSKModem(4, np.pi/4,                 gray_map=True,                 bin_input=True)

Проверяем то ли мы инициализировали, нарисовав сигнальное созвездие методом plot_const():

modem.plot_const()
Сигнальное созвездие QPSK с поворотом фазы pi/4, двоичным входом и наложением по Грею (Gray mapping)Сигнальное созвездие QPSK с поворотом фазы pi/4, двоичным входом и наложением по Грею (Gray mapping)

То же самое сделаем для 16-QAM (но для десятичных чисел на входе; указывать фазовый сдвиг не нужно - подразумевается наиболее распространенная прямоугольная QAM):

from ModulationPy import QAMModemmodem = QAMModem(16,                 gray_map=True,                  bin_input=False)modem.plot_const()
Сигнальное созвездие 16-QAM с десятичным входом и наложением по Грею (Gray mapping)Сигнальное созвездие 16-QAM с десятичным входом и наложением по Грею (Gray mapping)

На данный момент модуляция QAM реализована по примеру функции qammod в Octave [4]. И, да, реализованы только четные (в том смысле, что результат log2(M) - четное число) схемы модуляции (4-QAM, 16-QAM, 64-QAM). Пусть и не совсем полный набор, но как бы то ни было, в популярных стандартах беспроводной связи все равно нет "нечетных" схем модуляции (насколько я знаю).

Далее предлагаю перейти, собственно, к главному в модемах: к модуляции и демодуляции. Для этого нам понадобятся два метода:modulate() и demodulate() , доступные в обоих классах.

Метод modulate() принимает на вход всего один аргумент:

  • вектор входных значений (1-D ndarray of ints) - либо единиц и нулей, если выбрана опция bin_input=True , либо целых десятичных чисел от 0 до M-1, если bin_input=False ;

Методdemodulate() ожидает максимум два аргумента:

  • вектор, который должен быть демодулирован (1-D ndarray of complex symbols) ;

  • значение дисперсии аддитивного шума (float, по умолчанию 1.0).

Например, вот как это будет выглядеть для QPSK (двоичный вход/выход):

import numpy as npfrom ModulationPy import PSKModemmodem = PSKModem(4, np.pi/4,                  bin_input=True,                 soft_decision=False,                 bin_output=True)msg = np.array([0, 0, 0, 1, 1, 0, 1, 1]) # input messagemodulated = modem.modulate(msg) # modulationdemodulated = modem.demodulate(modulated) # demodulationprint("Modulated message:\n"+str(modulated))print("Demodulated message:\n"+str(demodulated)) >>>  Modulated message:   [0.70710678+0.70710678j  0.70710678-0.70710678j    -0.70710678+0.70710678j  -0.70710678-0.70710678j]>>> Demodulated message:   [0. 0. 0. 1. 1. 0. 1. 1.]

Или тоже QPSK, но уже с недвоичным входом / выходом:

import numpy as npfrom ModulationPy import PSKModemmodem = PSKModem(4, np.pi/4,                  bin_input=False,                 soft_decision=False,                 bin_output=False)msg = np.array([0, 1, 2, 3]) # input messagemodulated = modem.modulate(msg) # modulationdemodulated = modem.demodulate(modulated) # demodulationprint("Modulated message:\n"+str(modulated))print("Demodulated message:\n"+str(demodulated))>>> Modulated message:[ 0.70710678+0.70710678j -0.70710678+0.70710678j  0.70710678-0.70710678j -0.70710678-0.70710678j] >>> Demodulated message:[0, 1, 2, 3]

Пример для 16-QAM (десятичный вход / выход):

import numpy as npfrom ModulationPy import QAMModemmodem = PSKModem(16,                  bin_input=False,                 soft_decision=False,                 bin_output=False)msg = np.array([i for i in range(16)]) # input messagemodulated = modem.modulate(msg) # modulationdemodulated = modem.demodulate(modulated) # demodulationprint("Demodulated message:\n"+str(demodulated))>>> Demodulated message:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

В общем и целом, я думаю, понятно. Доступные опции старался делать по примеру доступных в рамках матлабовского Communication Toolbox. Подробное описание приведено в README.md проекта.

BER performance

Продемонстрируем адекватно ли модемы работают в случае присутствия шума (возьмем классический АБГШ, он же AWGN), используя простейшую модель приема-передачи:

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

Сверяться будем с теоретическими кривыми [5] (если кому интересно, все формулы описаны тоже в README.md).

Исходные коды для моделирования представлены по ссылкам: M-PSK, M-QAM.

Результаты:

Кривые битовых ошибок для AWGN (M-PSK).Кривые битовых ошибок для AWGN (M-PSK).Кривые битовых ошибок для AWGN (M-QAM).Кривые битовых ошибок для AWGN (M-QAM).

Да, с огрехами на малых значениях битовых ошибок (из-за относительно небольшого количества усреднений, я полагаю), однако.... it works!

Производительность

А теперь я хотел бы вернуться к вопросу "соревнования" с упомянутой выше CommPy.

Что нас отличает:

  • организация кода, стилистические различия (побочное, но не пренебрежимое);

  • я использовал более быстрый алгоритм демодуляции [6] (подробно описан в матлабовской документации [7], ну, и я добавил все в тот же README.md).

И вот, что получилось "забенчмаркать":

Результаты:

Метод (библиотека)

Среднее время исполнения (мс)

modulation (ModulationPy): QPSK

10.3

modulation (CommPy): QPSK

15.7

demodulation (ModulationPy): QPSK

0.4

demodulation (CommPy): QPSK

319

modulation (ModulationPy): 256-QAM

8.9

modulation (CommPy): 256-QAM

11.3

demodulation (ModulationPy): 256-QAM

42.6

demodulation (CommPy): 256-QAM

22 000

Разработчикам CommPy результаты, вроде, понравились (см. данный issue) - поэтому возможно в обозримом будущем что-то из моего ModulationPy будет перекочевывать в CommPy (я не против, главное, чтобы пользу приносило). Но это, как говорится, поживем - увидим.

И, да, пусть результаты производительности и не дотянули до MatLab (по крайней мере исходя из данного примера: см. вкладку "Examples"), я все равно считаю достигнутое неплохим стартом!

Послесловие

Наверное, проекту не хватает еще некоторых видов модуляции (тех же 32-QAM и 128-QAM или же используемой в DVB-S2/S2X APSK), однако, честно скажу, что не могу обещать их скорого добавления.

Проект всегда был для меня в большей мере площадкой для изучения языка Python и библиотеки NumPy на практике (и сопутствующих инструментов: юнит-тесты (не успел правда в данном случае перейти на pytest - каюсь), CI (использую Travis), подготовка модуля для PyPi и т.д.), однако, теперь, слава богу, всему этому есть приложение и в рамках рабочих задач!

Однако, все же буду рад вашим issue и pull request'ам! И если возьметесь интегрировать наработки в CommPy, тоже будет очень круто!

В общем, не серчайте, если вдруг не отвечу достаточно быстро, и да пребудет с вами сила науки!

Литература и ссылки

  1. Haykin S. Communication systems. John Wiley & Sons, 2008. p. 93

  2. Goldsmith A. Wireless communications. Cambridge university press, 2005. p. 88-92

  3. MathWorks: comm.PSKModulator (https://www.mathworks.com/help/comm/ref/comm.pskmodulator-system-object.html?s_tid=doc_ta)

  4. Octave: qammod (https://octave.sourceforge.io/communications/function/qammod.html)

  5. Link Budget Analysis: Digital Modulation, Part 3 (www.AtlantaRF.com)

  6. Viterbi, A. J. (1998). An intuitive justification and a simplified implementation of the MAP decoder for convolutional codes. IEEE Journal on Selected Areas in Communications, 16(2), 260-264.

  7. MathWorks: Approximate LLR Algorithm (https://www.mathworks.com/help/comm/ug/digital-modulation.html#brc6ymu)

Подробнее..

Перевод Оптимизируем затраты с помощью AWS Cost Explorer

15.04.2021 14:13:15 | Автор: admin

У Amazon Web Services отличный бесплатный пакет:хороший набор сервисов и щедрая раздача кредитов для разработчиков. Я был уверен: проблем с оплатой моего окружения не будет, поэтому о расходах не беспокоился. Мое приложение на 100% serverless, и я всегда укладывался в уровень бесплатного использования, так что просто игнорировал вопрос оплаты. В какой-то момент я расслабился и потерял бдительность.

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

AWS Cost Explorer

Сервис AWS Billing dashboard хорошо подходит для оплаты счетов и показывает график прогноза счетов за текущий месяц. Но этот сервис едва ли претендует на звание лучшего в AWS. Месячный прогноз часто врет, поэтому лучше игнорировать его вовсе.

Помимо Billing Dashboard, соседний Cost Explorer. Он предоставляет очень хорошую детализацию и возможность прогнозирования. Кроме просмотра стандартной разбивки потребления в AWS, можно писать код под Cost Explorer, извлекая много ценной информации. И мне это дело зашло.

Используя Cost Explorer, я смог заранее определить уязвимые места и исправить их задолго до того, как с меня начнут списывать за них деньги. Еще раз спасибо AWS.

Пользовательский интерфейс

Прежде чем начать работать, надо познакомиться со стандартным видом консоли Billing Dashboard. Нужно сначала включить её, что будет стоить денег. Лучше сделать это заранее, чтобы потом не было мучительно больно. У кого много остатку, тот не боится недостатку!

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

Это мой график потраченного за последние несколько месяцев.

Отчеты

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

Бюджеты

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

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

Обнаружение аномалий

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

Cost Explorer API

Стандартный вид консоли управления меня устраивает но только для эпизодического ознакомления. Для того, чтобы получить нечто большее, AWS предоставляет отличный API. Репозиторий AWS Samples Github дает нам наглядный пример доступа к API Cost Explorer.

Мой код основан на этом примере, и позволяет разработать собственный отчет для Cost Explorera.

Код Lambda функции

import osimport sys# Required to load modules from vendored subfolder (for clean development env)sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./vendored"))import boto3import datetimeimport loggingimport pandas as pd#For datefrom dateutil.relativedelta import relativedelta#For emailfrom email.mime.application import MIMEApplicationfrom email.mime.multipart import MIMEMultipartfrom email.mime.text import MIMETextfrom email.utils import COMMASPACE, formatdateSES_REGION="ap-south-1"CURRENT_MONTH = True#Default exclude support, as for Enterprise Support#as support billing is finalised later in month so skews trends    INC_SUPPORT = os.environ.get('INC_SUPPORT')if INC_SUPPORT == "true":    INC_SUPPORT = Trueelse:    INC_SUPPORT = FalseTAG_VALUE_FILTER = os.environ.get('TAG_VALUE_FILTER') or '*'TAG_KEY = os.environ.get('TAG_KEY')class CostExplorer:    """Retrieves BillingInfo checks from CostExplorer API    >>> costexplorer = CostExplorer()    >>> costexplorer.addReport(GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}])    >>> costexplorer.generateExcel()    """        def __init__(self, CurrentMonth=False):        #Array of reports ready to be output to Excel.        self.reports = []        self.client = boto3.client('ce', region_name='us-east-1')        # self.end = datetime.date.today().replace(day=1)        self.riend = datetime.date.today()        self.end = self.riend        # Default is last 12 months        self.start = (datetime.date.today() - relativedelta(months=+12)).replace(day=1) #1st day of month 12 months ago        self.ristart = (datetime.date.today() - relativedelta(months=+11)).replace(day=1) #1st day of month 11 months ago        self.sixmonth = (datetime.date.today() - relativedelta(months=+6)).replace(day=1) #1st day of month 6 months ago, so RI util has savings values        self.accounts = {}    def addRiReport(self, Name='RICoverage', Savings=False, PaymentOption='PARTIAL_UPFRONT', Service='Amazon Elastic Compute Cloud - Compute'): #Call with Savings True to get Utilization report in dollar savings        type = 'chart' #other option table        if Name == "RICoverage":            results = []            response = self.client.get_reservation_coverage(                TimePeriod={                    'Start': self.ristart.isoformat(),                    'End': self.riend.isoformat()                },                Granularity='MONTHLY'            )            results.extend(response['CoveragesByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_coverage(                    TimePeriod={                        'Start': self.ristart.isoformat(),                        'End': self.riend.isoformat()                    },                    Granularity='MONTHLY',                    NextPageToken=nextToken                )                results.extend(response['CoveragesByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            for v in results:                row = {'date':v['TimePeriod']['Start']}                row.update({'Coverage%':float(v['Total']['CoverageHours']['CoverageHoursPercentage'])})                rows.append(row)              df = pd.DataFrame(rows)            df.set_index("date", inplace= True)            df = df.fillna(0.0)            df = df.T        elif Name in ['RIUtilization','RIUtilizationSavings']:            #Only Six month to support savings            results = []            response = self.client.get_reservation_utilization(                TimePeriod={                    'Start': self.sixmonth.isoformat(),                    'End': self.riend.isoformat()                },                Granularity='MONTHLY'            )            results.extend(response['UtilizationsByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_utilization(                    TimePeriod={                        'Start': self.sixmonth.isoformat(),                        'End': self.riend.isoformat()                    },                    Granularity='MONTHLY',                    NextPageToken=nextToken                )                results.extend(response['UtilizationsByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            if results:                for v in results:                    row = {'date':v['TimePeriod']['Start']}                    if Savings:                        row.update({'Savings$':float(v['Total']['NetRISavings'])})                    else:                        row.update({'Utilization%':float(v['Total']['UtilizationPercentage'])})                    rows.append(row)                  df = pd.DataFrame(rows)                df.set_index("date", inplace= True)                df = df.fillna(0.0)                df = df.T                type = 'chart'            else:                df = pd.DataFrame(rows)                type = 'table' #Dont try chart empty result        elif Name == 'RIRecommendation':            results = []            response = self.client.get_reservation_purchase_recommendation(                #AccountId='string', May use for Linked view                LookbackPeriodInDays='SIXTY_DAYS',                TermInYears='ONE_YEAR',                PaymentOption=PaymentOption,                Service=Service            )            results.extend(response['Recommendations'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_purchase_recommendation(                    #AccountId='string', May use for Linked view                    LookbackPeriodInDays='SIXTY_DAYS',                    TermInYears='ONE_YEAR',                    PaymentOption=PaymentOption,                    Service=Service,                    NextPageToken=nextToken                )                results.extend(response['Recommendations'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            for i in results:                for v in i['RecommendationDetails']:                    row = v['InstanceDetails'][list(v['InstanceDetails'].keys())[0]]                    row['Recommended']=v['RecommendedNumberOfInstancesToPurchase']                    row['Minimum']=v['MinimumNumberOfInstancesUsedPerHour']                    row['Maximum']=v['MaximumNumberOfInstancesUsedPerHour']                    row['Savings']=v['EstimatedMonthlySavingsAmount']                    row['OnDemand']=v['EstimatedMonthlyOnDemandCost']                    row['BreakEvenIn']=v['EstimatedBreakEvenInMonths']                    row['UpfrontCost']=v['UpfrontCost']                    row['MonthlyCost']=v['RecurringStandardMonthlyCost']                    rows.append(row)              df = pd.DataFrame(rows)            df = df.fillna(0.0)            type = 'table' #Dont try chart this        self.reports.append({'Name':Name,'Data':df, 'Type':type})    def addReport(self, Name="Default",GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"},],     Style='Total', NoCredits=True, CreditsOnly=False, RefundOnly=False, UpfrontOnly=False, IncSupport=False):        type = 'chart' #other option table        results = []        if not NoCredits:            response = self.client.get_cost_and_usage(                TimePeriod={                    'Start': self.start.isoformat(),                    'End': self.end.isoformat()                },                Granularity='MONTHLY',                Metrics=[                    'UnblendedCost',                ],                GroupBy=GroupBy            )        else:            Filter = {"And": []}            Dimensions={"Not": {"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit", "Refund", "Upfront", "Support"]}}}            if INC_SUPPORT or IncSupport: #If global set for including support, we dont exclude it                Dimensions={"Not": {"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit", "Refund", "Upfront"]}}}            if CreditsOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit",]}}            if RefundOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Refund",]}}            if UpfrontOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Upfront",]}}            tagValues = None            if TAG_KEY:                tagValues = self.client.get_tags(                    SearchString=TAG_VALUE_FILTER,                    TimePeriod = {                        'Start': self.start.isoformat(),                        'End': datetime.date.today().isoformat()                    },                    TagKey=TAG_KEY                )            if tagValues:                Filter["And"].append(Dimensions)                if len(tagValues["Tags"]) > 0:                    Tags = {"Tags": {"Key": TAG_KEY, "Values": tagValues["Tags"]}}                    Filter["And"].append(Tags)            else:                Filter = Dimensions.copy()            response = self.client.get_cost_and_usage(                TimePeriod={                    'Start': self.start.isoformat(),                    'End': self.end.isoformat()                },                Granularity='MONTHLY',                Metrics=[                    'UnblendedCost',                ],                GroupBy=GroupBy,                Filter=Filter            )        if response:            results.extend(response['ResultsByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_cost_and_usage(                    TimePeriod={                        'Start': self.start.isoformat(),                        'End': self.end.isoformat()                    },                    Granularity='MONTHLY',                    Metrics=[                        'UnblendedCost',                    ],                    GroupBy=GroupBy,                    NextPageToken=nextToken                )                results.extend(response['ResultsByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False        rows = []        sort = ''        for v in results:            row = {'date':v['TimePeriod']['Start']}            sort = v['TimePeriod']['Start']            for i in v['Groups']:                key = i['Keys'][0]                if key in self.accounts:                    key = self.accounts[key][ACCOUNT_LABEL]                row.update({key:float(i['Metrics']['UnblendedCost']['Amount'])})             if not v['Groups']:                row.update({'Total':float(v['Total']['UnblendedCost']['Amount'])})            rows.append(row)          df = pd.DataFrame(rows)        df.set_index("date", inplace= True)        df = df.fillna(0.0)        if Style == 'Change':            dfc = df.copy()            lastindex = None            for index, row in df.iterrows():                if lastindex:                    for i in row.index:                        try:                            df.at[index,i] = dfc.at[index,i] - dfc.at[lastindex,i]                        except:                            logging.exception("Error")                            df.at[index,i] = 0                lastindex = index        df = df.T        df = df.sort_values(sort, ascending=False)        self.reports.append({'Name':Name,'Data':df, 'Type':type})    def generateExcel(self):        # Create a Pandas Excel writer using XlsxWriter as the engine.\        os.chdir('/tmp')        writer = pd.ExcelWriter('cost_explorer_report.xlsx', engine='xlsxwriter')        workbook = writer.book        for report in self.reports:            print(report['Name'],report['Type'])            report['Data'].to_excel(writer, sheet_name=report['Name'])            worksheet = writer.sheets[report['Name']]            if report['Type'] == 'chart':                # Create a chart object.                chart = workbook.add_chart({'type': 'column', 'subtype': 'stacked'})                chartend=13                for row_num in range(1, len(report['Data']) + 1):                    chart.add_series({                        'name':       [report['Name'], row_num, 0],                        'categories': [report['Name'], 0, 1, 0, chartend],                        'values':     [report['Name'], row_num, 1, row_num, chartend],                    })                chart.set_y_axis({'label_position': 'low'})                chart.set_x_axis({'label_position': 'low'})                worksheet.insert_chart('O2', chart, {'x_scale': 2.0, 'y_scale': 2.0})        writer.save()        #Time to deliver the file to S3        if os.environ.get('S3_BUCKET'):            s3 = boto3.client('s3')            s3.upload_file("cost_explorer_report.xlsx", os.environ.get('S3_BUCKET'), "cost_explorer_report.xlsx")        if os.environ.get('SES_SEND'):            #Email logic            msg = MIMEMultipart()            msg['From'] = os.environ.get('SES_FROM')            msg['To'] = COMMASPACE.join(os.environ.get('SES_SEND').split(","))            msg['Date'] = formatdate(localtime=True)            msg['Subject'] = "Cost Explorer Report"            text = "Find your Cost Explorer report attached\n\n"            msg.attach(MIMEText(text))            with open("cost_explorer_report.xlsx", "rb") as fil:                part = MIMEApplication(                    fil.read(),                    Name="cost_explorer_report.xlsx"                )            part['Content-Disposition'] = 'attachment; filename="%s"' % "cost_explorer_report.xlsx"            msg.attach(part)            #SES Sending            ses = boto3.client('ses', region_name=SES_REGION)            result = ses.send_raw_email(                Source=msg['From'],                Destinations=os.environ.get('SES_SEND').split(","),                RawMessage={'Data': msg.as_string()}            )     def lambda_handler(event, context):    costexplorer = CostExplorer(CurrentMonth=False)    #Default addReport has filter to remove Support / Credits / Refunds / UpfrontRI    #Overall Billing Reports    costexplorer.addReport(Name="Total", GroupBy=[],Style='Total',IncSupport=True)    costexplorer.addReport(Name="TotalChange", GroupBy=[],Style='Change')    costexplorer.addReport(Name="TotalInclCredits", GroupBy=[],Style='Total',NoCredits=False,IncSupport=True)    costexplorer.addReport(Name="TotalInclCreditsChange", GroupBy=[],Style='Change',NoCredits=False)    costexplorer.addReport(Name="Credits", GroupBy=[],Style='Total',CreditsOnly=True)    costexplorer.addReport(Name="Refunds", GroupBy=[],Style='Total',RefundOnly=True)    costexplorer.addReport(Name="RIUpfront", GroupBy=[],Style='Total',UpfrontOnly=True)    #GroupBy Reports    costexplorer.addReport(Name="Services", GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}],Style='Total',IncSupport=True)    costexplorer.addReport(Name="ServicesChange", GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}],Style='Change')    costexplorer.addReport(Name="Accounts", GroupBy=[{"Type": "DIMENSION","Key": "LINKED_ACCOUNT"}],Style='Total')    costexplorer.addReport(Name="AccountsChange", GroupBy=[{"Type": "DIMENSION","Key": "LINKED_ACCOUNT"}],Style='Change')    costexplorer.addReport(Name="Regions", GroupBy=[{"Type": "DIMENSION","Key": "REGION"}],Style='Total')    costexplorer.addReport(Name="RegionsChange", GroupBy=[{"Type": "DIMENSION","Key": "REGION"}],Style='Change')    if os.environ.get('COST_TAGS'): #Support for multiple/different Cost Allocation tags        for tagkey in os.environ.get('COST_TAGS').split(','):            tabname = tagkey.replace(":",".") #Remove special chars from Excel tabname            costexplorer.addReport(Name="{}".format(tabname)[:31], GroupBy=[{"Type": "TAG","Key": tagkey}],Style='Total')            costexplorer.addReport(Name="Change-{}".format(tabname)[:31], GroupBy=[{"Type": "TAG","Key": tagkey}],Style='Change')    #RI Reports    costexplorer.addRiReport(Name="RICoverage")    costexplorer.addRiReport(Name="RIUtilization")    costexplorer.addRiReport(Name="RIUtilizationSavings", Savings=True)    costexplorer.addRiReport(Name="RIRecommendation") #Service supported value(s): Amazon Elastic Compute Cloud - Compute, Amazon Relational Database Service    costexplorer.generateExcel()    return "Report Generated"

IAM Role

Чтобы запускаться, Lambda функция должна обладать ролью с приведенными ниже правами:

Базовая политика Lambda

{    "Version": "2012-10-17",    "Statement": [        {            "Effect": "Allow",            "Action": [                "logs:CreateLogGroup",                "logs:CreateLogStream",                "logs:PutLogEvents"            ],            "Resource": "*"        }    ]}

Разрешение для записи отчетов в S3 бакет

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": [                "s3:PutObject",                "s3:GetObject"            ],            "Resource": "arn:aws:s3:::account.admin/*"        }    ]}

Simple Email Service

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": [                "ses:SendEmail",                "ses:SendRawEmail"            ],            "Resource": "*"        }    ]}

Cost Explorer

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": "ce:*",            "Resource": "*"        }    ]}

Запуск на Event Bridge

Наконец, мы настраиваем регулярный запуск нашей Lambda функции на Event Bridge, например, 5 числа каждого месяца. В результате работы всех настроек я буду получать email с прикрепленным XLS-отчетом. Также можно настраивать срабатывание еженедельно и даже на определенные дни недели, при необходимости.

Подробнее..

Подключаем Sqlite3 к Telegram боту

16.04.2021 16:20:16 | Автор: admin

Вступление

Для многих новичков в разработке на Python возникает проблема как подключить базу данных? Я сам столкнулся с такой проблемой в начале разработки. Тема оказалось довольно простой, но в интернете есть множество гайдов, которые могут запутать. В этом туториале я расскажу о том, как просто интегрировать базу данных Sqlite3 на примере Telegram бота.

Начало работы

Для работы нам понадобятся базовые знания в Python, достаточные для написания простого бота. Данный гайд можно использовать не только для интеграции с ботом, но и для других проектов. Если вы не используете PyCharm, и у вас нет средства для работы с БД, то необходимо установить утилиту SQLiteStudio с официального сайта.

Теперь давайте перейдем к написанию кода. В терминале необходимо установить pyTelegramBotAPI (если по какой-то причине у вас его нет).

pip install pyTelegramBotAPI

Далее создаем новый файл с расширением .py. В нем прописываем следующее:

import sqlite3import telebotbot = telebot.TeleBot("token")

Устанавливать sqlite3 не нужно это стандартная библиотека. Вместо "token" необходимо указать токен бота, который выдал вам BotFather. Теперь проверим, работает ли бот.

@bot.message_handler(commands=['start'])def start_message(message):bot.send_message(message.chat.id, 'Добро пожаловать')

Запустим код. Если при запуске бота он пишет нам "Добро пожаловать", то все работает отлично.

Как мы видим, все работает.

Настройка базы данных

Открываем SQLiteStudio. Во вкладке Database выбираем пункт Add a database. Для удобства можно использовать сочетание Ctrl+O.

Далее нажимаем на зеленую кнопку Создать новый фал базы данных. Выбираем директорию вашего проекта. В ней можете создать отдельную папку db, где будет хранится файл с базой. Вводим название, например database. Расширение будет присвоено автоматически. Нажимаем Сохранить. Теперь слева в списке у нас появился наш файл. Кликаем по нему ПКМ и выбираем пункт Connect to the database. Соединение с базой установлено. Появилось два подпункта: Таблицы и Представления. В этом уроке мы будем затрагивать только пункт Таблицы. Нажимаем по нему ПКМ и выбираем Add a table.

В поле для ввода указываем имя для таблицы. Я назову ее test. Чуть выше есть активная кнопка Добавить столбец (Ins). В появившемся окне указываем название столбца и тип данных. Для начала вам могут понадобится такие условия, как Первичный ключ, Не NULLи Уникальность. Первый столбец я назову id, выберу тип данных INTEGER и установлю все три условия. У Первичный ключ выберу настройку Автоинкремент. Этот столбец будет автоматически создавать ID записи в таблице. Работать с ним в коде мы не будем.

Второй столбец будет хранить user_id. Он будет иметь тип данных INT, уникален и не равен нулю. Третий столбец я назову user_name, присвою тип данных STRING и выберу пункт Не NULL. Еще 2 столбца будут называться user_surname и username. После того, как вы создали столбцы, нажимаем зеленую кнопку с галочкой.

В общем все будет выглядеть вот так.

Работа с базой данных в коде

Теперь давайте вновь перейдем к коду. Создадим 2 переменные.

conn = sqlite3.connect('db/database.db', check_same_thread=False)cursor = conn.cursor()

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

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

def db_table_val(user_id: int, user_name: str, user_surname: str, username: str):cursor.execute('INSERT INTO test (user_id, user_name, user_surname, username) VALUES (?, ?, ?, ?)', (user_id, user_name, user_surname, username))conn.commit()

Итак, сейчас объясню, что тут происходит. В аргументах функции мы указываем переменную user_id с типом данных inst. Далее добавляем запись в таблицу test в столбец user_id. С остальными значениями точно так же. В конце мы просто применяем изменения. Эта функция не будет выполнятся, пока мы ее не вызовем. Давайте исправим это. Создадим обработчик сообщений от пользователя и будем вносить в базу его данные.

@bot.message_handler(content_types=['text'])def get_text_messages(message):if message.text.lower() == 'привет':bot.send_message(message.from_user.id, 'Привет! Ваше имя добавленно в базу данных!')        us_id = message.from_user.idus_name = message.from_user.first_nameus_sname = message.from_user.last_nameusername = message.from_user.usernamedb_table_val(user_id=us_id, user_name=us_name, user_surname=us_sname, username=username)

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

bot.polling(none_stop=True)

Пришло время проверить, как работает код. Запускаем бота и пишем ему "Привет".

Теперь можем зайти в SQLiteStudio, и выбрав вкладку Данные посмотреть что получилось:

Как мы видим, данные уже в базе. Если их по какой-то причине нет, то обновите базу, нажав на синюю кнопку Обновить

Вот весь код:

import sqlite3import telebotbot = telebot.TeleBot("token")conn = sqlite3.connect('db/database.db', check_same_thread=False)cursor = conn.cursor()def db_table_val(user_id: int, user_name: str, user_surname: str, username: str):cursor.execute('INSERT INTO test (user_id, user_name, user_surname, username) VALUES (?, ?, ?, ?)', (user_id, user_name, user_surname, username))conn.commit()@bot.message_handler(commands=['start'])def start_message(message):bot.send_message(message.chat.id, 'Добро пожаловать')@bot.message_handler(content_types=['text'])def get_text_messages(message):if message.text.lower() == 'привет':bot.send_message(message.chat.id, 'Привет! Ваше имя добавлено в базу данных!')us_id = message.from_user.idus_name = message.from_user.first_nameus_sname = message.from_user.last_nameusername = message.from_user.usernamedb_table_val(user_id=us_id, user_name=us_name, user_surname=us_sname, username=username)bot.polling(none_stop=True)

Заключение

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

Огромное спасибо за то, что прочитали. Надеюсь, кому-то помог. Всем удачи и поменьше багов в коде!

Подробнее..
Категории: Python , Api , Sqlite , Telegram

Категории

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

© 2006-2021, personeltest.ru