Проект был написан скорее в учебных целях (научиться сетевому программированию в Python), чем в практических. Такую же роль несёт и статься, ведь сейчас вряд ли кто-то будет скачивать сайты, чтобы прочитать пару статеек (за исключением некоторых случаев, когда подобное реально может пригодится).
Не так давно качество мобильного интернета в моём городе стало постепенно ухудшаться из-за возрастающей на сети операторов нагрузки и некоторые сайты, требующие большое количество соединений (зависимые файлы страницы) стали загружаться ну ОЧЕНЬ медленно. По вечерам скорость опускается на столько, что некоторые сайты могут полностью загружаться в течении нескольких десятков секунд.
Есть несколько способов решения данной проблемы, но я решил выбрать немного необычный для нашего времени способ. Я решил скачивать сайты. Конечно, данных способ не подходит для крупных сайтов, вроде Хабра, тут разумнее использовать парсер, но можно скачать и отдельный хаб, список пользователей, или только свои публикации с помощью HTTrack Website Copier, применив фильтры. Например, чтобы скачать хаб Python с Хабра нужно применить фильтр "+habrahabr.ru/hub/python/*".
Этот способ можно использовать ещё в нескольких целях. Например, чтобы скачать сайт, или его часть, перед тем, как вы окажитесь без интернет-соединения, например, в самолёте. Или для того, чтобы скачать заблокированные на территории РФ сайты, если скачивать их через Tor, что будет очень медленно, или через компьютер в другой стране, где данных сайт не запрещён, а потом передать его на компьютер, находящийся в РФ, что будет гораздо быстрее для многостраничных сайтов. Таким образом мы может скачать, например,xHamster Wikipedia через сервер в Германии или Нидерландах и получить сайт в сжатом виде по SFTP, FTP, HTTP или другому, удобному для вас, протоколу. Если, конечно, места хватит, для такого большого сайта :)
Ну что, начнём!? Приложение будет постепенно усложнятся и в него будет добавляться всё новых функционал, это позволит понять что вообще здесь происходит и как это всё работает. Код я буду сопровождать большим достаточным количеством комментариев, чтобы его мог понять даже человек, не знающий Python, но повторно комментировать уже описанные куски кода и функции не буду, дабы не загромождать код. И сервер и клиент пишутся и проверяются под Linux, но, теоретически, должны работать и под другими платформами, если установлены все необходимые приложения, а именно: httrack и tar, а так же выставлен необходимый путь в конфигурационном файле, который мы создадим ниже. Если у вас появятся проблемы с запускам под вашей платформой, пишите в комментариях
Для начала реализуем простой сервер который будет пересылать строку клиенту.
Теперь реализуем ещё более простой клиент, который будет выводить принятую (то есть отправленную им же серверу) строку.
При выводе мы использовали метод decode(original) чтобы получить из массива байт строку. Чтобы расшифровать массив байт нужно указать кодировку, в нашем случае это UTF-8.
Теперь нужно ненадолго остановиться и обдумать, каким образом мы будем использовать наше приложение, какие команды будут использоваться и как вообще будет выглядеть общение между клиентом и сервером.
Так как мы мы планируем использовать наше приложение изредка, то с удобством можно особо не париться. Что же должно уметь делать наше приложение? В первую очередь, это скачивать сайты. Хорошо, серверное приложение скачало наш сайт, что теперь? Нам ведь хочется его посмотреть, ведь так? Для этого нужно его передать с серверной машины на клиентскую, а так как количество файлов очень большое, а со временем установления соединения у нас большие проблемы, то неплохо было ещё и упаковать всё это, желательно ещё и хорошенько сжать. Ну и неплохо было бы иметь возможность просмотреть скаченные сайты, но об этом чуть позже.
Команды, передаваемые серверу, будут иметь следующий формат:
Например:
Для начала немного модифицируем наш клиент. Заменим
на
Теперь мы можем передавать на сервер произвольные команды, введённые с клавиатуры.
Перейдём к серверу, тут всё посложнее.
Для начала создадим два файла: httrack.py и config.py. Первый будет содержать функции для управления HTTrack, второй — конфигурацию для клиента и сервера (он будет общим). При желании можете сделать конфигурационный файл для сервера и клиента раздельным и использовать не питоновский формат, а конфигурационный .ini, или что-то подобное.
Со вторым файлом всё просто и понятно:
Перед тем как перейти к первому файлу, немного расскажу про функцию call из стандартной библиотеки subprocess.
Функция исполняет команду, переданную в массиве args. Эта функция так же может принимать параметр cwd, задающий каталог, в котором следует выполнить команду из массива args. Ждёт завершения исполняемой команды (вызванной программы) и возвращает код завершения.
Теперь напишем нашу, пока единственную, функцию управления HTTrack'ом, позволяющую скачивать сайт в нужную нам директорию:
Изменим server.py:
Тут, я думаю, всё понятно. Код немного усложнён функциями, которые можно объединить и тем самым упростить код, но они помогут нам в последующих изменениях кода.
На данный момент можем запустить сначала server.py, а затем client.py. В клиентском приложении вводим следующую команду:
Примерно через минуту, в зависимости от вашего интернет-соединения, серверное приложение выведет "Downloading is complete" и у вас в домашнем каталоге появиться папка Sites, а в ней каталог verysimplesites.co.uk, в котором уже лежит скаченный сайт, который можно открыть в браузере без интернет-соединения.
Но нам этого мало, мы ведь хотим, чтобы сайт можно было получить в сжатом виде, в архиве. Пусть теперь у команды dl будет три аргумента, а не один. Первый остаётся таким же, это сайт, который необходимо скачать. Второй — флаг, показывающий, удалять ли директорию по-завершении скачивания. Третий — формат архива, в который будет упакован сайт после скачивания (до удаления, если оно требуется).
Функция проверки статуса процесса httrack в server.py:
Команда dl в server.py:
httrack.py:
Появилось много нового кода, но ничего сложного в нём нет, просто появилось несколько новых условий. Из новых функций появилась только rmtree, которая удаляет переданную ей директорию, включая всё, что находилось в последней.
Можно добавить в функцию handle_commands простую команду list без параметров:
Подключив в начале необходимую библиотеку:
Ещё неплохо было бы увеличить максимальный размер принимаемых клиентом данных от сервера в client.py:
Теперь перезапустим server.py и запустим client.py. Для начала прикажем скачать серверу какой-нибудь сайт и упаковать его в tar.gz архив, после чего удалить:
После этого скачаем другой сайт, но упаковывать его не будем, удалять, разумеется, тоже:
И спустя примерно минуту проверим список сайтов:
Если вы вводили те же команды, то должны получить следующий ответ от сервера:
На сегодня это, пожалую, всё. Это, конечно, далеко всё, что можно и нужно реализовать, но, тем не менее, позволяет понять общий принцип работы подобных приложений. Если вам интересная данная тема, то пишите об этом в комментариях, если такие найдутся, то я постараюсь выделить время и написать статью о чуть большем функционале данного приложения, в том числе и просмотр статуса скачивания сайта, а так же покажу несколько способов защиты от проникновения на сервер посторонних, в том числе и защиту от проникновения в оболочку.
UPD: Часть 2. Протокол передачи данных.
Не так давно качество мобильного интернета в моём городе стало постепенно ухудшаться из-за возрастающей на сети операторов нагрузки и некоторые сайты, требующие большое количество соединений (зависимые файлы страницы) стали загружаться ну ОЧЕНЬ медленно. По вечерам скорость опускается на столько, что некоторые сайты могут полностью загружаться в течении нескольких десятков секунд.
Есть несколько способов решения данной проблемы, но я решил выбрать немного необычный для нашего времени способ. Я решил скачивать сайты. Конечно, данных способ не подходит для крупных сайтов, вроде Хабра, тут разумнее использовать парсер, но можно скачать и отдельный хаб, список пользователей, или только свои публикации с помощью HTTrack Website Copier, применив фильтры. Например, чтобы скачать хаб Python с Хабра нужно применить фильтр "+habrahabr.ru/hub/python/*".
Этот способ можно использовать ещё в нескольких целях. Например, чтобы скачать сайт, или его часть, перед тем, как вы окажитесь без интернет-соединения, например, в самолёте. Или для того, чтобы скачать заблокированные на территории РФ сайты, если скачивать их через Tor, что будет очень медленно, или через компьютер в другой стране, где данных сайт не запрещён, а потом передать его на компьютер, находящийся в РФ, что будет гораздо быстрее для многостраничных сайтов. Таким образом мы может скачать, например,
Ну что, начнём!? Приложение будет постепенно усложнятся и в него будет добавляться всё новых функционал, это позволит понять что вообще здесь происходит и как это всё работает. Код я буду сопровождать большим достаточным количеством комментариев, чтобы его мог понять даже человек, не знающий Python, но повторно комментировать уже описанные куски кода и функции не буду, дабы не загромождать код. И сервер и клиент пишутся и проверяются под Linux, но, теоретически, должны работать и под другими платформами, если установлены все необходимые приложения, а именно: httrack и tar, а так же выставлен необходимый путь в конфигурационном файле, который мы создадим ниже. Если у вас появятся проблемы с запускам под вашей платформой, пишите в комментариях
Для начала реализуем простой сервер который будет пересылать строку клиенту.
# FILE: server.py
import socket
# Создаём IPv4 сокет потокового типа (TCP/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
На сегодня это, пожалую, всё. Это, конечно, далеко всё, что можно и нужно реализовать, но, тем не менее, позволяет понять общий принцип работы подобных приложений. Если вам интересная данная тема, то пишите об этом в комментариях, если такие найдутся, то я постараюсь выделить время и написать статью о чуть большем функционале данного приложения, в том числе и просмотр статуса скачивания сайта, а так же покажу несколько способов защиты от проникновения на сервер посторонних, в том числе и защиту от проникновения в оболочку.
UPD: Часть 2. Протокол передачи данных.
Комментарии (12)
ivlis
18.10.2015 03:07А почему не использовали библиотеку?
docs.python-requests.org/en/latestSemperPeritus
18.10.2015 08:42+1Во-первых, это не стандартная библиотека, а меня интересовали именно встроенные в Python средства для сетевого программирования. Во-вторых, что самое главное, предназначение у этой библиотеки другое, что понятно даже по названию.
Feature SupportRequests is ready for today’s web.
International Domains and URLs
Keep-Alive & Connection Pooling
Sessions with Cookie Persistence
Browser-style SSL Verification
Basic/Digest Authentication
Elegant Key/Value Cookies
Automatic Decompression
Unicode Response Bodies
Multipart File Uploads
Connection Timeouts
.netrc support
Python 2.6—3.4
Thread-safe.danSamara
18.10.2015 12:43Специально для тех, кто придёт из гугла по запросу «скачать сайт» напоминаю про отличный многопоточный инструмент скачивания страниц/сайтов: github.com/binux/pyspider
Он не из простых, но за час можно вполне освоится со всеми фишками и успешно скачать сайт.
gorodianskyi
18.10.2015 13:17+1Это, конечно, далеко всё, что можно и нужно реализовать, но, тем не менее
Думаю вы имели ввиду «далеко не все» :) Исправте! А так, статья самое то для познания python, хорошо разжевана!
stavinsky
Скорее принимаем первые 1024 байта. Это работа с сырыми сокетами и команда, как я понимаю является прямым биндингом к recv в C.
Думаю в python есть более высокоуровневые решения.
Поправьте, если ошибаюсь.
stavinsky
А вообще жду на github. Так и читать код приятнее будет и может кто-то что-то захочет добавить.
SemperPeritus
Завтра собираюсь отрефакторить и залить, и, возможно, подготовить немного кода для следующей статьи, преимущественно про защиту.
SemperPeritus
Как и обещал, написал продолжение статьи и выложил код на GitGub.
SemperPeritus
Да, всё верно. Данная команда является просто связкой с системными сокетами и читает первые 1024 (или меньше) байт от ещё не прочитанных. Правильнее было бы читать их небольшими кусками в цикле, но, так как данные небольшие, можно просто прочитать первые 1024 байт. Сомневаюсь, что кому-то потребуется скачать сайт, у которого адрес больше тысячи символов длиной.
stavinsky
Бывают ссылки с трекингом googe analytics(utm=....). Они бывает и в 2к символов не укладываются. Плюс еще utf… Ну в общем вы поняли к чему я.
SemperPeritus
В следующей статье добавлю несколько вспомогательных функций для сокетов. В общем, следующая статья будет больше не на добавление функционала, а на исправление недостатков и дыр приложения.
Nirvano
Фокусы начнутся при плохом интернет соединении и нетерпеливом пользователе, который быстро пошлет 2 команды, а у вас на сервере они соединятся в 1, так как TCP это поток данных, он не разделяет сообщения.
Либо на оборот получение одной команды разобьется на «2 разных сообщения».