Введение
Меня заинтересовал данный фреймворк для сбора информации с сайтов. Здесь были публикации по Scrapy, но поскольку детальной информации на русском языке мало, то я хотел бы рассказать о своем опыте.
Задача
- Зайти на страницу со списком абитуриентов oreluniver.ru/abits?src=all_postupil. Затем пройти по каждой ссылке и собрать данные о поступивших абитуриентах и набранных ими баллах.
- С самой же страницы, собрать данные о специальностях, на которые велся набор.
- Сохранить все результаты в базу данных
Решение
Для решения задачи я использовал Python 2.7, Scrapy 1.1 Sqlalchemy 1, Sqlite. Установил все как описано в документации. В статье также описана установка на русском языке, там же о создании самого паука. Вот что у меня получилось.
Структура проекта:
\spiders
\spiders\__init__.py
\spiders\abiturlist.py
\spiders\SpecSpider.py
__init__.py
items.py
pipelines.py
settings.py
from scrapy.item import Item, Field
class SpecItem(Item):
spec = Field()
SpecName = Field()
class GtudataItem(Item):
family = Field()
name = Field()
surname = Field()
spec = Field()
ball = Field()
url = Field()
pagespec = Field()
Здесь описан класс паука для получения списка абитуриентов.
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.loader.processors import TakeFirst, Identity
from scrapy.loader import ItemLoader
from scrapy.selector import HtmlXPathSelector, Selector
from gtudata.items import GtudataItem
class AbiturLoader(ItemLoader):
default_output_processor = Identity()
class AbiturlistSpider(CrawlSpider):
name = "abiturlist"
allowed_domains = ["oreluniver.ru"]
start_urls = ["http://oreluniver.ru/abits?src=all_postupil"]
rules = (
Rule(LinkExtractor(allow=('spec_id=04.03.01')), callback='parse_item'),
)
def parse_item(self, response):
hxs = Selector(response)
all = hxs.xpath("//tr[position()>1]")
pg_spec = hxs.xpath("//div[@class='page-content']/b/div/text()").extract()[0].strip()
for fld in all:
Item = GtudataItem()
FIO = fld.xpath("./td[2]/p/text()").extract()[0].split()
Item['family'] = FIO[0]
Item['name'] = FIO[1]
Item['surname'] = FIO[2]
Item['spec'] = fld.xpath("./td[last()]/p/text()").extract()[0]
ball = fld.xpath("string(./td[3]/p)").extract()[0]
Item['ball'] = ball
Item['url'] = response.url
Item['pagespec'] = pg_spec
yield Item
Здесь описан класс паука для сбора списка специальностей. Для определения полей использован Xpath. Для разделения номера специальности и ее названия используем срезы, номер специальности занимает 9 символов.
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.selector import HtmlXPathSelector, Selector
from gtudata.items import SpecItem
class SpecSpider(CrawlSpider):
name = "speclist"
allowed_domains = ["oreluniver.ru"]
start_urls = ["http://oreluniver.ru/abits?src=all_postupil"] # /abits?src=all_postupil
rules = (
Rule(LinkExtractor(allow = ('src=all_postupil')), callback='parse_item'),
)
def parse_item(self, response):
hxs = Selector(response)
all = hxs.xpath('//a[contains(@href, "spec_id")]/text()').extract() #
print 'test'
for fld in all:
txt = fld.strip()
Item = SpecItem()
Item['SpecName'] = txt[9:]
Item['spec'] = txt[:8]
yield Item
Хотелось бы отметить возможность создания нескольких классов для собираемых данных в файле Items.py. В моем случае:
- SpecItem — для списка специальностей;
- GtudataItem — для данных абитуриентов.
Сохранение результатов в базу данных
В файле pipelines.py описаны действия по сохранению данных. Для создания базы данных sqlite с заданной структурой таблиц я использовал sqlalchemy.
Во первых создаем экземпляр класса declarative_base(), от которого будем наследовать классы, для описания таблиц базы данных, в которых будем сохранять найденную информацию. Это классы SpecTable, для сохранения списка специальностей, и DataTable, для сохранения данных абитуриентов.
В каждом классе задаем атрибут __tablename__. Это имя таблицы в базе данных. затем задаем поля:
id = Column(Integer, primary_key=True)
id — целое, первичный ключ.
Остальные поля, например номер специальности:
spec = Column(String)
В методе __init__() заполняем поля таблицы.
В классе GtudataPipeline описан процесс работы с базой данных. при инициализации проверяем наличие файла базы данных в папке проекта. Если файл отсутствует, то создаем базу данных с заданной структурой.
Base.metadata.create_all(self.engine)
В методе process_item описываем собственно сохранение в базу данных. Проверяем, экземпляром какого класса является item. В зависимости от этого заполняем одну из двух таблиц. Для этого создаем экземпляры классов DataTable и SpecTabl.
dt = DataTable(item['family'],item['name'], item['surname'], item['spec'], item['ball'], item['url'], item['pagespec'])
dt = SpecTable(item['spec'],item['SpecName'])
Для обеспечения уникальности сохраняемых данных (абитуриенты в таблицах могут повторяться) используем атрибут fio. это множество, элементы которого формируются следующей строчкой.
fio = item['family'] + item['name'] + item['surname']
Если такой абитуриент имеется в базе, то запись не сохраняется.
if fio not in self.fio:
dt = DataTable(item['family'],item['name'], item['surname'], item['spec'], item['ball'], item['url'], item['pagespec'])
self.fio.add(fio)
self.session.add(dt)
Добавляем новую запись:
self.session.add(dt)
При открытии паука создаем сессию:
def open_spider(self, spider):
self.session = Session(bind=self.engine)
При закрытии паука завершаем изменения:
def close_spider(self, spider):
self.session.commit()
self.session.close()
Вот что в итоге получилось:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, ForeignKey
from sqlalchemy.orm import Session
import os
from gtudata.items import SpecItem, GtudataItem
from scrapy.exceptions import DropItem
Base = declarative_base()
class SpecTable(Base):
__tablename__ = 'specdata'
id = Column(Integer, primary_key=True)
spec = Column(String)
spectitle = Column(String)
def __init__(self, spec, spectitle):
self.spec= spec
self.spectitle = spectitle
def __repr__(self):
return "<Data %s, %s>" % (self.spec, self.spectitle)
class DataTable(Base):
__tablename__ = 'gtudata'
id = Column(Integer, primary_key=True)
family = Column(String)
name = Column(String)
surname = Column(String)
spec = Column(String)
ball = Column(Integer)
url = Column(String)
pagespec = Column(String)
def __init__(self, family, name, surname, spec, ball, url, pagespec):
self.family = family
self.name = name
self.surname = surname
self.spec = spec
self.ball = ball
self.url = url
self.pagespec = pagespec
def __repr__(self):
return "<Data %s, %s, %s, %s, %s, %s, %s>" % (self.family, self.name, self.surname, self.spec, self.ball, self.url, self.pagespec)
class GtudataPipeline(object):
def __init__(self):
basename = 'data_scraped'
self.engine = create_engine("sqlite:///%s" % basename, echo=False)
if not os.path.exists(basename):
Base.metadata.create_all(self.engine)
self.fio = set()
def process_item(self, item, spider):
if isinstance(item, GtudataItem):
fio = item['family'] + item['name'] + item['surname']
if fio not in self.fio:
dt = DataTable(item['family'],item['name'], item['surname'], item['spec'], item['ball'], item['url'], item['pagespec'])
self.fio.add(fio)
self.session.add(dt)
elif isinstance(item, SpecItem):
dt = SpecTable(item['spec'],item['SpecName'])
self.session.add(dt)
return item
def close_spider(self, spider):
self.session.commit()
self.session.close()
def open_spider(self, spider):
self.session = Session(bind=self.engine)
Запускаем в папке с проектом:
scrapy crawl speclist
scrapy crawl abiturlist
И получаем результат. Полная версия проекта выложена на GitHub
Список источников
Комментарии (14)
okeld
28.08.2016 16:21-2Beautiful Soup
alan008
28.08.2016 16:27А суп кроулить по ссылкам умеет?
alan008
28.08.2016 22:30Вот интересно, какие умники минусят?
Человек в статье описывает фреймворк, который, судя по описанию, умеет делать XPath запросы к HTML-содержимому, а также перемещаться автоматически по ссылкам на другие страницы сайта, чтобы обойти его весь (crawling). okeld предложил альтернативу — библиотеку Beautiful Soup. В этой библиотеке тоже можно делать запросы к HTML-содержиму, только не XPath, а несколько другого вида (на мой взгляд, менее гибкого и менее удобного). Я хотел узнать, умеет ли Beatiful Soup осуществлять какой-либо crawling или там нужно самому закачивать содержимое других страниц по ссылкам.
Я сам ни с Beautiful Soup, ни со Scrapy не работал, я даже не Pyhton-программист. Тем не менее, ответ мне интересен, т.к. заниматься парсингом сайтов мне также приходится (используя другие технологии и инструменты).dmvcm
28.08.2016 23:16+1Beautiful Soup можно использовать вместо xpath для разбора html/xml. Насколько я могу судить Beatiful Soup не может осуществлять crawling. Данную библиотеку можно использовать совместно со scrapy при необходимости.
Crait
28.08.2016 19:47А что насчет JavaScript? Есть ли какое-то общее решение, которое позволит запускать паука, который будет находить все реквесты, которые может послать JavaScript со страницы?
dorsett
28.08.2016 20:27Возможно поможет замечательная npm-библиотека X-ray.
Либо Webdriver.io, по сути обертка над Selenium для Node.js.
XenoAura
29.08.2016 11:06+2Scrapy как минимум умеет работать с PhantomJS и Splash(self-hosted сервис рендеринга страниц c js)
GaroRobe
29.08.2016 13:32+2Мы решили вопрос с JS при помощи Splash (взяли готовый docker) + scrapy-splash.
estin
02.09.2016 13:12При записи сразу в БД главное не забывать об особенностях различных реляционных СУБД при конкурентной работе с одной и той же БД, так как возможны блокировки и долгие инсерты, которые будут тормозить асинхронного паука синхронной записью в БД (часто используют синхронные коннекты/сокеты)
Лучше избегать работу с реляционным БД напрямую из паука, а данные писать асинхронно в файл или другое хранилище заточенное для быстрого приема данных, а уже потом отдельно импортировать данные в целевую БД.
Но для простейших вещей можно и сразу в БД что бы было меньше звеньев )
nikitasius
regexp, не?
Akhristenko
Ну и легендарный уже ответ на этот вопрос
nikitasius
Я спрашиваю потому, что сам парсю и html и xml для выдергивания данных через регулярки и не понимаю, на кой черт сие парсить отдельным фреймворком.
Вот JSON… его парсинг мне мозг ломает и я просто использую библиотеку.