|
Для Герасимова 2. Разработка модели потребительских предпочтений на основе данных рекомендательной сети epinions com
В этой главе рассказано о написанных в процессе выполнения работы процедурах сбора, преобразования и обработки данных, и кратко объясняются принципы их работы. Полные исходные тексты программ можно найти в приложении Б.
Сбор данных с сайта www.epinions.com Разработанная при выполнении работы система сбора данных с сайта epinions.com состоит из следующих элементов:
база данных для хранения собранной информации набор функций для извлечения требуемых данных со страниц сайта набор функций для заполнения базы данных информацией со страниц.
В этом разделе речь пойдет о реализации всех указанных элементов, а также о порядке обработки страниц сайта, который позволяет за относительно короткое время получить большой объем данных.
Стратегия сбора данных После рассмотрения структуры страниц сайта (см. раздел 1.1.2) было установлено, что наиболее подходящая стратегия сбора данных состоит из следующих шагов:
Получить список ссылок на профили пользователей. Это проще всего сделать, воспользовавшись отношениями доверия, которые пользователи устанавливают друг с другом. Эти отношения формируют из пользователей граф. Это позволяет, начав с нескольких пользователей, сформировать больший список, исследуя граф с помощью поиска в ширину Загрузить информацию об оценках и оцененных предметах для каждого пользователя из списка, полученного на предыдущем шаге. Страница с пользовательскими отзывами имеет удобную структуру, которая позволяет узнать также категорию, к которой относится оцененный предмет. Загрузить информацию, содержащуюся в отзывах, например, текст отзыва или детальные оценки
Описанный здесь подход обладает рядом преимуществ перед другими:
Простота реализации Учет особенностей расположения информации на сайте, что позволяет минимизировать количество загружаемых страниц Возможность на каждом этапе регулировать объем загружаемых данных, не нарушая их целостность и полноту
Построение базы данных Построение системы сбора данных начнем со способа представления собранной информации. В этой работе в качестве хранилища данных выбрана реляционная СУБД SQLite, поэтому нам потребуется создать реляционную структуру для хранения интересующей нас информации.
Реляционная модель данных
Построение реляционной базы данных начинается с выявления хранимых сущностей и представления их в виде нормализованных отношений. В базе данных потребуется хранить 3 типа сущностей: Товар_и_Отзыв'>Пользователь, Товар и Отзыв.
Атрибуты пользователя: номер пользователя ссылка на профиль пользователя идентификатор пользователя на сайте
Атрибуты товара: номер товара ссылка на страницу с описанием название товара категория товара
Атрибуты отзыва: номер отзыва номер товара номер пользователя ссылка на страницу с отзывом общая оценка название свойства оценка свойства
Представим указанную диаграмму в виде отношений. Первичные ключи будем выделять подчеркиванием. Товар(код товара, название товара, категория товара, ссылка на товар) Пользователь(код пользователя, имя пользователя, ссылка на профиль) Отзыв(код отзыва, код пользователя, код товара, ссылка на отзыв, общая оценка, название свойства, оценка свойства)
Определим зависимости между атрибутами отношений: код товара > название товара, категория товара, ссылка на товар код пользователя > имя пользователя, ссылка на профиль код отзыва > код товара, код пользователя, ссылка на отзыв, общая оценка код отзыва, название свойства > оценка свойства
Последняя зависимость нарушает требования нормальной формы Бойса-Кодда. Проведем декомпозицию отношения Отзыв на два отношения: Отзыв(код отзыва, код пользователя, код товара, ссылка на отзыв, общая оценка) ОценкаСвойства(код отзыва, название свойства, оценка свойства)
Теперь отношения находятся в нормальной форме Бойса-Кодда. Отношения Отзыв и ОценкаСвойства имеют внешние ключи. Внешние ключи отношения Отзыв: кодПользователяПользователь.кодПользователя, кодТовараТовар.кодТовара. Внешние ключи отношения Оценка свойства: кодОтзываОтзыв.кодОтзыва
Изобразим полученные отношения в виде ER-диаграммы.
Рисунок 2.1.6 – ER-диаграмма базы данных для хранения информации с сайта Epinions
Реализация БД при помощи технологии ORM
Для упрощения работы с базой данных было решено использовать одну из систем ORM для языка PythonSQLAlchemy. База данных описана в модуле epintable.py. В модуле объявляются классы, которые одновременно описывают структуру таблиц в будущей базе. Также для каждого объекта определена функция get_persistent. Она принимает ссылку на текущую сессию БД. Функция облегчает синхронизацию между объектами, которые включены в БД. Функция get_engine принимает на вход имя базы данных, создает ее, если она еще не создана, согласно описанной структуре, и возвращает специальный объект, который требуется для взаимодействия с БД.
Извлечение данных с веб-страниц Интересующие нас данные о товарах, пользователях и отзывах можно найти на многих страницах сайта. Тем не менее, нужно определиться, с каких конкретно страниц мы будем загружать данные, чтобы минимизировать количество загружаемых страниц и упростить скрипты для загрузки. После изучения структуры сайта (см. раздел 1.1.2) были приняты следующие решения для каждого вида данных.
Сеть доверия. Получить информацию о связях пользователя в сети можно в его профиле. В профиле каждого пользователя есть две страницы, связанные с сетью доверия: список пользователей, которым он выразил доверие и список пользователей, которые выразили доверие ему. Первый список пользователи могут скрывать, но делается это редко. Товары и оценки. Информацию о товарах и отзывах можно получить на многих страницах сайта, но для целей данной работы это удобнее сделать в профиле пользователя. У каждого пользователя есть страница со списком всех его отзывов с оценками, причем к каждому отзыву прилагается дополнительная информация о предмете, на который написан отзыв. Это позволяет сократить количество загружаемых страниц. Отзывы. Текст каждого отзыва с оценками всех свойств предмета хранится на отдельной странице. Ссылки на эти страницы есть в списке отзывов пользователя.
Загрузка и предварительная обработка веб-страниц
Перед тем, как извлечь с веб-страницы нужную информацию, страницу нужно загрузить и сделать синтаксический разбор ее html-кода, с тем, чтобы можно было запрашивать содержимое тегов при помощи XPath. Для загрузки страниц в виде html-кода можно использовать библиотеку urllib2, которая входит в стандартную библиотеку Python. Для упрощения загрузки страниц была написана функция get_html(). Она принимает на вход URL страницы и возвращает строку с ее HTML-кодом. Функция делает несколько попыток загрузить страницу, так как иногда при загрузке возникают ошибки, и небольшой перерыв может помочь.
def get_html(page_address):
try:
return urlopen_with_retry(page_address).read()
except urllib2.URLError:
print "Couldn't open %s" % (page_address)
raise
Возможность повторной загрузки обеспечивается функцией urlopen_with_retry .
@retry(urllib2.URLError, tries=4, delay=3, backoff=4)
def urlopen_with_retry(address):
return urllib2.urlopen(address)
Ее параметры настраиваются при объявлении с помощью декоратора retry(описан в файле epinhelper.py). Первый аргумент – тип ошибки, при которой вызов функции повторяется. Аргумент tries – количество попыток, delay – время ожидания после первой неудачной попытки, перерыв после следующей неудачной попытки и будет больше предыдущего в backoff раз. При массовой загрузке страниц такой подход показал свою эффективность.
Далее нужно передать код страницы на вход парсеру, чтобы получить удобные средства извлечения данных из тегов. Для упрощения такой работы была написана функция parse_url. На вход она принимает URL страницы и возвращает объект из библиотеки lxml, который получается после синтаксического разбора кода этой страницы. К этому объекту можно применять функции библиотеки lxml, в частности, запрашивать содержимое тегов с помощью XPath.
def parse_url(url):
html_doc = get_html(url)
parsed_html = lxml.html.fromstring(html_doc)
parsed_html.make_links_absolute('http://www.epinions.com')
return parsed_html
Функции для загрузки данных
Легко видеть, что на каждой из страниц сайта Epinions по сути содержится список определенных нами ранее объектов. Например, страницы с отзывами пользователей можно представить как списки объектов Product и Review. Эту идею можно реализовать в виде набора функций, которые для каждого объекта в нашей задаче возвращают объекты, каким-то образом с ним связанные и загруженные с веб-страниц. Примером может послужить функция, которая принимает на вход пользователя и возвращает список пользователей, которым он выразил доверие, загрузив этот список из профиля.
def get_trust_list(reviewer):
parsed_html = parse_url(reviewer.url + "/show_trust")
trust_tags = parsed_html.xpath('//div[@class = "body_container_padded"]\
/table[2]/tr/td[2]/table/tr/td/table[2]/tr/td[1]/span/a/b/../../../..')
author_urls = [t.xpath('td[1]/span/a/@href')[0] for t in trust_tags]
dates_raw = [t.xpath('td[3]/span/text()')[0].strip() for t in trust_tags]
dates = [convert_date(i) if i != '-' else datetime.date(2001,1,1) for i in dates_raw]
return [(Reviewer(a), d) for a, d in zip(author_urls, dates)]
Эта функция не учитывает того факта, что при наличии у пользователя большого числа связей, веб-сервер может выдавать их на нескольких страницах. Поэтому для этой и других аналогичных функций требуется обеспечить возможность перехода по нумерованным страницам для сбора всей требуемой информации. При разработке такого механизма использовались следующие факты о сайте:
На сайте для перемещения по длинным спискам используются ссылки в виде номеров страниц, причем номер последней страницы отображается всегда. В html-коде ссылки реализованы единым образом для всех типов списков. Если список представлен в виде нескольких страниц, то url каждой страницы содержит в себе ее номер. Пример: http://www.epinions.com/user-popsrocks/sec_ WOT_list/show_trust/pp_2/pa_1 - адрес второй страницы списка связей пользователя popsrocks.
Рисунок 2.1.7 – Нумерация страниц одного типа
Пользуясь приведенными фактами, легко написать функцию, которая для страницы из любого списка на сайте будет возвращать количество страниц в этом списке.
def count_pages(parsed_page, url_template): regex = re.compile('^%s$' % url_template.format(pagenum='(\d+)'))page_links = [i for i in parsed_page.xpath('//a/@href') if i is not None and re.search(regex, i)] page_nums = [int(re.search(regex,i).group(1)) for i in page_links] return max(page_nums) if page_nums else 1
Аргументы функции:
parsed_page – объект класса lxml.html.HtmlElement для нужной страницы
url_template – шаблон адреса страницы, представленный, например, такой строкой (для списка связей пользователя popsrocks): "http://www.epinions.com/user-popsrocks /sec_WOT_list/show_trust/pp_{pagenum}/pa_1"
Для реализации механизма перехода по страницам можно воспользоваться тем, что в языке Python можно легко передавать функции в качестве аргументов и возвращать функцию в качестве значения другой функции. Введем функцию get_paged_data.
def get_paged_data(get_data_from_page):
def f(url_template):
time.sleep(3)
parsed_page = parse_url(url_template.format(pagenum = '1'))
Npages = count_pages(parsed_page, url_template)
for i in range(1, Npages+1):
parsed_url = parse_url(url_template.format(pagenum=str(i)))
for j in get_data_from_page(parsed_url):
yield j
return f
Функция get_paged_data принимает функцию, которая возвращает нужные объекты с одной страницы списка. Функция get_paged_data возвращает функцию, которая, в свою очередь, принимает шаблон страницы списка(тот же, что и url_template в функции count_pages) и возвращает итератор, который позволяет перебрать все объекты в списке. Покажем, как пользоваться этой функцией, чтобы получить все связи любого пользователя. Начнем с функции, которая возвращает связи и даты их установления с одной страницы списка:
def get_trust_paged(parsed_html):
trust_tags = parsed_html.xpath('//div[@class = "body_container_padded"]\
/table[2]/tr/td[2]/table/tr/td/table[2]/tr/td[1]/span/a/b/../../../..')
author_urls = [t.xpath('td[1]/span/a/@href')[0] for t in trust_tags]
dates_raw = [t.xpath('td[3]/span/text()')[0].strip() for t in trust_tags]
dates = [convert_date(i) if i != '-' else datetime.date(2001,1,1) for i in dates_raw]
return [(Reviewer(a), d) for a, d in zip(author_urls, dates)]
С помощью этой функции и функции get_paged_data теперь легко написать функцию, которая возвращает итератор со всеми связями данного пользователя.
def trusts(reviewer):
template = reviewer.url + "/sec_WOT_list/show_trust/pp_{pagenum}/pa_1"
trust_records = get_paged_data(get_trust_paged)
return trust_records(template)
Аналогичным образом пишутся другие функции, которые возвращают списки объектов со страниц, например, отзывы на товар или отзывы пользователя.
Наполнение базы данных Базу данных будем заполнять, извлекая со страниц сайта объекты классов Reviewer, Review, Product и помещая их в БД. Относительно быстро получить большой объем данных можно так:
Загрузить фрагмент сети доверия, добавив в БД пользователей и связи между ними Для каждого добавленного пользователя загрузить принадлежащие ему отзывы вместе с товарами, на которые эти отзывы написаны. Формат страницы с отзывами пользователя (см. Рисунок 1.1 .2) позволяет это сделать, не загружая отдельно страницы с описанием товаров. Загрузить детальные оценки для нужных отзывов
Загрузка пользователей
Сеть будем строить в два этапа. Сначала загрузим в БД достаточное количество пользователей, переходя по связям с помощью алгоритма поиска в ширину, затем добавим связи между ними. Хотя, выполняя поиск в ширину, можно добавлять связи одновременно с пользователями, в нашей задаче это не лучшее решение. Такой способ занимает дополнительное время, притом что значительная часть загружаемых связей нам не потребуется. Действительно, мы загружаем большое количество пользователей, чтобы потом выбрать из них тех, кто активно оставляет отзывы и тем самым сообщает о себе некоторую информацию. Если при анализе мы ограничиваем себя рассмотрением только небольшой части пользователей, то и связи можно загрузить только для них, уже после загрузки отзывов.
Для добавления пользователей в БД служит класс EpCrawler. Класс определен в файле epcrawler.py.
Объекты класса EpCrawler поддерживают сериализацию с помощью встроенной в Python библиотеки pickle. Это значит, что объект можно сохранить в файл, а потом легко восстановить в том же состоянии, что и перед сохранением.
Конструктор класса EpCrawler имеет 4 аргумента:
dbfilename – имя sqlite-файла с БД pkfilename – имя файла для сериализации объекта starting_set – начальный список пользователей(объектов Reviewer) для алгоритма поиска в ширину
run() – главная функция в классе. Она реализует алгоритм поиска в ширину, загружая связи пользователей и добавляя таким образом новых пользователей в БД. Единственный аргумент функции – временное ограничение на ее работу. После завершения работы функции, а также в случае непредвиденных ошибок во время ее работы, объект сериализуется. Сериализованный объект можно в любой момент восстановить:
with open('crawler.pkl','rb') as pkfile:
crawler1 = pickle.load(pkfile)
После восстановления состояние объекта будет таким же, как и при сериализации. Для восстановленного объекта можно сразу запустить функцию run(). Она продолжит работу с того места, где была остановлена.
В качестве стартового множества пользователей для EpCrawler можно взять список 1000 самых читаемых авторов epinions.com, который доступен в специальном разделе сайта. Для загрузки списка можно воспользоваться функцией top_authors_list(), определенной в файле epinparsers.py.
Класс EpinDB. Добавление отзывов, оценок и связей
Класс EpinDB дает возможность добавлять в БД объекты, связанные с уже имеющимися в ней. Например, добавить отзывы пользователя или детальные оценки отзыва. Класс определен в файле epindb.py.
При создании класса в конструктор нужно передать имя sqlite-файла с базой данных, которую требуется дополнить.
Функция add_user_reviews принимает объект класса Reviewer и добавляет в базу данных все отзывы этого пользователя (объекты Review) и товары, на которые эти отзывы написаны (объекты Product). Функция предполагает, что передаваемый в качестве аргумента пользователь уже добавлен в БД.
Функция add_user_connections принимает объект класса Reviewer и добавляет в базу данных выходящие связи этого пользователя (объекты Connection). Функция предполагает, что передаваемый в качестве аргумента пользователь уже добавлен в БД.
Функция add_review_details принимает объект класса Review и добавляет детальные оценки для этого отзыва (объекты AttributeRating). Функция предполагает, что передаваемый в качестве аргумента отзыв уже добавлен в БД.
Теперь, имея базу данных с пользователями, можно при помощи класса EpinDB дополнить базу, добавив в нее отзывы.
ep = EpinDB('crawl1.sqlite')
revlist = ep.session.query(Reviewer).all() ## получаем список всех пользователей, имеющихся в базе
for i in rev:
ep.add_user_reviews(i) ## добавляем отзывы для каждого из пользователей
Аналогичным образом можно добавить детальные оценки для отзывов. Добавим, к примеру оценки для отзывов на цифровые фотокамеры.
a = ep.session.query(Review).\
join(Product, Product.id == Review.product_id).\
filter(Product.category == 'Digital Cameras').all() ## получаем список отзывов на фотокамеры
for i in a:
ep.add_review_details(i) ## добавляем детальные оценки для каждого отзыва
Теперь добавим связи для пользователей, имеющих более 20 отзывов:
network_reviewers = ep.session.query(Reviewer).\
from_statement('''select * from reviewer
where show_trust is null and url in (
select reviewer_url
from review
group by reviewer_url
having count(*) > 20
)''').all()
for i in network_reviewers:
ep.add_user_connections(i)
|
|
|