Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/venv
.idea
.python-version
*.db
__pycache__/*
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,33 @@
Этот парсер предназначен для сбора информации о продуктах категории кофе на сайте магазина [Метро](https://online.metro-cc.ru/). Он извлекает информацию о продуктах, включая их идентификатор, название, ссылку, обычную цену, промо-цену и бренд (если доступен). Парсер реализован с использованием библиотеки Python requests для HTTP-запросов, bs4 для парсинга HTML и pandas для создания и сохранения данных в файл Excel

### Инструкции по использованию

1. Убедитесь, что у вас установлены все необходимые библиотеки, перечисленные в файле requirements.txt. Установите их, выполнив команду:

```
pip install -r requirements.txt
```

2. Запустите файл main.py:

```
python main.py
```
3. Парсер начнет сбор данных о продуктах категории кофе на сайте "Метро". Информация будет выводиться в консоль, а также сохраняется в файл Excel с именем coffee_metro.xlsx

3. Парсер начнет сбор данных о продуктах категории кофе на сайте "Метро". Информация будет выводиться в консоль, а также сохраняется в файл Excel с именем coffee_metro.xlsx
4. По завершении работы парсера в консоли вы увидите сообщение о времени выполнения

## Настройки
Измените переменную link_to_coffee в файле main.py, чтобы указать другую категорию продуктов
## Использование

При старте программы в консоле возникнет поле ввода. Впишите туда ссылку категории продуктов, которую хотите распарсить.

```
link_to_coffee = 'https://online.metro-cc.ru/category/chaj-kofe-kakao/kofe?from=under_search&in_stock=1'
Привет! Введи URL категории товара для парсинга:
https://online.metro-cc.ru/category/chaj-kofe-kakao/kofe?from=under_search&in_stock=1
```
Вы также можете настроить другие параметры, такие как классы элементов HTML для поиска продуктов и кнопки "Показать еще" в коде main.py, если сайт "Метро" изменится

Вы также можете настроить другие параметры, такие как классы элементов HTML для поиска продуктов и кнопки "Показать еще" в коде main.py, если сайт "Метро" изменится.

## Дополнения

Для ускорения работы парсера можно использовать асинхронный подход (asyncio, aiohttp). Буду рада :sparkles: pull-request :sparkles: c предложениями по оптимизацией работы кода
Буду рада pull-request c предложениями по оптимизацией работы кода.
Binary file modified coffee_metro.xlsx
Binary file not shown.
17 changes: 17 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathlib import Path

from dotenv import load_dotenv


load_dotenv()


class Settings:
BASE_DIR = Path(__file__).resolve().parent

@property
def DATABASE_URL(self):
return f'sqlite+aiosqlite:///metro.db'


settings = Settings()
76 changes: 76 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os

from sqlalchemy import Integer, String, Column, select, insert
from sqlalchemy.ext.asyncio import \
AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
import pandas as pd

from config import settings


engine = create_async_engine(
settings.DATABASE_URL,
connect_args = {"check_same_thread": False},
future=True,
echo=False,
)

async_session = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)


async def get_session() -> AsyncSession:
async with async_session() as session:
yield session


async def connect():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)


class Base(DeclarativeBase):
pass


class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, autoincrement=True)
product_id = Column(String(255))
name = Column(String(500))
link = Column(String(255))
regular_price = Column(String(255))
promo_price = Column(String(255))
brand = Column(String(255))

@staticmethod
async def add(**kwargs):
session: AsyncSession = [i async for i in get_session()][0]
async with session:
query = insert(Product).values(**kwargs)
await session.execute(query)
await session.commit()

@staticmethod
async def export():
print('Начинаем экспорт данных')
session: AsyncSession = [i async for i in get_session()][0]
async with session:
query = select(Product)
execute = await session.execute(query)
result = execute.unique().scalars().all()
df = pd.DataFrame([{
'id': row.product_id,
'name': row.name,
'link': row.link,
'regular_price': row.regular_price,
'promo_price': row.promo_price,
'brand': row.brand
} for row in result])
df.to_excel(
os.path.join(settings.BASE_DIR, 'coffee_metro.xlsx'),
index=False
)
216 changes: 121 additions & 95 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,106 +1,132 @@
import requests
import datetime as dt
import asyncio

import aiohttp
import aiolimiter
import bs4
import pandas
import datetime
import prompt

from database import connect, Product


PRODUCT_CLASS = 'catalog-2-level-product-card'
SHOW_MORE_BUTTON_CLASS = 'subcategory-or-type__load-more'


async def scrape(html: str, session: aiohttp.ClientSession):
# Создаем объект BeautifulSoup для парсинга HTML текущей страницы
soup = bs4.BeautifulSoup(html, 'html.parser')

# Находим все продукты на текущей странице
products = soup.find_all('div', PRODUCT_CLASS)

# Проверяем, есть ли продукты на текущей странице
if not products:
return

# Итерируемся по каждому продукту на текущей странице
for product in products:
product_id = product['data-sku']
name = product.find('span', 'product-card-name__text').text.strip()
link = 'https://online.metro-cc.ru' + product.find('a', 'product-card-photo__link')['href']

# Отправляем GET-запрос к странице продукта
response: aiohttp.ClientResponse = await session.get(link)

# Проверяем успешность запроса
if not response.ok:
break

# Считываем контент страницы
html = await response.text()

# Создаем объект BeautifulSoup для парсинга HTML страницы продукта
link_soup = bs4.BeautifulSoup(html, 'html.parser')

# Извлекаем информацию о бренде продукта
brand_elem = link_soup.find('meta', {'itemprop': 'brand'})
brand = brand_elem.get('content') if brand_elem else None

# Извлекаем информацию о цене продукта
regular_price_element = product.find('div', 'product-unit-prices__old-wrapper')
if regular_price_element:
regular_price = regular_price_element.find('span', 'product-price__sum-rubles')
regular_price = regular_price.text.strip().replace(" ", "") if regular_price else None
else:
regular_price = None

link_to_coffee = 'https://online.metro-cc.ru/category/chaj-kofe-kakao/kofe?from=under_search&in_stock=1'
product_class = 'catalog-2-level-product-card'
show_more_button_class = 'subcategory-or-type__load-more'
# Извлекаем информацию о промо-цене продукта
promo_price_element = product.find('div', 'product-unit-prices__actual-wrapper')
if promo_price_element:
promo_price = promo_price_element.find('span', 'product-price__sum-rubles')
promo_price = promo_price.text.strip() if promo_price else None
else:
promo_price = None

# Добавляем информацию о продукте в БД
await Product.add(
product_id = product_id,
name = name,
link = link,
regular_price = regular_price,
promo_price = promo_price,
brand = brand
)

def parse_metro_categories(category_link):
page_number = 1
data_list = []
print(f'Продукт {product_id} добавлен в базу данных.')


async def main():
throttler = aiolimiter.AsyncLimiter(max_rate=1000, time_period=1) # 1000 задач в секунду

category_link = prompt.string('Привет! Введи URL категории товара для парсинга:\n')

await connect()

# Используем менеджер сеансов для повторного использования соединения
with requests.Session() as session:
while True:
# Формируем URL текущей страницы
current_page_url = f"{category_link}&page={page_number}"

# Отправляем GET-запрос к текущей странице
request = session.get(current_page_url)

# Проверяем успешность запроса
if request.status_code != 200:
break

# Создаем объект BeautifulSoup для парсинга HTML текущей страницы
soup = bs4.BeautifulSoup(request.text, 'html.parser')

# Находим все продукты на текущей странице
products = soup.find_all('div', product_class)

# Проверяем, есть ли продукты на текущей странице
if not products:
break

# Итерируемся по каждому продукту на текущей странице
for product in products:
product_id = product['data-sku']
name = product.find('span', 'product-card-name__text').text.strip()
link = 'https://online.metro-cc.ru' + product.find('a', 'product-card-photo__link')['href']

# Отправляем GET-запрос к странице продукта
link_request = session.get(link)

# Проверяем успешность запроса
if link_request.status_code == 200:
# Создаем объект BeautifulSoup для парсинга HTML страницы продукта
link_soup = bs4.BeautifulSoup(link_request.text, 'html.parser')

# Извлекаем информацию о бренде продукта
brand_elem = link_soup.find('meta', {'itemprop': 'brand'})
brand = brand_elem.get('content') if brand_elem else None

# Извлекаем информацию о цене продукта
regular_price_element = product.find('div', 'product-unit-prices__old-wrapper')
if regular_price_element:
regular_price = regular_price_element.find('span', 'product-price__sum-rubles')
regular_price = regular_price.text.strip().replace(" ", "") if regular_price else None
else:
regular_price = None

# Извлекаем информацию о промо-цене продукта
promo_price_element = product.find('div', 'product-unit-prices__actual-wrapper')
if promo_price_element:
promo_price = promo_price_element.find('span', 'product-price__sum-rubles')
promo_price = promo_price.text.strip() if promo_price else None
else:
promo_price = None

# Добавляем информацию о продукте в список
data_list.append({
'id': product_id,
'name': name,
'link': link,
'regular_price': regular_price,
'promo_price': promo_price,
'brand': brand
})
# Выводим последний добавленный продукт и общее количество продуктов (для отладки)
print(data_list[-1])
print(len(data_list))

# Увеличиваем номер страницы для следующего запроса
page_number += 1

# Проверяем наличие кнопки "Показать еще"
show_more_button = soup.find('button', {'class': show_more_button_class})
if not show_more_button:
print("No 'Show More' button found. Exiting.")
break

# Создаем DataFrame из списка продуктов и сохраняем в Excel-файл
df = pandas.DataFrame(data_list)
df.to_excel('coffee_metro.xlsx', index=False)
return data_list
async with aiohttp.ClientSession() as session:
page_number = 1
tasks = []

async with throttler:
while True:
# Формируем URL текущей страницы
current_page_url = f'{category_link}&page={page_number}'
print(f'Парсим страницу {page_number}.')

# Отправляем GET-запрос к текущей странице
async with session.get(current_page_url) as response:

# Проверяем успешность запроса
if not response.ok:
break

# Считываем контент страницы
html = await response.text()

# Создаем задачу извлечения данных
task = asyncio.create_task(scrape(html, session))
tasks.append(task)
print(f'Задачу на страницу {page_number} поставили.')

soup = bs4.BeautifulSoup(html, 'html.parser')
# Проверяем наличие кнопки "Показать еще"
show_more_button = soup.find('button', {'class': SHOW_MORE_BUTTON_CLASS})
if not show_more_button:
print('Кнопка «Показать ещё» не найдена. Выход.')
break

# Инкрементируем счетчик
page_number += 1

await asyncio.gather(*tasks)

await Product.export()


if __name__ == "__main__":
# Замеряем время выполнения
start_time = datetime.datetime.now()
parse_metro_categories(link_to_coffee)
end_time = datetime.datetime.now()
print(f'Время выполнения: {end_time-start_time}')
start_time = dt.datetime.now()
asyncio.run(main())
end_time = dt.datetime.now()
print(f'Время выполнения: {end_time-start_time}')
Loading