Практикум. Интернет-магазин продажи штор.

Основы Python и Django. -> Создание интернет-магазина. Часть 1.

Создание интернет-магазина. Часть 1.

В начале давайте подумаем из чего состоит типовой интернет-магазин? Я выделил 3 основных функциональных блока:

Каталог товаров. Корзина пользователя. Оформление заказа.

Эти три части составляют основу и без них невозможно представить работу магазина. Начнем с первой части т.к. она наиболее трудоемка.

Каталог товаров.

Если вы решили создать свой интернет-магазин, то очевидно знаете примеры готовых решений, которые вам подходят больше всего и отражают вашу идею и ассортимент продукции. Глупо этим не воспользоваться и не скопировать информацию из других сайтов. Я говорю именно про информацию о товарах, ценах, картинках, описании и т.д. Наличие инструмента для автоматического сбора подобных данных может сэкономить колоссальное количество времени, требующееся для наполнения вашего будущего интернет магазина. Самым простым будет создание некоторого скрипта, который бы ходил по сайту-примеру и собирал все необходимое, складывая в виде определенной структуры папок и файлов. Далее эту структуру мы передадим другой программе, которая наполнит нашу базу данных и сам сайт.

Теперь обсудим структуру каталога данных, назовем его data.

Предположим, мы продаем шторы и хотим создать интернет магазин штор. Возьмем какой то пример. В выдачи по “магазин штор” у меня первым вышел https://pangardin.com.ua Думаю, было бы логичным подкаталоги назвать по имени интернет-ресурса. Но помимо каталога продукции, на всех сайтах присутствуют и статические страницы с информацией о владельце, доставке, обратной связи и пр. Думаю что подобную информацию можно оформить в специальных текстовых файла в корне каталога data.

Примерная структура каталога данных:

. data
..pages
...main.txt
...about.txt
...contact.txt
..pangardin.com.ua
...tul
...shtory
...furnitura

Как видно, мы разбиваем товары по каталогам, будем называть их каталог-категориями, при этом внутри таких категорий необходим какой-то мета файл с информацией о данной категории.

Можно воспользоваться форматом yml, как по мне, это наиболее читабельный вариант оформления структурированных данных.

Так и назовем этот файл - meta.yml. С учетом многоязычности этот файл может выглядеть так:

content_type: category
is_published: true
order: 1
title_ru: Тюль.
title_ua: Тюль.
desc_ru: |
    Самая лучшая <strong>тюль</strong>!
desc_ua: |
    Дуже гарна тюль!

meta_title_ru: Купить тюль. Интернет-магазин штор.
meta_title_ua:  Створення iнтернет магазина штор
meta_keywords_ru: тюль шторы купить интернет-магазин
meta_keywords_ua: ….
meta_description_ru: ….
meta_description_ua: ….

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

Подобным образом можно оформить и статичные страницы в файлах *.yml в каталоге pages. Например так:

content_type: page
is_published: true
title_ru: Разработка интернет-магазина штор.
desc_ru: |
    Как разработать интернет магазин на  <strong>python</strong>!

Примерная структура каталога товарной позиции:

.tul-metis
..images
...1.png
...2.png
meta.yml
description.md

Для описания товара я определил файл description.md, использующий формат markdown для разметки текста, но позволяющий использовать и нативный html код.

Файл описания товарной позиции.

name_slug: shtory-raduga
name_ru: Шторы радуга.
meta_title_ru: Первые шаги Python.
meta_keywords_ru: Первые шаги Python.
meta_description_ru: Первые шаги Python.
is_published: false

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

Итак, наша следующая задача - опросить сайт-источник скриптом и создать требуемую структуру каталогов. Для этого я буду использовать библиотеки Python requests и BeatifulSoup.

Библиотекой requests мы будем генерировать HTTP запросы на сервер и получать HTML код страниц.

Библиотекой BeatifulSoup мы этот HTML парсим и находим в нем интересующие элементы (теги) и забираем их содержимое.

