↩️ Назад

Категории

Как выгрузить расписание из 1С за VPN в интернет — гайд с примерами

22.05.2026 | Статья из категории: 1С

Проблема: 1С есть, но она внутри корпоративного VPN. Преподаватели/специалисты хотят смотреть расписание замен с телефона, по дороге на работу. Без паролей, без VPN. Просто ссылку открыл — и увидел. А 1С в интернет выпускать нельзя — политика безопасности.

Решение: Архитектура с посредником. 1С кладёт файл в сетевую папку, маленький скрипт забирает его через VPN и выкладывает на публичный сайт. Всё автоматически, раз в час. Ниже — полный рабочий код.

✅ Ключевая идея: VPN нужен только на 10 секунд, пока скрипт копирует файл. Остальное время всё работает без VPN. 1С не лезет в интернет — безопасно.

📌 Шаг 1. Что сделать на стороне 1С

Договоритесь с 1С-ником. Текст для него:


            "Сделай регламентное задание, которое раз в час сохраняет расписание замен в файл CSV 
и кладёт его в сетевую папку \\server\share\raspisanie.csv"

Пример формата CSV (важно для календаря):


             Дата,Время,Кабинет,Преподаватель,Предмет,Примечание
2026-05-22,09:00,101,Иванов А.А.,Физика,замена
2026-05-22,11:00,203,Петрова Б.Б.,Математика,
2026-05-23,10:00,105,Сидоров В.В.,Информатика,перенос<

Если 1С-ник скажет «не умею» — покажите ему этот код выгрузки (встроенный язык 1С):


            Процедура ВыгрузитьРасписание() Экспорт
    Таблица = Новый ТаблицаЗначений;
    // ... заполнить таблицу из справочника ...
    Путь = "\\server\share\raspisanie.csv";
    Запись = Новый ЗаписьТекста(Путь, "UTF-8", ложь);
    Запись.ЗаписатьСтроку("Дата,Время,Кабинет,Преподаватель,Предмет,Примечание");
    Для каждого Стр Из Таблица Цикл
        Запись.ЗаписатьСтроку(Стр.Дата + "," + Стр.Время + "," + Стр.Кабинет + "," + Стр.Преподаватель + "," + Стр.Предмет + "," + Стр.Примечание);
    КонецЦикла;
    Запись.Закрыть();
КонецПроцедуры

🐍 Шаг 2. Скрипт-посредник (Python)

Этот скрипт запускается на любом компьютере/виртуалке, который умеет подключаться к VPN и имеет выход в интернет. Он: подключает VPN → копирует файл → отключает VPN → заливает на FTP.

📁 upload_to_ftp.py

             #!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import shutil
import subprocess
import ftplib
import sys
from datetime import datetime

# ========== НАСТРОЙКИ (меняйте под себя) ==========
# VPN (Windows)
VPN_NAME = "Мой VPN"           # Имя подключения в Windows
VPN_LOGIN = "vpn_login"        
VPN_PASS = "vpn_password"      

# Файлы и пути
SOURCE_FILE = r"\\192.168.1.100\share\raspisanie.csv"
TEMP_FILE = r"C:\temp\raspisanie.csv"

# FTP
FTP_HOST = "ftp.ваш-сайт.ru"
FTP_USER = "ftp_login"
FTP_PASS = "ftp_password"
FTP_PATH = "/public_html/schedule/"
# ===================================================

def log(msg):
    print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}")

def vpn_connect():
    log("Подключаю VPN...")
    cmd = f'rasdial "{VPN_NAME}" {VPN_LOGIN} {VPN_PASS}'
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    if "connected" in result.stdout.lower():
        log("VPN подключён")
        return True
    log(f"Ошибка: {result.stderr}")
    return False

def vpn_disconnect():
    log("Отключаю VPN...")
    subprocess.run(f'rasdial "{VPN_NAME}" /disconnect', shell=True, capture_output=True)

