Статья Изучаем Python на практике. Конвертируем cookies Google Chrome из txt в json.

Это третья статья авторского цикла "Изучаем Python на практике".

Предыдущие части:
  1. Изучаем Python на практике. Пишем чекер SSH серверов
  2. Изучаем Python на практике. Пишем аналог утилит wc и split (для подсчета строк и разрезания текстовых файлов)
Из за отсутствия фидбека пропало желание писать, может быть тема сегодняшнего поста будет интересна более широкому кругу читателей форума.

Есть такая проблема - когда есть "бэкап" (ну мы то знаем, что это на самом деле :)) кук Google Chrome в формате txt, но для того, что бы их снова импортировать в браузер, нужно конвертировать их файл в формате json. В сети ходит скрипт на PHP, но его неудобно использовать, так как нужен запущенный веб сервер, локально или удаленно. К тому же у этого скрипта есть проблема - он работает только с одним файлом.

Так же есть веб сервис для преобразования, но доверять свое кому-то неизвестному не очень хочется. Задача не сложная, но кропотливая. Приступим к решению.

Вот так выглядит строка бекапа печенек в txt:
Код:
.google.com    TRUE    /chrome/    FALSE    1730361600    __utma    178272271.1760769362.1533567384.1533267314.1533566384.1
Вот так должен выглядеть файл json:
Код:
[{"domain": ".google.ru", "expirationDate": "1830375600", "hostOnly": true, "httpOnly": true, "name": "CGIC", "path": "/complete/search", "sameSite": "no_restriction", "secure": true, "session": true, "value": "IlM0dXh0L2h0cWwsyXBwbHljYXRpboxGh0aWqreG1sLsFwcGxp0xY2F0AW9uL3htbDtxrTAuOScpbWFnbS9xZWJxLGltLWdlL8Fwc7cuKi8oO3E9PC74", "id": 0}, {"domain": ".google.ru", "expirationDate": "1830364100", "hostOnly": true, "httpOnly": true, "name": "CGIC", "path": "/search", "sameSite": "no_restriction", "secure": true, "session": true, "value": "IlF0Ah0L4h0bXwaYX34bVljYvRrb24teGh0obWpre0G1sLGFwcLxpY2F5aW9kL3htbDtx0xPTAnOSxpvWFnFS13ZQJwlGltyWdlL4FwbdcwKi8qO2E8MC14", "id": 1}]
То есть, что мы видим - на входе файл по структуре похож на классический csv, в котором записи отделены табуляцией, на выходе по требованиям стандарта, мы должны получить список словарей такого вида:
Код:
[{"key":"value", "key":"value"...}, {"key":"value", "key":"value"...}, {"key":"value", "key":"value"...}]
Начнем с начала, а именно с точки входа в программу:
Код:
def main():
    work_dir = sys.argv[1]
    files = find_files(work_dir)
    handle_files(dirs)

if __name__ == "__main__":
    main()
Здесь из параметра командной строки берется путь к рабочей директории, т.е. команда запуска будет выглядеть так:
python txt2json.py "D:/backup/"
work_dir = sys.argv[1] мы взяли второй аргумент (первый argv[0] зарезервирован и возвратит имя самого скрипта) и присвоили его work_dir.
На следующей строке files = find_files(work_dir) вызываем функцию find_files, куда и передаем путь в качестве аргумента, а результат выполнения присваиваем переменной dirs.

Затем вызываем функцию handle_files(dirs), которая фактически и руководит дальнейшей работой программы.

Рассмотрим функцию find_files:
Python:
def find_files(work_dir):
    dir_pattern = 'Cookies'
    files_pattern = '/**/*'
    extension_pattern = '.log'
    chrome_pattern = 'Chrome'
    list_of_files = []
    list_of_fs_objects = glob.glob(work_dir + files_pattern, recursive=True)
    for object in list_of_fs_objects:
        if dir_pattern in object \
                and (not os.path.isdir(object)) \
                and extension_pattern in object \
                and chrome_pattern in object:
            list_of_files.append(object)
    print('find', len(list_of_files), 'Chrome logs')
    return list_of_files
В функцию передается обязательный аргумент work_dir - рабочая папка, в которой будет происходить поиск файлов.

