Поиск по блогу

среда, 5 февраля 2014 г.

Проба кода grab и furl для задачи "Процессинг csv таблиц"

У меня в репозитории ждут своего часа с десяток бибилиотек для парсинга html страниц. Здесь мы посмотрим, как работает grab.
С одной стороны, автор старается быть последовательным: сначала мы подготовили соглашение о наименованиях (файлов и директорий), потом, вот уже третий пост, последовательно кодируем процессы работы с файлами CSV.
Здесь подошла очередь процесса сохранения скачаных файлов. Очевидно, что сначала файлы нужно скачать. Потому здесь рассмотрим пример с библиотекой grab.
Вставка из середины поста. Здесь мне просто необходимо сделать отступление... после вступления. Я начал этот пост неделю назад..., но пришлось ликвидровать собственную безграмотность в понимании кодировок (предыдущий пост). Естественно, здесь меня в первую очередь заботили кодировки скачиваемых файлов. Но оказалось, что в бибилиотеке Grab все эти вопрос решены (см. код класса Response в середине этого поста). Так что все до раздела "Объект response" можно пропустить. Я решил, что могу здесь попробовать подобрать фрагманты кода под мои задачи.
  1. Репозиторий Grab Bitbucket
  2. Документация Grab Grab - фреймворк для парсинга сайтов
  3. Google Grab Google Code
  4. Github Grab Unicode HOWTO
  5. Документации Python Standard Encodings
Вот типичный пример того, как не надо делать. Здесь автор взял чужой работающий пример и "воткнул туда" несколько кусков кода. Потом этот "временный" файл пролежал пару месяцев. И вот теперь надо вспоминать, что же он делал? Автор запускал его с дебаггером в редакторе Spyder, потому под кодом приведены копии сообщений из консоли Spyder
In []:
# -*- coding: utf-8 -*-
"""
Created on Sat Dec 28 14:37:02 2013

@author: kiss
"""
import urllib
import csv
import logging

from grab.spider import Spider, Task
from grab import Grab
# Добавимв импорт Selector from grab.selector import Selector
from furl import furl #url parser

class Spider_mail_pages_1(Spider):
# Define Dictionaries
    agegd = {"0":"Все", "1":"до 12","2":"12-18","3":"19-24","4":"25-30","5":"31-35","6":"36-40","7":"41-45","8":"46-50","9":"старше 50"} 
    genderd = {"0":"All", "1":"Male","2":"Female"} 
    autod = {"mercedes-benz":"Mercedes", "toyota":"Toyota","volkswagen":"VW"}        
    def prepare(self):
        self.readme_file  = open('C:\\Users\\kiss\\Documents\\IPython Notebooks\\web\\Spyder\\result.txt', "wb")         
        self.result_write = csv.writer(self.readme_file)
    #### You have to edit this for every Spider  #########  
        base_url='http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=nissan&gender=1&agegroup=0&'
    # assign tulips with parts of URLs  
        all_auto=('mercedes-benz',)#,'nissan','toyota','volkswagen')#
        all_gender=('1','2')#There is not comma between '1''2'
        all_age=('4',)
    #### Warning: the number of URL = len( all_auto)*len( all_gender)*len( all_age)
        f = furl(base_url)
    # Выставляем счетчик и объявляем список  initial_urls 
    #  в него будем добаилять новые URL    
        self.result_counter = 0 # couner for URL list
        self.initial_urls=['NULL',] #URL list
        for auto1 in all_auto:
             f.args['filter']=auto1
             for g in all_gender:
                f.args['gender']=g
                for age in all_age:
                   f.args['agegroup']=age
                   print  '"это в цикле f.url= %s  /n' % f.url
                   self.initial_urls.append(f.url)
                   
        del self.initial_urls[0]# remove first NULL from URL list
        print  'self.initial_urls = %s ' % self.initial_urls        
        #return self.initial_urls
         
    # Список страниц, с которых Spider начнёт работу
    # для каждого адреса в этом списке будет сгенерировано
    # задание с именем initial
    #initial_urls = ['http://habrahabr.ru/']

    #initial_urls=prepare.initial_urls
    #print  'initial_urls = ' + self.initial_urls      

    def task_initial(self, grab, task):
        print 'Save body %s' % self.result_counter
        path = 'mail/mail_pages/%s.csv' % self.result_counter
        grab.response.save(path)
        self.folder_readme
        self.result_counter += 1# is to after initial_urls
     
    def folder_readme(self,grab):
        #path_fr = 'mail/mail_pages/readme.csv'
        fr= furl(grab.response.url)
        print "fr="+fr
        #ucsv="%s;%s;%s;%s;%s" % (fr.args['gender'],fr.args['gender'],fr.args['gender'],fr.args['gender'],fr.args['gender'])
        ucsv="98787"
        self.result_write.writerow(ucsv)
        
        self.readme_file.close()
        return None

    
     #def  open_readme (self):
     

    
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    # Запустим парсер в многопоточном режиме - два потока
    # Можно больше, только вас яндекс забанит
    # Он вас и с двумя то потоками забанит, если много будете его беспокоить
    bot = Spider_mail_pages_1(thread_number=2)
    bot.run()