def copy_file():
    if not os.path.exists(SOURCE_FILE):
        log(f"Файл не найден: {SOURCE_FILE}")
        return False
    os.makedirs(os.path.dirname(TEMP_FILE), exist_ok=True)
    shutil.copy2(SOURCE_FILE, TEMP_FILE)
    log(f"Скопирован: {TEMP_FILE} ({os.path.getsize(TEMP_FILE)} байт)")
    return True

def upload_ftp():
    try:
        ftp = ftplib.FTP(FTP_HOST)
        ftp.login(FTP_USER, FTP_PASS)
        ftp.cwd(FTP_PATH)
        with open(TEMP_FILE, 'rb') as f:
            ftp.storbinary('STOR raspisanie.csv', f)
        ftp.quit()
        log("Загружен на FTP")
        return True
    except Exception as e:
        log(f"Ошибка FTP: {e}")
        return False

def main():
    log("=== СТАРТ ===")
    if not vpn_connect():
        sys.exit(1)
    success = copy_file()
    vpn_disconnect()
    if success:
        upload_ftp()
    log("=== ГОТОВО ===")

if __name__ == "__main__":
    main()

Как запустить:

  • Установить Python (python.org)
  • Сохранить скрипт, заменить настройки
  • Проверить: python upload_to_ftp.py
  • Добавить в Планировщик заданий Windows (Task Scheduler) с повторением каждый час
⚠️ Для Linux: используйте openvpn и crontab. В скрипте замените rasdial на вызов openvpn --config.

📅 Шаг 3. Календарь на сайте (HTML + JS)

Сохраните этот код как index.html на вашем хостинге. Он загружает CSV и показывает красивое расписание с группировкой по дням, кликабельными деталями и навигацией по неделям.

