Flask – полное руководство

20 августа, 2020

Подписывайся на наш канал в Telegram, чтобы ежедневно совершенствоваться в Python. Там выходят задачи, полезные советы и сливы платных курсов - перейти

Оглавление

  • 1 часть. Настройка проекта
    • Настройка Heroku
    • Постановочный / производственный процесс
    • Настройки конфигурации
      • Локальные настройки
      • Настройки Heroku
    • Вывод
  • 2 часть. Настройка Postgres, SQLAlchemy и Alembic
    • Требования к установке
    • Обновление конфигурацию
    • Модель данных
    • Локальная миграция
    • Удаленная миграция
    • Вывод
  • 3 часть. Обработка текста с помощью запросов, BeautifulSoup и NLTK
    • Требования к установке
    • Реорганизуйте индексный маршрут
    • Запросы
    • Обработка текста
      • Что творится?
    • Показать результаты
    • Сводка
  • 4 часть. Реализация очереди задач Redis
    • Требования к установке
    • Настройка Worker
    • Обновление app.py
    • Получить результаты
    • Что дальше?
  • 5 часть. Интеграция Flask и Angular
    • Текущая функциональность
    • Обноление index.html
    • Создание модуля Angular
      • Внедрение зависимостей и $ scope
    • Рефакторинг app.py
    • Базовый опрос
    • Обновление DOM
    • Заключение и дальнейшие шаги
  • 6 часть. Обновление промежуточной среды
    • Тестовый Push
    • Redis
    • Worker
    • Вывод
  • 7 часть. Обновление пользовательского интерфейса
    • Текущий пользовательский интерфейс
    • Смена кнопки
    • Добавление счетчика
    • Работа с ошибками
    • Вывод
  • 8 часть. Пользовательская Angular директива с D3
    • Текущий пользовательский интерфейс
    • Angular директива
    • Гистограмма D3
      • Шаг 1. Функциональная логика
      • Шаг 2. Стилизация гистограммы
      • Шаг 3. Сделайте гистограмму более интерактивной
      • Шаг 4. Очистите для следующего поиска URL
    • Заключение и дальнейшие шаги

Добро пожаловать! Сегодня мы собираемся приступить к созданию приложения Flask, которое вычисляет пары частотности слов на основе текста из заданного URL. Это полное руководство.

Полный код находится в репозитории

1 часть. Настройка проекта

Мы начнем с простого приложения Hello World на Heroku с промежуточной (или предпроизводственной) и производственной средами. Для первоначальной настройки вы должны иметь некоторое представление о следующих инструментах:

Сперва, давайте настроим рабочий каталог:

$ mkdir flask-by-example && cd flask-by-example

Инициализируйте новое репозиторий git в своем рабочем каталоге:

$ git init

Настройте виртуальную среду для использования в нашем приложении:

$ python3 -m venv env
$ source env/bin/activate

Теперь вы должны увидеть (env) слева от приглашения в терминале, указывая, что теперь вы работаете в виртуальной среде.

Чтобы покинуть виртуальную среду, просто запустите deactivate, а затем запустите, source env/bin/activateкогда будете готовы снова работать над своим проектом.

Далее мы собираемся создать нашу базовую структуру для нашего приложения. Добавьте следующие файлы в папку «flask-by-example»:

$ touch app.py .gitignore README.md requirements.txt

Это даст вам следующую структуру:

├── .gitignore
├── app.py
├── README.md
└── requirements.txt

Обязательно обновите файл .gitignore из репозитория.

Затем установите Flask:

$ python -m pip install Flask==1.1.1

Добавляем установленные библиотеки в наш файл requirements.txt:

$ python -m pip freeze > requirements.txt

Откройте app.py в своем любимом редакторе и добавьте следующий код:

from flask import Flask
app = Flask(__name__)


@app.route('/')
def hello():
    return "Hello World!"

if __name__ == '__main__':
    app.run()

Запустите приложение:

$ python app.py

И вы должны увидеть свое базовое приложение «Hello world» по адресу http://localhost:5000/ . Когда закончите, отключите сервер.

Далее мы собираемся настроить наши среды Heroku как для производственного, так и для тестового приложения.

Настройка Heroku

Если вы еще этого не сделали, создайте учетную запись Heroku, загрузите и установите Heroku Toolbelt , а затем в своем терминале введите heroku login , чтобы войти в Heroku.

После этого создайте Procfile в корневом каталоге вашего проекта:

$ touch Procfile

Добавьте следующую строку в ваш вновь созданный файл

web: gunicorn app:app

Убедитесь в том , чтобы вы добавили gunicorn к вашему requirments.txt файлу

$ python -m pip install gunicorn==20.0.4
$ python -m pip freeze > requirements.txt

Нам также необходимо указать версию Python, чтобы Heroku использовал правильную среду выполнения Python для запуска нашего приложения. Просто создайте файл с именем runtime.txt со следующим кодом:

python-3.8.1

Зафиксируйте свои изменения в git (и, возможно, отправьте их на Github), затем создайте два новых приложения Heroku.

Один для производства:

$ heroku create wordcount-pro

И еще один для постановки:

$ heroku create wordcount-stage

Эти имена теперь заняты, поэтому вам нужно сделать свое имя приложения Heroku уникальным.

Добавьте свои новые приложения в свой git. Обязательно назовите один удаленный pro (для «production»), а другой stage (для «staging»):

$ git remote add pro git@heroku.com:YOUR_APP_NAME.git
$ git remote add stage git@heroku.com:YOUR_APP_NAME.git

Теперь мы можем отправить оба наших приложения в Heroku.

  • Для staging: git push stage master
  • Для production: git push pro master

После того, как оба они были отправлены, откройте URL-адреса в своем веб-браузере, и, если все прошло хорошо, вы должны увидеть свое приложение в обеих средах.

Постановочный/производственный процесс

Давайте внесем изменения в наше приложение и продвинемся только до стадии:

from flask import Flask
app = Flask(__name__)


@app.route('/')
def hello():
    return "Hello World!"


@app.route('/<name>')
def hello_name(name):
    return "Hello {}!".format(name)

if __name__ == '__main__':
    app.run()

Запустите приложение локально, чтобы убедиться, что все работает – python app.py

Протестируйте его, добавив имя после URL-адреса, то есть http://localhost: 5000/mike .

Теперь давайте опробуем наши изменения на стадии постановки, прежде чем запускать их в производство. Убедитесь, что ваши изменения зафиксированы в git, а затем отправьте свою работу в промежуточную среду – git push stage master.

Теперь, если вы перейдете в промежуточную среду, вы сможете использовать новый URL-адрес, то есть «/mike», и получить «Hello NAME» в зависимости от того, что вы поместили в URL-адрес в качестве вывода в браузере. Однако, если вы попробуете то же самое на производственном сайте, вы получите ошибку. Таким образом, мы можем создавать вещи и тестировать их в тестовой среде, а затем, когда мы будем довольны изменениями, мы можем запустить их в производство.

Теперь, когда мы довольны этим, давайте перейдем к рабочей версии нашего сайта – git push pro master

Теперь у нас есть такая же функциональность на нашем рабочем сайте.

Этот рабочий процесс подготовки/производства позволяет нам вносить изменения, показывать вещи клиентам, экспериментировать и т. д. – и все это на изолированном сервере, не вызывая каких-либо изменений на действующем производственном сайте, который, в общем, используется пользователями

Настройки конфигурации

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

Добавьте файл config.py в корень вашего проекта:

$ touch config.py

В нашем файле конфигурации мы собираемся кое-что позаимствовать из того, как настраивается конфигурация Django. У нас будет базовый класс конфигурации, от которого наследуются другие классы конфигурации. Затем мы импортируем соответствующий класс конфигурации по мере необходимости.

Добавьте следующее в ваш вновь созданный файл config.py:

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config(object):
    DEBUG = False
    TESTING = False
    CSRF_ENABLED = True
    SECRET_KEY = 'this-really-needs-to-be-changed'