In []:
Ниже сообщения из консоли Spyder (крякозябы из-за кириллицы в строке print  '"это в цикле f.url= %s  /n' % f.url)
In []:
>>> runfile('C:/Users/kiss/Documents/IPython Notebooks/web/Spyder/S-mail-csv-1.py', wdir=r'C:/Users/kiss/Documents/IPython Notebooks/web/Spyder')
"это в цикле f.url= http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=1&agegroup=4  /n
"это в цикле f.url= http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=2&agegroup=4  /n
self.initial_urls = ['http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=1&agegroup=4', 'http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=2&agegroup=4'] 
DEBUG:grab.spider.base:Using memory backend for task queue
DEBUG:grab.network:[01] GET http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=1&agegroup=4
DEBUG:grab.network:[02] GET http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=mercedes-benz&gender=2&agegroup=4
Save body 0
Save body 1
DEBUG:grab.spider.base:Job done!
Мы видим, что в окне консоли редактора есть проблемы с кодировкой. Пока для Python 2.x примем правило: "Строки Питона не поддерживают двухбайтные кодировки" и будем использовать только аглийский язык для комментариеа в скриптах. И будем переходит на третий Питон.
А ниже пример файла, с результатами работы скрипта, один из тех файлов, которые записывались в pages
Полный путь C:Notebooks
pages
In [1]:
filepath="C:\\Users\\kiss\\Documents\\IPython Notebooks\\web\\Spyder\\mail\\mail_pages\\0.csv" 
f = open(filepath)
In [2]:
f.encoding
Эта команда должна выдавать кодировку, но она подвисает... Не до нее, но оставим этот баг, как напоминание...
In [3]:
f.readlines
Out[3]:
<function readlines>
Так что скачаем на всякий случай документацию к объекту file (в з-ей версии такого объекта уже нет).
In [5]:
help (f)
Help on file object:

class file(object)
 |  file(name[, mode[, buffering]]) -> file object
 |  
 |  Open a file.  The mode can be 'r', 'w' or 'a' for reading (default),
 |  writing or appending.  The file will be created if it doesn't exist
 |  when opened for writing or appending; it will be truncated when
 |  opened for writing.  Add a 'b' to the mode for binary files.
 |  Add a '+' to the mode to allow simultaneous reading and writing.
 |  If the buffering argument is given, 0 means unbuffered, 1 means line
 |  buffered, and larger numbers specify the buffer size.  The preferred way
 |  to open a file is with the builtin open() function.
 |  Add a 'U' to mode to open the file for input with universal newline
 |  support.  Any line ending in the input file will be seen as a '\n'
 |  in Python.  Also, a file so opened gains the attribute 'newlines';
 |  the value for this attribute is one of None (no newline read yet),
 |  '\r', '\n', '\r\n' or a tuple containing all the newline types seen.
 |  
 |  'U' cannot be combined with 'w' or '+' mode.
 |  
 |  Methods defined here:
 |  
 |  __delattr__(...)
 |      x.__delattr__('name') <==> del x.name
 |  
 |  __enter__(...)
 |      __enter__() -> self.
 |  
 |  __exit__(...)
 |      __exit__(*excinfo) -> None.  Closes the file.
 |  
 |  __getattribute__(...)
 |      x.__getattribute__('name') <==> x.name
 |  
 |  __init__(...)
 |      x.__init__(...) initializes x; see help(type(x)) for signature
 |  
 |  __iter__(...)
 |      x.__iter__() <==> iter(x)
 |  
 |  __repr__(...)
 |      x.__repr__() <==> repr(x)
 |  
 |  __setattr__(...)
 |      x.__setattr__('name', value) <==> x.name = value
 |  
 |  close(...)
 |      close() -> None or (perhaps) an integer.  Close the file.
 |      
 |      Sets data attribute .closed to True.  A closed file cannot be used for
 |      further I/O operations.  close() may be called more than once without
 |      error.  Some kinds of file objects (for example, opened by popen())
 |      may return an exit status upon closing.
 |  
 |  fileno(...)
 |      fileno() -> integer "file descriptor".
 |      
 |      This is needed for lower-level file interfaces, such os.read().
 |  
 |  flush(...)
 |      flush() -> None.  Flush the internal I/O buffer.
 |  
 |  isatty(...)
 |      isatty() -> true or false.  True if the file is connected to a tty device.
 |  
 |  next(...)
 |      x.next() -> the next value, or raise StopIteration
 |  
 |  read(...)
 |      read([size]) -> read at most size bytes, returned as a string.
 |      
 |      If the size argument is negative or omitted, read until EOF is reached.
 |      Notice that when in non-blocking mode, less data than what was requested
 |      may be returned, even if no size parameter was given.
 |  
 |  readinto(...)
 |      readinto() -> Undocumented.  Don't use this; it may go away.
 |  
 |  readline(...)
 |      readline([size]) -> next line from the file, as a string.
 |      
 |      Retain newline.  A non-negative size argument limits the maximum
 |      number of bytes to return (an incomplete line may be returned then).
 |      Return an empty string at EOF.
 |  
 |  readlines(...)
 |      readlines([size]) -> list of strings, each a line from the file.
 |      
 |      Call readline() repeatedly and return a list of the lines so read.
 |      The optional size argument, if given, is an approximate bound on the
 |      total number of bytes in the lines returned.
 |  
 |  seek(...)
 |      seek(offset[, whence]) -> None.  Move to new file position.
 |      
 |      Argument offset is a byte count.  Optional argument whence defaults to
 |      0 (offset from start of file, offset should be >= 0); other values are 1
 |      (move relative to current position, positive or negative), and 2 (move
 |      relative to end of file, usually negative, although many platforms allow
 |      seeking beyond the end of a file).  If the file is opened in text mode,
 |      only offsets returned by tell() are legal.  Use of other offsets causes
 |      undefined behavior.
 |      Note that not all file objects are seekable.
 |  
 |  tell(...)
 |      tell() -> current file position, an integer (may be a long integer).
 |  
 |  truncate(...)
 |      truncate([size]) -> None.  Truncate the file to at most size bytes.
 |      
 |      Size defaults to the current file position, as returned by tell().
 |  
 |  write(...)
 |      write(str) -> None.  Write string str to file.
 |      
 |      Note that due to buffering, flush() or close() may be needed before
 |      the file on disk reflects the data written.
 |  
 |  writelines(...)
 |      writelines(sequence_of_strings) -> None.  Write the strings to the file.
 |      
 |      Note that newlines are not added.  The sequence can be any iterable object
 |      producing strings. This is equivalent to calling write() for each string.
 |  
 |  xreadlines(...)
 |      xreadlines() -> returns self.
 |      
 |      For backward compatibility. File objects now include the performance
 |      optimizations previously implemented in the xreadlines module.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  closed
 |      True if the file is closed
 |  
 |  encoding
 |      file encoding
 |  
 |  errors
 |      Unicode error handler
 |  
 |  mode
 |      file mode ('r', 'U', 'w', 'a', possibly with 'b' or '+' added)
 |  
 |  name
 |      file name
 |  
 |  newlines
 |      end-of-line convention used in this file
 |  
 |  softspace
 |      flag indicating that a space needs to be printed; used by print
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __new__ = <built-in method __new__ of type object>
 |      T.__new__(S, ...) -> a new object with type S, a subtype of T


Проблемы с открытием файлов с кириллицей решаются при помощи импорта библиотеки codecs. Обратите внимание, в выводе два варианта отображения строк (print line, repr(line)):
In [4]:
import codecs
f1 = codecs.open(filepath, encoding='cp1251')
for line in f1:
    print line, repr(line)
"Авто@Mail.Ru";85837;"http://auto.mail.ru"
u'"\u0410\u0432\u0442\u043e@Mail.Ru";85837;"http://auto.mail.ru"\n'

