@EDiPensier

Как получить исходный ответ сайта в нужной кодировке?

Имеется проблема с парсингом страниц абстрактного сайта.

Изначально отправлялся запрос посредством модуля request, где в body приходили верные данные, но потом возникла потребность имитировать браузер через puppeteer (chromium).

Прямое открытие страницы браузером через GET выдаёт так называемые "кракозябры", однако AJAX запрос возвращает нормальный ответ.

Заголовки ответа:
content-encoding: br
content-type: text/plain

Данные при открытии страницы (мониторинг сети):
Preview: ХОНДА ЦИВИК
Response: ХОÐДРЦИВИК

Отображение на самой странице: ХОНДА Р¦РР’РРљ

Простой AJAX запрос:

function reqListener () {
  console.log(this.responseText);
}

var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "<url>");
oReq.send();

и верный ответ: ХОНДА ЦИВИК

Попытки раскодировки (с помощью модуля iconv-lite/iconv) имеют неполный успех, некоторые данные теряются.

Первая проба - раскодировать ответ, который де-факто отображается.

page.goto(uri).then(async (response) => {

const content = await page.evaluate(() => document.body.innerText);
const dataToSave = iconv.encode(iconv.decode(Buffer.from(content), 'utf8'), 'win1251').toString();

//ХОНДА Ц�?В�?К , потеряна заглавная И (� ), в других словах могут быть потеряны также некоторые другие буквы русского алфавита.

})

Вторая попытка - получить буфер и попытаться раскодировать его, имея приблизительные данные о возможной кодировке.

page.goto(uri).then(async (response) => {

const data = await response.buffer();

iconv.encode(iconv.decode(data, 'utf8'), "windows-1252");
//ХО�?Д�? ЦИВИК , потери более явные, из замеченного - э я с А Н Ё

iconv.encode(iconv.decode(data, 'utf8'), "iso88591");
//Х�?Н�?А Ц�?�?�?�? , потеряно более чем

iconv.encode(iconv.decode(data, 'utf8'), "binary");
//Х�~Н�А Ц������a

})

Буфер, получаемый через 2 способ ( C3 90 C2 A5 C3 90 C5 BE C3 90 C2 9D C3 90 E2 80 9D C3 90 C2 90 20 C3 90 C2 A6 C3 90 CB 9C C3 90 E2 80 99 C3 90 CB 9C C3 90 C5 A1 ) попытался прогнать по всем доступным кодировкам iconv-lite:

iconv.encodings = require("iconv-lite/encodings");

const uniqEncodings = [];

for(const encoding in iconv.encodings) {
	
	if(iconv.encodings[encoding].constructor.name == "Object") {
		
		uniqEncodings.push(encoding);
		
	}
	
}

const hex = `C3 90 C2 A5 C3 90 C5 BE C3 90 C2 9D C3 90 E2 80 9D C3 90 C2 90 20 C3 90 C2 A6 C3 90 CB 9C C3 90 E2 80 99 C3 90 CB 9C C3 90 C5 A1`.toLowerCase().replace(/ /g, "");

let txtTest = "";

const buffer = Buffer.from(hex, "hex");

for(let i = 0; i < uniqEncodings.length; ++i) {
	
	txtTest += `${uniqEncodings[i]}: ${iconv.encode(buffer, uniqEncodings[i]).toString()}\n`;
	
}

Результат

