Представление полноценных HTML-страниц на сервере

На предыдущем занятии мы рассмотрели общий принцип добавления и считывания публикаций из БД. И прописали меню непосредственно в шаблоне base.html. Я верну его считывание из списка:

{% for p in menu %}
<li><a href="{{p.url}}">{{p.title}}</a></li>
{% endfor %}

И здесь у нас возникала ошибка: при отображении страницы:

http://127.0.0.1:5000/post/1

ссылка «Добавить статью» становилась следующей:

http://127.0.0.1:5000/post/add_post

Я здесь вначале сделаю небольшую работу над ошибками и поправлю этот баг при работе с БД. Исправляется все очень просто: в таблице в поле url вместо «add_post» нужно прописать «/add_post». После сохранения изменений и обновления страницы сайта, все стало работать как и должно быть, ссылка стала:

http://127.0.0.1:5000/ add_post

А теперь о главном. Все наши прежние статьи были очень простыми: заголовок и обычный текст. Реальные HTML-страницы содержат гораздо более сложную информацию: различные теги, ссылки на изображения и прочее. Как их хранить на сервере, добавлять на сайт и отдавать пользователю по определенному запросу? Вот об этом и пойдет речь на этом занятии.

В качестве примера я подготовил страницу

framework-flask-intro.htm

которая содержит множество различных изображений, расположенных в отдельном каталоге framework-flask-intro.files. И первый вопрос: где на сервере разместить эти изображения? Мы их расположим в каталоге static и в нем создадим специальный подкаталог images_html для хранения изображений наших HTML-страниц. И в этот подкаталог скопируем каталог framework-flask-intro.files с нашими изображениями. Почему мы их разместили именно здесь? Об этом чуть позже, а пока давайте реализуем функционал для добавления самого HTML-документа в БД.

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

домен/post/framework-flask-intro

Как видите, это более понятный для человека адрес. Так вот, чтобы наше приложение «знало» что подставлять после /post/, дополнительно в таблицу posts добавим поле url и структура таблицы будет следующая:

CREATE TABLE IF NOT EXISTS posts (
id integer PRIMARY KEY AUTOINCREMENT,
title text NOT NULL,
text text NOT NULL,
url text NOT NULL,
time integer NOT NULL
);

Обновим нашу БД, чтобы таблицы posts приняла такой вид.

Добавление статьи в БД

Далее, в форме добавления статьи (в add_post.html) пропишем еще одно поле ввода для url:

<form action="{{url_for('addPost')}}" method="post" class="form-contact">
<p><label>Название статьи: </label> <input type="text" name="name" value="" requied />
<p><label>URL статьи: </label> <input type="text" name="url" value="" requied />
<p><label>Текст статьи:</label>
<p><textarea name="post" rows=7 cols=40></textarea>
<p><input type="submit" value="Добавить" />
</form>

А в функции-представления addPost в метод dbase.addPost передадим этот третий параметр с url:

res = dbase.addPost(request.form['name'], request.form['post'], request.form['url'])

Соответственно, сам метод в классе FDataBase поправим, следующим образом:

    def addPost(self, title, text, url):
        try: 
            self.__cur.execute("SELECT COUNT() as `count` FROM posts WHERE url LIKE ?", (url,))
            res = self.__cur.fetchone()
            if res['count'] > 0:
                print("Статья с таким url уже существует")
                return False
 
            tm = math.floor(time.time())
            self.__cur.execute("INSERT INTO posts VALUES(NULL, ?, ?, ?, ?)", (title, text, url, tm))
            self.__db.commit()
        except sqlite3.Error as e:
            print("Ошибка добавления статьи в БД "+str(e))
            return False
 
        return True

Смотрите, мы здесь вначале проверяем: существует ли такой url в таблице posts, и если существует, то статья не добавляется, т.к. все статьи должны иметь уникальный url. После этой проверки происходит добавление записи.

Все статья добавляется. Давайте, перейдем на страницу добавления и пропишем там заголовок, url и скопируем HTML-текст в тело статьи. После нажатия на кнопку добавить, первая статья будет добавлена в БД.

