Недавно я описував задачу, згідно якої потрібно було в багатопотоковому режимі генерувати QR коди. І от, вирішив написати це рішення, щоб воно вже було під руками у випадку необхідності. Розкажу як це було і як виглядає код.
Перед тим як писати код я сформулював і описав в текстовому вигляді завдання. Робив я це у довільній формі, щоб зрозуміти з чого потрібно почати і які функції повинні бути в реалізації.
Почав з того, що в мене поряд з проєктом повинні бути текстові файли і папка для збереження картинок QR кодів. Щоб не створювати їх руками я створив блок Свій C# код з таким фрагментом коду, який зразу і виконав (після чого блок можна було видалити, тому що робота зроблена).
string[] files = new[]{"input.txt", "bad.txt", "good.txt"}; string dir = Path.Combine(project.Directory, "data"); if(!Directory.Exists(dir)) Directory.CreateDirectory(dir); foreach(string name in files) { string path = Path.Combine(project.Directory, name); if(!File.Exists(path)) File.WriteAllText(path, string.Empty); }
Дальше вже створив необхідних три списки – input, good, bad і прив’язав їх до створених текстових файлів.
Так як текстовий файл input.txt не містив ніяких даних, то я вирішив згенерувати декілька посиланнь, щоб можна було на них потренуватись.
Для цього сформував слідуючий фрагмент коду, який також запустив одноразово на виконання:
var input = project.Lists["input"]; for(int i =0;i<100;i++){ string url = string.Format("https://blog.yosyfovych.te.ua/?p={0}", i); input.Add(url); }
Тепер в мене вже були вхідні дані, і були необхідні списки для збереження результату роботи.
Вирішення завдання в мене складається з трьох блоків – створення імені файла використовуючи sha256, скачування QR коду, збереження файла в папку.
Тому я зразу і написав ці функції і помістив їх в контекст.
Для цього створив новий блок Свій C# код з таким фрагментом коду:
Func<string, string> CreateName = delegate(string text) { var sb = new StringBuilder(); using (var hash = System.Security.Cryptography.SHA256Managed.Create()) { var enc = Encoding.UTF8; foreach (var b in hash.ComputeHash(enc.GetBytes(text))) sb.Append(b.ToString("x2")); } return string.Format("{0}.jpg",sb.ToString()); }; Func<string, byte[]> Get = delegate(string link) { string url = string.Format("https://chart.googleapis.com/chart?chs=350x350&cht=qr&chl={0}&choe=UTF-8&chld=H|4", link); var type = ZennoLab.InterfacesLibrary.Enums.Http.ResponceType.BodyOnly; var method = ZennoLab.InterfacesLibrary.Enums.Http.HttpMethod.GET; byte[] bytes = new Byte[0]; try { bytes = ZennoPoster.HTTP.RequestBytes( method: method, // метод яким буде відправлено запит url: url,// по якому адресу буде відправлено запит content: string.Empty, contentPostingType: string.Empty, proxy: string.Empty, respType: type, // в якому вигляді повернути результат UseOriginalUrl: true, // не змінювати параметри запиту removeDefaultHeaders: true // видалення стандартних заголовків ); } catch { project.SendErrorToLog("error get",true); } return bytes; }; Func<byte[], string, string, bool> Save = delegate(byte[] qr, string dir, string name) { bool check = false; try { using (MemoryStream ms = new MemoryStream(qr)){ using (var img = Image.FromStream(ms)) { if(img.Width > 0){ img.Save(Path.Combine(dir, name),System.Drawing.Imaging.ImageFormat.Jpeg); check = true; } } } } catch { project.SendInfoToLog("error save",true); } return check; }; project.Context["name"] = CreateName; project.Context["sw"] = System.Diagnostics.Stopwatch.StartNew(); project.Context["get"] = Get; project.Context["save"] = Save;
Власне цей блок необхідно буде виконати один раз на початку виконання проєкту, а дальше уже визивати ці фунції з контексту необхідну кількість разів. Зручно тим, що визивати їх можна одним рядком, і можна більше зосередити увагу при написанні на самій логіці.
Після чого уже в новому блоці написав реалізацію, яка одержує перший рядок з видаленням з файлу який містить вхідні дані. І якщо файл пустий – виходить з блоку по помилці (щоб можна було зупинити виконання проєкту згідно не успішних виконаннь). А коли є рядки з даними – то в лог виводиться повідомлення скільки їх там залишилось.
Після чого, по черзі визиваються фунції створення імені, скачування файлу і власне його збереження.
Ім’я завжди буде створено без помилок.
Скачування картинки QR коду може бути з помилкою, тому в фунції яка описана вище код одягнутий в конструкцію try/catch. І якщо виникла помилка – повертається з фунції пустий масив, тобто без даних.
Дальше переходимо до збереження картинки, для цього з масиву байт створюється зображення. Якщо в масиві не зображення – то створена картинка не буде мати ширини – це я і перевіряю. У випадку коли це картинка – зберігаю і повертаю true, інакше false.
Залишилось тільки зберегти результат в список з успішним чи не успішним виконанням і вивести повідомлення в лог скільки рядків є у кожному списку.
Вишенькою на торті буде також повідомлення про те, скільки часу було затрачено на скачування і збереження QR коду, що може знадобитись, коли захочеться пришвидшити процес.
Нижче наведений описаний код:
var input = project.Lists["input"]; var bad = project.Lists["bad"]; var good = project.Lists["good"]; var sw = project.Context["sw"]; string dir = Path.Combine(project.Directory, "data"); string link = string.Empty; string name = string.Empty; int count = 0; lock(SyncObjects.ListSyncer){ count = input.Count; if(count > 0) { link = input.GetItem("0", true); project.SendInfoToLog(string.Format("count all: {0}", count-1), true); } } if(count == 0) { project.SendInfoToLog("Stop", true); throw new Exception(); } name = project.Context["name"](link); byte[] qr = project.Context["get"](link); bool check = project.Context["save"](qr, dir, name); if(check) { lock(SyncObjects.ListSyncer) good.Add(string.Join("|", new[]{name, link })); } else lock(SyncObjects.ListSyncer) bad.Add(link); project.SendInfoToLog(string.Format("count all: {0} good: {1} bad: {2}", input.Count,good.Count, bad.Count), true); sw.Stop(); project.SendInfoToLog(string.Format("Стоп: {0:c} ms", sw.Elapsed));