Странички убежавших - теперь только web.archive.org в помощь
Странички убежавших - теперь только web.archive.org в помощь

В конце статьи Python скрипт для сохранения заметок.

Последняя шлюпка с «Титаника», чтобы утащить с ЖЖ свои и чужие мысли на жесткий диск, пока «эффективные менеджеры» окончательно не пустили всё на дно.

Я писал свои заметки в ЖЖ более 15 лет. Полтора десятилетия текстов, некоторые из которых даже влетали в топ главной страницы, теша мое самолюбие. Но всему есть предел.

Декабрьские конвульсии администрации — введение сегрегации пользователей, разделение на касты и монетизация каждого вздоха — стали последней каплей. Это больше не дом, это режимный объект с пропусками. Я принял решение об уходе, забирая с собой всё, что нажил непосильной графоманией.

Небольшой исторический экскурс по руинам

Для поколения, вскормленного стерильным фастфудом VK и Дзена, это прозвучит как бред сумасшедшего деда, но был момент, когда LiveJournal был не просто сайтом. Он был центральной нервной системой русскоязычного хаоса.

Это не был «еще один сервис». Это была цифровая агора, где каждый вечер сходились в рукопашной журналисты, красноглазые айтишники, депутаты и городские сумасшедшие. Место, где грузили сервера не DDoS-атаками, а интеллектуальным словом, параллельно изобретая то, что позже цинично назовут «гражданским обществом», а потом аккуратно задушат.

От тетрадки с замочком к культурному феномену

Формально всё начал американский программист Брэд Фицпатрик. 18 марта 1999 была создана первая запись, а домен LiveJournal.com зарегистрирован 15 апреля 1999

Но в России, в нашем ледяном царстве, точка отсчета иная. Традиционно первыми авторами, которые начали раскручивать русскоязычный сегмент LiveJournal, считаются:

Роман Лейбов r-l.livejournal.com - Считается первым, кто начал вести регулярный блог на русском языке, и целенаправленно привлекал в сообщество своих друзей.

Антон Носик dolboeb.livejournal.com Один из самых известных пионеров Рунета. Позже, как медиа-директор компании SUP, занимался развитием купленного русскоязычного сегмента ЖЖ. Именно Лейбов и Носик своими активными действиями сыграли ключевую роль в популяризации платформы как новой социальной среды в Рунете.

К середине нулевых стало ясно: мы создали монстра. В нашей резервации интернета ЖЖ мутировал в:

  • Главный сайт Рунета. По данным Яндекса 2006 года, 45% всех записей русскоязычной блогосферы генерировались здесь. Почти половина всего смысла в сети рождалась в сине-белых полях ввода.

  • Дефолтную блог-платформу. 950 тысяч аккаунтов в 2007-м. Десятки тысяч сообществ — прообразы нынешних пабликов, только без дебильных мемов и с реальными дискуссиями.

  • Монополиста внимания. 7.5 миллионов человек аудитории в 2009-м. Если тебя не было в ЖЖ — ты был цифровым призраком. Тебя просто не существовало.

Цифры, которые сегодня кажутся сказкой

Сейчас, когда любой инфоцыганин радуется накрученным 10к ботов в «телеге», статистика «золотого века» ЖЖ выглядит сюрреализмом:

  • 30 миллионов посетителей в мире к концу нулевых. Рунет давал львиную долю трафика — миллионы живых, думающих душ.

  • К 2010 году Россия держала почти половину всей аудитории сервиса. Мы оккупировали эту территорию.

  • Рейтинги Яндекса тех лет не врали: все медийные тяжеловесы жили здесь. Новости рождались в постах, а уже потом попадали в «зомбоящик».

Френдлента как прототип того, что мы потеряли

То, что сегодня убито алгоритмами, тогда было глотком свободы:

  • Френдлента. Никакой «умной ленты», решающей за тебя, что тебе жрать. Только ручная выборка. Ты сам строил свою информационную диету. Твоя лента была твоим личным медиа, где в одном строю шли министр, панк и домохозяйка.

  • Сообщества. Коллективный разум в чистом виде. От политсрачей до узкоспециальных технарей.

  • Это была одна большая городская площадь. В комментариях под твоим постом могли сцепиться олигарх и школьник из Твери, и еще неизвестно, кто уходил оплеванным. Там мы учились искусству сетевой войны задолго до того, как интернет превратился в стерильный концлагерь с банхаммерами.

Официальное признание — когда ЖЖ стал «частью системы»

Государство, как хищник, почуявший кровь, быстро поняло: здесь опасно, здесь думают.

  • 2007 год — SUP Fabrik получает «Премию Рунета». Начало конца.

  • 2009 год — ЖЖ получает премию в категории «Государство и общество». Фактически, нам вручили черную метку. Инфраструктура гражданского общества была замечена и взята на карандаш.

В ЖЖ писали все: от президентов до поп-звезд. Facebook был сборищем картинок, ВК — песочницей для студентов, Twitter — пейджером. Вся тяжелая артиллерия — расследования, аналитика, бунты — всё шло через ЖЖ.