Отображение списка статей

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

self.__cur.execute(f"SELECT id, title, text, url FROM posts ORDER BY time DESC")

А в шаблоне index.html, укажем его:

<p class="title"><a href="{{ url_for('showPost', alias=p.url)}}">{{p.title}}</a></p>

Дополнительно, чтобы не отображать в анонсе теги HTML-документа, пропишем фильтр striptags:

<p class="annonce">{{ p.text[:50] | striptags }}</p>

Все, теперь при обновлении главной страницы, увидим нашу добавленную статью с анонсом.

Отображение статьи

Осталось отобразить статью. Для начала перепишем обработчик showPost, следующим образом:

@app.route("/post/<alias>")
def showPost(alias):
    db = get_db()
    dbase = FDataBase(db)
    title, post = dbase.getPost(alias)
    if not title:
        abort(404)
 
    return render_template('post.html', menu=dbase.getMenu(), title=title, post=post)

Мы теперь после post/ указываем переменную alias, которая может принимать строковые значения. Затем, в метод getPost передаем alias из URL и по нему уже отбираем статью:

    def getPost(self, alias):
        try:
            self.__cur.execute("SELECT title, text FROM posts WHERE url LIKE ? LIMIT 1", (alias,))
            res = self.__cur.fetchone()
            if res:
                return res
        except sqlite3.Error as e:
            print("Ошибка получения статьи из БД "+str(e))
 
        return (False, False)

Если сейчас попробовать отобразить страницу, то получим также и отображение тегов, т.к. по умолчанию шаблон преобразовывает наши данные и теги заменяются специальными символами. Чтобы этого не происходило и теги страницы передавались браузеру в чистом виде, необходимо в шаблоне post.html указать фильтр safe:

{{ post | safe}}

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

Прописываем пути к изображениям

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

 res = self.__cur.fetchone()
 if res: 
     base = url_for('static', filename='images_html')
     text = re.sub(r"(?P<tag><img\s+[^>]*src=)(?P<quote>[\"'])(?P<url>.+?)(?P=quote)>",
        "\\g<tag>"+base+"/\\g<url>>",
        res['text'])
 
     return (res['title'], text)

Мы здесь с помощью регулярного выражения в тексте выделяем все URL тегов img и корректируем их с поправкой на папку static, полученной с помощью функции

base = url_for('static', filename='images_html')

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

import re
from flask import url_for

Все, теперь, при запуске мы увидим изображения в нашем HTML-документе.

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

Модификация HTML-страницы перед ее добавлением в БД

У приведенной реализации есть один серьезный недостаток: выполнять регулярное выражение для каждой отдаваемой по запросу страницы – это ресурсоемкая операция, которая негативно скажется на нагрузке сервера. Лучше это преобразование выполнять при добавлении поста в БД. Сделаем это. В методе addPost пропишем следующие строчки:

    def addPost(self, title, text, url):
        try:
            self.__cur.execute("SELECT COUNT() as `count` FROM posts WHERE url LIKE ?", (url,))
            res = self.__cur.fetchone()
            if res['count'] > 0:
                print("Статья с таким url уже существует")
                return False
 
            base = url_for('static', filename='images_html')
            text = re.sub(r"(?P<tag><img\s+[^>]*src=)(?P<quote>[\"'])(?P<url>.+?)(?P=quote)>",
                          "\\g<tag>"+base+"/\\g<url>>",
                          text)
 
            tm = math.floor(time.time())
            self.__cur.execute("INSERT INTO posts VALUES(NULL, ?, ?, ?, ?)", (title, text, url, tm))
            self.__db.commit()
        except sqlite3.Error as e:
            print("Ошибка добавления статьи в БД "+str(e))
            return False
 
        return True

Смотрите, мы здесь перед добавлением текста в БД, выполняем регулярное выражение и корректируем URL-адреса для изображений. Теперь, наш документ полностью готов для отправки пользователю по запросу. Соответственно, в методе getPost регулярное выражением можно просто убрать.

Давайте для примера добавим еще одну статью и посмотрим как это будет работать.

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

Видео по теме