class ProductionConfig(Config):
    DEBUG = False


class StagingConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class DevelopmentConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class TestingConfig(Config):
    TESTING = True

Мы импортировали os, а затем установили переменную basedir как относительный путь из любого места, которое мы вызываем, к этому файлу. Затем мы настраиваем базовый класс Config() с некоторыми базовыми настройками, от которых наследуются другие наши классы конфигурации. Теперь мы сможем импортировать соответствующий класс конфигурации в зависимости от текущей среды. Таким образом, мы можем использовать переменные среды, чтобы выбрать, какие настройки мы собираемся использовать в зависимости от среды – например, локальные, промежуточные, производственные.

Локальные настройки

Чтобы настроить наше приложение с переменными среды, мы собираемся использовать autoenv. Эта программа позволяет нам устанавливать команды, которые будут запускаться каждый раз, когда мы войдем в наш каталог. Чтобы использовать его, нам необходимо установить его глобально. Сначала выйдите из виртуальной среды в терминале, установите autoenv, затем добавьте файл .env:

$ deactivate
$ pip install autoenv==1.0.0
$ touch .env$ deactivate
$ pip install autoenv==1.0.0
$ touch .env

Затем в ваш .env файл добавьте следующее:

source env/bin/activate
export APP_SETTINGS="config.DevelopmentConfig"

Выполните следующее, чтобы обновить, а затем обновите свой .bashrc:

$ echo "source `which activate.sh`" >> ~/.bashrc
$ source ~/.bashrc

Теперь, если вы переместите каталог вверх, а затем снова войдете в него cd, виртуальная среда будет автоматически запущена и будет объявлена ​​переменная APP_SETTINGS.


Настройки Heroku

Точно так же мы собираемся установить переменные среды на Heroku.

Для staging выполните следующую команду

$ heroku config:set APP_SETTINGS=config.StagingConfig --remote stage

Для production:

$ heroku config:set APP_SETTINGS=config.ProductionConfig --remote pro

Чтобы убедиться, что мы используем правильную среду, измените app.py:

import os
from flask import Flask


app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])


@app.route('/')
def hello():
    return "Hello World!"


@app.route('/<name>')
def hello_name(name):
    return "Hello {}!".format(name)

if __name__ == '__main__':
    app.run()

Мы импортировали os и использовали метод os.environ для импорта соответствующих переменных APP_SETTINGS, в зависимости от нашей среды. Затем мы настраиваем конфигурацию в нашем приложении с помощью метода app.config.from_object.

Зафиксируйте и отправьте изменения как в промежуточную, так и в рабочую среду (и Github, если он у вас настроен).

Хотите проверить переменные среды, чтобы убедиться, что она определяет правильную среду (проверка работоспособности!)? Добавьте оператор печати в app.py:

print(os.environ['APP_SETTINGS'])

Теперь, когда вы запустите приложение, оно покажет, какие настройки конфигурации оно импортирует:

Local:

$ python app.py
config.DevelopmentConfig

Зафиксируйте и снова подтолкните к staging и production. А теперь давайте проверим …

Staging:

$ heroku run python app.py --app wordcount-stage
Running python app.py on wordcount-stage... up, run.7699
config.StagingConfig

Production:

$ heroku run python app.py --app wordcount-pro
Running python app.py on wordcount-pro... up, run.8934
config.ProductionConfig

Обязательно удалите print (os.environ [‘APP_SETTINGS’]), когда закончите, зафиксируйте и отправьте обратно в различные среды.

Вывод первой части

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

2 часть. Настройка Postgres, SQLAlchemy и Alembic

В этой части мы собираемся настроить базу данных Postgres для хранения результатов подсчета слов, а также SQLAlchemy, Object Relational Mapper и Alembic для обработки миграции базы данных.

Установка всего необходимого

Инструменты, используемые в этой части:

  • PostgreSQL ( 11.6 )
  • Psycopg2 ( 2.8.4 ) – адаптер Python для Postgres
  • Flask-SQLAlchemy ( 2.4.1 ) – расширение Flask, обеспечивающее поддержку SQLAlchemy
  • Flask-Migrate ( 2.5.2 ) – расширение, которое поддерживает миграцию базы данных SQLAlchemy через Alembic

Для начала установите Postgres на свой локальный компьютер, если у вас его еще нет. Поскольку Heroku использует Postgres, нам будет хорошо разрабатывать локально в той же базе данных. Если у вас не установлен Postgres, Postgres.app – это простой способ начать работу для пользователей Mac OS X. Посетите страницу загрузки для получения дополнительной информации.

После установки и запуска Postgres создайте базу данных с именем wordcount_dev, которая будет использоваться в качестве нашей локальной базы данных для разработки:

$ psql
# create database wordcount_dev;
CREATE DATABASE
# \q

Чтобы использовать нашу недавно созданную базу данных в приложении Flask, нам нужно установить несколько вещей:

$ cd flask-by-example

cd в каталоге должен активировать виртуальную среду и установить переменные среды, найденные в .env файле, с помощью autoenv , который мы установили в части 1.

$ python -m pip install psycopg2==2.8.4 Flask-SQLAlchemy===2.4.1 Flask-Migrate==2.5.2
$ python -m pip freeze > requirements.txt

Если вы используете OS X и у вас возникли проблемы с установкой psycopg2, прочтите эту статью о переполнении стека. Возможно, вам придется установить psycopg2-binary вместо psycopg2 , если ваша установка не удалась.

Обновление конфигурации

Добавьте поле SQLALCHEMY_DATABASE_URI в класс Config() в файле config.py, чтобы настроить приложение для использования вновь созданной базы данных при разработке (локальной), промежуточной и производственной:

import os

class Config(object):
    ...
    SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']

Ваш файл config.py теперь должен выглядеть так:

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config(object):
    DEBUG = False
    TESTING = False
    CSRF_ENABLED = True
    SECRET_KEY = 'this-really-needs-to-be-changed'
    SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']


class ProductionConfig(Config):
    DEBUG = False


class StagingConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class DevelopmentConfig(Config):
    DEVELOPMENT = True
    DEBUG = True


class TestingConfig(Config):
    TESTING = True

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

Подобно тому, как мы добавляли переменную среды в предыдущем посте, мы собираемся добавить переменную DATABASE_URL. Запустите это в терминале:

$ export DATABASE_URL="postgresql:///wordcount_dev"

А затем добавьте эту строку в свой файл .env.

В вашем файле app.py импортируйте SQLAlchemy и подключитесь к базе данных:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os


app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

from models import Result


@app.route('/')
def hello():
    return "Hello World!"


@app.route('/<name>')
def hello_name(name):
    return "Hello {}!".format(name)


if __name__ == '__main__':
    app.run()

Модель данных

Настройте базовую модель, добавив файл models.py:

from app import db
from sqlalchemy.dialects.postgresql import JSON


class Result(db.Model):
    __tablename__ = 'results'

    id = db.Column(db.Integer, primary_key=True)
    url = db.Column(db.String())
    result_all = db.Column(JSON)
    result_no_stop_words = db.Column(JSON)

    def __init__(self, url, result_all, result_no_stop_words):
        self.url = url
        self.result_all = result_all
        self.result_no_stop_words = result_no_stop_words

    def __repr__(self):
        return '<id {}>'.format(self.id)

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

Сначала мы импортируем соединение с базой данных, созданное в нашем файле app.py, а также JSON из диалектов PostgreSQL SQLAlchemy. Столбцы JSON довольно новы для Postgres и доступны не во всех базах данных, поддерживаемых SQLAlchemy, поэтому нам нужно импортировать их специально.

Затем мы создали класс Result () и присвоили ему имя таблицы результатов. Затем мы устанавливаем атрибуты, которые хотим сохранить для результата –

  • Идентификатор результата, который мы сохранили
  • URL, из которого мы посчитали слова
  • Полный список слов, которые мы посчитали
  • Список слов, которые мы посчитали за вычетом стоп-слов (подробнее об этом позже)

