# -*- coding: UTF-8 -*-
#06-09-04
#v1.3.0 

# scraper.py
# Обычный анализатор HTML ('parser') и отдельный пример того, как изменять URL в тегах.

#Описание:

#Простой анализатор HTML ('parser'), который "читает" HTML-файл и вызывает функции для обработки данных, тегов и т.д.
#Полезен, если вам нужно создать прямой анализатор, который просто извлекает информацию из файла *или* модифицирует теги и т.п.

#Не зависает на плохом HTML.

#Обсуждение:

#Scraper - это класс для синтаксического анализа HTML-файлов, содержащий методы обработки "порций" HTML и тегов.
#Их можно заменить, если требуется создать свои собственные методы обработки HTML в подклассе.
#Этот класс выполняет большую часть того, что делает HTMLParser.HTMLParser, за исключением зависания на плохом HTML (кроме символьных объектов и т.п.). 
#Я считаю, что BeautifulSoup (и даже BeautifulStoneSoup) вносит слишком много изменений в HTML-код; 
#предлагаемый класс почти не меняет HTML в ходе анализа. Менее полезный для извлечения информации из HTML, 
#но более полезный, когда нужно просто модифицировать страницу.

#В нем используются регулярные выражения и часть логики из sgmllib.py (из стандартного дистрибутива python)

#Единственный плохо организованный HTML-код, который вызывает ошибки,- это тег с пропущенной закрывающей угловой скобкой '>'. (К сожалению, так бывает часто.)
#В этом случае тег будет автоматически закрыт при следующем '<'.

#Остальные подробности см. в docstring.


# Copyright Michael Foord
# Вы можете изменять, использовать и релицензировать этот код по своему усмотрению.
# Не предоставляются и не подразумеваются никакие гарантии точности, соответствия назначению и т.п. по отношению к этому коду....
# Используйте его на свой страх и риск !!!

# E-mail michael AT foord DOT me DOT uk
# Web-адрес: www.voidspace.org.uk/atlantibots/pythonutils.html

import re

#предполагается, что namefind соответствует имени тега и его атрибутам в группах 1 и 2, соответственно.
#первоначальная версия этого шаблона:
# namefind = re.compile(r'(\S*)\s*(.+)', re.DOTALL)
#Он настаивает на том, что атрибуты обязательно должны быть и в случае необходимости "заимствует" для этого последний символ у имени тега. Это раздражает, поэтому попробуем так:
namefind = re.compile(r'(\S+)\s*(.*)', re.DOTALL)

attrfind = re.compile(
    r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
    r'(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')            # Это взято из sgmllib

class Scraper:
    def __init__(self):
        """Инициализация анализатора."""
        self.buffer = ''
        self.outfile = ''

    def reset(self):
        """Этот метод очищает входной и выходной буферы."""
        self.buffer = ''
        self.outfile = ''

    def push(self):
        """Возвращает все обработанные на текущий момент данные и очищает выходной буфер."""
        data = self.outfile
        self.outfile = ''
        return data

    def close(self):
        """Возвращает любые необработанные данные (не обрабатывая их) и возвращает анализатор в исходное состояние.
        Его следует использовать, после того как все данные были обработаны feed и затем собраны push.
        Он возвращает все оставшиеся данные, которые не могут быть обработаны.

        Если вы обрабатываете все за один заход, без опаски пользуйтесь этим методом, чтобы вернуть все.
        """
        data = self.push() + self.buffer
        self.buffer = ''
        return data

    def feed(self, data):
        """Вводит данные в анализатор.
        Обрабатывается все, что можно, но из этого метода ничего не возвращается.
        """
        self.index = -1
        self.tempindex = 0
        self.buffer = self.buffer + data
        outlist = []
        thischunk = []
        while self.index < len(self.buffer)-1:          # заменить списком все '<' и прыжками перемещаться между ними, что гораздо быстрее, чем двигаться посимвольно; причем и последнее происходит достаточно быстро...
            self.index += 1
            inchar = self.buffer[self.index]
            if inchar == '<':
                outlist.append(self.pdata(''.join(thischunk)))
                thischunk = []
                result = self.tagstart()
                if result: outlist.append(result)
                if self.tempindex: break
            else:
                thischunk.append(inchar) 
        if self.tempindex:
            self.buffer = self.buffer[self.tempindex:]
        else:
            self.buffer = ''
            if thischunk: self.buffer = ''.join(thischunk)
        self.outfile = self.outfile + ''.join(outlist)

    def tagstart(self):
        """Мы достигли начала тега.
        self.buffer - это данные
        self.index - это точка, которой мы достигли.
        Эта функция должна извлечь имя тега и все атрибуты, а затем обработать их!"""
        test1 = self.buffer.find('>', self.index+1)
        test2 = self.buffer.find('<', self.index+1)    # выполняется только для неправильных тегов с пропущенной '>'
        test1 += 1
        test2 += 1
        if not test2 and not test1:                     
            self.tempindex = self.index                # если мы получаем это задолго до заполнения буфера (тег еще не закрыт)
            self.index = len(self.buffer)              # это сигнализирует процедуре feed, что часть буфера нужно сохранить
            return
        if test1 and test2:
            test = min(test1, test2)
            if test == test2:           # если закрывающий тег пропущен, и мы работаем, начиная со следующего открывающего тега,- следует быть аккуратным с расположением индекса...
                mod=1
            else:
                mod=0
        else:
            test = test1 or test2
            if test2:
                mod=1
            else:
                mod=0
        thetag = self.buffer[self.index+1:test-1].strip()

        if thetag.startswith('!'):               # это объявление или комментарий
            return self.pdecl()
        if thetag.startswith('?'):
            return self.ppi()                    # это команда обработки 

        if mod:                                  # как только мы вернемся, к индексу сразу же будет добавлена 1
            self.index = test -2
        else:
            self.index = test -1
            
        if thetag.startswith('/'):
            return self.endtag(thetag)           # это завершение тега 

        nt = namefind.match(thetag)
        if not nt: return self.emptytag(thetag)                              # ничего внутри тега ?
        name, attributes = nt.group(1,2)

        matchlist = attrfind.findall(attributes)
        attrs = []
        #документация утверждает, что "пустой" тег обязательно должен быть безымянным, так что удалите 
        #следующую строку, вызывающую любой тег без атрибутов "empty"
        #if not matchlist: return self.emptytag(thetag)                              # ничего внутри тега ?
        for entry in matchlist:
            attrname, rest, attrvalue = entry            # этот небольшой кусок взят из sgmllib, за исключением того, что
            if not rest:                                 #findall используется для соответствия всем атрибутам
                attrvalue = attrname
            elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
                 attrvalue[:1] == '"' == attrvalue[-1:]:
                attrvalue = attrvalue[1:-1]
            attrs.append((attrname.lower(), attrvalue))
        return self.handletag(name.lower(), attrs, thetag)              # разбирается с тем, что мы уже нашли.

