Использование библиотеки Mechanize в Python для авторизации, отправки веб-форм и скачивания файлов

Для языка Python существует довольно много библиотек для работы с веб-ресурсами. Это и стандартная urllib / urllib2 и сторонние – mechanize, Twill, Request, Client Form.

В данной статье я рассмотрю работу с библиотекой mechanize, т. к. именно ее инструменты помогли мне “победить” задачу автоматизации скачивания материала с одного веб-сайта.

Суть проблемы я буду излагать достаточно пространно, т. к. не могу приводить здесь конкретный пример, но надеюсь – вам будет примерно понятно.

Проблема заключалась в следующих моментах:

  • для скачивания нужно было авторизоваться на сайте. Причем, авторизация с помощью forms, а не Basic HTTP. 
  • необходимо было хранить кукисы, дабы авторизация не пропала. 
  • путем манипуляций со списками (SelectControl) выбирались параметры скачивания. 
  • далее нужно было получить ссылку, содержащую уникальный токен, не позволявший сачать файл по одной и той же ссылке боле 1 раза просто поменяв параметры GET запроса. 
  • и авторизация и скачивание и всё остальное нехорошие программисты сделали на одной html-странице в одной и той же форме, только с разными кнопками типа Submit.

Итак, начнемс. Вначале импорт необходимых библиотек. Mechanize не идет в стандартной поставке с Python  и его необходимо установить или с помозью PiPy или вручную, как написано на официальном сайте (python setup.py install).

import cookielib, shutil, os
from mechanize import Browser

Далее, создаем объекты: основной Browser и объект для хранения кукисов, который прикрепляем к нашему Browser. Объект Browser представляет собой контейнер по смыслу похожий на вкладку настоящего браузера. Т. е. в пределах его можно оществлять навигацию по web-сайту, авторизацию, хранить кукисы и т. д.

br = Browser()
# Create cookie jar and attach it to Browser
cj = cookielib.LWPCookieJar()
br.set_cookiejar(cj)

Затем, добавляем html хэдэры и открываем URL нужной странички в объекте Browser.

# Add some headers
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')]
# Open url in Browser instance
br.open('http://www.example.com/download.aspx')

Далее выбираем форму, в которую будем вносить данные. nr=0 – индекс формы по счету в коде веб-страницы (0,1,2 …) И вводим данные в поля для авторизации в формате (для элементов типа TextControl):

br[“id_html_элемента”] = “Значение”

id элементов и возможные значения можно посмотреть двумя способами:

  • в браузере Google Chrome нажать правой кнопкой мыши на нужном элементе и выбрать Просмотр исходного кода элемента, затем правой кнопкой на выделенном элементе и Copy as HTML. После этого, вставить текст в любой текстовый редактор. 
  • выполнив код в интерпретаторе python или в отдельном скрипте:

import cookielib
from mechanize import ParseResponse, urlopen
response = urlopen("http://www.eoddata.com/download.aspx")
forms = ParseResponse(response, backwards_compat=False)
form = forms[0]
print form

Вы получите список всех элементов формы и их возможных значений в формате:

<Тип(id=список_значений)>

Вроде такого:

<aspnetForm POST http://www.example.com/download.aspx application/x-www-form-urlencoded
  <SelectControl(ctl00$DataFormat=[17, 2, 36, 16, 27, 28, 33, 32, 8, 24, 35, *9, 1, 39, 11, 14, 6, 5, 4, 12, 3, 26, 15])>
  <SubmitControl(ctl00$Download=Download) (readonly)>
  <TextControl(ctl00$txtEmail=)>
  <PasswordControl(ctl00$txtPassword=)>
  <CheckboxControl(ctl00$Remember=[on])>
  <SubmitControl(ctl00$btnLogin=Login) (readonly)>>

Итак, пример кода основного скрипта для авторизации:

