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

Парсинг сайта

Как проанализировать рынок фотостудий с помощью Python (23). База данных

31.07.2020 10:05:30 | Автор: admin
В предыдущей статье в рамках коммерческого проекта по анализу рынка фотостудий рассмотрел создание парсинга: выгрузка списка фотостудий, списка залов, данных по бронированию с момента открытия зала до последней брони.

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

В статье рассмотрю:
  • создание простой SQLite базы данных;
  • запись информации с помощь Python;
  • чтение данных и перевод в формат DataFrame;
  • обновление парсинга с учетом данных БД.




Требования к базе данных


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

Нашей БД не требуется:
  • разграничивать доступ к схемам, т.к. доступ будет только у пользователя парсингом;
  • сохранять доступ 24/7, т.к. извлечение данных допустимо по мере необходимости проведения анализа;
  • создание процедур, т.к. все вычисления будут проводится в python.

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

Особенности работы с SQLite через python


Для работы с SQLite через python используем библиотеку sqlite3.
Подключаемся к базе данных простой командой:
sqlite3.connect(путь к файлу)

Если файл отсутствует, будет создана новая база.

Запросы к базе выполняются следующим образом:
conn = sqlite3.connect(путь к файлу)cur = conn.cursor()cur.execute(запрос)df = cur.fetchall()

cur.fetchall() выполняется в том случае, когда в результате запроса мы хотим получить данные из БД.

В конце записи данных в БД не забывайте заканчивать транзакцию:
conn.commit()

а в конце работы с базой не забывайте её закрывать:
conn.close()

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

Создание таблиц стандартное:
CREATE TABLE t1 (поле1 тип, поле2 тип...)

или более универсальный вариант, создающий таблицу в случае её отсутствия:
CREATE TABLE IF NOT EXISTS t1 (поле1 тип, поле2 тип...)

Записываем данные в таблицу, избегая повторов:
INSERT OR IGNORE INTO t1 (поле1, поле2, ...) VALUES(значение1, значение2, ...)

Обновляем данные:
UPDATE t1 SET поле1 = значение1 WHERE поле2 = значение2

Для более удобной работы с SQLite можно использовать SQLite Manager или DB Browser for SQLite.

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


Вторая программа полноценное desktop-приложение:




Структура базы данных


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

Создаем таблицы:
def create_tables(conn, table = 'all'):    cur = conn.cursor()        if (table == 'all') or (table == 'uStudios'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uStudios            (studio_id INT PRIMARY KEY UNIQUE,            name TEXT UNIQUE,            metro TEXT,            address TEXT,            phone TEXT,            email TEXT,            established_date DATE)            ''')        print('Table uStudios is created.')    if (table == 'all') or (table == 'uHalls'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uHalls            (hall_id INT PRIMARY KEY UNIQUE,            studio_id INT,            name TEXT,            is_hall INT,            square FLOAT,            ceiling FLOAT,            open_date DATE)            ''')        print('Table uHalls is created.')    if (table == 'all') or (table == 'uBooking_parsing'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uBooking_parsing            (hall_id INT,            date DATE,            hour INT,            is_working_hour INT,            min_hours INT,            price INTEGER,            is_booked INT,            duration INT,            parsing_date DATE)            ''')        print ('Table uBooking_parsing is created.')    if (table == 'all') or (table == 'uBooking'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uBooking            (hall_id INT,            date DATE,            hour INT,            is_working_hour INT,            min_hours INT,            price INTEGER,            is_booked INT,            duration INT,            parsing_date DATE)            ''')        print ('Table uBooking is created.')


Параметром table задаю название таблицы, которую необходимо создать. По умолчанию создает все.

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

Взаимодействие с базой данных


Создадим 6 процедур для взаимодействия с базой данных:
  1. Запись списка фотостудий в базу данных;
  2. Выгрузка списка фотостудий из базы данных;
  3. Запись списка залов;
  4. Выгрузка списка залов;
  5. Выгрузка данных по бронированию;
  6. Запись данных по бронированию.


1. Запись списка фотостудий в базу данных


На входе в процедуру передаем параметры соединения с БД и таблицу в виде DataFrame. Записываем данные построчно, перебирая все строчки циклом. Полезным для этой операции свойством строковых данных в python является замена символов "?" элементами кортежа, указанным после.
Процедура записи списка фотостудий выглядит следующим образом:
def studios_to_db(conn, studio_list):     cur = conn.cursor()    for i in studio_list.index:        cur.execute('INSERT OR IGNORE INTO uStudios (studio_id, name, metro, address, phone, email) VALUES(?, ?, ?, ?, ?, ?)',                   (i,                   studio_list.loc[i, 'name'],                   studio_list.loc[i, 'metro'],                   studio_list.loc[i, 'address'],                   studio_list.loc[i, 'phone'],                   studio_list.loc[i, 'email']))



2. Выгрузка списка фотостудий из базы данных


На вход в процедуру передаем параметры соединения с БД. Выполняем select-запрос, перехватываем выгружаемые данные и записываем в DataFrame. Переводим дату основания фотостудии в формат даты.
Полностью процедура выглядит следующим образом:
def db_to_studios(conn):    cur = conn.cursor()    cur.execute('SELECT * FROM uStudios')    studios = pd.DataFrame(cur.fetchall()                           , columns=['studio_id', 'name', 'metro', 'address', 'phone', 'email', 'established_date']                          ).set_index('studio_id')    studios['established_date'] = pd.to_datetime(studios['established_date'])    return studios



3. Запись списка залов в базу данных


Процедура аналогична записи списка фотостудий: передаем параметры подключения и таблицу залов, построчно записываем данные в базу.
Процедура записи списка залов в БД
def halls_to_db(conn, halls):     cur = conn.cursor()    for i in halls.index:        cur.execute('INSERT OR IGNORE INTO uHalls (hall_id, studio_id, name, is_hall, square, ceiling) VALUES(?, ?, ?, ?, ?, ?)',                   (i,                   halls.loc[i, 'studio_id'],                   halls.loc[i, 'name'],                   halls.loc[i, 'is_hall'],                   halls.loc[i, 'square'],                   halls.loc[i, 'ceiling']))



