справочная информация
У компании есть необходимость просканировать шквал прямых трансляций по короткой ссылке комнаты прямых трансляций Taobao, но даже в Google она может найти только связанную тему, и нет ответа, поэтому она может только поддерживать себя.
Адрес репозитория краулера на github в конце статьи, давайте посмотрим на окончательный эффект от краулера:
Давайте посмотрим на процесс исследования и воссоздадим его.
Анализ страницы
Адрес комнаты прямого эфира можно получить при публикации прямого эфира:
Экран пули обычно представляет собой веб-сокет или сокет. Мы открываем инструменты разработчика, чтобы отфильтровать запрос ws, чтобы увидеть адрес веб-сокета:
Давайте упомянем Douyu: он использует флэш-сокет. Даже если мы откроем инструменты разработчика, мы все равно запутаемся. К счастью, Douyu официально открыл API сокета напрямую.
Мы продолжаем проверять полученное сообщение и обнаруживаем, что есть два типа сжатия сообщения: COMMON и GZIP, значение данных должно быть целевым сообщением, похоже, что оно было закодировано base64, и будет обсуждаться процесс расшифровки позже.
Теперь первая проблема, которую нам предстоит решить, это как получить адрес вебсокета.Проанализируйте исходный html и обнаружите, что скрипт можно найти через неизмененную часть:
Однако, получив весь этот скрипт и отформатировав его, я обнаружил, что исходный код явно был разработан по модульному принципу, и он был запакован и сжат, поэтому мы можем анализировать только небольшой фрагмент кода в модуле, что бессмысленно. .Но мы можем заметить, что единственная разница между адресами веб-сокетов в разных комнатах прямых трансляций - это токен, поэтому мы можем найти способ получить токен.Конечно, это очень отвратительная ссылка, никакой подсказки, и все возможности, о которых мы думали, потерпели неудачу. Глядя на запрос, инициированный страницей, как безголовая муха, я нашел его...
Токен получен через запрос API, а адрес API:
http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/
Что ж, проблема с адресом вебсокета решена, приступаем к написанию краулера.
написать сканер
Взгляните на кучу динамических параметров в строке запроса API, не думайте об обычных краулерах, мы пожертвовали артефактом:puppeteer.
puppeteer — это безголовый браузер, запущенный Google с открытым Node API. Теоретически он может программно управлять различными действиями браузера. Для нашего сценария это: После загрузки живой страницы перехватите запрос API для получения токена веб-сокета и проанализируйте результат, чтобы получить токен.Код в этой части выглядит следующим образом:
const browser = await puppeteer.launch()
const page = (await browser.pages())[0]
await page.setRequestInterception(true)
const api = 'http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/'
const { url } = message
// intercept request obtaining the web socket token
page.on('request', req => {
if (req.url.includes(api)) {
console.log(`[${url}] getting token`)
}
req.continue()
})
page.on('response', async res => {
if (!res.url.includes(api)) return
const data = await res.text()
const token = data.match(/"result":"(.*?)"/)[1]
const url = `ws://acs.m.taobao.com/accs/auth?token=${token}`
})
// open the taobao live page
await page.goto(url, { timeout: 0 })
console.log(`[${url}] page loaded`)
Вот небольшой трюк для оптимизации производительности.Получение экземпляра страницы в официальном примере puppeteer откроет новую страницу:
const page = await browser.newPage()
, На самом деле, при запуске браузера по умолчанию открывается страница about:blank.В нашем коде мы напрямую получаем этот открытый экземпляр для перехода на живую страницу, чтобы можно было сохранить на один процесс меньше.
Вы можете сравнить количество процессов, запущенных с помощью ps ax|grep puppeteer, по умолчанию есть два основных процесса, а остальные — страничные процессы.
После получения адреса веб-сокета можно установить соединение для получения сообщений:
const url = `ws://acs.m.taobao.com/accs/auth?token=${token}`
const ws = new WebSocket(url)
ws.on('open', () => {
console.log(`\nOPEN: ${url}\n`)
})
ws.on('close', () => {
console.log('DISCONN')
})
ws.on('message', msg => {
console.log(msg)
})
расшифровка сообщения
Теперь мы можем продолжать извлекать сообщения, что облегчит анализ. Когда мы анализировали страницу ранее, мы обнаружили, что есть два типа CompressType: COMMON и GZIP. После попытки COMMON может напрямую получить открытый текст, а GZIP должен пройти через Декодирование gunzip снова, результат декодирования примерно такой, и в нем уже можно увидеть никнейм и баррейдж контент:
Однако все только начинается... В контенте есть искаженные символы, и регулярное сопоставление по такому контенту бесплодно. Если вы попытаетесь сохранить его напрямуюbuffer
илиbuffer.toString()
Когда вы доберетесь до файла, вы обнаружите, что файл вообще не может быть открыт, а содержимое не может быть проанализировано:
Ни в коем случае, мы можем анализировать только кодировку utf8 исходного буферного массива.Вот дыра в мозгу, напрямую использовать строку, полученную путем объединения буферного массива, для анализа его закономерностей (код анализа см. в файле analysis.js):
Результаты анализа нескольких образцов следующие, выделены неизмененные части:
Эти значения могут быть преобразованы из допустимых кодировок символов по определенным правилам, но кто догадается, да и не нужно.
Таким образом, мы можем разобрать ник и шквал с помощью регулярного выражения:
/.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/
Конечно, этот шаблон также может соответствовать шквалу, который следует за якорем, что нам не нужно Мы можем заранее отфильтровать такого рода новости через определенную строку буфера:
const followedPattern = '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119'
Пока что мы можем разобрать чистый ник + заградительный огонь, полный код расшифровки выглядит следующим образом:
function decode(msg) {
// base64 decode
let buffer = Buffer.from(msg.data, 'base64')
if (msg.compressType === 'GZIP') {
// gzip decode
buffer = zlib.gunzipSync(buffer)
}
const bufferStr = buffer.join(',')
// [followed] notifications are ignored
const followedPattern = '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119'
if (bufferStr.includes(followedPattern)) {
return
}
// // print for debugging
// console.log(bufferStr)
// console.log(buffer.toString())
// first match is nick name and second match is barrage content
const barragePattern = /.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/
const matched = bufferStr.match(barragePattern)
if (matched) {
const nick = parseStr(matched[1])
const barrage = parseStr(matched[2])
console.log(`${nick}: ${barrage}`)
}
}
Конечно, еще может быть проблема, связанная с результатами анализа в таблице выше.barrage前
, есть 5 последовательных цифр, которые фиксированы и неизменны.На самом деле в начале 6 цифр вместе с предыдущей цифрой остаются неизменными.В итоге через сутки прежняя цифра изменилась со 130 на 131, а частота предыдущих цифр изменилась.Очень высокая.Так что я подозреваю,что эти значения могут быть связаны с текущим временем.
Может быть, эти 5 фиксированных значений будут меняться через неопределенный промежуток времени, и тогда регулярность придется корректировать, но она должна иметь возможность нормально работать в течение длительного времени.Если кому-то из коллег интересно, вы можете ищите закономерность.
Обслуживание процесса
Фактический процесс должен быть примерно таким: после получения запроса основной процесс разветвляет подпроцесс сканера для получения URL-адреса веб-сокета, и подпроцесс возвращает результат основному процессу.После того, как пользователь устанавливает соединение с веб-сокетом ( захват соединения), подпроцесс может совершить самоубийство, чтобы высвободить ресурсы, совершив самоубийствоbrowser.close()
Убить процессы, связанные с кукловодом.
Причина этого в том, чтобы протестировать его: срок действия токена истекает вскоре после отключения веб-сокета.
Репозиторий на гитхабе
Запомни звезду 😉
GitHub.com/Сяожули…