Почему всё сломалось — анатомия убийства

К 2010г ощущение центра мира начало таять. Я был там, в первом ряду партера, и видел, как рушится наш Колизей.

  • Соцсети для масс.
    ВК и Одноклассники предложили то, чего жаждала толпа: простоту. Регистрация без инвайтов, музыка, котики, игры. Жвачка для мозгов победила смыслы.

  • Техническая импотенция.
    ЖЖ оставался динозавром эпохи десктопа. Пока мир пересаживался на смартфоны, владельцы ЖЖ чесали затылки. Мобильные приложения были кривыми, как руки их создателей.

  • DDoS и паралич.
    2011 год. Кто-то решил задушить ЖЖ. Серия атак клала площадку на лопатки. Вчера ты был трибуном, сегодня у тебя «Error 503». Аудитория сильно просела. Люди уходили туда, где не падает — в уютные загоны Цукерберга. https://www.vedomosti.ru/technology/articles/2011/10/12/poluzhivoj_zhurnal

  • Смена поколения - клиповое мышление.
    Новые варвары не умеют читать «многабукав». Им нужны картинки, тиктоки и сториз, исчезающие через 24 часа. Культура лонгридов умерла, задавленная дофаминовой иглой лайков.

  • Алчность и цензура.
    Продажа LiveJournal компании SUP стала фатальной ошибкой. «Эффективные менеджеры» принесли с собой худшее из корпоративного ада: конфликтные комиссии, теневые баны, выборочную модерацию и жажду наживы. Доверие старой гвардии было растоптано. Мы ждали развития, а получили рекламные баннеры на пол-экрана и цензуру.

К концу 2010-х ЖЖ превратился в дом престарелых. Теплое, ламповое гетто, которое больше не влияет ни на что.

Наследие — что мы оставили на пепелище

Я не скажу, что ЖЖ умер. Зомби тоже технически двигаются. Но дух ушел. Тем не менее, именно этот труп удобрил почву Рунета:

  • Культура блогерства. «Пруфы под кат», этика спора, работа с аудиторией — всё это родом оттуда.

  • Цифровое сопротивление. Идея, что пост в интернете может снять губернатора или изменить закон, родилась в ЖЖ. Сейчас это кажется наивным, но тогда это работало.

  • Интерфейсы. Лайки, репосты, треды — всё это обкатывалось на нас, как на подопытных кроликах.

  • Тексты. Журналисты и аналитики, которых вы читаете сегодня, учились писать в ЖЖ.

Справка по посещаемости на ноябрь 2025г:

Ниже — “места” (ранжирование) сайтов по количеству визитов из России за ноябрь 2025 (метрика Visits / All devices) по оценке Semrush. Отбор сайтов субъективный.

Pikabu (pikabu.ru) — 94.57M визитов из РФ (88.03% трафика сайта)

Habr (habr.com) — 24.22M визитов из РФ (68.14%)

Yaplakal (yaplakal.com) — 19.11M визитов из РФ (82.87%)

LiveJournal (livejournal.com) — 15.85M визитов из РФ (51.04%)

Рейтинг сайтов в РФ по версии Similarweb:

pikabu.ru #17

habr.com #59

yaplakal.com #75

livejournal.com #88

Вместо эпитафии

История ЖЖ — это хроника того, как мы променяли свободу слова на быстрый дофамин. Платформа, бывшая голосом поколения, стала жертвой жадности владельцев и контроля государства. Смейтесь над «лонгридами» сколько хотите. Но там, под слоями пыли и мертвых комментариев, лежит эпоха, когда:

  • мы сами выбирали, кого читать, а не алгоритм за нас;

  • мы писали коряво, но честно, а не ради охватов;

  • интернет был местом диалога, а не базаром тщеславия.

Если у вас защемило сердце — поздравляю, вы тоже динозавр.

После очередного "гениального" нововведения администрации я психанул. За полчаса, на чистом раздражении и кофеине, навайбкодил нейросетками скрипт. Он выкачивает всё: посты, комментарии, картинки, если они есть на серверах ЖЖ. Спасайте свои архивы, пока сервера окончательно не отключили по прихоти собственника.


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

Скрипт — для личного архива/ресёрча, не перезаливайте чужие тексты публично без разрешения автора, уважайте лимиты/не DDoS’ьте площадку. Не торопитесь скачивать заметки, слишком агрессивная установка переменных в скрипте стоила мне суточного бана по IP от ЖЖ. С установками в скрипте по умолчанию бана не получал, но риск остается.

Код на Python ниже, скорость сохранения порядка 5 записей в минуту:

Python скрипт сохранения заметок ЖЖ
import requests
from bs4 import BeautifulSoup
import os
import time
import re
import sys
import random
from urllib.parse import urljoin, urlparse

# ================= НАСТРОЙКИ ПО УМОЛЧАНИЮ =================
USERNAME = ""
YEAR = ""
BASE_URL = ""
OUTPUT_FOLDER = ""
IMAGES_FOLDER = "images"
COMMENTS_FOLDER = "comments_raw"