u'\n'
"Переходы";"%";"Страницы"
u'"\u041f\u0435\u0440\u0435\u0445\u043e\u0434\u044b";"%";"\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b"\n'
"327";"8,11";"http://cars.mail.ru/reviews/mercedes-benz/vito"
u'"327";"8,11";"http://cars.mail.ru/reviews/mercedes-benz/vito"\n'
"283";"7,02";"http://cars.mail.ru/reviews/mercedes-benz/c"
u'"283";"7,02";"http://cars.mail.ru/reviews/mercedes-benz/c"\n'
"257";"6,37";"http://cars.mail.ru/reviews/mercedes-benz/glk"
u'"257";"6,37";"http://cars.mail.ru/reviews/mercedes-benz/glk"\n'
"247";"6,12";"http://cars.mail.ru/reviews/mercedes-benz/a"
u'"247";"6,12";"http://cars.mail.ru/reviews/mercedes-benz/a"\n'
"242";"6,00";"http://cars.mail.ru/catalog/mercedes-benz"
u'"242";"6,00";"http://cars.mail.ru/catalog/mercedes-benz"\n'
"222";"5,50";"http://cars.mail.ru/reviews/mercedes-benz/e"
u'"222";"5,50";"http://cars.mail.ru/reviews/mercedes-benz/e"\n'
"218";"5,40";"http://cars.mail.ru/reviews/mercedes-benz/gl"
u'"218";"5,40";"http://cars.mail.ru/reviews/mercedes-benz/gl"\n'
"199";"4,93";"http://cars.mail.ru/reviews/mercedes-benz"
u'"199";"4,93";"http://cars.mail.ru/reviews/mercedes-benz"\n'
"192";"4,76";"http://cars.mail.ru/reviews/mercedes-benz/m"
u'"192";"4,76";"http://cars.mail.ru/reviews/mercedes-benz/m"\n'
"178";"4,41";"http://cars.mail.ru/reviews/mercedes-benz/s"
u'"178";"4,41";"http://cars.mail.ru/reviews/mercedes-benz/s"\n'
"134";"3,32";"http://cars.mail.ru/reviews/mercedes-benz/g"
u'"134";"3,32";"http://cars.mail.ru/reviews/mercedes-benz/g"\n'
"134";"3,32";"http://cars.mail.ru/reviews/mercedes-benz/sprinter"
u'"134";"3,32";"http://cars.mail.ru/reviews/mercedes-benz/sprinter"\n'
"104";"2,58";"http://cars.mail.ru/reviews/mercedes-benz/190"
u'"104";"2,58";"http://cars.mail.ru/reviews/mercedes-benz/190"\n'
"58";"1,44";"http://cars.mail.ru/reviews/mercedes-benz/230"
u'"58";"1,44";"http://cars.mail.ru/reviews/mercedes-benz/230"\n'
"44";"1,09";"http://cars.mail.ru/reviews/mercedes-benz/b"
u'"44";"1,09";"http://cars.mail.ru/reviews/mercedes-benz/b"\n'
"43";"1,07";"http://cars.mail.ru/reviews/mercedes-benz/200"
u'"43";"1,07";"http://cars.mail.ru/reviews/mercedes-benz/200"\n'
"39";"0,97";"http://cars.mail.ru/reviews/mercedes-benz/glk/2012/54385"
u'"39";"0,97";"http://cars.mail.ru/reviews/mercedes-benz/glk/2012/54385"\n'
"39";"0,97";"http://cars.mail.ru/reviews/mercedes-benz/viano"
u'"39";"0,97";"http://cars.mail.ru/reviews/mercedes-benz/viano"\n'
"38";"0,94";"http://cars.mail.ru/reviews/mercedes-benz/cls"
u'"38";"0,94";"http://cars.mail.ru/reviews/mercedes-benz/cls"\n'
"33";"0,82";"http://cars.mail.ru/reviews/mercedes-benz/gl/2013/55075"
u'"33";"0,82";"http://cars.mail.ru/reviews/mercedes-benz/gl/2013/55075"\n'

u'\n'

Легко заметить, что вместо символов кириллицы здесь выводятся шестнадцатиричные последовательности. В предыдущем посте мы решили эту проблему. Здесь ниже приведем только решение, но тратить время на обсуждение не будем, поскольку в любом редакторе этот файл отображается нормально. Что нам, собственно, и нужно.
А ниже снова проверим кодировку, но у объекта f1
In [7]:
f1.encoding
Out[7]:
'cp1251'
Обратите внимание команда f.encoding не выполнилась... Но пока мы не будем разбираться почему... Нас здесь интересуют более насущные проблемы.

Объект response