utf8: ХОÐДРЦИВИК
cesu8: ХОÐДРЦИВИК
ucs2: � � � ~� � � � � � � � �� � �� a
binary: Х�~Н�А Ц������a
base64:
hex:
utf32le: � � � ~ � � � � � � � � � � � � � a
utf32be: � � � ~ � � � � � � � � � � � � � a
maccenteuro: ???�???�?? ?????�???�
cp808: ?????????? ??????????
mik: ?????????? ??????????
cp720: ?????????� ??????????
windows874: ???????�?? ?????�????
windows1250: ???�???�?? ?�???�???�
windows1251: ???????�?? ?�???�????
windows1252: ХО�?Д�? ЦИВИК
windows1253: ?�?????�?? ?�???�????
windows1254: ?�?????�?? ?�?�?�?�?�
windows1255: ?�?????�?? ?�?�?�?�??
windows1256: ?�?????�?? ?�???�????
windows1257: ???�???�?? ?�???�???�
windows1258: ?�?????�?? ?�?�?�?�??
iso88591: Х�?Н�?А Ц�?�?�?�?
iso88592: ???�?�???� ?????????�
iso88593: ?????�???� ??????????
iso88594: ???�?�???� ?????????�
iso88595: ?????�???� ??????????
iso88596: ?????�???� ??????????
iso88597: ?????�???� ?�???�????
iso88598: ?�???�???� ?�????????
iso88599: ?�???�???� ?�????????
iso885910: �?мН�?А �?�?�?�?к
iso885911: ?????�???� ??????????
iso885913: ???�?�?�?� ?�???�???�
iso885914: ?????�???� ??????????
iso885915: ХиН�?А �?�?�?�?Ш
iso885916: ???�?�?�?� ?????????�
cp437: ?�???????? ??????????
cp737: ?????????? ??????????
cp775: ???�???�?? ?�???�???�
cp850: Ѿ�?�?�?�? ���?�?�?�?
cp852: ???�?????? ?????????�
cp855: ?????????? ??????????
cp856: ?�???????? ?�????????
cp857: ?�???????? ?�????????
cp858: Ѿ�?�?�?�? ���?�?�?�?
cp860: ?????????? ??????????
cp861: �?�?�?�?�? �?�?�?�?�?
cp862: ?�???????? ??????????
cp863: ?????????? ?�????????
cp864: ?????????? ?�????????
cp865: ?????????? ??????????
cp866: ?????????? ??????????
cp869: ?????????? ?�???�????
cp922: ?�?�?�???� ?�???????�
cp1046: ?????????? ??????????
cp1124: ?????�???� ??????????
cp1125: ?????????? ??????????
cp1129: ?�???�???� ?�????????
cp1133: ?????�???� ?�????????
cp1161: ?????????? ?�????????
cp1162: ?????�?�?� ?????�????
cp1163: ?�???�???� ?�????????
maccroatian: ???�???�?? ???�?�?�?�
maccyrillic: ???????�?? ?????�????
macgreek: ?�?????�?? ?�???�????
maciceland: ܴ�?�?���? �?�������?
macroman: ?�?????�?? ???�?�?�??
macromania: ?�?????�?? ???�?�?�??
macthai: ???????�?? ?????�????
macturkish: ?�?????�?? ???�?�?�??
macukraine: ???????�?? ?????�????
koi8r: ?????????? ??????????
koi8u: ?????????? ??????????
koi8ru: ?????????? ??????????
koi8t: ???????�?? ?�???�????
armscii8: ?????�???� ??????????
rk1048: ???????�?? ?�???�????
tcvn: ?????????? ??????????
georgianacademy: ?�???�?�?� ?�?�?�?�?�
georgianps: ?�???�?�?� ?�?�?�?�?�
pt154: ???????�?? ?????�????
viscii: ?????????? ??????????
iso646cn: ?$???????? ??????????
iso646jp: ?\???????? ??????????
hproman8: ��?��?� �?��?���
macintosh: ?�?????�?? ???�?�?�??
ascii: ?????????? ??????????
tis620: ?????????? ??????????
shiftjis: ?\?????�h?? ?????�f????
eucjp: ?\?���???��?? ?���???��???���
cp936: ???????��?? ?????��????
gbk: ???????��?? ?????��????
gb18030: �0�4�0�6�0�4�0�6�0�4�0�9�0�4���0�4�0�6 �0�4�0�7�0�4�0�0�0�4���0�4�0�0�0�4�0�8
cp949: ��?��?��?������? ��?��?������?��?
cp950: ???????��?? ?????��????
big5hkscs: ???????��?? ?????��????