4. Выгрузка списка залов из базы данных


Процедура аналогична выгрузки списка фотостудий: передача параметров подключения, select-запрос, перехват, запись в DataFrame, перевод даты открытия зала в формат даты.
Единственное отличие: id студии и признак зала записались в байтовом виде. Возвращаем значение функцией:
int.from_bytes(число, 'little')

Процедура выгрузки списка залов выглядит следующим образом:
def db_to_halls(conn):    cur = conn.cursor()    cur.execute('SELECT * FROM uHalls')    halls = pd.DataFrame(cur.fetchall(), columns=['hall_id', 'studio_id', 'name', 'is_hall', 'square', 'ceiling', 'open_date']).set_index('hall_id')    for i in halls.index:        halls.loc[i, 'studio_id'] = int.from_bytes(halls.loc[i, 'studio_id'], 'little')        halls.loc[i, 'is_hall'] = int.from_bytes(halls.loc[i, 'is_hall'], 'little')    halls['open_date'] = pd.to_datetime(halls['open_date'])    return halls



5. Выгрузка информации по бронированию из базы данных


В процедуру передаем параметры подключения к БД и параметр parsing, показывающий из какой таблицы по бронированию запрашиваем информацию: 0 из актуальной (по умолчанию), 1 из таблицы парсинга. Далее выполняем select-запрос, перехватываем его, переводим в DataFrame. Даты переводим в формат дат, числа из байтового формата в формат чисел.
Процедура выгрузки информации по бронированию:
def db_to_booking(conn, parsing = 0):    cur = conn.cursor()    if parsing == 1:        cur.execute('SELECT * FROM uBooking_parsing')    else:        cur.execute('SELECT * FROM uBooking')    booking = pd.DataFrame(cur.fetchall(), columns=['hall_id',                                                      'date', 'hour',                                                      'is_working_hour',                                                      'min_hours',                                                      'price',                                                      'is_booked',                                                      'duration',                                                      'parsing_date'])    booking['hall_id'] = [int.from_bytes(x, 'little') if not isinstance(x, int) else x for x in booking['hall_id']]    booking['is_booked'] = [int.from_bytes(x, 'little') if not isinstance(x, int) else x for x in booking['is_booked']]    booking['date'] = pd.DataFrame(booking['date'])    booking['parsing_date'] = pd.DataFrame(booking['parsing_date'])        return booking



6. Запись информации по бронированию в базу данных


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

Чтобы определить последнюю дату актуальных данных,
запросим из базы последнюю дату парсинга по каждому id зала:
parsing_date = db_to_booking(conn, parsing = 1).groupby('hall_id').agg(np.max)['parsing_date']


Перебираем каждый id зала с помощью цикла.

В каждом id зала первом делом определяем
количество недель, которые необходимо парсить в прошлом:
        try:            last_day_str = parsing_date[id]            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            delta_days = (datetime.datetime.now() - last_day).days            weeks_ago = delta_days // 7        except:            last_day_str = '2010-01-01'            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            weeks_ago = 500


Если id зала есть в базе, то вычисляем. Если нет, то парсим 500 недель в прошлое или останавливаемся, когда 2 месяца не было броней (ограничение описано в предыдущей статье).

Дальше выполняем процедуры парсинга:
        d = get_past_booking(id, weeks_ago = weeks_ago)                d.update(get_future_booking(id))        book = hall_booking(d)


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

Завершающим этапом записываем данные по бронированию зала в базу и закрываем транзакцию.
Процедура записи информации по бронированию в базу данных выглядит следующим образом:
def booking_to_db(conn, halls_id):    cur = conn.cursor()    cur_date = pd.Timestamp(datetime.date.today())    parsing_date = db_to_booking(conn, parsing = 1).groupby('hall_id').agg(np.max)['parsing_date']        for id in halls_id:                #download last parsing_date from DataBase        try:            last_day_str = parsing_date[id]            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            delta_days = (datetime.datetime.now() - last_day).days            weeks_ago = delta_days // 7        except:            last_day_str = '2010-01-01'            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            weeks_ago = 500                d = get_past_booking(id, weeks_ago = weeks_ago)                d.update(get_future_booking(id))        book = hall_booking(d)        for i in list(range(len(book))):#book.index:            cur.execute('INSERT OR IGNORE INTO uBooking_parsing (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date) VALUES(?,?,?,?,?,?,?,?,?)',                       (book.iloc[i]['hall_id'],                       book.iloc[i]['date'].date().isoformat(),                       book.iloc[i]['hour'],                       book.iloc[i]['is_working_hour'],                       book.iloc[i]['min_hours'],                       book.iloc[i]['price'],                       book.iloc[i]['is_booked'],                       book.iloc[i]['duration'],                       cur_date.date().isoformat()))        conn.commit()        print('hall_id ' + str(id) + ' added. ' + str(list(halls_id).index(id) + 1) + ' from ' + str(len(halls_id)))



Обновление дней открытия студии и залов


Дата открытия зала это самая ранняя дата бронирования зала.
Дата открытия фотостудии это самая ранняя дата открытия зала этой студии.

Исходя из этой логики,
выгружаем самые ранние даты бронирования каждого зала из базы
halls = db_to_booking(conn).groupby('hall_id').agg(min)['date']


Затем построчно обновляем данные открытия залов:
    for i in list(range(len(halls))):        cur.execute('''UPDATE uHalls SET open_date = '{1}' WHERE hall_id = {0}'''                    .format(halls.index[i], str(halls.iloc[i])))