# Если посты под замком, вставьте сюда строку Cookie (например: 'ljsession=...')
COOKIES = {} 

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Encoding': 'gzip, deflate, br',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1',
}

# Настройки задержек
DELAYS = {
    'between_pages': (1, 3),      # между страницами журнала (уменьшил для скорости поиска)
    'between_posts': (2, 6),      # между скачиванием постов (сек)  
    'between_comments': (1, 4),   # между загрузкой комментариев (сек)
    'between_days': (0.5, 2),     # между днями при сканировании
    'before_retry': (5, 10)       # перед повторной попыткой
}

RUS_MONTHS = {
    'января': '01', 'февраля': '02', 'марта': '03', 'апреля': '04',
    'мая': '05', 'июня': '06', 'июля': '07', 'августа': '08',
    'сентября': '09', 'октября': '10', 'ноября': '11', 'декабря': '12',
    'янв': '01', 'фев': '02', 'мар': '03', 'апр': '04', 'май': '05', 'июн': '06',
    'июл': '07', 'авг': '08', 'сен': '09', 'окт': '10', 'ноя': '11', 'дек': '12'
}

# ==========================================================

def random_delay(delay_range):
    """Случайная задержка в заданном диапазоне"""
    min_delay, max_delay = delay_range
    sleep_time = random.uniform(min_delay, max_delay)
    # Можно закомментировать print, чтобы не спамить в консоль при поиске
    # print(f"⏳ Пауза {sleep_time:.1f}с...")
    time.sleep(sleep_time)

def clean_filename(text):
    return re.sub(r'[\\/*?:"<>|]', "", text).strip()[:100]

def parse_russian_date_from_text(text):
    if not text: return None
    text = text.lower()
    match = re.search(r'(\d{1,2})\s+([а-я]+)[,\s]+(\d{4})', text)
    if match:
        day, month_name, year = match.groups()
        month_num = RUS_MONTHS.get(month_name)
        if month_num: return f"{year}_{month_num}_{day.zfill(2)}"
    match_iso = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
    if match_iso: return f"{match_iso.group(1)}_{match_iso.group(2)}_{match_iso.group(3)}"
    return None

def get_post_date(soup):
    meta_date = soup.find('meta', property='article:published_time')
    if meta_date and meta_date.get('content'):
        return meta_date.get('content').split('T')[0].replace('-', '_')

    time_tags = soup.find_all('time')
    for tag in time_tags:
        dt = tag.get('datetime')
        if dt: return dt.split('T')[0].replace('-', '_')
        parsed = parse_russian_date_from_text(tag.get_text())
        if parsed: return parsed

    potential_containers = soup.find_all(class_=re.compile(r'(date|time|header|meta|subject)'))
    for container in potential_containers:
        parsed = parse_russian_date_from_text(container.get_text())
        if parsed: return parsed
            
    full_text = soup.get_text()[:10000] 
    parsed = parse_russian_date_from_text(full_text)
    if parsed: return parsed

    return "0000_00_00"

def download_image(img_url, filename_prefix, folder_path, max_retries=2):
    for attempt in range(max_retries + 1):
        try:
            parsed = urlparse(img_url)
            ext = os.path.splitext(parsed.path)[1].lower()
            if not ext or len(ext) > 5: ext = '.jpg'
            local_filename = f"{filename_prefix}{ext}"
            local_filepath = os.path.join(folder_path, local_filename)
            if os.path.exists(local_filepath): return local_filename
            
            img_data = requests.get(img_url, headers=HEADERS, timeout=15).content
            with open(local_filepath, 'wb') as handler: handler.write(img_data)
            return local_filename
        except Exception as e:
            if attempt < max_retries:
                # print(f"⚠️ Ошибка загрузки изображения (попытка {attempt+1}): {e}")
                random_delay(DELAYS['before_retry'])
            else:
                return None

def extract_post_urls_from_page(soup, username):
    """Извлекает ссылки на посты со страницы"""
    urls = set()
    for link in soup.find_all('a', href=True):
        href = link['href']
        # Ищем ссылки формата username.livejournal.com/NNNNN.html
        if f"{username}.livejournal.com" in href and re.search(r'/\d+\.html$', href):
            urls.add(href)
    return urls

def get_day_urls_from_year_page(soup, username, year):
    """Извлекает ссылки на дни с записями из страницы года"""
    day_urls = set()
    base = f"https://{username}.livejournal.com"
    
    for link in soup.find_all('a', href=True):
        href = link['href']
        # Ищем ссылки формата /YYYY/MM/DD/ или полный URL
        match = re.search(rf'/{year}/(\d{{2}})/(\d{{2}})/?$', href)
        if match:
            full_url = urljoin(base, href)
            day_urls.add(full_url)
    
    return day_urls