Попытка раскодировать оригинальный ответ (ХОÐДРЦИВИК) посредством сторонних сайтов в условных 9/10 случаев дал абсолютно аналогичный iconv-lite'у результат, однако единственный 1/10 с помощью 2cyr с исходным в utf8, отображаемым как cp1252, собственно, конвертировал в абсолютно верные данные (не учитывая Р, которая при повторном копировании зашифрованного текста превращается в неизвестный символ по причине изменения пробела на nbsp).

В Firefox имеется функция угадывания правильной кодировки текста по содержимому страницы, что также работает превосходно. Модуль iconv-lite и iconv ранее работали в соответствии с нормами, кроме того, пробовал более нативные способы конвертации, а также некоторые специализирующиеся сайты декодеры, что приводило к потере.

Какой может быть источник проблемы? И как, если так выражаться, "по нормальному" получать то что требуется?

Из костыльных мыслей имеется имитация AJAX запроса в evaluate, вывод в консоль и перехват с помощью page.on("console"), но это же есть не совсем корректно, не так ли?
  • Вопрос задан
  • 318 просмотров
Решения вопроса 1
@EDiPensier Автор вопроса
В общем, проблема была именно с Content-Type, который отдавался в text/plain.

Самый схожий случай с близлежащей проблемой описан здесь, а популярные методы решения тут (не для вышеприведённого).

Первое что пришло в голову после обнаружения корня зла - попытка модифицировать ответ на text/html, с которым не возникало подобного, по этому варианту, параллельно решая дополнительные задачи. Всё-бы ничего, но только к этому варианту нужен собственный body для корректной работы. Если же передавать пустой или попытаться получить с помощью Fetch.getResponseBody (в рамках описанного варианта), то подключение не пройдёт успешно (страница висит в ожидании ответа), а единственная ошибка, которая вероятнее всего прилетит - таймаут. Описанный ниже метод ещё более костыльный, так как отправляет запрос через request, от которого я изначально уходил.

В конечном итоге посчастливилось столкнуться с тем, что нужно и вникать в суть проблемы более глубже. Собственно, рекомендуют использовать Fetch.getResponseBody более нестандартно, что может вызвать конфликты с плагинами ( кооперация появилась относительно недавно, но это лишь часть решения, кроме того до сих пор не интегрировано в популярный puppeteer... ), ибо puppeteer регулирует Fetch.enable самостоятельно для другой сессии клиента.

Выложу в данном ответе 2 решения.

Первое, используя единичную стратегию, через Chrome Devtools Protocol -> Fetch с последующим отключением:

const URI = "<someUri>";

//...launch puppeteer

const next = async (browser) => {

	let realResponse = null;
	
	const page = await browser.newPage()
		
	const client = await page.target().createCDPSession();

	await client.send("Fetch.enable", {
		patterns: [{ requestStage: "Response" }]
	});

	const requestPausedHandler = async (event) => {

		const { url } = event.request;
		
		if(url == URI) {
			
			const { requestId } = event;

			const responseCdp = await client.send("Fetch.getResponseBody", { requestId });
			
			realResponse = responseCdp.base64Encoded
					? Buffer.from(responseCdp.body, "base64").toString("utf-8")
					: responseCdp.body;
			
			//При правильном регулировании - возможен вызов закомментированного, но здесь принцип once.
			//await client.send("Fetch.continueRequest", { requestId });
			
			//Поэтому следует использовать сразу использовать Fetch.disable для созданной сессии, дабы отключиться от фазы Response (отдаёт ответ параллельно).
			await client.send("Fetch.disable");

		}
		
	};

	client.on("Fetch.requestPaused", requestPausedHandler);

	page.goto(uri).then(async (response) => {
		
		client.off("Fetch.requestPaused", requestPausedHandler);
		
		console.log(realResponse);
		
	});
	
}

