<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python on Заметки программиста</title><link>https://kveselkov.ru/tags/python/</link><description>Recent content in Python 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/tags/python/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></channel></rss>