Как видно из первого примера, результаты работы парсера содержатся в объекте response, вот содержание файла документации из моего локального репозитория C:.rst
.. _response: ================ Работа с ответом ================ Объект Response =============== Результатом обработки запроса является объект класса :class:`~grab.response.Response`. Вы можете получить к нему доступ через аттрибут `response`:: g.go('http://mail.ru') print g.response.headers['Content-Type'] Смотрите полный перечень аттрибутов и методов объекта `Response` в API справочнике :class:`~grab.response.Response`. Самое важное, что вам нужно знать: :body: оригинальное тело ответа :unicode_body(): тело ответа, приведённое к unicode-представлению :code: HTTP-статус ответа :headers: HTTP-заголовки ответа :charset: кодировка документа :cookies: кукисы ответа :url: URL документа. В случае автоматической обработки редиректов, этот URL может отличаться от запрашиваемого.
В контексте вопросов с кодировками файлов - атрибут unicode_body() надо использовать, кроме того, действительно, здесь есть все, что нас может понадобится на первых порах. На всякий случай, скопируем код из локального репозитория:
In [5]:
!type C:\\Users\\kiss\\Documents\\GitHub\\grab\\grab\\response.py
"""
The Response class is the result of network request maden with Grab instance.
"""
import re
from copy import copy
import logging
import email
try:
    from urllib2 import Request
except ImportError:
    from urllib.request import Request
import os
import json
try:
    from urlparse import urlsplit, parse_qs
except ImportError:
    from urllib.parse import urlsplit, parse_qs
import tempfile
import webbrowser
import codecs
from datetime import datetime

import grab.tools.encoding
from .cookie import CookieManager
from .tools.files import hashed_path
from grab.util.py3k_support import *

logger = logging.getLogger('grab.response')

RE_XML_DECLARATION = re.compile(br'^[^<]{,100}<\?xml[^>]+\?>', re.I)
RE_DECLARATION_ENCODING = re.compile(br'encoding\s*=\s*["\']([^"\']+)["\']')
RE_META_CHARSET = re.compile(br'<meta[^>]+content\s*=\s*[^>]+charset=([-\w]+)',
                             re.I)

# Bom processing logic was copied from
# https://github.com/scrapy/w3lib/blob/master/w3lib/encoding.py
_BOM_TABLE = [
    (codecs.BOM_UTF32_BE, 'utf-32-be'),
    (codecs.BOM_UTF32_LE, 'utf-32-le'),
    (codecs.BOM_UTF16_BE, 'utf-16-be'),
    (codecs.BOM_UTF16_LE, 'utf-16-le'),
    (codecs.BOM_UTF8, 'utf-8')
]

_FIRST_CHARS = set(char[0] for (char, name) in _BOM_TABLE)

def read_bom(data):
    """Read the byte order mark in the text, if present, and 
    return the encoding represented by the BOM and the BOM.

    If no BOM can be detected, (None, None) is returned.
    """
    # common case is no BOM, so this is fast
    if data and data[0] in _FIRST_CHARS:
        for bom, encoding in _BOM_TABLE:
            if data.startswith(bom):
                return encoding, bom
    return None, None