Данные открытия фотостудий обновляем аналогично: выгружаем данные по датам открытия залов из базы данных, вычисляем наименьшую дату для каждой студии, переписываем дату открытия фотостудии.
Процедура обновления дат открытия:
def update_open_dates(conn):        cur = conn.cursor()        #update open date in uHalls    halls = db_to_booking(conn).groupby('hall_id').agg(min)['date']        for i in list(range(len(halls))):        cur.execute('''UPDATE uHalls SET open_date = '{1}' WHERE hall_id = {0}'''                    .format(halls.index[i], str(halls.iloc[i])))    #update open date in uStudios    studios = db_to_halls(conn)    studios['open_date'] = pd.to_datetime(studios['open_date'])    studios = studios.groupby('studio_id').agg(min)['open_date']    for i in list(range(len(studios))):        cur.execute('''UPDATE uStudios SET established_date = '{1}' WHERE  studio_id = {0}'''                    .format(studios.index[i], str(studios.iloc[i])))        conn.commit()



Обновление парсинга


Все процедуры в этой и предыдущей статьях мы объединим в данной процедуре. Её можно запускать как при первом парсинге, так и при обновлении данных.
Процедура выглядит следующим образом:
def update_parsing(directory = './/', is_manual = 0):        start_time = time.time()        #is DataBase exists?    if not os.path.exists(directory + 'photostudios_moscow1.sqlite'):        if is_manual == 1:            print('Data base is not exists. Do you want to create DataBase (y/n)? ')            answer = input().lower()        else:             answer == 'y'                if answer == 'y':            conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')            conn.close()            print('DataBase is created')        elif answer != 'n':            print('Error in input!')            return list()        print('DataBase is exists')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()            #connect to DataBase    conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')    cur = conn.cursor()           #has DataBase 4 tables?    tables = [x[0] for x in list(cur.execute('SELECT name FROM sqlite_master WHERE type="table"'))]    if not ('uStudios' in tables) & ('uHalls' in tables) & ('uBooking_parsing' in tables) & ('uBooking' in tables):        if is_manual == 1:            print('Do you want to create missing tables (y/n)? ')            answer = input().lower()        else:            answer = 'y'                if anwer == 'y':            if not ('uStudios' in tables):                create_tables(conn, table = 'uStudios')            if not ('uHalls' in tables):                create_tables(conn, table = 'uHalls')            if not ('uBooking_parsing' in tables):                create_tables(conn, table = 'uBooking_parsing')            if not ('uBooking' in tables):                create_tables(conn, table = 'uBooking')        elif answer != 'n':            print('Error in input!')            return list()    conn.commit()    print(str(tables) + ' are exist in DataBase')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uStudios    studios = studio_list()    new_studios = studios[[x not in list(db_to_studios(conn).index) for x in list(studios.index)]]    if len(new_studios) > 0:        print(str(len(new_studios)) + ' new studios detected: \n' + str(list(new_studios['name'])))        studios_to_db(conn, new_studios)        conn.commit()    print('Studio list update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uHalls    halls = hall_list(list(studios.index)).sort_index()    new_halls = halls[[x not in list(db_to_halls(conn).index) for x in list(halls.index)]]    if len(new_halls) > 0:        halls_to_db(conn, new_halls)        conn.commit()    print('Halls list update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()            #update uBooking_parsing    booking_to_db(conn, halls.index)           conn.commit()    print('Booking_parsing update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uBooking from uBooking_parsing    cur.execute('DELETE FROM uBooking')    cur.execute('''        insert into uBooking (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date)         select hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date        from        (            select *, row_number() over(partition by hall_id, date, hour order by parsing_date desc) rn             from uBooking_parsing        ) t        where rn = 1    ''')    conn.commit()    print('Booking update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        update_open_dates(conn)    conn.commit()    print('Open date update was successful')    print("--- %s seconds ---" % (time.time() - start_time))        conn.close()


Разберем ее работу по порядку.

На входе в процедуру передаем 2 параметра: адрес папки, откуда брать базу данных или куда ее установить (по умолчанию берем папку с python-документов), и необязательный параметр is_manual, который при значении 1 будет запрашивать необходимость создания базы данных или таблиц в случае их отсутствия.

Вначале проверяем есть ли база данных. Если нет, создаём:
    if not os.path.exists(directory + 'photostudios_moscow1.sqlite'):        if is_manual == 1:            print('Data base is not exists. Do you want to create DataBase (y/n)? ')            answer = input().lower()        else:             answer == 'y'                if answer == 'y':            conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')            conn.close()            print('DataBase is created')        elif answer != 'n':            print('Error in input!')            return list()


Присоединяемся к БД и сохраняем функцию курсок отдельной переменной:
    conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')    cur = conn.cursor() 


Следующим действием проверяем, все ли таблицы созданы. Если нет, создаем недостающие. В конце изменений завершаем транзакцию:
    tables = [x[0] for x in list(cur.execute('SELECT name FROM sqlite_master WHERE type="table"'))]    if not ('uStudios' in tables) & ('uHalls' in tables) & ('uBooking_parsing' in tables) & ('uBooking' in tables):        if is_manual == 1:            print('Do you want to create missing tables (y/n)? ')            answer = input().lower()        else:            answer = 'y'                if anwer == 'y':            if not ('uStudios' in tables):                create_tables(conn, table = 'uStudios')            if not ('uHalls' in tables):                create_tables(conn, table = 'uHalls')            if not ('uBooking_parsing' in tables):                create_tables(conn, table = 'uBooking_parsing')            if not ('uBooking' in tables):                create_tables(conn, table = 'uBooking')        elif answer != 'n':            print('Error in input!')            return list()    conn.commit()



Обновляем список фотостудий. Сравниваем с данными БД и выводим количество и список новых фотостудий:
    studios = studio_list()    new_studios = studios[[x not in list(db_to_studios(conn).index) for x in list(studios.index)]]    if len(new_studios) > 0:        print(str(len(new_studios)) + ' new studios detected: \n' + str(list(new_studios['name'])))        studios_to_db(conn, new_studios)


