Обходимо ліміт пошуку LinkedIn, граючи з API

ліміт

Є на LinkedIn таке обмеження. Ліміт комерційного використання. Дуже ймовірно, що ви, як і я донедавна, ніколи не стикалися і не чули про нього.

Обходимо ліміт пошуку LinkedIn, граючи з API

Суть ліміту в тому, що якщо ви використовуєте пошук людей поза вашими контактами занадто часто (точних метрик немає, вирішує алгоритм, на основі ваших дій - як часто і багато шукали, додавали людей), то результат пошуку буде обмежений трьома профілями замість 1000 ( за промовчанням 100 сторінок, по 10 профілів на сторінку). Ліміт скидається на початку кожного місяця. Звичайно, преміум акаунти такого обмеження не мають.

Але нещодавно, для одного пет-проекту, я почав багато грати з пошуком на LinkedIn і раптово отримав це обмеження. Звичайно, таке мені не дуже сподобалося, адже я не використав його в будь-яких комерційних цілях, тому першою думкою було вивчити обмеження та спробувати його обійти.

[Важливе уточнення — матеріали у статті представлені виключно для ознайомлення та навчальних цілей. Автор не заохочує їх використання у комерційних цілях.]

Вивчаємо проблему

Маємо: замість десяти профілів з пагінацією, пошук видає лише три, після яких вставляється блок з "рекомендацією" преміум акаунту і нижче йдуть розмиті та неклікабельні профілі.

Відразу ж рука тягнеться в консоль розробника, щоб подивитися ці приховані профілі — можливо, ми можемо прибрати якісь стилі, що ставить блюр, або витягти інформацію з блоку в розмітці. Але, цілком очікувано, ці профілі лише картинки-заглушки і жодної інформації не зберігають.

Обходимо ліміт пошуку LinkedIn, граючи з API

Добре, тепер подивимося у вкладку Network і перевіримо, чи дійсно спрацьовує альтернативна видача результатів пошуку, яка повертає лише три профілі. Знаходимо цікавий для нас запит до “/api/search/blended” і дивимося на відповідь.

Обходимо ліміт пошуку LinkedIn, граючи з API

Профілі приходять у масиві `included`, але сутностей у ньому аж 15. У даному випадку, перші три з них - об'єкти з додатковою інформацією, кожен об'єкт містить інформацію по конкретному профілю (наприклад, профіль преміумом).

Обходимо ліміт пошуку LinkedIn, граючи з API

