Вебскрепінг, недобросовісні провайдери й неочевидні рішення для синхронізації

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт! Я Олег Денисенко, Software Architect ITOMYCH STUDIO. Темою моєї статті буде Web Scraping. Що таке вебскрепінг (від англійського scraping — «зішкрібання, зачищання»)? Це процес автоматизованого збору структурованих вебданих, простіше кажучи, отримання вебданих.

Основні випадки, де використовується вебскрепінг — це, наприклад, моніторинг та аналіз цін, моніторинг новин, генерація потенційних клієнтів та дослідження ринку тощо.

Де використовується вебскрепінг

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

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

А от ще був випадок

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

Штош © ¯\_(ツ)_/¯. Я взяв час для аналізу даних на сайті, дістав наявні логіни та паролі користувача, та пішов тиснути F12 у Chrome.

Попереднє дослідження показало, що сайт написано на ReactJS, backend — на NodeJS, запити йдуть через REST API. Більша частина (майже вся) API — зовсім не закрита, але для 3 endpoint (це з 20, sic!) тра AWS Cognito authentication.

Збірка коду для скрепінга

Настав час спробувати щось забрати до себе. Спочатку я вирішив, що напишу на Bash або на улюбленому Python купу скриптів, але зрозумів, що девелопери будуть робити новий бекенд на .NET, вирішив відразу намалювати консольний аплікейшн, де модельки респонсу можна буде переюзати в майбутньому WEB API.

Як я вже казав раніше, сайт працює на REST API, тому перше, що я додав до консольного аплікейшина — це RestEase та купа AWS-пакетів.

<ItemGroup>
    <PackageReference Include="AWSSDK.S3" Version="3.7.2.5" />
    <PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.1" />
    <PackageReference Include="Amazon.Extensions.CognitoAuthentication" Version="2.2.2" />
    <PackageReference Include="AWSSDK.CognitoIdentityProvider" Version="3.7.1.37" />
    <PackageReference Include="RestEase" Version="1.5.5" />
  </ItemGroup>

Далі зробив собі ApiFactory.

[Header("Authorization", "Bearer")]
public interface ICensoredProjectContract
{
}

   public class CensoredProjectApiFactory
    {
        private readonly HttpMessageHandler _handler;

        public CensoredProjectApiFactory(CensoredProjectConfig config = null)
        {
            _handler = new CensoredProjectHttpClientHandler(config);
        }

        public T GetClient<T>(Uri url)
            where T : ICensoredProjectContract
        {
            var httpClient = new HttpClient(_handler)
            {
                BaseAddress = url,
                Timeout = TimeSpan.FromSeconds(180),
            };
            return RestClient.For<T>(httpClient);
        }

        public T GetClient<T>(Uri url)
            where T : ICensoredProjectContract
        {
            var httpClient = new HttpClient(_handler)
            {
                BaseAddress = url,
                Timeout = TimeSpan.FromSeconds(180),
            };
            return RestClient.For<T>(httpClient);
        }

Додав «брудну» авторизацію у AWS Cognito.

private async Task<string> Login(string login, string password)
{
   using (var client = new HttpClient())
   {
      client.DefaultRequestHeaders.Add("X-Amz-Target", "AWSCognitoIdentityProviderService.InitiateAuth");
      client.BaseAddress = new Uri("https://cognito-idp.us-west-2.amazonaws.com");
      var json = "{\"AuthParameters\" : {\"USERNAME\" : \"" + login + "\", \"PASSWORD\" : \"" + password + "\"}, \"AuthFlow\" : \"USER_PASSWORD_AUTH\",\"ClientId\" : \"xxxxxxxxxxxxxxxxxxxxxxxxx\"}";
      var content = new StringContent(json, Encoding.UTF8, "application/x-amz-json-1.1");
      var result = await client.PostAsync("/", content);
      string resultContent = await result.Content.ReadAsStringAsync();
      JObject jsonObject = JObject.Parse(resultContent);
      return jsonObject["AuthenticationResult"]["AccessToken"].ToString();
   }
}

Після чого додав купу інтерфейсів на REST API сайту.

public interface IServiceUserApi : ICensoredProjectContract
{
   [Get("profiles/{userId}")]
   Task<Profiles> GetProfiles([Path] string userId);
}

public interface IServicePhotoApi: ICensoredProjectContract
{
   [Get("videos")]
   Task<List<Video>> GetVideos([Query] string userId);

   [Get("videos")]
   Task<List<Video>> GetVideos();

   [Get("photos")]
   Task<List<Photo>> GetPhotos([Query] string userId);

   [Get("albums")]
   Task<List<Album>> GetAlbums([Query] string userId);

   [Get("albums/{albumId}")]
   Task<FullAlbum> GetAlbum([Path] string albumId);
}

І спробував зробити перший запит до API.

Log.Debug($"Fetching user {userId} data..");
[....]

Log.Debug($"Profile");
var profile = await _userApi.GetProfiles(userId);
_data.AddProfiles(profile, userId);
_context.SaveChanges();

Так! Це працює!

Запуск вебскрепінга

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

Насамперед я стягнув довідники, тобто ті дані, які використовує система: наприклад, список валют, список штатів тощо.

Наступний етап — отримання даних по кожному юзеру, його профіль та купа інформації, яка має лежати за ним.

Відверто кажучи, на етапі датаскрепінга я робив усі моделі, як є — один одному з API response. Це було зроблено, щоб якнайскоріше дістати дані з сайту, бо замовник казав, що його вимкнуть через тиждень-два. На момент розробки нового проєкту я переробив майже всю архітектуру моделей та entities у базі.

Синхронізація медіафайлів — завдання з зірочкою

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

Усі оригінальні дані сайт зберігав у AWS S3, закритий через CloudFront. Якби в мене був логін до оригінального AWS, можна було б за п’ять хвилин зробити копію в себе...

aws s3 sync s3://source-old-site-bucket s3://my-new-site-bucket

... але ж ніт. Ми ж тут для того, щоби скрепінгувати дані, а не «оцевотвсе».

var photoApi = new CensoredProjectApiFactory().GetClient<IServicePhotoApi>(new Uri("https://service-photo.censored-sie.com"));

Дістаємо метадані через API. Далі найнеприємніше: ми маємо витягнути файл зі старого сайту, зберегти локально та знову залити його вже до нашого bucket-у. Ще одна проблема — відео зберігалися як потокові. Тобто, треба було спочатку забрати m3u8 файл...

using (WebClient localClient = new WebClient())
{
var m3u8 = await localClient.DownloadStringTaskAsync(item.HlsUrl);

... а далі взяти всі.ts файли і злити їх у єдиний. Бо згідно з умовами замовника файли мають бути не потоковими, а звичайними на кшталт mp4, а зверху ще й накладений вотермарк компанії.

var fileName = Path.Combine(tmpVideosFolder, $"{item.Id}.mp4");
await SaveVideo(url, fileName);
var s3Media = await UploadToS3(fileName, $"{userid}/Videos/{item.Id}.mp4", "video/mp4");

З огляду на те, що в нас було десь 500 профайлів, — це купа часу.

Щоби запустити «копіювання» відео даних з одного бакету до нашого й накласти вотермарк, ми написали просту AWS Lamda. Вона працювала дуже просто: коли файл потрапляв до s3 temp фолдеру, вмикався тригер і lambda починала опрацьовувати файл. Відверто кажучи, сама лямбда була на 10 рядків коду, один із яких був дуже «всратий» :) — це запуск ffmpeg.

Ви заціните:

args = f'/opt/ffmpeg -y -i {original_path} -i {watermark} -filter_complex [1]colorchannelmixer=aa=1.0,scale=iw*0.042:-1[wm];[0][wm]overlay=x=(main_w-overlay_w-120):y=(main_h-overlay_h-60),split=2[vid][img];[img]scale=min(800\\,iw):-1[img] -map [vid] -map 0:a? -codec:a copy -b:v 8192k -preset ultrafast -async 1 -movflags +faststart {output_video_path} -map [img] -frames:v 1 {output_thumb_path}'

Лямбда працює, скрепінг відео даних запущено. Доки йде процес, можна випити кави.

Чи це взагалі законно

Зверніть увагу, що скрейпінг процес проєкту був легальним, тому що права були саме в клієнта.

Та чи сканування інтернету та даних — легальне явище? Це окреме правове питання. Ставлення до легальності вебскрепінгу в різних країнах відрізняється. Деякі вебсайти навіть забороняють скрепінг у правилах використання, але юридичні наслідки такої заборони не є чіткими. А що ви про це думаєте?

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

У скрейпінгу відкритих даних немає нічого кримінального, хоча якщо ваш скрейпінг бот ddos-ить вебсайт, то це досить погано.

Ну ddos-ить сайт НБУ з курсом валют то такє :) а так взагалі якщо правильно написано сайт, та стоїть щось на кшталт nginx то най ddos-ять :)

