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

в 9:39, , рубрики: captcha, mnist, python, selenium-webdriver, TensorFlow, машинное обучение, парсинг сайта

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

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

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

image

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

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

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

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

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

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

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

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

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

from selenium import webdriver
i = 1000
driver = 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, attrgetter
from PIL import Image
import glob
list_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 Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, Activation, BatchNormalization, AveragePooling2D
from tensorflow.keras.optimizers import SGD, RMSprop, Adam
def 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_split

X_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) #Преобразуем матрицу векторов размерностью 100
test_data = X_test.reshape(X_test.shape[0], 10*10) #Преобразуем матрицу векторов размерностью 100
#преобразуем номер класса в вектор размерностью 10
num_classes = 10
train_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_source
soup = 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

Автор: Алексей

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js