class Response(object):
    """
    HTTP Response.
    """

    __slots__ = ('status', 'code', 'head', '_body', '_runtime_body',
                 'body_path', 'headers', 'url', 'cookies',
                 'charset', '_unicode_body', '_unicode_runtime_body',
                 'bom', 'timestamp',
                 'name_lookup_time', 'connect_time', 'total_time',
                 'download_size', 'upload_size', 'download_speed',
                 'error_code', 'error_msg',
                 )

    def __init__(self):
        self.status = None
        self.code = None
        self.head = None
        self._body = None
        self._runtime_body = None
        #self.runtime_body = None
        self.body_path = None
        self.headers =None
        self.url = None
        self.cookies = CookieManager()
        self.charset = 'utf-8'
        self._unicode_body = None
        self._unicode_runtime_body = None
        self.bom = None
        self.timestamp = datetime.now()
        self.name_lookup_time = 0
        self.connect_time = 0
        self.total_time = 0
        self.download_size = 0
        self.upload_size = 0
        self.download_speed = 0
        self.error_code = None
        self.error_msg = None

    def parse(self, charset=None):
        """
        Parse headers and cookies.

        This method is called after Grab instance performes network request.
        """

        # Extract only valid lines which contain ":" character
        valid_lines = []
        for line in self.head.split('\n'):
            line = line.rstrip('\r')
            if line:
                # Each HTTP line meand the start of new response
                # self.head could contains info about multiple responses
                # For example, then 301/302 redirect was processed automatically
                # Maybe it is a bug and should be fixed
                # Anyway, we handle this issue here and save headers
                # only from last response
                if line.startswith('HTTP'):
                    self.status = line
                    valid_lines = []
                else:
                    if ':' in line:
                        valid_lines.append(line)

        self.headers = email.message_from_string('\n'.join(valid_lines))

        if charset is None:
            if isinstance(self.body, unicode):
                self.charset = 'utf-8'
            else:
                self.detect_charset()
        else:
            self.charset = charset

        self._unicode_body = None

    def detect_charset(self):
        """
        Detect charset of the response.

        Try following methods:
        * meta[name="Http-Equiv"]
        * XML declaration
        * HTTP Content-Type header

        Ignore unknown charsets.

        Use utf-8 as fallback charset.
        """

        charset = None

        body_chunk = None
        if self.body_path:
            with open(self.body_path, 'rb') as inp:
                body_chunk = inp.read(4096)
        elif self._body:
            body_chunk = self._body[:4096]

        if body_chunk:
            # Try to extract charset from http-equiv meta tag
            try:
                charset = RE_META_CHARSET.search(body_chunk).group(1)
            except AttributeError:
                pass

            # TODO: <meta charset="utf-8" />
            bom_enc, bom = read_bom(body_chunk)
            if bom_enc:
                charset = bom_enc
                self.bom = bom

            # Try to process XML declaration
            if not charset:
                if body_chunk.startswith(b'<?xml'):
                    match = RE_XML_DECLARATION.search(body_chunk)
                    if match:
                        enc_match = RE_DECLARATION_ENCODING.search(match.group(0))
                        if enc_match:
                            charset = enc_match.group(1)

        if not charset:
            if 'Content-Type' in self.headers:
                pos = self.headers['Content-Type'].find('charset=')
                if pos > -1:
                    charset = self.headers['Content-Type'][(pos + 8):]

        if charset:
            if not isinstance(charset, str):
                # Convert to unicode (py2.x) or string (py3.x)
                charset = charset.decode('utf-8')
            # Check that python knows such charset
            try:
                codecs.lookup(charset)
            except LookupError:
                logger.error('Unknown charset found: %s' % charset)
                self.charset = 'utf-8'
            else:
                self.charset = charset

    def process_unicode_body(self, body, bom, charset, ignore_errors, fix_special_entities):
        if isinstance(body, unicode):
            #if charset in ('utf-8', 'utf8'):
            #    return body.strip()
            #else:
            #    body = body.encode('utf-8')
            #
            body = body.encode('utf-8')
        if bom:
            body = body[len(self.bom):]
        if fix_special_entities:
            body = grab.tools.encoding.fix_special_entities(body)
        if ignore_errors:
            errors = 'ignore'
        else:
            errors = 'strict'
        return body.decode(charset, errors).strip()

    def unicode_body(self, ignore_errors=True, fix_special_entities=True):
        """
        Return response body as unicode string.
        """

        self._check_body()
        if not self._unicode_body:
            self._unicode_body = self.process_unicode_body(
                self._body, self.bom, self.charset,
                ignore_errors, fix_special_entities)
        return self._unicode_body

    def unicode_runtime_body(self, ignore_errors=True, fix_special_entities=True):
        """
        Return response body as unicode string.
        """

        if not self._unicode_runtime_body:
            self._unicode_runtime_body = self.process_unicode_body(
                self.runtime_body, None, self.charset,
                ignore_errors, fix_special_entities)
        return self._unicode_runtime_body

    def copy(self):
        """
        Clone the Response object.
        """

        obj = Response()

        copy_keys = ('status', 'code', 'head', 'body', 'total_time',
                     'connect_time', 'name_lookup_time',
                     'url', 'charset', '_unicode_body')
        for key in copy_keys:
            setattr(obj, key, getattr(self, key))

        obj.headers = copy(self.headers)
        # TODO: Maybe, deepcopy?
        obj.cookies = copy(self.cookies)

        return obj

    def save(self, path, create_dirs=False):
        """
        Save response body to file.
        """

        path_dir, path_fname = os.path.split(path)
        if not os.path.exists(path_dir):
            try:
                os.makedirs(path_dir)
            except OSError:
                pass

        with open(path, 'wb') as out:
            if isinstance(self._body, unicode):
                out.write(self._body.encode('utf-8'))
            else:
                out.write(self._body)

    def save_hash(self, location, basedir, ext=None):
        """
        Save response body into file with special path
        builded from hash. That allows to lower number of files
        per directory.

        :param location: URL of file or something else. It is
            used to build the SHA1 hash.
        :param basedir: base directory to save the file. Note that
            file will not be saved directly to this directory but to
            some sub-directory of `basedir`
        :param ext: extension which should be appended to file name. The
            dot is inserted automatically between filename and extension.
        :returns: path to saved file relative to `basedir`

        Example::

            >>> url = 'http://yandex.ru/logo.png'
            >>> g.go(url)
            >>> g.response.save_hash(url, 'some_dir', ext='png')
            'e8/dc/f2918108788296df1facadc975d32b361a6a.png'
            # the file was saved to $PWD/some_dir/e8/dc/...

        TODO: replace `basedir` with two options: root and save_to. And
        returns save_to + path
        """

        if isinstance(location, unicode):
            location = location.encode('utf-8')
        rel_path = hashed_path(location, ext=ext)
        path = os.path.join(basedir, rel_path)
        if not os.path.exists(path):
            path_dir, path_fname = os.path.split(path)
            try:
                os.makedirs(path_dir)
            except OSError:
                pass
            with open(path, 'wb') as out:
                if isinstance(self._body, unicode):
                    out.write(self._body.encode('utf-8'))
                else:
                    out.write(self._body)
        return rel_path

    @property
    def json(self):
        """
        Return response body deserialized into JSON object.
        """

        return json.loads(self.body)

    def url_details(self):
        """
        Return result of urlsplit function applied to response url.
        """

        return urlsplit(self.url) 

    def query_param(self, key):
        """
        Return value of parameter in query string.
        """

        return parse_qs(self.url_details().query)[key][0]

    def browse(self):
        """
        Save response in temporary file and open it in GUI browser.
        """

        fh, path = tempfile.mkstemp()
        self.save(path)
        webbrowser.open('file://' + path)

    def _check_body(self):
        if not self._body:
            if self.body_path:
                with open(self.body_path, 'rb') as inp:
                    self._body = inp.read()

    def _read_body(self):
        # py3 hack
        if PY3K:
            return self.unicode_body()

        self._check_body()
        return self._body

    def _write_body(self, body):
        self._body = body
        self._unicode_body = None

    body = property(_read_body, _write_body)

    def _read_runtime_body(self):
        if self._runtime_body is None:
            return self._body
        else:
            return self._runtime_body

    def _write_runtime_body(self, body):
        self._runtime_body = body
        self._unicode_runtime_body = None

    runtime_body = property(_read_runtime_body, _write_runtime_body)

    def body_as_bytes(self, encode=False):
        self._check_body()
        if encode:
            return self.body.encode(self.charset)
        return self._body

    @property
    def time(self):
        logger.error('Attribute Response.time is deprecated. Use Response.total_time instead.')
        return self.total_time