################################################################################################
    # Следующие методы вызываются для обработки различных элементов HTML.
    # Они должны быть заменены в подклассах.

    def pdata(self, inchunk):
        """Вызывается, когда мы сталкиваемся с новым тегом. Все необработанные данные, начиная с последнего тега, передаются этому методу.
        Фиктивный метод, подлежащий замене. Просто возвращает неизмененные данные."""
        return inchunk

    def pdecl(self):
        """Вызывается, когда мы достигаем *начала* декларации или комментария. &lt;!....
        Он использует self.index, и в него ничего не передается.
        Фиктивный метод, подлежащий замене. Просто возврат."""
        return '<'
    
    def ppi(self):
        """Вызывается, когда мы достигаем *начала* команды на обработку. &lt;?....
        Он использует self.index, и в него ничего не передается.
        Фиктивный метод, подлежащий замене. Просто возврат."""
        return '<'

    def endtag(self, thetag):
        """Вызывается, когда мы достигаем закрывающего тега. &lt;/....
        Передается содержимое тега (включая начальный '/') и оно же возвращается без изменений."""
        return '<' + thetag + '>'

    def emptytag(self, thetag):
        """Вызывается, когда мы достигаем тега, из которого нельзя извлечь значимое имя или атрибуты.
        Передается содержимое тега, которое просто возвращается без изменений."""
        return '<' + thetag + '>'  

    def handletag(self, name, attrs, thetag):
        """Вызывается, когда мы достигаем тега.
        Передается название тега и список (attrname, attrvalue), а также первоначальное содержимое тега в виде строки."""
        return '<' + thetag + '>'



#################################################################
# Этот простой тестовый сценарий ищет файл под названием 'index.html'
# Найденный файл анализируется и сохраняется под именем 'index2.html'
#
# Обратите внимание, как весь проанализированный файл может быть безопасно сохранен с помощью метода close.
# Если Scraper работает, то новый файл должен оказаться почти идеальной копией первоначального.

if __name__ == '__main__':
#    a = approxScraper('http://www.pythonware.com/daily', 'approx.py')
    a = Scraper()
    a.feed(open('index.html').read())              # считываем и пропускаем через feed
    open('index2.html','w').write(a.close())

#################################################################
    
