В один из будничных дней, под вечер, от моего начальника прилетела интересная задачка. Прилетает ссылка с текстом: «хочу отсюда получить все, но есть нюанс». Через 2 часа расскажешь, какие есть мысли по решению задачи. Время 16:00.
Как раз об этом нюансе и будет эта статья.
Я как обычно запускаю selenium, и после первого перехода по ссылке, где лежит искомая таблица с результатами выборов Республики Татарстан, вылетает оно
Как вы поняли, нюанс заключается в том, что после каждого перехода по ссылке появляется капча.
Проанализировав структуру сайта, было выяснено, что количество ссылок достигает порядка 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®ion=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)
Теперь нам надо нарезать наши капчи на цифры и преобразовать в единый размер 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])
А после приходит твит, который изменил мою жизнь
Автор: Алексей