В начале, нас интересует сама структура каталога. Ее можно найти на главной странице во всплывающем блоке.

HTML код этого списка выглядит так:

<ul class="sub-menu">
    <li class="menu-item ..."><h4 class='mega_menu_title'>Категории</h4>
    <ul class="sub-menu">
        <li class="menu-item ..."><a href="..."><span class="avia-bullet"></span>Ткани-Компаньоны</a></li>
        ...

Я убрал лишние css классы для наглядности. Что мы имеем - это два вложенных списка ul с категориями и подкатегориями.

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

Все, приведенные ниже примеры, даны для операционной сисnемы на базе Debian.

Создание виртуального окружения.

virtualenv -p python3 venv3

После создания виртуального окружения необходимо его активировать набрав такую команду в консоле.

. ./venv3/bin/activate

Установить в него наши библиотеки можно такими командами.

pip install requests
pip install beatifulsoup4

Если у вас не установлена программа pip, то поставить ее можно так:

sudo apt install python-pip

Ниже приведен Python код с подробными комментариями, который разбирает cтруктуру категорий и выводит ее на экран терминала.

#!/usr/bin/env python
# импортируем библиотеки
import requests
from bs4 import BeautifulSoup
print("Getting catalog")
# делаем запрос на получение главной страницы 
url = 'https://pangardin.com.ua'
r = requests.get(url)
# распознаем HTML, создавая специальный объект soup 
soup = BeautifulSoup(r.text, 'html.parser')
# находим главный (родительский) элемент списка категорий
ulsout = soup.find('ul',{'class': 'sub-menu'})
# в цикле проходим по всем его элементам  
for ulout in ulsout:
    liout = ulout.find('li')
    if liout != -1:
        print('.'+liout.text)
        # для каждой родительской категории находим дочерний список подкатегорий
        ulins = ulout.find('ul')
        # в цикле проходим по подкатегориям
        for liin in ulins.findAll('li'):
            print('..'+liin.text)

Вывод программы:

.Ткани-Компаньоны
..Ткани-Компаньоны
..Тюль
..Портьерные ткани
..Готовые шторы
..Шторы нити
..Постельное белье
..Фурнитура
….

Все что тут происходит - это перебираются ul - списки в двух вложенных циклах.

Теперь осталось перевести названия в транслит и создать нужные каталоги.

Транслитирировать можно библиотекой python-slugify и делается это передачей нужной строки в функцию slugify.

name_slug = slugify(name)

Для удобства, программу всегда разделяют на более мелкие подпрограммы, например функциии.

Функцию сохранения категории на диск можно описать так:

def save_category(cat_name,parent_name):
    ''' сохранение каталогов-категорий  '''
    # транслитерируем русское название категории
    cat_name_slug = slugify(cat_name)
    # транслитерируем русское название подкатегории
    parent_name_slug = slugify(parent_name)
    # выводим отладочное сообщение в консоль
    print("Creating category %s with parent %s" % (cat_name,parent_name))
    # формируем путь для для создания каталога
    dir_name = os.path.join(DATA_DIR,cat_name_slug)
    # создаем каталог, если его не существует
    if not os.path.isdir(dir_name):
        os.mkdir(dir_name)

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

Это будем делать во внутреннем цикле.

    for liin in ulins.findAll('li'):
        save_category(liin.text, liout.text)

Полный код программы и результат его работы приведен ниже.

#!/usr/bin/env python
import requests
from bs4 import BeautifulSoup
import os
from slugify import slugify

### создаю каталоги с данными
if not os.path.isdir('data'):
    os.mkdir('data')

if not os.path.isdir(os.path.join('data','pangardin.com.ua')):
    os.mkdir(os.path.join('data','pangardin.com.ua'))
###

DATA_DIR = os.path.join('data','pangardin.com.ua')