__doc__ = """
Scraper - это класс для синтаксического анализа HTML-файлов, содержащий методы обработки "порций" HTML и тегов.
Их можно заменить, если требуется создать свои собственные методы обработки HTML в подклассе.
#Этот класс выполняет большую часть того, что делает HTMLParser.HTMLParser, за исключением зависания на плохом HTML.
В нем используются регулярные выражения и часть логики из sgmllib.py (из стандартного дистрибутива python).

Единственный плохо организованный HTML-код, который вызывает ошибки,- это тег с пропущенной закрывающей угловой скобкой '>'. (К сожалению, так бывает часто.)
В этом случае тег будет автоматически закрыт при следующем '<'.
При этом некоторые данные будут ошибочно помещены внутрь тега.

Полезные методы экземпляра Scraper:

feed(data)  -   Вводит данные в анализатор.
                Обрабатывается все, что возможно, но из этого метода ничего не возвращается.  
push()      -   Возвращает все обработанные на текущий момент данные и очищает выходной буфер.
close()     -   Возвращает любые необработанные данные (не обрабатывая их) и возвращает анализатор в исходное состояние.
                Следует использовать после того, как все данные обработаны с помощью feed и затем собраны с помощью push.
                Возвращает любые проблемные данные, которые не могут быть обработаны.
reset()     -   Этот метод очищает входной и выходной буфер.

Следующие методы вызываются для обработки различных частей HTML-документа.
В обычном экземпляре Scraper они ничего не делают и подлежат замене.
Некоторые из них зависят от свойства атрибута экземпляра self.index, которое говорит, где в  self.buffer мы находимся.
Некоторые из них явно передают тег, над которым они работают,- в этом случае self.index будет задан в конце этого тега.
После того как все эти методы возвратят то, что должны, self.index перейдет к следующему символу.
Если ваши методы предполагают будущую обработку данных, они могут модифицировать self.index вручную.
Все эти методы должны возвращать что-нибудь, что могло бы быть включено в обработанный документ.

pdata(inchunk)
    Вызывается, когда мы сталкиваемся с новым тегом. Все необработанные данные, начиная с последнего тега, передаются этому методу.
    Фиктивный метод, подлежащий замене. Просто возвращает неизмененные данные.

pdecl()
    Вызывается, когда мы достигаем *начала* декларации или комментария. &lt;!....
    Он использует self.index, и в него ничего не передается.
    Фиктивный метод, подлежащий замене. Просто возвращает '<'.
    
ppi()
    Вызывается, когда мы достигаем *начала* команды на обработку. &lt;?....
    Он использует self.index, и в него ничего не передается.
    Фиктивный метод, подлежащий замене. Просто возвращает '<'.

endtag(thetag)
    Вызывается, когда мы достигаем закрывающего тега. &lt;/....
    Передается содержимое тега (включая начальный '/') и оно же возвращается без изменений.

emptytag(thetag)
    Вызывается, когда мы достигаем тега, из которого нельзя извлечь значимое имя или атрибуты.
    Передается содержимое тега, которое просто возвращается без изменений.

handletag(name, attrs, thetag)
    Вызывается, когда мы достигаем тега.
    Передается название тега и атрибуты (список кортежей (attrname, attrvalue)), а также первоначальное содержимое тега в виде строки.


Типичное использование:

filehandle = open('file.html', 'r')
parser = Scraper()
while True:
    data = filehandle.read(10000)         # данные считываются порциями
    if not data: break                    # мы достигли конца файла - python может работать с синтаксисом do:...while ...
    parser.feed(data)
##    print parser.push()                 # можно выводить данные во время обработки с помощью метода push
processedfile = parser.close()            # или все сразу за один проход с помощью close  
## print parser.close()                   # Даже при использовании push вам придется в конце закрыть файл
filehandle.close()


СДЕЛАТЬ/ПРОБЛЕМЫ
Может быть ускорено, если ввести перемещение прыжком от '<' до '<' вместо посимвольного поиска (который тоже довольно быстр).
Необходимо проверять, что теги и атрибуты в tagdict approxScraper'а правильные.
Единственная дополнительная модификация кода HTML - это закрытие тегов, у которых нет завершающего '>'... теоретически, как я полагаю, они могут быть закрыты не в том месте....
(Конечно, это очень плохой HTML - но приходится следить, чтобы таким образом не была потеряна часть содержимого.)
Можно ввести проверку символьных и именных примитивов HTML как в HTMLParser.
Не делает ничего особенного с самозакрывающимися тегами (напр. &lt;br /&gt;)


ИЗМЕНЕНИЯ
06-09-04        Версия 1.3.0
Пара патчей, написанных Полом Перкинсом (Paul Perkins), - в основном они не дают регулярному выражению namefind обрабатывать символы при отсутствии атрибутов.

28-07-04        Версия 1.2.1
При каждом использовании feed терялась часть данных. Сейчас это исправлено.

24-07-04        Версия 1.2.0
Классы Scraper и approxScraper разделены.
Теперь это универсальный, простой анализатор кода HTML.

19-07-04        Версия 1.1.0
Изменена для вывода URL с помощью метода PATH_INFO - см. approx.py
Подчищена обработка тегов - сейчас она работает правильно, если пропущен закрывающий тег (обычно - но см. СДЕЛАТЬ - должна предположить, где его закрыть).

11-07-04        Версия 1.0.1
Добавлен метод close.

09-07-04        Версия 1.0.0
Первая версия, предназначенная для работы с CGI-прокси approx.py.

"""

Python для инженеров и исследователей

Автор: Michael Foord
Web-адрес: http://www.voidspace.org.uk/python/archive.shtml#scraper или http://code.activestate.com/recipes/286269-html-scraper/
Лицензия: Creative Commons Attribution-ShareAlike 2.0 UK: England & Wales

Перевод на русский язык: Филипп Занько
Лицензия перевода: Creative Commons Attribution-ShareAlike 2.0 UK: England & Wales

О своих предложениях, замеченных ошибках, неточностях, опечатках просьба сообщать по электронному адресу:
russianlutheran@gmail.com