- Blind mode tutorial
lichess.org
Donate

Получение данных через API LiChess - конкретные примеры на Google Apps Script

Software DevelopmentTournamentLichessOff topic
Многие знания - многие печали...

Автор выражает благодарность создателям чата DeepSeek, без которого данная публикация не состоялась бы, а также моей дочери Александре, которая научила меня работать с этим чатом.

Эта публикация является продолжением моей предыдущей публикации "Как получить доступ к данным LiChess через API и автоматически обрабатывать их". Для погружения в тему рекомендуется начинать с неё.


Итак, у меня была написана программа (на Google Apps Script (GAS), то есть почти на JavaScript) и она работала. Программа обновляла данные игроков (рейтинги) и следила за результатами в партиях по переписке. Однако через какое-то время начались сбои выполнения, непонятные ошибки и др. Как оказалось в результате, причиной этого оказались следующие факторы:

- изменения, сделанные разработчиками API, в запросах и ответах;
- далеко неоптимальный код с моей стороны;
- переход GAS на среду выполнения V8, что может вызвать проблемы совместимости.
В итоге оказалось, что проще всё переписать с самого нуля, конечно опираясь на некоторые наработки.

Функция для запроса данных

Наиболее частой ошибкой при выполнении программы у меня было предупреждение "Exception: Адрес недоступен: https:// url". После этого происходил вылет, невозможно было даже определить код ответа. Следует подчеркнуть, что иногда программа срабатывала, вероятно причины крылись в доступе к серверам ЛиЧесс, а не в ошибках в коде. Конечно хотелось более стабильной работы: во-первых, избежать остановки работы программы, а во-вторых, иметь возможность повторно отправить запрос. Итогом стала такая функция.

function main(){
  //например запрос игр по их ID
  // необходимо предварительно составить список ID игр в виде строки, разделенной запятыми
  var strGames_ID;     
  var text = apiExportGamesByID(strGames_ID); 
}

function apiExportGamesByID(strGames_ID) {
  //https://lichess.org/api#tag/Games/operation/gamesExportIds  
  var url = "https://lichess.org/api/games/export/_ids?moves=false";  
  var options = {method : 'post', payload : strGames_ID, muteHttpExceptions: true};  
  var text;
  for(var i = 0; i<3; i++){    
    try{        
        var response = UrlFetchApp.fetch( url,options);      
        var respCode = response.getResponseCode();          
        if (respCode < 300) {         
            text = response.getContentText();        
            Logger.log(`Партии - код ${respCode}`);        
            return (text);
        }       
        else {
            text = false;        
            Logger.log(`Попытка No${i+1} - код ${respCode}`);      
        }
    }    
    catch{
          text = null; 
           Logger.log(`Попытка No${i+1} - партии не считались`);      
    }  
    Utilities.sleep(70000);  
  }
  return text;
}

Как видно из кода, если получен положительный ответ, то его код записывается в лог и возвращается текст ответа. Если код больше 300, то включаем паузу 70 секунд и повторяем попытку. То же самое делаем, если адрес обращения оказался недоступен (ветка catch). Всего делаем три попытки запроса (время выполнения гугл-скрипта ограничено шестью минутами). Здесь следует понимать, что по правилам доступа к API при получении кода ответа 429 (Too Many Requests («слишком много запросов»)) следует выдержать паузу не менее одной минуты перед следующим запросом. Если же код другой, например, 404, то, скорее всего, ошибка находится в вашем запросе (например, неправильно написаны options) и повторный запрос ничего не изменит. Анализируйте логи - при запуске со странички скрипта их видно сразу, а при запуске по триггеру или через макрос они находятся здесь, Apps Script/ Количество выполнений, разворачиваются по галочке справа:

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

Массовый запрос данных

Удобно, конечно делать запрос по одному игроку или игре - получил данные, обработал, вставил. Никаких лишних движений. Но если вам надо получить информацию по N игрокам и M играм, то это в конечном счете выливается в довольно большое число запросов. А если учесть, что на каждой стадии может случиться сбой (или не может, мы же написали функцию для обхода этой ошибки?)...
К счастью, существуют API-запросы, позволяющие за один запрос получить информацию по нескольким объектам. Например:

- Get members of a team;
- Get users by ID;
- Export games by IDs;
- Create a bulk pairing.

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


Запрос Get members of a team

Описание: https://lichess.org/api#tag/Teams/operation/teamIdUsers
Синтаксис:

var url = https://lichess.org/api/team/{teamId}/users;
или var url = https://lichess.org/api/team/{teamId}/users?full=true;
var options = { muteHttpExceptions: true
//авторизация требуется только для получения списка приватного клуба, т.е. обычно не требуется
};

Зачем нужно: предположим, вам нужно сделать проверку на принадлежность игрока клубу. Гораздо проще получить список игроков этого клуба и искать среди них игрока, чем получить список клубов этого игрока и искать среди них данный клуб. Это должно работать на клубах до 5000 человек (или 1000 человек с параметром "full=true"). Информация выдается в обратном хронологическом порядке (начинается с последнего игрока, присоединившегося к клубу, и заканчивается его основателем.
В чем разница в запросах? При запросе по первому варианту для игрока мы получаем только ключи "joinedTeamAt", "id", "name", "title", "patron". При запросе по второму варианту ("?full=true") мы получаем еще и данные рейтинга, например "perfs.correspondence.rating".

Таким образом, если нужно проверить, состоят ли игроки в клубе и обновить их рейтинг, достаточно одного запроса с url = https://lichess.org/api/team/{teamId}/users?full=true;

Важное отличие: В зависимости от используемого запроса, имя игрока хранится под разными ключами. При первом (коротком) запросе - под ключом "name", при втором запросе (с параметром "full=true") - под ключом "username". Конечно, это недоработка, если обратить внимание разработчиков, её, наверное, исправят.
Какие данные мы получаем: поскольку в функции, получающей данные, мы использовали метод getContentText(), то мы получаем текстовую строку, которая выглядит, как NDJSON, а именно "{данные JSON} /r(?) /n {данные JSON} /r(?) /n {данные JSON} /r(?) /n"
Обработка данных. Вариант 1.
Переводим строку text в настоящий массив, который будет в качестве элементов содержать объекты JSON. Парсим каждый элемент массива и извлекаем нужные данные. Этими данными заполняем массив arrData, который потом будем выводить на лист. Параллельно заполняем массив arrUserID данными id игроков. Этот массив понадобится нам позже при запросе Get users by ID.

    var arr = [];
    // строку переводим в массив, при этом исчезает разделитель '\r (0 или 1 раз) + \n'
    arr = text.split(/\r?\n/);
    //отфильтровываем пустые строки
    arr = arr.filter(item => item.trim() !== '');
    //это массив с данными, заполняем его и потом выводим на лист
    var arrData = [];
    // массив пользователей в виде ID для второго запроса
    var arrUserID = [];
    //парсим каждый элемент массива 
    for(var i in arr ){
      var objJSONData = JSON.parse(arr[i]);
      arrData[i] = [];
      //при коротком запросе - name, при длинном (?full=true) - username;
      arrData[i][0] = objJSONData.name;
      arrUserID.push(objJSONData.id);
      arrUser[i][1] = objJSONData.url;
      //эти данные можно получить при полном запросе (?full=true)
      //arrUser[i][2] = objJSONData.perfs.correspondence.rating;
      //arrUser[i][3] = objJSONData.perfs.bullet.rating;
      //arrUser[i][4] = objJSONData.perfs.blitz.rating;
      //arrUser[i][5] = objJSONData.perfs.rapid.rating;
    } 

Обработка данных. Вариант 2.
Переводим строку text в СТРОКУ, выглядящую, как массив "[{данные JSON},{данные JSON},{данные JSON}]". Каждый "элемент" такого "массива" также содержит объект JSON. Различие с первым случаем заключается в том, что теперь эту строку можно распарсить напрямую и получить при этом объект, который является массивом JSON.

// заменяем новые строки на запятые везде (/g)
    text = text.replace(/\r?\n/g,',');
    // в конце остается запятая, которая мешает - ее убрать
    text = text.slice(0,-1);
    //добавить символы "[" и "]"
    text = `[${text}]`;
    var arrData = [];
    var objJSONData = JSON.parse(text);
    for(var i in objJSONData){
      arrData[i] = [];
      arrData[i][0] = objJSONData[i].username;
      arrData[i][1] = objJSONData[i].perfs.correspondence.rating; 
    } 

Как мы увидим позже, очень похожий подход применяется в запросе Get users by ID.


Запрос Get users by ID

Описание: https://lichess.org/api#tag/Users/operation/apiUsers
Синтаксис:

var url = https://lichess.org/api/users;
//строка из ID пользователей, разделенных запятыми, например "thibault,maia1,maia5"
// в предыдущем примере, в варианте 1, мы получали массив из ID пользователей. Используем его сейчас.
var strPl_ID = String(arrUserID);
var options = {  method : 'post',  payload : strPl_ID,  muteHttpExceptions: true  };

Зачем нужно: если вы составляете табличку с данными игроков, то это запрос несет массу ценной информации про каждого из них. Информация выдается согласно порядку ID. Ключи смотрите в описании запроса и во фрагменте кода ниже. Мы получаем данные в виде текстовой строки, которая выглядит, как массив "[{данные JSON},{данные JSON},{данные JSON}]". Таким образом, случай аналогичен ранее рассмотренному Get members of a team. Вариант 2, за исключением того, что со строкой не надо проводит никаких предварительных преобразований.

objJSONData = JSON.parse(text);
for(var i in objJSONData){
   arrData[i][1] = objJSONData[i].perfs.correspondence.rating;
   arrData[i][2] = objJSONData[i].perfs.bullet.rating;
   arrData[i][3] = objJSONData[i].perfs.blitz.rating;
   arrData[i][4] = objJSONData[i].perfs.rapid.rating;
}  

Аналогично можно получить данные профиля, например, пишем arrData[i][5] = objJSONData[i].profile.realName и ... получаем вместо имени вылет, если у игрока не заполнена эта позиция в профиле. Здесь имеет значение среда выполнения, в старой версии у переменной было бы значение undefined, в версии V8 предлагаю следующий код (вставить внутри for в предыдущем примере:

if ( objJSONData[i].profile) {
  if (objJSONData[i].profile.realName){arrUser[i][5] = objJSONData[i].profile.realName;}
  else{arrUser[i][5] = '';}
  if (objJSONData[i].profile.links){ arrUser[i][6] = objJSONData[i].profile.links;}
  else { arrUser[i][6] = ''; }
  if (objJSONData[i].profile.flag){ arrUser[i][7] = objJSONData[i].profile.flag;}
  else { arrUser[i][7] = ''; }
  if (objJSONData[i].profile.location){ arrUser[i][8] = objJSONData[i].profile.location;}
  else { arrUser[i][8] = ''; }
}
else{arrUser[i][5] = arrUser[i][6] = arrUser[i][7] = arrUser[i][8] = '';}
if ( objJSONData[i].title) { arrUser[i][9] = objJSONData[i].title; }
else { arrUser[i][9] = ''; }

Также ценную информацию могут дать поля "seenAt" (последнее посещение), "createdAt" (регистрация на сайте), "joinedTeamAt" (регистрация в клубе, из запроса Get members of a team).

Таким образом, если вы хотите получить всю доступную информацию про игроков в клубе, необходимо провести запросы с url = https://lichess.org/api/team/{teamId}/users
и url = https://lichess.org/api/users


Запрос Export games by IDs

Описание: https://lichess.org/api#tag/Games/operation/gamesExportIds
Синтаксис:

// параметр "moves=false" означает, что в выдачу не надо включать сами ходы из партии, нам достаточно только заголовков
var url = "https://lichess.org/api/games/export/_ids?moves=false";
//строка из ID игр, разделенных запятыми, например "TJxUmbWK,4OtIh2oh,ILwozzRZ"
var strGames_ID;
var options = {method : 'post', payload : strGames_ID, muteHttpExceptions: true};

Зачем нужно: для мониторинга прохождения партий (как правило, заочных) и выставления результатов. Откуда взять ID игр - считать из таблички, в которой должны содержаться имена игроков, их цвет, ссылка на партию и результат этой партии.
Как получить ссылку на партию? В первой версии мой скрипт находил партии между двумя заданными игроками с ограничениями в виде даты начала партии и вида шахмат, и первую партию, удовлетворяющую условиям, записывал в таблицу. Однако, после того, как разработчики доработали API, гораздо удобнее оказалось получить список партий, созданных при массовом старте (об этом позднее) и записать в таблицу их. Пока будем считать, что у вас уже есть таблица с полными ссылками на партии всех игроков. При добавлении ID партии в список необходимо проверить, что 1) результат партии не определен, 2) партия уже не находится в списке.
Обработка данных: при данной форме запроса мы получаем строку, содержащую для каждой партии шапку вида (формат PGN):
[Event "Rated correspondence game"]
[Site "https://lichess.org/ozSuylv7"]
[Date "2025.01.04"]
[White "Tamik92"]
[Black "Alexander_64483"]
[Result "1-0"]
[GameId "ozSuylv7"]
С использованием заголовков можно получить вывод в виде NDJSON, но мне кажется, что в такой строке искать даже проще. Каждая новая партия начинается с тега "[Event". Когда мы разрезаем строку, например, начиная с подстроки '[White " ' и заканчивая первым вхождением после нее подстроки ' " ', получаем имя игрока белыми. И т.п.

