вторник, 23 декабря 2014 г.

Задача

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

Именно таким образом я и пришел к задаче:
Написать прогу, которая бы заходила на сайт superjob.ru, логинилась, переходила в резюме и на нужном из них обновляла дату публикации. Естественно из консоли. Естественно чтоб можно было её повесить в cron :)

Язык исполнения: python
Инструменты: linux debian + ipython/ipython3, Chrome browser в виндозе для просмотра кодов элементов в режиме отладки, google..куда же без него.

Сходу задача показалась простой, при этом ни каких дополнительных вещей типа try-except или проверки на return из функций я решил не делать - это к сути задачи не относится и при желании может быть добавлено в любое время. Да и с другой стороны - лично я предпочитаю на стадии написания увидеть все возможные трейсбэки своими глазами, а не давить их эксепшенами. Полезно это, имхо.
Для языка python есть множество модулей с уже готовыми api для работы с web-страницами.
Присмотревшись к переписанному из perl'a в python модулю Mechanize родилась вот такая штука:

#!/usr/bin/python

from mechanize import Browser
import socks
import socket


TOR_IP = '127.0.0.1'
TOR_PORT = 9050
URL='http://spb.superjob.ru'
NAME='qwertyu@email.my'
PASSWD='password'

""" на случай если очень захочется пройти инкогнито через tor """
def enable_tor():
        socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, TOR_IP, TOR_PORT)
        socket.socket = socks.socksocket

#enable_tor()
br = Browser()
br.set_handle_robots(False)
br.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1')]
br.set_handle_equiv(False)
html=br.open(URL)
br.select_form(name='login_form')
br['LoginForm[login]']=NAME
br['LoginForm[password]']=PASSWD
result_page = br.submit()
result_page=br.follow_link(text_regex='Моё резюме')
data = html.read()

И вот тут меня ждало разочарование. Оказывается хитрецы из superjob сделали ссылку обновления даты резюме на javascript. А mechanize ну никак не умеет работать с явой - только классический html. Дальнейшее гугленье мне рассказало про замечательный проект selenium, который умеет и яву и все что угодно потому как использует сторонние веб-движки через webdriver. В частности можно сделать так:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
driver = webdriver.Firefox()
driver.get("http://www.python.org")
assert "Python" in driver.title
elem = driver.find_element_by_name("q")
elem.send_keys("pycon")
elem.send_keys(Keys.RETURN)
assert "No results found." not in driver.page_source
driver.quit()


Но и здесь кроется засада. Дело в том что для того чтобы юзать webdriver firefox в таком    вот виде, надо чтобы файрфокс был установлен в системе. У меня же вообще нет иксов! Просто debian + консоль. И вообще - это виртуалка на VSphere и ставить иксы туда мне лень и вообще не надо потому как виртуалка для обкатки скриптов. С другой стороны есть возможность скачать любой из поддерживаемых selnium'ом веб-драйверов и попробовать его использовать как RemoteDriver приблизительно вот так 


from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
server_url = "http://%s:%s" % (localhost, 9015)
dc = DesiredCapabilities.HTMLUNIT
wd = webdriver.Remote(server_url, dc)
wd.get('http://www.google.com')


как услужливо подсказывает нам ipython про wbdriver:
from selenium import webdriver

In [141]: webdriver.
webdriver.Opera     webdriver.Android              webdriver.PhantomJS   
webdriver.Chrome  webdriver.Proxy           webriver.Remote  
webdriver.Safari     webdriver.Firefox  


Скачать драйвер Chrome и прицепить его через webdriver.Remote с наскоку не удалось потому как я работаю через ssh, а webdriver.Remote требует в любом случае иксового DISPLAY :0. 

Я по-быстрому забил на эту идею и присмотрелся к PhantomJS. Почему именно он? 

А потому что существует проект WebKit, на котором построены такие браузеры как Safari, Chromium/Crome и иже с ними - много их там. Но отличие PhantomJS в том, что он изначально разрабатывался как консольный вариант, без использования каких-либо GUI.

Ставим:



 apt-get update && apt-get install build-essential chrpath libssl-dev libxft-dev libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev

 cd /tmp && mkdir webkit && cd webkit

 wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-i686.tar.bz2

 tar -jxf ./phantomjs-1.9.8-linux-i686.tar.bz2
 cp phantomjs-1.9.8-linux-i686/bin/phantomjs /usr/bin
(обязательно он должен быть на маршруте, иначе работать не будет)

проверяем:

from selenium import webdriver
driver=webdriver.PhantomJS()
driver.get('http://ya.ru')
print driver.title
driver.quit()

Яндекс

Работает! Продолжаем))

Переход по элементам страницы можно организовать как угодно. Я так думаю что придется подтянуть теорию xpath, потому как мне это очень понравилось, хоть я в нем ни бум ни тресь. Именно он (ентон самый xpath) и заменяет де-факто регулярки - так что придется, да и плюс в нете груда иныф про него.
Доступ к элементам я организовал самым простым способом: в лоб))
Еще за замечу, что есть элементы которые в классе находятся в еденичном варианте, а следовательно к таким элементам можно достучаться даже используя find_element_by_class_name
Кстати, а почему бы мне его не применить))

В общем вот что получилось в итоге:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver=webdriver.PhantomJS()

driver.get('http://spb.superjob.ru')
#print driver.title - проверим был ли переход можно тут, а можно и try/except

loginform=driver.find_element_by_class_name('LoginForm_link')

loginform.click()
driver.find_element_by_id('LoginForm_login').send_keys('username@mail(dot)domain')
driver.find_element_by_id('LoginForm_password').send_keys('password')
driver.find_element_by_id('LoginForm_password').send_keys(Keys.ENTER)
driver.find_element_by_link_text(u'Моё резюме').click()
driver.find_element_by_link_text(u'обновить дату публикации').click()
driver.quit()



*меня тут прибило искать форму с сабмитом и я использовал тот факт, что клавиша ВВОД обычно и есть "submit", но можно было и через вот это попробовать - это более канонично.
find_element_by_name("submit").click()


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

http://selenium-python.readthedocs.org/
http://www.ibm.com/developerworks/ru/library/l-python-mechanize-beautiful-soup/
http://wwwsearch.sourceforge.net/mechanize/
http://habrahabr.ru/post/65383/

have a fun
================ UPDATED 13.01.15 ======================
Ребром встал вопрос о сокрытии паролей внутри программы. Даже компиляция в .pyc-файл не очень тут спасает, потому как при просмотре файла внутри .pyc отлично видны такие вещи как username@mail(dot)domain, ну а прикинуть что из текста будет паролем - вообще несложно.
Можно конечно использовать модуль base64, он тоже кодирует, но на глаз сразу видны кодированные строки и раскодировать их вообще "нипраблема". Покопавшись и почитав я принял решение подключить модуль crypto и выбрав какой-либо метод шифрования, хитрый ключ  и размер блока, шифрануть все что надо. Вот что в итоге получилось

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Crypto.Cipher import AES
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from base64 import b64decode,b64encode
from sys import exit

""" URL, LOGIN, PASSWD -сгенеренные от ключа numkey http://spb.superjob.ru, login+password"""

numkey=1234567890123456
URL='XmwrgNWXKqkkoRJDgC020R7Uq9RfuJsgey6mcfGYCug='
LOGIN='n5sr2mgr7Kj7cSnp/p1Z8qKbMqsR0VG0Z62fpJB3VrU='
PASSWD='+hS577+JJ1+w91wO7KH4n1Q=='
BLOCK_SIZE = 16
PADDING = '{'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: b64encode(c.encrypt(pad(s)))
DecodeAES = lambda c, e: c.decrypt(b64decode(e)).rstrip(PADDING)
cipher = AES.new(str(numkey))
driver=webdriver.PhantomJS()

# ожидание загрузки страницы зависит от скорости инета/компа..
# да и еще PhantomJS - достаточно тормознутая хрень, делаем неявное ожидание 20 секунд
driver.implicitly_wait(20)

"""
можно по-другому, например до появления како-то элемента
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


driver = webdriver.Firefox()
driver.get('...')

// wait for svg to appear
WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.TAG_NAME, 'svg')))
"""

#заключам весь процесс в секцию try/except - неизвестно на каком этапе срубится, может 
#пароль сменим внезапно.. ;)
retcode=0
try:
   driver.get(DecodeAES(cipher,URL))
   loginform=driver.find_element_by_class_name('LoginForm_link')
   loginform.click()
   driver.find_element_by_id('LoginForm_login').send_keys(DecodeAES(cipher,LOGIN))
   driver.find_element_by_id('LoginForm_password').send_keys(DecodeAES(cipher,PASSWD))
   driver.find_element_by_id('LoginForm_password').send_keys(Keys.ENTER)
   driver.find_element_by_link_text(u'Моё резюме').click()
   driver.find_element_by_link_text(u'обновить дату публикации').click()
except:
   retcode=1

driver.quit()
exit(retcode)

скомпилировав это чудо через
import py_compile
py_compile.compile('myfile.py')

я получил вполне достойный псевдобинарник, где нет видных глазу данных. Конечно, я же этот бинарник могу сломать, но не зная как выглядит ключ это нетривиально, что и требовалось.
Ни о какой сверхЪзащите здесь речи и не шло. С другой стороны, для усложнения поиска ключа, итоговый ключ можно было разнести на несколько переменных смешанных со статическими данными. Типа такого:
cr1='.-.+.\./'
cr2=random.randint(1000000000000000,9999999999999999)
cr3='$}887zO`'

размерность ключа должна быть кратной 8ми, а итоговый ключ может выглядеть так

key=cr3+str(random.randint(10000000,99999999))+cr1+cr2
что в .pyc будет выглядеть несовсем понятно.

литература:
https://www.dlitz.net/software/pycrypto/
http://selenium2.ru/docs/webdriver-advanced-usage.html

================ UPDATED 23.04.15 ======================
def superjobupdate(driver, cipher, SJURL, SJLOGIN, SJPASSWD):
   retcode=0
   try:
      driver.get(DecodeAES(cipher,SJURL))
      driver.find_element_by_class_name('LoginForm_link').click()
      driver.find_element_by_id('LoginForm_login').send_keys(DecodeAES(cipher,SJLOGIN))
      driver.find_element_by_id('LoginForm_password').send_keys(DecodeAES(cipher,SJPASSWD)+Keys.ENTER)
      driver.find_element_by_xpath('//*[@id="ng-app"]/div[2]/div/div[2]/div/div[1]/div[3]/div[2]/div[1]/a').click()
      driver.find_element_by_link_text(u'обновить дату публикации').click()
   except:
      retcode=2
   return retcode

def hhupdate(driver, cipher, HHURL, HHLOGIN, HHPASSWD):
   retcode=0
   try:
      driver.set_window_size(1400,1000)
      driver.get(DecodeAES(cipher,HHURL))
      driver.find_element_by_xpath('/html/body/div[3]/div/div[3]/div/div[2]/form/label[1]/input').send_keys(DecodeAES
      driver.find_element_by_xpath('/html/body/div[3]/div/div[3]/div/div[2]/form/label[2]/input').send_keys(DecodeAES
      driver.get(DecodeAES(cipher,HHURL)+'/applicant/resumes')
      arr=driver.find_elements_by_class_name("resumelist__resume")
      maxresumes=len(arr)
      for resumeindex in range(0,maxresumes):
              el=arr[resumeindex].find_element_by_class_name('b-resumelist-vacancyname')
              el.click()
              try:
                 driver.find_element_by_css_selector('body > div.HH-MainContent > div.g-row.m-row_content > div.g-col
              except:
                 pass
              driver.back()
              arr=driver.find_elements_by_class_name("resumelist__resume")
   except:
      retcode=20
   return retcode