GUI Генератор паролей на Python

Kate

Administrator
Команда форума
Штош. В этой статье я научу вас делать кроссплатформенное приложение генератор паролей с графическим интерфейсом. Мы будем использовать язык Python и библиотеку PySide6 - привязку к инструментарию фреймворка Qt.

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

В статье я постарался затронуть все моменты создания и сборки приложения. Ознакомиться с проектом можно на GitHub.

Устанавливаем все необходимое​

Скачайте и установите Python последней версии (желательно). Приложение должно работать с Python версии 3.7+

Создадим директорию проекта password-generator и виртуальное окружение, в моем случае venvPasswordGenerator

Установим библиотеку PySide6.

pip install pyside6

Документация Qt for Python.

Создаем интерфейс​

Переходим к созданию интерфейса. Для этого нам понадобится программа Qt Designer. Её можно найти в папке установленной ранее библиотеки.

venv*/Lib/site-packages/PySide6/designer

Окно новой формы в программе Qt Designer
Окно новой формы в программе Qt Designer
Создаем MainWindow. Убираем menubar и statusbar

68361e4b9a3e76f7c380aaf6da0b63d1.png

Сразу сохраняем файл интерфейса. Я называю его main.ui, потому что в будущем я могу захотеть сделать отдельную форму для настроек или еще чего-то.

Сначала закинем 4 Horizontal Layout для компоновки элементов. Скопировать элемент можно перетаскиванием с зажатой клавишей Ctrl.

627527471e8d982cfcb639f4b2c18494.png

Выберем вертикальное расположение для центрального виджета. centralwidget -> Lay Out Vertically

3204e6758fcc816c82a1b0acdfe79c6b.png

Компоновка выбора символов​

Закинем одну кнопку выбора символов в нижнюю горизонтальную компоновку.

8bd6e0e68411ef58e8778148b563f916.png

Чтобы кнопка имела 2 состояния, нужно поставить свойство checkable

3d0e0413256f492420efbc198f309cfb.png

Скопируем кнопку 3 раза. Поставим текст. Я думаю, так должно быть понятно, за какой набор символов отвечает каждая отдельная кнопка.

1e182006d9f3e757064f77d86a4ac326.png

Дадим элементам осмысленные имена.

752434386c30562b76469204154e9ec7.png

Проставим свойство checked на всех символах, кроме специальных.

c9b359adc13eef5633983c45f7c3ec43.png

Компоновка длины пароля​

Здесь будет указываться длина пароля с помощью слайдера и счетчика. Компоновку я назову layout_length, слайдер - slider_length, счетчик - spinbox_length

43446c9c1760c301f422aec9164f6dfc.png

Компоновка энтропии и сложности пароля​

Закидываем лейбл.

c8506cf84a7adcba2499be2ce4b27b1e.png

Ставим горизонтальное выравнивание по центру.

8fe1a53757b54a1cfa4f7ddeaeedebcc.png

Копируем элемент и вводим примерный текст.

9e8e696f126d807177c2a27fbefd8008.png

Компоновка пароля​

Перед нами встает интересная задача. Как поместить кнопку видимости пароля в элемент Line Edit? К сожалению, встроенных в Qt Designer методов для такого действия я не нашел. Лучшим решением я посчитал поместить поле и кнопку рядом во фрейме. Если вы знаете способ лучше, поделитесь в комментариях.

Widget Box -> Containers -> Frame

7ac8b9256753efa15489f1f8ef84c1ea.png

Поставим горизонтальное выравнивание для фрейма.

b42da920eb95c2b820e6dd91040b66cf.png

Добавим 2 кнопки в горизонтальную компоновку, а не во фрейм. Перетаскивать элемент нужно в самый правый край компоновки.

65360f11adc606a51bcd80166a826d5d.png

Еще я захотел поставить сверху изображение замка. На удивление, в Qt Designer это тоже реализовано через одно место, в котором никто не хочет побывать. Есть способ с помощью лейбла и его свойства pixmap, но в таком случае нельзя изменять размер изображения. Поэтому я решил сделать через иконку заблокированной кнопки.

91653ea179bca7101f00c535e9a71fa3.png

Иконки Material Icons​