# Select form for modification and "enter" login info
br.select_form(nr=0)
br["ctl00$txtEmail"] = "User"
br["ctl00$txtPassword"] = "Secret"
# Submit form
br.submit(nr=3)

Обратите внимание на строку

br.submit(nr=3)

В скобках указан номер кноки типа Submit по порядку её нахождения в html-коде страницы. Это важно, если в форме есть несколько кнопок типа Submit, но выполняющих различные функции (как в нашем случае). Нумерация 0,1,2…

Далее, выбираем необходимые данные в списках значений, например (значения посмотреть описанными выше методами):

br.select_form(nr=0)
# Select Options
br["ctl00$cboSomething"] = ["EXAMPLE",]
br["ctl00$cboDataFormat"] = ["11",]
# Update page
br.submit(nr=1)

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

Скачиваем файл (считая, что искомый URL на файл находится в переменной download_url):

local_path = "/home/user"
filename = "very_important.txt"
f = br.retrieve(download_url)[0]
# Copy saved file to normal location
shutil.copy2(f,os.path.join(local_path, filename))

В переменной f мы получаем путь к скачанному файлу, обычно это какое-то случайное имя в директории /tmp. Далее перемещаем его в нужное нам место.

Ссылки:

  • http://stockrt.github.io/p/emulating-a-browser-in-python-with-mechanize/
  • http://www.voidspace.org.uk/python/articles/authentication.shtml
  • http://mozgovipc.blogspot.com/2012/06/python-http-basic-authentication-with.html
  • http://docs.python-requests.org/en/latest/
  • http://stackoverflow.com/questions/9541677/urllib2-post-request
  • http://docs.python.org/2/howto/urllib2.html
  • http://stackoverflow.com/questions/9288662/need-more-mechanize-documentation-python
  • http://www.pythonforbeginners.com/cheatsheet/python-mechanize-cheat-sheet
  • http://blog.spritecloud.com/2010/01/posting-forms-with-python/
http://geckich.blogspot.com/

Определение и вызов fabric task в одном Python скрипте

Библиотека fabric для Python является хорошим средством для автоматизации действий (админимстрирования, deployment и т. д.) для инфраструктуры серверов под управлением ОС Linux. Библиотека использует протокол open ssh для выполнения команд и перемещения данных.

Подробнее о библиотеке можно прочитать на официальном сайте или здесь.

Бывают ситуации, когда необходимо в одном скрипте и объявить таск из библиотеки fabric и выполнить его.

Пример структуры такого скрипта:

#!/usr/bin/env python
# Script dependencies: python-devel, fabric (https://github.com/fabric/fabric/archive/master.zip)
# Usage: python script.py
import fabric
from fabric.api import *
# Define ssh-like address user@server_hostname
remote_host = user@server

def some_fab_task():
# Define fabric task here
# hide('everything') helps to execute task with minimum output to console
with settings(hide('everything')):
try:
....
except:
....

# Main function
if __name__ == '__main__':
# Call fabric task from here
fabric.tasks.execute(some_fab_task, hosts=[remote_host])

Для того, чтобы скрипт выполнялся “тихо” – без запросов на ввод пароля для удаленного пользователя, необходимо организовать доступ на удаленноый сервер с помощью ssh-ключей,  например как я описывал здесь.

Update:

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

fabric.tasks.execute(some_fab_task, arg1, arg2, hosts=[remote_host])

Для передачи xargs, можно указывать их также до или после hosts=

fabric.tasks.execute(some_fab_task, arg1, arg2, hosts=[remote_host], xarg1=value1)
http://geckich.blogspot.com/

Автоматизация скриптов с помощью expect

Недавно открыл для себя интересную консольную утилиту expect. Сия утилита является интерпретатором (со своим языком) для взаимодействия с интерактивными (т. е. требующими какого-либо ввода от пользователя) программами.

Дабы не писать много лишнего, рассмотрим использование expect на примере.

Допустим,

  • есть удалённый sftp-сервер с адресом 192.168.1.2,
  • к которому у нас есть доступы вида user/SECRET, порт 20022,
  • нам нужно автоматически брать с этого сервера файл вида TODAYDATE_smtng_important.txt, где TODAYDATE – текущая дата вида YYYYMMDD,
  • файлик лежит в корневой директории.

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

Вот именно здесь в автоматизации этой задачи поможет нам линуксовая консольная утилита expect.

создаем файл get_file.sh со следующим содержимым:

#!/usr/bin/expect
# Script to get important file from sftp quetly. Uses expect (yum install expect -y)
# Usage: ./get_file.sh YYYYMMDD
# where YYYYMMDD – today’s date

set today [lindex $argv 0]
spawn sftp -oPort=20022 user@192.168.1.2:${today}_smtng_important.txt /home/user/${today}_smtng_important.txt
expect “password:”
send “SECRET\n”;
interact

Строки после комментария:

  1. Создаем переменную today, присваивая ей первый аргумент, переданный скрипту при запуске.
  2. Выполняем команду для подключения к sftp, где вместо ${today} интерпретатор вставляет значение даты (переданное скрипту)
  3. Далее скрипт ждет указанного запроса (prompt) – здесь возможно много вариантов, в т. ч. и таймауты и различные prompt-ы, об этом можно прочитать в мане или в более расширенных статьях.
  4. Получив указанный выше промпт, посылаем в ответ наш пароль и знак окончания строки (аналог нажатия пользователем [Enter])
  5. Далее скрипт полностью передает управление пользователю.

Запускать скрипт, понятное дело после chmod o+x, например так:

./get_file.sh 20142902

Минусом изложенного подхода, конечно же, является хранение пароля для sftp в открытом, не зашифрованном виде.

Ссылки:

  1. Хорошая статья по командам expect
  2. Туториал по expect
  3. Ман по sftp в Linux
http://geckich.blogspot.com/

Как получить размер свободного дискового пространства точки монтирования в Python (Linux)

Чтобы получить размер свободного пространства для точки монтирования (или размер свободное место на разделе, где находится указанная папка), необходимо выполнить следующее:

import os
st = os.statvfs("/home")
du = st.f_bsize * st.f_bavail
print(du)

Получим значение в байтах, чтобы получить значение, например, в Мб:

du = st.f_bsize * st.f_bavail / 1024 / 1024
http://geckich.blogspot.com/

Полезные Bash Shell команды и однострочные скрипты

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

Статья будет дополняться.

1. Сделать что-либо со всеми файлами в папке, например, распаковать все zip архивы в текущей папке:

for f in *.zip ; do unzip $f ; done

2. То же самое, только с файлами из поддиректорий текущей. Например, установить rpm пакеты:

for D in *; do [ -d “${D}” ] && yum install ${D}/*.rpm ; done

3. Добавить пользователя в группу:

usermod -a -G group user

4. Как расшарить содержимое папки по протоколу HTTP.
Заходим в нужную папку и выполняем команду:

python -m SimpleHTTPServer 8080

Далее зайдя в браузере по адресу http://ip_of_that_server:8080, мы увидим содержимое с возможностью навигации и скачивания файлов:

5. Получение информации о Virtual Hosts для apache2 в Debian Linux:

apache2ctl -t -D DUMP_VHOSTS

6. Создание файла заданного размера:

truncate -s 14M filename

7. Замена строчек в текстовом файле:

sed -Ei ‘s/foo|bar|baz/foobar/g’ file

Заменить foo, bar или baz на foobar

8. Подмонтировать CD-ROM с указанием типа файловой системы:

mount -t iso9660 /dev/scd0 /media/cdrom

9. Посмотреть параметры загрузки ядра Linux:
cat /proc/cmdline
10. Максимальный уровень компрессии в tar:
env GZIP=-9 tar cvzf file.tar.gz /path/to/directory

Проверка доступности портов между двумя Linux-машинами (для Solaris тоже работает)

Бывают ситуации, когда нам необходимо проверить видимость TCP портов одной машины для другой. Казалось бы всё просто – telnet. А если на серверной машине еще не установлено или не запущено приложение, слушающее нужный порт? Я уверен, что существует масса способов, но мне было проще написать 2 маленьких скрипта на Python.

Один скрипт – сервер. Слушает заданный порт и пишет в консоль информацию о клиентах. А второй – клиентский скрипт – стучится по заданному IP в заданный порт и возвращает ответ от сервера (если порт доступен).

Серверный скрипт.

#!/usr/bin/env python
import socket, sys
# Server address and buffer size
TCP_IP = '0.0.0.0'
BUFFER_SIZE = 1024
# Usage string
usage = "server.py PORT"
# Check number of arguments and print usage if not enough arguments
if (len(sys.argv) < 2):
print(usage)
sys.exit(0)
TCP_PORT = int(sys.argv[1])
# Create socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(1)
# Listen for client connection until Ctrl+C will be pressed
while 1:
c = s.accept()
cli_sock, cli_addr = c
cli_sock.send("Hello, person from %s"%(str(cli_addr)))
cli_sock.close()
print("received data from:", str(cli_addr))


Запуск. Копируем в текстовый файл вышенаписанный код и запускаем:

python server.py <port>

Клиентский скрипт.

#!/usr/bin/env python
import socket, sys
usage = "client.py IP PORT"
# Check number of arguments and print usage if not enough arguments
if (len(sys.argv) < 3):
print(usage)
sys.exit(0)
TCP_IP = sys.argv[1]
TCP_PORT = int(sys.argv[2])
BUFFER_SIZE = 1024
# Create socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))
# Recieving response from the server
data = s.recv(BUFFER_SIZE)
s.close()
print("received data:", data)

Запуск. Копируем в текстовый файл вышенаписанный код и запускаем:

python client.py <server_IP> <server_port>

http://geckich.blogspot.com/

Python, извлечение параметров из Post или Get запросов.

Извлекать параметры, введенные в форму (get или post), довольно легко. В этом поможет следующая конструкция:

from mod_python import util

def handler(req):
form_data = util.FieldStorage(req, keep_blank_values=1)
myparameter = form_data.getfirst("parameter")

В myparameter теперь находится значение того, что было передано с именем “parameter”. Например, того, что было в ведено в “EditBox1”.

http://geckich.blogspot.com/

SQLAlchemy в Python

В python для работы с MySQL есть библиотечка SQLAlchemy.

У неё много всяких фишек-плюшек, но я в подавляющем большинстве случаев использую конструкции такого типа:

from sqlalchemy import create_engine
b_connection_string = 'mysql://user:password@localhost/db_name'
user = "admin"
db = create_engine(db_connection_string)
for row in db.execute("select Permissions from users where Name = "%s""%(user)):
result.append(str(row))
Где в 1й строчке мы импортируем функцию, затем создаем соединение с БД, а затем выполняем какой-то запрос. В итоге мы получим такую конструкцию (в данном примере):

[“(‘On, Off, Bla-bla’,)”]

Теперь можно облагородить результат, добавив , например:

result = result[0].replace("('","")
result = result.replace("',)","")
result = result.replace(", ", ",")

Получим в result:  ‘On, Off, Bla-bla’

http://geckich.blogspot.com/

Проверка балланса GSM-модема, подключенного к Asterisk и формирование лога звонков за прошедшие сутки с отправкой по e-mail на Python 3

Недавно был описанный в заголовке таск.

Т. к. я очень люблю питончик, то скрипты пишу на нём. В скрипте выполняется команда астериска (USSD-запрос), далее вытягивается содержимое ответа на ussd, вытягивается лог звонков за сутки, формируется письмо в виде html и отсылается на заданные адреса по электронной почте.

1. Импорты необходимых библиотек:

#!/usr/bin/env python3
import os, smtplib, time, subprocess, shlex, sys, csv
from email.utils import formatdate
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

2. Формируем шапку письма – кому, куда, что и зачем 🙂

# me == my email address
# you == recipient's email address
# cc == copy's address
me = "asterisk@example.com"
сс = "recipient1@example.com"
you = "recipient2@example.com"
# Create message container - the correct MIME type is multipart/alternative.
msg = MIMEMultipart('alternative')
msg['Subject'] = "GSM modem balance ("+time.strftime("%m/%d/%Y",time.localtime())+")"
msg['From'] = me
msg['To'] = you+", "+cc
msg['Date'] = formatdate(localtime=True)

Отправка USSD-запроса сосстояния балланса и обработка результата. Для посылки запроса использую вызов bash команды через subprocess.call – может быть и слишком криво, но быстро и работает.

Затем идет пауза в 10 сек – необходима, чтобы пришел ответ от оператора.
Далее читаем файлик с ответом (что за файлик и где его брать – настраиватьеся в extensions.conf астериска и я писал уже об этом) и делаем некоторые украшательства для html-письма.

# call ussd command
subprocess.call(["asterisk","-r","-x","dongle ussd dongle0 *111#"],stdin=None, stdout=None, stderr=None)
time.sleep(10)
exit()
# open file
f = open ("/var/log/asterisk/ussd.txt","r")
#Read whole file into data
ussd_data = f.read()
f.close()
# Format ussd data as html
replace1='Na rahunku:<strong><font color="red">'
replace2='grn.</font></strong><br>'
ussd_data=ussd_data.replace('Na rahunku:',replace1)
ussd_data=ussd_data.replace('grn.',replace2)

Здесь идет формирование html-таблицы из стандартного лога звонков. User1, User2 – это пользователи, от которых могут идти исходящие звонки, т. е. – все ваши пользователи. Это нужно для определения, в какую таблицу кидать запись – входящие или исходящие звонки. Алгоритм не оптимален, но это первое рабочее решение, пришедшее мне в голову и подходящее под мои требования.

#########call log part
#input log file path
ipath = '/var/log/asterisk/cdr-csv/Master.csv'
ArrMessg = []
OutCalls = []
InCalls = []
LastArr = []
nextmsg = []
#open log
ifile = open(ipath, "r")
#read file using csv module
reader = csv.reader(ifile)
for row in reader:
ArrMessg.append(row)
#if (row[9] == "0"):
date = time.strftime("%Y-%m-%d",time.localtime())
for row in ArrMessg:
nextmsg = row[9]
if (nextmsg[0:10] == date):
tmpmsg = []
tmpmsg.append(row[1])
tmpmsg.append(row[2])
tmpmsg.append(row[9])
tmpmsg.append(row[12])
if (row[1] in ["User1","User2","User3"]):
OutCalls.append(tmpmsg)
else:
InCalls.append(tmpmsg)
#########

Здесь идет формирование html-кода таблиц вх. и исх. звонков. Тут всё должно быть понятно – вставляем куда нужно теги.

# Attach letter into message container.
html = """
<html>
<head></head>
<body>
<p><br>
"""
html = html+ussd_data
html = html+"""
<br>
<br>
<strong><font color="blue">Incoming Calls:</font></strong><br>
<br>
<table width="60%" border="2" cellspacing="0" cellpadding="4">
<tr align="center" bgcolor="#999999">
<td>Source</td> <td>Extension</td> <td>Date</td> <td>Duration</td> <td>Note</td>
</tr>
"""
for row in InCalls:
tmp = str(row)
rep1 = '<tr align="center">'
tmp=tmp.replace("[",rep1)
tmp=tmp.replace("]","</tr>")
tmp=tmp.replace("',","</td>")
tmp=tmp.replace("'","<td>")
html = html+tmp
html = html+"</table>"

html = html+"""
<br>
<strong><font color="blue">Outgoing Calls:</font></strong><br>
<br>
<table width="60%" border="2" cellspacing="0" cellpadding="4">
<tr align="center" bgcolor="#999999">
<td>Source</td> <td>Destination</td> <td>Date</td> <td>Duration</td> <td>Note</td>
</tr>
"""
for row in OutCalls:
tmp = str(row)
rep1 = '<tr align="center">'
tmp=tmp.replace("[",rep1)
tmp=tmp.replace("]","</tr>")
tmp=tmp.replace("',","</td>")
tmp=tmp.replace("'","<td>")
html = html+tmp
html = html+"</table>"

html = html+"""
<br>
<br>
<br>
Best regards,<br>
Developers Team<br>
</p>
</body>
</html>
"""
#print (html)
ifile.close()

Отправка письма через сервер mail.example.com и очиска файлика с ussd.

# Record the MIME type
letter = MIMEText(html, 'html')
# Attach letter into message container.
msg.attach(letter)
# sendmail function takes 3 arguments: sender's address, recipient's address
# and message to send - here it is sent as one string.
s = smtplib.SMTP('mail.example.com')
s.sendmail(me, you, msg.as_string())
s.quit()
subprocess.call(["rm","-f","/var/log/asterisk/ussd.txt"])

Выжимка из /etc/asterisk/extensions.conf для настройки USSD:

[dongle-incoming]

exten => ussd,1,System(/bin/echo “${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M:%S)} – ${USSD}” >> /var/log/asterisk/ussd.txt)
exten => ussd,n,Hangup()

Т. е. здесть говорится, что если пришел ответ от сервера на экстеншн ussd, то выполнить команду оболочки – записать в такой-то файлик текущее время и тело ответа от оператора.

Чтобы посылать письмо утром, надо либо разделить скрипт на 2 части – чтобы одна чекала в 23:50, а вторая отсылала уже утром. Либо что-то мудрить с датами (мне проще было сделать 1й вариант).

http://geckich.blogspot.com/

Hello world! с помощью mod_python

Написание сайта с использованием mod_python состоит из двух этапов: конфигурирование сервера (apache) и собственно написание кода.

1. Конфигурация сервера:
Вначале установим модуль, если его еще нет:

aptitude install libapache2-mod-python
a2enmod python

Подредактируем, например, на Debian файл /etc/apache2/sites-available/default :

<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www
<Directory /var/www/>
Options Indexes MultiViews
AllowOverride None
Order allow,deny
allow from all
AddHandler mod_python .py #говорит апачу передавать обработку файлов *.py в mod_python
PythonHandler index #говорит, в каком файле находится обработчик хэндлеров.
DirectoryIndex index.py #указывает, какой файл отображается по-умолчанию в директории
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
AddHandler mod_python .py
PythonHandler mod_python.publisher
PythonDebug On #включает выведение ошибок в браузер - удобно пр иотладке
DirectoryIndex index.py
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Активируем виртуалхост (если он не активен) и перезапускаем веб-сервер, чтобы изменения встепили в силу:

a2ensite default
service apache2 restart

2. Написание кода:

from mod_python import apache          #импорт класса apache
def handler(req): #функция обработки хэндлера
req.content_type = "text/html" #задаем тип содержимого веб-страницы
req.send_http_header() #посылка дефолтового хэдера html-страницы
req.write("Hello World!") #отправляем Hello World! на страницу
return apache.OK #рендеринг страницы с кодом ОК

Называем файл index.py и сохраняем в директорию виртуал-хоста (в нашем примере это /var/www), не забывая сменить хозяина файла на www-data. Перед проверкой нужно удалить дефолтовый файл index.html из директории виртуалхоста и удалить русскоязычные буквы из питоновского файла – иначе будет ругаться.

Вот так выглядит Hello World на mod_python:

http://geckich.blogspot.com/