- Регистрация
- 26.09.2010
- Сообщения
- 1 291
- Благодарностей
- 119
- Баллы
- 63
Часть 1. Подготовка.
За основу берем проект с прошлой статьи. Но так как нам предстоит кликать по картинкам рекапчи, добавим в класс Helpers несколько вспомогательных методов из которых затем соберем метод для клика:
Во-первых метод для поиска элемента по xpath:
C#:
public static async Task<string> PerformSearchAsync(CDPClient cdpClient, string query, string sessionId = null)
{
try
{
var response = await cdpClient.SendCommandAsync("DOM.performSearch", new { query }, sessionId: sessionId);
var result = response["result"];
if (result != null)
{
string searchId = result.Value<string>("searchId");
if (string.IsNullOrEmpty(searchId))
{
throw new Exception("searchId null");
}
int resultCount = result.Value<int>("resultCount");
if (resultCount == 0)
{
throw new Exception("resultCount = 0");
}
return searchId;
}
else
{
throw new Exception("PerformSearchAsync result null");
}
}
catch (Exception ex)
{
throw new Exception($"Произошла ошибка: {ex.Message}");
}
}
C#:
public static async Task<int> GetSearchResultsAsync(CDPClient cdpClient, string searchId, int matchNum = 0, string sessionId = null)
{
try
{
var response = await cdpClient.SendCommandAsync("DOM.getSearchResults", new { searchId, fromIndex = matchNum, toIndex = matchNum + 1 }, sessionId: sessionId);
var result = response["result"] as JObject;
if (result != null)
{
var nodeIdsArray = result["nodeIds"] as JArray;
if (nodeIdsArray != null && nodeIdsArray.Count > 0)
{
int firstNodeId = nodeIdsArray[0].Value<int>();
if (firstNodeId == 0)
{
throw new Exception("GetSearchResultsAsync firstNodeId = 0, result = " + response.ToString() + " searchId = " + searchId);
}
return firstNodeId;
}
else
{
throw new Exception("GetSearchResultsAsync нет массив nodeIds пуст");
}
}
else
{
throw new Exception("GetSearchResultsAsync нет поля result");
}
}
catch (Exception ex)
{
throw new Exception($"Произошла ошибка {ex.Message}");
}
}
C#:
public static async Task<string> GetBoxModelAsync(CDPClient cdpClient, int nodeId, string sessionId = null)
{
try
{
var response = await cdpClient.SendCommandAsync("DOM.getBoxModel", new { nodeId }, sessionId: sessionId);
var result = response["result"]["model"];
if (result != null)
{
var contentArray = result["content"] as JArray;
if (contentArray != null)
{
string modelContent = contentArray.ToString();
if (string.IsNullOrEmpty(modelContent))
{
throw new Exception("modelContent null");
}
return modelContent;
}
else
{
throw new Exception("GetBoxModelAsync массив null");
}
}
else
{
throw new Exception("GetBoxModelAsync result null");
}
}
catch (Exception ex)
{
throw new Exception($"Произошла ошибка GetBoxModelAsync: {ex.Message}");
}
}
C#:
public static async Task ClickAsync(CDPClient cdpClient, string xpath, string firstPageSessionId)
{
for (int i = 0; i < 5; i++)
{
try
{
//Подгружаем DOM-модель
await cdpClient.SendCommandAsync("DOM.getDocument", sessionId: firstPageSessionId);
Random rand = new Random();
//Делаем поиск элемента по Xpath
string searchId = await PerformSearchAsync(cdpClient, xpath, sessionId: firstPageSessionId);
await Task.Delay(150);
//Запрашиваем результаты поиска
int nodeId = await GetSearchResultsAsync(cdpClient, searchId, 0, sessionId: firstPageSessionId);
await Task.Delay(150);
//Получаем box model найденного элемента
string modelContent = await GetBoxModelAsync(cdpClient, nodeId, sessionId: firstPageSessionId);
//Получаем координаты из box model
string input = modelContent.Trim('[', ']', ' ');
string[] stringArray = input.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
double[] numbers = stringArray
.Select(s => s.Trim())
.Select(s => double.Parse(s, CultureInfo.InvariantCulture))
.ToArray();
int x = rand.Next(Convert.ToInt16(numbers[0]), Convert.ToInt16(numbers[2]));
int y = rand.Next(Convert.ToInt16(numbers[1]), Convert.ToInt16(numbers[5]));
//Делаем сам клик
await cdpClient.SendCommandAsync("Input.dispatchMouseEvent", new { type = "mousePressed", x, y, button = "left", clickCount = 1 }, sessionId: firstPageSessionId);
await Task.Delay(100); //Задержка между нажатием и отпусканием кнопки
await cdpClient.SendCommandAsync("Input.dispatchMouseEvent", new { type = "mouseReleased", x, y, button = "left", clickCount = 1 }, sessionId: firstPageSessionId);
return;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
await Task.Delay(2000);
}
throw new Exception($"Не получилось кликнуть на {xpath}");
}
C#:
public static async Task<string> GetOuterHTMLAsync(CDPClient cdpClient, int nodeId, string sessionId)
{
try
{
var response = await cdpClient.SendCommandAsync("DOM.getOuterHTML", new { nodeId }, sessionId: sessionId);
var result = response["result"];
string outerHTML = result.Value<string>("outerHTML");
if (string.IsNullOrEmpty(outerHTML))
{
throw new Exception("outerHTML null");
}
return outerHTML;
}
catch (Exception ex)
{
throw new Exception($"GetOuterHTMLAsync Произошла ошибка: {ex.Message}");
}
}
Часть 2. Рекапча.
Теперь переходим к самому интересному. Для начала немного поговорим о том как устроена сама рекапча. Если она сразу отображается при открытии страницы, то это означает что при загрузке страницы создаются несколько фреймов, в которых она и находится. В CDP и страница и фрейм (и не только они) это target, т.е. потенциальная цель для того чтобы подключиться, получить sessionId и взаимодействовать (как мы делали со страницей в прошлой статье). Так как фреймов несколько, напишем небольшой метод, который будет получать все таргеты через Target.getTargets и по нужным критериям (тип и url) возвращать нужный нам:
C#:
public static async Task<string> GetTargetId(CDPClient cdpClient, string url = null, string type = "page")
{
var targets = await cdpClient.SendCommandAsync("Target.getTargets");
string targetId = (string)targets["result"]["targetInfos"]
.Children()
.FirstOrDefault(t => url == null ? ((string)t["url"])?.Contains("chrome://newtab/") == true : ((string)t["url"])?.Contains(url) == true && (string)t["type"] == type)?
["targetId"];
if (!String.IsNullOrEmpty(targetId))
{
return targetId;
}
throw new Exception($"targetId для {url} не найден");
}
Получим таргеты и сессии к этим фреймам:
C#:
string frameTarget = await GetTargetId(cdpClient, "recaptcha/api2/anchor", "iframe");
command = await cdpClient.SendCommandAsync("Target.attachToTarget", new
{
targetId = frameTarget,
flatten = true
});
string framePageSessionId = command["result"]["sessionId"].Value<string>();
string secondFrameTarget = await GetTargetId(cdpClient, "recaptcha/api2/bframe", "iframe");
command = await cdpClient.SendCommandAsync("Target.attachToTarget", new
{
targetId = secondFrameTarget,
flatten = true
});
string secondFramePageSessionId = command["result"]["sessionId"].Value<string>();
Network.requestWillBeSent (браузер собирается отправить запрос) -> (опционально, если запрос реально идет по сети, а не просто возвращается ответ из кеша) Network.requestWillBeSentExtraInfo (дополнительные данные запроса, например заголовки с сетевого уровня) -> Network.responseReceived (получен ответ) -> (опционально) Network.responseReceivedExtraInfo (данные сетевого уровня ответа) -> Network.dataReceived (получены данные/часть данных, может вызываться несколько раз) -> Network.loadingFinished (загрузка данных успешно завершена, либо loadingFailed если неуспешно)
После того как запрос успешно завершен для получения его данных необходимо вызвать CDP-метод Network.getResponseBody. На C# это будет выглядеть примерно так:
C#:
public static async Task<string> GetResponseBodyAsync(CDPClient cdpClient, string requestId, string sessionId)
{
try
{
string body = null;
for (int i = 0; i < 15; i++)
{
var response = await cdpClient.SendCommandAsync("Network.getResponseBody", new { requestId }, sessionId: sessionId);
var error = response["error"];
if (error != null)
{
string errorMessage = error["message"]?.Value<string>();
if (errorMessage == "No resource with given identifier found")
{
throw new Exception($"Resource {requestId} error - No resource with given identifier found");
}
await Task.Delay(1000);
continue;
}
var result = response["result"];
if (result == null)
{
await Task.Delay(1000);
continue;
}
body = result.Value<string>("body");
if (!string.IsNullOrEmpty(body))
{
break;
}
await Task.Delay(1000);
}
if (string.IsNullOrEmpty(body))
{
throw new Exception("body null");
}
return body;
}
catch (Exception ex)
{
throw new Exception($"Произошла ошибка: {ex.Message}");
}
}
А теперь сам метод который будет ловить содержимое ответов на запросы по определенным урлам (специально для рекапчи сделан именно не один урл, а список урлов, т.к. задания приходят с разных урлов). Помимо прочего метод принимает два параметра: disableNetwork и durableMessages. Первый нужен чтобы домен Network не отключался после работы, т.к. если ставим сразу несколько заданий на ловлю содержимого разных запросов отключение Network может мешать. А опция durableMessages нужна для того чтобы точно сохранился ответ на запрос потому что иногда бывает такое что браузер очищает эти данные. Так же есть таймаут, чтобы не ожидать бесконечно (на случай если проблемы с коннектом).
C#:
public static async Task<string> CaptureFromUrl(CDPClient cdpClient, string sessionId, List<string> targets, int timeout = 30000, bool disableNetwork = true, bool durableMessages = false)
{
var base64Tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
string targetRequestId = null;
string targetsLog = string.Join(", ", targets);
Console.WriteLine($"Запустил CaptureFromUrl для списка: {targetsLog} с таймаутом {timeout}мс");
Action<JObject> requestWillBeSentHandler = null;
Action<JObject> responseHandler = null;
Action<JObject> loadingFinishedHandler = null;
Action<JObject> loadingFailedHandler = null;
void Cleanup()
{
if (responseHandler != null) cdpClient.UnsubscribeEvent("Network.responseReceived", responseHandler);
if (loadingFinishedHandler != null) cdpClient.UnsubscribeEvent("Network.loadingFinished", loadingFinishedHandler);
if (loadingFailedHandler != null) cdpClient.UnsubscribeEvent("Network.loadingFailed", loadingFailedHandler);
}
responseHandler = (parameters) =>
{
try
{
string url = parameters["response"]?["url"]?.Value<string>();
string currentRequestId = parameters["requestId"]?.Value<string>();
if (url != null)
{
bool isMatch = targets.Any(t => url.Contains(t));
if (isMatch)
{
if (targetRequestId == null)
{
//Нашли нужный requestId
targetRequestId = currentRequestId;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка в обработчике responseReceived: {ex.Message}");
}
};
loadingFinishedHandler = async (parameters) =>
{
try
{
string requestId = parameters["requestId"]?.Value<string>();
bool isMatch = false;
//Проверяем загруженный запрос, если это наш targetRequestId который мы поймали в обработчике responseHandler значит успех
if (targetRequestId != null && requestId == targetRequestId)
{
isMatch = true;
}
if (isMatch)
{
//Запрашиваем тело найденого запроса, возвращаем результат и отписываемся
string base64 = await GetResponseBodyAsync(cdpClient, targetRequestId, sessionId);
if (!string.IsNullOrEmpty(base64))
{
base64Tcs.TrySetResult(base64);
Cleanup();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка в обработчике loadingFinished: {ex.Message}");
base64Tcs.TrySetException(ex);
Cleanup();
}
};
loadingFailedHandler = (parameters) =>
{
try
{
string requestId = parameters["requestId"]?.Value<string>();
bool isMatch = false;
if (targetRequestId != null && requestId == targetRequestId)
{
isMatch = true;
}
//Если наш запрос недогрузился возвращаем исключение
if (isMatch)
{
base64Tcs.TrySetException(new Exception($"Loading failed for request {requestId}"));
Cleanup();
}
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка в обработчике loadingFailed: {ex.Message}");
}
};
cdpClient.SubscribeEvent("Network.responseReceived", responseHandler);
cdpClient.SubscribeEvent("Network.loadingFinished", loadingFinishedHandler);
cdpClient.SubscribeEvent("Network.loadingFailed", loadingFailedHandler);
if (durableMessages)
{
await cdpClient.SendCommandAsync("Network.configureDurableMessages", new
{
maxTotalBufferSize = 100 * 1024 * 1024,
maxResourceBufferSize = 50 * 1024 * 1024
}, sessionId: sessionId);
}
await cdpClient.SendCommandAsync("Network.enable", sessionId: sessionId);
string result = null;
try
{
var completedTask = await Task.WhenAny(base64Tcs.Task, Task.Delay(timeout));
if (completedTask == base64Tcs.Task)
{
result = await base64Tcs.Task;
}
else
{
Cleanup();
string errorMsg = $"CaptureFromUrl timed out ({timeout}ms) for list: {targetsLog}";
throw new TimeoutException(errorMsg);
}
}
finally
{
if (disableNetwork)
{
try
{
await cdpClient.SendCommandAsync("Network.disable", sessionId: sessionId);
}
catch (Exception ex)
{
Console.WriteLine($"Error disabling network: {ex.Message}");
}
}
Cleanup();
Console.WriteLine($"{DateTime.Now} - Закончил CaptureFromUrl для {targetsLog}, result = {(result != null && result.Length < 300 ? result : result?.Substring(0, 300))}");
}
return result;
}
Сначала метод solve. Он подключается к фреймам рекапчи, кликает на квадратик, ловит из трафика задания (вида /m/0pg52, CapMonster как раз их понимает), кладет их в очередь, ловит base64 картинку. Отправляет это все на CapMonster, затем получает решение и кликает но нужным элементам. Если вдруг нет картинок куда кликнуть, то перезагружает капчу (и так же ловит в трафике задания и картинку). Если вдруг попадается динамическая капча то вызывает метод HandleDynamicGrid. Когда новых заданий нет, капча считается решенной и метод завершается.
C#:
public static async Task solve(CDPClient cdpClient, string firstPageSessionId)
{
string frameTarget = await Helpers.GetTargetId(cdpClient, "recaptcha/api2/anchor", "iframe");
var command = await cdpClient.SendCommandAsync("Target.attachToTarget", new
{
targetId = frameTarget,
flatten = true
});
string framePageSessionId = command["result"]["sessionId"].Value<string>();
string secondFrameTarget = await Helpers.GetTargetId(cdpClient, "recaptcha/api2/bframe", "iframe");
command = await cdpClient.SendCommandAsync("Target.attachToTarget", new
{
targetId = secondFrameTarget,
flatten = true
});
string secondFramePageSessionId = command["result"]["sessionId"].Value<string>();
Console.WriteLine($"framePageSessionId = {framePageSessionId}, secondFramePageSessionId = {secondFramePageSessionId}");
//Создаем задание для ловли картинок
var captureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "payload?p=" }, disableNetwork: false, durableMessages: true);
//И для ловли типа задания
var taskCaptureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "recaptcha/api2/reload", "recaptcha/api2/userverify" }, disableNetwork: false, durableMessages: true);
await Helpers.ClickAsync(cdpClient, ".//div[@class='recaptcha-checkbox-borderAnimation']", framePageSessionId);
//Очередь заданий
Queue<string> taskKeyQueue = new();
for (int i = 0; i < 15; i++)
{
Console.WriteLine($"Iteration {i}");
bool isDynamic = false;
string grid = String.Empty;
string task = String.Empty;
//Если очередь заданий пуста то ждем ответа на запрос в котором приходят задания
if (taskKeyQueue.Count == 0)
{
string fullResponse = await taskCaptureTask;
//Если нет слова pmeta то капча решена
if (!fullResponse.Contains("pmeta"))
{
break;
}
//Парсим сами задания и кладем в очередь
var matches = Regex.Matches(fullResponse, @"(?<=\["")/m/0.*?(?="")");
foreach (Match m in matches)
{
if (!String.IsNullOrEmpty(m.Value))
{
Console.WriteLine($"Add to queue {m.Value}");
taskKeyQueue.Enqueue(m.Value);
}
}
//Если есть слово dynamic значит картинки капчи динамически обновляются
if (fullResponse.Contains("dynamic"))
{
isDynamic = true;
}
}
// Берём следующее задание из очереди
if (taskKeyQueue.Count > 0)
{
task = taskKeyQueue.Dequeue();
}
string base64 = await captureTask;
//На всякий случай проверяем на пустоту
if (String.IsNullOrEmpty(base64))
{
throw new Exception("Не подгрузилась картинка рекапчи");
}
//Определяем размер сетки
string gridHTML = await Helpers.GetHtmlAsync(cdpClient, ".//img[contains(@class, 'rc-image-tile')]", secondFramePageSessionId);
grid = Regex.Match(gridHTML, @"(?<=rc-image-tile-).*?(?="")").Value;
if (String.IsNullOrEmpty(task) || String.IsNullOrEmpty(grid))
{
throw new Exception("Не смог выпарсить задание или grid");
}
//Отправляем задание на Capmonster
int RCId = await cmCloud.sendRC(new List<string> { base64 }, task, grid);
//очищаем переменную base64 перед получением новой картинки
base64 = null;
List<bool> result = await cmCloud.get(RCId);
// Функция для перезагрузки капчи при ошибке
async Task Reload()
{
captureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "payload?p=" }, disableNetwork: false);
taskKeyQueue.Clear();
taskCaptureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "recaptcha/api2/reload", "recaptcha/api2/userverify" }, disableNetwork: false);
await Helpers.ClickAsync(cdpClient, ".//button[@id='recaptcha-reload-button']", secondFramePageSessionId);
}
//Если среди не нашлось правильных ответов то перезагружаем капчу и пробуем снова
if (result == null)
{
await Reload();
continue;
}
//Словарь для маппинга динамических картинок 1х1
Dictionary<string, int> newBaseDict = new();
List<string> base64List = new();
//Кликаем на нужные картинки
for (int y = 0; y < result.Count; y++)
{
if (result[y])
{
if (grid == "44")
{
await Helpers.ClickAsync(cdpClient, $".//td[@id='{y}']", secondFramePageSessionId);
}
else
{
//Если сетка 3x3 значит картинки могут быть динамическими, т.е. подгружаться новые после нажатия
var captureTask1 = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "payload?p=" }, timeout: 10000);
await Helpers.ClickAsync(cdpClient, $".//td[@id='{y}']", secondFramePageSessionId);
if (isDynamic)
{
//Ловим новые картинки 1x1
string base641 = await captureTask1;
Console.WriteLine($"Поймал картинку 1*1 номер по порядку {y}");
base64List.Add(base641);
//Когда мы отправим капмонстру картинку 1x1 нам просто придет true или false, чтобы понять на какой элемент по порядку кликать добавляем в словарь base64 картинки как ключ и порядковый номер в сетке
newBaseDict[base641] = y;
}
}
}
await Task.Delay(Random.Shared.Next(150, 500));
}
if (base64List.Count > 0)
{
bool dynamicSuccess = await HandleDynamicGrid(cdpClient, secondFramePageSessionId, task, base64List, newBaseDict);
if (!dynamicSuccess)
{
Console.WriteLine($"dynamicSuccess not success");
await Reload();
continue;
}
}
captureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "payload?p=" }, disableNetwork: false, durableMessages: true);
taskCaptureTask = Helpers.CaptureFromUrl(cdpClient, secondFramePageSessionId, new List<string> { "recaptcha/api2/reload", "recaptcha/api2/userverify" }, disableNetwork: false, durableMessages: true);
await Helpers.ClickAsync(cdpClient, ".//button[@id='recaptcha-verify-button']", secondFramePageSessionId);
//Проверяем, если есть сообщение о том что динамическая капча не решена то обновляем и пробуем заново
if (await Helpers.TryGetHtmlAsync(cdpClient, @".//div[(@class='rc-imageselect-error-select-more' or @class='rc-imageselect-error-dynamic-more') and not(contains(@style,'display:none'))]", secondFramePageSessionId, timeout: 2))
{
await Helpers.ClickAsync(cdpClient, ".//button[@id='recaptcha-reload-button']", secondFramePageSessionId);
}
}
}
C#:
private static async Task<bool> HandleDynamicGrid(CDPClient cdpClient, string sessionId, string task, List<string> base64List, Dictionary<string, int> baseDict)
{
List<string> newBase64List = new();
Dictionary<string, int> newBaseDict = new();
var captureTask = Helpers.CaptureFromUrl(cdpClient, sessionId, new List<string> { "payload?p=" }, timeout: 15000);
Console.WriteLine("start HandleDynamicGrid");
for (int i = 0; i < base64List.Count; i++)
{
var base64 = base64List[i];
try
{
int RCId = await cmCloud.sendRC(new List<string> { base64 }, task, "1x1");
List<bool> result = await cmCloud.get(RCId);
if (result == null)
{
return false;
}
if (result != null && result.Count > 0 && result[0])
{
if (baseDict.TryGetValue(base64, out int originalIndex))
{
//Пауза для того чтобы одиночная картинка успела отобразиться. После клика она отображается не сразу и если клик произойдет слишком быстро пока она не отобразилась то он улетит в пустую
await Task.Delay(2000);
Console.WriteLine($"{DateTime.Now} - Кликаю по {originalIndex}");
await Helpers.ClickAsync(cdpClient, $".//td[@id='{originalIndex}']", sessionId);
string newBase64 = await captureTask;
Console.WriteLine($"Поймал еще картинку 1*1 номер по порядку {originalIndex}{Environment.NewLine}");
if (!string.IsNullOrEmpty(newBase64))
{
newBase64List.Add(newBase64);
newBaseDict[newBase64] = originalIndex;
}
// Перезапускаем ловлю картинки для новой итерации
if (i < base64List.Count - 1)
{
captureTask = Helpers.CaptureFromUrl(cdpClient, sessionId, new List<string> { "payload?p=" });
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"HandleDynamicGrid произошла ошибка {ex.Message}");
return false;
}
}
if (newBase64List.Count > 0)
{
//Рекурсия
return await HandleDynamicGrid(cdpClient, sessionId, task, newBase64List, newBaseDict);
}
return true;
}
Вложения
-
151 КБ Просмотры: 4
Последнее редактирование:



