пятница, 9 ноября 2012 г.

Python Парсер сайтов на urllib2 и Поисковый движок на BeautifulSoup

Для создания поискового движка на Python вам сначала необходимо научиться получать содержимое веб-страниц при переходе по URL-адресам сайтов.

Для этих целей нам подойдет urllib2.

urllib2 - это библиотека, входящая в состав Python, которая позволяет загружать код HTML-страниц, а также любых файлов, размещенных в интернете по какому-либо URL-адресу. Благодаря urllib2 вы сможете легко создать свой парсер сайтов на Python.

Для того, чтобы увидеть мощь библиотеки в действии, наберите в консоли следующие строки кода:

>> import urllib2
>> c=urllib2.urlopen('http://kiwitobes.com/wiki/Programming_language.html')
>> contents=c.read( )
>> print contents[0:50]
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Trans'

Как видите, все что вам необходимо сделать, это создать соединение с сайтом, на котором находится интересующая вас веб-страница и прочитать её содержимое, которое будет получено вами в виде текстовой строки.

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

>> import urllib2
>> c=urllib2.urlopen('http://yandex.st/jquery/1.6.3/jquery.min.js')
>> contents=c.read( )
>> print contents[0:50]
/*! jQuery v1.6.3 http://jquery.com/ | http://jque

Позвольте представить также вам Beautiful Soup.

Beautiful Soup - это превосходный парсер HTML и XML страниц, созданный на языке Python. Данный парсер хорошо умеет обрабатывать страницы с неправильным HTML и XML кодом.

Домашняя страница Beautiful Soup располагается по адресу http://www.crummy.com/software/BeautifulSoup

Установка Beautiful Soup.

Перейдите на страницу по адресу http://www.crummy.com/software/BeautifulSoup/bs4/download/ для скачивания Beautiful Soup.
Скачайте файл beautifulsoup4-4.1.3.tar.gz (текущая версия файла может отличаться).
Распакуйте скаченный архив какую-либо папку.
В консоли перейдите в эту папку и наберите следующую команду:

python setup.py install

В результате библиотека Beautiful Soup будет установлена в папку с вашим Python.

Простой пример использования Beautiful Soup.

Код данного примера получает HTML-код домашней страницы поисковой системы Google и показывает как вытащить различные DOM-элементы страницы и осуществить сбор гиперссылок.

>>> from bs4 import BeautifulSoup # Старый код: from BeautifulSoup import BeautifulSoup
>>> from urllib import urlopen
>>> soup = BeautifulSoup(urlopen('http://google.com'))
>>> soup.head.title
<title>Google</title>
>>> links = soup('a')
>>> len(links)
19
>>> links[0]
<a href="http://www.google.com/ig?hl=en">iGoogle</a>
>>> links[0].contents[0]
u'iGoogle'

Более подробные примеры использования библиотеки Beautiful Soup вы можете посмотреть, перейдя по ссылке http://www.crummy.com/software/
BeautifulSoup/documentation.html

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

Для начала работы создадим модуль для Python под названием "searchengine", который будет содержать 2 класса: один для сбора информации и размещения её в базе данных, другой для проведения полнотекстового поиска по базе данных с целью нахождения искомых слов в собранном материале.
Наш пример будет использовать базу данных SQLite, но вы сможете легко его адаптировать для работы и с другими базами данных.

Итак, создадим на жестом диске папку под название "searchengine". В данной папке создадим файл searchengine.py, в котором разместим код нашего поискового движка:

class crawler:

    # Предварительная инициализация сборщика информации crawler и передача ему названия базы данных dbname.
    def __init__(self, dbname):
        pass

    def __del__(self):
        pass

    def dbcommit(self):
        pass

    # Функция получения id и добавления его, в базу данных, если он отсутствует.
    def getentryid(self, table, field, value, createnew=True):
        return None

    # Получения индекса отдельной страницы.
    def addtoindex(self, url, soup):
        print 'Indexing %s' % url

    # Вытаскивание только текста (но не тэгов) из HTML-страницы.
    def gettextonly(self, soup):
        return None

    # Отделения слов друг от друга с помощью любых не пробельных символов.
    def separatewords(self, text):
        return None

    # Функция врзвращает true, если даннй url уже проиндексирован.
    def isindexed(self, url):
        return False

    # Добавление связующей гиперссылки между двумя страницами.
    def addlinkref(self, urlFrom, urlTo, linkText):
        pass

    # Начиная со списка страниц делает первый обширный поиск до заданной глубины, при этом индексируя страницы.
    def crawl(self, pages, depth=2):
        pass

    # Создает таблицы базы данных.
    def createindextables(self):
        pass

Используя urllib2 и BeautifulSoup вы сможете создать полноценного crawler, который будет обходить список URL для индексации и сбора найденных на веб-страницах гиперссылок.

Сперва давайте импортируем библиотеки urllib2 и BeautifulSoup в файле searchengine.py:

import urllib2
from BeautifulSoup import *
from urlparse import urljoin

# Создаем список слов, которые юудем игнорировать при поиске.
ignorewords=set(['the','of','to','and','a','in','is','it'])

Теперь вы можете заполнить код функции crawl. Пока она еще ничего не сохраняет в базе данных. Но в данный момент она способна выводить на экран URL для того, чтобы вы могли увидеть, что она работает.
Вам необходимо разместить данный код в конце файла searchengine.py (так как он является частью класса crawler):

def crawl(self, pages, depth=2):
    for i in range(depth):
        newpages = set( )
        for page in pages:
            try:
                c = urllib2.urlopen(page)
            except:
                print "Could not open %s" % page
                continue
            soup = BeautifulSoup(c.read( ))
            self.addtoindex(page, soup)

            links = soup('a')
            for link in links:
                if ('href' in dict(link.attrs)):
                    url = urljoin(page,link['href'])
                    if url.find("'") != -1: continue
                    url = url.split('#')[0] # Удаление локальной порции
                    if url[0:4] == 'http' and not self.isindexed(url):
                        newpages.add(url)
                    linkText = self.gettextonly(link)
                    self.addlinkref(page, url, linkText)

            self.dbcommit( )

        pages = newpages

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

Затем она использует библиотеку BeautifulSoup для получения всех гиперссылок со страницы и добавления их URL в набор под названием newpages. В конце цикла newpages становится pages и процесс повторяется.

Вы можете проверить работу функции в консоли:

>> import searchengine
>> pagelist = ['http://kiwitobes.com/wiki/Perl.html']
>> crawler = searchengine.crawler('')
>> crawler.crawl(pagelist)
Indexing http://kiwitobes.com/wiki/Perl.html
Could not open http://kiwitobes.com/wiki/Module_%28programming%29.html
Indexing http://kiwitobes.com/wiki/Open_Directory_Project.html
Indexing http://kiwitobes.com/wiki/Common_Gateway_Interface.html

Вы можете заметить, что некоторые страницы повторяются. В коде данной функции зарезервировано место для вызова другой функции isindexed, которая будет определять проиндексирована ли уже страница перед добавлением её в newpages. Это позволит использовать данную функцию с любым наюором URL без необходимости проделывать лишнюю работу.

Следующий шаг это установка базы данных для полнотекстового поиска. Для этого му будем использовать базу данных SQLite. Python использует для работы с базой даннх SQLite "pysqlite", который вы можете скачать по адресу http://initd.org/tracker/pysqlite

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

Для опреационных систем отличных от Windows вам потребуется установить pysqlite из исходного кода. Исходный код pysqlite доступен в качестве tarball архива на домашней странице.

Скачайте последнюю актуальную версию и введите следующие команды в консоли, заменяя 2.3.3 на номер той версии, что вы скачали:

$ gunzip pysqlite-2.3.3.tar.gz
$ tar xvf pysqlite-2.3.3.tar.gz
$ cd pysqlite-2.3.3
$ python setup.py build
$ python setup.py install

Простой пример использования базы данных SQLite.

Код данного примера создает новую таблицу базы данных и добавляет в неё строку, после чего комитит изменения. Затем таблица вызывается для поиска новой строки:

>>> from pysqlite2 import dbapi2 as sqlite
>>> con=sqlite.connect('test1.db')
>>> con.execute('create table people (name,phone,city)')
<pysqlite2.dbapi2.Cursor object at 0x00ABE770>
>>> con.execute('insert into people values ("toby","555-1212","Boston")')
<pysqlite2.dbapi2.Cursor object at 0x00AC8A10>
>>> con.commit( )
>>> cur=con.execute('select * from people')
>>> cur.next( )
(u'toby', u'555-1212', u'Boston')

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

После того, как SQLite будет вами установлена, добавьте данную строчку кода в начало файла searchengine.py:

from pysqlite2 import dbapi2 as sqlite

Также вам необходимо изменить методы __init__, __del__, и dbcommit для открытия и закрытия базы данных:

def __init__(self, dbname):
    self.con = sqlite.connect(dbname)

def __del__(self):
    self.con.close()

def dbcommit(self):
    self.con.commit()

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

Схема базы данных состоит из 5 таблиц.
Первая таблица "urllist" представляет собой список проиндексированнх URL.
Вторая таблица "wordlist" представляет собой список слов.
Третья таблица "wordlocation" представляет собой мест расположения слов в документах.
Оставшиеся 2 таблицы описывают связи между документами. Связующая таблица хранит два URL ID, отражающие связи от одной таблицы к другой и линк-слова, используемые вместе с колонками wordid и linkid для хранения тех слов, что используются в данной гиперссылке.



Все таблицы в SQLite имеют по умолчанию колонку под названием rowid, поэтому нет необходимости специально задавать ID для этих таблиц.
Для создания функции добавления всех этих таблиц в базу данных добавьте следующий код в конец файла searchengine.py, так как он является частью класса crawler:

def createindextables(self):
    self.con.execute('create table urllist(url)')
    self.con.execute('create table wordlist(word)')
    self.con.execute('create table wordlocation(urlid, wordid, location)')
    self.con.execute('create table link(fromid integer, toid integer)')
    self.con.execute('create table linkwords(wordid, linkid)')
    self.con.execute('create index wordidx on wordlist(word)')
    self.con.execute('create index urlidx on urllist(url)')
    self.con.execute('create index wordurlidx on wordlocation(wordid)')
    self.con.execute('create index urltoidx on link(toid)')
    self.con.execute('create index urlfromidx on link(fromid)')
    self.dbcommit()

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

Войдите в консоль и наберите следующую команду для создания базы данных под названием searchindex.db:

>> reload(searchengine)
>> crawler=searchengine.crawler('searchindex.db')
>> crawler.createindextables()

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

Нахождение слов на странице.

Скачанные вами страницы содержат HTML-тэги. Поэтому для начала нам необходимо отделить от них текстовое содержимое страницы. Мы можем сделать это проведя поиск по содержимому.
Добавьте данный код в функцию gettextonly:

def gettextonly(self,soup):
    v = soup.string
    if v == None:
        c = soup.contents
        resulttext = ''
        for t in c:
            subtext = self.gettextonly(t)
            resulttext += subtext + '\n'
        return resulttext
    else:
        return v.strip()

Данная функция возвращает длинную строку, содержащую весь текст на странице.

Далее измените функцию separatewords, разделяющую строку на список разделенных слов, которые могут быть добаылены в индекс:

def separatewords(self, text):
    splitter=re.compile('\\W*')
    return [s.lower( ) for s in splitter.split(text) if s!='']

Добавление в индекс.
Теперь изменим код функции addtoindex:

def addtoindex(self, url, soup):
    if self.isindexed(url): return
    print 'Indexing ' + url

    # Получение индивидуальных слов.
    text = self.gettextonly(soup)
    words = self.separatewords(text)

    # Получение URL id.
    urlid = self.getentryid('urllist', 'url', url)

    # Связывание каждого слова с его url
    for i in range(len(words)):
        word = words[i]
        if word in ignorewords: continue
        wordid = self.getentryid('wordlist', 'word', word)
        self.con.execute("insert into wordlocation(urlid, wordid, location) values (%d, %d, %d)" % (urlid, wordid, i))

Эта функция будет вызывать 2 функции, которые были определены выше для сбора списка слов на странице.
Затем будет происходить добавление страници и всех её слов в индекс, после чего будут созданы связи между их расположением в документе.
В данном примере расположение будет индексом внутри списка слов.

Вам также необходимо обновить функцию getentryid:

def getentryid(self, table, field, value, createnew=True):
    cur = self.con.execute(
    "select rowid from %s where %s='%s'" % (table,field,value))
    res = cur.fetchone( )
    if res == None:
        cur = self.con.execute(
        "insert into %s (%s) values ('%s')" % (table,field,value))
        return cur.lastrowid
    else:
        return res[0]

Все, что делает эта функция, это возвращет ID вхождения. Если вхождение не существует, то оно создается и ID возвращается.

Наконец, вам необходимо заполнить код функции isindexed, которая определяет какая страница уже находится в базе данных и, если это так, то ассоциированы ли с ней какие-либо слова:

def isindexed(self, url):
    u=self.con.execute("select rowid from urllist where url='%s'" % url).fetchone()
    if u != None:
    # Проверить. Может быть она уже обработана.
        v = self.con.execute(
        'select * from wordlocation where urlid=%d' % u[0]).fetchone( )
        if v != None: return True
    return False

Теперь вы можете перезапустить crawler и проиндексировать страницы. Вы можете сделать это, набрав в консоли следующие команды:

>> reload(searchengine)
>> crawler = searchengine.crawler('searchindex.db')
>> pages = ['http://kiwitobes.com/wiki/Categorical_list_of_programming_languages.html']
>> crawler.crawl(pages)

Crawler возможно будет долго обрабатывать страницы. Поэтому вместо того, чтобы ждать окончания его работы, я рекомендую вам скачать уже готовую версию базы иданных searchindex.db, перейдя по адресу http://kiwitobes.com/db/searchindex.db

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

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

>> [row for row in crawler.con.execute('select rowid from wordlocation where wordid=1')]
[(1,), (46,), (330,), (232,), (406,), (271,), (192,),...

Запросы к базе данных.

Теперь у нас есть рабочий crawler и большая коллекция индексов документов и вы готовы настроить по ним поиск. Для этого сперва создадим новый класс в файле searchengine.py, который будет использоваться для поиска:

class searcher:

    def __init__(self, dbname):
        self.con=sqlite.connect(dbname)

    def __del__(self):
        self.con.close( )

Таблица wordlocation упрощает путь связывания слов с таблицами. Поэтому очень просто увидеть какие страницы содержат данное слово. Однако поисковый движок ограничен до тех пор пока не сможет искать по нескольким словам вразу.

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

def getmatchrows(self, q):
    # Чтроки для построения запроса к базе данных.
    fieldlist = 'w0.urlid'
    tablelist = ''
    clauselist = ''
    wordids = []

    # Разделение слов по пробелу.
    words = q.split(' ')
    tablenumber = 0

    for word in words:
        # Получение ID слова.
        wordrow = self.con.execute("select rowid from wordlist where word='%s'" % word).fetchone()
    if wordrow != None:
        wordid = wordrow[0]
        wordids.append(wordid)
        if tablenumber > 0:
            tablelist += ','
            clauselist += ' and '
            clauselist + ='w%d.urlid=w%d.urlid and ' % (tablenumber - 1, tablenumber)
        fieldlist += ',w%d.location' % tablenumber
        tablelist += 'wordlocation w%d' % tablenumber
        clauselist += 'w%d.wordid=%d' % (tablenumber,wordid)
        tablenumber += 1

    # Построение запроса из разрозненных частей.
    fullquery = 'select %s from %s where %s' % (fieldlist,tablelist,clauselist)
    cur = self.con.execute(fullquery)
    rows = [row for row in cur]

    return rows, wordids

Данная функция просто создает ссылки на таблицу wordlocation для кажого слова в списке и робъединяет их все по их URL ID.



Таким образом запрос двух слов с ID 10 и 17 будет выглядеть так:

select w0.urlid,w0.location,w1.location
from wordlocation w0,wordlocation w1
where w0.urlid=w1.urlid
and    w0.wordid=10
and    w1.wordid=17

Попробуйте вызвать эту функцию из консоли:

>> reload(searchengine)
>> e=searchengine.searcher('searchindex.db')
>> e.getmatchrows('functional programming')
([(1, 327, 23), (1, 327, 162), (1, 327, 243), (1, 327, 261),
(1, 327, 269), (1, 327, 436), (1, 327, 953),..

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

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

def getscoredlist(self, rows, wordids):
    totalscores = dict([(row[0], 0) for row in rows])

    # Сюда вы позже добавить функцию scoring
    weights=[]

    for (weight, scores) in weights:
        for url in totalscores:
            totalscores[url] += weight * scores[url]

    return totalscores


def geturlname(self, id):
    return self.con.execute("select url from urllist where rowid=%d" % id).fetchone()[0]

def query(self, q):
    rows, wordids = self.getmatchrows(q)
    scores = self.getscoredlist(rows, wordids)
    rankedscores = sorted([(score, url) for (url, score) in scores.items()], reverse=1)
    for (score, urlid) in rankedscores[0:10]:
        print '%f\t%s' % (score, self.geturlname(urlid))

В данный момент метод query не включает подсчет результатов, но он отображает URL вместе с местом для расположения очков:

>> reload(searchengine)
>> e=searchengine.searcher('searchindex.db')
>> e.query('functional programming')
0.000000 http://kiwitobes.com/wiki/XSLT.html
0.000000 http://kiwitobes.com/wiki/XQuery.html
0.000000 http://kiwitobes.com/wiki/Unified_Modeling_Language.html
...

Самая важная функция в этом классе это getscoredlist.

Далее надо переработать весь код, так как нужен только сбор и складирование данных, а не их анализ.

Последним шагом является вывод отсортированной и сгруппированной информации на экран после выполнения соответствующего запроса.

Больше узнать по данной теме вы можете из книги "Programming Collective Intelligence".

Cover of Programming Collective Intelligence

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

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