Введение


Меня заинтересовал данный фреймворк для сбора информации с сайтов. Здесь были публикации по 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=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 символов.

Файл 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 "<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

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


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

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


  1. nikitasius
    28.08.2016 14:04
    -6

    regexp, не?


    1. Akhristenko
      28.08.2016 17:49
      +2

      Ну и легендарный уже ответ на этот вопрос


      1. nikitasius
        28.08.2016 20:30
        -4

        Я спрашиваю потому, что сам парсю и html и xml для выдергивания данных через регулярки и не понимаю, на кой черт сие парсить отдельным фреймворком.
        Вот JSON… его парсинг мне мозг ломает и я просто использую библиотеку.


  1. okeld
    28.08.2016 16:21
    -2

    Beautiful Soup


    1. alan008
      28.08.2016 16:27

      А суп кроулить по ссылкам умеет?


      1. alan008
        28.08.2016 22:30

        Вот интересно, какие умники минусят?
        Человек в статье описывает фреймворк, который, судя по описанию, умеет делать XPath запросы к HTML-содержимому, а также перемещаться автоматически по ссылкам на другие страницы сайта, чтобы обойти его весь (crawling). okeld предложил альтернативу — библиотеку Beautiful Soup. В этой библиотеке тоже можно делать запросы к HTML-содержиму, только не XPath, а несколько другого вида (на мой взгляд, менее гибкого и менее удобного). Я хотел узнать, умеет ли Beatiful Soup осуществлять какой-либо crawling или там нужно самому закачивать содержимое других страниц по ссылкам.
        Я сам ни с Beautiful Soup, ни со Scrapy не работал, я даже не Pyhton-программист. Тем не менее, ответ мне интересен, т.к. заниматься парсингом сайтов мне также приходится (используя другие технологии и инструменты).


        1. dmvcm
          28.08.2016 23:16
          +1

          Beautiful Soup можно использовать вместо xpath для разбора html/xml. Насколько я могу судить Beatiful Soup не может осуществлять crawling. Данную библиотеку можно использовать совместно со scrapy при необходимости.


          1. ilBEastli
            29.08.2016 06:32
            +1

            В Scrapy используется своя библиотека Parsel для извлечения данных, основанная на lxml и cssselect. Но можно использовать непосредственно lxml или BeautifulSoup.


    1. andjel
      29.08.2016 12:44

      Для парсинга можно. Можно даже Selenium прикрутить, но мееедленно.


  1. Crait
    28.08.2016 19:47

    А что насчет JavaScript? Есть ли какое-то общее решение, которое позволит запускать паука, который будет находить все реквесты, которые может послать JavaScript со страницы?


    1. dorsett
      28.08.2016 20:27

      Возможно поможет замечательная npm-библиотека X-ray.
      Либо Webdriver.io, по сути обертка над Selenium для Node.js.


    1. XenoAura
      29.08.2016 11:06
      +2

      Scrapy как минимум умеет работать с PhantomJS и Splash(self-hosted сервис рендеринга страниц c js)


    1. GaroRobe
      29.08.2016 13:32
      +2

      Мы решили вопрос с JS при помощи Splash (взяли готовый docker) + scrapy-splash.


  1. estin
    02.09.2016 13:12

    При записи сразу в БД главное не забывать об особенностях различных реляционных СУБД при конкурентной работе с одной и той же БД, так как возможны блокировки и долгие инсерты, которые будут тормозить асинхронного паука синхронной записью в БД (часто используют синхронные коннекты/сокеты)


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


    Но для простейших вещей можно и сразу в БД что бы было меньше звеньев )