var arrRunningGames = [];
  var i=0;
  while( text.indexOf("[Event") != -1){
    text= text.slice( text.indexOf("[Event") + "[Event".length  );
    //записываем в массив для вывода только игры с определившимся результатом
    if( cutString (text, '[Result "', '"') != "*" ){
      arrRunningGames[i] = new Array[];
      //ссылка на игру
      arrRunningGames[i][0] = cutString (text, '[Site "', '"');
      // игрок белыми   
      arrRunningGames[i][1] = cutString (text, '[White "', '"');
      // игрок черными
      arrRunningGames[i][2] = cutString (text, '[Black "', '"');
      // результат
      arrRunningGames[i][3] = cutString (text, '[Result "', '"');
      i=i+1;
    }
 }

function cutString (startString, startElem, endElem){
  var i = startString.indexOf(startElem);
  var cuted;
  if(i != -1){
      var promString = startString.slice(i + startElem.length);
      var j = promString.indexOf(endElem);
      if(j != -1){cuted = promString.substr(0,j);}
      else {cuted = "";}
  }
  else {cuted = "";}
  return cuted;
}

Информация выдается в порядке создания игр, то есть в непредсказуемом порядке при массовом старте. Придется сверять найденную ссылку на игру со ссылками, записанными в таблице, и в зависимости от цвета игрока записывать результат (1-0, 1/2-1/2, 0-1) в виде очков.

Таким образом, за один запрос мы проверяем результаты всех неоконченных игр url = "https://lichess.org/api/games/export/_ids?moves=false"


Запрос Create a bulk pairing

Описание: https://lichess.org/api#tag/Bulk-pairings/operation/bulkPairingCreate
Синтаксис:

var url = "https://lichess.org/api/bulk-pairing";
//Количество дней на ход, только для переписки
var Days = 3;
var allBulks = "token1:token2,token3:token4,token5:token6";
// Произвольное сообщение, может содержать поля {opponent} и {game}, которые заполняются сайтом автоматически
//по дефолту "Your game with {opponent} is ready: {game}."
var tournamentMessage = "Your game in Название турнира with {opponent} is ready: {game}."
var options = {
    'method' : 'post',
    'headers' : {Authorization: 'Bearer ' + myToken },
// партии рейтинговые rated=true, вариант классические variant=standard или Фишера (=chess960) 
// сейчас недоступны рейтинговые партии в шахматы Фишера по переписке, только без рейтинга    
'payload' : `rated=true&variant=standard&days=${Days}&players=${allBulks}&message=${tournamentMessage}`
  };

Зачем нужно: Создает массовый старт партий, например для турнира заочных шахмат (но необязательно заочных). Возвращает объект JSON, содержащий, в том числе, id созданных игр и имена игроков.
Обработка данных: запрос создает заданное количество партий (если соблюдены все условия для этого) и возвращает объект JSON, который далее анализируем стандартными методами. Данные сохраняем в виде массива [ [игрок белыми, игрок черными, id партии],]. Дальше находим совпадения с игроками и заполняем клетку в таблице ссылкой на партию (или ее id).

var response = UrlFetchApp.fetch(link, options);
var objJSONData = JSON.parse(response);
// и далее анализ этого объекта (возможно в другой функции)
var objGames = {};
objGames = objJSONData.games;
var games = [];
var white = [];
var black = [];
var id = [];
for(var i in objGames){
  white[i] = objGames[i].white;
  black[i] = objGames[i].black;
  id[i] = objGames[i].id;
  games[i] = [white[i], black[i], id[i] ];
}

for (var item of games ){
  //здесь надо перебрать игроков из вашей таблички (правильно расставить индексы) 
  for(var i = 0;i<lastRow;i++){
      var name =   ;
      var color =   ;
      var pl2 =     ;
      var idGame = ;
      //имя игрока - игрок белыми и цвет = белый  
      if( item[0] == name && color == "w" && item[1] == pl2){
        link = `https://lichess.org/${idGame}`;
      }
      //имя игрока - игрок черными и цвет - черный
      else if(item[1] == name && color == "b" && item[0] == pl2){
          link = `https://lichess.org/${idGame}`;
      }
  }
}

Таким образом, проанализировав ответ на запрос var url = "https://lichess.org/api/bulk-pairing"; мы получаем id всех созданных партий, а также игроков и их цвет.

Удачи вам в изучении API. По вопросам его работы (или неполадок) связывайтесь с разработчиками - https://discord.gg/lichess#lichess-api-support .