Возьмем иконки для приложения. Я буду использовать бесплатные Material Icons.

Скачаем белую векторную иконку замка. Я выберу первый вид - Outlined. Вы можете взять другой, как вам приятнее.

c95c8a77402811b15c6f73faf34858dd.png

Для повторной генерации возьмем Refresh. Копирование в буфер обмена - Content Copy. Видимость пароля - Visibility и Visibility Off. Для иконки приложения возьмем черную иконку ключа в формате png и сконвертируем её в формат для иконок ico

Иконки в Windows File Explorer
Иконки в Windows File Explorer

Создаем файл ресурсов​

Создадим файл ресурсов, который будет хранить иконки. Позже мы сконвертируем его в Python код. Resource Browser -> Edit Resources -> New Resource File -> resources.qrc

Добавляем префикс icons

4612b3919e3a36410efea51da058c3f4.png

Добавляем все файлы иконок.

80853eb97f8aeb7ed1f8e3d00741ea1b.png
e9d75871a004f0d787072e51c4a1a1ba.png

Проставляем иконки​

В заблокированную кнопку ставим иконку замка с помощью свойства icon -> Disabled On. Выбираем иконку с помощью Choose Resource

5b5801445eeb2df4866f0a421bf55b43.png

Для кнопки видимости выбираем свойство checkable, так как она тоже имеет 2 состояния. Сразу проставим checked по умолчанию.

a65d99778aac4a1c69c5dab26ca4c0c1.png

В Normal Off берем иконку невидимости, в Normal On - видимости.

9aecf6b984b547b46203956b2e3ff3d9.png

Остальные иконки проставляем в Normal On

78c2a714a8b670bddfae1c2f8744168d.png

Стилизуем интерфейс​

Для стилизации приложения я буду использовать урезанный язык CSS. Писать советую в каком-нибудь редакторе. Я пишу в Visual Studio Code, конечно же.

Главное окно​

Создадим файл QMainWindow.css. Для главного виджета поставим цвет фона #121212, белый цвет текста, шрифт Verdana с размером 16 поинтов и внешний отступ 10 пикселей.

QWidget {
background-color: #121212;
color: white;
font-family: Verdana;
font-size: 16pt;
margin: 10px;
}
Для кнопок ставим сплошную серую границу 2 пикселя с радиусом границы 5 пикселей.

QPushButton {
border: 2px solid gray;
border-radius: 5px;
}
Вставим код в элемент MainWindow с помощью опции Change styleSheet

26e1cee67d9c71b8cf2da6b8d46b5d6b.png

Отдельно для кнопок символов я поставлю внутренний отступ 10 пикселей, текст располагается слишком близко к границам.

QPushButton#btn_lower,
#btn_upper,
#btn_digits,
#btn_special {
padding: 10px;
}
При наведении на кнопку цвет границ будет меняться на зеленый #090.

QPushButton:hover {
border-color: #090;
}
При нажатии граница будет увеличиваться до 4 пикселей.

QPushButton:pressed {
border: 4px solid #090;
border-radius: 5px;
}
Для отмеченного состояния кнопки поставим на фон темно-зеленый цвет #006300.

QPushButton:checked {
background-color: #006300;
border-color: #090;
}
QMainWindow.css

Чтобы посмотреть превью интерфейса, нужно нажать сочетание клавиш Ctrl + R
d0c649138d9cba22e4530b7fb07a7247.png

Иконка замка​

Уберем границы с помощью стиля border: none;
Поставим размер 100 на 100 пикселей.
83eada14943ee2e1803c67aafc2c98ae.png

Фрейм пароля​

Поставим для фрейма пароля такую же границу, как и для кнопок. Уберем отступ до правого элемента.
QFrame {
border: 2px solid gray;
border-radius: 5px;
margin-right: 0;
}

QFrame:hover {
border-color: #090;
}
Поставим вертикальную политику Maximum
faac8aac6ad802d171a32493f5e2545e.png

Поле пароля​

Для поля пароля уберем границы и внешние отступы. Поставим размер шрифта 20 поинтов.
QLineEdit {
border: none;
margin: 0;
font-size: 20pt;
}

Кнопка видимости​

