Статья описывает разработку скрипта на языке Python. Скрипт выполняет парсинг HTML-кода, составление списка материалов сайта, скачивания статей и предварительную очистку текста статьи от «посторонних» элементов. Используется библиотеки urllib (получение HTML-страниц), lxml (парсинг HTML-кода, удаление элементов и сохранение «очищенной» статьи), re (работа с регулярными выражениями), configobj (чтение файлов конфигурации).
Для написания скрипта достаточно базовых знаний языка Python, навыков программирования и отладки кода.
В статье даются пояснения по применению библиотек на примере составления списка публикаций С.М. Голубицкого, приведена ссылка на работающий скрипт.
Едва ли ошибусь, сказав, что многие хабражители знакомы с неуемным творчеством Сергея Голубицкого. За без малого 15 лет околокомпьютерной публицистики имярек выдал на гора 433 статьи в безвременно почившей в бозе бумажной Компьтерре и более 300 Голубятен на портале Компьютерра-онлайн. И это не считая аналитических изысканий о героях забугорных гешефтов в Бизнес-журнале, приоткрытии завесы над тайнами творчества в “Домашнем Компьютере”, статей в “Русском журнале”, “D`” и проч. и проч. Претендующие на полноту обзоры жизнетворчества интересующиеся найдут по приведенным выше ссылкам.
В прошлом году начал работу авторский проект “Старый голубятник и его друзья”, который задумывался (и стал) в частности постоянно пополняемым архивом публикаций самого автора и площадкой для ведения культурповидлианских дискуссий. Как человек, неравнодушный к виртуозно вскрываемым автором темам сетевой жизни, социальной мифологии и саморазвития, а так же охочий до качественного досужего чтения, однажды стал завсегдатаем посиделок на Голубятне и я. По мере сил стараюсь не только держать проект в поле зрения, но и как-то участвовать в его развитии.
Подвизаясь на ниве корректорских правок статей, переносимых в архив с портала Компьютерра-онлайн, первым делом я решил составить опись всех Голубятен.
Итак, задача, на примере которой мы будем рассматривать парсинг сайтов на Python, состояла в следующем:
В части языка программирования мой выбор сразу же и однозначно пал на Python. Не только потому, что несколько лет назад изучал его на досуге (а потом какое-то время использовал шелл Active Python как продвинутый калькулятор), но и за обилие библиотек, примеров исходного кода и простоту написания и отладки скриптов. Не в последнюю очередь интересовали и перспективы дальнейшего использования полученных навыков для решения очередных задач: интеграции с API Google Docs, автоматизации обработки текстов и т.д.
Решая вполне конкретную задачу практически с нуля, инструментарий подбирал так, чтобы затратить минимальное время на чтение документации, сравнение библиотек и, в конечном итоге, реализацию. С другой стороны, решение должно обладать универсальностью, достаточной для легкой адаптации к другим сайтам и аналогичным задачам. Возможно, некоторые инструменты и библиотеки неидеальны, но они позволили в конечном итоге довести задуманное до конца.
Итак, выбор инструментов начался определения подходящей версии Python. Первоначально пробовал использовать Python 3.2, но в процессе экспериментов остановился на Python 2.7, т.к. некоторые примеры на “тройке” не пошли.
Для упрощения установки дополнительных библиотек и пакетов использовал setuptools — средство для загрузки, сборки и инсталляции пакетов.
Дополнительно были установлены библиотеки:
В качестве подручных средств использовались:
Неоценимую помощь оказали статьи и обсуждения на Хабре:
А так же мануалы, примеры и документации:
И, конечно же, книга Язык программирования Python
Задача включает в себя четыре однотипных процедуры загрузки материалов с четырех разных сайтов. На каждом из них есть одна или несколько страниц со списком статей и ссылками на материал. Чтобы не тратить много времени на формализацию и унификацию процедуры, был написан базовый скрипт, на основе которого под каждый сайт дорабатывался собственный скрипт с учетом особенностей структуры списка материалов и состава HTML страниц. Так, парсинг материалов на Internettrading.net, где HTML видимо формировался вручную, потребовал множество дополнительных проверок и сценариев разбора страницы, в то время как формируемые CMS Drupal (“Старый голубятник и его друзья”) и Bitrix (“Компьютерра-онлайн”, архивы бумажной Компьютерры) страницы содержали минимум особенностей.
В дальнейшем я буду ссылаться на детали исторически самого свежего скрипта парсинга портала Старого голубятника.
Список статей выводится в разделе “Протограф”. Здесь есть название, ссылка на статью и синопсис. Список разбит на несколько страниц. Перейти к следующей странице можно изменяя в цикле параметр в строке адреса (?page=n), но мне показалось изящнее доставать ссылку на следующую страницу из текста HTML.
На странице статьи есть дата публикации в формате DD Месяц YYYY, собственное ее текст и указание на источник в подписи.
Для работы с различными типами данных было создано два объекта: MaterialList(object) — список статей (содержит метод парсинга отдельной страницы списка _ParseList и метод получения URL следующей страницы _GetNextPage, хранит список материалов и их идентификаторов) и Material(object) — собственно статья(содержит метод формирования идентификатора на основе даты _InitID, метод парсинга страницы _ParsePage, метод определения источника публикации _GetSection и атрибуты статьи, такие как дата публикации, тип материала и проч.)
Дополнительно определены функции работы с элементами дерева документа:
И функция get_month_by_name(month), возвращающая номер месяца по его названию для разбора даты.
Основной код (процедура main()) содержит загрузку конфигурации из файла, проход по страницам списка материалов с загрузкой содержимого в память и дальнейшее сохранение в файлы как самого списка (в формате CSV), так и текстов статей (в HTML, имя файла формируется на основе идентификатора материала).
В файле конфигурации хранятся URL начальной страницы списка материалов, все пути XPath для страниц материалов и списка статей, имена файлов и путь к каталогу для сохранения статей.
В этой части я рассмотрю основные моменты кода, так или иначе вызвавшие затруднений или курение мануалов.
Для упрощения отладки путей внутри документов и облегчения чтения кода все XPath вынесены в отдельный конфигурационный файл. Для работы с файлом конфигурации вполне подошла библиотека configobj. Файл конфигурации имеет следующую структуру:
Вложенность подсекций может быть произвольной, допускаются комментарии к секциям и конкретным переменным. Пример работы с файлом конфигурации:
Загрузка html-страницы реализована с помощью библиотеки urllib. С помощью lxml преобразуем документ в дерево и фиксим относительные ссылки:
При разборе списка публикаций нам потребуется перебрать в цикле все элементы списка. Для этого подойдет метод lxml.html.HtmlElement.findall(path). Например:
Сейчас самое время сделать замечание по поводу плагина FirePath и его использования для построения XPath. Действительно, как уже писали на Хабре, FirePath дает пути, которые отличаются от путей в lxml. Незначительно, но разница есть. Довольно скоро эти отличия удалось выявить и в дальнейшем использовать FirePath с поправками, например, тег tbody заменять на * (самая частая проблема). В то же время, скорректированные таким образом пути можно проверять в FirePath, что существенно ускоряет дело.
В то время как
Методы find и findall работают только с простыми путями, не содержащими логических выражений в условиях, например:
Для того, чтобы использовать более сложные условия, например, вида
потребуется уже метод xpath(path), который возвращает список элементов. Вот пример кода, вычищающий из дерева выбранные элементы (как работает эта магия я не понимаю до сих пор, но элементы действительно удаляются из дерева):
В этом фрагменте также используется метод lxml.html.tostring, сохраняющий дерево (уже без лишних элементов!) в строку в заданной кодировке.
В заключение приведу два примера работы с библиотекой регулярных выражений re. Первый пример реализует разбор даты в формате «DD Месяц YYYY»:
Используется функция re.split(regexp,string,start,options), которая формирует список из элементов строки, разделенных по определенной маске (в данном случае, по пробелу). Опция re.U позволяет работать со строками, содержащими русские символы в юникоде. Функция zfill(n) добивает строку нулями слева до указанного количества символов.
Второй пример показывает, как использовать регулярные выражения для поиска подстроки.
В приведенном примере показан код функции _GetSection(item, path), которой передается поддерево, содержащее указание на источник публикации, например «Впервые опубликовано в Бизнес-журнале». Обратите внимание на фрагмент регулярного выражения ?P<gsource>. Помещенный в скобки он позволяет определять именованные группы в строке и обращаться к ним с помощью parser.group('gsource'). Опция re.LOCALE аналогична re.U.
Исходный код парсера выложен в Google Docs. Чтобы уберечь сайт Старого голубятника от потока парсеров, выкладываю только код, без конфигурационного файла со ссылками и путями.
Результатом применения технологии стал архив статей с четырех сайтов на жестком диске и списки всех публикаций Голубятен. Списки были вручную загружены в таблицу Google Docs, статьи из архива также переносятся вручную для правки в документы Google.
В планах решение задач:
P.S. Большое спасибо всем за комментарии, поддержку и конструктивную критику. Надеюсь, что большинство замечаний станут мне полезны в будущем после внимательного изучения.
Для написания скрипта достаточно базовых знаний языка Python, навыков программирования и отладки кода.
В статье даются пояснения по применению библиотек на примере составления списка публикаций С.М. Голубицкого, приведена ссылка на работающий скрипт.
Предисловие или немного лирики
Едва ли ошибусь, сказав, что многие хабражители знакомы с неуемным творчеством Сергея Голубицкого. За без малого 15 лет околокомпьютерной публицистики имярек выдал на гора 433 статьи в безвременно почившей в бозе бумажной Компьтерре и более 300 Голубятен на портале Компьютерра-онлайн. И это не считая аналитических изысканий о героях забугорных гешефтов в Бизнес-журнале, приоткрытии завесы над тайнами творчества в “Домашнем Компьютере”, статей в “Русском журнале”, “D`” и проч. и проч. Претендующие на полноту обзоры жизнетворчества интересующиеся найдут по приведенным выше ссылкам.
В прошлом году начал работу авторский проект “Старый голубятник и его друзья”, который задумывался (и стал) в частности постоянно пополняемым архивом публикаций самого автора и площадкой для ведения культурповидлианских дискуссий. Как человек, неравнодушный к виртуозно вскрываемым автором темам сетевой жизни, социальной мифологии и саморазвития, а так же охочий до качественного досужего чтения, однажды стал завсегдатаем посиделок на Голубятне и я. По мере сил стараюсь не только держать проект в поле зрения, но и как-то участвовать в его развитии.
Подвизаясь на ниве корректорских правок статей, переносимых в архив с портала Компьютерра-онлайн, первым делом я решил составить опись всех Голубятен.
Постановка задачи
Итак, задача, на примере которой мы будем рассматривать парсинг сайтов на Python, состояла в следующем:
- Составить список всех Голубятен, размещенных на Компьютерре-онлайн. Список должен включать название статьи, дату публикации, информацию о содержимом статьи (только текст, наличие картинок, видео), Синопсис, ссылку на источник.
- Дополнить список материалами, опубликованными в бумажной Компьютерре, найти дубликаты.
- Дополнить список материалами из архива сайта Internettrading.net
- Загрузит ь список статей, уже опубликованных на портале “Старого голубятника”
- Скачать статьи на локальный диск для дальнейшей обработки, по возможности автоматически очистив текст от ненужных элементов.
Подбор инструментария
В части языка программирования мой выбор сразу же и однозначно пал на Python. Не только потому, что несколько лет назад изучал его на досуге (а потом какое-то время использовал шелл Active Python как продвинутый калькулятор), но и за обилие библиотек, примеров исходного кода и простоту написания и отладки скриптов. Не в последнюю очередь интересовали и перспективы дальнейшего использования полученных навыков для решения очередных задач: интеграции с API Google Docs, автоматизации обработки текстов и т.д.
Решая вполне конкретную задачу практически с нуля, инструментарий подбирал так, чтобы затратить минимальное время на чтение документации, сравнение библиотек и, в конечном итоге, реализацию. С другой стороны, решение должно обладать универсальностью, достаточной для легкой адаптации к другим сайтам и аналогичным задачам. Возможно, некоторые инструменты и библиотеки неидеальны, но они позволили в конечном итоге довести задуманное до конца.
Итак, выбор инструментов начался определения подходящей версии Python. Первоначально пробовал использовать Python 3.2, но в процессе экспериментов остановился на Python 2.7, т.к. некоторые примеры на “тройке” не пошли.
Для упрощения установки дополнительных библиотек и пакетов использовал setuptools — средство для загрузки, сборки и инсталляции пакетов.
Дополнительно были установлены библиотеки:
- urllib — получение HTML-страниц сайтов;
- lxml — библиотека для парсинга XML и HTML кода;
- configobj — библиотека для чтения файлов конфигурации.
В качестве подручных средств использовались:
- Notepad++ — текстовый редактор с подсветкой синтаксиса:
- FireBug — плагин браузера FireFox, позволяющий просматривать исходный код HTML-страниц
- FirePath — плагин браузера FireFox для анализа и тестирования XPath:
- Встроенный Python GUI для отладки кода.
Неоценимую помощь оказали статьи и обсуждения на Хабре:
- Подходы к извлечению данных из веб-ресурсов
- Примеры xpath-запросов к html
- «LXML» или как парсить HTML с лёгкостью
- Какой библиотекой вы парсите сайты
- Лёгкий парсинг сайтов с помощью «Beautiful Soup»
А так же мануалы, примеры и документации:
- Погружение в Python 3 (Пилгрим)/XML
- Package lxml. API reference
- примеры XPath в библиотеке MSDN
- The ElementTree XML API в документации Pyton
- The lxml.etree Tutorial
И, конечно же, книга Язык программирования Python
Обзор решения
Задача включает в себя четыре однотипных процедуры загрузки материалов с четырех разных сайтов. На каждом из них есть одна или несколько страниц со списком статей и ссылками на материал. Чтобы не тратить много времени на формализацию и унификацию процедуры, был написан базовый скрипт, на основе которого под каждый сайт дорабатывался собственный скрипт с учетом особенностей структуры списка материалов и состава HTML страниц. Так, парсинг материалов на Internettrading.net, где HTML видимо формировался вручную, потребовал множество дополнительных проверок и сценариев разбора страницы, в то время как формируемые CMS Drupal (“Старый голубятник и его друзья”) и Bitrix (“Компьютерра-онлайн”, архивы бумажной Компьютерры) страницы содержали минимум особенностей.
В дальнейшем я буду ссылаться на детали исторически самого свежего скрипта парсинга портала Старого голубятника.
Список статей выводится в разделе “Протограф”. Здесь есть название, ссылка на статью и синопсис. Список разбит на несколько страниц. Перейти к следующей странице можно изменяя в цикле параметр в строке адреса (?page=n), но мне показалось изящнее доставать ссылку на следующую страницу из текста HTML.
На странице статьи есть дата публикации в формате DD Месяц YYYY, собственное ее текст и указание на источник в подписи.
Для работы с различными типами данных было создано два объекта: MaterialList(object) — список статей (содержит метод парсинга отдельной страницы списка _ParseList и метод получения URL следующей страницы _GetNextPage, хранит список материалов и их идентификаторов) и Material(object) — собственно статья(содержит метод формирования идентификатора на основе даты _InitID, метод парсинга страницы _ParsePage, метод определения источника публикации _GetSection и атрибуты статьи, такие как дата публикации, тип материала и проч.)
Дополнительно определены функции работы с элементами дерева документа:
- get_text(item, path) — получение текста элемента по пути path в документе item
- get_value(item) — получение текста ноды в документе item
- get_value_path(item, path) — получение текста ноды в документе item по пути path
- get_attr_path(item, path, attr) — получение аттрибута элемента по пути path в документе item
И функция get_month_by_name(month), возвращающая номер месяца по его названию для разбора даты.
Основной код (процедура main()) содержит загрузку конфигурации из файла, проход по страницам списка материалов с загрузкой содержимого в память и дальнейшее сохранение в файлы как самого списка (в формате CSV), так и текстов статей (в HTML, имя файла формируется на основе идентификатора материала).
В файле конфигурации хранятся URL начальной страницы списка материалов, все пути XPath для страниц материалов и списка статей, имена файлов и путь к каталогу для сохранения статей.
Детали реализации
В этой части я рассмотрю основные моменты кода, так или иначе вызвавшие затруднений или курение мануалов.
Для упрощения отладки путей внутри документов и облегчения чтения кода все XPath вынесены в отдельный конфигурационный файл. Для работы с файлом конфигурации вполне подошла библиотека configobj. Файл конфигурации имеет следующую структуру:
# Comment
[ Section_1 ]
# Comment
variable_1 = value_1
# Comment
variable_2 = value_2
[[Subsection_1]]
variable_3 = value_3
[[Subsection_2]]
[ Section_2 ]
Вложенность подсекций может быть произвольной, допускаются комментарии к секциям и конкретным переменным. Пример работы с файлом конфигурации:
from configobj import ConfigObj
# Загрузить файл конфигурации
cfg = ConfigObj('sgolub-list.ini')
# Получить значение параметра url из секции sgolub
url = cfg['sgolub']['url']
Загрузка html-страницы реализована с помощью библиотеки urllib. С помощью lxml преобразуем документ в дерево и фиксим относительные ссылки:
import urllib
from lxml.html import fromstring
# Загрузка html-документа в строку
html = urllib.urlopen(url).read();
# Преобразование документа к типу lxml.html.HtmlElement
page = fromstring(html)
# Преобразование относительных ссылок внутри документа в абсолютные
page.make_links_absolute(url)
При разборе списка публикаций нам потребуется перебрать в цикле все элементы списка. Для этого подойдет метод lxml.html.HtmlElement.findall(path). Например:
for item in page.findall(path):
url = get_attr_path(item,cfg['sgolub']['list']['xpath_link'],'href')
Сейчас самое время сделать замечание по поводу плагина FirePath и его использования для построения XPath. Действительно, как уже писали на Хабре, FirePath дает пути, которые отличаются от путей в lxml. Незначительно, но разница есть. Довольно скоро эти отличия удалось выявить и в дальнейшем использовать FirePath с поправками, например, тег tbody заменять на * (самая частая проблема). В то же время, скорректированные таким образом пути можно проверять в FirePath, что существенно ускоряет дело.
В то время как
page.findall(path)
возвращает список элементов, для получения отдельного элемента существует метод find(path). Например:content = page.find(cfg['sgolub']['doc']['xpath_content'])
Методы find и findall работают только с простыми путями, не содержащими логических выражений в условиях, например:
xpath_blocks = './/*[@id='main-region']/div/div/div/table/*/tr/td'
xpath_nextpage = './/*[@id='main-region']/div/div/div/ul/li[@class="pager-next"]/a[@href]'
Для того, чтобы использовать более сложные условия, например, вида
xpath_purifytext = './/*[@id="fin" or @class="info"]'
потребуется уже метод xpath(path), который возвращает список элементов. Вот пример кода, вычищающий из дерева выбранные элементы (как работает эта магия я не понимаю до сих пор, но элементы действительно удаляются из дерева):
from lxml.html import tostring
for item in page.xpath(cfg['computerra']['doc']['xpath_purifytext']):
item.drop_tree()
text=tostring(page,encoding='cp1251')
В этом фрагменте также используется метод lxml.html.tostring, сохраняющий дерево (уже без лишних элементов!) в строку в заданной кодировке.
В заключение приведу два примера работы с библиотекой регулярных выражений re. Первый пример реализует разбор даты в формате «DD Месяц YYYY»:
import re
import datetime
# content имеет тип lxml.html.HtmlElement
# и является частью страницы, содержащей непосредственно статью
datestr=get_text(content,cfg['sgolub']['doc']['xpath_date'])
if len(datestr)>0:
datesplit=re.split('\s+',datestr,0,re.U)
self.id = self._InitID(list,datesplit[2].zfill(4)+str(get_month_by_name(datesplit[1])).zfill(2)+datesplit[0].zfill(2))
self.date = datetime.date(int(datesplit[2]),get_month_by_name(datesplit[1]),int(datesplit[0]))
else:
self.id = self._InitID(list,list.lastid[0:8])
self.date = datetime.date(1970,1,1)
Используется функция re.split(regexp,string,start,options), которая формирует список из элементов строки, разделенных по определенной маске (в данном случае, по пробелу). Опция re.U позволяет работать со строками, содержащими русские символы в юникоде. Функция zfill(n) добивает строку нулями слева до указанного количества символов.
Второй пример показывает, как использовать регулярные выражения для поиска подстроки.
def _GetSection(item, path):
# рекомендуется компилировать регулярные выражения
reinfo = re.compile(r'.*«(?P<gsource>.*)».*',re.LOCALE)
for info in item.xpath(path):
src=get_value_path(info,'.').strip('\n').strip().encode('cp1251')
if src.startswith('Впервые опубликовано'):
parser = self.reinfo.search(src)
if parser is not None:
if parser.group('gsource')=='Бизнес-журнале':
return 'Бизнес-журнал'
else:
return parser.group('gsource')
break
return ''
В приведенном примере показан код функции _GetSection(item, path), которой передается поддерево, содержащее указание на источник публикации, например «Впервые опубликовано в Бизнес-журнале». Обратите внимание на фрагмент регулярного выражения ?P<gsource>. Помещенный в скобки он позволяет определять именованные группы в строке и обращаться к ним с помощью parser.group('gsource'). Опция re.LOCALE аналогична re.U.
Исходный код парсера выложен в Google Docs. Чтобы уберечь сайт Старого голубятника от потока парсеров, выкладываю только код, без конфигурационного файла со ссылками и путями.
Заключение
Результатом применения технологии стал архив статей с четырех сайтов на жестком диске и списки всех публикаций Голубятен. Списки были вручную загружены в таблицу Google Docs, статьи из архива также переносятся вручную для правки в документы Google.
В планах решение задач:
- Написание службы, автоматически отслеживающей новые публикации
- Интеграция с API Google Docs для автоматического внесения новых публикаций в список
- Преобразование архивных статей из HTML к XML-формату с автоматической коррекцией части ошибок и загрузкой в Google Docs
P.S. Большое спасибо всем за комментарии, поддержку и конструктивную критику. Надеюсь, что большинство замечаний станут мне полезны в будущем после внимательного изучения.