Кто не делает API для своего сайта — получает скрапинг :)

ну я бачив проєкти які скрапять прайсі з сайтів. Тобто такий сам собі price.ua чи hotline.ua :)

Я думаю що скрапінг це гарна штука щоб почати заробляти новачку в IT. Завдань на фрілансі повно

З мого досвіду (займаюсь webscraping з 2015 року) ця теза — зараз вже неактуальна.
За декілька останніх років поріг входу в webscraping збільшився радикально з майже нуля до чогось ближче до рівня black hat хакерів.
Років 10 тому дійсно — в ті часи більшість сайтів можна було зіскрейпити маючи базові знання html, python і задати потрібний селектор xpath/css (антиботів, 302 на сторінку з captcha і. т.д. — тоді у широкому вжитку не було).
З року ~2017 сайти почали суттєво ускладнюватися, не в останню чергу завдяки поширенню React, Vue і інших javascript фреймворків, де дані на сторінках були вже не прямо в html тегах, а десь всередині script тегів в якійсь javascript змінній, або завантажувались динамічно.
З ~2019 — google зробив recaptca безкоштовним для використання і тепер captcha може з’явитись про спробі зіскрейпити навіть блог на безкоштовному хостингу (раніше використання captcha було дорогим дли тих, хто хотів зробити сайт недоступним для webscraping). В подальшому інші antibot рішення, націлені на захист сайтів від webscraping — стали більш поширеними, доступними і складнішими.
З 2020 з початком карантину кількість проектів/замовлень webscraping на фрилансі радикально зменшилась, а кількість бажаючись стати фрілансерами (в тому числі через webscraping) — значно збільшилась.
З 2020 і станом на зараз проект з категорії webscraping опублікований на Upwork отримує 50+ бажаючих/кандидатів за добу (ситуація не сприятлива для новачків в IT).

У нас в усіх топових інтернет магазинах дуже часто на один і той же товар встановлюється однакова ціна. Магія :-)

Це ймовірніше за все результат окремої угоди між виробником/постачальником товару і магазинами, де його продають. Наприклад, щось на кшталт такого:(взято з сайту, де продають акумулятори для авто)

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

тому що, безконтрольне «змагання цінами» магазинів між собою — не вигідне ні постачальникам/виробникам, ні магазинам.

Підписатись на коментарі