Из комментариев в коде ясно, что при скачивании файлов Grab тщательнейшим образом пытается определить кодировку документа. Если это не удается, то сохраняет файл в utf-8. А здесь и онлайн руководствоКодировка документа Причем, документ читается построчно как байтовый файл. Это сохраняет кодировку оригинала. Таким образом, кодировка cp1251 из примера файла в начале поста, скорее всего, взята с сервера. Проверить это предположение легко, вот заголовок:
In []:
Pragma: no-cache
Date: Tue, 04 Feb 2014 06:08:37 GMT
Server: nginx/1.2.9
Content-Type: text/comma-separated-values; charset=windows-1251
Cache-control: no-cache
Content-Disposition: attachment
Connection: keep-alive
Content-Length: 1406
Grab предоставляет возможность перекодировать файл "на лету", чтобы хранить его в utf-8. Стоит ли это делать? Пока не знаю. Так что займемся изучением объекта Response. Вот здесь есть документация на русском
И, оказывается, есть таки довольно подробное описание grab.response: класс ответа сервера
In [11]:
from grab import Grab
g=Grab()
In [12]:
myurl='http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=nissan&gender=1&agegroup=0&'
In [14]:
g.go(myurl)
print g.response.headers['Content-Type']
print g.response.charset
print g.response.url
print g.response.cookies
text/comma-separated-values; charset=windows-1251
windows-1251
http://top.mail.ru/referers.csv?id=85837&period=2&date=&sf=0&pp=20&filter_type=0&filter=nissan&gender=1&agegroup=0&
{}

In [16]:
print g.response.body()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-377942660d01> in <module>()
----> 1 print g.response.body()

TypeError: 'str' object is not callable
In [15]:
print g.response.unicode_body()
"Авто@Mail.Ru";85837;"http://auto.mail.ru"

