Парсінг сайтів. Tornado мікро-фреймворк.

Основи Python и Django. -> Домашнє завдання. Генератори. Парсинг сайту.

Домашнее задание. Генераторы. Парсинг сайта.

Дан список url-ов.

urls = [‘https://www.lifewire.com/’, ‘https://www.geeksforgeeks.org/’,’https://rtfm.co.ua/’]

Використовуючи функцію-генератор у циклі пройтися цим масивом і зібрати всі посилання з головних сторінок всередині генератора.

for url in mygenerator():
    print(url)

Результат зберегти в json файли для кожної адреси.

www.lifewire.com.json

[
   {'title': '...', 'url': '..'}, {} ... 
]
Основи Python и Django. -> Парсинг сайтів.

Парсінг сайтів.

Припустимо, ми продаємо штори та хочемо створити інтернет магазин штор. Візьмемо якийсь приклад. У видачі “магазин штор” у мене першим вийшов https://pangardin.com.ua

Орієнтовна структура каталогу даних:

. 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: Дуже гарна тюль!

Я заклав мінімум інформації про каталог. Насамперед я визначаю тип інформації, для того щоб відрізняти опис категорії та товару та визначати в якому каталозі знаходиться програма. Або це каталог-категорія, або каталог-товар. Далі йде ознака опублікованості на сайті, сортування та інформація для сторінки категорії.

Так само можна оформити і статичні сторінки у файлах *.yml у каталозі pages. Наприклад так:

content_type: page
is_published: true
title_ua: Розробка інтернет-магазину штор.
 desc_ua: Як розробити інтернет магазин на <strong>python</strong>!

 Орієнтовна структура каталогу товарної позиції:

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

Файл опису товарної позиції.

 name_slug: shtory-raduga
 name_ua: Штори веселка.
meta_title_ru: ...
meta_keywords_ru: ...
meta_description_ru: ...
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 з категоріями та підкатегоріями.

Підкатегорії, крім назви, мають і посилання на сторінку з товарами, її ми також використовуватимемо для отримання інформації про продукти кожної взятої категорії.

Всі наведені нижче приклади наведено для операційної системи на базі Debian.

Створення віртуального оточення.

virtualenv -p python3 venv3

Після створення віртуального оточення необхідно його активувати, набравши таку команду в консолі.

. ./venv3/bin/activate

Встановити наші бібліотеки можна такими командами.

pip install requests
pip install beatifulsoup4

Якщо у вас не встановлена програма pip, то можна поставити її так:

sudo apt install python-pip

Нижче наведено Python код із докладними коментарями, який розбирає структуру категорій та виводить її на екран терміналу.

#!/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)
     # створюємо рядок в який вставляємо назви категорій
    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. -> Tornado мікро-фреймворк.

Мікрофреймворк Tornado.

Tornado — веб-сервер, що розширюється, не блокує і фреймворк, написаний на Python.

Його у 2009 році придбала компанія Facebook, після чого вихідні коди Tornado були відкриті.

Tornado був створений для забезпечення високої продуктивності і є одним із веб-серверів, здатних витримати проблему 10000 з’єднань.

Розглянемо основні прийоми роботи з цим фреймворком.

Запуск веб-сервера.

Підключення шаблонів та статики.

Робота з GET та POST запитами.

Шаблонізація.

Робота із базою даних.

Віддача json.

Встановлення

 pip install tornado

Перший сервер.

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Шаблонізація.

from tornado import template
loader = template.Loader("./templates")


class MainHandler(tornado.web.RequestHandler):
    def get(self):

        mydict = [
            {'name': 'Dima'},
            {'name': 'Vova'},
            {'name': 'Gena'},
        ]

        tpl = loader.load("index.html").generate(myvalue="XXX", mydict=mydict)
        self.write(tpl)

Шаблон.

<h1>Hello, world {{ myvalue }}  </h1>

<ul>
 {% for d in mydict %}
   {% block d %}
     <li>{{ d['name'] }}</li>
   {% end %}
 {% end %}
</ul>

Спадкування шаблонів

templates/layout.html

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css" >
    <title>Hello, ffff world!</title>
  </head>
  <body>
    <table border="1">
        <tr>
            <td> 
                <a href="/">Main</a> <br/>
                <a href="/contact">Contact</a>
            </td>
            <td>
                {% block content %}{% end %}
            </td>
        </tr>
    </table>
  </body>    
</html>


templates/index.html

{% extends "layout.html" %}

{% block content %}
<h1>Hello, world {{ myvalue }}  </h1>

<ul>
 {% for d in mydict %}
   {% block d %}
     <li>{{ d['name'] }}</li>
   {% end %}
 {% end %}
</ul>

{% end %}



templates/contact.html

{% extends "layout.html" %}

{% block content %}
<h1>Contact</h1>

<form method="POST" action="/contact">
  <input name="title" />
  <input type="submit" name="Submit" value="Submit" />
</form>

{% end %}

Автоматичне перезавантаження сервера під час зміни шаблонів та коду сервера.

def make_app():
    return tornado.web.Application([
        ...
    ], autoreload=True)

if __name__ == "__main__":
    app = make_app() 
    app.listen(8888) 
    tornado.autoreload.watch('./templates/index.html')
    tornado.autoreload.watch('./templates/layout.html')
    tornado.ioloop.IOLoop.current().start()

Відпрацювання запиту POST.

class ContactHendler(tornado.web.RequestHandler):
    def get(self):
        tpl = loader.load("contact.html").generate() 
        self.write(tpl) 
    def post(self):
        tpl = loader.load("contact.html").generate() 
        print(self.get_argument("title"))
        self.write(tpl)

   ...

   (r"/contact", ContactHendler),

Редирект.

 self.redirect('/tasks')

Робота з базою даних бібліотека tornado_mysql.

Встановлення.

sudo pip install Tornado-MySQL

Це бібліотека асинхронна та експериментальна.

Використовується із корутинами торнадо.

приклади.

class ContactHendler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', passwd='1q2w3e', db='test')
        cur = conn.cursor()
        yield cur.execute("select * from users")
        cur.close()
        records = cur.fetchall()
        tpl = loader.load("contact.html").generate(records=records) 
        self.write(tpl) 
    @gen.coroutine
    def post(self):
        conn = yield tornado_mysql.connect(host='127.0.0.1', port=3306, user='root', passwd='1q2w3e', db='test')
        cur = conn.cursor()
        yield cur.execute("insert into users (name) values ('%s')" % self.get_argument("title")) 
        conn.commit()
        cur.close()
        self.redirect('/contact')
Основи Python и Django. -> Створення проекту Django та імпорт даних у БД.

Створення інтернет-магазину штор.

У попередній частині ми скопіювали з сайту pangardin.com.ua каталог товарів та зберегли його у певній структурі каталогів.

Наступне завдання полягає в тому, щоб створити проект на Django та заповнити базу даних із нашої структури каталогів.

Для завантаження ми створимо команду import, яка запускатиметься таким чином:

./manage.py import

Створення проекту Django.

Створюємо проект командою.

django-admin.py startproject prj

При цьому створюється новий каталог проекту, в якому присутній однойменний каталог з налаштуваннями проекту.

Переходимо до каталогу проекту.

cd prj

Створюємо базу даних командою:

./manage.py migrate

При цьому створюється файл db.sqlite3 з таблицями програм, які постачаються з Django.

Створити програму в проекті можна за допомогою наступної команди.

./manage.py startapp shop

Після виконання цієї команди буде створено новий каталог shop у папці проекту.

Далі нам необхідно додати цю програму до списку змінної INSTALLED_APPS у файлі settings.py.

Таким чином ми включаємо наш додаток до проекту.

Заповнимо файл shop/models.py класами нашої моделі.

from django.db import models
from django.utils.safestring import mark_safe

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()

У класах моделі, кожен із яких представляє таблицю бази даних, властивості визначають поля таблиці.

Таблиці зв’язуються між собою спеціальним полем типу ForeignKey.

Після того, як всі класи створені, необхідно створити файл міграції командою:

./manage.py makemigrations

Файл міграції містить інструкції для оновлення бази даних, щоб їх застосувати необхідно виконати таку команду.

./manage.py migrate

Після цього таблиці будуть фізично створені.

Створення команди імпорту.

При створенні команди Django необхідно дотримати необхідну структуру каталогів.

Усередині папки програми shop необхідно створити каталог management і у ньому каталог commands.

У кожний із цих двох каталогів необхідно помістити порожній файл init.py для того, щоб позначити їх як пакети Python.

Тепер усередині каталогу commands створимо скрипт import.py, який і буде нашою командою.

Для того, щоб написати команду, потрібно створити клас Command, успадкувавши його від базового класу Django BaseCommand.

Потім потрібно переписати метод handle, який є точкою входу в команду.

from django.core.management.base import BaseCommand, CommandError

def import_catalog():
    pass

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

Визначимо змінну з абсолютним шляхом до папки з даними.

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

Після імпорту класів моделі ми спочатку очистимо всі таблиці.

from shop.models import *

def import_catalog():
    Subcategory.objects.all().delete()
    Category.objects.all().delete()
    Good.objects.all().delete()
    Image.objects.all().delete()

І далі в циклі пройдемо підкаталогами і прочитаємо вміст файлу meta.yml.

for item in os.listdir(DATA_DIR):
    if os.path.isdir(os.path.join(DATA_DIR,item)):
        # читаем meta.yml
        with open(os.path.join(DATA_DIR,item,'meta.yml'),'r') as f:
            rez = f.read()

Для того, щоб перетворити рядок, в якому знаходиться вміст файлу на словник, застосуємо бібліотеку pyyaml.

 pip install pyyaml

Далі в коді розпаримо вміст за допомогою функції load().

yml_data = yaml.load(rez)

Тепер у змінній yml_data ми маємо словник і можемо звертатися до даних за ключом, наприклад: yml_data[‘name_ru’]

Створимо категорії у базі, виключаючи дублі.

 # створюємо категорії в базі якщо такої немає
try: 
    cat = Category.objects.get(name_slug=yml_data['parent_slug'])
except:
    cat = Category()
    cat.name = yml_data['parent_name_ru']
    cat.name_slug = yml_data['parent_slug']
    cat.save()

Аналогічно вчинимо і з підкатегоріями:

 # створюємо підкатегорії в базі якщо такої немає
try: 
    scat = Subcategory.objects.get(name_slug=yml_data['name_slug'])
except:
    scat = Subcategory()
    scat.name = yml_data['name_ru']
    scat.name_slug = yml_data['name_slug']
    scat.category = cat
    scat.save()

Далі в циклі пробіжимо каталогами продуктів і викличемо функцію збереження товару.

for item_good in os.listdir(os.path.join(DATA_DIR,item)):
    if os.path.isdir(os.path.join(DATA_DIR,item,item_good)):
        import_goods(item_good,os.path.join(DATA_DIR,item),scat)

Зберігаємо тільки якщо в цикл потрапляє каталог.

Опишемо функцію збереження товару.

def import_goods(name_slug,path,sub_category):
    print('Importing ..... %s' % name_slug)
    # читаем meta.yml
    try:
        with open(os.path.join(path,name_slug,'meta.yml'),'r') as f:
            rez = f.read()
    except:
        return False
    # парсим yml формат
    yml_data = yaml.load(rez) 
    # сохраняем позицию товара
    g = Good()
    g.name = yml_data['name_ru']
    g.name_slug = yml_data['name_slug']
    g.desc = yml_data['description_ru']
    g.subcategory = sub_category
    g.save()

У цю функцію передається транслітна назва продукту, шлях до каталогу категорії та об’єкт категорії, де знаходиться товар.