Затем мы создали метод __init __(), который будет запускаться при первом создании нового результата, и, наконец, метод __repr __() для представления объекта, когда мы запрашиваем его.

Локальная миграция

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

Примечание: Flask-Migrate использует новый инструмент CLI Flasks. Однако в этой статье используется интерфейс, предоставляемый Flask-Script, который ранее использовался Flask-Migrate. Чтобы использовать его, вам необходимо установить его через:

$ python -m pip install Flask-Script==2.0.6
$ python -m pip freeze > requirements.txt

Создайте новый файл с именем manage.py:

import os
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

from app import app, db


app.config.from_object(os.environ['APP_SETTINGS'])

migrate = Migrate(app, db)
manager = Manager(app)

manager.add_command('db', MigrateCommand)


if __name__ == '__main__':
    manager.run()

Чтобы использовать Flask-Migrate, мы импортировали Manager, а также Migrate и MigrateCommand в наш файл manage.py. Мы также импортировали приложение и базу данных, поэтому у нас есть доступ к ним из сценария.

Во-первых, мы настраиваем нашу конфигурацию так, чтобы наша среда – на основе переменной среды – создавала экземпляр migrate с app и db в качестве аргументов и настраивала команду менеджера для инициализации экземпляра Manager для нашего приложения. Наконец, мы добавили в manager команду db, чтобы мы могли запускать миграции из командной строки.

Для запуска миграции инициализируйте Alembic:

$ python manage.py db init
  Creating directory /flask-by-example/migrations ... done
  Creating directory /flask-by-example/migrations/versions ... done
  Generating /flask-by-example/migrations/alembic.ini ... done
  Generating /flask-by-example/migrations/env.py ... done
  Generating /flask-by-example/migrations/README ... done
  Generating /flask-by-example/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/flask-by-example/migrations/alembic.ini' before proceeding.

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

Давайте создадим нашу первую миграцию, выполнив команду migrate.

$ python manage.py db migrate
  INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
  INFO  [alembic.runtime.migration] Will assume transactional DDL.
  INFO  [alembic.autogenerate.compare] Detected added table 'results'
    Generating /flask-by-example/migrations/versions/63dba2060f71_.py
    ... done

Теперь вы заметите, что в папке «версии» есть файл миграции. Этот файл автоматически создается Alembic на основе модели. Вы можете создать (или отредактировать) этот файл самостоятельно; однако в большинстве случаев подойдет автоматически созданный файл.

Теперь мы применим обновления к базе данных с помощью команды db upgrade:

$ python manage.py db upgrade
  INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
  INFO  [alembic.runtime.migration] Will assume transactional DDL.
  INFO  [alembic.runtime.migration] Running upgrade  -> 63dba2060f71, empty message

База данных теперь готова для использования в нашем приложении:

$ psql
# \c wordcount_dev
You are now connected to database "wordcount_dev" as user "michaelherman".
# \dt

                List of relations
 Schema |      Name       | Type  |     Owner
--------+-----------------+-------+---------------
 public | alembic_version | table | michaelherman
 public | results         | table | michaelherman
(2 rows)

# \d results
                                     Table "public.results"
        Column        |       Type        |                      Modifiers
----------------------+-------------------+------------------------------------------------------
 id                   | integer           | not null default nextval('results_id_seq'::regclass)
 url                  | character varying |
 result_all           | json              |
 result_no_stop_words | json              |
Indexes:
    "results_pkey" PRIMARY KEY, btree (id)

Удаленная миграция

Наконец, давайте применим миграции к базам данных на Heroku. Однако сначала нам нужно добавить детали промежуточной и производственной баз данных в файл config.py.

Чтобы проверить, настроена ли у нас база данных на промежуточном сервере, запустите:

$ heroku config --app wordcount-stage
=== wordcount-stage Config Vars
APP_SETTINGS: config.StagingConfig

Обязательно замените wordcount-stage названием вашего промежуточного приложения.

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

$ heroku addons:create heroku-postgresql:hobby-dev --app wordcount-stage
  Creating postgresql-cubic-86416... done, (free)
  Adding postgresql-cubic-86416 to wordcount-stage... done
  Setting DATABASE_URL and restarting wordcount-stage... done, v8
  Database has been created and is available
   ! This database is empty. If upgrading, you can transfer
   ! data from another database with pg:copy
  Use `heroku addons:docs heroku-postgresql` to view documentation.

hobby-dev – это бесплатный уровень аддона Heroku Postgres.

Теперь, когда мы снова запускаем heroku config –app wordcount-stage, мы должны увидеть настройки подключения для базы данных:

=== wordcount-stage Config Vars
APP_SETTINGS: config.StagingConfig
DATABASE_URL: postgres://azrqiefezenfrg:Zti5fjSyeyFgoc-U-yXnPrXHQv@ec2-54-225-151-64.compute-1.amazonaws.com:5432/d2kio2ubc804p7

Затем нам нужно зафиксировать изменения, которые вы внесли в git, и отправить на промежуточный сервер:

$ git push stage master

Запустите миграции, которые мы создали для миграции нашей промежуточной базы данных, с помощью команды heroku run:

$ heroku run python manage.py db upgrade --app wordcount-stage
  Running python manage.py db upgrade on wordcount-stage... up, run.5677
  INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
  INFO  [alembic.runtime.migration] Will assume transactional DDL.
  INFO  [alembic.runtime.migration] Running upgrade  -> 63dba2060f71, empty message

Обратите внимание на то, что мы выполнили только обновление, а не команды init или migrate, как раньше. Наш файл миграции уже настроен и готов к работе; нам просто нужно применить его к базе данных Heroku.

А теперь сделаем то же самое с производством.

  1. Настройте базу данных для своего производственного приложения на Heroku, как вы это делали для постановки: heroku addons:create heroku-postgresql:hobby-dev --app wordcount-pro
  2. Отправьте изменения на рабочий сайт: git push pro master Обратите внимание, что вам не нужно вносить какие-либо изменения в файл конфигурации – он устанавливает базу данных на основе недавно созданной переменной среды DATABASE_URL.
  3. Примените миграции: heroku run python manage.py db upgrade --app wordcount-pro

Теперь и на нашем промежуточном, и на производственном сайтах базы данных настроены, они перенесены – и готовы к работе!

Когда вы применяете новую миграцию к производственной базе данных, может возникнуть время простоя. Если это проблема, вы можете настроить репликацию базы данных, добавив «подчиненную» (обычно известную как подчиненную) базу данных. Подробнее об этом читайте в официальной документации Heroku

Вывод второй части

Конец второй части. В части 3 мы собираемся создать функцию подсчета слов и отправить ее в очередь задач, чтобы иметь дело с более длительной обработкой подсчета слов.

3 часть. Обработка текста с помощью запросов, BeautifulSoup и NLTK

В этой части серии мы собираемся очистить содержимое веб-страницы, а затем обработать текст для отображения количества слов.

Установка всего необходимого

Используемые инструменты:

  • requests( 2.22.0 ) – библиотека для отправки HTTP-запросов
  • BeautifulSoup ( 4.8.2 ) – инструмент, используемый для парсинга и анализа документов из Интернета.
  • Natural Language Toolkit ( 3.4.5 ) – библиотека обработки естественного языка

Перейдите в каталог проекта, чтобы активировать виртуальную среду через autoenv, а затем установите требования:

$ cd flask-by-example
$ python -m pip install requests==2.22.0 beautifulsoup4==4.8.2 nltk==3.4.5
$ python -m pip freeze > requirements.txt

Рефакторинг Index Route

Для начала давайте избавимся от части “hello world” в Index Route в нашем файле app.py и настроим Route для отображения формы для приема URL-адресов. Сначала добавьте папку шаблонов для хранения наших шаблонов и добавьте в нее файл index.html.

