[Из песочницы] Scrapy: собираем данные и сохраняем в базу данных

Введение


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

Задача


  1. Зайти на страницу со списком абитуриентов oreluniver.ru/abits? src=all_postupil. Затем пройти по каждой ссылке и собрать данные о поступивших абитуриентах и набранных ими баллах.
  2. С самой же страницы, собрать данные о специальностях, на которые велся набор.
  3. Сохранить все результаты в базу данных

Решение


Для решения задачи я использовал 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

Файл items.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()

Здесь описан класс паука для получения списка абитуриентов.
Файл abiturlist.py
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=')), 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 символов.
Файл SpecSpider.py
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()

Вот что в итоге получилось:
Файл pipelines.py
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 "" % (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 "" % \
               (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

Список источников


  1. Документация Scrapy
  2. Собираем данные с помощью Scrapy
  3. Sqlalchemy

Комментарии (0)

© Habrahabr.ru