- Регистрация
- 23.09.18
- Сообщения
- 12.347
- Реакции
- 176
- Репутация
- 0

You must be registered for see links
|
You must be registered for see links
|
You must be registered for see links
Наверное, это продолжение статьи, в которой я парсил Хабр в базу данных. Теперь настало время её применить.
Маленькие правки, упрощающие жизнь
Структура базы данных

Ускоряем базу данных с помощью создания индекса.
CREATE INDEX article_indx ON articles(id);
CREATE INDEX comments_indx ON comments(article);
Время запроса до создания индекса — 1 с.
Время запроса первой строки — 5 мс.
Время запроса последней строки — 10 мс.
Парсим комментарии
В предыдущей статье мне посоветовали развить идею вставки в базу сразу, без промежуточного сохранения на диске (SSD сказал мне спасибо).
Однако из-за моей кривости рук мне не хватило терпения решить задачу без использования еще одного слоя абстракции(сколько же боли скрывается за этим предложением). Да здравствует sqlite3worker и его автор. Его библиотека позволяет вставлять значения в базу данных, не задумываясь о порядке, блокировки базы и других важных вещах.
Код комментариев
from sqlite3worker import Sqlite3Worker
from multiprocessing.dummy import Pool as ThreadPool
from datetime import datetime
import json
import requests
import logging
sql_worker = Sqlite3Worker("habr.db")
sql_worker.execute("CREATE TABLE IF NOT EXISTS comments(id INTEGER,"
"parent_id INTEGER,"
"article INTEGER,"
"level INTEGER,"
"timePublished TEXT,"
"score INTEGER,"
"message TEXT,"
"children TEXT,"
"author TEXT)")
def worker(i):
url = "
You must be registered for see links
{}/comments/?fl=ru%2Cen&hl=ru".format(i)try:
r = requests.get(url)
if r.status_code == 503:
logging.critical("503 Error")
raise SystemExit
except:
with open("req_errors.txt", "a") as file:
logging.critical("requests error")
file.write(str(i))
return 2
try: data = json.loads(r.text)
except: logging.warning("[{}] Json loads failed".format(i))
if data['success']:
comments = data['data']['comments']
for comment in comments:
current = comments[comment]
id = current['id']
parent_id = current['parentId']
article = i
level = current['level']
time_published = current['timePublished']
score = current['score']
message = current['message']
children = [children for children in current['children']]
author = current['author']
try: data = (id,
parent_id,
article,
level,
time_published,
score,
message,
str(children),
str(author['login']))
except:
data = (None, None, None, None, None, None, None, None, None)
sql_worker.execute("INSERT INTO comments VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", data)
logging.info("Comments on article {} were parsed".format(i))
min = 490000
max = 495000
pool = ThreadPool(3)
start_time = datetime.now()
results = pool.map(worker, range(min, max))
pool.close()
pool.join()
sql_worker.close()
print(datetime.now() - start_time)
Как грамотно хранить комментарии в базе до меня уже рассказал
You must be registered for see links
в своей
You must be registered for see links
.За меня все уже сделали авторы Хабра и мне оставалось лишь сохранить поля в базе.
Веб-приложение
Самая, как мне кажется, интересная часть статьи заключается именно в создании веб-приложения.
Для его создания выбрал Flask, просто потому, что раньше имел с ним дело и он достаточно прост для быстрого старта.
Создаем приложение
Просто создаем обычное приложение на Flask, определяем переменные.
DEBUG — пригодится нам во время разработки — расширенная информация об ошибках и динамическая перезагрузка скрипта при изменениях в нём. Т.е. нам не нужно в ручную перезапускать скрипт каждый раз, когда мы внесли изменения в код.
from flask import Flask, g, render_template, request, redirect, url_for
import sqlite3
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'supersecretkey'
DATABASE = "./habr.db"
Функция-помощник
Возвращает нам объект базы данных.
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
return db
Главная страница
Создадим главную страницу нашего приложения. Обработчик, возвращаем шаблон главной страницы. Позже мы его изменим, сделаем чуточку полезнее.
@app.route("/")
def index():
return render_template("index.html")
Страница поста
Здесь мы создаем обработчик страницы /post/id, где делаем запрос к базе и передаем данные в шаблон.
@app.route('/post//')
def show_post(post_id):
cur = get_db().cursor()
res = cur.execute("SELECT * FROM articles WHERE id = :id", {"id": post_id} )
article = res.fetchone()
return render_template("post.html", post=article)
Шаблоны
/post/id page
Теперь поговорим о шаблонах. Во Flask для рендера html-страниц используются шаблоны.
Для их использования необходимо создать папку templates и поместить в неё html-файлы, которые будут обрабатываться шаблонизатором Jinja.
Например, для того, чтобы напечатать в тег title название нашей статьи, мы должны поместить наш массив с данными в две фигурные скобки.
Для лучшего понимания, эту конструкцию можно представить как {{ var }} = print(var) в Python.
{% if post[9] == 1 %}
Tutorial
{% endif %}
{{ post[6] }} |
{{ post[8] }} |
{{ post[7] }} раз
{{ post[4]|safe }}
Теги: {{ post[10] }}
Комментарии
Теперь о комментариях. В базе они хранятся в отдельной таблице comments.
Давайте извлечем еще одну таблицу, добавив к исходному скрипту
@app.route('/post//')
def show_post(post_id):
cur = get_db().cursor()
...
res = cur.execute("SELECT * FROM comments WHERE article = :id", {"id": post_id} )
comments = res.fetchall()
return render_template("post.html", post=article, comments=comments)
Далее выведем их в нашем шаблоне:
Комментарии
{% for comment in comments %}
#{{comment[0]}}
{{ comment[8] }}
{{comment[4]}} | {{ comment[5] }}
{% if comment[1] != 0 %}
You must be registered for see links
{{comment[6]|safe}}
{% else %}
{{comment[6]|safe}}
{% endif %}
{% endfor %}
Для условных выражений Jinja использует конструкцию {% %}
Index page
К созданию главной страницы никаких особых требованией нет — она просто должна выглядеть прилично.
Для того, чтобы работал переход к статьям, добавим стандартную форму с текстовым полем и кнопкой.
Скрипт будет на этой же странице принимать POST-запрос и перенаправлять нас к статье:
main.py
@app.route('/', methods=['POST'])
def index_post():
text = request.form['url']
id = ''.join(x for x in text if x.isdigit())
if id != '':
return redirect(url_for('show_post', post_id=id))
else: return "Некорректный ввод"
index.html
...
Перейти
...
Так же давайте добавим отображение количества статей в базе:
@app.route("/")
def index():
cur = get_db().cursor()
res = cur.execute("SELECT min(id), max(id) FROM articles")
counter = res.fetchone()
return render_template("index.html", counter = counter)
...
...
Промежуток доступных статей = {{ counter[0] }}..{{ counter [1] }}
...
...
Теперь как это выглядит:

Прочие жизненно-необходимые костыли
Итак, наше веб-приложение практически готово. Однако есть шероховатости. Например, дата поста отображается в формате ISO 8601, что немного не удобно читать.
Давайте исправим это: будем форматировать дату и передавать её отдельной переменной в шаблон.
import dateutil.parser
from datetime import datetime
...
@app.route('/post//')
def show_post(post_id):
...
date = dateutil.parser.parse(article[1])
date = datetime.strftime(date, "%d.%m.%Y %H:%M")
...
return render_template("post.html", post=article, date=date, comments=comments)
Хабр использует lazy load для изображений, соответственно, добавим поддержку и в наше приложение:
// Загрузка изображений
(async () => {
for (let node of document.getElementsByTagName('img')) {
await new Promise(res => {
if(node.dataset.src !== undefined){
node.src = node.dataset.src;
node.onload = () => res();
}
})
}
})();
КДПВ, в отличии от остальных изображений, загружается напрямую, поэтому есть проверка на data-src тег
В некоторых постах есть спойлеры, давайте же "оживим" их:
$(document).on('click', '.spoiler_title', function (e) {
e.preventDefault();
$(this).toggleClass('active');
$(this).parent().find('.spoiler_text').first().slideToggle(300);
});
Ну и в завершение. Мне всегда нравилась темная тема, благо её можно легко добавить скриптомYou must be registered for see links
Разворачиваем на боевом сервере
Для того, чтобы веб-приложением можно было комфортно пользоваться отовсюду, его нужно где-нибудь разместить(Ваш кэп).
Локально
В самом простом(но не безопасном!) случае достаточно воспользоваться встроенным во flask сервером с отключенным дебаггером.
export FLASK_APP=main.py
export FLASK_ENV=production
flask run
Но, хоть я и не нашел почему, нас везде уверяют, что использовать встроенный сервер для полноценного деплоя небезопасно,
поэтому будем использовать сторонний веб-сервер uWSGI, можно даже вместе с nginx.
Всему миру
Как всё грамотно настраивать — уже хорошо объяснили в
You must be registered for see links
к uWSGI, поэтому повторять не вижу смысла, просто дам листинг команд, которые использую я.pip install uwsgi
uwsgi --socket 0.0.0.0:3031 --protocol http --wsgi-file main.py --callable app
Не уверен, что это безопасно — вероятнее всего, нужно прятать за nginx, но я в его настройке профан
Конец
Спасибо за прочтение, надеюсь, вам было интересно. Буду рад, если кто-то присоединится к проекту и сделает его лучше.
Посмотреть, как это выглядит, можно
You must be registered for see links
(база в несколько тысяч статей, сервер в России).Ну и код как всегда на
You must be registered for see links
Так же из-за моего слабого VPS и возможного хабраэффекта сайт может быть недоступен. Будет просто замечательно, если кто-нибудь поднимет аналогичный сервер.