🌐 index.html (полностью рабочий)

             <!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Расписание замен</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; background: #f5f7fa; padding: 20px; }
        .container { max-width: 1300px; margin: 0 auto; }
        h1 { font-size: 28px; margin-bottom: 10px; color: #2c3e50; }
        .last-update { color: #7f8c8d; font-size: 14px; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #e0e0e0; }
        .controls { margin-bottom: 25px; display: flex; gap: 15px; flex-wrap: wrap; align-items: center; }
        button { background: #3498db; color: white; border: none; padding: 8px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; }
        button:hover { background: #2980b9; }
        .week-nav { display: flex; gap: 10px; background: white; padding: 8px 15px; border-radius: 30px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
        .week-nav button { background: #ecf0f1; color: #2c3e50; padding: 5px 12px; }
        .calendar { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
        .day-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); overflow: hidden; }
        .day-header { background: #2c3e50; color: white; padding: 15px; font-size: 18px; font-weight: bold; }
        .day-header small { font-size: 13px; font-weight: normal; opacity: 0.8; }
        .lessons-list { padding: 10px; }
        .lesson-item { border-bottom: 1px solid #eee; padding: 12px; cursor: pointer; transition: background 0.2s; }
        .lesson-item:hover { background: #f8f9fa; }
        .lesson-time { font-weight: bold; color: #2980b9; font-size: 15px; margin-bottom: 5px; }
        .lesson-room { color: #27ae60; font-size: 13px; margin-bottom: 3px; }
        .lesson-teacher { color: #7f8c8d; font-size: 14px; }
        .lesson-details { margin-top: 10px; padding: 10px; background: #ecf0f1; border-radius: 8px; font-size: 14px; display: none; }
        .lesson-details.show { display: block; }
        .empty-day { text-align: center; padding: 30px; color: #95a5a6; }
        .loading { text-align: center; padding: 50px; color: #7f8c8d; }
        @media (max-width: 750px) { .calendar { grid-template-columns: 1fr; } body { padding: 10px; } }
    </style>
</head>
<body>
<div class="container">
    <h1>📅 Расписание замен</h1>
    <div class="last-update" id="lastUpdate">Загрузка...</div>
    <div class="controls">
        <button onclick="loadSchedule()">🔄 Обновить</button>
        <div class="week-nav">
            <button onclick="changeWeek(-1)">◀ Пред.</button>
            <span style="padding: 0 10px;" id="weekRange">Текущая неделя</span>
            <button onclick="changeWeek(1)">След. ▶</button>
        </div>
    </div>
    <div id="calendarContainer" class="loading">Загрузка расписания...</div>
</div>

<script>
    const CSV_URL = "raspisanie.csv";
    const COL_DATE = 0, COL_TIME = 1, COL_ROOM = 2, COL_TEACHER = 3, COL_SUBJECT = 4, COL_NOTE = 5;
    let allLessons = [];
    let currentWeekOffset = 0;

    function parseCSV(text) {
        const lines = text.split(/\r?\n/);
        const result = [];
        for (let i = 1; i < lines.length; i++) {
            const line = lines[i].trim();
            if (!line) continue;
            const row = [];
            let current = "", inQuotes = false;
            for (let ch of line) {
                if (ch === '"') inQuotes = !inQuotes;
                else if (ch === ',' && !inQuotes) { row.push(current.trim()); current = ""; }
                else current += ch;
            }
            row.push(current.trim());
            if (row.length >= 4) {
                result.push({
                    date: row[COL_DATE] || "",
                    time: row[COL_TIME] || "",
                    room: row[COL_ROOM] || "",
                    teacher: row[COL_TEACHER] || "",
                    subject: row[COL_SUBJECT] || "",
                    note: row[COL_NOTE] || ""
                });
            }
        }
        return result;
    }

    async function loadSchedule() {
        document.getElementById("calendarContainer").innerHTML = '<div class="loading">Загрузка...</div>';
        try {
            const res = await fetch(CSV_URL + "?t=" + Date.now());
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            const text = await res.text();
            allLessons = parseCSV(text);
            document.getElementById("lastUpdate").innerHTML = `📅 Обновлено: ${new Date().toLocaleString("ru-RU")} | Записей: ${allLessons.length}`;
            currentWeekOffset = 0;
            renderCalendar();
        } catch(e) {
            document.getElementById("calendarContainer").innerHTML = `<div style="background:#e74c3c;color:white;padding:15px;border-radius:8px;">❌ Ошибка: ${e.message}</div>`;
        }
    }

    function getWeekStart(offset = 0) {
        const now = new Date();
        now.setHours(0,0,0,0);
        let day = now.getDay();
        const mondayOffset = day === 0 ? -6 : 1 - day;
        const monday = new Date(now);
        monday.setDate(now.getDate() + mondayOffset + offset * 7);
        return monday;
    }

    function formatDate(d) { return d.toLocaleDateString("ru-RU", { day: 'numeric', month: 'long' }); }

    function isSameDate(dateStr, target) {
        if (!dateStr) return false;
        let y,m,d;
        if (dateStr.includes('-')) [y,m,d] = dateStr.split('-');
        else if (dateStr.includes('.')) [d,m,y] = dateStr.split('.');
        else return false;
        const lessonDate = new Date(parseInt(y), parseInt(m)-1, parseInt(d));
        return lessonDate.toDateString() === target.toDateString();
    }

    function getLessonsForDate(date) {
        return allLessons.filter(l => isSameDate(l.date, date)).sort((a,b) => a.time.localeCompare(b.time));
    }

    function changeWeek(delta) { currentWeekOffset += delta; renderCalendar(); }

    function toggleDetails(id) {
        const el = document.getElementById(`details-${id}`);
        if (el) el.classList.toggle('show');
    }

    function renderCalendar() {
        const weekStart = getWeekStart(currentWeekOffset);
        const weekDays = [];
        for (let i = 0; i < 7; i++) {
            const d = new Date(weekStart);
            d.setDate(weekStart.getDate() + i);
            weekDays.push(d);
        }
        const startStr = formatDate(weekDays[0]);
        const endStr = formatDate(weekDays[6]);
        document.getElementById("weekRange").innerHTML = `${startStr} — ${endStr}`;
        
        let html = '<div class="calendar">';
        for (let day of weekDays) {
            const lessons = getLessonsForDate(day);
            const dayName = day.toLocaleDateString("ru-RU", { weekday: 'long' });
            html += `<div class="day-card"><div class="day-header">${dayName}, ${formatDate(day)} <small>${lessons.length}</small></div><div class="lessons-list">`;
            if (lessons.length === 0) html += '<div class="empty-day">📭 Нет занятий</div>';
            else {
                lessons.forEach((l, idx) => {
                    const uid = `${day.getTime()}_${idx}`;
                    html += `<div class="lesson-item" onclick="toggleDetails('${uid}')">
                        <div class="lesson-time">⏰ ${l.time || '—'}</div>
                        <div class="lesson-room">🏫 ${l.room || 'Кабинет не указан'}</div>
                        <div class="lesson-teacher">👨‍🏫 ${l.teacher || '—'}</div>
                        <div class="lesson-details" id="details-${uid}">${l.subject ? `<div>📖 Предмет: ${l.subject}</div>` : ''}${l.note ? `<div>📝 ${l.note}</div>` : ''}</div>
                    </div>`;
                });
            }
            html += `</div></div>`;
        }
        html += `</div>`;
        document.getElementById("calendarContainer").innerHTML = html;
    }

    loadSchedule();
</script>
</body>
</html>

⚙️ Как собрать всё вместе

  • 1 Договориться с 1С-ником — пусть настроит выгрузку CSV в сетевую папку раз в час.
  • 2 Подготовить виртуалку или компьютер — с Python, VPN-клиентом и доступом к той самой сетевой папке.
  • 3 Положить скрипт upload_to_ftp.py, заменить настройки (VPN, FTP, пути).
  • 4 Настроить планировщик (Task Scheduler или cron) на запуск скрипта каждый час.
  • 5 На хостинге создать папку, загрузить index.html и пустой raspisanie.csv для начала.
  • 6 Проверить — через час после первого запуска CSV должен обновиться на сайте.
💡 Совет: Сначала всё протестируйте вручную. Запустите скрипт из командной строки — посмотрите логи. Убедитесь, что VPN подключается и файл копируется. Потом уже настраивайте автозапуск.

❓ Частые проблемы и решения

  • VPN не подключается из скрипта — проверьте имя подключения (должно точно совпадать). В Windows можно посмотреть командой rasdial.
  • Файл не копируется из сетевой папки — права доступа. Убедитесь, что скрипт запускается от имени пользователя, у которого есть доступ к \\server\share\ .
  • FTP не заливает — проверьте пароль, путь на сервере, активен ли FTP на хостинге.
  • На сайте ошибка CORS — если CSV и HTML лежат в одной папке — проблем нет. Если на разных доменах — настройте CORS на сервере или положите файлы рядом.
  • CSV в другой кодировке — если буквы «кракозябры», попросите 1С-ника сохранять в UTF-8, либо в скрипте Python добавьте encoding='utf-8' при чтении.

🎯 Итог

Вы получили полностью рабочий pipeline:

  • ✅ 1С выгружает CSV в сетевую папку (без доступа в интернет)
  • ✅ Python-скрипт раз в час подключается к VPN, забирает файл, отключается и заливает на FTP
  • ✅ Красивый календарь на сайте показывает расписание с группировкой по дням, кликабельными деталями и навигацией по неделям

Всё это работает на любом хостинге с поддержкой статических файлов. Никаких баз данных, бекендов, сложных настроек. Просто, надёжно, безопасно.

Удачи! Пусть ваши специалисты всегда знают, кто и когда их заменяет. 🚀




Категории:

Категории

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку статей

Посетителей сегодня: 0
о блоге | карта блога | 📡 Подписаться на RSS

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA