Последние годы увлекаюсь любительскими соревнованиями и существует проблема с поиском близлежащих соревнований - есть несколько разных сайтов которые публикуют анонсы организаторов и на этих сайтах обычно все дисциплины вперемешку и никогда нет даже общей карты, в каких городах проводятся эти соревнования.
Например, я живу в Перми и хочу поучаствовать на следующих выходных в какой нибудь активности, например, побегать, прокатиться на велосипеде или поплавать. Ввожу в поиске «Пермь», но в результатах выдачи ничего нет. Хотя есть соревнование в посёлке Юг, который находится меньше часа езды от Перми. Но раз я ищу «Пермь», то «Юг» понятное дело в списке не выводится...
Вместе с Александром Ивановым, одним из авторов телеграм -канала "Таблицы Гугл", мы решили разобраться с этой проблемой техническим способом - написать Apps Script, который обойдёт общий список соревнований на каждом из сайтов, где публикуются анонсы и соберет информацию о названии соревнования, дате и и городе, где проводится соревнование. А потом соберёт сводную таблицу предстоящих соревнований и, в идеале, отобразит эти точке на карте - так любитель сразу может понять, где проводятся ближайшие соревнования, какая у них дата и куда можно поехать.
По итогу получилось не совсем так, как планировалось, но это может стать отправной точкой к собственным поискам.
К тому же любительские соревнования это в первую очередь не сам спорт, а укрепление здоровья и забота о себе - по крайней мере, для меня. Я не фокусируюсь на спортивных достижениях, а участвую ради удовольствия, физической активности и возможности побывать в новых местах.
![Скриншот таблицы amateur_sports_competitions на 01.07.2024 Скриншот таблицы amateur_sports_competitions на 01.07.2024](https://habrastorage.org/getpro/habr/upload_files/01a/59f/249/01a59f249cb9c37e782944c3d32bc867.png)
Проблемы с поиском любительских соревнований
Проблема с поиском на самом деле существует - уже несколько лет в теме и последний год занимаюсь вместо живого тренера под руководством ChatGPT. Но когда я хочу найти какое-нибудь соревнование по бегу, велосипедам или плаванию, то информация об этом разбросана по нескольким сайтам, что очень неудобно.
Например, у меня есть свободные выходные и приходится тратить много времени на изучение разных сайтов, например:
И всё потому, что нет какого-то централизованного источника по всем дисциплинам. Мало того, что сайтов достаточно много, так ещё и функции поиска на этих сайтах неэффективны.
Например, я могу захотеть съездить в другой город, но больше 6 часов на машине я не поеду. Или даже могу слетать куда-нибудь, чтобы побывать в Санкт-Петербурге. То есть интерес затрагивает в целом все города России.
Но особенно обидно, когда понимаешь что было время и пропустил какую-нибудь близлежащую локацию из-за требований точного совпадения города в поиске на всех сайтах. Набираю «Пермь», а соревнование было в посёлке Юг.
![Маршрут из Перми до Юга Маршрут из Перми до Юга](https://habrastorage.org/getpro/habr/upload_files/c21/daa/d31/c21daad31eaaef8280badd1a0d2648b6.png)
Предлагаемое решение
Ребята из телеграм -канала "Таблицы Гугл" создали простой Google Apps Script, который автоматизирует процесс сбора данных о событиях с определенных сайтов и обновляет Google Таблицу с этой информацией.
Решение доступно по ссылке: amateur_sports_competitions.
Как это работает?
Этот скрипт Google Apps автоматизирует процесс парсинга и сохраняет данные об соревнованиях с трех конкретных самых популярных веб-сайтов в соответствующие вкладки гугл таблицы.
Функция run
запускает функции сбора данных для iron-star.com
, myrace.info
и russiarunning.com
и гарантирует, что документ Google Sheets будет заполнен актуальной информацией о соревнованиях из этих источников данных.
Этот код состоит из трех основных функций, каждая из которых посвящена конкретному сайту, и центральной функции для координации этих задач по автоматическому извлечению данных.
Вот подробное описание того, что делает каждая часть кода:
1. Манифест (файл appsscript.json):
Этот файл устанавливает в проекте следующие настройки:
- Устанавливается часовой пояс Москва.
- Добавляет библиотеку Cheerio для анализа HTML.
- Включает регистрацию исключений через Stackdriver.
- Для сценария выполнения используется среда V8.
{
"timeZone": "Europe/Moscow",
"dependencies": {
"libraries": [
{
"userSymbol": "Cheerio",
"version": "14",
"libraryId": "1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
2. Основная функция (файл 0.index.gs):
Основная функция run
служит точкой входа, которая последовательно вызывает три другие функции для извлечение данных с сайтов:
userActionScrapeMyraceInfo
userActionScrapeIronStarCom
userActionScrapeRussiarunningCom
.
function run() {
userActionScrapeMyraceInfo();
userActionScrapeIronStarCom();
userActionScrapeRussiarunningCom();
}
3. Парсинг сайта iron-star.com (файл iron-star.com.gs):
Сайт IRONSTAR TRIATHLON, это крупнейшая в России серия соревнований по триатлону.
![Автор на соревнованиях Ironstar Sprint Ekaterinburg Автор на соревнованиях Ironstar Sprint Ekaterinburg](https://habrastorage.org/getpro/habr/upload_files/f79/7a9/f5d/f797a9f5d35e95e721eb9914a1e74767.jpg)
Эта функция парсит информацию о соревнованиях с сайта iron-star.com. Она извлекает содержимое веб-страницы, анализирует его с помощью Cheerio, извлекает URL-адреса событий, даты, имена и города, а затем записывает данные в документ Google Sheets на вкладку с именем iron-star.com.
function userActionScrapeIronStarCom() {
console.log('userActionScrapeIronStarCom', 'start');
// Получаем новые ссылки
const url = 'https://iron-star.com/event/';
const contentList = UrlFetchApp.fetch(url).getContentText();
const $ = Cheerio.load(contentList);
const itemList = $('.event-item-wrap a.event-item');
const values = [];
itemList.map((_, item) => {
const $item = Cheerio.load(item);
const date = $item('.date').text().trim();
const name = $item('.title').text().trim();
const city = $item('.place').text().trim();
const url = `https://iron-star.com${$(item).attr('href')}`;
values.push([url, date, name, city]);
});
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book.getSheetByName('iron-star.com');
sheet.getRange(3, 1, sheet.getMaxRows() - 1, sheet.getMaxColumns()).clearContent();
if (values.length) {
sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
} else {
sheet.getRange('2:2').clearContent();
}
console.log('userActionScrapeIronStarCom', 'finish');
}
4. Парсинг сайта myrace.info (файл myrace.info.gs):
Сайт MyRace это площадка для регистрации на спортивные события в России.
![Автор на соревнованиях X-Waters Saint Petersburg. Заплыв вокруг Елагина Автор на соревнованиях X-Waters Saint Petersburg. Заплыв вокруг Елагина](https://habrastorage.org/getpro/habr/upload_files/b68/517/200/b685172008f29bacd93f5747c19b18e6.jpg)
Аналогично предыдущей функции, эта функция собирает информацию о соревнованиях с myrace.info. Она извлекает содержимое веб-страницы, анализирует его с помощью Cheerio, извлекает URL-адреса событий, даты, имена и города, а затем записывает данные в документ Google Sheets на вкладку с именем myrace.info.
function userActionScrapeMyraceInfo() {
console.log('userActionScrapeMyraceInfo', 'start');
// Получаем новые ссылки
const url = 'https://myrace.info/take/';
const contentList = UrlFetchApp.fetch(url).getContentText();
const $ = Cheerio.load(contentList);
const itemList = $('.container-content.centered > a.events-list__item.row');
const values = [];
itemList.map((_, item) => {
const $item = Cheerio.load(item);
const date = $item('.date')
.text()
.replace(/(\d+)-(\d)+/, '$1');
const name = $item('h2').clone().children().remove().end().text().trim();
const city = $item('.flag').text().trim();
const url = `https://myrace.info${$(item).attr('href')}`;
values.push([url, date, name, city]);
});
const book = SpreadsheetApp.getActive();
const sheet = book.getSheetByName('myrace.info');
sheet.getRange(3, 1, sheet.getMaxRows() - 1, sheet.getMaxColumns()).clearContent();
if (values.length) {
sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
} else {
sheet.getRange('2:2').clearContent();
}
console.log('userActionScrapeMyraceInfo', 'finish');
}
5. Парсинг russiarunning.com (файл russiarunning.com.gs):
Сайт RussiaRunning — это крупнейшая платформа для регистрации на спортивные события в России. Она объединяет организаторов спортивных событий и участников соревнований. На сайте можно выбрать событие, зарегистрироваться на него, оплатить участие и получить подтверждение.
![Автор на Пермском марафоне Автор на Пермском марафоне](https://habrastorage.org/getpro/habr/upload_files/279/1d9/474/2791d9474745bd42ba1f87d96a22e15c.jpg)
Эта функция собирает информацию о соревнованиях с russiarunning.com
и работает по другому, отправляя POST-запрос к API сайта. Она получает информацию о событии в формате JSON, извлекает соответствующие детали (URL-адреса, даты, имена и места) и записывает данные в документ Google Sheets на вкладку с именем russiarunning.com.
function userActionScrapeRussiarunningCom() {
console.log('userActionScrapeRussiarunningCom', 'start');
const url = 'https://russiarunning.com/api/events/list/ru';
const payload = {
Take: 500,
DateFrom: new Date().toISOString().split('T')[0],
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
const data = UrlFetchApp.fetch(url, options);
const items = JSON.parse(data.getContentText()).Items;
const values = items.map((item) => {
const { c, d, t, p } = item;
const link = `https://russiarunning.com/event/${c}/`;
const date = d.split('T')[0];
const row = [link, date, t, p];
return row;
});
const book = SpreadsheetApp.getActive();
const sheet = book.getSheetByName('russiarunning.com');
sheet.getRange(3, 1, sheet.getMaxRows() - 1, sheet.getMaxColumns()).clearContent();
if (values.length) {
sheet.getRange(2, 1, values.length, values[0].length).setValues(values);
} else {
sheet.getRange('2:2').clearContent();
}
console.log('userActionScrapeRussiarunningCom', 'finish');
}
Что в итоге?
После парсинга трёх сайтов через QUERY запрос делается выборка всех предстоящих соревнований от сегодняшнего дня вперёд в будущее. QUERY запрос нужен чтобы отсечь предыдущие даты - ведь при парсинге некоторые сайты отдают и предыдущие соревнования.
![Скриншот таблицы amateur_sports_competitions на 29.06.2024 Скриншот таблицы amateur_sports_competitions на 29.06.2024](https://habrastorage.org/getpro/habr/upload_files/021/0a0/4ee/0210a04eec0d3ad247acd579ef7b7d41.png)
Эта формула в гугл таблицах объединяет данные о соревнованиях из трех разных листов, фильтрует их, чтобы включить только будущие события (или события, происходящие сегодня), и сортирует эти события по дате в порядке возрастания:
=QUERY(
{
myrace.info!A:D;
'iron-star.com'!A:D;
russiarunning.com!A:D
};
"SELECT *
WHERE Col2 >= date '"&text(today();"yyyy-MM-dd")&"'
Order by Col2 asc
")
Что не получилось сделать
Идея сделать подобную таблицу пришла ещё в середине мае, но работа была закончена только сейчас и перед вами уже вторая версия кода - первый был гораздо более сложный и по другому осуществлял процесс парсинга.
Важную часть идею - не только собрать, но и отобразить места на карте - решили перенести во вторую часть этой статьи (работа над которой сейчас идёт), потому что столкнулись с тем, что не всегда указаны места проведения и не всегда они указаны четко: например указан какой-то посёлок, но в какой он находится области - это непонятно.
⚠️ Как пользоваться для самых далёких от программирования
По ссылке ниже откроется ваша копия таблицы. Если в браузере выполнен вход в разные гугл-аккаунты, то нужно оставить только один, иначе при переходе по ссылке может появиться ошибка. По ссылке Вам предложат создать свою копию таблицы - создайте копию в свой аккаунт:
➡️amateur_sports_competitions⬅️
Дальше откройте редактор сценариев Google Apps:
– В Google Таблице выберите «Расширения» > «Apps Script».
– Редактор скриптов Google Apps откроется на новой вкладке.
Теперь, когда у вас открыт редактор сценариев приложений запустите функцию под названием run()
. Чтобы её запустить нажмите кнопку воспроизведения (значок треугольника).
При первом запуске скрипту необходимо авторизоваться для работы. Для этого потребуется авторизация в вашем аккаунте. Полный процесс запуска можно изучить по статье открываем редактор скриптов в Гугл таблице. Не бойтесь, что скрипт как-то повредит вам - в любой момент вы можете посмотреть список выданных вами разрешений на специальной странице и в один клик их отозвать. В целом, вам не стоит беспокоиться, если это ваша Таблица и ваш код, так как все права администрирования остаются только за вами.
Гугл предупредит, что приложение не проверено. Нужно выбрать «Дополнительные настройки», а потом перейти по нижней ссылке. После этого останется нажать на кнопку «Разрешить», и на этом первоначальная настройка будет готова.
Выполнив эти шаги, вы сможете успешно запустить скрипт поиска соревнований по всей России - он обновит уже имеющиеся данные в вашей копии таблицы.
Заключение
Проведена большая работа по созданию сводной таблицы по самым популярным площадкам, которые публикуют информацию о любительских соревнованиях.
Вы всегда можете сделать собственную копию данной таблицы и пользоваться ей - она автоматически скачает для вас все соревнования и представит их в удобном табличном виде.
Репозиторий программы: googlesheets-ru/amateur_sports_competitions
Код создан командой телеграм-канала «Таблицы Гугл»
Автор текста и идеи: Михаил Шардин
1 июля 2024 г.