def save_category(cat_name,parent_name):
    ''' сохранение каталогов-категорий  '''
    cat_name_slug = slugify(cat_name)
    parent_name_slug = slugify(parent_name)
    print("Creating category %s with parent %s" % (cat_name,parent_name))
    dir_name = os.path.join(DATA_DIR,cat_name_slug)
    if not os.path.isdir(dir_name):
        os.mkdir(dir_name)

print("Getting catalog")
url = 'https://pangardin.com.ua'
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
ulsout = soup.find('ul',{'class': 'sub-menu'})

for ulout in ulsout:
    liout = ulout.find('li')
    if liout != -1:
        ulins = ulout.find('ul')
        for liin in ulins.findAll('li'):
            save_category(liin.text, liout.text)

Дополним функцию save_category созданием meta.yml файлов, минимально описывающих категории.

В эти файлы будем писать имена и псевдонимы текущей и родительской категории.

def save_category(cat_name,parent_name):
    ''' сохранение каталогов-категорий  '''
    # транслитерируем русское название категории
    cat_name_slug = slugify(cat_name)
    # транслитерируем русское название подкатегории
    parent_name_slug = slugify(parent_name)
    # выводим отладочное сообщение в консоль
    print("Creating category %s with parent %s" % (cat_name,parent_name))
    # формируем путь для для создания каталога
    dir_name = os.path.join(DATA_DIR,cat_name_slug)
    # создаем каталог, если его не существует
    if not os.path.isdir(dir_name):
        os.mkdir(dir_name)
    # создаем строку в которую вставляем названия категорий
    meta_info = '''
content_type: category
is_published: true
name_slug: %s
name_ru: %s
parent_slug: %s
parent_name_ru: %s
order: 1
descr: |
    ''' % (cat_name_slug,cat_name,parent_name_slug,parent_name)
    with open(os.path.join(dir_name,'meta.yml'),'w') as f:
        f.write(meta_info)

Функция прохода по странице каталога

def save_goods(url,category):
    # Получаем станицу категории
    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'html.parser')    
    # находим список товаров
    products = soup.find('ul',{'class': 'products'}).findAll('li')    
    for product in products:
        # заголовок
        title = product.find('div',{'class': 'inner_product_header'}).find('h3').text
        title_slug = slugify(title)        
        # фоормируем путь
        path = os.path.join(DATA_DIR,category,title_slug)
        # создаем каталог товара
        print("Saving ... %s" % title_slug)
        if not os.path.isdir(path):
            os.mkdir(path)  
        # находим ссылку на страницу товара
        link = product.find('div',{"class": "inner_cart_button"}).find('a').get('href')
        # сохраняем позицию
        save_position(link,path)

Взглянем на то как выглядит HTML код позиции товара на странице категории

https://pangardin.com.ua/category/complect/artdeco/

<li class="...">
  <div class="inner_product wrapped_style ">
    <div class="avia_cart_buttons single_button">
      <div class="inner_cart_button">
        <a href="https://pangardin.com.ua/magazin/ballades/" >Просмотр товара</a>
      </div>
    </div>
    <a href="https://pangardin.com.ua/magazin/ballades/">
      <div class="thumbnail_container">
        <img src="..." class="attachment-shop_catalog wp-post-image" alt="Ballade">
      </div>
      <div class="inner_product_header">
        <h3>Ballades</h3>
          <span class="price"><span class="amount">208грн.</span>
          <span class="amount">366грн.</span>
        </span>
      </div>
    </a>
    </div>
</li>

После получения элемента li

products = soup.find(‘ul’,{‘class’: ‘products’}).findAll(‘li’)

мы выбрали блок с классом inner_product_header и использовали заголовок для нахождения названия товара.

title = product.find(‘div’,{‘class’: ‘inner_product_header’}).find(‘h3’).text

Теперь необходимо получить url адрес страницы с описанием товара.

link = product.find(‘div’,{“class”: “inner_cart_button”}).find(‘a’).get(‘href’)

Опишем функцию сохранения позиции.

