<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Заметки программиста</title><link>https://kveselkov.ru/</link><description>Recent content on Заметки программиста</description><generator>Hugo -- gohugo.io</generator><language>ru</language><lastBuildDate>Wed, 23 Sep 2026 10:30:00 +0400</lastBuildDate><atom:link href="https://kveselkov.ru/index.xml" rel="self" type="application/rss+xml"/><item><title>Получение статистики с gpx файла</title><link>https://kveselkov.ru/posts/gpx-statistics/</link><pubDate>Wed, 23 Sep 2026 10:30:00 +0400</pubDate><guid>https://kveselkov.ru/posts/gpx-statistics/</guid><description>&lt;p&gt;Недавно проехал сокращенный маршрут &lt;a class="link" href="https://vk.ru/russia_bikepacking" target="_blank" rel="noopener"&#10; &gt;Уральской байкпакинговой гонки 2026&lt;/a&gt;. Вышло 538 км и 2,5 суток в седле. Трек записывал велокомпьютером Coros Dura. После заезда хотел посмотреть такой параметр, как &amp;ldquo;время в движении&amp;rdquo;, но его не оказалось в приложении COROS.&lt;/p&gt;&#10;&lt;p&gt;Первая мысль — скачаю трек и загружу в любой сторонний сервис для анализа тренировок. Но, как оказалось, что не каждый ресурс переваривает такие объемы. А точнее я не нашел ресурса, который переварил бы мои метрики с гонки. Например, Strava принимает файлы до 25 МБ, а мой gpx файл весил целых 35 МБ.&lt;/p&gt;&#10;&lt;p&gt;Что хотелось узнать?&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Время в движении&lt;/li&gt;&#10;&lt;li&gt;Время остановок:&#10;&lt;ul&gt;&#10;&lt;li&gt;Длинные на ночлег: более 2 часов&lt;/li&gt;&#10;&lt;li&gt;Короткие на отдых/перекусы/магазины: от 5 минут до 2 часов&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Накидал простенький скрипт на python:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;import xml.etree.ElementTree as ET&#10;from datetime import datetime&#10;import math&#10;import os&#10;&#10;# --- FILTER SETTINGS ---&#10;# Minimum speed to consider as moving (in km/h).&#10;MIN_MOVING_SPEED_KMH = 0.5 &#10;&#10;# Minimum distance between points to consider as moving (in meters).&#10;MIN_MOVING_DISTANCE_M = 1.5&#10;&#10;# Maximum pause between points (in seconds). &#10;MAX_TIME_GAP_SEC = 30 &#10;&#10;# Micro-stop threshold (in seconds). Stops shorter than this are considered as part of riding.&#10;# 5 minutes = 300 seconds&#10;MICRO_STOP_THRESHOLD_SEC = 5 * 60 &#10;&#10;# Sleep threshold (in seconds). Stops longer than this are considered as sleep.&#10;# 2 hours = 7200 seconds&#10;SLEEP_THRESHOLD_SEC = 2 * 60 * 60 &#10;&#10;def haversine(lat1, lon1, lat2, lon2):&#10; &amp;#34;&amp;#34;&amp;#34;Calculates distance between two coordinates in meters.&amp;#34;&amp;#34;&amp;#34;&#10; R = 6371000.0 &#10; phi1 = math.radians(lat1)&#10; phi2 = math.radians(lat2)&#10; dphi = math.radians(lat2 - lat1)&#10; dlambda = math.radians(lon2 - lon1)&#10; &#10; a = math.sin(dphi/2.0)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda/2.0)**2&#10; c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))&#10; return R * c&#10;&#10;def parse_gpx(file_path):&#10; print(f&amp;#34;Читаем файл: {file_path} ({os.path.getsize(file_path) / (1024*1024):.2f} МБ)...&amp;#34;)&#10; &#10; tree = ET.parse(file_path)&#10; root = tree.getroot()&#10; &#10; points = []&#10; &#10; for trkpt in root.iter(&amp;#39;{http://www.topografix.com/GPX/1/1}trkpt&amp;#39;):&#10; lat = float(trkpt.get(&amp;#39;lat&amp;#39;))&#10; lon = float(trkpt.get(&amp;#39;lon&amp;#39;))&#10; time_str = trkpt.find(&amp;#39;{http://www.topografix.com/GPX/1/1}time&amp;#39;).text&#10; &#10; time_str = time_str.replace(&amp;#39;Z&amp;#39;, &amp;#39;+00:00&amp;#39;)&#10; dt = datetime.fromisoformat(time_str)&#10; &#10; points.append({&amp;#39;lat&amp;#39;: lat, &amp;#39;lon&amp;#39;: lon, &amp;#39;time&amp;#39;: dt})&#10; &#10; print(f&amp;#34;Найдено точек: {len(points)}&amp;#34;)&#10; return points&#10;&#10;def calculate_stats(points):&#10; total_distance = 0.0&#10; total_time_sec = 0.0&#10; moving_time_sec = 0.0&#10; short_stops_sec = 0.0 # Stops from 5 minutes to 2 hours&#10; sleep_time_sec = 0.0 # Stops 2 hours or more&#10; &#10; current_stop_duration = 0.0 &#10; &#10; for i in range(1, len(points)):&#10; p1 = points[i-1]&#10; p2 = points[i]&#10; &#10; dist = haversine(p1[&amp;#39;lat&amp;#39;], p1[&amp;#39;lon&amp;#39;], p2[&amp;#39;lat&amp;#39;], p2[&amp;#39;lon&amp;#39;])&#10; time_delta = (p2[&amp;#39;time&amp;#39;] - p1[&amp;#39;time&amp;#39;]).total_seconds()&#10; &#10; if time_delta &amp;lt;= 0:&#10; continue&#10; &#10; total_distance += dist&#10; total_time_sec += time_delta&#10; &#10; speed_ms = dist / time_delta&#10; speed_kmh = speed_ms * 3.6&#10; &#10; is_moving = (dist &amp;gt;= MIN_MOVING_DISTANCE_M) and (speed_kmh &amp;gt;= MIN_MOVING_SPEED_KMH) and (time_delta &amp;lt;= MAX_TIME_GAP_SEC)&#10; &#10; if is_moving:&#10; moving_time_sec += time_delta&#10; &#10; # If we are moving, the previous stop has ended. Sorting it:&#10; if current_stop_duration &amp;gt; 0:&#10; if current_stop_duration &amp;gt;= SLEEP_THRESHOLD_SEC:&#10; sleep_time_sec += current_stop_duration&#10; elif current_stop_duration &amp;gt; MICRO_STOP_THRESHOLD_SEC:&#10; short_stops_sec += current_stop_duration&#10; else:&#10; # Stops less than 5 minutes are merged into moving time&#10; moving_time_sec += current_stop_duration &#10; &#10; current_stop_duration = 0.0&#10; else:&#10; current_stop_duration += time_delta&#10; &#10; # Check the final stop at the end of the track&#10; if current_stop_duration &amp;gt; 0:&#10; if current_stop_duration &amp;gt;= SLEEP_THRESHOLD_SEC:&#10; sleep_time_sec += current_stop_duration&#10; elif current_stop_duration &amp;gt; MICRO_STOP_THRESHOLD_SEC:&#10; short_stops_sec += current_stop_duration&#10; else:&#10; moving_time_sec += current_stop_duration&#10; &#10; return total_distance, total_time_sec, moving_time_sec, short_stops_sec, sleep_time_sec&#10;&#10;def format_time(seconds):&#10; &amp;#34;&amp;#34;&amp;#34;Converts seconds to HH:MM:SS format&amp;#34;&amp;#34;&amp;#34;&#10; hours = int(seconds // 3600)&#10; minutes = int((seconds % 3600) // 60)&#10; secs = int(seconds % 60)&#10; return f&amp;#34;{hours:02d}:{minutes:02d}:{secs:02d}&amp;#34;&#10;&#10;if __name__ == &amp;#34;__main__&amp;#34;:&#10; # Specify your file name here:&#10; GPX_FILE = &amp;#34;my_ride.gpx&amp;#34; &#10; &#10; if not os.path.exists(GPX_FILE):&#10; print(f&amp;#34;ОШИБКА: Файл &amp;#39;{GPX_FILE}&amp;#39; не найден!&amp;#34;)&#10; else:&#10; pts = parse_gpx(GPX_FILE)&#10; dist, total_t, mov_t, short_t, sleep_t = calculate_stats(pts)&#10; &#10; print(&amp;#34;\n&amp;#34; + &amp;#34;=&amp;#34;*50)&#10; print(f&amp;#34;Общая дистанция: {dist / 1000:.2f} км&amp;#34;)&#10; print(f&amp;#34;Общее время: {format_time(total_t)} (от старта до финиша)&amp;#34;)&#10; print(&amp;#34;-&amp;#34; * 50)&#10; print(f&amp;#34;ВРЕМЯ В ДВИЖЕНИИ: {format_time(mov_t)}&amp;#34;)&#10; print(f&amp;#34;Остановки: {format_time(short_t)} (от 5 мин до 2 часов)&amp;#34;)&#10; print(f&amp;#34;Время остановки на ночлег: {format_time(sleep_t)} (более 2 часов)&amp;#34;)&#10; print(&amp;#34;=&amp;#34;*50)&#10; if mov_t &amp;gt; 0:&#10; print(f&amp;#34;Средняя скорость (в движении): {(dist/1000) / (mov_t/3600):.2f} км/ч&amp;#34;)&#10; if total_t &amp;gt; 0:&#10; print(f&amp;#34;Средняя скорость (общая): {(dist/1000) / (total_t/3600):.2f} км/ч&amp;#34;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Вывод работы скрипта:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;python3 gpx_calc.py&#10;Читаем файл: my_ride.gpx (34.30 МБ)...&#10;Найдено точек: 134273&#10;&#10;==================================================&#10;Общая дистанция: 538.94 км&#10;Общее время: 61:52:21 (от старта до финиша)&#10;--------------------------------------------------&#10;ВРЕМЯ В ДВИЖЕНИИ: 39:26:49&#10;Остановки: 07:20:54 (от 5 мин до 2 часов)&#10;Время остановки на ночлег: 15:04:38 (более 2 часов)&#10;==================================================&#10;Средняя скорость (в движении): 13.66 км/ч&#10;Средняя скорость (общая): 8.71 км/ч&#10;&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Северное сияние 2025</title><link>https://kveselkov.ru/posts/polar-lights-2025/</link><pubDate>Mon, 29 Sep 2025 12:00:00 +0400</pubDate><guid>https://kveselkov.ru/posts/polar-lights-2025/</guid><description>&lt;p&gt;&lt;img alt="Северное сияние" class="gallery-image" data-flex-basis="359px" data-flex-grow="149" height="1707" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://kveselkov.ru/posts/polar-lights-2025/polar%20lights.jpg" srcset="https://kveselkov.ru/posts/polar-lights-2025/polar%20lights_hu_ce0ae8f9d245e976.jpg 800w, https://kveselkov.ru/posts/polar-lights-2025/polar%20lights_hu_841a3535386c3de6.jpg 1600w, https://kveselkov.ru/posts/polar-lights-2025/polar%20lights_hu_a262ace18c780aa6.jpg 2400w, https://kveselkov.ru/posts/polar-lights-2025/polar%20lights.jpg 2560w" width="2560"&gt;&lt;/p&gt;&#10;&lt;p&gt;29.09.2025&lt;br&gt;&#10;f/5 + 10 сек выдержки&lt;/p&gt;&#10;</description></item><item><title>Лунное затмение 2025</title><link>https://kveselkov.ru/posts/lunar-eclipse-2025/</link><pubDate>Sun, 07 Sep 2025 12:00:00 +0400</pubDate><guid>https://kveselkov.ru/posts/lunar-eclipse-2025/</guid><description>&lt;p&gt;&lt;img alt="Лунное затмение" class="gallery-image" data-flex-basis="180px" data-flex-grow="75" height="1921" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://kveselkov.ru/posts/lunar-eclipse-2025/lunar%20eclipse.jpg" srcset="https://kveselkov.ru/posts/lunar-eclipse-2025/lunar%20eclipse_hu_9ae87dbfd948b48b.jpg 800w, https://kveselkov.ru/posts/lunar-eclipse-2025/lunar%20eclipse.jpg 1441w" width="1441"&gt;&lt;/p&gt;&#10;&lt;p&gt;Лунное затмение&lt;br&gt;&#10;07.09.2025&lt;br&gt;&#10;Sky-Watcher P130650AZ + телефон&lt;/p&gt;&#10;</description></item><item><title>Коммета C/2025 A6 (Lemmon)</title><link>https://kveselkov.ru/posts/c-2025-lemon-a6/</link><pubDate>Sun, 10 Aug 2025 12:00:00 +0400</pubDate><guid>https://kveselkov.ru/posts/c-2025-lemon-a6/</guid><description>&lt;p&gt;&lt;img alt="C/2025 A6 (Lemmon)" class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="1441" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://kveselkov.ru/posts/c-2025-lemon-a6/C%202025%20A6%20Lemmon.jpg" srcset="https://kveselkov.ru/posts/c-2025-lemon-a6/C%202025%20A6%20Lemmon_hu_bdbf3c0aea738a67.jpg 800w, https://kveselkov.ru/posts/c-2025-lemon-a6/C%202025%20A6%20Lemmon_hu_aae7acd47b42697a.jpg 1600w, https://kveselkov.ru/posts/c-2025-lemon-a6/C%202025%20A6%20Lemmon_hu_56ac9bfbd9e29ba7.jpg 2400w, https://kveselkov.ru/posts/c-2025-lemon-a6/C%202025%20A6%20Lemmon.jpg 2560w" width="2560"&gt;&lt;/p&gt;&#10;&lt;p&gt;C/2025 A6 Lemmon&lt;br&gt;&#10;08.10.2025&lt;br&gt;&#10;Sky-Watcher P130650AZ + телефон&lt;br&gt;&#10;Сложение 6 кадров по 2 сек&lt;/p&gt;&#10;</description></item><item><title>Комета C/2020 F3 (NEOWISE)</title><link>https://kveselkov.ru/posts/c-2020-f3-neowise/</link><pubDate>Fri, 11 Jul 2025 12:00:00 +0400</pubDate><guid>https://kveselkov.ru/posts/c-2020-f3-neowise/</guid><description>&lt;p&gt;&lt;img alt="Комета C/2020 F3 (NEOWISE)" class="gallery-image" data-flex-basis="319px" data-flex-grow="133" height="1000" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://kveselkov.ru/posts/c-2020-f3-neowise/C%202020%20F3%20%28NEOWISE%29.jpg" srcset="https://kveselkov.ru/posts/c-2020-f3-neowise/C%202020%20F3%20%28NEOWISE%29_hu_2a71027e8d59714c.jpg 800w, https://kveselkov.ru/posts/c-2020-f3-neowise/C%202020%20F3%20%28NEOWISE%29.jpg 1333w" width="1333"&gt;&lt;/p&gt;&#10;&lt;p&gt;Комета C/2020 F3 (NEOWISE)&#10;11.07.2020&#10;телефон&lt;/p&gt;&#10;</description></item><item><title>Коммит в репозиторий языка Go</title><link>https://kveselkov.ru/posts/go-cr/</link><pubDate>Fri, 06 Apr 2018 12:09:41 +0400</pubDate><guid>https://kveselkov.ru/posts/go-cr/</guid><description>&lt;p&gt;Запустил линтер go-simple на репозиторий языка Go. Ради интереса отправил &lt;a class="link" href="https://go-review.googlesource.com/c/go/&amp;#43;/105195" target="_blank" rel="noopener"&#10; &gt;CR&lt;/a&gt; и Brad Fitzpatrick внезапно принял его. Ну пусть будет 🤷😀&lt;/p&gt;&#10;</description></item></channel></rss>