def get_all_post_urls_by_year():
    """Сбор ссылок на посты за указанный год (без скачивания контента)"""
    print(f"   ? Сканирую {YEAR} год...")
    unique_urls = set()
    
    year_url = f"https://{USERNAME}.livejournal.com/{YEAR}/"
    
    try:
        r = requests.get(year_url, headers=HEADERS, cookies=COOKIES, timeout=20)
        
        if r.status_code == 429:
            print("      ? Rate limit! Ожидание...")
            random_delay((60, 120))
            r = requests.get(year_url, headers=HEADERS, cookies=COOKIES, timeout=20)
            
        if r.status_code != 200:
            print(f"      ❌ Страница архива недоступна: {r.status_code}")
            return []
        
        soup = BeautifulSoup(r.text, 'html.parser')
        
        # СПОСОБ 1: Ищем ссылки на дни с записями (календарный вид)
        day_urls = get_day_urls_from_year_page(soup, USERNAME, YEAR)
        
        if day_urls:
            # print(f"      ? Найдено {len(day_urls)} дней с записями")
            
            for i, day_url in enumerate(sorted(day_urls)):
                try:
                    r = requests.get(day_url, headers=HEADERS, cookies=COOKIES, timeout=20)
                    if r.status_code == 200:
                        day_soup = BeautifulSoup(r.text, 'html.parser')
                        found_urls = extract_post_urls_from_page(day_soup, USERNAME)
                        unique_urls.update(found_urls)
                    random_delay(DELAYS['between_days'])
                except Exception:
                    pass
        
        else:
            # СПОСОБ 2: Посты отображаются прямо на странице года (с пагинацией)
            # Сначала собираем со страницы года
            found_urls = extract_post_urls_from_page(soup, USERNAME)
            unique_urls.update(found_urls)
            
            # Проверяем пагинацию - сканируем по месяцам (упрощенно)
            for month in range(1, 13):
                month_url = f"https://{USERNAME}.livejournal.com/{YEAR}/{month:02d}/"
                try:
                    r = requests.get(month_url, headers=HEADERS, cookies=COOKIES, timeout=15)
                    if r.status_code == 200:
                        month_soup = BeautifulSoup(r.text, 'html.parser')
                        found = extract_post_urls_from_page(month_soup, USERNAME)
                        unique_urls.update(found)
                        
                        # Также ищем дни внутри месяца
                        month_day_urls = set()
                        for link in month_soup.find_all('a', href=True):
                            href = link['href']
                            match = re.search(rf'/{YEAR}/{month:02d}/(\d{{2}})/?$', href)
                            if match:
                                full_url = urljoin(month_url, href)
                                month_day_urls.add(full_url)
                        
                        for d_url in month_day_urls:
                             r_d = requests.get(d_url, headers=HEADERS, cookies=COOKIES, timeout=15)
                             if r_d.status_code == 200:
                                 d_soup = BeautifulSoup(r_d.text, 'html.parser')
                                 unique_urls.update(extract_post_urls_from_page(d_soup, USERNAME))
                                 time.sleep(0.5)

                except:
                    pass
                random_delay(DELAYS['between_pages'])
                
    except Exception as e:
        print(f"      ❌ Ошибка сканирования года: {e}")
        
    print(f"      ✅ Найдено постов в {YEAR} году: {len(unique_urls)}")
    return sorted(list(unique_urls), reverse=True)

def get_mobile_comments(username, post_id, raw_save_path):
    mobile_url = f"https://m.livejournal.com/read/user/{username}/{post_id}/comments"
    print(f"   ? Загружаю комментарии...")
    
    try:
        r = requests.get(mobile_url, headers=HEADERS, cookies=COOKIES, timeout=25)
        
        with open(raw_save_path, 'w', encoding='utf-8') as f:
            f.write(r.text)

        if r.status_code == 429:
            return "<p>Комментарии временно недоступны (rate limit).</p>"
        if r.status_code != 200:
            return f"<p>Ошибка доступа к комментариям (код {r.status_code}).</p>"
        
        soup = BeautifulSoup(r.text, 'html.parser')
        
        comments_container = soup.find(class_='b-tree') or \
                             soup.find(class_='comments-body') or \
                             soup.find('section', class_='comments') or \
                             soup.find('div', id='comments') or \
                             soup.find(class_='b-comments')

        if not comments_container:
            main_wrapper = soup.find('main') or soup.find('div', class_='app-widget')
            if main_wrapper:
                comments_container = main_wrapper
            else:
                return "<p>Комментариев нет или не удалось извлечь.</p>"

        for junk in comments_container.find_all(['form', 'input', 'textarea', 'button', 'script', 'style']):
            junk.decompose()
        for a in comments_container.find_all('a'):
            txt = a.get_text().lower()
            if 'ответить' in txt or 'reply' in txt or 'expand' in txt or 'развернуть' in txt:
                a.decompose()
        for img in comments_container.find_all('img'):
            src = img.get('src')
            if src: img['src'] = urljoin(mobile_url, src)

        return str(comments_container)

    except Exception as e:
        return f"<p>Ошибка при обработке комментариев: {e}</p>"

def process_posts(urls, current_output_folder):
    """Скачивание списка постов в указанную папку"""
    if not os.path.exists(current_output_folder):
        os.makedirs(current_output_folder)
    images_dir = os.path.join(current_output_folder, IMAGES_FOLDER)
    if not os.path.exists(images_dir):
        os.makedirs(images_dir)
    comments_raw_dir = os.path.join(current_output_folder, COMMENTS_FOLDER)
    if not os.path.exists(comments_raw_dir):
        os.makedirs(comments_raw_dir)

    print(f"\n? Папка сохранения: {current_output_folder}")
    
    for i, url in enumerate(urls):
        try:
            post_id_match = re.search(r'/(\d+)\.html', url)
            if not post_id_match: continue
            post_id = post_id_match.group(1)

            print(f"\n[{i+1}/{len(urls)}] ? Пост #{post_id} ({url})")
            
            # Проверка, скачан ли уже файл
            # (нужно знать дату заранее, но мы не знаем, поэтому проверим после загрузки или примерно)
            # Для упрощения грузим страницу.
            
            r = requests.get(url, headers=HEADERS, cookies=COOKIES, timeout=25)
            
            if r.status_code == 429:
                print("   ? Rate limit! Большая пауза...")
                random_delay((120, 180))
                # Можно добавить retry
                continue
                
            if r.status_code != 200: 
                print(f"   ❌ Пост недоступен: {r.status_code}")
                continue
                
            if r.encoding != 'utf-8': r.encoding = r.apparent_encoding
            soup = BeautifulSoup(r.text, 'html.parser')
            
            date_str = get_post_date(soup)
            title_tag = soup.find('h1') or soup.find('h3', class_='entry-header') or soup.find('title')
            title_text = title_tag.get_text(strip=True) if title_tag else "No Title"
            safe_title = clean_filename(title_text)
            
            html_filename = f"{date_str}_{post_id}_{safe_title}.html"
            html_filepath = os.path.join(current_output_folder, html_filename)
            
            if os.path.exists(html_filepath):
                print(f"   ✅ Файл уже существует, пропускаем.")
                continue

            content = soup.find(class_='entry-content') or \
                      soup.find(class_='b-singlepost-body') or \
                      soup.find('article') or \
                      soup.find('div', {'id': 'entry-text'})
            if not content: content = soup.find('body')

            # Картинки
            images = content.find_all('img')
            if images:
                print(f"   ?️ Обработка {len(images)} картинок...")
                img_counter = 1
                for img in images:
                    src = img.get('src')
                    if not src: continue
                    abs_url = urljoin(url, src)
                    img_prefix = f"{date_str}_{post_id}_{img_counter}"
                    local_img_name = download_image(abs_url, img_prefix, images_dir)
                    if local_img_name:
                        img['src'] = f"{IMAGES_FOLDER}/{local_img_name}"
                        # Убираем жесткие размеры
                        for attr in ['width', 'height', 'style', 'srcset']:
                            if img.has_attr(attr): del img[attr]
                    img_counter += 1

            # Комментарии
            raw_comment_filename = f"comments_{post_id}.html"
            raw_comment_path = os.path.join(comments_raw_dir, raw_comment_filename)
            random_delay(DELAYS['between_comments'])
            comments_html = get_mobile_comments(USERNAME, post_id, raw_comment_path)

            # Сохранение HTML
            final_html = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <title>{title_text}</title>
    <style>
        body {{ font-family: 'Verdana', sans-serif; max-width: 800px; margin: auto; padding: 20px; background: #f4f4f4; }}
        .container {{ background: #fff; padding: 30px; border-radius: 5px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }}
        img {{ max-width: 100%; height: auto; display: block; margin: 15px 0; border-radius: 3px; }}
        h1 {{ color: #333; }}
        .meta {{ color: #777; font-size: 0.9em; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 10px; }}
        a {{ color: #0066cc; text-decoration: none; }}
        .comments-section {{ margin-top: 50px; border-top: 2px solid #eee; padding-top: 20px; }}
        .comments-header {{ font-size: 1.5em; font-weight: bold; margin-bottom: 20px; }}
        /* Стиль для комментариев с мобильной версии может требовать доработки CSS */
        .b-tree {{ padding-left: 0; }}
        .b-tree__item {{ list-style: none; margin-bottom: 20px; border-left: 3px solid #e0e0e0; padding-left: 15px; }}
        .b-tree__item .b-tree__item {{ margin-left: 15px; margin-top: 10px; border-left: 2px solid #ccc; }}
        .userbox {{ font-weight: bold; color: #444; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>{title_text}</h1>
        <div class="meta">
            <a href='{url}' target='_blank'>Оригинал</a> | ID: {post_id} | Дата: {date_str}
        </div>
        <div class="content">
            {str(content)}
        </div>
        
        <div class="comments-section">
            <div class="comments-header">Комментарии</div>
            {comments_html}
        </div>
    </div>
</body>
</html>"""
            
            with open(html_filepath, 'w', encoding='utf-8') as f:
                f.write(final_html)
            
            print(f"   ✅ Сохранено: {html_filename}")
            
        except Exception as e:
            print(f"   ❌ Ошибка с постом: {e}")
            
        random_delay(DELAYS['between_posts'])


# ================= ГЛАВНЫЙ БЛОК =================
if __name__ == "__main__":
    # Исправляем кодировку консоли для Windows, чтобы не было кракозябр
    if sys.platform == 'win32':
        os.system('chcp 65001 > nul')

    print("=" * 60)
    print("? СКАЧИВАНИЕ LIVEJOURNAL (Диапазон лет)")
    print("=" * 60)
    
    # 1. Запрос имени пользователя
    # Выносим эмодзи в print, чтобы не ломать курсор
    print("\n? Введите название журнала (username):") 
    user_input = input(" > ").strip()
    
    if not user_input:
        print("❌ Пустое имя.")
        sys.exit()
    USERNAME = user_input
    BASE_URL = f"https://{USERNAME}.livejournal.com"

    # 2. Запрос диапазона лет
    try:
        print("\n? Введите НАЧАЛЬНЫЙ год (например, 2011):")
        start_year_input = int(input(" > ").strip())
        
        print("? Введите КОНЕЧНЫЙ год (например, 2025):")
        end_year_input = int(input(" > ").strip())
    except ValueError:
        print("❌ Год должен быть числом.")
        sys.exit()

    if start_year_input > end_year_input:
        print("❌ Начальный год больше конечного. Меняю местами...")
        start_year_input, end_year_input = end_year_input, start_year_input

    print(f"\n? Поиск постов в диапазоне {start_year_input} - {end_year_input}...")
    
    # Структура для хранения найденного: { '2011': [url1, url2], '2012': [url3] }
    found_posts_by_year = {}
    total_posts_found = 0

    # 3. Цикл поиска (без скачивания)
    for y in range(start_year_input, end_year_input + 1):
        YEAR = str(y) # Обновляем глобальную переменную для функции
        urls = get_all_post_urls_by_year()
        if urls:
            found_posts_by_year[YEAR] = urls
            total_posts_found += len(urls)
        # Пауза между годами
        time.sleep(1)

    # 4. Спрос пользователя
    print("\n" + "=" * 60)
    if total_posts_found == 0:
        print("❌ За указанный период постов не найдено.")
        sys.exit()

    print(f"? ИТОГО НАЙДЕНО: {total_posts_found} постов.")
    
    print("? Скачать найденные посты? (y/n):")
    choice = input(" > ").strip().lower()

    if choice != 'y':
        print("⛔ Отмена скачивания.")
        sys.exit()

    print("\n? Начинаем загрузку контента...")
    
    # 5. Скачивание
    # Проходим по собранному словарю
    for year_str, urls_list in found_posts_by_year.items():
        if not urls_list: continue
        
        print(f"\n? === ОБРАБОТКА {year_str} ГОДА ({len(urls_list)} постов) ===")
        
        # Устанавливаем глобальные переменные для текущего шага
        YEAR = year_str
        # Формируем папку для конкретного года
        current_year_folder = os.path.join(USERNAME, year_str)
        
        # Запускаем обработку списка
        process_posts(urls_list, current_year_folder)

    print("\n" + "=" * 60)
    print("? ВСЕ ЗАДАЧИ ЗАВЕРШЕНЫ!")
    print(f"? Папка с архивом: {USERNAME}/")
    print("=" * 60)
    
    # Добавляем ожидание в конце, чтобы консоль не закрылась мгновенно
    input("\nНажмите Enter, чтобы выйти...")

Репозиторий со скриптом тут: https://github.com/isemaster/SaveLj

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

Комментарии (68)


  1. Goron_Dekar
    11.01.2026 15:48

    Эй, этот скрипт сохраняет только публичные записи. А у меня всё, что хочу сохранить - приватное!


    1. gliderman Автор
      11.01.2026 15:48

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


    1. shoorick
      11.01.2026 15:48

      Если хочется сохранить своё, то в ЖЖ есть функция эксперта в XML и CSV — не надо ничего изобретать, это штатная возможность. Правда, выдаёт всего лишь за месяц, но не проблема скачать в цикле. Но это только посты без комментов.


      1. gliderman Автор
        11.01.2026 15:48

        Многие посты потеряют в выразительности без комментов. Мой скрипт бережно их сохранит, а также любой чужой журнал.


    1. 22inch
      11.01.2026 15:48

      Да, у меня большинство из более чем 18000 постов большинство - приватные. Очень бы хотелось такой функционал.


    1. n99
      11.01.2026 15:48

      Тогда импортируйте в dreamwidth.org


  1. Emelian
    11.01.2026 15:48

    Я писал свои заметки в ЖЖ более 15 лет

    Я тоже, довольно долго там тусовался. Раньше, горячими темами была политика, поэтому, писал, в основном, на эту тему:

    http://scholium2.livejournal.com/
    https://cccp-v2.livejournal.com

    Тем не менее, пробовал переключиться на программирование, но, получалось плохо:
    https://cpp-qt.livejournal.com (одна статья)
    остальные журналы – пустые:
    https://1c77.livejournal.com
    https://1c8x.livejournal.com
    https://scholium3.livejournal.com

    Использовал их для возможности делать комментарии в других жж-блогах. Однако и этого уже не делаю много лет, ибо стало скучно.

    В ЖЖ, ныне, читаю только одного автора (COLONELCASSAD), без комментариев и комментирования. Остальных «топовых» авторов перестал читать, по разным причинам, тоже, достаточно давно.

    В общем, всему свое время. ЖЖ свою роль отыграл и это вполне естественно.


    1. gliderman Автор
      11.01.2026 15:48

      Меня задело, что нас всех поделили на касты, и сайт может исчезнуть, а черновиков нет. И как ритм времени, что меня интересовало за прошедшие годы, мне жалко было бы потерять. Да и куча журналов френдов пропасть может. Уникальный срез эпохи.


      1. Emelian
        11.01.2026 15:48

        сайт может исчезнуть, а черновиков нет

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

        Ничего этого, конечно, никогда не скачешь и даже не вспомнишь. А компьютер тоже не вечен, вся информация, рано или поздно, потеряется. Как почти все книги древности.

        Однако, как я заметил, информацию о своем прошлом всегда хочется иметь, но, почему-то, просматриваю её очень редко, практически случайно. Это касается и фото / видео материалов, как онлайновых, так и оффлайновых.

        Так, что особо грустить, думаю, не стоит, тем более, что говорят, что наше «надсознание» всю нашу информацию всегда помнит и «до» и «после». Для этого, достаточно, четко сформулировать ее даже мысленно, не говоря уже о своей речи, тексте, звуко и видео записи.


    1. Emelian
      11.01.2026 15:48

      P.S.:

      Python скрипт сохранения заметок ЖЖ

      Я, вот, попросил, недавно, бесплатный «Мистраль»:

      Как сделать, с помощью Питона, статью, допустим, по ссылке: https://habr.com/ru/articles/983062/ автономной, так чтобы ее можно было бы посмотреть локально, на компьютере, в том же самом виде, без подключения к Интернету?

      И он мне, после пары уточнений, дал вполне рабочий скрипт, с помощью, которого я получил html-текст своей базовой статьи, отсюда: « https://habr.com/ru/articles/848836/ » и перенес ее на свой сайт: « http://scholium.webservis.ru/Articles/Lecole00.html ».

      «Дёшево и сердито!» :) .


      1. gliderman Автор
        11.01.2026 15:48

        Полагаю, что он не сильно от моего отличается, вполне возможно мой скрипт можно одним запросом сделать универсальным. Даем ссылку - и он делает локальную копию.


        1. Emelian
          11.01.2026 15:48

          Полагаю, что он не сильно от моего отличается

          Вполне возможно, я не вникал. Я, вообще, не любитель изучать полные скрипты, в статьях, ибо воспринимаю их плохо. Максимум, существенный фрагмент, на 10-20 строк. Потому, не показываю их в своих публикациях, только ссылки на архивы. Причем такие, чтобы было минимум телодвижений. Вроде: «Жми этот cmd-файл, он запустит такой-то py-скрипт, возьмет данные отсюда и положит их туда». Если проект «приплюснутый», то тоже самое, вот вам архив, там есть скомпилированные бинарники, можете делать их сами либо запустить готовые.

          Не знаю, как у других, но я люблю вникать в чужой код только на своем компьютере, все остальное – от лукавого. Главное, чтобы для запуска чьей-то программы не надо было сильно напрягаться и не организовывать «танцы с бубном». Однако, когда приспичит, то приходится. Скажем, чтобы скомпилировать линуксовский консольный видеоплейер «FFPlay.c» в своей оконной программе «МедиаТекст» (см. скриншот в http://scholium.webservis.ru/Pics/MediaText.png ), мне пришлось переделать «FFPlay.c» в «FFPlay.cpp» и организовать там соответствующую систему классов. Это была муторная работа, но не безнадежная. Кстати, я так и не встретил на Гитхабе безпроблемного демо-проекта запускающего «FFPlay.c» в оконной программе. Что-то, конечно, было, но, сложное, громоздкое и некрасивое. Поэтому, как говорил «дедушка» Ленин: «Мы пошли другим путём!».


          1. gliderman Автор
            11.01.2026 15:48

            А сейчас и вникать особо не надо - любую чужую открытую программу скармливаем в нейросеть и спрашиваем - есть ли бэкдоры? Меня больше напрягают чужие скомпилированные и обфусцированные релизы. Что там может быть - одному разработчику известно. Можно, конечно, в песочнице запускать, да Wiresharkом логи смотреть, но это не дает гарантии от спящих закладок.


            1. Emelian
              11.01.2026 15:48

              есть ли бэкдоры?

              Дело не в «бэкдорах», а в «месте». Классика: «Парламент – не место для дискуссий!». В перефразе: «Статьи на «Хабре» – не место для «портянок» кода!». Я, лично предпочитаю такую структуру своих статей: Введение / Результат / Нюансы реализации / Выводы. Весь «большой» код и данные – в архивы. Никому её не навязываю, просто сообщаю, что «много букафф кода», в статьях, я просто игнорирую. Но, если автору нравится – пусть публикует, меня это мало волнует. Вот есть же в статистике статей, на «Хабре», процент пользователей, читающих статью менее 10 секунд. Думаю, чтобы понять, что читать не нужно, достаточно и пяти секунд.

              Меня больше напрягают чужие скомпилированные и обфусцированные релизы. Что там может быть - одному разработчику известно.

              Не вижу проблем! Вот в моей последней статье: «Ключевые слова в иностранном языке или как увеличить свой словарный запас?» ( https://habr.com/ru/articles/982660/ ) используется такой «скомпилированный и обфусцированный релиз» – «piper.exe» (не мой, а из установленного пакета Питона «piper-tts», по-моему). Обойти этот файл никак нельзя, он делает главную работу – локальную озвучку иностранного текста. Поэтому, я обратил на него пристальное внимание, прогнал через антивирусы («DrWeb» – рулит, обновлять его антивирусную базу можно каждый день). Вирусов там нет, но код обфусцирован – зачем? Точнее, это оказался sfx-архив, который самораспаковывающийся ехе-шник размещает во временной директории и оттуда запускает полученный код, в данном случае, скрипт на Питоне. В этом скрипте – всего шесть строчек, поэтому я их показал в своей статье. Похоже, он просто передаёт управление упомянутому пакету Питона, отвечающего за создание TTS. Чисто технически, это очень удобно – генерить звук через ехе-шник, а не вызывая скрипты Питона непосредственно. Чем я, вовсю, и пользовался.

              А если не запускать чужие бинарники, без исходников, то тогда наша жизнь станет «блеклой». Лучше уж, для контроля вирусов, применять антивирусы, что, как бы, вполне разумное решение.


  1. wilelf
    11.01.2026 15:48

    Всё когда-нибудь заканчивается. ЖЖ не был прям вот офигенным. Это было место якобы "свободы" без цензуры. Все эти Носики, Марты Кетро и прочие, кто пользовался тем, что вокруг было пусто.

    Можно было написать короткую заметку (про сиськи или ещё что повседневное), и получить кучу комментариев и реакций. Но когда некоторые из этих деятелей попытались написать нечто более осмысленное (книгу), то выяснилось, что писать много и целостно они не умеют. И никогда не сумеют, судя по дню сегодняшнему.


    1. gliderman Автор
      11.01.2026 15:48

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


      1. wilelf
        11.01.2026 15:48

        Пусть так.

        Дело в том, что большинство "звёзд" ЖЖ позиционируют себя в качестве основателей Рунета. Тогда как основателями были инженеры, техники и работяги, которые тянули витые пары и телефонные провода.


        1. gliderman Автор
          11.01.2026 15:48

          Есть один нюанс. Чтобы люди захотели протянуть себе витую пару должен был появиться интересный контент. Кто хотел себе просто развлечений - тянули себе кабельное ТВ. Поэтому любой автор, что хоть что-то чирикнул в ЖЖ - полноценный участник развития интернета наряду с техниками и инженерами.


          1. wilelf
            11.01.2026 15:48

            Сложно сказать. Мы чирикали в аськах и чатах региональных самописных. А ещё делали сайты на чистом html, где писали best view in IE 7.0


            1. gliderman Автор
              11.01.2026 15:48

              Я про массового клиента интернета. Гики так те с модемов и BBS начинали.


            1. Emelian
              11.01.2026 15:48

              А ещё делали сайты на чистом html, где писали best view in IE 7.0

              А почему «делали»? Я и сейчас делаю! Вот, только три дня назад, перенёс свою базовую статью, об обучающей программе «L'école», отсюда, на свой сайт, который делается на «чистом html» плюс начал пробовать (пока, в режиме скрытого тестирования) лепить туда cgi-скрипты и прочие «няшки» : http://scholium.webservis.ru/Articles/Lecole00.html .


        1. Goron_Dekar
          11.01.2026 15:48

          Нет, работяги ничего не основывали и не создавали. Они тиражировали технологию. А вот "звёзды" ЖЖ, фидо, форумов - создавали. Совё. Новое. И непохожее на то, что было вокруг.


          1. wilelf
            11.01.2026 15:48

            +. И что же они создали в итоге?


            1. PereslavlFoto
              11.01.2026 15:48

              В статье есть ответ:

              Журналисты и аналитики, которых вы читаете сегодня, учились писать в ЖЖ.

              Они создали доходный бизнес для себя и обеспечили рекламный доход для платформы.


              1. wilelf
                11.01.2026 15:48

                Очень точно. Они сделали себе имя и деньги. Но в итоге провалились на реальных проектах.

                Рунет делали инженеры и инвесторы. Сегалович, Ашманов, куча лишних людей из Рамблера...


  1. anonymous
    11.01.2026 15:48


  1. anonymous
    11.01.2026 15:48