О чем эта статья В этой статье будет описано распознавание простого текста, а именно номера телефона продавца с Авито простым способом. Чтобы показать, что мы тута не в бирюльки играем, этот метод был назван наукообразным (и даже отражающим суть) термином. Для кого эта статья Статья может быть интересна любому человеку, занимающемуся парсингом контента. Метод, описанный в этой статье, можно модифицировать и для более сложных случаев распознавания текста. Предмет статьи Защита avito от парсинга и метод ее обхода были рассмотрены в статье за авторством anshlag. Уже понятно, как получить картинку с телефоном. Рассмотрим некоторые особенности, позволяющие довольно просто эту картинку распознать. Картинка с телефоном представляет из себя PNG-файл с альфа-каналом, 102 пикселя в ширину, 16 пикселей в высоту. Например, компоненты картинки будут таковы: основной канал , альфа-канал . Забегая вперед, скажу, что в данному случае нам хватит данных из одного канала для успешного распознавания. Далее, набрав N картинок с телефонами, стекируем их (т.е. располагаем друг над другом в одном большом коллаже), используя, например, такой кодес: from PIL import Image from glob import glob filenames = glob('images/phone*.png') number_of_images = len(filenames) width, height = Image.open(filenames[0]).size total_height = height*number_of_images stacked = Image.new('RGBA', (width, total_height)) for index,filename in enumerate(filenames): image = Image.open(filename) stacked.paste(image,(0,index*height)) stacked.save('stacked.png') Получаем примерно такой результат: Проводим вертикальные границы каждой из цифр: Выясняется, что каждая цифра имеет размер 6 на 10 пикселей. Также выясняется, что для каждой картинки с номером положение каждой цифры статично. Таким образом, имя картинку с номером, мы можем легко разбить ее на отдельные цыферки. Собрав отдельные цифры (используя главный канал), получаем: Как видим, цифры выглядят довольно своеобразно, но в деле распознавания образов предпочтительные для человека и компьютера качества картинки редко совпадают. Другими словами, зачастую страшные кривые цифры могут быть легко распознаны компьютером, а красивый шрифт с вензелями - нет. Ну а теперь сам секретный метод. Для каждой цифры подсчитываем количество черных пикселей. Чтобы не производить подсчет вручную, вычисляем гистограмму, получив при этом список из 256 значений, каждое из которых равно количеству пикселей с соответствующим значением (от 0 до 255). Таким образом, первый элемент списка (доступный по индексу 0) равен количеству черных пикселей. Выполнив следующий код from PIL import Image for digit in xrange(0,10): filename = '{}.png'.format(digit) image = Image.open(filename) width, height = image.size histogram = image.histogram() print '{} ----> {},{}'.format(digit,histogram[0],histogram[-1]) Получим вывод Для каждой цифры имеем количество черных (значение пикселя = 0) и белых (значение пикселя = 255) пикселей. Стоит отметить, что во-первых, сумма этих двух значений для каждой цифры равна 60 (=6x10, то есть количеству всех пикселей цифры), то есть в главном канале изображения цифры полутонов нет, и во-вторых, каждая цифра может быть однозначно идентифицирована по количеству черных (или белых) пикселей. Именно второе наблюдение позволяет нам распознавать номера телефонов без особых хитростей. Соединяя воедино и воспользовавшись находками из статьи anshlag'а, получаем следующий код. import requests from lxml import html from re import findall from PIL import Image import base64 from StringIO import StringIO from collections import namedtuple import sys Advertisement = namedtuple('Advertisement','id price phone') URLS = ["https://www.avito.ru/moskva/telefony/novye_iphone_5s_16gb_lte_garantiya_original_731816493", "https://www.avito.ru/moskva/telefony/zaryadnoe_ustroystvo_dlya_ayfon_5.6_791740810", "https://www.avito.ru/moskva/telefony/optom_i_v_roznitsu-vse_v_nalichii-magazin-24_678666846", "https://www.avito.ru/moskva/telefony/iphone_6_1286416_-optom_i_v_roznitsu-24chasa_578416635", "https://www.avito.ru/moskva/telefony/iphone_44s_chehol-zaryadka_522990551", ] def get_image_pkey(ad_id, ad_phone): if ad_id and ad_phone: ad_subhash = findall(r'[0-9a-f]+', ad_phone) if int(ad_id) % 2 == 0: ad_subhash.reverse() ad_subhash = ''.join(ad_subhash) return ad_subhash[::3] def recognize(base64_image): image = Image.open(StringIO(base64.b64decode(base64_image))).convert('LA') left_margins = (2,13,20,27,39,46,53,65,73,84,92) DIGITS_BY_HIST = {44: '0', 16: '1',38: '2', 43: '3', 28: '4', 41: '5', 45: '6',25: '7', 53: '8', 47: '9'} digits=[] for index, left_margin in enumerate(left_margins, start=1): box = (left_margin,4, left_margin+6,14) digit = image.crop(box) characteristic = digit.histogram()[0] digits.append(DIGITS_BY_HIST.get(characteristic,'x')) return ''.join(digits) def parse(url): ad_id = url.split('_')[-1] headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} s = requests.Session() response = s.get(url, headers=headers) doc = html.fromstring(response.content) script = doc.xpath('//div[@class="clearfix" and @itemscope and @itemtype="http://schema.org/Product"]/script') if not script: return None script = script[0].text price, phone_hash = None, None for line in script.split('\n'): line = line.strip() if not line: continue var_name, var_value = [item.strip('\'";') for item in line.split(' = ')] if var_name=='avito.item.price': price = int(var_value) if var_name=='avito.item.phone': phone_hash = var_value pkey = get_image_pkey(ad_id, phone_hash) image_url = 'https://www.avito.ru/items/phone/{}?pkey={}'.format(ad_id,pkey) headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36', 'x-requested-with':'XMLHttpRequest', 'referer':url} image = s.get(image_url, headers=headers).json().get('image64') phone = None if image: base64_image = image.split(',')[1] phone = recognize(base64_image) result = Advertisement(ad_id, price, phone) return result if __name__=='__main__': if len(sys.argv)>1: url = sys.argv[1] print "Parsing URL {}".format(url) ad = parse(url) if ad: print "Result: ID {}, phone {}, price {}.".format(ad.id,ad.phone, ad.price) else: print "Could not parse URL. " else: TEMPLATE = "{:10} | {:11} | {:>6}" print TEMPLATE.format('ID','Phone','Price') for url in URLS: ad = parse(url) if ad: print TEMPLATE.format(ad.id, ad.phone, ad.price) else: print TEMPLATE.format(None,None,None) Чтобы использовать его, сначала установите зависимости (используется python 2.7): pip install requests pip install lxml pip install pillow Использование: передайте URL объявления аргументом к скрипту: python avito-parser.py https://www.avito.ru/moskva/telefony/iphone_5s_16gb_silver_794946127 и получите вывод: Result: ID 794946127, phone 89645892690, price 12999. При запуске без аргументов скрипт обрабатывает захардкоженные ссылки, со временем они могут устареть. python avito-parser.py ID | Phone | Price 731816493 | 89067128880 | 11990 791740810 | 89128821808 | 500 678666846 | 89651386888 | 9900 578416635 | 89651386888 | 19900 522990551 | 89055622660 | 1200 В некоторых случаях, например при ограничении количества запросов на сервере (rate-limit) или появлении капчи, скрипт может вываливаться с исключением. Следует иметь в виду, что скрипт представляет из себя proof-of-concept, при желании можете самостоятельно его доработать под свои нужды.