conn.commit()


Обновляем список залов и выводим название новых:
    halls = hall_list(list(studios.index)).sort_index()    new_halls = halls[[x not in list(db_to_halls(conn).index) for x in list(halls.index)]]    if len(new_halls) > 0:        halls_to_db(conn, new_halls)        conn.commit()



Обновляем информацию по бронированию в таблице uBooking_parsing. Скрипт получился простым, т.к. всю сложную работу мы сделали в самой процедуре booking_to_db
    booking_to_db(conn, halls.index)           conn.commit()



Обновляем актуальную информацию по бронированию в таблице uBooking. Для этого удаляем старую версию uBooking и записываем данные из таблицы uBooking_parsing с последними (для каждого зала, даты и часа брони) датами парсинга:
    cur.execute('DELETE FROM uBooking')    cur.execute('''        insert into uBooking (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date)         select hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date        from        (            select *, row_number() over(partition by hall_id, date, hour order by parsing_date desc) rn             from uBooking_parsing        ) t        where rn = 1    ''')    conn.commit()


Обновляем даты открытия студий и залов:
    update_open_dates(conn)    conn.commit()


И закрываем базу
    conn.close()



Парсинг с сохранением данных в БД настроен успешно!

Инициируем парсинг/обновление следующей процедурой:
update_parsing()



Итог


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

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

Готовый проект вы можете найти на моей странице в github.
Подробнее..

Как проанализировать рынок фотостудий с помощью Python (33). Аналитика

05.08.2020 10:11:24 | Автор: admin
Каждый, кто открывает свой бизнес, хочет угадать идеальный момент открытия, найти идеальное место и выполнить точные, эффективные действия для того, чтобы бизнес выжил и приумножился. Найти идеальные параметры невозможно, но оценить наилучшие возможности помогают инструменты статистического анализа.

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

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

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




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

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

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

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

Какие направления для анализа мы будем использовать


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


Выгрузка данных из БД


Для выгрузки выполняем следующие действия:
устанавливаем соединение с базой
directory = './/'conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')cur = conn.cursor() 


выгружаем данные по студиям
studios = db_to_studios(conn)studios


по залам
halls = db_to_halls(conn)halls


по бронированию
booking = db_to_booking(conn)booking


оставляем студии с датами открытия и исключаем гримерки из списка залов
studios = studios[[x.year > 0 for x in studios['established_date']]]halls = halls[halls['is_hall'] == 1]



Динамика открытия фотостудий по годам


Построим частотную гистограмму открытия фотостудий по разным годам. Для этого вычисляем количество периодов (лет) и строим гистограмму.

построение гистограммы
num_bins = np.max(studios['established_date']).year - np.min(studios['established_date']).year + 1plt.hist([x.year for x in studios['established_date']], num_bins)plt.show()



На гистограмме видим явный рост новых фотостудий из года в год. Эта закономерность говорит нам не о фактическом росте рынка в 2 раза ежегодно, а, скорее, о росте самого агрегатора.

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

Выявление новых фотостудий


Какую фотостудию можно считать новой? Ту, которая только-только раскручивается и набирает клиентов. По визуальному анализу календарей бронирования от момента открытия видно, что стабильный поток клиентов студия набирает за несколько месяцев.

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

Вначале объединим все таблицы и оставим только забронированные часы
# merge all tablesdata = (booking         .merge(halls, left_on = 'hall_id', right_on = 'hall_id', how = 'inner')         .merge(studios, left_on ='studio_id', right_on = 'studio_id', how = 'inner')        )data = data[data['is_working_hour'] == 1]data['date'] = pd.to_datetime(data['date'])data



Затем вычислим доход в первые полмесяца работы фотостудии
first_month = (data[data['date'] <= [x + datetime.timedelta(days = 15) for x in data['established_date']]]               .loc[:, ['studio_id', 'price', 'duration']]              )first_month['income'] = first_month['price'] * first_month['duration']first_month = first_month.groupby('studio_id').agg(np.sum)first_month



В полмесяца спустя год
month_after_year = (data[(data['date'] >= [x + datetime.timedelta(days = 365) for x in data['established_date']])                         & (data['date'] <= [x + datetime.timedelta(days = 365 + 15) for x in data['established_date']])                        ]                    .loc[:, ['studio_id', 'price', 'duration']]                   )month_after_year['income'] = month_after_year['price'] * month_after_year['duration']month_after_year = month_after_year.groupby('studio_id').agg(np.sum)month_after_year



Показатели через год разделим на аналогичные при открытии
month_diff = (month_after_year.merge(first_month, left_on = 'studio_id', right_on = 'studio_id', how = 'inner')              .merge(halls.groupby('studio_id').count()                     , left_on = 'studio_id', right_on = 'studio_id', how = 'inner')             )[['income_x', 'income_y', 'is_hall']]month_diff['income_diff'] = (month_diff['income_x'] / month_diff['income_y']) ** (1 / month_diff['is_hall'])month_diff.sort_values('income_diff')


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

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

В какой месяц лучше открывать фотостудию?


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

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

Расчет среднего дохода за год в зависимости от месяца открытия
new = studios['is_new'].reset_index().merge(data, left_on = 'studio_id', right_on = 'studio_id', how = 'inner')new = new[new['is_new'] == 1]new = new[new['date'] <= [x + datetime.timedelta(days = 365) for x in new['established_date']]]new['est_year'] = [x.year for x in new['established_date']]new['est_month'] = [x.month for x in new['established_date']]new['income'] = new['price'] * new['is_booked']mean_income = (new .groupby(['hall_id', 'est_year', 'est_month']).agg('sum')['income'].reset_index() .groupby('est_month').agg('mean')['income']plt.bar(range(1, 12), mean_income)plt.show())




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

Интересно будет сравнить эти данные с сезонностью рынка.

Определение сезонности бизнеса


Сезонность изменение количества заказов в зависимости от периода. Проанализируем годовую сезонность.

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

Расчет сезонности
season = data[(data['open_date'] < '2018-01-01') & (data['date'] > '2018-01-01')]season['income'] = season['price'] * season['duration']season['year'] = [x.year for x in season['date']]season['month'] = [x.month for x in season['date']]incomes = season.groupby(['year', 'month']).agg(np.sum)['income']incomes = incomes[incomes.index]


Построение графика
incomes = incomes[: -3]plt.figure(figsize = (20, 10))plt.plot([str(x[0]) + '-' + str(x[1]) for x in incomes.index], incomes)plt.xticks(rotation=60) plt.grid()plt.show()




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

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

Расчет доходности зала


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

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

Расчет доходности 1 зала
hall_income = season.groupby(['studio_id','hall_id', 'year', 'month']).agg(sum)['income'].reset_index()hall_income = hall_income[hall_income['year'] < 2020]hall_income['income'].describe()



count 648.000000
mean 184299.691358
std 114304.925311
min 0.000000
25% 95575.000000
50% 170350.000000
75% 256575.000000
max 617400.000000
Name: income, dtype: float64

Получили доход на 1 зал в рублях.

Из данных по персентилям видно, что доход половины залов укладывается в интервал от 95 000 руб. до 256 000 руб. с медианным значением в 170 000 руб.

Из данных по средней и стандартному отклонению видим, что согласно правилу 1 сигмы две трети залов приносят от 70 000 руб. до 300 000 руб. с серединов в 184 000 руб.

Получается, средний зал может рассчитывать на доход в 170 000 180 000 руб. 80 000 руб.

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

Сколько залов открыть в фотостудии?


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

Расчет доходности зала в зависимости от количества залов в фотостудии
(hall_income .groupby(['studio_id', 'hall_id']).agg('mean').reset_index() .groupby('studio_id').agg(['count', 'mean'])['income'] .groupby('count').agg('mean'))


mean
count
1 134847.916667
2 146531.944444
3 300231.944444
4 222202.604167

Получили среднемесячную доходность 1 зала в зависимости от количества залов в фотостудии. Заметим закономерность: чем больше залов, тем больше доходность. Максимальная доходность у студий с 3 залами.

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

Зависимость дохода от расположения зала


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

Для расчета посчитаем среднемесячную доход зала, сгруппируем по признаку метро и отсортируем в порядке возрастания.

Доходность зала в зависимости от удаленности от центра
data['income'] = data['price'] * data['duration']data['year'] = [x.year for x in data['date']]data['month'] = [x.month for x in data['date']](data .groupby(['hall_id', 'metro', 'year', 'month']).agg('sum')['income'].reset_index() .groupby(['hall_id', 'metro']).agg('mean')['income'].reset_index() .groupby('metro').agg('mean')['income'].sort_values())[-59:]


Получили следующие данные:
metro
электрозаводская 5016.666667
метро Алексеевская 10485.264378
Дубровка 11925.000000
Марксистская/Нижегородская 18116.666667
Новогиреево, Щелковская 19000.000000
Войковская 21963.333333
Текстильщики 30667.051729
Нижегородская 31031.250000
Нагатинская 37787.500000
Павелецкая/Добрынинская 39357.142857
Партизанская 44354.375000
Полежаевская 45888.888889
Волгоградский проспект 46566.666667
Чкаловская 48541.666667
м. Марксистская, МЦК Нижегородская 49086.503623
Красносельская 55340.659341
Речной вокзал, Митино, Комсомольская 55944.444444
м. Марксистская/ мцк. Нижегородская 59771.111111
Академика Янгеля 66780.000000
Молодежная 66847.058824
Серпуховская 67692.545788
м.Шаболовская 70090.341880
м.Алексеевская 70337.676411
Молодежная, Парк Победы 72974.494949
Кутузовская 79987.083333
Преображенская Площадь 88800.000000
Нагатинская 95550.000000
Электрозаводская 98326.086957
Алексеевская 99216.279070
99925.000000
ВДНХ и Ботанический Сад 102835.622784
м. ВДНХ, м. Ростокино, ст. Яуза\Северянин 104956.521739
Преображенская площадь 111050.684459
Киевская 111090.000000
Калужская 111909.090909
Андроновка 116426.892180
Автозаводская Тульская 117450.000000
Бауманская 118382.236364
Озерная 122626.500000
Савёловская, Марьина роща 123258.518519
Петровско-Разумовская 124557.894737
Чкаловская, Курская 126300.000000
Дмитровская 129222.916667
Павелецкая 135281.642512
Бауманская, Площадь Ильича 138945.454545
Кевская 152246.883469
Серпуховская, Павелецкая 168484.500000
м.Электрозаводская 169079.381010
м. Дмитровская 172618.798439
Электрозаводская 173777.659900
Кожуховская 178254.545455
ВДНХ 181041.818182
Трубная 187283.444198
Ботанический сад 189140.857975
Курская или Площадь Ильича 250975.000000
Крылатское, Киевская, Белорусская 252685.714286
Бауманская, Электрозаводская 264164.473684
Новые-Черемушки 277162.791991
Марьина Роща 556621.746032
Name: income, dtype: float64


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

Из данных видим, что в районах с дорогой недвижимостью, таких как Марьина Роща, Новые Черемушки, Крылатское, доходность на зал выше.

Сколько залов у студий-конкурентов


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

Расчет количество залов у студий
hall_num = studios.merge(halls, left_on='studio_id', right_on='studio_id').groupby('studio_id').agg('count')['is_hall']plt.hist(hall_num, range(np.min(hall_num), np.max(hall_num)+1))plt.show()hall_num.describe()




count 105.000000
mean 2.685714
std 2.292606
min 1.000000
25% 1.000000
50% 2.000000
75% 3.000000
max 13.000000