def save_position(link,path):
    r = requests.get(link)
    with open('log.html','w') as f:
        f.write(r.text)
    soup = BeautifulSoup(r.text, 'html.parser')
    print(link)
    #sys.exit('s')
    # находим ссылку на изображение
    img_link = soup.find('a',{"class": "MagicZoomPlus"}).get('href')
    print("Downloading pic %s" % img_link)
    # забираем картинку
    r = requests.get(img_link, stream=True)
    # создаем каталог images
    path_to_img = os.path.join(path,'images')
    if not os.path.isdir(path_to_img):
        os.mkdir(path_to_img)    
    # сохраняем картинку
    image_full_path = os.path.join(path_to_img,'1.png')
    if r.status_code == 200:
        with open(image_full_path, 'wb') as f:
            r.raw.decode_content = True
            shutil.copyfileobj(r.raw, f)    
    # находим название и описание
    title = soup.find('h1',{"class": "product_title"}).text  
    description = soup.find('div',{"itemprop": "description"}).text
    # формируем содержимое meta.yml
    meta_info = '''
name_slug: %s
name_ru: %s
meta_title_ru: %s
meta_keywords_ru: %s
meta_description_ru: %s
is_published: true
description_ru: |
%s
    ''' % (slugify(title),title,title,title,title,description)
    # запись в файл
    with open(os.path.join(path,'meta.yml'),'w') as f:
        f.write(meta_info)

При поиске картинки, мы вначале находим элемент div с классом inner_cart_button, а потом в нем ссылку, из которой атрибут href. Загрузив страницу описания товара найдем элемент с изображением в исходном коде.

<a class="MagicZoomPlus" href="https://pangardin.com.ua/wp-content/uploads/2013/04/DSC00047.jpg" >
  <img itemprop="image" src="..." >
  <div class="MagicZoomPup">
  </div>
...
</a>

Получить на него ссылку можно такой строчкой кода, где мы ищем по классу MagicZoomPlus.

img_link = soup.find('a',{"class": "MagicZoomPlus"}).get('href')

Для того, чтобы получить более подробную и “чистую” информацию о товаре понадобиться некоторая сноровка в работе со структурой HTML страницы.

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

Пример нашего сайта-источника написан на wordpress и для каждого нового сайта понадобится написать свой собственный парсер контента.

Основы Python и Django. -> Создание интернет-магазина. Часть 2.

Создание интернет-магазина. Часть 2.

Создание Django приложения.

django-admin.py startproject prj
cd prj
./manage.py migrate

Создание приложения shop

./manage.py startapp shop

Добавим приложение в проект.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'shop'
]

Создадим модель (shop/models.py).

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=250)
    name_slug = models.CharField(max_length=250)


class Subcategory(models.Model):
    name = models.CharField(max_length=250)
    name_slug = models.CharField(max_length=250)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL,  null=True)

class Good(models.Model):
    name = models.CharField(max_length=250)
    name_slug = models.CharField(max_length=250)
    desc = models.TextField()
    subcategory = models.ForeignKey(Subcategory, on_delete=models.SET_NULL,  null=True)

class Image(models.Model):
    good = models.ForeignKey(Good, on_delete=models.SET_NULL, null=True)
    image = models.ImageField()

Опишем классы админ интерфейса и привяжем их к модели (shop/admin.py).

from django.contrib import admin
from .models import *


class CategoryAdmin(admin.ModelAdmin):
    pass

class SubcategoryAdmin(admin.ModelAdmin):
    pass

class GoodAdmin(admin.ModelAdmin):
    pass

class ImageAdmin(admin.ModelAdmin):
    pass

admin.site.register(Category, CategoryAdmin)
admin.site.register(Subcategory,SubcategoryAdmin)
admin.site.register(Good, GoodAdmin)
admin.site.register(Image, ImageAdmin)

Создадим новую команду импорта данных (shop/management/commands/import.py).

from django.core.management.base import BaseCommand, CommandError
from shop.models import *

class Command(BaseCommand):
    def handle(self, *args, **options):
        print('Importing data')

** Не забываем создавать файлы ini.py в новых каталогах management и commands **