"Переходы";"%";"Страницы"
"23824";"34,97";"http://cars.mail.ru/catalog/nissan"
"2968";"4,36";"http://cars.mail.ru/catalog/nissan/x-trail/ii/crossover"
"1767";"2,59";"http://cars.mail.ru/catalog/nissan/almera/iii_g11/sedan"
"1588";"2,33";"http://cars.mail.ru/catalog/nissan/qashqai/2956/crossover"
"1328";"1,95";"http://cars.mail.ru/reviews/nissan/qashqai"
"1255";"1,84";"http://cars.mail.ru/catalog/nissan/qashqai/plus_2/crossover"
"1152";"1,69";"http://cars.mail.ru/reviews/nissan/x-trail"
"1017";"1,49";"http://cars.mail.ru/catalog/nissan/juke/4013/crossover"
"944";"1,39";"http://cars.mail.ru/catalog/nissan/x-trail/ii/reviews"
"749";"1,10";"http://cars.mail.ru/catalog/nissan/pathfinder/iii/crossover"
"689";"1,01";"http://cars.mail.ru/reviews/nissan/almera"
"517";"0,76";"http://cars.mail.ru/sale/msk/all/nissan/x-trail"
"477";"0,70";"http://cars.mail.ru/reviews/nissan/pathfinder"
"461";"0,68";"http://cars.mail.ru/catalog/nissan/note/3585/minivan"
"439";"0,64";"http://cars.mail.ru/catalog/nissan/teana/ii/sedan"
"410";"0,60";"http://cars.mail.ru/sale/msk/used/nissan/x-trail"
"386";"0,57";"http://cars.mail.ru/catalog/nissan/almera/iii_g11/reviews"
"380";"0,56";"http://cars.mail.ru/catalog/nissan/tiida/2349/sedan"
"373";"0,55";"http://cars.mail.ru/catalog/nissan/x-trail/ii/crossover/reviews"
"330";"0,48";"http://cars.mail.ru/catalog/nissan/x-trail"

Пока все здорово, проблем с кодировкой не возникает, функция unicode_body() работает замечательно. Проверим ка еще функцию save. Здесь неудача, у объекта unicode_body() нет метода save. А стоит ли его дописывать самому? Пожалуй, что нет, вот, как этот объект будет выглядеть в текстовом файле
In [19]:
ftemp=g.response.unicode_body()
ftemp
Out[19]:
u'"\u0410\u0432\u0442\u043e@Mail.Ru";85837;"http://auto.mail.ru"\n\n"\u041f\u0435\u0440\u0435\u0445\u043e\u0434\u044b";"%";"\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u044b"\n"23824";"34,97";"http://cars.mail.ru/catalog/nissan"\n"2968";"4,36";"http://cars.mail.ru/catalog/nissan/x-trail/ii/crossover"\n"1767";"2,59";"http://cars.mail.ru/catalog/nissan/almera/iii_g11/sedan"\n"1588";"2,33";"http://cars.mail.ru/catalog/nissan/qashqai/2956/crossover"\n"1328";"1,95";"http://cars.mail.ru/reviews/nissan/qashqai"\n"1255";"1,84";"http://cars.mail.ru/catalog/nissan/qashqai/plus_2/crossover"\n"1152";"1,69";"http://cars.mail.ru/reviews/nissan/x-trail"\n"1017";"1,49";"http://cars.mail.ru/catalog/nissan/juke/4013/crossover"\n"944";"1,39";"http://cars.mail.ru/catalog/nissan/x-trail/ii/reviews"\n"749";"1,10";"http://cars.mail.ru/catalog/nissan/pathfinder/iii/crossover"\n"689";"1,01";"http://cars.mail.ru/reviews/nissan/almera"\n"517";"0,76";"http://cars.mail.ru/sale/msk/all/nissan/x-trail"\n"477";"0,70";"http://cars.mail.ru/reviews/nissan/pathfinder"\n"461";"0,68";"http://cars.mail.ru/catalog/nissan/note/3585/minivan"\n"439";"0,64";"http://cars.mail.ru/catalog/nissan/teana/ii/sedan"\n"410";"0,60";"http://cars.mail.ru/sale/msk/used/nissan/x-trail"\n"386";"0,57";"http://cars.mail.ru/catalog/nissan/almera/iii_g11/reviews"\n"380";"0,56";"http://cars.mail.ru/catalog/nissan/tiida/2349/sedan"\n"373";"0,55";"http://cars.mail.ru/catalog/nissan/x-trail/ii/crossover/reviews"\n"330";"0,48";"http://cars.mail.ru/catalog/nissan/x-trail"'
Здесь стоит сделать паузу. Пусть в этом посте будут только возможности Grab и немного Furl... Прежде, чем двигаться дальше надо будет порассуждать о соглашении о наименованиях...


Посты чуть ниже также могут вас заинтересовать

Комментариев нет:

Отправить комментарий