Для кнопки видимости пароля так же уберем границы и внешние отступы. Еще поставим прозрачный фон и размер 30 на 30 пикселей.
QPushButton {
border: none;
margin: 0;
background-color: transparent;
}
51a69e33cfb7a66fdb10c868836d1e71.png

Кнопка генерации пароля​

Для кнопки генерации пароля поставим размер иконки 52 на 52 пикселя. Так границы кнопки будут идти ровно по границам фрейма. Уберем правый и левый внешний отступ.
QPushButton {
margin-right: 0;
margin-left: 0;
}
e8d508e9080838951160254dd261b8b1.png

Кнопка копирования пароля в буфер обмена​

Почему-то иконка копирования очень плотно прилегает к границам. Я нашел размер 42 на 42 пикселя и внутренний отступ 5 пикселей. Уберем отступ до левого элемента.
QPushButton {
padding: 5px;
margin-left: 0;
}
e17837c8ec7163755b469cdd5a9d0c63.png

Слайдер​

Для псевдоэлемента groove уберем цвет и поставим высоту 5 пикселей. Грув - это линия слайдера, или "желобок", "канавка", "борозда", если верить гугл переводчику.
QSlider::groove:horizontal {
background-color: transparent;
height: 5px;
}
Слева от ручки слайдера будет зеленый цвет. Для этого используется селектор псевдоэлемента sub-page
QSlider::sub-page:horizontal {
background-color: #090;
}
Справа от ручки будет серый цвет. Псевдоэлемент add-page
QSlider::add-page:horizontal {
background-color: gray;
}
Ручку слайдера я сделаю белой с шириной 22 пикселя, радиусом 10 пикселей и внешними отступами снизу и сверху по -8 пикселей.
QSlider::handle:horizontal {
background-color: white;
width: 22px;
border-radius: 10px;
margin-top: -8px;
margin-bottom: -8px;
}
Поставим максимум 100 пикселей и значение по умолчанию 12.
eebaed8776ba14421ce537ba7ff7d235.png

Счетчик​

Счетчик похож на кнопки.
QSpinBox {
border: 2px solid gray;
border-radius: 5px;
background: transparent;
padding: 2px;
}

QSpinBox:hover {
border-color: #009900;
}
Поставим значения, как у слайдера. Сделаем горизонтальное выравнивание по центру и уберем эти отвратительные стрелочки.
024aa4ea45bf227c8143c2d58d792c67.png

Лейблы информации​

Если растягивать окно интерфейса по вертикали, большую часть будут занимать лейблы информации. Мы этого не хотим, и поэтому ставим у двух лейблов вертикальную политику Maximum
4d7747714d5b165cfb895202ec177347.png

Делаем последние штрихи​

Поставим размер приложения по умолчанию. Мне нравится 542 на 418 пикселей. Можете сделать круглые числа, как вам угодно.
2359e871764cd124c219c53035f5b5e4.png

Напишем название приложения в свойстве windowTitle. Поставим иконку в windowIcon
4fca7f6dea064a549f598af15973a224.png

Поставим курсор Pointing Hand для кнопок и слайдера.
3583e55ecee7304713ee4182f599c983.png

Уберем демонстрационный текст из лейблов. Он будет генерироваться программой.
Штош. Интерфейс готов, переходим к следующему этапу.
34bfa6debc8f04bd3a71bc11e45e3690.png

Конвертируем файл ресурсов и интерфейса​

Для того, чтобы сконвертировать файл ресурсов, нужно написать в терминал pyside6-rcc, название файла ресурсов, флаг -o и название файла на выходе.
pyside6-rcc resources.qrc -o resources.py
Файл интерфейса конвертируется таким же образом, только с помощью приложения pyside6-uic
pyside6-uic main.ui -o ui_main.py
Этот файл хочет, чтобы я добавлял в конце _rc к файлу ресурсов, а я не хочу. Поменяем import resources_rc на import resources

Пишем код​

Рекомендую писать код в среде разработки PyCharm или Visual Studio Code. Vim-еры, не бейте.
Для начала создадим модуль приложения - app.py. Я вставлю готовый сниппет для запуска приложения с файлом дизайна.
Сниппет

Поменяю название класса App на PasswordGenerator