И второе, которое описал в конце вопроса. Заключается в том, чтобы получать главную страницу сайта и оттуда выполнять XMLHttpRequest с помощью page.evaluate (предварительно сохраняя оригинальные вспомогательные методы воздействия):

const MAIN_URI = "<someUri>";
const NEED_URI = "<someUri>";

const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

const primordials = ["Promise", "XMLHttpRequest"];
const defaultLengthOfHiddenIdentifier = 50;

const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

const genRandomString = length => {
	
	let result = "";
	const alphabetLMinusOne = alphabet.length - 1;
	
	for(let i = 0; i < length; ++i) {
		
		result += alphabet[rand(0, alphabetLMinusOne)];
		
	}
	
	return result;
	
};

//...launch puppeteer

const next = async (browser) => {
	
	const page = await browser.newPage();
	
	//Запрос на страницу должен идти после сохранения primordials
	//await page.goto(MAIN_URI);
	
	//...possible waitUntil
		
	let nowLengthOfHI = defaultLengthOfHiddenIdentifier;
	
	for(let i = 0; i < primordials.length; ++i) {
		
		const object = primordials[i];
		let possibleIdentifier = genRandomString(nowLengthOfHI);
		
		for(let j = i - 1; j > 0; --j) {
			
			const prevObject = primordials[j];
			
			if(prevObject[1] == possibleIdentifier) {
				
				possibleIdentifier = genRandomString(++nowLengthOfHI);
				
				break;
				
			}
			
		}
		
		primordials[i] = [
		
			object,
			possibleIdentifier,
		
		];
		
	}
	
	//Сохранение оригинальных значений для Promise и XMLHttpRequest, чтобы быть уверенными, что сайт не перезаписал их.
	await page.evaluateOnNewDocument(primordials => {
		
		const getOriginObject = object => {
			
			if(object.includes(".")) {
				
				const splitted = object.split(".");
				
				let ret = window;
				
				for(let i = 0; i < splitted.length; ++i) {
					
					ret = ret[splitted[i]];
					
				}
				
				return ret;
				
			}
			
			return window[object];
			
		};
		
		for(let i = 0; i < primordials.length; ++i) {
			
			const object = primordials[i][0];
			const identifier = primordials[i][1];
			
			Object.defineProperty(window, identifier, { value: getOriginObject(object) });
			
		}
		
	}, primordials);

	await page.goto(MAIN_URI);

	const answerObject = await page.evaluate(async (url, primordials) => {
		
		const _Promise = window[ primordials.find(obj => obj[0] == "Promise")[1] ];
		const _XMLHttpRequest = window[ primordials.find(obj => obj[0] == "XMLHttpRequest")[1] ];
		
		return new _Promise(resolve => {
			
			let wasCalled = false;
		
			const handleEvent = e => {
				
				if(wasCalled) return;
				
				wasCalled = true;
				
				const answer = {
						
					error: false,
					
					type: e.type,
					status: xhr.status,
					response: xhr.responseText,
					
				};
				
				if(e.type == "error" || e.type == "abort" || e.type == "timeout") {
					
					answer.error = true;
					
				}
				
				if(xhr.status >= 400 && xhr.status < 600) {
					
					answer.error = true;
					
				}
				
				return resolve(answer);
				
			};
		
			const xhr = new _XMLHttpRequest();
			
			xhr.addEventListener('error', handleEvent);
			xhr.addEventListener('timeout', handleEvent);
			xhr.addEventListener('load', handleEvent);
				
			xhr.open("GET", url);
			
			xhr.send();
		
		});
		
	}, NEED_URI, primordials);
	
	console.log(answerObject);
	
}
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
SilenceOfWinter
@SilenceOfWinter
та еще зажигалка...
какой целеустремленный... тебе нужно было почитать о заголовке "content-encoding: br" и выйти на https://en.wikipedia.org/wiki/Brotli
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы