Простой веб-сервер с использованием Python и Flask
Существует множество способов поднять свой собственный веб-сервер, который будет обрабатывать HTTP запросы пользователей и возвращать им в браузеры результат.
Поскольку мы используем Python в качестве основного языка, библиотеку, упрощающую нам создание веб-сервера, выберем тоже из мира Python.
Flask — это инструмент для веб-сайтов на языке Python. Представляет из себя микрофреймворк со встроенным веб-сервером. Договоримся, что вы используете Linux в качестве операционной системы, либо знаете как выполнить аналоги команд в Windows.
Установка необходимых библиотек
В предыдущей статье вы уже установили и настроили python, pip и virtualenv. Осталось загрузить сам Flask:
pip install flask
Если вы желаете работать с виртуальными окружениями, перейдите в директорию с ним и выполните команду:
source venv/bin/activate
Чтобы проверить все ли правильно установилось, вы можете создать файл server.py со следующим содержимым:
from flask import Flask app = Flask(__name__) @app.route(«/») def hello(): return «Hello World!» if __name__ == «__main__»: app.run()
Выполнить его можно командой:
python server.py
По умолчанию, Flask работает на порту 5000. Перейдите по адресу http://localhost:5000 в браузере. Если вы все сделали правильно, на экране отобразится надпись «Hello World!».
Flask позволяет делать много замечательных вещей, например, обрабатывать GET и POST параметры. Более подробно можно ознакомиться со всеми функциями в официальной документации:
Модифицируем скрипт таким образом, чтобы он принимал имя пользователя и выводил на экран приветствие:
from flask import Flask app = Flask(__name__) @app.route(«/<username>», methods=[‘GET’]) def index(username): return «Hello, %s!» % username if __name__ == «__main__»: app.run(host=’0.0.0.0′, port=4567)
Теперь скрипт будет работать на 4567 порту, а также принимать от пользователя имя в адресной строке. Перейдите в браузере по ссылке: http://localhost:4567/yourname Вы увидите ответ: «Hello, yourname». Это значит, что сервер успешно работает и возвращает ожидаемую строку.
Настраиваем прокси
Чтобы ваш сайт был доступен другим людям, нужно иметь внешний IP адрес. Если вы знаете, что это такое или у вас есть VPS, вы можете настроить все самостоятельно. Если же вы слышите эти слова первый раз — воспользуйтесь более простым, хотя и не очень универсальным методом, который хорошо описан тут. Суть данного метода заключается в использовании прокси сервера.
В качестве прокси, будем использовать бесплатную программу ngrok. Ее главная задача — держать постоянное соединение и доставлять вам всю полученную от любого человека информацию. Запустите ее командой, передав в качестве параметра любой свободный порт:
./ngrok http 4567
В ответ вы получите несколько строчек информации, среди которой будет нечто подобное:
Forwarding http://7e9ea9dc.ngrok.io -> 127.0.0.1:4567
Адрес http://7e8ea9dc.ngrok.io можете смело пересылать своим друзьям, пройдя по нему, они попадут на ваш сайт.
If you like this article, share a link with your friends
Read more
We talk about interesting technologies and share our experience of using them.
Быстрый старт¶
Рвётесь в бой? Эта страница даёт хорошее введение в Flask. Предполагается, что вы уже имеете установленный Flask. Если это не так, обратитесь к секции Инсталляция .
Минимальное приложение¶
Минимальное приложение Flask выглядит примерно так:
from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!' if __name__ == '__main__': app.run()
Просто сохраните его под именем наподобие hello.py и запустите с помощью вашего интерпретатора Python. Только, пожалуйста, не давайте приложению имя flask.py , так как это вызовет конфликт с самим Flask.
$ python hello.py * Running on http://127.0.0.1:5000/
Проследовав по ссылке http://127.0.0.1:5000/ вы увидите ваше приветствие миру.
Итак, что же делает этот код?
- Сначала мы импортировали класс Flask . Экземпляр этого класса и будет вашим WSGI-приложением.
- Далее мы создаём экземпляр этого класса. Первый аргумент — это имя модуля или пакета приложения. Если вы используете единственный модуль (как в этом примере), вам следует использовать __name__ , потому что в зависимости от того, запущен ли код как приложение, или был импортирован как модуль, это имя будет разным ( ‘__main__’ или актуальное имя импортированного модуля соответственно). Это нужно, чтобы Flask знал, где искать шаблоны, статические файлы и прочее. Для дополнительной информации, смотрите документацию Flask .
- Далее, мы используем декоратор route() , чтобы сказать Flask, какой из URL должен запускать нашу функцию.
- Функция, которой дано имя, используемое также для генерации URL-адресов для этой конкретной функции, возвращает сообщение, которое мы хотим отобразить в браузере пользователя.
- Наконец, для запуска локального сервера с нашим приложением, мы используем функцию run() . Благодаря конструкции if __name__ == ‘__main__’ можно быть уверенным, что сервер запустится только при непосредственном вызове скрипта из интерпретатора Python, а не при его импортировании в качестве модуля.
Для остановки сервера, нажмите Ctrl+C.
Публично доступный сервер
Если вы запустите сервер, вы заметите, что он доступен только с вашего собственного компьютера, а не с какого-либо другого в сети. Так сделано по умолчанию, потому что в режиме отладки пользователь приложения может выполнить код на Python на вашем компьютере.
Если у вас отключена опция debug или вы доверяете пользователям в сети, вы можете сделать сервер публично доступным, просто изменив вызов метода run() таким вот образом:
app.run(host='0.0.0.0')
Это укажет вашей операционной системе, чтобы она слушала сеть со всех публичных IP-адресов.
Режим отладки¶
Метод run() чудесно подходит для запуска локального сервера для разработки, но вы будете должны перезапускать его всякий раз при изменении вашего кода. Это не очень здорово, и Flask здесь может облегчить жизнь. Если вы включаете поддержку отладки, сервер перезагрузит сам себя при изменении кода, кроме того, если что-то пойдёт не так, это обеспечит вас полезным отладчиком.
Существует два способа включить отладку. Или установите флаг в объекте приложения:
app.debug = True app.run()
Или передайте его как параметр при запуске:
app.run(debug=True)
Оба метода вызовут одинаковый эффект.
Несмотря на то, что интерактивный отладчик не работает в многопоточных окружениях (что делает его практически неспособным к использованию на реальных рабочих серверах), тем не менее, он позволяет выполнение произвольного кода. Это делает его главной угрозой безопасности, и поэтому он никогда не должен использоваться на реальных «боевых» серверах.
Снимок экрана с отладчиком в действии:
Предполагаете использовать другой отладчик? Тогда смотрите Работа с отладчиками .
Маршрутизация¶
Современные веб-приложения используют «красивые» URL. Это помогает людям запомнить эти URL, это особенно удобно для приложений, используемых с мобильных устройств с более медленным сетевым соединением. Если пользователь может перейти сразу на желаемую страницу, без предварительного посещения начальной страницы, он с большей вероятностью вернётся на эту страницу и в следующий раз.
Как вы увидели ранее, декоратор route() используется для привязки функции к URL. Вот простейшие примеры:
@app.route('/') def index(): return 'Index Page' @app.route('/hello') def hello(): return 'Hello World'
Но это еще не все! Вы можете сделать определенные части URL динамически меняющимися и задействовать в функции несколько правил.
Правила для переменной части¶
Чтобы добавлять к адресу URL переменные части, можно эти особые части выделить как . Затем подобные части передаются в вашу функцию в качестве аргумента — в виде ключевого слова. Также может быть использован конвертер — с помощью задания правила следующего вида . Вот несколько интересных примеров
@app.route('/user/') def show_user_profile(username): # показать профиль данного пользователя return 'User %s' % username @app.route('/post/') def show_post(post_id): # вывести сообщение с данным id, id - целое число return 'Post %d' % post_id
Существуют следующие конвертеры:
int | принимаются целочисленные значения |
float | как и int , только значения с плавающей точкой |
path | подобно поведению по умолчанию, но допускаются слэши |
Уникальные URL / Перенаправления
Правила для URL, работающие в Flask, основаны на модуле маршрутизации Werkzeug. Этот модуль реализован в соответствие с идеей обеспечения красивых и уникальных URL-адресов на основе исторически попавшего в обиход — из поведения Apache и более ранних HTTP серверов.
Возьмём два правила:
@app.route('/projects/') def projects(): return 'The project page' @app.route('/about') def about(): return 'The about page'
Хоть они и выглядят довольно похожими, есть разница в использовании слэша в определении URL. В первом случае, канонический URL имеет завершающую часть projects со слэшем в конце. В этом смысле он похож на папку в файловой системе. В данном случае, при доступе к URL без слэша, Flask перенаправит к каноническому URL с завершающим слэшем.
Однако, во втором случае, URL определен без косой черты — как путь к файлу на UNIX-подобных системах. Доступ к URL с завершающей косой чертой будет приводить к появлению ошибки 404 «Not Found».
Такое поведение позволяет продолжить работать с относительными URL, даже если в конце строки URL пропущен слэш — в соответствии с тем, как работают Apache и другие сервера. Кроме того, URL-адреса останутся уникальными, что поможет поисковым системам избежать повторной переиндексации страницы.
Построение (генерация) URL¶
Раз Flask может искать соответствия в URL, может ли он их генерировать? Конечно, да. Для построения URL для специфической функции, вы можете использовать функцию url_for() . В качестве первого аргумента она принимает имя функции, кроме того она принимает ряд именованных аргументов, каждый из которых соответствует переменной части правила для URL. Неизвестные переменные части добавляются к URL в качестве параметров запроса. Вот некоторые примеры:
>>> from flask import Flask, url_for >>> app = Flask(__name__) >>> @app.route('/') . def index(): pass . >>> @app.route('/login') . def login(): pass . >>> @app.route('/user/') . def profile(username): pass . >>> with app.test_request_context(): . print url_for('index') . print url_for('login') . print url_for('login', next='/') . print url_for('profile', username='John Doe') . / /login /login?next=/ /user/John%20Doe
(Здесь также использован метод test_request_context() , который будет объяснён ниже. Он просит Flask вести себя так, как будто он обрабатывает запрос, даже если мы взаимодействуем с ним через оболочку Python. Взгляните на нижеследующее объяснение. Локальные объекты контекста (context locals) .
Зачем Вам может потребоваться формировать URL-ы с помощью функции их обращения url_for() вместо того, чтобы жёстко задать их в ваших шаблонах? Для этого есть три веские причины:
1. По сравнению с жёстким заданием URL внутри кода обратный порядок часто является более наглядным. Более того, он позволяет менять URL за один шаг, и забыть про необходимость изменять URL повсюду. 2. Построение URL будет прозрачно для вас осуществлять экранирование специальных символов и данных Unicode, так что вам не придётся отдельно иметь с ними дела. 3. Если ваше приложение размещено не в корневой папке URL root (а, скажем, в /myapplication вместо / ), данную ситуацию нужным для вас образом обработает функция url_for() .
Методы HTTP¶
HTTP (протокол, на котором общаются веб-приложения) может использовать различные методы для доступа к URL-адресам. По умолчанию, route отвечает лишь на запросы типа GET , но это можно изменить, снабдив декоратор route() аргументом methods . Вот некоторые примеры:
from flask import request @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': do_the_login() else: show_the_login_form()
Если присутствует метод GET , то автоматически будет добавлен и HEAD . Вам не придётся иметь с ним дело. Также, при этом можно быть уверенным, что запросы HEAD будут обработаны в соответствии с требованиями HTTP RFC (документ с описанием протокола HTTP), так что вам не требуется ничего знать об этой части спецификации HTTP. Кроме того, начиная с Flask версии 0.6, для вас будет автоматически реализован метод OPTIONS автоматически.
Не имеете понятия, что такое метод HTTP? Не беспокойтесь, здесь приводится быстрое введение в методы HTTP, и почему они важны:
HTTP-метод (также часто называемый командой) сообщает серверу, что хочет сделать клиент с запрашиваемой страницей. Очень распространены Следующие методы:
GET Браузер говорит серверу, чтобы он просто получил информацию, хранимую на этой странице, и отослал её. Возможно, это самый распространённый метод. HEAD Браузер просит сервер получить информацию, но его интересует только заголовки, а не содержимое страницы. Приложение предполагает обработать их так же, как если бы был получен запрос GET , но без доставки фактического содержимого. В Flask, вам вовсе не требуется иметь дело с этим методом, так как нижележащая библиотека Werkzeug сделает всё за вас. POST Браузер говорит серверу, что он хочет сообщить этому URL некоторую новую информацию, и что сервер должен убедиться, что данные сохранены и сохранены в единожды. Обычно, аналогичным образом происходит передача из HTML форм на сервер данных. PUT Похоже на POST , только сервер может вызвать процедуру сохранения несколько раз, перезаписывая старые значения более одного раза. Здесь вы можете спросить, зачем это нужно, и есть несколько веских причин, чтобы делать это подобным образом. Предположим, во время передачи произошла потеря соединения: в этой ситуации система между браузером и сервером, ничего не нарушая, может совершенно спокойно получить запрос во второй раз. С POST такое было бы невозможно, потому что он может быть вызван только один раз. DELETE Удалить информацию, расположенную в указанном месте. OPTIONS Обеспечивает быстрый способ выяснения клиентом поддерживаемых для данного URL методов. Начиная с Flask 0.6, это работает для вас автоматически.
Теперь самое интересное: в HTML 4 и XHTML1, единственными методами, которыми форма может отправить серверу данные, являются GET и POST . Но для JavaScript и будущих стандартов HTML вы также можете использовать и другие методы. Кроме того, в последнее время HTTP стал довольно популярным, и теперь браузеры уже не единственные клиенты, использующие HTTP. Например, его используют многие системы контроля версий.
Статические файлы¶
Динамические веб-приложения также нуждаются и в статических файлах. Обычно, именно из них берутся файлы CSS и JavaScript. В идеале ваш веб-сервер уже сконфигурирован так, чтобы обслуживать их для вас, однако в ходе разработке это также может делать и сам Flask. Просто создайте внутри вашего пакета или модуля папку с названием static , и она будет доступна из приложения как /static .
Чтобы сформировать для статических файлов URL, используйте специальное окончание ‘static’ :
url_for('static', filename='style.css')
Этот файл должен храниться в файловой системе как static/style.css .
Визуализация шаблонов¶
Генерация HTML из Python — невесёлое и на самом деле довольно сложное занятие, так как вам необходимо самостоятельно заботиться о безопасности приложения, производя для HTML обработку специальных последовательностей (escaping). Поэтому внутри Flask уже автоматически преднастроен шаблонизатор Jinja2.
Для визуализации шаблона вы можете использовать метод render_template() . Всё, что вам необходимо — это указать имя шаблона, а также переменные в виде именованных аргументов, которые вы хотите передать движку обработки шаблонов:
from flask import render_template @app.route('/hello/') @app.route('/hello/') def hello(name=None): return render_template('hello.html', name=name)
Flask будет искать шаблоны в папке templates . Поэтому, если ваше приложение выполнено в виде модуля, эта папка будет рядом с модулем, а если в виде пакета, она будет внутри вашего пакета:
Первый случай — модуль:
/application.py /templates /hello.html
Второй случай — пакет:
/application /__init__.py /templates /hello.html
При работе с шаблонами вы можете использовать всю мощь Jinja2. За дополнительной информацией обратитесь к официальной Документации по шаблонам Jinja2
Вот пример шаблона:
title>Hello from Flasktitle> if name %> h1>Hello <name >>!h1> else %> h1>Hello World!h1> endif %>
Также, внутри шаблонов вы имеете доступ к объектам request , session и g [1], а также к функции get_flashed_messages() .
Шаблоны особенно полезны при использовании наследования. Если вам интересно, как это работает, обратитесь к документации по заготовкам Template Inheritance . Проще говоря, наследование шаблонов позволяет разместить определённые элементы (такие, как заголовки, элементы навигации и «подвал» страницы) на каждой странице.
Автоматическая обработка специальных (escape-) последовательностей (escaping) включена по умолчанию, поэтому если name содержит HTML, он будет экранирован автоматически. Если вы можете доверять переменной и знаете, что в ней будет безопасный HTML (например, потому что он пришёл из модуля конвертирования разметки wiki в HTML), вы можете пометить её в шаблоне, как безопасную — с использованием класса Markup или фильтра |safe . За дополнительными примерами обратитесь к документации по Jinja2.
Вот основные возможности по работе с классом Markup :
>>> from flask import Markup >>> Markup('Hello %s!') % '' Markup(u'Hello <blink>hacker</blink>!') >>> Markup.escape('') Markup(u'<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() u'Marked up \xbb HTML'
Изменено в версии 0.5: Автоматическая обработка escape-последовательностей больше не активирована для всех шаблонов. Вот расширения шаблонов, которые активизируют автообработку: .html , .htm , .xml , .xhtml . Шаблоны, загруженные из строк, не будут обрабатывать специальные последовательности.
[1] | Затрудняетесь понять, что это за объект — g ? Это то, в чём вы можете хранить информацию для ваших собственных нужд, для дополнительной информации смотрите документацию на этот объект ( g ) и sqlite3 . |
Доступ к данным запроса¶
Для веб-приложений важно, чтобы они реагировали на данные, которые клиент отправляет серверу. В Flask эта информация предоставляется глобальным объектом request . Если у вас есть некоторый опыт по работе с Python, вас может удивить, как этот объект может быть глобальным, и как Flask при этом умудрился остаться ориентированным на многопоточное выполнение.
Локальные объекты контекста (context locals)¶
Информация от инсайдера
Прочтите этот раздел, если вы хотите понять, как это работает, и как вы можете реализовать тесты с локальными переменными контекста. Если вам это неважно, просто пропустите его.
Некоторые объекты в Flask являются глобальными, но необычного типа. Эти объекты фактически являются прокси (посредниками) к объектам, локальным для конкретного контекста. Труднопроизносимо. Но на самом деле довольно легко понять.
Представьте себе контекст, обрабатывающий поток. Приходит запрос, и веб-сервер решает породить новый поток (или нечто иное — базовый объект может иметь дело с системой параллельного выполнения не на базе потоков). Когда Flask начинает осуществлять свою внутреннюю обработку запроса, он выясняет, что текущий поток является активным контекстом и связывает текущее приложение и окружение WSGI с этим контекстом (потоком). Он делает это с умом — так, что одно приложение может, не ломаясь, вызывать другое приложение.
Итак, что это означает для вас? В принципе, вы можете полностью игнорировать, что это так, если вы не делаете чего-либо вроде тестирования модулей. Вы заметите, что код, зависящий от объекта запроса, неожиданно будет работать неправильно, так как отсутствует объект запроса. Решением является самостоятельное создание объекта запроса и его привязка к контексту. Простейшим решением для тестирования модулей является использование менеджера контекстов test_request_context() . В сочетании с оператором with этот менеджер свяжет тестовый запрос так, что вы сможете с ним взаимодействовать. Вот пример:
from flask import request with app.test_request_context('/hello', method='POST'): # теперь, и до конца блока with, вы можете что-либо делать # с контекстом, например, вызывать простые assert-ы: assert request.path == '/hello' assert request.method == 'POST'
Другая возможность — это передача целого окружения WSGI методу request_context() method:
from flask import request with app.request_context(environ): assert request.method == 'POST'
Объект запроса¶
Объект запроса документирован в секции API, мы не будем рассматривать его здесь подробно (смотри request ). Вот широкий взгляд на некоторые наиболее распространённые операции. Прежде всего, вам необходимо импортировать его из модуля flask :
from flask import request
В настоящее время метод запроса доступен через использование атрибута method . Для доступа к данным формы (данным, которые передаются в запросах типа POST или PUT ), вы можете использовать атрибут form . Вот полноценный пример работы с двумя упомянутыми выше атрибутами:
@app.route('/login', methods=['POST', 'GET']) def login(): error = None if request.method == 'POST': if valid_login(request.form['username'], request.form['password']): return log_the_user_in(request.form['username']) else: error = 'Invalid username/password' # следущий код выполняется при методе запроса GET # или при признании полномочий недействительными return render_template('login.html', error=error)
Что произойдёт, если ключ, указанный в атрибуте form , не существует? В этом случае будет возбуждена специальная ошибка KeyError . Вы можете перехватить её подобно стандартной KeyError , но если вы этого не сделаете, вместо этого будет показана страница с ошибкой HTTP 400 Bad Request . Так что во многих ситуациях вам не придётся иметь дело с этой проблемой.
Для доступа к параметрам, представленным в URL ( ?ключ=значение ), вы можете использовать атрибут args :
searchword = request.args.get('key', '')
Мы рекомендуем доступ к параметрам внутри URL через get или через перехват KeyError , так как пользователь может изменить URL, а предъявление ему страницы с ошибкой 400 bad request не является дружественным.
За полным списком методов и атрибутов объекта запроса, обратитесь к следующей документации: request .
Загрузка файлов на сервер¶
В Flask обработка загружаемых на сервер файлов является несложным занятием. Просто убедитесь, что вы в вашей HTML-форме не забыли установить атрибут enctype=»multipart/form-data» , в противном случае браузер вообще не передаст файл.
Загруженные на сервер файлы сохраняются в памяти или во временной папке внутри файловой системы. Вы можете получить к ним доступ, через атрибут объекта запроса files . Каждый загруженный файл сохраняется в этом словаре. Он ведёт себя так же, как стандартный объект Python file , однако он также имеет метод save() , который вам позволяет сохранить данный файл внутрь файловой системы сервера. Вот простой пример, показывающий, как это работает:
from flask import request @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': f = request.files['the_file'] f.save('/var/www/uploads/uploaded_file.txt') .
Если вы хотите до загрузки файла в приложение узнать, как он назван на стороне клиента, вы можете просмотреть атрибут filename . Однако, имейте в виду, что данному значению никогда не стоит доверять, потому что оно может быть подделано. Если вы хотите использовать имя файла на клиентской стороне для сохранения файла на сервере, пропустите его через функцию secure_filename() , которой вас снабдил Werkzeug:
from flask import request from werkzeug import secure_filename @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': f = request.files['the_file'] f.save('/var/www/uploads/' + secure_filename(f.filename)) .
Некоторые более удачные примеры можно найти в разделе заготовок: Загрузка файлов .
Cookies¶
Для доступа к cookies можно использовать атрибут cookies . Для установки cookies можно использовать метод объектов ответа set_cookie . Атрибут объектов запроса cookies — это словарь со всеми cookies, которые передаёт клиент. Если вы хотите использовать сессии, то не используйте cookies напрямую, вместо этого используйте во Flask Сессии , который при работе с cookies даст вам некоторую дополнительную безопасность.
from flask import request @app.route('/') def index(): username = request.cookies.get('username') # Чтобы не получить в случае отсутствия cookie ошибку KeyError # используйте cookies.get(key) вместо cookies[key]
from flask import make_response @app.route('/') def index(): resp = make_response(render_template(. )) resp.set_cookie('username', 'the username') return resp
Заметьте, что cookies устанавливаются в объектах ответа. Так как вы обычно просто возвращаете строки из функций представления, Flask конвертирует их для вас в объекты ответа. Если вы это хотите сделать явно, то можете использовать функцию, make_response() , затем изменив её.
Иногда вы можете захотеть установить cookie в точке, где объект ответа ещё не существует. Это можно сделать, используя заготовку deferred-callbacks .
Также об этом можно почитать здесь: Об ответах .
Ошибки и перенаправления¶
Чтобы перенаправить пользователя в иную конечную точку, используйте функцию redirect() ; для того, чтобы преждевременно прервать запрос с кодом ошибки, используйте функцию abort() function:
from flask import abort, redirect, url_for @app.route('/') def index(): return redirect(url_for('login')) @app.route('/login') def login(): abort(401) this_is_never_executed()
Это довольно бессмысленный пример, потому что пользователь будет перенаправлен с индексной страницы на страницу, на которую у него нет доступа ( 401 означает отказ в доступе), однако он показывает, как это работает.
По умолчанию, для каждого кода ошибки отображается чёрно-белая страница с ошибкой. Если вы хотите видоизменить страницу с ошибкой, то можете использовать декоратор errorhandler() :
from flask import render_template @app.errorhandler(404) def page_not_found(error): return render_template('page_not_found.html'), 404
Обратите внимание на 404 после вызова render_template() . Это сообщит Flask, что код статуса для этой страницы должен быть 404, что означает «не найдено». По умолчанию предполагается код «200», который означает «всё прошло хорошо».
Об ответах¶
Возвращаемое из функции представления значение автоматически для вас конвертируется вас в объект ответа. Если возвращаемое значение является строкой, оно конвертируется в объект ответа в строку в виде тела ответа, код статуса 200 OK и в mimetype со значением text/html . Логика, которую применяет Flask для конвертации возвращаемых значений в объекты ответа следующая:
- Если возвращается объект ответа корректного типа, он прямо возвращается из представления.
- Если это строка, создаётся объект ответа с этими же данными и параметрами по умолчанию.
- Если возвращается кортеж, его элементы могут предоставлять дополнительную информацию. Такие кортежи должны соответствовать форме (ответ, статус, заголовки) , кортеж должен содержать хотя бы один из перечисленных элементов. Значение статус заменит код статуса, а элемент заголовки может быть или списком или словарём с дополнительными значениями заголовка.
- Если ничего из перечисленного не совпало, Flask предполагает, что возвращаемое значение — это допустимая WSGI-заявка, и конвертирует его в объект ответа.
Если вы хотите в результате ответа заполучить объект внутри представления, то можете использовать функцию make_response() .
Представим, что вы имеете подобное представление:
@app.errorhandler(404) def not_found(error): return render_template('error.html'), 404
Вам надо всего лишь обернуть возвращаемое выражение функцией make_response() и получить объект ответа для его модификации, а затем вернуть его:
@app.errorhandler(404) def not_found(error): resp = make_response(render_template('error.html'), 404) resp.headers['X-Something'] = 'A value' return resp
Сессии¶
В дополнение к объекту ответа есть ещё один объект, называемый session , который позволяет вам сохранять от одного запроса к другому информацию, специфичную для пользователя. Это реализовано для вас поверх cookies, при этом используется криптографическая подпись этих cookie. Это означает, что пользователь может посмотреть на содержимое cookie, но не может ничего в ней изменить, если он конечно не знает значение секретного ключа, использованного для создания подписи.
В случае использования сессий вам необходимо установить значение этого секретного ключа. Вот как работают сессии:
from flask import Flask, session, redirect, url_for, escape, request app = Flask(__name__) @app.route('/') def index(): if 'username' in session: return 'Logged in as %s' % escape(session['username']) return 'You are not logged in' @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': session['username'] = request.form['username'] return redirect(url_for('index')) return '''''' @app.route('/logout') def logout(): # удалить из сессии имя пользователя, если оно там есть session.pop('username', None) return redirect(url_for('index')) # set the secret key. keep this really secret: app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT'
Упомянутая escape() осуществляет для вас обработку специальных последовательностей (escaping), что необходимо, если вы не используете движок шаблонов (как в этом примере).
Как генерировать хорошие секретные ключи
Проблемой случайных значений является то, что трудно сказать, что действительно является является случайным. А секретный ключ должен быть настолько случайным, насколько это возможно. У вашей операционной системы есть способы для генерации достаточно случайных значений на базе криптографического случайного генератора, который может быть использован для получения таких ключей:
>>> import os >>> os.urandom(24) '\xfd
Просто возьмите, скопируйте/вставьте это в ваш код, вот и готово.
Замечание о сессиях на базе cookie: Flask возьмёт значения, которые вы помещаете в объект сессии, и сериализует их в cookie. Если вы обнаружили какие-либо значения, которые не сохраняются между запросами, а cookies реально включены, а никаких ясных сообщений об ошибках не было, проверьте размер cookie в ответах вашей страницы и сравните с размером, поддерживаемым веб-браузером.
Message Flashing¶
Хорошие приложения и интерфейсы пользователя дают обратную связь. Если пользователь не получает достаточной обратной связи, вскоре он может начать ненавидеть приложение. При помощи системы всплывающих сообщений Flask предоставляет пользователю по-настоящему простой способ обратной связи. Система всплывающих сообщений обычно делает возможным записать сообщение в конце запроса и получить к нему доступ во время обработки следующего и только следующего запроса. Обычно эти сообщения используются в шаблонах макетов страниц, которые его и отображают.
Чтобы вызвать всплывающие сообщения, используйте метод flash() , чтобы заполучить сообщения, можно использовать метод, также доступный для шаблонов — get_flashed_messages() . Полный пример приведён в разделе Всплывающие сообщения .
Ведение журналов¶
Добавлено в версии 0.3.
Иногда может возникнуть ситуация, в которой вы имеете дело с данными, которые должны быть корректными, но в действительности это не так. К примеру, у вас может быть некий код клиентской стороны, который посылает HTTP-запрос к серверу, однако он очевидным образом неверен. Это может произойти из-за манипуляции пользователя с данными, или из-за неудачной работы клиентского кода. В большинстве случаев ответом, адекватным ситуации будет 400 Bad Request , но иногда, когда надо, чтобы код продолжал работать, это не годится.
Вы по-прежнему хотите иметь журнал того, что пошло не так. Вот где могут пригодиться объекты создания журнала logger . Начиная с Flask 0.3, инструмент для журналирования уже настроен для использования.
Вот некоторые примеры вызовов функции журналирования:
app.logger.debug('Значение для отладки') app.logger.warning('Предупреждение: (%d яблок)', 42) app.logger.error('Ошибка')
Прилагаемый logger это стандартный класс журналирования Logger , так что за подробностями вы можете обратиться к официальной документации по журналированию.
Как зацепиться (hooking) к промежуточному слою WSGI¶
Если вы хотите добавить в ваше приложение слой промежуточного, или связующего для WSGI программного обеспечения (middleware), вы можете обернуть внутреннее WSGI-приложение. К примеру, если вы хотите использовать одно из middleware из пакета Werkzeug для обхода известных багов в lighttpd, вы можете сделать это подобным образом:
from werkzeug.contrib.fixers import LighttpdCGIRootFix app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app)
Развёртывание приложения на веб-сервере¶
Готовы к развёртыванию на сервере вашего нового приложения Flask? В завершение краткого руководства, вы можете немедленно развернуть приложение на одной из платформ хостинга, предоставляющих бесплатное размещение для малых проектов:
- Развёртывание приложения Flask на Heroku
- Развёртывание WSGI в dotCloud с специфическими для Flask замечаниями
Другие места, где можно разместить ваше приложение:
- Развёртывание приложения Flask на Webfaction
- Развёртывание приложения Flask в Google App Engine
- Общий доступ к локальному хосту с помощью Localtunnel
Если вы управляете собственными хостами и желаете разместиться у себя, смотрите раздел Варианты развёртывания .
Как запустить сайт с помощью Gunicorn: Django, Flask, что угодно
Рано или поздно вы приходите к тому, что надо запустить проект на сервере. Этот туториал покажет как превратить любой Python-скрипт в сайт. Также будут примеры запуска проектов, сделанных на Django и Flask.
Цель туториала — сделать сайт, который узнает ваш IP-адрес:
Что надо знать
Конечно же, без определённых знаний и навыков запустить сайт на сервере не получится. Вот небольшой список того, что вы должны знать и уметь:
- Знаете что такое веб-сервер
- Знаете что такое HTTP запрос/ответ
- Умеете работать с вёрсткой
- Умеете работать с Systemd
- Умеете подключаться к серверу по ssh
- Запускаете команды из консоли
Системные требования
- ОС Ubuntu 20.04 LTS
- Python3
1. Запустите веб-сервер
Без веб-сервера не работает ни один сайт в интернете. Каждый раз, когда вы открываете страницу сайта ваш браузер связывается с сервером и запрашивает необходимые для работы ресурсы: HTML, картинки, шрифты и прочее. Веб-сервер отвечает за то, чтобы найти эти файлы и переслать их браузеру.
Давайте начнём с самого простого сайта, который просто показывает текст Here be dragons . Сайт будет реализован в виде Python-скрипта. Чтобы превратить скрипт в сайт нужен веб-сервер. А чтобы веб-сервер понял, как правильно запустить скрипт, существует стандарт WSGI.
WSGI — это стандарт взаимодействия между Python-скриптом и веб-сервером
Стандарт WSGI требует, чтобы в скрипте была особенная функция. Она принимает на вход два аргумента — словарь с данными HTTP-запроса и обработчик запроса. Когда веб-сервер снова получит от браузера HTTP-запрос, то он найдёт эту функцию и запустит.
Стандарт WSGI поддерживает много разных веб-серверов. Один из них Gunicorn. Он относительно быстр, легко настраивается и работает со многими веб-фреймворками. Написан Gunicorn на Python и поставляется в виде обычной библиотеки.
Gunicorn — это веб-сервер с поддержкой стандарта WSGI
Что-ж, пора начинать. Сперва установите Gunicorn:
# pip install gunicorn
Теперь создайте ту самую функцию для обработки HTTP-запросов. Позже её запустит Gunicorn. Создайте файл server.py и положите в него код:
def process_http_request(environ, start_response): status = '200 OK' response_headers = [ ('Content-type', 'text/plain; charset=utf-8'), ] start_response(status, response_headers) text = 'Here be dragons'.encode('utf-8') return [text]
Веб-сервер Gunicorn сам запустит функцию process_http_request . Согласно стандарту WSGI она обязана принимать два аргумента environ и start_response .
environ — это словарь с данными об HTTP запросе. Запрос от браузер сначала прилетает к веб-серверу Gunicorn, тот упаковывает полезную информацию в словарь environ и передаёт его в функцию process_http_request .
start_response — это функция, которую даёт Gunicorn. Она нужна, чтобы отправить в браузер первую самую важную часть – статус HTTP ответа и заголовки. В качестве аргументов она принимает статус 200 OK и заголовок Content-type: text/plain; charset=utf-8; . Gunicorn упакует их и отправит браузеру.
Функция process_http_request возвращает список строк с текстом Here be dragons . Это то, что браузер получит в теле HTTP ответа. Перед отправкой программа кодирует текст в utf-8 , так как в интернете можно передавать только закодированный текст.
Протокол HTTP позволяет отвечать браузеру не сразу, а порциями. Случается, что веб-сервер раздаёт не только мелкие фрагменты тексты, но и огромные видеофайлы. За один раз передать такой объём невозможно, поэтому HTTP-ответ разбивается на части. А раз стандарт WSGI обязан быть универсальным, то он всегда требует от функций списка или другого итерируемого объекта с бинарными строками внутри. В данном случае используется список с одним единственным элементом – закодированной строкой ‘Here be dragons’ .
В скрипте сейчас есть всё необходимое, чтобы увидеть текст Here be dragons в браузере. Осталось запустить веб-сервер Gunicorn. Запустите его командой:
$ gunicorn -b 82.148.28.32:80 server:process_http_request
Ключ -b или —bind означает “привязка” к определённому IP-адресу и порту.
82.148.28.32 — это IP-адрес сервера на котором хотите запустить Gunicorn. Тем же способом можно запустить веб-сервер локально на адресе 127.0.0.1 , либо на всех сетевых интерфейсах сразу через 0.0.0.0 .
80 — номер порта, на котором по умолчанию работает протокол HTTP . Если привязать Gunicorn к другому порту, то придётся вручную указывать порт в адресной строке браузера: http://82.148.28.32:8080 .
server — название вашего скрипта без .py , а process_http_request — название функции. Так вы укажете Gunicorn, где искать функцию, которая обработает запрос браузера.
После запуска Gunicorn сообщит в консоль, что он готов и ждёт входящих запросов от браузера:
[2020-08-06 11:32:08 +0400] [13234] [INFO] Starting gunicorn 20.0.4 [2020-08-06 11:32:08 +0400] [13234] [INFO] Listening at: http://82.148.28.32:80 (13234) [2020-08-06 11:32:08 +0400] [13234] [INFO] Using worker: sync [2020-08-06 11:32:08 +0400] [13237] [INFO] Booting worker with pid: 13237
Давайте проверим работу Gunicorn. Для этого откройте сайт и в адресной строке браузера введите IP-адрес сервера http://82.148.28.32 . Вы увидите текст Here be dragons :
Скрипт отправляет не только текст, но ещё статус и заголовки. Их можно проверить через инструменты разработчика браузера:
В Status Code вы видите статус 200 OK и заголовок Content-type , в котором обычный текст text/plain .
Отлично, вы теперь умеете запускать веб-сервер Gunicorn!
2. Сделайте сайт
У вас есть заготовка сайта Here be dragons. Превратим её в сайт, что определяет IP-адрес пользователя.
Добавьте в скрипт server.py HTML разметку:
HTML = """ Узнать IP адрес Ваш IP-адрес 0.0.0.0
""" def process_http_request(environ, start_response): status = '200 OK' response_headers = [ ('Content-type', 'text/html; charset=utf-8'), ] start_response(status, response_headers) html_as_bytes = HTML.encode('utf-8') return [html_as_bytes]
Так как вместо Here be dragons функция теперь возвращает HTML, то Content-type изменился на text/html — вы сообщаете браузеру, что это разметка HTML.
Протестируйте обновлённый скрипт. Остановите Gunicorn командой Ctrl-C в консоли и запустите снова. В браузере обновите страницу сайта:
Сайт работает, IP-адрес он показывает не настоящий: 0.0.0.0 . Это лишь временная заглушка.
А откуда взять IP-адрес? Функция process_http_request принимает всего лишь два аргумента: словарь environ и функцию start_response . Данные об HTTP запросе environ получены от вашего браузера, значит и IP-адрес стоит искать там.
Содержимое словаря environ можно вывести в консоль и посмотреть что там лежит. Для этого добавьте в вашу функцию одну отладочную строчку кода перед return :
def process_http_request(environ, start_response): ... print(environ) return [html_as_bytes]
Перезапустите Gunicorn и обновите страницу в браузере. В консоли вы увидите подобный вывод:
[2020-08-06 10:30:02 +0000] [58594] [INFO] Starting gunicorn 20.0.4 [2020-08-06 10:30:02 +0000] [58594] [INFO] Listening at: http://0.0.0.0:80 (58594) [2020-08-06 10:30:02 +0000] [58594] [INFO] Using worker: sync [2020-08-06 10:30:02 +0000] [58596] [INFO] Booting worker with pid: 58596 , 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': , 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.0.4', 'wsgi.input': , 'gunicorn.socket': , 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': '82.148.28.32', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_CACHE_CONTROL': 'max-age=0', 'HTTP_DNT': '1', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 'HTTP_ACCEPT_LANGUAGE': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '79.141.160.49', 'REMOTE_PORT': '57897', 'SERVER_NAME': '0.0.0.0', 'SERVER_PORT': '80', 'PATH_INFO': '/', 'SCRIPT_NAME': ''>
IP-адрес вашего компьютера находится в значении по ключу REMOTE_ADDR : 79.141.160.49 . Чтобы проверить свой IP можете обратиться к уже готовому сервису в интернете — 2ip.ru.
Остался последний рывок — отобразить IP-адрес на сайте. Допишите скрипт и перезапустите Gunicorn. Получится что-то подобное:
HTML = """ Узнать IP адрес Ваш IP-адрес """
def process_http_request(environ, start_response): status = '200 OK' response_headers = [ ('Content-type', 'text/html; charset=utf-8'), ] start_response(status, response_headers) html = HTML.format(ip_address=environ["REMOTE_ADDR"]) html_as_bytes = html.encode('utf-8') return [html_as_bytes]
Ура! Теперь у вас работает полноценный сайт. Сайт по определению IP-адреса:
3. Демонизируйте Gunicorn
В норме программы, запущенные внутри сеанса ssh будут остановлены вместе с завершением этого сеанса. Стоит пользователю отключиться от сервера и сайт сразу упадёт из-за остановки Gunicorn.
Для решения таких проблем в Linux существуют демоны — это программы, которые работают в фоне. Их запускает не пользователь, а операционная система, благодаря чему демон продолжает работать даже когда пользователь уходит с сервера. Чтобы сайт не падал Gunicorn тоже надо сделать демоном.
Systemd — система, которая управляет демонами. С её помощью вы сделаете Gunicorn демоном, а также сможете отслеживать его работу. С Systemd ваш сайт всегда будет онлайн.
Перейдите в папку /etc/systemd/system и создайте файл getip.service с настройками для будущего демона Gunicorn:
[Unit] Description=GetIP site [Service] Type=simple WorkingDirectory=/root/wsgi ExecStart=gunicorn -b 82.148.28.32:80 server:process_http_request Restart=always [Install] WantedBy=multi-user.target
Замените путь к каталогу WorkingDirectory=/root/wsgi на тот, где лежит ваш скрипт. Также поменяйте IP адрес в настройке ExecStart .
После добавления нового сервиса перенастройте Systemd и запустите сервис:
$ systemctl daemon-reload $ systemctl start getip.service
Если при запуске сервиса будет ошибка, то вы не заметите этого. Проверить работу сервиса можно командой:
# systemctl status getip
Вот пример вывода статуса сервиса:
Если всё прошло успешно, вы увидите зелёный кружок перед именем сервиса getip.service и зелёный индикатор активности active (running) .
Осталось добавить сервис getip в автозапуск при старте сервера:
# systemctl enable getip.service
Вы увидите в консоли:
Created symlink /etc/systemd/system/multi-user.target.wants/getip.service → /etc/systemd/system/getip.service.
Теперь ваш сайт будет работать всегда! Даже если вы перезапустите сервер, то Gunicorn включится сам.
«Девман» — авторская методика обучения программированию. Готовим к работе крутых программистов на Python. Станьте программистом, пройдите курс программирования на Python.
4. Добавьте воркеров к Gunicorn
Gunicorn — это серьёзный веб-сервер, рассчитанный на большую нагрузку. Он умеет обрабатывать несколько запросов одновременно благодаря своей архитектуре. У Gunicorn есть главный процесс, который управляет набором рабочих процессов — воркеров. Главный процесс только распределяет запросы от клиентов сайта, а обрабатывают их воркеры.
Проверьте, сколько воркеров сейчас использует Gunicorn. Вдруг он использует сервер не на полную? Количество воркеров можно проверить в статусе демона. Проверьте статус Gunicorn командой:
# systemctl status getip
Вы увидите статус Gunicorn:
На скриншоте красным выделена группа процессов. Видно, что запущено 2 процесса: 1 главный и 1 рабочий — воркер.
Веб серверу нужен хотя бы один воркер. Но одного воркера мало, чтобы разогнать Gunicorn по максимуму. Это всё из-за того, что воркеру мешают операции ввода/вывода, из за которых Python засыпает и ждёт ответа. Если воркеров будет мало, сервер работает не в полную силу, а если слишком много — тормозит. Число воркеров зависит от ядер процессора. Документация Gunicorn советует придерживаться такой формулы:
N воркеров = Количество ядер x 2 + 1
Количество ядер процессора показывает штатная утилита nproc:
$ nproc
Вот пример вывода npoc:
Итак, если у процессора 1 ядро, то по формуле получается 3 воркера. Эта опция указывается в настройке демона Gunicorn. Вот новое содержимое файла getip.service :
[Unit] Description=GetIP site [Service] Type=simple WorkingDirectory=/root/wsgi ExecStart=gunicorn -w 3 -b 82.148.28.32:80 server:process_http_request Restart=always [Install] WantedBy=multi-user.target
Поменялись только настройки ExecStart . Gunicorn запускается с ещё одним ключом -w или —workers , что как раз и означает количество воркеров.
А теперь проверьте статус Gunicorn:
# systemctl status getip
В консоли будет:
Снова обратите внимание на группу процессов. Сейчас запущено 4 процесса: 1 главный и 3 воркера. Теперь Gunicorn будет работать быстрее.
5. Как запустить Django через Gunicorn
В Django уже есть встроенный веб-сервер — runserver. Его используют для быстрой отладки во время разработки. Но сами же разработчики Django прямым текстом пишут о том, что использовать в продакшне его не стоит:
DO NOT USE THIS SERVER IN A PRODUCTION SETTING. It has not gone through security audits or performance tests.
Gunicorn быстрее и заботится о безопасности. Его можно использовать в продакшне, и даже ребята из Django советуют это делать.
Django поддерживает работу с WSGI веб-серверами из коробки. Django оборачивает весь проект в одну простую функцию и кладёт её в файлы wsgi.py и asgi.py в папке проекта рядом с settings.py .
Ниже будет пример запуска пустого Django-проекта через Gunicorn. В результате вы увидите в браузере страницу Django “Congratulations!” . Это та самая страница с ракетой.
В репозитории Django уже создала файлы wsgi.py и asgi.py . Который их них запускать? Файл asgi.py создан для асинхронных веб-серверов, а Gunicorn так не умеет. Используйте файл wsgi.py .
Для Django проекта важно из какого каталога будет запущен Gunicorn. Скрипты Python вычисляют пути к другим файлам проекта принимая за точку отсчёта корень проекта. А корнем считается текущий путь – тот каталог, откуда запущены скрипты. Если запустить Gunicorn из другого каталога, то и путь к корню проекта получится неправильный.
Воспользуйтесь утилитой tree, чтобы понять, откуда запускать Gunicorn:
$ tree . ├── blog │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py 1 directory, 6 files
Как видно, wsgi.py лежит в папке blog . Gunicorn запускается уровнем выше — оттуда, где лежит manage.py :
$ gunicorn -b 82.148.28.32:80 blog.wsgi:application
Вот и всё, что требуется чтобы запустить Django-проект через Gunicorn!
6. Как запустить Flask через Gunicorn
Фреймворк Flask, как и Django, тоже поддерживает работу с Gunicorn из коробки.
Вот простейший сайт на Flask, который выводит в браузер текст Here be dragons . Содержимое скрипта server.py :
from flask import Flask, Response app = Flask(__name__) @app.route("/") def index(): return Response("Here be dragons"), 200 if __name__ == "__main__": app.run(debug=True)
Сайт можно запустить в отладочном режиме:
$ python server.py
Для этого в скрипте есть блок if __name__ == «__main__» со строкой запуска отладочного сервера Flask.
Как и runserver в Django, тот тоже работает в однопоточном режиме и не справится с нагрузкой на сайт. На помощь придёт Gunicorn. Нужна функция соответствующая WSGI-протоколу.
Объект app во Flask-скрипте ведёт себя подобно функции – его можно запустить, передав аргументы:
app(environs, start_response)
Чтобы запустить сайт Flask через Gunicorn достаточно выполнить команду:
$ gunicorn -b 82.148.28.32:80 server:app
server — название модуля без .py , app — функция в скрипте.
Как видите, запустить Flask-проект через Gunicorn очень легко.
Что почитать
- PEP 333 – Python Web Server Gateway Interface
- Официальный сайт Gunicorn
Попробуйте бесплатные уроки по Python
Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.
Переходите на страницу учебных модулей «Девмана» и выбирайте тему.
Как запустить Flask на сервере?
Здравствуйте, у меня возникли трудности при продакшене Flask на сервере. Насколько я разобрался в теме, ему нужен сервер, чаще всего это apache или nginx и как правило linux, видел решение для windows, также нужно настроить wsgi, который объяснит серверу питон. Есть ли какие-то хорошие решение под windows server, что лучше использовать, чтобы работало через https и домен? Есть ли какая-то инструкция для развёртывания этого всего на удалённой машине? В приложение также используется MySQL, это тоже я думаю стоит учитывать, при выборе веб сервера для Flask.
Пробовал этот гайд для windows: https://thilinamad.medium.com/flask-app-deployment. . С ним возникли проблемы, так как apache пишет it work, но на деле самого проекта не видно и это только на 443 порте, а это http, https, как я понял работает на 80 портах, когда их указывал писал forbiden. (проблемы с доступом к файлу). Заранее извиняюсь за лишнию воду, просто не знаю как описать это более точно.
- Вопрос задан более двух лет назад
- 1546 просмотров