$ mkdir templates
$ touch templates/index.html

Настройте очень простую HTML-страницу:

<!DOCTYPE html>
<html>
  <head>
    <title>Wordcount</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <style>
      .container {
        max-width: 1000px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Wordcount 3000</h1>
      <form role="form" method='POST' action='/'>
        <div class="form-group">
          <input type="text" name="url" class="form-control" id="url-box" placeholder="Enter URL..." style="max-width: 300px;" autofocus required>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>
      <br>
      {% for error in errors %}
        <h4>{{ error }}</h4>
      {% endfor %}
    </div>
    <script src="//code.jquery.com/jquery-2.2.1.min.js"></script>
    <script src="//netdna.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
  </body>
</html>

Мы использовали Bootstrap, чтобы добавить немного стиля, чтобы наша страница не выглядела полностью отвратительной. Затем мы добавили форму с полем ввода текста, в которое пользователи могут вводить URL. Кроме того, мы использовали цикл Jinja for для перебора списка ошибок, отображая каждую из них.

Обновите app.py для обслуживания шаблона:

import os
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

from models import Result


@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')


if __name__ == '__main__':
    app.run()

Почему оба метода HTTP methods, methods=['GET', 'POST']? Что ж, в конечном итоге мы будем использовать один и тот же маршрут для запросов GET и POST – для обслуживания страницы index.html и обработки отправки формы соответственно.

Запустите приложение, чтобы проверить его:

$ python manage.py runserver

Перейдите по адресу http://localhost:5000/ , и вы должны увидеть, как форма смотрит на вас.

Requests

Теперь давайте воспользуемся библиотекой requests, чтобы извлечь HTML-страницу из отправленного URL.

Измените свой индексный маршрут следующим образом:

@app.route('/', methods=['GET', 'POST'])
def index():
    errors = []
    results = {}
    if request.method == "POST":
        # get url that the user has entered
        try:
            url = request.form['url']
            r = requests.get(url)
            print(r.text)
        except:
            errors.append(
                "Unable to get URL. Please make sure it's valid and try again."
            )
    return render_template('index.html', errors=errors, results=results)

Не забудьте также обновить импорт:

import os
import requests
from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
  • Здесь мы импортировали библиотеку запросов, а также объект запроса из Flask. Первый используется для отправки внешних HTTP-запросов GET для получения определенного пользователем URL-адреса, а второй используется для обработки запросов GET и POST в приложении Flask.
  • Затем мы добавили переменные для фиксации ошибок и результатов, которые передаются в шаблон.
  • Внутри самого представления мы проверили, является ли запрос GET или POST-
    • Если POST: мы взяли значение (URL) из формы и присвоили его переменной url. Затем мы добавили исключение для обработки любых ошибок и, при необходимости, добавили общее сообщение об ошибке в список ошибок. Наконец, мы визуализировали шаблон, включая список ошибок и словарь результатов.
    • Если GET: мы просто визуализировали шаблон.

Давайте проверим это:

$ python manage.py runserver

Вы должны иметь возможность ввести действительную веб-страницу, и в терминале вы увидите текст возвращенной страницы.

Примечание. Убедитесь, что ваш URL-адрес включает http:// или https://. В противном случае наше приложение не обнаружит, что это действительный URL.

Обработка текста

Имея в руках HTML-код, давайте посчитаем частоту слов, которые появляются на странице, и покажем их конечному пользователю. Обновите свой код в app.py до следующего, и мы рассмотрим, что происходит:

import os
import requests
import operator
import re
import nltk
from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
from stop_words import stops
from collections import Counter
from bs4 import BeautifulSoup


app = Flask(__name__)
app.config.from_object(os.environ['APP_SETTINGS'])
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)

from models import Result


@app.route('/', methods=['GET', 'POST'])
def index():
    errors = []
    results = {}
    if request.method == "POST":
        # get url that the person has entered
        try:
            url = request.form['url']
            r = requests.get(url)
        except:
            errors.append(
                "Unable to get URL. Please make sure it's valid and try again."
            )
            return render_template('index.html', errors=errors)
        if r:
            # text processing
            raw = BeautifulSoup(r.text, 'html.parser').get_text()
            nltk.data.path.append('./nltk_data/')  # set the path
            tokens = nltk.word_tokenize(raw)
            text = nltk.Text(tokens)
            # remove punctuation, count raw words
            nonPunct = re.compile('.*[A-Za-z].*')
            raw_words = [w for w in text if nonPunct.match(w)]
            raw_word_count = Counter(raw_words)
            # stop words
            no_stop_words = [w for w in raw_words if w.lower() not in stops]
            no_stop_words_count = Counter(no_stop_words)
            # save the results
            results = sorted(
                no_stop_words_count.items(),
                key=operator.itemgetter(1),
                reverse=True
            )
            try:
                result = Result(
                    url=url,
                    result_all=raw_word_count,
                    result_no_stop_words=no_stop_words_count
                )
                db.session.add(result)
                db.session.commit()
            except:
                errors.append("Unable to add item to database.")
    return render_template('index.html', errors=errors, results=results)


if __name__ == '__main__':
    app.run()

Создайте новый файл с именем stop_words.py и добавьте следующий список:

stops = [
    'i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you',
    'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his',
    'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself',
    'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which',
    'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are',
    'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having',
    'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if',
    'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for',
    'with', 'about', 'against', 'between', 'into', 'through', 'during',
    'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in',
    'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then',
    'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any',
    'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no',
    'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's',
    't', 'can', 'will', 'just', 'don', 'should', 'now', 'id', 'var',
    'function', 'js', 'd', 'script', '\'script', 'fjs', 'document', 'r',
    'b', 'g', 'e', '\'s', 'c', 'f', 'h', 'l', 'k'
]

Что творится?

Обработка текста

  • В нашем маршруте индекса мы использовали beautifulsoup для очистки текста, удалив HTML-теги, которые мы получили обратно из URL-адреса, а также nltk to-
  • Чтобы nltk работал правильно, вам необходимо загрузить правильные токенизаторы. Сначала создайте новый каталог – mkdir nltk_data – затем запустите – python -m nltk.downloader .

Когда появится окно установки, обновите «Каталог загрузок», указав любой_абсолютный_путь_к_вашему_приложению /nltk_data/ .

Затем перейдите на вкладку «Модели» и выберите «пункт» в столбце «Идентификатор». Нажмите «Загрузить». Дополнительную информацию можно найти в официальной документации.

Удаление знаков препинания, подсчет необработанных слов

  1. Поскольку мы не хотим, чтобы знаки препинания учитывались в окончательных результатах, мы создали регулярное выражение, которое соответствует чему-либо, не входящему в стандартный алфавит.
  2. Затем, используя понимание списка, мы создали список слов без знаков препинания и цифр.
  3. Наконец, мы подсчитали, сколько раз каждое слово появлялось в списке, используя Counter .

Стоп-слова

Наш текущий вывод содержит множество слов, которые мы, вероятно, не хотим считать, например, «я», «меня», «тот» и так далее. Это так называемые стоп-слова.

  1. Со stops списком мы снова использовали понимание списка, чтобы создать окончательный список слов, которые не включают эти стоп-слова.
  2. Затем мы создали словарь со словами (в качестве ключей) и связанными с ними счетчиками (в качестве значений).
  3. И, наконец, мы использовали метод сортировки, чтобы получить отсортированное представление нашего словаря. Теперь мы можем использовать отсортированные данные для отображения слов с наибольшим количеством в верхней части списка, что означает, что нам не придется выполнять такую ​​сортировку в нашем шаблоне Jinja.

Для более надежного списка стоп-слов используйте корпус NLTK стоп-слов .

Сохраните результаты

Наконец, мы использовали try/except для сохранения результатов поиска и последующих подсчетов в базе данных.

Показать результаты

Давайте обновим index.html, чтобы отобразить результаты:

<!DOCTYPE html>
<html>
  <head>
    <title>Wordcount</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <style>
      .container {
        max-width: 1000px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-sm-5 col-sm-offset-1">
          <h1>Wordcount 3000</h1>
          <br>
          <form role="form" method="POST" action="/">
            <div class="form-group">
              <input type="text" name="url" class="form-control" id="url-box" placeholder="Enter URL..." style="max-width: 300px;">
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
          </form>
          <br>
          {% for error in errors %}
            <h4>{{ error }}</h4>
          {% endfor %}
          <br>
        </div>
        <div class="col-sm-5 col-sm-offset-1">
          {% if results %}
            <h2>Frequencies</h2>
            <br>
            <div id="results">
              <table class="table table-striped" style="max-width: 300px;">
                <thead>
                  <tr>
                    <th>Word</th>
                    <th>Count</th>
                  </tr>
                </thead>
                {% for result in results%}
                  <tr>
                    <td>{{ result[0] }}</td>
                    <td>{{ result[1] }}</td>
                  </tr>
                {% endfor %}
              </table>
            </div>
          {% endif %}
        </div>
      </div>
    </div>
    <br><br>
    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
    <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
  </body>
</html>

Здесь мы добавили оператор if, чтобы увидеть, есть ли что-нибудь в нашем словаре результатов, а затем добавили цикл for для перебора результатов и отображения их в таблице. Запустите приложение, и вы должны иметь возможность ввести URL-адрес и вернуть количество слов на странице.

$ python manage.py runserver

Что, если бы мы хотели отображать только первые десять ключевых слов?

results = sorted(
    no_stop_words_count.items(),
    key=operator.itemgetter(1),
    reverse=True
)[:10]

Проверьте это.

Cводка

Хорошо, отлично. Имея URL-адрес, мы можем подсчитать количество слов на странице. Если вы используете сайт без большого количества слов, например https://realpython.com, обработка должна происходить довольно быстро. Но что будет, если на сайте много слов? Например, попробуйте https://gutenberg.ca. Вы заметите, что это занимает больше времени.

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

Какое решение?

Вместо того, чтобы подсчитывать слова после того, как каждый пользователь делает запрос, нам нужно использовать очередь для обработки этого запроса в бэкэнде – именно с этого места мы начнем в следующий раз в Части 4.

На данный момент зафиксируйте свой код, но перед тем, как перейти к Heroku, вы должны удалить все языковые токенизаторы, кроме английского, вместе с zip-файлом. Это значительно уменьшит размер коммита. Однако имейте в виду, что если вы обрабатываете неанглийский сайт, он будет обрабатывать только английские слова.

└── nltk_data
    └── tokenizers
        └── punkt
            ├── PY3
            │   └── english.pickle
            └── english.pickle

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

$ git push stage master
Flask by example part 3 final

Проверьте это на постановке.

Часть 5. Интеграция Flask и Angular

Добро пожаловать. С настройкой очереди задач Redis давайте воспользуемся AngularJS для опроса серверной части, чтобы увидеть, завершена ли задача, а затем обновим DOM, как только данные станут доступны.

Готовы? Давайте начнем с рассмотрения текущего состояния нашего приложения…

Текущая функциональность

Сначала запустите Redis в одном окне терминала:

$ redis-server

В другом окне перейдите в каталог вашего проекта и запустите worker:

$ cd flask-by-example
$ python worker.py
20:38:04 RQ worker started, version 0.5.6
20:38:04
20:38:04 *** Listening on default...

Наконец, откройте третье окно терминала, перейдите в каталог своего проекта и запустите основное приложение:

$ cd flask-by-example
$ python manage.py runserver

Откройте http: // localhost: 5000 / и проверьте URL-адрес https://realpython.com. В терминале должен быть выведен идентификатор задания. Возьмите идентификатор и перейдите по этому URL-адресу:

http://localhost:5000/results/add_the_job_id_here

Вы должны увидеть аналогичный ответ JSON в своем браузере:

[
  [
    "Python", 
    315
  ], 
  [
    "intermediate", 
    167
  ], 
  [
    "python", 
    161
  ], 
  [
    "basics", 
    118
  ], 
  [
    "web-dev", 
    108
  ], 
  [
    "data-science", 
    51
  ], 
  [
    "best-practices", 
    49
  ], 
  [
    "advanced", 
    45
  ], 
  [
    "django", 
    43
  ], 
  [
    "flask", 
    41
  ]
]

Теперь мы готовы добавить Angular.

Обновление index.html

Добавьте Angular в index.html:

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>

Добавьте следующие директивы в index.html:

  1. ng-app: <html ng-app=”WordcountApp”>
  2. ng-controller: <body ng-controller=”WordcountController”>
  3. ng-submit: <form role=”form” ng-submit=”getResults()”>

Итак, мы загрузили Angular – который сообщает Angular рассматривать этот HTML-документ как приложение Angular – добавили контроллер, а затем добавили функцию getResults(), которая запускается при отправке формы.

Создание модуля Angular

Создайте «статический» каталог, а затем добавьте в него файл с именем main.js. Обязательно добавьте требование в файл index.html:

<script src="{{ url_for('static', filename='main.js') }}"></script>

Начнем с этого базового кода:

(function () {
  'use strict';

  angular.module('WordcountApp', [])

  .controller('WordcountController', ['$scope', '$log',
    function($scope, $log) {
      $scope.getResults = function() {
        $log.log("test");
      };
    }
  ]);

}());

Здесь, когда форма отправляется, вызывается getResults(), который просто записывает текст «test» в консоль JavaScript в браузере. Обязательно проверьте это.

Внедрение зависимостей и $scope

В приведенном выше примере мы использовали внедрение зависимостей, чтобы «внедрить» объект $scope и службу $log. Остановитесь здесь. Очень важно понимать $scope. Начните с документации по Angular, а затем обязательно прочтите вводное руководство по Angular, если вы еще этого не сделали.

$scope может показаться сложным, но на самом деле это просто средство связи между представлением и контроллером. Оба имеют к нему доступ, и когда вы изменяете переменную, прикрепленную к $scope в одной, переменная автоматически обновляется в другой (привязка данных). То же самое можно сказать и о внедрении зависимостей: это намного проще, чем кажется. Думайте об этом как о небольшом волшебстве для получения доступа к различным службам. Итак, внедрив службу, мы теперь можем использовать ее в нашем контроллере.

Вернуться в наше приложение…

Если вы проверите это, вы увидите, что отправка формы больше не отправляет запрос POST на серверную часть. Это именно то, что мы хотим. Вместо этого мы воспользуемся сервисом Angular $http для асинхронной обработки этого запроса:

.controller('WordcountController', ['$scope', '$log', '$http',
  function($scope, $log, $http) {

  $scope.getResults = function() {

    $log.log("test");

    // get the URL from the input
    var userInput = $scope.url;

    // fire the API request
    $http.post('/start', {"url": userInput}).
      success(function(results) {
        $log.log(results);
      }).
      error(function(error) {
        $log.log(error);
      });

  };

}
]);

Также обновите элемент ввода в index.html:

<input type="text" ng-model="url" name="url" class="form-control" id="url-box" placeholder="Enter URL..." style="max-width: 300px;">

Мы внедрили службу $ http, взяли URL-адрес из поля ввода (через ng-model = “url”), а затем отправили запрос POST в серверную часть. Обратные вызовы успеха и ошибки обрабатывают ответ. В случае ответа 200 он будет обработан обработчиком успеха, который, в свою очередь, записывает ответ в консоль.

Перед тестированием давайте сделаем рефакторинг серверной части, так как конечная точка /start в настоящее время не существует.

Рефакторинг app.py

Выполните рефакторинг создания задания Redis из функции просмотра индекса, а затем добавьте его в новую функцию просмотра с именем get_counts():

@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')


@app.route('/start', methods=['POST'])
def get_counts():
    # this import solves a rq bug which currently exists
    from app import count_and_save_words

    # get url
    data = json.loads(request.data.decode())
    url = data["url"]
    if not url[:8].startswith(('https://', 'http://')):
        url = 'http://' + url
    # start job
    job = q.enqueue_call(
        func=count_and_save_words, args=(url,), result_ttl=5000
    )
    # return created job id
    return job.get_id()

Не забудьте добавить следующий импорт вверху:

import json

Эти изменения должны быть простыми.

Теперь тестируем. Обновите браузер, отправьте новый URL. Вы должны увидеть идентификатор задания в консоли JavaScript. Отлично. Теперь, когда у Angular есть идентификатор задания, мы можем добавить функцию опроса.

Базовый опрос

Обновите main.js, добавив в контроллер следующий код:

function getWordCount(jobID) {
  var timeout = "";

  var poller = function() {
    // fire another request
    $http.get('/results/'+jobID).
      success(function(data, status, headers, config) {
        if(status === 202) {
          $log.log(data, status);
        } else if (status === 200){
          $log.log(data);
          $timeout.cancel(timeout);
          return false;
        }
        // continue to call the poller() function every 2 seconds
        // until the timeout is cancelled
        timeout = $timeout(poller, 2000);
      });
  };
  poller();
}

Затем обновите обработчик success в запросе POST:

$http.post('/start', {"url": userInput}).
  success(function(results) {
    $log.log(results);
    getWordCount(results);

  }).
  error(function(error) {
    $log.log(error);
  });

Не забудьте также добавить в контроллер службу $timeout.

Что тут происходит?

  1. Успешный HTTP-запрос приводит к срабатыванию функции getWordCount()
  2. Внутри функции polle() мы вызвали конечную точку /results/job_id.
  3. Используя службу $timeout, эта функция продолжает срабатывать каждые 2 секунды до тех пор, пока тайм-аут не будет отменен, когда будет возвращен ответ 200 вместе с подсчетом слов. Ознакомьтесь с документацией по Angular, чтобы узнать, как работает служба $timeout.

Когда вы это протестируете, обязательно откройте консоль JavaScript. Вы должны увидеть что-то подобное:

Nay! 202
Nay! 202
Nay! 202
Nay! 202
Nay! 202
Nay! 202
(10) [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]

Итак, в приведенном выше примере функция poller() вызывается семь раз. Первые шесть вызовов вернули 202, а последний вызов вернул 200 вместе с массивом подсчета слов.

Отлично.

Теперь нам нужно добавить количество слов в DOM

Обновление DOM

Обновите index.html:

<div class="container">
  <div class="row">
    <div class="col-sm-5 col-sm-offset-1">
      <h1>Wordcount 3000</h1>
      <br>
      <form role="form" ng-submit="getResults()">
        <div class="form-group">
          <input type="text" name="url" class="form-control" id="url-box" placeholder="Enter URL..." style="max-width: 300px;" ng-model="url" required>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>
    </div>
    <div class="col-sm-5 col-sm-offset-1">
      <h2>Frequencies</h2>
      <br>
      {% raw %}
      <div id="results">

          {{wordcounts}}

      </div>
      {% endraw %}
    </div>
  </div>
</div>

Что мы изменили?

  1. input тег теперь имеет required
  2. Попрощайтесь с тегами шаблона Jinja2. Jinja2 обслуживается на стороне сервера, и поскольку опрос полностью обрабатывается на стороне клиента, нам нужно использовать теги Angular. Тем не менее, поскольку теги шаблонов Jinja2 и Angular используют двойные фигурные скобки, {{}}, мы должны экранировать теги Jinja2, используя {% raw %} и {% endraw %}. Если вам нужно использовать несколько тегов Angular, рекомендуется изменить теги шаблонов, которые использует AngularJS, с помощью $interpolateProvider. Для получения дополнительной информации ознакомьтесь с документацией по Angular.

Во-вторых, обновите обработчик успеха в функции poller():

success(function(data, status, headers, config) {
  if(status === 202) {
    $log.log(data, status);
  } else if (status === 200){
    $log.log(data);
    $scope.wordcounts = data;
    $timeout.cancel(timeout);
    return false;
  }
  // continue to call the poller() function every 2 seconds
  // until the timeout is cancelled
  timeout = $timeout(poller, 2000);
});

Здесь мы прикрепили результаты к объекту $scope, чтобы он был доступен в View.

Проверьте это. Если все прошло хорошо, вы должны увидеть объект в DOM. Не очень красиво, но это легко исправить с помощью Bootstrap: добавьте следующий код под div с id=results и удалите теги {% raw %} и {% endraw %}, которые обертывали div результатов из приведенного выше кода:

<div id="results">
  <table class="table table-striped">
    <thead>
      <tr>
        <th>Word</th>
        <th>Count</th>
      </tr>
    </thead>
    <tbody>
      {% raw %}
      <tr ng-repeat="element in wordcounts">

        <td>{{ element[0] }}</td>
        <td>{{ element[1] }}</td>

      </tr>
    {% endraw %}
    </tbody>
  </table>
</div>

Заключение пятой части и дальнейшие шаги

Прежде чем перейти к построению графиков с помощью D3, нам все еще необходимо:

  1. Добавьте счетчик загрузки: также известный как throbber, он будет отображаться, пока задача не будет выполнена, чтобы конечный пользователь знал, что что-то происходит.
  2. Реорганизуйте Angular Controller: прямо сейчас в контроллере происходит слишком много (логики). Нам нужно перенести большую часть функциональности в сервис. Мы обсудим, почему и как
  3. Обновление промежуточной среды: нам нужно обновить промежуточную среду на Heroku – добавив изменения кода, нашего воркера и Redis.

Часть 6. Обновление промежуточной среды

В этой части мы настроим Redis на Heroku и посмотрим, как запустить и веб-процесс, и рабочий процесс на одном динамометрическом стенде, обновляя промежуточную среду функцией подсчета слов.

Тестовый Push

Начните с pushing кода в его текущем состоянии и посмотрите, что нужно исправить:

$ cd flask-by-example
$ git add -A
$ git commit -m "added angular and the backend worker process"
$ git push stage master
$ heroku open --app wordcount-stage

Обязательно замените wordcount-stage названием вашего приложения.

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

Что ж, если вы откроете вкладку «Сеть» в «Инструментах разработчика Chrome», вы увидите, что почтовый запрос к конечной точке /start вернул код состояния 500 (внутренняя ошибка сервера):

Flask app Heroku 500 error screenshot

Подумайте о том, как мы запускали это приложение локально: мы запускали рабочий процесс и сервер Redis вместе с сервером разработки Flask. То же самое должно произойти и на Heroku.

Redis

Начните с добавления Redis в промежуточное приложение:

$ heroku addons:create redistogo:nano --app wordcount-stage

Вы можете проверить, установлен ли REDISTOGO_URL как переменная среды, с помощью следующей команды:

$ heroku config --app wordcount-stage | grep REDISTOGO_URL

Нам нужно убедиться, что мы связываемся с Redis URI в нашем коде, который на самом деле уже настроен. Откройте worker.py и найдите этот код:

redis_url = os.getenv('REDISTOGO_URL', 'redis://localhost:6379')

Здесь мы сначала попытались использовать URI, связанный с переменной окружения REDISTOGO_URL. И если эта переменная не существует (например, в нашей локальной среде), мы использовали URI redis://localhost:6379. Отлично.

Обязательно ознакомьтесь с официальной документацией Heroku, чтобы узнать больше о работе с Redis.

После установки Redis нам просто нужно запустить рабочий процесс.

Worker

Heroku позволяет запускать один бесплатный дино. И вы должны запускать один процесс на каждом динамометрическом стенде. В нашем случае веб-процесс должен работать на одном динамометрическом стенде, а рабочий процесс – на другом. Однако, поскольку мы работаем над небольшим проектом, есть обходной путь, который мы можем использовать для запуска обоих процессов на одном динамометрическом стенде. Имейте в виду, что этот метод не рекомендуется для крупных проектов, поскольку процессы не будут правильно масштабироваться при увеличении трафика.

Сначала добавьте в корневой каталог сценарий bash с именем heroku.sh:

#!/bin/bash
gunicorn app:app --daemon
python worker.py

Затем обновите Procfile следующим образом:

web: sh heroku.sh

Теперь и web (демонизированный, в фоновом режиме), и worker (на переднем плане) процессы выполняются под веб-процессом в Procfile.

Обратите внимание, что на Heroku есть и другие способы бесплатно запустить веб и рабочий процесс. Мы рассмотрим альтернативный метод в следующей публикации (если будет интерес).

Давайте проверим это локально, прежде чем отправлять на промежуточный сервер. В новом окне терминала запустите сервер Redis – redis-server. Затем запустите heroku local:

$ heroku local
forego | starting web.1 on port 5000
web.1  | 18:17:00 RQ worker 'rq:worker:Michaels-MacBook-Air.9044' started, version 0.5.6
web.1  | 18:17:00

Перейдите по адресу http://localhost:5000/ и протестируйте приложение. Он должен работать.

Зафиксируйте свои изменения, а затем отправьте в Heroku. Проверьте это.

Вывод шестой части

Домашнее задание! Хотя у нас есть еще много дел, приложение действительно работает – так что давайте сделаем итерацию, чтобы мир увидел. Обновите производственную среду, используя тот же рабочий процесс.

Ссылки:

  1. Repo
  2. Sample Staging App
  3. Sample Production App

Часть 7. Обновление пользовательского интерфейса

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

Давайте посмотрим на текущий пользовательский интерфейс…

Текущий пользовательский интерфейс

Запустите Redis в окне терминала:

$ redis-server

Затем запустите своего рабочего в другом окне:

$ cd flask-by-example
$ python worker.py
17:11:39 RQ worker started, version 0.5.6
17:11:39
17:11:39 *** Listening on default...

Наконец, в третьем окне запустите приложение:

$ cd flask-by-example
$ python manage.py runserver

Протестируйте приложение, чтобы убедиться, что оно по-прежнему работает. Вы должны увидеть что-то вроде:

Current UI of the Flask Wordcount example app

Внесем некоторые изменения.

  1. Мы начнем с отключения кнопки отправки, чтобы пользователи не нажимали постоянно, пока они ждут, когда отправленный сайт будет подсчитан.
  2. Затем, пока приложение подсчитывает слова, мы добавим счетчик пульсации / загрузки дисплея, куда будет идти список подсчета слов, чтобы показать пользователю, что в серверной части происходит какая-то активность.
  3. Наконец, мы отобразим ошибку, если домен не может быть достигнут.

Смена кнопки

Измените кнопку в HTML на следующее:

{% raw %}
  <button type="submit" class="btn btn-primary"
  ng-disabled="loading">{{ submitButtonText }}</button>
{% endraw %}

Мы добавили директиву ng-disabled и прикрепили ее к загрузке. Это отключит кнопку, когда загрузка будет истинной. Затем мы добавили переменную submitButtonText для отображения пользователю. Таким образом, мы сможем изменить текст с «Отправить» на «Загрузка …», чтобы пользователь знал, что происходит.

Затем мы заключили кнопку в {% raw%} и {% endraw%}, чтобы Jinja знал, что это необработанный HTML. Если мы этого не сделаем, Flask попытается оценить {{submitButtonText}} как переменную Jinja, и Angular не сможет ее оценить.

Сопутствующий JavaScript довольно прост.

Вверху WordcountController в main.js добавьте следующий код:

$scope.loading = false;

Это устанавливает начальное значение загрузки равным false, чтобы кнопка не была отключена. Он также инициализировал текст кнопки как «Отправить».

Измените вызов POST на:

$http.post('/start', {'url': userInput}).
  success(function(results) {
    $log.log(results);
    getWordCount(results);
    $scope.wordcounts = null;
    $scope.loading = true;
    $scope.submitButtonText = 'Loading...';
  }).
  error(function(error) {
    $log.log(error);
  });

Мы добавили три строки, которые устанавливают…

  1. wordcounts равным нулю, чтобы старые значения были удалены.
  2. loading на true, чтобы кнопка загрузки была отключена с помощью директивы ng-disabled, которую мы добавили в HTML.
  3. submitButtonText в «Загрузка …», чтобы пользователь знал, почему кнопка отключена.

Затем обновите функцию опроса:

var poller = function() {
  // fire another request
  $http.get('/results/'+jobID).
    success(function(data, status, headers, config) {
      if(status === 202) {
        $log.log(data, status);
      } else if (status === 200){
        $log.log(data);
        $scope.loading = false;
        $scope.submitButtonText = "Submit";
        $scope.wordcounts = data;
        $timeout.cancel(timeout);
        return false;
      }
      // continue to call the poller() function every 2 seconds
      // until the timeout is cancelled
      timeout = $timeout(poller, 2000);
    });
};

Когда результат успешен, мы снова устанавливаем для загрузки значение false, чтобы кнопка снова была включена, и меняем текст кнопки обратно на «Отправить», чтобы пользователь знал, что он может отправить новый URL.

Проверьте это!

Добавление счетчика

Затем давайте добавим счетчик под секцией подсчета слов, чтобы пользователь знал, что происходит. Это достигается путем добавления анимированного gif под div результатов, как показано ниже:

<div class="col-sm-5 col-sm-offset-1">
  <h2>Frequencies</h2>
  <br>
  <div id="results">
    <table class="table table-striped">
      <thead>
        <tr>
          <th>Word</th>
          <th>Count</th>
        </tr>
      </thead>
      <tbody>
        {% raw %}
          <tr ng-repeat="(key, val) in wordcounts">
            <td>{{key}}</td>
            <td>{{val}}</td>            
          </tr>
        {% endraw %}
      </tbody>
    </table>
  </div>
  <img class="col-sm-3 col-sm-offset-4" src="{{ url_for('static',
  filename='spinner.gif') }}" ng-show="loading">
</div>

Обязательно возьмите spinner.gif из репозитория.

Вы можете видеть, что ng-show прикрепляется к загрузке точно так же, как и кнопка. Таким образом, когда для загрузки установлено значение true, отображается гифка счетчика. Если для загрузки установлено значение false – например, когда процесс подсчета слов завершается – счетчик исчезает.

Работа с ошибками

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

<div class="alert alert-danger" role="alert" ng-show='urlerror'>
  <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
  <span class="sr-only">Error:</span>
  <span>There was an error submitting your URL.<br>
  Please check to make sure it is valid before trying again.</span>
</div>

При этом используется класс предупреждений Bootstrap для отображения диалогового окна с предупреждением, если пользователь отправляет неверный URL. Мы использовали директиву Angular ng-show, чтобы отображать диалог только тогда, когда urlerror истинно.

Наконец, в WordcountController инициализируйте $ scope.urlerror значением false, чтобы предупреждение изначально не отображалось:

$scope.urlerror = false;

Отловите ошибки в функции опроса:

var poller = function() {
  // fire another request
  $http.get('/results/'+jobID).
    success(function(data, status, headers, config) {
      if(status === 202) {
        $log.log(data, status);
      } else if (status === 200){
        $log.log(data);
        $scope.loading = false;
        $scope.submitButtonText = "Submit";
        $scope.wordcounts = data;
        $timeout.cancel(timeout);
        return false;
      }
      // continue to call the poller() function every 2 seconds
      // until the timeout is cancelled
      timeout = $timeout(poller, 2000);
    }).
    error(function(error) {
      $log.log(error);
      $scope.loading = false;
      $scope.submitButtonText = "Submit";
      $scope.urlerror = true;
    });
};

Это зарегистрировало ошибку в консоли, изменило загрузку на false, снова установило текст кнопки отправки на «Отправить», чтобы пользователь мог попробовать отправить еще раз, и изменил urlerror на true, чтобы появилось предупреждение.

Наконец, в функции успеха для вызова POST для ‘/start’ установите для urlerror значение false:

$scope.urlerror = false;

Теперь диалоговое окно с предупреждением исчезнет, когда пользователь попытается отправить новый URL-адрес.

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

Вывод седьмой части

Что еще вы могли бы добавить или изменить, чтобы улучшить взаимодействие с пользователем? Внесите изменения самостоятельно или оставьте комментарии ниже. По завершении обязательно обновите как тестовую, так и производственную среду.

Пользовательская Angular директива с D3

Добро пожаловать. Настроив Angular вместе с индикатором загрузки и нашим переработанным контроллером Angular, давайте перейдем к последней части и создадим настраиваемую директиву Angular для отображения диаграммы распределения частот с помощью JavaScript и библиотеки D3.

Давайте посмотрим, что у нас сейчас есть …

Текущий пользовательский интерфейс

Запустите Redis в окне терминала:

$ redis-server

Затем запустите своего рабочего процесса в другом окне:

$ cd flask-by-example
$ python worker.py
17:11:39 RQ worker started, version 0.4.6
17:11:39
17:11:39 *** Listening on default...

Наконец, в третьем окне запустите приложение:

$ cd flask-by-example
$ python manage.py runserver

Вы должны увидеть, что ваш счетчик слов работает. Теперь мы можем добавить настраиваемую директиву Angular для отображения результатов на диаграмме D3.

Angular директива

Начните с добавления библиотеки D3 (v3) в файл index.html:

<!-- scripts -->
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="//code.jquery.com/jquery-2.2.1.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script src="{{ url_for('static', filename='main.js') }}"></script>

Теперь давайте создадим новую специальную директиву.

Angular директивы – это маркеры на элементе DOM, которые позволяют нам вставлять разделы HTML с определенными событиями и прикрепленными к ним атрибутами. Давайте создадим первую часть нашей Директивы, добавив следующий код чуть ниже контроллера в main.js:

.directive('wordCountChart', ['$parse', function ($parse) {
  return {
    restrict: 'E',
    replace: true,
    template: '<div id="chart"></div>',
    link: function (scope) {}
   };
}]);

restrict: «E» создает директиву, которая ограничена элементом HTML. replace: true просто заменяет директиву HTML на HTML в шаблоне. Функция ссылки дает нам доступ к переменным в области, определенной в контроллере.

Затем добавьте функцию отслеживания, чтобы «следить» за любыми изменениями переменных и реагировать соответствующим образом. Добавьте это в функцию ссылки следующим образом:

link: function (scope) {
  scope.$watch('wordcounts', function() {
    // add code here
  }, true);
}

Наконец, добавьте директиву чуть ниже закрывающего разделителя в <div class = “row”>:

<br>
<word-count-chart data="wordcounts"></word-count-chart>

Установив Директиву, давайте обратим наше внимание на библиотеку D3…

Гистограмма D3

D3 – это мощная библиотека, которая использует HTML, CSS и SVG для отображения данных в DOM и JavaScript, чтобы сделать их интерактивными. Мы будем использовать его для создания базовой гистограммы.

Шаг 1. Функциональная логика

Добавьте следующее к функции часов в директиве Angular:

scope.$watch('wordcounts', function() {
  var data = scope.wordcounts;
  for (var word in data) {
    d3.select('#chart')
      .append('div')
      .selectAll('div')
      .data(word[0])
      .enter()
      .append('div');
  }
}, true);

Теперь, когда изменяется scope.wordcounts, эта функция запускается, что обновляет DOM. Поскольку объект возвращается из запроса AJAX, мы перебираем его, чтобы добавить определенные данные в диаграмму. По сути, каждое слово добавляется к новому div через data join.

Попробуйте запустить код.

Что случается? Ничего не появляется, правда? После отправки нового сайта проверьте модель DOM в инструментах разработчика Chrome. Вы должны увидеть несколько вложенных div. Нам просто нужно добавить стили…

Шаг 2. Стилизация гистограммы

Начнем с простого CSS:

#chart {
  overflow-y: scroll;
}

#chart {
  background: #eee;
  padding: 3px;
}

#chart div {
  width: 0;
  transition: all 1s ease-out;
  -moz-transition: all 1s ease-out;
  -webkit-transition: all 1s ease-out;
}

#chart div {
  height: 30px;
  font: 15px;
  background-color: #006dcc;
  text-align: right;
  padding: 3px;
  color: white;
  box-shadow: 2px 2px 2px gray;
}

Обязательно включите это в верхней части HTML-страницы после таблицы стилей Bootstrap:

<link rel="stylesheet" type="text/css" href="../static/main.css">

Запустите приложение в нашем браузере. Что сейчас происходит?

Теперь при поиске веб-сайта вы должны увидеть серую область с тонкими синими полосами с левой стороны. Итак, вы можете видеть, что мы создаем полосу для каждого возвращаемого элемента данных – всего 10. Однако нам нужно изменить наш код D3, чтобы увеличить ширину каждой полосы, чтобы они были удобочитаемыми.

Шаг 3. Сделайте гистограмму более интерактивной

Мы можем связать это с нашим существующим кодом и использовать функцию стиля D3:

scope.$watch('wordcounts', function() {
  var data = scope.wordcounts;
  for (var word in data) {
    var key = data[word][0];
    var value = data[word][1];
    d3.select('#chart')
      .append('div')
      .selectAll('div')
      .data(word)
      .enter()
      .append('div')
      .style('width', function() {
        return (value * 3) + 'px';
      })
      .text(function(d){
        return key;
      });
  }
}, true);

Теперь мы динамически создаем ширину на основе числового значения того, как часто слово появляется на веб-странице:

.style('width', function() {
  return (value * 3) + 'px';
})
.text(function(d){
  return key;
});

Стиль вычисляется путем возврата значения, связанного с каждым словом, умножения этого числа на 3 и последующего преобразования его в пиксели. Мы также можем добавить текст к каждому элементу панели, вставив строковое значение слова вместе с тем, как часто оно появляется на странице.

Попробуйте это. Вы должны увидеть что-то вроде:

Chart: word count and frequencies

Однако кое-чего еще не хватает. Что происходит, когда вы ищете новый веб-сайт? Попытайся. Новая диаграмма добавляется под предыдущей. Нам нужно очистить наш div диаграммы перед созданием нового.

Шаг 4. Очистите для следующего поиска URL

Обновите функцию ссылки в Директиве:

link: function (scope) {
  scope.$watch('wordcounts', function() {
    d3.select('#chart').selectAll('*').remove();
    var data = scope.wordcounts;
    for (var word in data) {
      var key = data[word][0];
      var value = data[word][1];
      d3.select('#chart')
        .append('div')
        .selectAll('div')
        .data(word)
        .enter()
        .append('div')
        .style('width', function() {
          return (value * 3) + 'px';
        })
        .text(function(d){
          return key;
        });
    }
  }, true);
}

d3.select('#chart').selectAll('*').remove(); просто очищает диаграмму каждый раз, когда запускается функция $scope.watch. Теперь у нас есть таблица, которая очищается перед каждым новым использованием, и у нас есть полностью функциональное приложение для подсчета слов !!

Проверьте это!


Заключение руководства и дальнейшие шаги

Это оно. Отправьте свои изменения на промежуточный и рабочий серверы. Давайте рассмотрим, над чем мы работали:

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

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

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

У нас есть MVP, но многое еще предстоит сделать:

  1. Рефакторинг , рефакторинг, рефакторинг!
  2. Напишите тесты
  3. Обработка ошибок и исключений
  4. Абстрагирование состояния в приложении Angular до службы
  5. Работа над UI и UX

Ссылки:


Совершенствуй знания каждый день у нас в Телеграм-каналах

Вопросы, реклама — VK | Telegram