Наступні 12 – це реальні профілі – результати пошуку, з яких нам покажуть лише три. Як уже можна здогадатися, показує лише тих, на кого надходить додаткова інформація (перші три об'єкти). Наприклад, якщо взяти відповідь із профілю без ліміту, то прийде 28 сутностей - 10 об'єктів з дод. інформацією та 18 профілів.

Відповідь для профілю без лімітуОбходимо ліміт пошуку LinkedIn, граючи з API
Обходимо ліміт пошуку LinkedIn, граючи з API

Чому профілів приходить більше 10, хоча вимагають саме 10, і вони ніяк не беруть участі у відображенні, навіть на наступній сторінці їх не буде - поки не знаю. Якщо проаналізувати урл запиту можна побачити, що count=10 (скільки профілів повернути у відповіді, максимум 49).

Обходимо ліміт пошуку LinkedIn, граючи з API

Радію будь-яким коментарям з цього приводу.

Експериментуємо

Добре, найголовніше ми тепер достеменно знаємо — профілів приходить у відповіді більше, ніж нам показують. Отже, ми можемо отримати більше даних, не дивлячись на ліміт. Спробуємо смикнути апі самі, прямо з консолі, за допомогою fetch.

Обходимо ліміт пошуку LinkedIn, граючи з API

Очікуємо, отримуємо помилку, 403. Це пов'язано з безпекою, тут ми не відсилаємо CSRF токен (CSRF на Вікіпедії. Якщо двома словами — до кожного запиту додається унікальний токен, який перевіряється на сервері на справжність).

Обходимо ліміт пошуку LinkedIn, граючи з API

Його можна скопіювати з будь-якого іншого успішного запиту або з cookies, де він зберігається в полі 'JSESSIONID'.

Де знайти токенЗаголовок іншого запиту:

Обходимо ліміт пошуку LinkedIn, граючи з API

Або з куки, прямо через консоль:

Обходимо ліміт пошуку LinkedIn, граючи з API

Пробуємо ще раз, на цей раз передаємо в fetch налаштування, в яких вказуємо параметром в header наш csrf-token.

Обходимо ліміт пошуку LinkedIn, граючи з API

Успіх нам приходять всі 10 профілів. :tada:

Через різницю заголовків структура відповіді трохи відрізняється від того, що приходить в оригінальному запиті. Можна отримати таку ж структуру, якщо додати 'Accept: 'application/vnd.linkedin.normalized+json+2.1', до нас в об'єкт, поруч із csrf токеном.
Приклад відповіді з доданим заголовкомОбходимо ліміт пошуку LinkedIn, граючи з API

Більше про заголовок Accept

Що далі?

Далі можна редагувати (руками або автоматизувати) параметр `start`, що вказує на індекс, починаючи з якого нам віддадуть 10 профілів (за замовчуванням = 0) зі всього результату пошуку. Інакше кажучи, інкрементуючи його на 10 після кожного запиту, у нас виходить звичайна посторінкова видача по 10 профілів за раз.

На цьому етапі я мав достатньо даних та свободи, щоб продовжувати роботу над пет-проектом. Але гріх було не спробувати ці дані відобразити прямо на місці, якщо вони на руках. У Ember, який використовується на фронті, не лізтимемо. На сайті був підключений jQuery і відкопавши в пам'яті знання базового синтаксису, можна за пару хвилин створити наступне.

Код на jQuery

/* рендер блока, принимаем данные профиля и вставляем блок в список профилей используя эти данные */
const  createProfileBlock = ({ headline, publicIdentifier, subline, title }) => {
    $('.search-results__list').append(
        `<li class="search-result search-result__occluded-item ember-view">
            <div class="search-entity search-result search-result--person search-result--occlusion-enabled ember-view">
                <div class="search-result__wrapper">
                    <div class="search-result__image-wrapper">
                        <a class="search-result__result-link ember-view" href="/uk/in/${publicIdentifier}/">
                            <figure class="search-result__image">
                                <div class="ivm-image-view-model ember-view">
                                    <img class="lazy-image ivm-view-attr__img--centered EntityPhoto-circle-4  presence-entity__image EntityPhoto-circle-4 loaded" src="http://www.userlogos.org/files/logos/give/Habrahabr3.png" />
                                </div>
                            </figure>
                        </a>
                    </div>
                    
                    <div class="search-result__info pt3 pb4 ph0">
                        <a class="search-result__result-link ember-view" href="/uk/in/${publicIdentifier}/">
                            <h3 class="actor-name-with-distance search-result__title single-line-truncate ember-view">
                                ${title.text}
                            </h3>
                        </a>

                        <p class="subline-level-1 t-14 t-black t-normal search-result__truncate">${headline.text}</p>

                        <p class="subline-level-2 t-12 t-black--light t-normal search-result__truncate">${subline.text}</p>
                    </div>
                </div>
            </div>
        <li>`
    );
};

// дергаем апи, получаем данные и рендерим профили
const fetchProfiles = () => {
    // токен
   const csrf = 'ajax:9082932176494192209';
    
   // объект с настройками запроса, передаем токен
   const settings = { headers: { 'csrf-token': csrf } }

    // урл запроса, с динамическим индексом старта в конце
   const url = `https://www.linkedin.com/voyager/api/search/blended?count=10&filters=List(geoRegion-%3Ejp%3A0,network-%3ES,resultType-%3EPEOPLE)&origin=FACETED_SEARCH&q=all&queryContext=List(spellCorrectionEnabled-%3Etrue,relatedSearchesEnabled-%3Etrue)&start=${nextItemIndex}`; 
    /* делаем запрос, для каждого профиля в ответе вызываем рендер блока, и после инкрементируем стартовый индекс на 10 */
    fetch(url, settings).then(response => response.json()).then(data => {
        data.elements[0].elements.forEach(createProfileBlock);
        nextItemIndex += 10;
});
};


// удаляем все профили из списка
$('.search-results__list').find('li').remove();
// вставляем кнопку загрузки профилей
$('.search-results__list').after('<button id="load-more">Load More</button>');
// добавляем функционал на кнопку
$('#load-more').addClass('artdeco-button').on('click', fetchProfiles);

// ставим по умолчания индекс профиля для запроса
window.nextItemIndex = 0;

Якщо виконати це прямо в консолі на сторінці пошуку, то це додасть кнопку, що завантажує 10 нових профілів при кожному натисканні, і що їх орендує списком. Звичайно, токен та урл перед цим поміняти на необхідний. Блок профілю міститиме ім'я, посаду, локацію, посилання на профіль та картинку-заглушку.

Обходимо ліміт пошуку LinkedIn, граючи з API

Висновок

Таким чином, при мінімумі зусиль ми змогли знайти вразливе місце і повернути собі пошук без обмежень. Достатньо було проаналізувати дані та їхній шлях, заглянути в сам запит.

Я не можу сказати, що це є серйозною проблемою для LinkedIn, тому що ніякої загрози не несе. Максимум, це втрачений прибуток через подібні «обходи», що дозволяє не платити за преміум. Можливо, така відповідь сервера необхідна для коректної роботи інших частин сайту, або ж це просто ліньки розробників брак ресурсів, що не дозволяє зробити добре. (Обмеження з'явилося із січня 2015 року, до цього ліміту не було).

PS

Звичайно, код на jQuery є досить примітивним прикладом можливостей. Зараз я створив extension для браузера під свої потреби. Він додає кнопки контролю та рендерит повноцінні профілі з картинками, кнопкою запрошення та загальними коннектами. Плюс динамічно збирає фільтри локацій, компаній та іншого, дістає токен з куки. Тож нічого хардокдити вже не потрібно. Ну і додає додаткові поля налаштувань, а-ля «скільки профілів вимагати за раз, до 49».

Обходимо ліміт пошуку LinkedIn, граючи з API

Над цим доповненням я все ще працюю і планую викласти його у відкритий доступ. Пишіть, якщо вам цікаво.

Джерело: habr.com

Додати коментар або відгук