Из полученных данных видим, что у большинства фотостудий (более 75%) не больше 3 залов. На всем рынке, как правило, у студий не более 5 залов.

Влияние других параметров на доход фотостудии


Высота потолка


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

Посчитаем средний месячный доход каждого зала с сохранением данных по высоте потолка, потом рассчитаем средний доход в зависимости от высоты потолка и постоим график.
Доход зала в зависимости от высоты потолка в метрах
halls_sq_ceil = (data .groupby(['hall_id', 'ceiling', 'square', 'year', 'month']).agg('sum')['income'].reset_index() .groupby(['hall_id', 'ceiling', 'square']).agg('mean')['income'].reset_index())plt.bar(halls_sq_ceil.groupby('ceiling').agg('mean')['income'].index[:-2],        halls_sq_ceil.groupby('ceiling').agg('mean')['income'][: len(halls_sq_ceil) - 2]       )plt.show()



В полученных данных видим, что до 6 метров есть прямая зависимость доходности фотостудии от высоты потолка. Оптимальная высота 5-6 метров.

Площадь залов


Гипотеза: чем больше площадь зала, тем больший доход зал приносит.

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

Доход зала в зависимости от его площади
square = halls_sq_ceil.groupby('square').agg('mean')['income']plt.bar(square.index[:-3],        square.iloc[: len(square) - 3]       )plt.show()



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

Цена бронирования


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

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

Средняя месячная доходность студии в зависимости от цены бронирования зала
price = (data .groupby(['hall_id', 'price', 'year', 'month']).agg('sum')['income'].reset_index() .groupby(['hall_id', 'price']).agg('mean')['income'].reset_index() .groupby('price').agg('mean')['income'])



На сколько залов установлена определенная цена за часовую аренду
plt.figure(figsize = (20, 10))plt.hist(price.iloc[: len(price) - 5].index)plt.show()



Из частотной гистограммы видим, что большинство студий установило цену за аренду от 500 до 2000 руб. Ниже 500 руб. редкость. Максимальная цена аренды зала 3500 руб.

График зависимости среднего месячного дохода от цены аренды зала
price = price[price > 10000]plt.figure(figsize = (20, 10))plt.scatter(price.index, price)plt.show()



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

Другие направления аналитики рынка


Анализ оснащенность


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

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

Анализ влияния нескольких параметров на доход


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

Расширение собираемых данных


Фотостудии на сайте ugoloc.ru составляют меньше трети рынка по количеству. По доходу и сегменту рынка долю студий с данного сайта-агрегатора оценить не представляется возможным. Для более точной картины стоит собирать данные с AppEvent, Google-Календарей, возможно, и с самописных приложений по бронированию.

Учет расходы


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

От площади также зависит и стоимость ремонта: чем больше площадь, тем дороже ремонт.

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

Анализ удаленности от метро


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

Удаленность от конкурентов


Чаще всего студии располагаются близко друг к другу. На одном лишь Электрозаводе их около 40. Есть гипотеза, что близость к другим фотостудиям повышает доходность. Для клиентов может быть знакомо именно место (здание/бизнес-центр) и они могут ему доверять, что положительно скажется на всех фотостудиях локации.

Загруженность фотостудий


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


Состояние фотостудий в несезон


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

Анализ доходности конкурентов


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

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

Этапы анализа


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

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

В этом случае аналитику можно проводить более предметно и точно.

Итог


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

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


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

На сколько полезна была информация в этих трех статьях?
Поделитесь своим мнением.

Готовый проект вы можете найти на моей странице в github.
Подробнее..

Из песочницы Как задача из классического сбора данных, перешла в решение простенькой задачи MNIST. Или как я спарсил сайт ЦИК

02.10.2020 14:18:50 | Автор: admin
В один из будничных дней, под вечер, от моего начальника прилетела интересная задачка. Прилетает ссылка с текстом: хочу отсюда получить все, но есть нюанс. Через 2 часа расскажешь, какие есть мысли по решению задачи. Время 16:00.

Как раз об этом нюансе и будет эта статья.

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

image

Как вы поняли, нюанс заключается в том, что после каждого перехода по ссылке появляется капча.

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

Мне ничего не оставалось делать, как поискать на просторах интернета способы распознавания капчи. Нашел один сервис

+ Капчу распознают 100%, так же, как человек
Среднее время распознавания 9 сек, что очень долго, так как у нас порядка 30 тысяч различных ссылок, по которым нам надо перейти и распознать капчу.

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

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

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

Для начала нужно собрать обучающую выборку.

Открываю вебдрайвер Хрома и скриню 1000 капчей себе в папку.

from selenium import webdriveri = 1000driver = webdriver.Chrome('/Users/aleksejkudrasov/Downloads/chromedriver')while i>0:    driver.get('http://www.vybory.izbirkom.ru/region/izbirkom?action=show&vrn=4274007421995&region=27&prver=0&pronetvd=0')    time.sleep(0.5)    with open(str(i)+'.png', 'wb') as file:        file.write(driver.find_element_by_xpath('//*[@id="captchaImg"]').screenshot_as_png)    i = i - 1


Так как у нас всего два цвета преобразовал наши капчи в чб:

from operator import itemgetter, attrgetterfrom PIL import Imageimport globlist_img = glob.glob('path/*.png')for img in list_img:    im = Image.open(img)    im = im.convert("P")    im2 = Image.new("P",im.size,255)    im = im.convert("P")    temp = {}# Бежим по картинке и переводим её в чб    for x in range(im.size[1]):        for y in range(im.size[0]):            pix = im.getpixel((y,x))            temp[pix] = pix            if pix != 0:                 im2.putpixel((y,x),0)    im2.save(img)

20761


Теперь нам надо нарезать наши капчи на цифры и преобразовать в единый размер 10*10.
Сначала мы разрезаем капчу на цифры, затем, так как капча смещается по оси OY, нам нужно обрезать все лишнее и повернуть картинку на 90.

def crop(im2):    inletter = False    foundletter = False    start = 0    end = 0    count = 0    letters = []    for y in range(im2.size[0]):         for x in range(im2.size[1]):             pix = im2.getpixel((y,x))            if pix != 255:                inletter = True#ищем первый черный пиксель цифры по оси OX        if foundletter == False and inletter == True:             foundletter = True            start = y#ищем последний черный пиксель цифры по оси OX         if foundletter == True and inletter == False:             foundletter = False            end = y            letters.append((start,end))        inletter = False    for letter in letters:#разрезаем картинку на цифры        im3 = im2.crop(( letter[0] , 0, letter[1],im2.size[1] )) #поворачиваем на 90        im3 = im3.transpose(Image.ROTATE_90)         letters1 = []#Повторяем операцию выше        for y in range(im3.size[0]): # slice across            for x in range(im3.size[1]): # slice down                pix = im3.getpixel((y,x))                if pix != 255:                    inletter = True            if foundletter == False and inletter == True:                foundletter = True                start = y            if foundletter == True and inletter == False:                foundletter = False                end = y                letters1.append((start,end))            inletter=False        for letter in letters1:#обрезаем белые куски            im4 = im3.crop(( letter[0] , 0, letter[1],im3.size[1] )) #разворачиваем картинку в исходное положение         im4 = im4.transpose(Image.ROTATE_270)         resized_img = im4.resize((10, 10), Image.ANTIALIAS)        resized_img.save(img)

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

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

Для этого создаем входной слой из 100 нейронов, так как размер картинки 10*10. В качестве выходного слоя 10 нейронов каждый из которых соответствует цифре от 0 до 9.

from tensorflow.keras import Sequentialfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, Activation, BatchNormalization, AveragePooling2Dfrom tensorflow.keras.optimizers import SGD, RMSprop, Adamdef mnist_make_model(10, 10):    # Neural network model    model = Sequential()    model.add(Dense(100, activation='relu', input_shape=(10*10)))    model.add(Dense(10, activation='softmax'))    model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])    return model

Разбиваем наши данные на обучающую и тестовую выборку:

list_folder = ['0','1','2','3','4','5','6','7','8','9']X_Digit = []y_digit = []for folder in list_folder:    for name in glob.glob('path'+folder+'/*.png'):        im2 = Image.open(name)        X_Digit.append(np.array(im2))        y_digit.append(folder)

Разбиваем на обучающую и тестовую выборку:

from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X_Digit, y_digit, test_size=0.15, random_state=42)train_data = X_train.reshape(X_train.shape[0], 10*10) #Преобразуем матрицу векторов размерностью 100test_data = X_test.reshape(X_test.shape[0], 10*10) #Преобразуем матрицу векторов размерностью 100#преобразуем номер класса в вектор размерностью 10num_classes = 10train_labels_cat = keras.utils.to_categorical(y_train, num_classes)test_labels_cat = keras.utils.to_categorical(y_test, num_classes)

Обучаем модель.

Эмпирическим путем подбираем параметры количество эпох и размер бэтча:

model = mnist_make_model(10,10)model.fit(train_data, train_labels_cat, epochs=20, batch_size=32, verbose=1, validation_data=(test_data, test_labels_cat))


Сохраняем веса:

model.save_weights("model.h5")

Точность на 11 эпохе получилась отличная: accuracy = 1.0000. Довольный, в 19:00 иду домой отдыхать, завтра еще нужно будет написать парсер для сбора информации с сайта ЦИКа.

Утро следующего дня.

Дело осталось за малым, осталось обойти все страницы на сайте ЦИКа и забрать данные:

Загружаем веса обученной модели:

model = mnist_make_model(10,10)model.load_weights('model.h5')

Пишем функцию для сохранения капчи:

def get_captcha(driver):    with open('snt.png', 'wb') as file:        file.write(driver.find_element_by_xpath('//*[@id="captchaImg"]').screenshot_as_png)    im2 = Image.open('path/snt.png')    return im2

Пишем функцию для предсказания капчи:

def crop(im):    list_cap = []    im = im.convert("P")    im2 = Image.new("P",im.size,255)    im = im.convert("P")    temp = {}    for x in range(im.size[1]):        for y in range(im.size[0]):            pix = im.getpixel((y,x))            temp[pix] = pix            if pix != 0:                im2.putpixel((y,x),0)        inletter = False    foundletter=False    start = 0    end = 0    count = 0    letters = []    for y in range(im2.size[0]):         for x in range(im2.size[1]):             pix = im2.getpixel((y,x))            if pix != 255:                inletter = True        if foundletter == False and inletter == True:            foundletter = True            start = y        if foundletter == True and inletter == False:            foundletter = False            end = y            letters.append((start,end))        inletter=False    for letter in letters:        im3 = im2.crop(( letter[0] , 0, letter[1],im2.size[1] ))        im3 = im3.transpose(Image.ROTATE_90)        letters1 = []        for y in range(im3.size[0]):            for x in range(im3.size[1]):                pix = im3.getpixel((y,x))                if pix != 255:                    inletter = True            if foundletter == False and inletter == True:                foundletter = True                start = y            if foundletter == True and inletter == False:                foundletter = False                end = y                letters1.append((start,end))            inletter=False        for letter in letters1:            im4 = im3.crop(( letter[0] , 0, letter[1],im3.size[1] ))        im4 = im4.transpose(Image.ROTATE_270)        resized_img = im4.resize((10, 10), Image.ANTIALIAS)        img_arr = np.array(resized_img)/255        img_arr = img_arr.reshape((1, 10*10))        list_cap.append(model.predict_classes([img_arr])[0])    return ''.join([str(elem) for elem in list_cap])

Добавляем функцию, которая скачивает таблицу:

def get_table(driver):    html = driver.page_source #Получаем код страницы     soup = BeautifulSoup(html, 'html.parser') #Оборачиваем в "красивый суп"    table_result = [] #Объявляем лист в котором будет лежать финальная таблица    tbody = soup.find_all('tbody') #Ищем таблицу на странице    list_tr = tbody[1].find_all('tr') #Собираем все строки таблицы    ful_name = list_tr[0].text #Записываем название выборов    for table in list_tr[3].find_all('table'): #Бежим по всем таблицам        if len(table.find_all('tr'))>5: #Проверяем размер таблицы            for tr in table.find_all('tr'): #Собираем все строки таблицы                snt_tr = []#Объявляем временную строку                for td in tr.find_all('td'):                    snt_tr.append(td.text.strip())#Собираем все стоблцы в строку                table_result.append(snt_tr)#Формируем таблицу    return (ful_name, pd.DataFrame(table_result, columns = ['index', 'name','count']))

Собираем все линки за 13 сентября:

df_table = []driver.get('http://www.vybory.izbirkom.ru')driver.find_element_by_xpath('/html/body/table[2]/tbody/tr[2]/td/center/table/tbody/tr[2]/td/div/table/tbody/tr[3]/td[3]').click()html = driver.page_sourcesoup = BeautifulSoup(html, 'html.parser')list_a = soup.find_all('table')[1].find_all('a')for a in list_a:    name = a.text    link = a['href']    df_table.append([name,link])df_table = pd.DataFrame(df_table, columns = ['name','link'])

К 13:00 я дописываю код с обходом всех страниц:

result_df = []for index, line in df_table.iterrows():#Бежим по строкам таблицы с ссылками    driver.get(line['link'])#Загружаем ссылку    time.sleep(0.6)    try:#Разгадываем капчу если она вылетает        captcha = crop(get_captcha(driver))        driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)        driver.find_element_by_xpath('//*[@id="send"]').click()        time.sleep(0.6)        true_cap(driver)    except NoSuchElementException:#Отлавливаем ошибку если капче не появилась        pass    html = driver.page_source    soup = BeautifulSoup(html, 'html.parser')    if soup.find('select') is None:#Проверяем есть ли выпадающий список на странице        time.sleep(0.6)        html = driver.page_source        soup = BeautifulSoup(html, 'html.parser')                  for i in range(len(soup.find_all('tr'))):#Ищем ссылку на результат выборов            if '\nРЕЗУЛЬТАТ ВБОРОВ\n' == soup.find_all('tr')[i].text:#Ищем фразу, следующая за этой фразой наша ссылка на таблицу с выборами                rez_link = soup.find_all('tr')[i+1].find('a')['href']        driver.get(rez_link)        time.sleep(0.6)        try:            captcha = crop(get_captcha(driver))            driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)            driver.find_element_by_xpath('//*[@id="send"]').click()            time.sleep(0.6)            true_cap(driver)        except NoSuchElementException:            pass        ful_name , table = get_table(driver)#Получаем таблицу        head_name = line['name']        child_name = ''        result_df.append([line['name'],line['link'],rez_link,head_name,child_name,ful_name,table])    else:#Если выпадающий список присутствует, обходим все ссылки        options = soup.find('select').find_all('option')        for option in options:            if option.text == '---':#Пропускаем первую строку из выпадающего списка                continue            else:                link = option['value']                head_name = option.text                driver.get(link)                try:                    time.sleep(0.6)                    captcha = crop(get_captcha(driver))                    driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)                    driver.find_element_by_xpath('//*[@id="send"]').click()                    time.sleep(0.6)                    true_cap(driver)                except NoSuchElementException:                    pass                html2 = driver.page_source                second_soup = BeautifulSoup(html2, 'html.parser')                for i in range(len(second_soup.find_all('tr'))):                    if '\nРЕЗУЛЬТАТ ВБОРОВ\n' == second_soup.find_all('tr')[i].text:                        rez_link = second_soup.find_all('tr')[i+1].find('a')['href']                driver.get(rez_link)                try:                    time.sleep(0.6)                    captcha = crop(get_captcha(driver))                    driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)                    driver.find_element_by_xpath('//*[@id="send"]').click()                    time.sleep(0.6)                    true_cap(driver)                except NoSuchElementException:                    pass                ful_name , table = get_table(driver)                child_name = ''                result_df.append([line['name'],line['link'],rez_link,head_name,child_name,ful_name,table])                if second_soup.find('select') is None:                    continue                else:                    options_2 = second_soup.find('select').find_all('option')                    for option_2 in options_2:                        if option_2.text == '---':                            continue                        else:                            link_2 = option_2['value']                            child_name = option_2.text                            driver.get(link_2)                            try:                                time.sleep(0.6)                                captcha = crop(get_captcha(driver))                                driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)                                driver.find_element_by_xpath('//*[@id="send"]').click()                                time.sleep(0.6)                                true_cap(driver)                            except NoSuchElementException:                                pass                            html3 = driver.page_source                            thrid_soup = BeautifulSoup(html3, 'html.parser')                            for i in range(len(thrid_soup.find_all('tr'))):                                if '\nРЕЗУЛЬТАТ ВБОРОВ\n' == thrid_soup.find_all('tr')[i].text:                                    rez_link = thrid_soup.find_all('tr')[i+1].find('a')['href']                            driver.get(rez_link)                            try:                                time.sleep(0.6)                                captcha = crop(get_captcha(driver))                                driver.find_element_by_xpath('//*[@id="captcha"]').send_keys(captcha)                                driver.find_element_by_xpath('//*[@id="send"]').click()                                time.sleep(0.6)                                true_cap(driver)                            except NoSuchElementException:                                pass                            ful_name , table = get_table(driver)                            result_df.append([line['name'],line['link'],rez_link,head_name,child_name,ful_name,table])

А после приходит твит, который изменил мою жизнь

2020-09-29-12-11-31
Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru