среда, 5 декабря 2012 г.

Сервер на Python

Напишем простой сервер на Python.

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import socket
  5.  
  6. host = "localhost"
  7. port = 44444
  8.  
  9. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. s.bind((host, port))
  12. s.listen(5)
  13. sock, addr = s.accept()
  14. while True:
  15. buf = sock.recv(1024)
  16. if buf == "exit":
  17. sock.send("bye")
  18. break
  19. elif buf:
  20. sock.send(buf)
  21. sock.close()

Разберем код нашего сервера.

Сначала в строке 4 мы подключаем модуль для работы с сокетами. Он содержит весь необходимый нам функционал.

Далее в строках 6 и 7 мы определим хост, на котором сервер будет ждать соединение, и порт, который он будет слушать.

9 строка создает сокет для Ipv4.

В 10 строке мы устанавливаем опцию повторного использования порта, чтобы не ждать пока он освободится после останова сервера.

Далее в строке 11 мы ассоциируем (биндим) сокет с хостом и портом.

В 12 строке мы указываем количество ожидающих обработки запросов.

В строке 13 функция accept() переводит приложение в режим ожидания подключения клиента. При успешном подключении accept возвратит кортеж (пару) из объекта соединения и адреса клиента. Полученный объект мы и будем использовать для взаимодействия с клиентом.

В строке 14 мы запускаем вечный цикл while, в котором читаем из объекта отправленные данные блоками указанной в 15 строке величины (в данном случае 1024).

В строках с 16 по 20 мы проверяем полученные данные. Если клиент прислал слово exit, то мы отправляем ему bye и выходим из цикла, закрывая соединение. Если же принятые данные не exit, то отправляем их обратно.

Теперь мы напишем клиент к нашему серверу на Python.
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import socket
  5.  
  6. host = "localhost"
  7. port = 44444
  8.  
  9. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. s.connect((host, port))
  11. while True:
  12. buf = raw_input(">>")
  13. s.send(buf)
  14. result = s.recv(1024)
  15. print result
  16. if buf == "exit":
  17. break
  18. s.close()
Разберем код клиента.

В начале все как у сервера. Создаём сокет, биндим к адресу и порту сервера. Далее в цикле организуем что то вроде чата с сервером.

В 12 строке читаем введенные с клавиатуры данные. Отправляем их серверу, получаем ответ и выводим в консоль.

Далее, если мы отправили серверу команду exit, то выходим из клиента или же переходим к следующей итерации цикла, возвращаясь к приему данных с клавиатуры.

Запустите на разных терминалах клиент с сервером на Python и попробуйте протестировать их взаимодействие.

Сервер получился самый простой. После закрытия соединения клиентом сервер сам закрывается.

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


Код многопоточный сервера на Python.
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import socket
  5. import threading
  6.  
  7. host = "localhost"
  8. port = 44444
  9.  
  10. class Connect(threading.Thread):
  11. def __init__(self, sock, addr):
  12. self.sock = sock
  13. self.addr = addr
  14. threading.Thread.__init__(self)
  15. def run (self):
  16. while True:
  17. buf = self.sock.recv(1024)
  18. if buf == "exit":
  19. self.sock.send("bye")
  20. break
  21. elif buf:
  22. self.sock.send(buf)
  23. self.sock.close()
  24.  
  25. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  26. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  27. s.bind((host, port))
  28. s.listen(5)
  29. while True:
  30. sock, addr = s.accept()
  31. Connect(sock, addr).start()
Разберем код многопоточного сервера.

Сначала мы подключили модуль threading.

В 10 строке создаём класс Connect, являющийся наследником класса threading.Thread, в котором описываем взаимодействие с клиентом переопределив родительский метод run(). Именно этот метод создает отдельный поток и выполняет в нём своё содержимое.

Далее после создания и бинда сокета запускаем цикл в котором ожидаем подключение.

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

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

Воспользовавшись модулем pickle можно консервировать и отправлять любые структуры данных: объекты, списки и так далее.

Другой пример кода сервера на Python приведен ниже.

Код сервера.

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

# Импортируем необходимые библиотеки
import socket
import threading

# Указываем IP адрес и порт, на котором будет сервер
host = "127.0.0.1"
port = 12815

# Создаем класс-потомок threading.Thread в котором
# описываем взаимодействие с клиентом переопределив родительский
# метод run(). Именно этот метод создаёт отдельный поток и выполняет
# в нём своё содержимое.

class Connect(threading.Thread):
    def __init__(self, sock, addr):
        self.sock = sock
        self.addr = addr
        threading.Thread.__init__(self)
    def run (self):
        while True:
            buf = self.sock.recv(1024)
            # Если клиент запросил справку - выводим её
            if buf == 'help':
            sock.send('\nThis is help!')
         # Если же прислал exit - разрываем соединение
            elif buf == 'exit':
                sock.send('Bye')
                break
            # Неопределенная команда вернёт её обратно
            elif buf:
                self.sock.send('Dont know what is '+buf)
        self.sock.close()

# создаем и биндим на порту сокет
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(5)

# В этом цикле ожидаем подключения клиента вне зависимости от состояния
# предыдущих подключений
while True:
    sock, addr = s.accept()
    Connect(sock, addr).start()
Код клиента.

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

# Импортируем необходимые библиотеки
import socket

# Указываем IP адрес и порт для подключения
host = "127.0.0.1"
port = 12815

# Приветственное сообщение
print 'Welcome to our superserver Client!\n\r'

# Задаем способ подключения и подключаемся к серверу по IP и порту
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

# Циклически "болтаем" с сервером о том и о сем :)
# А если вернее - принимаем команды, пока не будет exit
while True:
   # получаем команду от пользователя
    buf = raw_input('serv: >> ')
    # отсылаем серверу
    s.send(buf)
    # получаем и выводим ответ
    result = s.recv(1024)
    print result
    # елси команда была exit - разъединяемся
    if buf == "exit":
        break
s.close()
Далее представим код сервера на Python, предназначенного для обработки на нем загруженных данных. В задаче необходимо получить от пользователя exe-файл, на сервере его обработать нужными программами и выдать результат, то есть обработанный файл для скачивания.

На Python такой тип программы можно реализовать достаточно просто, используя стандартные заготовки и библиотеки, такие как BaseHTTPServer.

Ниже следует листинг сервера с краткими комментариями.

# -*- coding: utf-8 -*-

import cgi
from os import curdir, sep
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import subprocess

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            if self.path != "/output.exe":
                f = open(curdir+sep+"upload.html")
                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                self.wfile.write(f.read())
                f.close()
            else:
                self.send_response(200)
                self.send_header("Content-type", "application/octet-stream")
                self.end_headers()
                self.wfile.write(open(curdir+sep+"output.exe", "rb").read())
        except IOError:
            self.send_error(404,"File Not Found: %s" % self.path)

    def do_POST(self):
        try:
            ctype, pdict = cgi.parse_header(self.headers.getheader("content-type"))
            if ctype == "multipart/form-data":
                query = cgi.parse_multipart(self.rfile, pdict)
            self.send_response(200)
            self.end_headers()
            upfile = query.get("file")

            f = open(curdir+sep+"output.exe", "wb")
            f.write(upfile[0])
            f.close()

            params = " np output.exe"

            p = query.get("encryption")
            if p[0] == "aes":
                params += " sf 1"
            elif p[0] == "rc5":
                params += " sf 2"
            elif p[0] == "xor":
                params += " sf 3"
            else:
                params += " sf 0"

            p = query.get("hw_bind")
            if p[0] == "yes":
                p = query.get("hw_bind_serial")
                assert len(p[0]) == 8
                params += " sn " + p[0]
            else:
                params += " sn 0"

            p = query.get("passwd")
            assert len(p[0]) > 0
            params += " pass " + p[0]

            p = query.get("pack")
            if p[0] == "yes":
                params += " pack 1"
            else:
                params += " pack 0"

            pipe = subprocess.Popen("processor.exe "+params, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            pipe.stdin.close()
            pipe.wait()

            self.wfile.write('<a href="/output.exe">Download results</a>.');
        except :
            pass

if __name__ == "__main__":
    try:
        server = HTTPServer(("", 8080), MyHandler)
        print "started httpserver..."
        server.serve_forever()
    except KeyboardInterrupt:
        print "^C received, shutting down server"
        server.socket.close()

Итак, для начала импортируем из библиотеки BaseHTTPServer два класса: HTTPserver - собственно сам сервер и BaseHTTPRequestHandler - класс, который служит для обработки запросов от пользователей.

Объявляем свой класс MyHandler, который наследуется от BaseHTTPRequestHandler. И перегружаем в нём два метода: do_GET и do_POST для обработки данных, переданных методом GET и POST соответственно.

В первом обработчике проверяем запрос от пользователя (содержится в self.path). Если запрашиваемый документ не является /output.exe, то выдаём форму для загрузки файла, иначе считываем и выдаём output.exe. Так же перед этим посылается ответ от сервера с кодом 200, что соответствует успешной обработке запроса от клиента и передаем заголовок, в котором указан тип содержимого (text/html для html-код или application/octet-stream для бинарного файла).

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

Далее в самой программе создаем сервер, указав при этом порт, на котором он будет работать (в примере это 8080) и класс, который необходимо использовать для обработки запросов от пользователей.

Недостаток заключается в том, что данная реализация не сможет обработать одновременную загрузку файлов от разных пользователей, так как. в лучшем случае просто один из запросов затрет файл output.exe, полученный в другом запросе.

Так же приведем код html-файла upload.html, который должен располагаться в той же директории, что и сервер на Python.

<form action="/" enctype="multipart/form-data" method="post">
<table border="1">
<tbody>
<tr>
<td>Encryption:</td>
<td>
<input checked="checked" name="encryption" type="radio" value="none" />None
<input name="encryption" type="radio" value="aes" />AES
<input name="encryption" type="radio" value="rc5" />RC5
<input name="encryption" type="radio" value="xor" />XOR</td>
</tr>
<tr>
<td>Hardware binding:</td>
<td>
<input checked="checked" name="hw_bind" type="radio" value="no" />No
<input name="hw_bind" type="radio" value="yes" />Yes
<input name="hw_bind_serial" value="media serial number" /></td>
</tr>
<tr>
<td>Password:</td>
<td>
<input name="passwd" type="password" /></td>
</tr>
<tr>
<td>Pack:</td>
<td>
<input checked="checked" name="pack" type="radio" value="no" />No
<input name="pack" type="radio" value="yes" />Yes</td>
</tr>
<tr>
<td colspan="2">
<input name="file" type="file" />
<input type="submit" value="Отправить" /></td>
</tr>
</tbody></table>
</form>

Надеемся, что приведенные примеры создания серверов на Python помогут вам в вашей работе.


понедельник, 3 декабря 2012 г.

Putty Команды

Putty просто отображает окно для ввода команд в операционной системе Windows для дистанционного управления компьютером, находящимся под управлением операционной системы Linux.
Команды, которые вы вводите, - это обычные команды Linux, а не особые команды Putty. Вот некоторые из них:



ls - показывает список директорий (папок):

Пример:

ls
handbook-draft.pdf  iso

ls -lh
total 3.4M
-rw-r--r-- 1 pavlo pavlo 3.4M 2007-05-15 05:53 handbook-draft.pdf
drwxr-xr-x 2 pavlo pavlo 4.0K 2007-04-10 00:25 iso

ls iso/
rhel-5-client-x86_64-disc6.iso

cd - перейти в какую-либо директорию (папку):

Пример:

cd iso

cp - скопировать файл:

Пример:

cp ../handbook-draft.pdf .

mv - переместить файл куда-либо или переименовать его:

Пример:

mv handbook-draft.pdf howto.pdf

rm - удалить файл:


Пример:

rm handbook-draft.pdf

mkdir - создать новую директорию (папку):

Пример:

mkdir new

pwd - показать адрес той директорий (папки), в которой вы сейчас находитесь:

Пример:

pwd
/home/pavlo/example/iso

whoami - определить под именем какого пользователя вы в работаете сейчас:

Пример:

whoami
pavlo

date - отобразить текущую дату и время:

Пример:

date
Tue May 15 06:07:45 UTC 2007

Некотрые комманды зависят от версии операционной системы, такие как apt в Debian, которая скачивает и устанавливает программы:

Пример:

apt-get install expect

man - открывает подсказку с документацией по всем командам:

Пример:

man expect

q - выход из программы (например, если вы закончили чтение документации man, то введите q to для выхода).


Полный перечень команд от A до Z для Linux представлен ниже:

a
  alias    Create an alias •
  apropos  Search Help manual pages (man -k)
  apt-get  Search for and install software packages (Debian/Ubuntu)
  aptitude Search for and install software packages (Debian/Ubuntu)
  aspell   Spell Checker
  awk      Find and Replace text, database sort/validate/index
b
  basename Strip directory and suffix from filenames
  bash     GNU Bourne-Again SHell
  bc       Arbitrary precision calculator language
  bg       Send to background
  break    Exit from a loop •
  builtin  Run a shell builtin
  bzip2    Compress or decompress named file(s)
c
  cal      Display a calendar
  case     Conditionally perform a command
  cat      Concatenate and print (display) the content of files
  cd       Change Directory
  cfdisk   Partition table manipulator for Linux
  chgrp    Change group ownership
  chmod    Change access permissions
  chown    Change file owner and group
  chroot   Run a command with a different root directory
  chkconfig System services (runlevel)
  cksum    Print CRC checksum and byte counts
  clear    Clear terminal screen
  cmp      Compare two files
  comm     Compare two sorted files line by line
  command  Run a command - ignoring shell functions •
  continue Resume the next iteration of a loop •
  cp       Copy one or more files to another location
  cron     Daemon to execute scheduled commands
  crontab  Schedule a command to run at a later time
  csplit   Split a file into context-determined pieces
  cut      Divide a file into several parts
d
  date     Display or change the date & time
  dc       Desk Calculator
  dd       Convert and copy a file, write disk headers, boot records
  ddrescue Data recovery tool
  declare  Declare variables and give them attributes •
  df       Display free disk space
  diff     Display the differences between two files
  diff3    Show differences among three files
  dig      DNS lookup
  dir      Briefly list directory contents
  dircolors Colour setup for `ls'
  dirname  Convert a full pathname to just a path
  dirs     Display list of remembered directories
  dmesg    Print kernel & driver messages
  du       Estimate file space usage
e
  echo     Display message on screen •
  egrep    Search file(s) for lines that match an extended expression
  eject    Eject removable media
  enable   Enable and disable builtin shell commands •
  env      Environment variables
  ethtool  Ethernet card settings
  eval     Evaluate several commands/arguments
  exec     Execute a command
  exit     Exit the shell
  expect   Automate arbitrary applications accessed over a terminal
  expand   Convert tabs to spaces
  export   Set an environment variable
  expr     Evaluate expressions
f
  false    Do nothing, unsuccessfully
  fdformat Low-level format a floppy disk
  fdisk    Partition table manipulator for Linux
  fg       Send job to foreground
  fgrep    Search file(s) for lines that match a fixed string
  file     Determine file type
  find     Search for files that meet a desired criteria
  fmt      Reformat paragraph text
  fold     Wrap text to fit a specified width.
  for      Expand words, and execute commands
  format   Format disks or tapes
  free     Display memory usage
  fsck     File system consistency check and repair
  ftp      File Transfer Protocol
  function Define Function Macros
  fuser    Identify/kill the process that is accessing a file
g
  gawk     Find and Replace text within file(s)
  getopts  Parse positional parameters
  grep     Search file(s) for lines that match a given pattern
  groupadd Add a user security group
  groupdel Delete a group
  groupmod Modify a group
  groups   Print group names a user is in
  gzip     Compress or decompress named file(s)
h
  hash     Remember the full pathname of a name argument
  head     Output the first part of file(s)
  help     Display help for a built-in command •
  history  Command History
  hostname Print or set system name
i
  iconv    Convert the character set of a file
  id       Print user and group id's
  if       Conditionally perform a command
  ifconfig Configure a network interface
  ifdown   Stop a network interface
  ifup     Start a network interface up
  import   Capture an X server screen and save the image to file
  install  Copy files and set attributes
j
  jobs     List active jobs •
  join     Join lines on a common field
k
  kill     Stop a process from running
  killall  Kill processes by name
l
  less     Display output one screen at a time
  let      Perform arithmetic on shell variables •
  ln       Make links between files
  local    Create variables •
  locate   Find files
  logname  Print current login name
  logout   Exit a login shell •
  look     Display lines beginning with a given string
  lpc      Line printer control program
  lpr      Off line print
  lprint   Print a file
  lprintd  Abort a print job
  lprintq  List the print queue
  lprm     Remove jobs from the print queue
  ls       List information about file(s)
  lsof     List open files
m
  make     Recompile a group of programs
  man      Help manual
  mkdir    Create new folder(s)
  mkfifo   Make FIFOs (named pipes)
  mkisofs  Create an hybrid ISO9660/JOLIET/HFS filesystem
  mknod    Make block or character special files
  more     Display output one screen at a time
  mount    Mount a file system
  mtools   Manipulate MS-DOS files
  mtr      Network diagnostics (traceroute/ping)
  mv       Move or rename files or directories
  mmv      Mass Move and rename (files)
n
  netstat  Networking information
  nice     Set the priority of a command or job
  nl       Number lines and write files
  nohup    Run a command immune to hangups
  notify-send  Send desktop notifications
  nslookup Query Internet name servers interactively
o
  open     Open a file in its default application
  op       Operator access
p
  passwd   Modify a user password
  paste    Merge lines of files
  pathchk  Check file name portability
  ping     Test a network connection
  pkill    Stop processes from running
  popd     Restore the previous value of the current directory
  pr       Prepare files for printing
  printcap Printer capability database
  printenv Print environment variables
  printf   Format and print data •
  ps       Process status
  pushd    Save and then change the current directory
  pwd      Print Working Directory
q
  quota    Display disk usage and limits
  quotacheck Scan a file system for disk usage
  quotactl Set disk quotas
r
  ram      ram disk device
  rcp      Copy files between two machines
  read     Read a line from standard input •
  readarray Read from stdin into an array variable •
  readonly Mark variables/functions as readonly
  reboot   Reboot the system
  rename   Rename files
  renice   Alter priority of running processes
  remsync  Synchronize remote files via email
  return   Exit a shell function
  rev      Reverse lines of a file
  rm       Remove files
  rmdir    Remove folder(s)
  rsync    Remote file copy (Synchronize file trees)
s
  screen   Multiplex terminal, run remote shells via ssh
  scp      Secure copy (remote file copy)
  sdiff    Merge two files interactively
  sed      Stream Editor
  select   Accept keyboard input
  seq      Print numeric sequences
  set      Manipulate shell variables and functions
  sftp     Secure File Transfer Program
  shift    Shift positional parameters
  shopt    Shell Options
  shutdown Shutdown or restart linux
  sleep    Delay for a specified time
  slocate  Find files
  sort     Sort text files
  source   Run commands from a file `.'
  split    Split a file into fixed-size pieces
  ssh      Secure Shell client (remote login program)
  strace   Trace system calls and signals
  su       Substitute user identity
  sudo     Execute a command as another user
  sum      Print a checksum for a file
  suspend  Suspend execution of this shell •
  symlink  Make a new name for a file
  sync     Synchronize data on disk with memory
t
  tail     Output the last part of file
  tar      Tape ARchiver
  tee      Redirect output to multiple files
  test     Evaluate a conditional expression
  time     Measure Program running time
  times    User and system times
  touch    Change file timestamps
  top      List processes running on the system
  traceroute Trace Route to Host
  trap     Run a command when a signal is set(bourne)
  tr       Translate, squeeze, and/or delete characters
  true     Do nothing, successfully
  tsort    Topological sort
  tty      Print filename of terminal on stdin
  type     Describe a command •
u
  ulimit   Limit user resources •
  umask    Users file creation mask
  umount   Unmount a device
  unalias  Remove an alias •
  uname    Print system information
  unexpand Convert spaces to tabs
  uniq     Uniquify files
  units    Convert units from one scale to another
  unset    Remove variable or function names
  unshar   Unpack shell archive scripts
  until    Execute commands (until error)
  uptime   Show uptime
  useradd  Create new user account
  userdel  Delete a user account
  usermod  Modify user account
  users    List users currently logged in
  uuencode Encode a binary file
  uudecode Decode a file created by uuencode
v
  v        Verbosely list directory contents (`ls -l -b')
  vdir     Verbosely list directory contents (`ls -l -b')
  vi       Text Editor
  vmstat   Report virtual memory statistics
w
  wait     Wait for a process to complete •
  watch    Execute/display a program periodically
  wc       Print byte, word, and line counts
  whereis  Search the user's $path, man pages and source files for a program
  which    Search the user's $path for a program file
  while    Execute commands
  who      Print all usernames currently logged in
  whoami   Print the current user id and name (`id -un')
  wget     Retrieve web pages or files via HTTP, HTTPS or FTP
  write    Send a message to another user
x
  xargs    Execute utility, passing constructed argument list(s)
  xdg-open Open a file or URL in the user's preferred application.
  yes      Print a string until interrupted
  .        Run a command script in the current shell
  !!       Run the last command again
  ###      Comment / Remark

Команды помеченные • являются встроенными командами. Они доступны во всех системах.

вторник, 13 ноября 2012 г.

Grab:Spider - Python фреймворк для парсинга сайтов

Модуль Spider это фреймворк позволяющий описать парсер сайта как набор функций обработчиков, где каждый обработчик отвечает за специфичный тип запроса. Например, при парсинге форума у вас будут обработчики для главной страницы, страницы подфорума, страницы топика, страницы профиля участника. Изначально такая структура парсера была разработана в силу ограничений асинхронного режима, но впоследствии оказалось, что писать парсеры в таком структурированном виде (один запрос - одна функция) очень удобно.

Модуль Spider работает асинхронно. Это значит что всегда есть только один рабочий поток программы. Для множественных запросов не создаются ни треды, ни процессы. Все созданные запросы обрабатываются библиотекой multicurl. Суть асинхронного подхода в том, что программа создаёт сетевые запросы и ждёт сигналы о готовности ответа на эти запроссы. Как только готов ответ, то вызывается функция-обработчик, которую мы привязали к конкретному запросу. Асинхронный подход позволяет обрабатывать большее количество одновременных соединений чем подход, связанный с созданием тредов или процессов т.к. память занята всего одним процессом и процессору не нужно постоянно переключаться между множество процессов.

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

Каждая функция-обработчки получает два входных аргумента. Первый аргумент — это объект Grab, в котором хранится информация о сетевом ответе. Вся прелесть Spider модуля в том, что он сохранил знакомый вам интерфейс для работы с синхронными запросами. Второй аргумент функции-обработчика это Task объект. Task объекты создаются в Spideer для того, чтобы добавить в очередь сетевых запросов новое задание. С помощью Task объекта можно сохранять промежуточные данные между множественными запросами.

У threading-подхода к парсинга сайтов есть плюсы и минусы. Плюс в том, что мы запускаем отдельный поток(thread) и делаем в нём, что хотим: можем делать последовательно несколько сетевых вызовов и всё это в пределах одного контекста - никуда не надо переключаться, что-то запоминать и вспоминать. Минус в том, что треды тормозят и жрут память.

Какие альтернативы?

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

Так вот, я написал интерфейс к multicurl - это часть библиотеки pycurl, которая позволяет работать с сетью асинхронно. Я выбрал multicurl, потому что Grab использует pycurl и я подумал, что мне удастся использовать его и для работы с multicurl. Архитектура парсеров на базе Grab:Spider весьма похожа на парсеры на базе фреймворка scrapy, что, в общем, не удивительно и логично.

Приведу пример простейшего паука:

# coding: utf-8
from grab.spider import Spider, Task

class SimpleSpider(Spider):
    initial_urls = ['http://ya.ru']

    def task_initial(self, grab, task):
        grab.set_input('text', u'ночь')
        grab.submit(make_request=False)
        yield Task('search', grab=grab)

    def task_search(self, grab, task):
        for elem in grab.xpath_list('//h2/a'):
            print elem.text_content()


if __name__ == '__main__':
    bot = SimpleSpider()
    bot.run()
    print bot.render_stats()

Что тут происходит? Для каждого URL в "self.initial_urls" создаётся задание с именем "initial". После того, как multicurl скачивает документ, вызывается обработчик с именем "task_initial". Самое главное, это то, что внутри обработчика мы получаем Grab-объект связанный с запрошенным документом. В результате мы можем использовать практические любые функции из Grab API. В данном примере, мы используем его работу с формами. Обратите внимание, нам нужно указать параметр "make_request=False", чтобы форма не отсылалась тут же, ибо мы хотим, чтобы этот сетевой запрос был обработан асинхронно.

Работа с Grab:Spider сводится к генерации запросов с помощью Task объектов и дальнейшей их обработке в специальных методах. У каждого задания есть имя, именно по нему потом выбирается метод для обработки запрошенного сетевого документа.

Создать Task объект можно двумя способами. Простой способ:

Task('foo', url='http://google.com')

После того, как документ будет полностью скачан из сети, будет вызван метод с именем "task_foo".

Более сложный способ:

g = Grab()
g.setup(....настраиваем запрос как угодно...)
Task('foo', grab=g)

Этим способом мы можем настроить параметры запроса в соответствии с нашими нуждами: выставить куки, специальные заголовки, сгенерировать POST-запрос.

В каких местах можно создавать запросы?
В любом методе-обработчике можно сделать yield Task объекта и он будет добавлен в асинхроннную очередь для скачивания. Также можно вернуть Task объект через return. Кроме того есть ещё два пути генерации Task объектов.

1) Можно указать в аттрибуте "self.initial_urls" список адресов и для них будут созданы задания с именем "initial".

2) Можно определить метод "task_generator" и yield'ить в нём сколько угодно запросов. Причём новые запросы из него будут браться по мере выполнения старых. Это позволяет без проблем проитерировать по миллиону строк из файла и не засорять ими всю память.

Первоначально я планировал сделать обработку извлечённых данных как в scrapy. Там это сделано с помощю Pipeline-объектов. Например, вы получили страницу с фильмом, пропарсили её и вернули Pipeline объект с типом Movie. А ещё предварительно вы написали в конфиге что Movie Pipeline должен сохраняться в базу данных или в CSV-файл. Как-то так. На практике оказалось, что проще не заморачиваться с дополнительной обёрткой и писать данные в БД или в файл сразу в методе обработчике запроса. Конечно, это не будет работать в случае распараллеливания методов по облаку машин, но до этого момента ещё надо дожить, а пока удобнее делать всё непосредственно в методе обработчике.

Task-объекту можно передавать дополнительные аргументы. Например, мы делаем запрос в google поиск. Формируем нужный url и создаём Task объект: Task('search', url='...', query=query) Далее в методе "task_search" мы сможем узнать какой именно запрос мы искали, обратившись к аттрибуту "task.query"

Grab:spider автоматически пытается исправить сетевые ошибки. В случае network timeout он выполняет задание ещё раз. Количество попыток вы можете настраивать с помощью опции "network_try_limit" при создании Spider объекта.

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

Если вы используете Grab, то оцените поглядите модуль spider. Возможно, вам понравится. Если вы не знаете, что такое Grab, возможно вам лучше обратить внимание на фреймворк scrapy, так как он документирован в сто крат краше нежели Grab.

Рассмотрим пример простого парсера. Допустим, мы хотим зайти на сайт habrahabr.ru, считать заголовки последних новостей, далее для каждого заголовка найти картинку с помощью images.yandex.ru и сохранить полученные данные в файл:

# coding: utf-8
import urllib
import csv
import logging

from grab.spider import Spider, Task

class ExampleSpider(Spider):
    # Список страниц, с которых Spider начнёт работу
    # для каждого адреса в этом списке будет сгенерировано
    # задание с именем initial
    initial_urls = ['http://habrahabr.ru/']

    def prepare(self):
        # Подготовим файл для записи результатов
        # Функция prepare вызываетя один раз перед началом
        # работы парсера
        self.result_file = csv.writer(open('result.txt', 'w'))
        # Этот счётчик будем использовать для нумерации
        # найденных картинок, чтобы создавать им простые имена файлов.
        self.result_counter = 0

    def task_initial(self, grab, task):
        print 'Habrahabr home page'

        # Это функция обработчик для заданий с именем initial
        # т.е. для тех заданий, чтобы были созданы для
        # адреов указанных в self.initial_urls

        # Как видите интерфейс работы с ответом такой же
        # как и в обычном Grab
        for elem in grab.xpath_list('//h1[@class="title"]/a[@class="post_title"]'):
            # Для каждой ссылки-заголовка создадим новое задание
            # с именем habrapost
            # Обратите внимание, что мы создаём задания с помощью
            # вызова yield - это сделано исключительно ради красоты
            # По-сути это равносильно следующему коду:
            # self.add_task(Task('habrapost', url=...))
            yield Task('habrapost', url=elem.get('href'))

    def task_habrapost(self, grab, task):
        print 'Habrahabr topic: %s' % task.url

        # Эта функция, как вы уже догадываетесь
        # получает результаты обработки запросов, кооторые
        # мы создали для кадого хабратопика, найденного на
        # главной странице хабры

        # Для начала сохраним адрес и заголовк топика в массив
        post = {
            'url': task.url,
            'title': grab.xpath_text('//h1/span[@class="post_title"]'),
        }

        # Теперь создадим запрос к поиску картинок яндекса, обратите внимание,
        # что мы передаём объекту Task информацию о хабрапосте. Таким образом
        # в функции обработки поиска картинок мы будем знать, для какого именно
        # хабрапоста мы получили результат поиска картинки. Дело в том, что все
        # нестандартные аргументы конструктора Task просто запоминаются в созданном
        # объекте и доступны в дальнейшем как его атррибуты
        query = urllib.quote_plus(post['title'].encode('utf-8'))
        search_url = 'http://images.yandex.ru/yandsearch?text=%s&rpt=image' % query
        yield Task('image_search', url=search_url, post=post)

    def task_image_search(self, grab, task):
        print 'Images search result for %s' % task.post['title']

        # В этой функции мы получили результат обработки поиска картинок, но
        # это ещё не сама картинка! Это только список найденных картинок,
        # Теперь возьмём адрес первой картинки и создадим задание для её
        # скачивания. Не забудем передать информацию о хабрапосте, для которого
        # мы ищем картинку, эта информация хранится в `task.post`.
        image_url = grab.xpath_text('//div[@class="b-image"]/a/img/@src')
        yield Task('image', url=image_url, post=task.post)

    def task_image(self, grab, task):
        print 'Image downloaded for %s' % task.post['title']

        # Это последнняя функция в нашем парсере.
        # Картинка получена, можно сохранить результат.
        path = 'images/%s.jpg' % self.result_counter
        grab.response.save(path)
        self.result_file.writerow([
            task.post['url'].encode('utf-8'),
            task.post['title'].encode('utf-8'),
            path
        ])
        # Не забудем увеличить счётчик ответов, чтобы
        # следующая картинка записалась в другой файл
        self.result_counter += 1


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    # Запустим парсер в многопоточном режиме - два потока
    # Можно больше, только вас яндекс забанит
    # Он вас и с двумя то потоками забанит, если много будете его беспокоить
    bot = ExampleSpider(thread_number=2)
    bot.run()


Пример кода реального парсера для парсинга сайта.

from grab.spider import Spider, Task, Data
from grab.tools.logs import default_logging
from grab import Grab
import pymongo
from hashlib import sha1
import os
from grab.tools.rex import rex_cache

db = pymongo.Connection()['bestflashgames']

class FlashSpider(Spider):
    initial_urls = ['http://www.bestflashgames.com/categorieslist/']

    def prepare(self):
        self.parsed_games = []

    def get_id(self, url):
        return url.rstrip('/').split('/')[-1]

    def task_initial(self, grab, task):
        for url in grab.tree.xpath('//div[@class="figure"]/a/@href'):
            yield Task('category', url=grab.make_url_absolute(url))

    def task_category(self, grab, task):
        # Integrity
        cid = self.get_id(task.url)
        category = {'_id': cid}
        category = db.category.find_one(category) or category
        category['title'] = grab.css_text('.gallery b font')

        # Save, logging
        db.category.save(category)
        print category['title']

        # Task to parse list of games
        yield Task('category_page', url=task.url, category=category)

    def task_category_page(self, grab, task):
        # Games list
        for url in grab.tree.xpath('//div[@class="figure"]/a/@href'):
            url = grab.make_url_absolute(url, resolve_base=True)
            yield Task('game', url=url, category=task.category)

        ## Next page
        nav = grab.css('.navigation-list a.right', None)
        if nav is not None:
            yield Task(
               'category_page',
               url=grab.make_url_absolute(nav.get('href'), resolve_base=True),
               category=task.category)

    def task_game(self, grab, task):
        # Integrity
        if grab.xpath_exists('//strong[contains(text(), "Not found")]'):
            print 'GAME NOT FOUND'
            return
        gid = self.get_id(task.url)
        game = {'_id': gid}
        if game['_id'] in self.parsed_games:
            print 'Already parsed in this session'
            return
        game = db.game.find_one(game) or game
        game['title'] = grab.css_text('.head p span')

        # Parse categories
        cats = grab.tree.xpath('//div[@class="head"]/ul[1]/li/a/@href')
        game['categories'] = [self.get_id(x) for x in cats]
        game['description'] = grab.xpath_text(
            '//div[@class="post"]/b[text()="Description:"]/../text()', '')
        game['image_url'] = grab.xpath_text('//div[@class="code"]//img/@src', '')
        game['gameid'] = grab.rex_text(rex_cache('gameid=(\d+)'))
        game['url'] = task.url

        # Logging
        print 'GAME', game['title']
        print game['categories']
        print game['description']
        print game['image_url']

        # Save
        db.game.save(game)
        self.parsed_games.append(game['_id'])

        # Task to save game's image
        if not 'image' in game:
            yield Task('game_image', url=game['image_url'], game=game,
                       disable_cache=True)

        yield Task('swf', url='http://www.bestflashgames.com/site/getgame.php?id=%s' % game['gameid'],
                   game=game)

    def task_game_image(self, grab, task):
        # Show activity
        print 'DOWNLOAD %s' % task.url

        # Calculate hash from URL
        img_hash = sha1(task.url).hexdigest()
        img_dir = 'static/game/%s/%s' % (img_hash[:2], img_hash[2:4])

        # Prepare directory
        try:
            os.makedirs(img_dir)
        except OSError:
            pass

        # Find extension
        ext = task.url.split('.')[-1]
        if len(ext) > 4:
            ext = 'bin'

        # Save file
        img_path = os.path.join(img_dir, '%s.%s' % (img_hash, ext))
        grab.response.save(img_path)

        task.game['image'] = img_path
        db.game.save(task.game)

    def task_swf(self, grab, task):
        print 'SWF GATE', task.url
        try:
            url = grab.rex_text(rex_cache(
                'show_flash\(\'([^\']+)'))
        except IndexError:
            try:
                url = grab.rex_text(rex_cache(
                    'name="movie" value="(http://[^"]+)'))
            except IndexError:
                try:
                    url = grab.rex_text(rex_cache(
                        '<embed src="(http[^"]+)'))
                except IndexError, ex:
                    try:
                        url = grab.rex_text(rex_cache(
                            '<iframe src="(http[^"]+)'))
                    except IndexError, ex:
                        url = ''


        task.game['swf'] = url
        db.game.save(task.game)

class SwfSizeSpider(FlashSpider):
    initial_urls = None
    size = 0

    def task_generator(self):
        for game in db.game.find({'swf': {'$ne': ''}}):
            g = Grab()
            g.setup(url=game['swf'], method='head')
            yield Task('swf', grab=g, game=game, disable_cache=True)

    def task_swf(self, grab, task):
        size = int(grab.response.headers.get('Content-Length', 0))
        task.game['swf_size'] = size
        db.game.save(task.game)
        print size

    def shutdown(self):
        print 'Total size', self.size / float((1024 * 1024))

if __name__ == '__main__':
    default_logging()
    bot = SwfSizeSpider(
        thread_number=10,
        cache_db='bestflashgames',
        use_cache=True)
    bot.setup_proxylist('var/proxy.txt', 'http', auto_change=True)
    try:
        bot.run()
    except KeyboardInterrupt:
        pass
    print bot.render_stats()


Немного про инструментарий который помогает в работе.

В качестве рабочего браузера я использую FireFox с плагинами HttpFox (позволяет анализировать входящий/исходящий http-трафик), XPather (позволяет проверять xpath выражения), SQLite Manager (просмотр sqlite таблиц), код набираю в emacs, где активно использую сниппеты (YASnippets) для часто встречающихся конструкций.

Из-за специфики фрэймворка  на первом этапе сайт полностью (или если данных много то частично) сохраняется в локальный кэш на базе mongodb, что очень экономит время, так как считывание страниц идет из кэша.

Для работы с sql базами куда, как правило (реже в json/xml), нужно разложить данные мы используем ORM - SQLAlchemy.

Собственно сам фрэймворк Grab предполагает большую гибкость в построении проекта и контроль за своими действиями. Однако, последние несколько проектов хорошо ложились в следующую структуру, отлично знакомую тем кто занимается веб-разработкой:

1) models.py - описываю модели данных.
2) config.py - аналог settings.py из мира Django: настройки, инициализация orm.
3) /spiders/*.py - код пауков.
4) spider.py или project_name.py - главный файл проекта, по совместительству обычно реализует command-line интерфейс для запуска различных пауков, так как зачастую сайт парсится по частям.

В качестве примера не сильно оторванного от реальной жизни напишем парсер «Trending projects» и «Most popular Python projects» c open-source цитадели GitHub.

Сперва нужно описать модель.

class Item(Base):
    __tablename__ = 'item'

    sqlite_autoincrement = True
    id = Column(Integer, primary_key=True)

    title = Column(String(160))
    author = Column(String(160))
    description = Column(String(255))
    url = Column(String(160))

    last_update = Column(DateTime, default=datetime.datetime.now)

Далее, в файле config.py выполняется начальная инициализация orm, создание таблиц, константы и находится функция которая конструирует параметры запуска паука в зависимости от настроек (default_spider_params), которая обычно общая для всех пауков в проекте.

def init_engine():
    db_engine = create_engine(
        'sqlite+pysqlite:///data.sqlite', encoding='utf-8')
    Base.metadata.create_all(db_engine)
    return db_engine
   
db_engine = init_engine()
Session = sessionmaker(bind=db_engine)

def default_spider_params():
    params = {
        'thread_number': MAX_THREADS,
        'network_try_limit': 20,
        'task_try_limit': 20,
    }
    if USE_CACHE:
        params.update({
            'thread_number': 3,
            'use_cache': True,
            'cache_db': CACHE_DB,
            'debug_error' :True,
        })
       
    return params

В большинстве случаев нет необходимости использовать mongodb на сервере, поэтому удобно сделать кэш отключаемым. При деплое проекта я просто ставлю USE_CACHE = False и все отлично работает. SAVE_TO_DB используется чтобы резрешить/запретить запись данных в базу данных.

Собственно переходим к самому интересному у нас будет 2-а паука: первый будет парсить 5 репозиториев «Top Trending» проектов, а второй «Most watched Python».

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

Не будем пренебрегать ООП и напишем BaseHubSpider в котором определим 2-а метода save() и log_progress().

class BaseHubSpider(Spider):
    initial_urls = ['http://github.com']

    items_total = 0

    def save(self, data):
        if not SAVE_TO_DB:
            return
           
        session = Session()

        if not session.query(Item).filter_by(title=data['title']).first():
            obj = Item(**data)
            session.add(obj)
        session.commit()

    def log_progress(self, str):
        self.items_total += 1
        print "(%d) Item scraped: %s" % (self.items_total, str)

В реальном приложении весьма вероятно наличие функции разбора страницы в зависимости от каких-то параметров - названий полей которые на каждой странице разные в то время как xpath путь к ним практически одинаковый и так далее.

Например, как-нибудь так (это не рабочий пример, а просто иллюстрация для лучшего понимания):

    XPATH = u'//table[@class="standart-table table"]' + \
            u'//tr[th[text() = "%s"]]/td'

    values = (
        ('title', u'Наименование товара'),
        ('rating', u'Рейтинг'),
        ('categories', u'Категория товара'),
        ('description', u'Описание'),       
    )
   
    for db_field, field_title in values:
        try:
            data[db_field] = get_node_text(grab.xpath(
                XPATH % field_title, None)).strip()
        except AttributeError:
            data[db_field] = ''

https://github.com/istinspring/grab-default-project-example/blob/master/spiders/lang_python.py

Код паука который парсит и сохраняет в базу данных 20 самых популярных python проектов.

Обратите внимание на

        repos = grab.xpath_list(
            '//table[@class="repo"]//tr/td[@class="title"]/..')
        for repo in repos:
            data = {
                'author': repo.xpath('./td[@class="owner"]/a/text()')[0],
                'title': repo.xpath('./td[@class="title"]/a/text()')[0],}

repos = grab.xpath_list('') - возвращает список lxml объект, в то время как например grab.xpath('') возвращает первый элемент, так как xpath в данном случае метод объекта grab, то есть оперируя в цикле repo.xpath('./h3/a[1]/text()') - мы получаем список или исключение если lxml не смог найти xpath. Проще говоря, xpath от объекта grab и xpath от lxml объекта — разные вещи, в первом случае вернется первый элемент (или default или бросит exception), а во втором вернется список элементов ['something'].

Grab - Python библиотека для парсинга сайтов

Сводка недоступна. Нажмите эту ссылку, чтобы открыть запись.