http://habrahabr.ru/post/268993/
Проект был написан скорее в учебных целях (научиться сетевому программированию в Python), чем в практических. Такую же роль несёт и статься, ведь сейчас вряд ли кто-то будет скачивать сайты, чтобы прочитать пару статеек (за исключением некоторых случаев, когда подобное реально может пригодится).
Не так давно качество мобильного интернета в моём городе стало постепенно ухудшаться из-за возрастающей на сети операторов нагрузки и некоторые сайты, требующие большое количество соединений (зависимые файлы страницы) стали загружаться ну ОЧЕНЬ медленно. По вечерам скорость опускается на столько, что некоторые сайты могут полностью загружаться в течении нескольких десятков секунд.
Есть несколько способов решения данной проблемы, но я решил выбрать немного необычный для нашего времени способ. Я решил скачивать сайты. Конечно, данных способ не подходит для крупных сайтов, вроде Хабра, тут разумнее использовать парсер, но можно скачать и отдельный хаб, список пользователей, или только свои публикации с помощью HTTrack Website Copier, применив фильтры. Например, чтобы скачать хаб Python с Хабра нужно применить фильтр "+habrahabr.ru/hub/python/*".
Этот способ можно использовать ещё в нескольких целях. Например, чтобы скачать сайт, или его часть, перед тем, как вы окажитесь без интернет-соединения, например, в самолёте. Или для того, чтобы скачать заблокированные на территории РФ сайты, если скачивать их через Tor, что будет очень медленно, или через компьютер в другой стране, где данных сайт не запрещён, а потом передать его на компьютер, находящийся в РФ, что будет гораздо быстрее для многостраничных сайтов. Таким образом мы может скачать, например,
xHamster Wikipedia через сервер в Германии или Нидерландах и получить сайт в сжатом виде по SFTP, FTP, HTTP или другому, удобному для вас, протоколу. Если, конечно, места хватит, для такого большого сайта :)
Ну что, начнём!? Приложение будет постепенно усложнятся и в него будет добавляться всё новых функционал, это позволит понять что вообще здесь происходит и как это всё работает. Код я буду сопровождать большим достаточным количеством комментариев, чтобы его мог понять даже человек, не знающий Python, но повторно комментировать уже описанные куски кода и функции не буду, дабы не загромождать код. И сервер и клиент пишутся и проверяются под Linux, но,
теоретически, должны работать и под другими платформами, если установлены все необходимые приложения, а именно:
httrack и
tar, а так же выставлен необходимый путь в конфигурационном файле, который мы создадим ниже. Если у вас появятся проблемы с запускам под вашей платформой, пишите в комментариях
Для начала реализуем простой сервер который будет пересылать строку клиенту.
# FILE: server.py
import socket
# Создаём IPv4 сокет потокового типа
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Связываем сокет с адресом localhost и портом 65042
sock.bind(("localhost", 65042))
# Начинаем слушать
sock.listen(True)
# По мере поступления
while True:
# При присоединении клиента создаём две переменные для управления соединения с клиентом и адрес этого клиента
conn, addr = sock.accept()
# Выводим адрес этого клиента
print('Connected by', addr)
# Читаем переданных клиентом данные, но не более 1024 байт
data = conn.recv(1024)
# Отправляем клиенту полученную от него же строку
conn.sendall(data)
Теперь реализуем ещё более простой клиент, который будет выводить принятую (то есть отправленную им же серверу) строку.
# FILE: client.py
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Присоединяемся к серверу
sock.connect(("localhost", 65042))
sock.sendall(b"Hello, world")
# Читаем данные от сервера, но не более 1024 байт
data = sock.recv(1024)
# Закрываем соединение
sock.close()
# Выводим полученные данные
print(data.decode("utf-8"))
При выводе мы использовали метод
decode(original) чтобы получить из массива байт строку. Чтобы расшифровать массив байт нужно указать кодировку, в нашем случае это
UTF-8.
Теперь нужно ненадолго остановиться и обдумать, каким образом мы будем использовать наше приложение, какие команды будут использоваться и как вообще будет выглядеть общение между клиентом и сервером.
Так как мы мы планируем использовать наше приложение изредка, то с удобством можно особо не париться. Что же должно уметь делать наше приложение? В первую очередь, это скачивать сайты. Хорошо, серверное приложение скачало наш сайт, что теперь? Нам ведь хочется его посмотреть, ведь так? Для этого нужно его передать с серверной машины на клиентскую, а так как количество файлов очень большое, а со временем установления соединения у нас большие проблемы, то неплохо было ещё и упаковать всё это, желательно ещё и хорошенько сжать. Ну и неплохо было бы иметь возможность просмотреть скаченные сайты, но об этом чуть позже.
Команды, передаваемые серверу, будут иметь следующий формат:
<command> [args]
Например:
dl site.ru 0 gz
list
list during
Для начала немного модифицируем наш клиент. Заменим
sock.sendall(b"Hello, world")
на
sock.sendall(bytes(input(), encoding="utf-8"))
Теперь мы можем передавать на сервер произвольные команды, введённые с клавиатуры.
Перейдём к серверу, тут всё посложнее.
Для начала создадим два файла:
httrack.py и
config.py. Первый будет содержать функции для управления HTTrack, второй — конфигурацию для клиента и сервера (он будет общим). При желании можете сделать конфигурационный файл для сервера и клиента раздельным и использовать не питоновский формат, а конфигурационный
.ini, или что-то подобное.
Со вторым файлом всё просто и понятно:
from os import path
host = 'localhost'
port = 65042
# Путь для скачивания сайтов. На данный момент - директория <b>Sites</b> в домашнем каталоге. Измените значение, если данный путь вам не подходит, рекомендуется указать пустую или несуществующую директорию.
sites_directory = path.expanduser("~") + "/Sites"
Перед тем как перейти к первому файлу, немного расскажу про функцию
call из стандартной библиотеки
subprocess.
subprocess.call(args)
Функция исполняет команду, переданную в массиве
args. Эта функция так же может принимать параметр
cwd, задающий каталог, в котором следует выполнить команду из массива
args. Ждёт завершения исполняемой команды (вызванной программы) и возвращает код завершения.
Теперь напишем нашу, пока единственную, функцию управления HTTrack'ом, позволяющую скачивать сайт в нужную нам директорию:
# FILE: httrack.py
from subprocess import call
from os import makedirs
# Файл с конфигурацией в директории с проектом
import config
def download(url):
# Избавляемся от указанного протокола (в строке, разумеется), если он есть.
if url.find("//"):
url = url[url.find("//")+2:]
# И от завершающего слэша
if url[-1:] == '/':
url = url[:-1]
site = config.sites_directory + '/' + url
print("Downloading ", url, " started.")
# Создаём папку, в которую будут скачиваться все сайты
makedirs(config.sites_directory, mode=0o755, exist_ok=True)
# Вызываем HTTrack в нужной нам директории
call(["httrack", url], cwd=config.sites_directory)
print("Downloading is complete")
Изменим
server.py:
import socket
import threading
# Файлы в директории с проектом
import httrack
import config
def handle_commands(connection, command, params):
if command == "dl":
# Создание отдельного треда (потока, процесса, если хотите) для HTTrack'а
htt_thread = threading.Thread(target=httrack.download, args=(params[0]))
# и его запуск
htt_thread.start()
connection.sendall(b'Downloading has started')
else:
connection.sendall(b"Invalid request")
def args_analysis(connection, args):
# Разбиваем строку на массив на каждом пробеле. Например "dl site.ru 0 gz" превратится в ["dl", "site.ru", "0", "gz"].
args = args.decode("utf-8").split()
# [1:] - срез. В данном случае, с первого до последнего элемента.
handle_commands(connection=connection, command=args[0], params=args[1:])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((config.host, config.port))
sock.listen(True)
while True:
conn, addr = sock.accept()
print('Connected by ', addr)
data = conn.recv(1024)
args_analysis(connection=conn, args=data)
Тут, я думаю, всё понятно. Код немного усложнён функциями, которые можно объединить и тем самым упростить код, но они помогут нам в последующих изменениях кода.
На данный момент можем запустить сначала
server.py, а затем
client.py. В клиентском приложении вводим следующую команду:
dl http://verysimplesites.co.uk/
Примерно через минуту, в зависимости от вашего интернет-соединения, серверное приложение выведет "
Downloading is complete" и у вас в домашнем каталоге появиться папка
Sites, а в ней каталог
verysimplesites.co.uk, в котором уже лежит скаченный сайт, который можно открыть в браузере без интернет-соединения.
Но нам этого мало, мы ведь хотим, чтобы сайт можно было получить в сжатом виде, в архиве. Пусть теперь у команды
dl будет три аргумента, а не один. Первый остаётся таким же, это сайт, который необходимо скачать. Второй — флаг, показывающий, удалять ли директорию по-завершении скачивания. Третий — формат архива, в который будет упакован сайт после скачивания (до удаления, если оно требуется).
Функция проверки статуса процесса httrack в
server.py:
def dl_status_checker(thread, connection):
if thread.isAlive:
connection.sendall(b'Downloading has started')
else:
connection.sendall(b'Downloading has FAILED')
Команда
dl в
server.py:
if command == "dl":
# Флаг удаления директории поднят, если аргумент не равен <b>"0"</b>
if params[1] == '0':
params[1] = False
else:
params[1] = True
# Если директорию удалять мы не собираемся и формат архива мы не передали, то упаковывать мы не будем
if not params[1] and len(params) == 2:
params.append(None)
htt_thread = threading.Thread(target=httrack.download, args=(params[0], params[1], params[2]))
htt_thread.start()
# Через 2 секунды проверить, работает ли всё ещё HTTrack
dl_status = threading.Timer(2.0, dl_status_checker, args=(htt_thread, connection))
dl_status.start()
httrack.py:
from subprocess import call
from os import makedirs
from shutil import rmtree
import config
def download(url, remove, archive_format):
if url.find("//"):
url = url[url.find("//")+2:]
if url[-1:] == '/':
url = url[:-1]
site = config.sites_directory + '/' + url
print("Downloading ", url, " started.")
makedirs(config.sites_directory, mode=0o755, exist_ok=True)
call(["httrack", url], cwd=config.sites_directory)
print("Downloading is complete")
if archive_format:
if archive_format == "gz":
# Например: <b>tar -czf /home/user/Sites/site.ru.tar.gz -C /home/user/Sites /home/user/Sites/site.ru</b>
call(["tar", "-czf", config.sites_directory + '/' + url + ".tar.gz",
"-C", config.sites_directory, url], cwd=config.sites_directory)
elif archive_format == "bz2":
call(["tar", "-cjf", config.sites_directory + '/' + url + ".tar.bz2",
"-C", config.sites_directory, url], cwd=config.sites_directory)
elif archive_format == "tar":
call(["tar", "-cf", config.sites_directory + '/' + url + ".tar",
"-C", config.sites_directory, url], cwd=config.sites_directory)
else:
print("Archive format is wrong")
else:
print("The site is not packed")
if remove:
rmtree(site)
print("Removing is complete")
else:
print("Removing is canceled")
Появилось много нового кода, но ничего сложного в нём нет, просто появилось несколько новых условий. Из новых функций появилась только
rmtree, которая удаляет переданную ей директорию, включая всё, что находилось в последней.
Можно добавить в функцию
handle_commands простую команду
list без параметров:
elif command == "list":
# Получаем список файлов и директорий в директории с сайтами
file_list = listdir(config.sites_directory)
folder_list = []
archive_list = []
# Проверяем в цикле, есть ли у нас директории или архивы, содержащие сайты
for file in file_list:
if path.isdir(config.sites_directory + '/' + file) and file != "hts-cache":
folder_list.append(file)
if path.isfile(config.sites_directory + '/' + file) and \
(file[-7:] == ".tar.gz" or file[-8:] == ".tar.bz2" or file[-5:] == ".tar"):
archive_list.append(file)
site_string = ""
folder_found = False
# Проверка на пустоту массива
if folder_list:
site_string += "List of folders:\n" + "\n".join(folder_list)
folder_found = True
if archive_list:
if folder_found:
site_string += "\n================================================================================\n"
site_string += "List of archives:\n" + "\n".join(archive_list)
if site_string == "":
site_string = "Sites not found!"
connection.sendall(bytes(site_string, encoding="utf-8"))
Подключив в начале необходимую библиотеку:
from os import listdir, path
Ещё неплохо было бы увеличить максимальный размер принимаемых клиентом данных от сервера в
client.py:
data = sock.recv(65536)
Теперь перезапустим
server.py и запустим
client.py. Для начала прикажем скачать серверу какой-нибудь сайт и упаковать его в tar.gz архив, после чего удалить:
dl http://verysimplesites.co.uk/ 1 gz
После этого скачаем другой сайт, но упаковывать его не будем, удалять, разумеется, тоже:
dl http://example.com/ 0
И спустя примерно минуту проверим список сайтов:
list
Если вы вводили те же команды, то должны получить следующий ответ от сервера:
List of folders:
example.com
================================================================================
List of archives:
verysimplesites.co.uk.tar.gz
На сегодня это, пожалую, всё. Это, конечно, далеко всё, что можно и нужно реализовать, но, тем не менее, позволяет понять общий принцип работы подобных приложений. Если вам интересная данная тема, то пишите об этом в комментариях, если такие найдутся, то я постараюсь выделить время и написать статью о чуть большем функционале данного приложения, в том числе и просмотр статуса скачивания сайта, а так же покажу несколько способов защиты от проникновения на сервер посторонних, в том числе и защиту от проникновения в оболочку.