Гусеничный заграждение Taobao

Node.js рептилия

справочная информация

У компании есть необходимость просканировать шквал прямых трансляций по короткой ссылке комнаты прямых трансляций 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/Сяожули…