Є на LinkedIn таке обмеження. Ліміт комерційного використання. Дуже ймовірно, що ви, як і я донедавна, ніколи не стикалися і не чули про нього.
Суть ліміту в тому, що якщо ви використовуєте пошук людей поза вашими контактами занадто часто (точних метрик немає, вирішує алгоритм, на основі ваших дій - як часто і багато шукали, додавали людей), то результат пошуку буде обмежений трьома профілями замість 1000 ( за промовчанням 100 сторінок, по 10 профілів на сторінку). Ліміт скидається на початку кожного місяця. Звичайно, преміум акаунти такого обмеження не мають.
Але нещодавно, для одного пет-проекту, я почав багато грати з пошуком на LinkedIn і раптово отримав це обмеження. Звичайно, таке мені не дуже сподобалося, адже я не використав його в будь-яких комерційних цілях, тому першою думкою було вивчити обмеження та спробувати його обійти.
[Важливе уточнення — матеріали у статті представлені виключно для ознайомлення та навчальних цілей. Автор не заохочує їх використання у комерційних цілях.]
Вивчаємо проблему
Маємо: замість десяти профілів з пагінацією, пошук видає лише три, після яких вставляється блок з "рекомендацією" преміум акаунту і нижче йдуть розмиті та неклікабельні профілі.
Відразу ж рука тягнеться в консоль розробника, щоб подивитися ці приховані профілі — можливо, ми можемо прибрати якісь стилі, що ставить блюр, або витягти інформацію з блоку в розмітці. Але, цілком очікувано, ці профілі лише картинки-заглушки і жодної інформації не зберігають.
Добре, тепер подивимося у вкладку Network і перевіримо, чи дійсно спрацьовує альтернативна видача результатів пошуку, яка повертає лише три профілі. Знаходимо цікавий для нас запит до “/api/search/blended” і дивимося на відповідь.
Профілі приходять у масиві `included`, але сутностей у ньому аж 15. У даному випадку, перші три з них - об'єкти з додатковою інформацією, кожен об'єкт містить інформацію по конкретному профілю (наприклад, профіль преміумом).
Наступні 12 – це реальні профілі – результати пошуку, з яких нам покажуть лише три. Як уже можна здогадатися, показує лише тих, на кого надходить додаткова інформація (перші три об'єкти). Наприклад, якщо взяти відповідь із профілю без ліміту, то прийде 28 сутностей - 10 об'єктів з дод. інформацією та 18 профілів.
Відповідь для профілю без ліміту
Чому профілів приходить більше 10, хоча вимагають саме 10, і вони ніяк не беруть участі у відображенні, навіть на наступній сторінці їх не буде - поки не знаю. Якщо проаналізувати урл запиту можна побачити, що count=10 (скільки профілів повернути у відповіді, максимум 49).
Радію будь-яким коментарям з цього приводу.
Експериментуємо
Добре, найголовніше ми тепер достеменно знаємо — профілів приходить у відповіді більше, ніж нам показують. Отже, ми можемо отримати більше даних, не дивлячись на ліміт. Спробуємо смикнути апі самі, прямо з консолі, за допомогою fetch.
Очікуємо, отримуємо помилку, 403. Це пов'язано з безпекою, тут ми не відсилаємо CSRF токен (CSRF на Вікіпедії. Якщо двома словами — до кожного запиту додається унікальний токен, який перевіряється на сервері на справжність).
Його можна скопіювати з будь-якого іншого успішного запиту або з cookies, де він зберігається в полі 'JSESSIONID'.
Де знайти токенЗаголовок іншого запиту:
Або з куки, прямо через консоль:
Пробуємо ще раз, на цей раз передаємо в fetch налаштування, в яких вказуємо параметром в header наш csrf-token.
Успіх нам приходять всі 10 профілів. :tada:
Через різницю заголовків структура відповіді трохи відрізняється від того, що приходить в оригінальному запиті. Можна отримати таку ж структуру, якщо додати 'Accept: 'application/vnd.linkedin.normalized+json+2.1', до нас в об'єкт, поруч із csrf токеном. Приклад відповіді з доданим заголовком
Далі можна редагувати (руками або автоматизувати) параметр `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, тому що ніякої загрози не несе. Максимум, це втрачений прибуток через подібні «обходи», що дозволяє не платити за преміум. Можливо, така відповідь сервера необхідна для коректної роботи інших частин сайту, або ж це просто ліньки розробників брак ресурсів, що не дозволяє зробити добре. (Обмеження з'явилося із січня 2015 року, до цього ліміту не було).
PS
Звичайно, код на jQuery є досить примітивним прикладом можливостей. Зараз я створив extension для браузера під свої потреби. Він додає кнопки контролю та рендерит повноцінні профілі з картинками, кнопкою запрошення та загальними коннектами. Плюс динамічно збирає фільтри локацій, компаній та іншого, дістає токен з куки. Тож нічого хардокдити вже не потрібно. Ну і додає додаткові поля налаштувань, а-ля «скільки профілів вимагати за раз, до 49».
Над цим доповненням я все ще працюю і планую викласти його у відкритий доступ. Пишіть, якщо вам цікаво.