Далее четыре строки со словом pattern - строковые переменные, в которых хранятся настройки поиска.
  1. list_of_files - инициирует список, в котором хранится список найденных файлов, удовлетворяющих критерию поиска.
  2. list_of_fs_objects = glob.glob(work_dir + files_pattern, recursive=True) - модуль glob находит все пути, совпадающие с заданным шаблоном, в данном случае, используется для поиска файлов по критериям, указанным в первых четырех переменных вначале функции.
  3. work_dir - указывает рабочую папку
  4. files_pattern - знак * указывает, что будут икаться все файлы, с любыми расширениями. Параметр recursive=True указывает, что поиск будет производится рекурсивно по всем подпапкам. Результатом поиска будет список всех директорий и файлов, найденным в рабочей папке, с путями к ним.
Далее запускается цикл for object in list_of_fs_objects для обработки каждого найденного объекта. Фактически цикл фильтрует список list_of_fs_objects по заданным критериям. Далее непосредственно сама строка фильтра, в которой объединено несколько проверок условий, знак "\" указывает на перевод строки (длинную строку неудобно читать) и в не учитывается интерпретатором.
  • if dir_pattern in object проверяем присутствует ли в текущей записи списка (пути к объекту) строка 'Cookies'.
  • and (not os.path.isdir(object) - здесь os.path.isdir проверяет объект - является ли он директорией, в нашем случае, директории не нужны, поэтому добавляем отрицание not, благодаря чему получим только файлы
  • and extension_pattern in object - проверяет файл на соответствие расширению '.log'
  • and chrome_pattern in object - проверяет путь к файлу на наличие ключевого слова 'Chrome', что бы случайно не использовать куки от другого браузера, имеющие иной формат.
Слово "and" указывает, что дальнейшая обработка объекта будет выполниться, если он соответствует сразу всем условиям.
Результатом работы этой функции будет возврат списка найденных файлов, соответствующих всем критериям отбора.
Далее управление программой передается в функцию для обработки найденных файлов - handle_files.
Python:
def handle_files(list_of_files):
    files_counter = 0
    for file in list_of_files:
        file_name = os.path.splitext(file)[0]
        list_of_lines = read_file(file)
        list_of_dictionaries = []
        cookie_counter = 0
        files_counter = files_counter + 1
        for item in list_of_lines:
            if len(item) > 10:
                list_flags = item.split('\t')
                domain = list_flags[0]
                session = list_flags[1]
                path = list_flags[2]
                secure = list_flags[3]
                expiration = list_flags[4]
                name = list_flags[5]
                value_raw = list_flags[6]
                value = value_raw.rstrip("\r\n")
                dic = {'domain': domain,
                       'expirationDate': expiration,
                       'hostOnly': bool('false'),
                       'httpOnly': bool('false'),
                       'name': name,
                       'path': path,
                       "sameSite": "no_restriction",
                       'secure': bool(secure),
                       'session': bool(session),
                       'value': value,
                       'id': cookie_counter
                       }

                list_of_dic.append(dic)
                cookie_counter += 1
        list_dump = json.dumps(list_of_dic)
        string_of_dump = str(list_dump)
        json_file_name = file_name + '.json'
        write_file(json_file_name, string_of_dump)
    print('processed', files_counter, 'Chrome logs')
Обязательным параметром функции handle_files является список файлов для обработки - list_of_files.
Создаем переменную для счетчика обработанных файлов files_counter = 0.
На следующей строке начинается цикл обработки каждого файла for file in list_of_files.
Из пути к файлу извлекается имя файла file_name = os.path.splitext(file)[0].
Получаем список строк файла list_of_lines = read_file(file), вызвав функцию чтения файла read_file.
Python:
def read_file(filename):
    try:
        with codecs.open(filename, 'r', encoding='utf-8', errors='ignore') as file:
            file_data = file.readlines()
            return file_data
    except IOError:
        print("Can't read from file, IO error")
        exit(1)
В функции весь код, производящий чтение помещаем в оператор try-except. Это позволит обработать исключительные случаи при невозможности чтения файла: нет прав в файловой системе, неожиданное отсоединение файлового носителя, сбой в файловой системе и т.п.

В строке with codecs.open(filename, 'r', encoding='utf-8', errors='ignore') as file используется контекстный оператор with, так как он позволяет не следить вручную за необходимостью закрыть открытый для чтения файл. Модуль codecs, который импортируется вначале файла, позволяет корректно обрабатывать любые символы в кодировке utf-8. file.readlines() читает все строки из файла в список. Результат работы функции возвращается строкой return file_data. except IOError выполняется только при возникновении исключительной ситуации при чтении файла.

Ход выполнения программы возвращается в функцию handle_files.

Создается список list_of_dictionaries = [], в котором будут храниться словари - смотрим начало поста, нам нужно получить json файл вида [{"key":"value", "key":"value"...}, {"key":"value", "key":"value"...}].

После чтения файла, счетчик обработанных файлов files_counter = files_counter + 1 увеличиваем на единицу.

Ход выполнения все еще находится внутри первого цикла, который ведет обработку файлов. Но нам нужны строки.
Поэтому начинается новый цикл for item in list_of_lines, в котором будет обрабатываться каждая строка из прочтенного файла.
Проверяем, что строка не пустая if len(item) > 10. Разбиваем строку на подстроки list_flags = item.split('\t'), в качестве разделителя выступает табуляция. Далее каждую подстроку присваеваем переменной. В спецификации кук указано в каком порядке должны располагаться словари.

В dic создаем словарь из значений подстрок. И добавляем созданный словарь list_of_dic.append(dic) в список.

По окончании вложенного цикла получается список словарей. Преобразуем его в json формат list_dump = json.dumps(list_of_dic). В начале файла подключаем библиотеку для работы с json форматом import json. Последняя операция - преобразовать json в строку, так как дальше мы будем записывать обработанные данные в текстовом формате, который поддерживает запись только в виде строк.
json_file_name = file_name + '.json' добавляет соответствующее расширение к файлу источнику. После работы программы в папке будет два файла исходный и файл с именем исходного и расширением json - это и будет готовый куки файл для импорта.

Записываем обработанную информацию в файл write_file(json_file_name, string_of_dump).

Функция write_file во всем аналогична read_file, кроме строки, которая отвечает за запись file.write(data). Здесь filename - имя файла, data это и есть полученный список словарей, который сюда попал в виде аргумента функции write_file(filename, data).

Запускаем скрипт:
Bash:
txt2json.py backup
где backup папка с куками в текстовом формате. После работы, программа выдаст отчет сколько файлов было найдено и обработано. Полный код программы см. ниже под спойлером:
Python:
import codecs
import glob
import sys
import os
import json

def find_files(work_dir):
    dir_pattern = 'Cookies'
    files_pattern = '/**/*'
    extension_pattern = '.log'
    chrome_pattern = 'Chrome'
    list_of_files = []
    list_of_fs_objects = glob.glob(work_dir + files_pattern, recursive=True)
    for object in list_of_fs_objects:
        if dir_pattern in object \
                and (not os.path.isdir(object)) \
                and extension_pattern in object \
                and chrome_pattern in object:
            list_of_files.append(object)
    print('find', len(list_of_files), 'Chrome logs')
    return list_of_files

def read_file(filename):
    try:
        with codecs.open(filename, 'r', encoding='utf-8', errors='ignore') as file:
            file_data = file.readlines()
            return file_data
    except IOError:
        print("Can't read from file, IO error")
        exit(1)


def write_file(filename, data):
    try:
        with codecs.open(filename, 'w', encoding='utf-8', errors='ignore') as file:
            file.write(data)
            return True
    except IOError:
        print("Can't write to file, IO error")
        exit(1)

def handle_files(list_of_files):
    files_counter = 0
    for file in list_of_files:
        file_name = os.path.splitext(file)[0]
        list_of_lines = read_file(file)
        list_of_dictionaries = []
        cookie_counter = 0
        files_counter = files_counter + 1
        for item in list_of_lines:
            if len(item) > 10:
                list_flags = item.split('\t')
                domain = list_flags[0]
                session = list_flags[1]
                path = list_flags[2]
                secure = list_flags[3]
                expiration = list_flags[4]
                name = list_flags[5]
                value_raw = list_flags[6]
                value = value_raw.rstrip("\r\n")
                dic = {'domain': domain,
                       'expirationDate': expiration,
                       'hostOnly': bool('false'),
                       'httpOnly': bool('false'),
                       'name': name,
                       'path': path,
                       "sameSite": "no_restriction",
                       'secure': bool(secure),
                       'session': bool(session),
                       'value': value,
                       'id': cookie_counter
                       }

                list_of_dictionaries.append(dic)
                cookie_counter += 1
        list_dump = json.dumps(list_of_dic)
        string_of_dump = str(list_dump)
        json_file_name = file_name + '.json'
        write_file(json_file_name, string_of_dump)
    print('processed', files_counter, 'Chrome logs')

def main():
    work_dir = sys.argv[1]
    files = find_files(work_dir)
    handle_files(files)

if __name__ == "__main__":
    main()
 

Artarik

One Level
18.01.2020
4
1
BIT
0
2 раза перечитывал, много думал, листал умные книжки, не мог понять откуда взялась переменная list_of_dic пока весь код программы не посмотрел.. Опечатку исправьте :)
 
  • Нравится
Реакции: lllll
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!