Модуль кнопок​

Давайте сделаем отдельный модуль с привязкой сущностей к кнопкам - buttons.py. В стандартной библиотеке string есть строковые переменные для пула символов. Возьмем ascii_lowercase, ascii_uppercase, digits и punctuation.
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
Вот так они выглядят.
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits = '0123456789'
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
Давайте сделаем перечисление, которое привязывает кнопки выбора символов к этим строковым переменным.
from enum import Enum

class Characters(Enum):
btn_upper = ascii_uppercase
btn_lower = ascii_lowercase
btn_digits = digits
btn_special = punctuation
Сразу создадим словарь с количеством символов на каждый пул. Нам это пригодится для расчета энтропии.
CHARACTER_NUMBER = {
'btn_lower': len(Characters.btn_lower.value),
'btn_upper': len(Characters.btn_upper.value),
'btn_digits': len(Characters.btn_digits.value),
'btn_special': len(Characters.btn_special.value)
}

Модуль генерации пароля​

Давайте сделаем модуль генерации паролей без привязки к интерфейсу - password.py. Напишем функцию создания нового пароля. Аргументами будут длина пароля и все используемые символы в виде строки.
Для генерации безопасного пароля мы будем использовать библиотеку secrets. Сделаем генератор из случайно выбранных символов и соединим их в строку с помощью метода join
import secrets

def create_new(length: int, characters: str) -> str:
return "".join(secrets.choice(characters) for _ in range(length))

Получаем энтропию пароля​

Теперь сделаем функцию для получения энтропии. В нее нужно будет передавать длину пароля и количество символов. Формула энтропии пароля выглядит следующим образом:
H=log_2N^L=L \times log_2N

где N — это количество возможных символов, а L — количество символов в пароле. H измеряется в битах.
Давайте импортируем логарифм по основанию 2 из библиотеки math.
from math import log2
Возвращаем значение, округленное до 2 знаков после запятой.
def get_entropy(length: int, character_number: int) -> float:
entropy = length * log2(character_number)
return round(entropy, 2)

Перечисление для сложности пароля​

Создадим целочисленное перечисление сложности пароля к его энтропии по нижней границе. Я не нашел единого стандарта, в разных генераторах сила пароля считается по-разному, поэтому сделаю так: ничтожный пароль от 0 до 30 бит, слабый после 30, хороший после 50, сильный после 70 и замечательный после 120.
from enum import IntEnum

class StrengthToEntropy(IntEnum):
Pathetic = 0
Weak = 30
Good = 50
Strong = 70
Excellent = 120

Связываем значения слайдера и счетчика​

Давайте сделаем метод класса приложения, который будет связывать значения слайдера и счетчика. Для этого соединим сигнал изменения значения valueChanged с установкой значения другого элемента.
def connect_slider_to_spinbox(self) -> None:
self.ui.slider_length.valueChanged.connect(self.ui.spinbox_length.setValue)
self.ui.spinbox_length.valueChanged.connect(self.ui.slider_length.setValue)
Не забудьте прописать метод в конструкторе класса.
class PasswordGenerator(QMainWindow):
def __init__(self):
...
self.connect_slider_to_spinbox()

Получаем символы для пароля​

Создадим новый метод для получения символов отмеченных кнопок. Объявим пустую строку chars. Для каждой кнопки в перечислении добавляем её символы в строку, если она отмечена.
def get_characters(self) -> str:
chars = ""

for btn in buttons.Characters:
if getattr(self.ui, btn.name).isChecked():
chars += btn.value

return chars

Ставим пароль​

Ну и наконец-то, метод установки пароля. Ставим в элемент line_password новый пароль с помощью метода setText. Длину берем из слайдера или счетчика, не имеет значения.
def set_password(self) -> None:
self.ui.line_password.setText(
password.create_new(
length=self.ui.slider_length.value(),
characters=self.get_characters())
)
Добавим метод в конструктор. Приложение должно сгенерировать пароль при запуске.
Соединим изменение слайдера или счетчика с установкой пароля.
def connect_slider_to_spinbox(self) -> None:
...
self.ui.spinbox_length.valueChanged.connect(self.set_password)
Позалипаем на быстрое изменение паролей :3
78467b7086648b036562b251d4c74dbf.gif

Когда не нажата ни одна кнопка символов, происходит IndexError. Обработаем этот случай, просто очищая поле пароля.
def set_password(self) -> None:
try:
self.ui.line_password.setText(
password.create_new(
length=self.ui.slider_length.value(),
characters=self.get_characters())
)
except IndexError:
self.ui.line_password.clear()

Получаем количество символов​

Принцип тот же, что и у получения строки символов, только в этот раз мы работаем со словарем, а не с перечислением.
def get_character_number(self) -> int:
num = 0

for btn in buttons.CHARACTER_NUMBER.items():
if getattr(self.ui, btn[0]).isChecked():
num += btn[1]

return num

Ставим энтропию пароля​

Делаем метод для лейбла энтропии. Берем длину из текста пароля, а не из слайдера или счетчика, так нужно. Ставим в лейбл выражение с помощью f-строки.
def set_entropy(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()

self.ui.label_entropy.setText(
f"Entropy: {password.get_entropy(length, char_num)} bit"
)
Добавим установку энтропии в конце метода установки пароля.
def set_password(self) -> None:
...

self.set_entropy()
Без отмеченных кнопок символов получается ValueError. Обработаем эту ошибку в функции get_entropy модуля password. В таком случае она будет возвращать 0.0.
def get_entropy(length: int, character_number: int) -> float:
try:
entropy = length * log2(character_number)
except ValueError:
return 0.0

return round(entropy, 2)

Ставим сообщение о сложности пароля​

Теперь сделаем метод для лейбла сложности пароля. Для каждой сложности из перечисления сравниваем энтропию со значением.
def set_strength(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()

for strength in password.StrengthToEntropy:
if password.get_entropy(length, char_num) >= strength.value:
self.ui.label_strength.setText(f"Strength: {strength.name}")
Добавим в конец метода установки пароля.
7c10716283af2a3f8d3c5c25db0b092c.gif

Ставим генерацию пароля на кнопки​

Запишу в модуль buttons кортеж с именами кнопок, при нажатии на которые будет генерироваться новый пароль.
GENERATE_PASSWORD = (
'btn_refresh', 'btn_lower', 'btn_upper', 'btn_digits', 'btn_special'
)
Теперь соединим кнопки с методом в конструкторе класса.
for btn in buttons.GENERATE_PASSWORD:
getattr(self.ui, btn).clicked.connect(self.set_password)

Изменяем видимость пароля​

Напишем метод для изменения видимости пароля. Если кнопка отмечена, ставим нормальный эхо мод, иначе ставим эхо мод пароля.
from PySide6.QtWidgets import QApplication, QMainWindow, QLineEdit

...

def change_password_visibility(self) -> None:
if self.ui.btn_visibility.isChecked():
self.ui.line_password.setEchoMode(QLineEdit.Normal)
else:
self.ui.line_password.setEchoMode(QLineEdit.Password)
Соединим нажатие кнопки с методом в конструкторе класса.
class PasswordGenerator(QMainWindow):
def __init__(self):
...
self.ui.btn_visibility.clicked.connect(self.change_password_visibility)
b5af54838b843d8f0acb7d6a393fce18.gif

Копируем в буфер обмена​

Чтобы скопировать текст в буфер обмена, нужно вызвать метод clipboard класса QApplication и поставить в него текст.
def copy_to_clipboard(self) -> None:
QApplication.clipboard().setText(self.ui.line_password.text())
Так же соединим нажатие кнопки с методом в конструкторе класса. Я думаю, вы справитесь.

Меняем лейблы при ручном изменении пароля​

Остался последний штрих. При ручном изменении пароля энтропия не меняется. Решить проблему можно с помощью сигнала редактирования текста textEdited
def do_when_password_edit(self) -> None:
self.ui.line_password.textEdited.connect(self.set_entropy)
self.ui.line_password.textEdited.connect(self.set_strength)
Фишка в том, что это будет не совсем правильная энтропия, потому что количество допустимых символов берется по прежнему из отмеченных кнопок. Предлагаю вам самим решить эту проблему.
buttons.py

password.py

Меняем структуру проекта​

Штош. Давайте поменяем структуру проекта. Поместим все файлы в папку password-generator. Здесь создадим папку ui, в которую поместим все файлы, связанные с интерфейсом. Поменяем импортирование модулей, чтобы все работало. Из ui_main.py я убрал импортирование ресурсов и добавил его в главный модуль приложения.
from ui.ui_main import Ui_MainWindow
import ui.resources
app.py

Создаем файл зависимостей​

Создадим файл зависимостей с помощью команды pip freeze > requirements.txt
PySide6==6.3.1
PySide6-Addons==6.3.1
PySide6-Essentials==6.3.1
shiboken6==6.3.1

Система контроля версий​

Инициализируем систему контроля версий, конечно же Git. Установите систему, если вы еще этого не сделали. Пропишите в терминал git init из корневого каталога или используйте удобный графический интерфейс в PyCharm.
Создадим файл .gitignore. Будем игнорировать любую папку виртуального окружения, папку PyCharm в моем случае и питонячий кэш. Для VS Code можно прописать .vscode
venv*
.idea
.vscode
__pycache__
Теперь можно сделать первый коммит. Опять же, в PyCharm это делается очень удобно, но если вы любите работать в терминале, то пропишите команды:
git add .
git commit -m "Initial commit"

Собираем приложение для Windows​

Переходим к созданию исполняемых файлов. У Qt есть понятная документация, в которой собраны разные способы дистрибуции приложения. Самый простой способ - это PyInstaller. Нет, вру, самый простой способ - это PyInstaller с интерфейсом, auto-py-to-exe. Все эти библиотеки хороши своей понятностью и легкостью, но обычно на выходе получаются небезопасные и тяжелые бинарники.
Я решил показать вам инструмент получше - компилятор Nuitka. Он доступен для всех версий Qt, работает на всех платформах и распространяется по свободной лицензии MIT.
Установим библиотеку с помощью команды pip install nuitka. Для сборки приложения в один файл понадобится библиотека zstandard, добавим ее в команду через пробел.
Пишем команду компиляции. Собираем приложение в один файл с помощью флага --onefile. Флаг --follow-imports нужен для соблюдения всех импортов. Добавляем плагин PySide6 с помощью флага --enable-plugin=pyside6. Чтобы убрать консольное окно в системе Windows добавим --windows-disable-console. Добавим иконку с помощью флага --windows-icon-from-ico
Чтобы убрать все генерируемые нуиткой папки сборки добавим флаг --remove-output
Можно указать название файла через флаг -o, но это вообще необязательно, всегда можно просто переименовать файл. В конце напишем путь к файлу скрипта, который мы собираемся собирать.
nuitka --onefile --follow-imports --enable-plugin=pyside6 --windows-disable-console --windows-icon-from-ico=ui\icons\app-icon.ico --remove-output -o password-generator.exe app.py
На выходе получилось приложение с размером 16.7 мегабайт.
9632f1cf1c66577fe030f4949ae4ec52.png

Собираем приложение для Linux​

Я использовал дистрибутив Ubuntu версии 22.04. С нуиткой вышло громадное приложение в 145 мегабайт. Если вы знаете, как сделать меньше - пишите в комментарии.
python -m nuitka --onefile --follow-imports --enable-plugin=pyside6 --remove-output app.py
Я попробовал собрать приложение с PyInstaller, получилось уже получше, 57,6 мегабайт. С иконкой в докбаре и проводнике вообще беда, она просто не хотела ставиться.
pyinstaller --name="Password Generator" --windowed app.py

Собираем приложение для macOS​

Я использовал версию 10.15 Catalina. Для сборки приложения под macOS нужно прописать специальный параметр --macos-create-app-bundle
python3 -m nuitka --onefile --follow-imports --enable-plugin=pyside6 --macos-create-app-bundle --remove-output -o password-generator.app app.py
Вышло почти так же компактно, как и на винде - 18 мегабайт. С иконкой тоже возникли проблемы, поэтому собирал без нее.

Штош​

Надеюсь, вам понравился такой комплексный туториал. Генерируйте сильные пароли, дегенерируйте слабые, и будет вам счастье. До встречи.


 
Сверху