puppeteer+mysql - новый метод для краулеров! Получайте новости и обзоры так просто!

JavaScript

Puppeteer — это официальный безголовый инструмент Chrome от команды Google Chrome. Из-за этого официального объявления многие библиотеки автоматизированного тестирования в отрасли были прекращены, включая PhantomJS. Проект Selenium IDE для Firefox также был закрыт из-за отсутствия сопровождающих.

Summary

В этой статье для сканирования Sina Weibo будут использоваться Chrome Headless, Puppeteer, Node и Mysql. Войдите в систему и просмотрите новости с домашней страницы People's Daily и сохраните их в базе данных Mysql.

Установить

Установка Puppeteer может завершиться ошибкой из-за невозможности загрузить пакет драйверов Chromium. Решение по установке Puppeteer было представлено в предыдущей статье, поэтому в этой статье я не буду его представлять.Кукловод устанавливает и наступает на яму - решение

Начать

Давайте начнем со скриншота страницы и посмотрим на некоторые API, которые Puppeteer использует для запуска браузера и выполнения работы.

screenshot.js

const puppeteer = require('puppeteer');

(async () => {
    const pathToExtension = require('path').join(__dirname, '../chrome-mac/Chromium.app/Contents/MacOS/Chromium');
    const browser = await puppeteer.launch({
        headless: false,
        executablePath: pathToExtension
    });
    const page = await browser.newPage();
    await page.setViewport({width: 1000, height: 500});
    await page.goto('https://weibo.com/rmrb');
    await page.waitForNavigation();
    await page.screenshot({path: 'rmrb.png'});
    await browser.close();
})();
  • puppeteer.launch (передается, когда Puppeteer подключается к экземпляру Chromiumpuppeteer.launchилиpuppeteer.connectСоздайте объект браузера. )
    • executablePath: путь для запуска Chromium или Chrome.
    • headless: следует ли запускать браузер в безголовой форме. (Безголовый Chrome означает запуск Google Chrome в автономном режиме. Используется для автоматизированного тестирования и серверов, не требующих визуального пользовательского интерфейса.)
  • browser.newPage открывает новую страницу и возвращает обещание. Большинство методов в API Puppeteer вернутPromiseобъект, нам нужноasync+awaitС использованием.

запустить код

$ node screenshot.js

Скриншот будет сохранен в корневой каталог

Анализировать структуру страницы и извлекать новости

Наша цель — получить текст и дату Weibo, опубликованного People’s Daily.

  • Дом узла новостей: div[action-type=feed_list_item]
  • Новостной контент находится в dom: div[action-type=feed_list_item]>.WB_detail>.WB_text
  • Дом времени выпуска новостей: div[action-type=feed_list_item]>.WB_detail>.WB_from a").eq(0).attr("date")

Puppeteer предоставляет методы извлечения элементов страницы:Page.evaluate. Потому что он действует в контексте, в котором работает браузер. Когда мы загружаем страницу, используйтеPage.evaluateметод может использоваться для анализа узлов dom

page.evaluate(pageFunction, ...args)

  • pageFunction Метод для выполнения в контексте экземпляра страницы
  • ...args<...> Параметры для передачи в pageFunction
  • Возвращает: >pageFunctionрезультат исполнения

Если pageFunction возвращает [Promise], page.evaluate будет ждать завершения обещания и вернет его возвращаемое значение.

Если pageFunction возвращает значение, которое не может быть сериализовано, оно вернет неопределенное значение.

Код для анализа информации страницы Weibo выглядит следующим образом:

const LIST_SELECTOR = 'div[action-type=feed_list_item]'
return await page.evaluate((infoDiv)=> {
    return Array.prototype.slice.apply(document.querySelectorAll(infoDiv))
        .map($userListItem => {
            var weiboDiv = $($userListItem)
            var webUrl = 'http://weibo.com'
            var weiboInfo = {
                "tbinfo": weiboDiv.attr("tbinfo"),
                "mid": weiboDiv.attr("mid"),
                "isforward": weiboDiv.attr("isforward"),
                "minfo": weiboDiv.attr("minfo"),
                "omid": weiboDiv.attr("omid"),
                "text": weiboDiv.find(".WB_detail>.WB_text").text().trim(),
                'link': webUrl.concat(weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("href")),
                "sendAt": weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("date")
            };

            if (weiboInfo.isforward) {
                var forward = weiboDiv.find("div[node-type=feed_list_forwardContent]");
                if (forward.length > 0) {
                    var forwardUser = forward.find("a[node-type=feed_list_originNick]");
                    var userCard = forwardUser.attr("usercard");
                    weiboInfo.forward = {
                        name: forwardUser.attr("nick-name"),
                        id: userCard ? userCard.split("=")[1] : "error",
                        text: forward.find(".WB_text").text().trim(),
                        "sendAt": weiboDiv.find(".WB_detail>.WB_from a").eq(0).attr("date")
                    };
                }
            }
            return weiboInfo
        })
}, LIST_SELECTOR)

Передаем блок новостей LIST_SELECTOR в качестве параметраpage.evaluate,существуетpageFunctionМетод документа можно использовать для работы с узлом dom в верхней и нижней части экземпляра страницы функции. Пройдите блок новостей, проанализируйте структуру dom и получите соответствующую информацию.

Цитировать~

Поскольку я чувствую, что не привык манипулировать узлами DOM с помощью собственных методов JS (jQuery привык быть некомпетентным, я чрезвычайно зависим от jQuery... 2333), поэтому я решил позволить среде разработки поддерживать jQuery.

метод первый

page.addScriptTag(options)

Вставляет тег скрипта, указывающий src (url) или код (content) на текущую страницу.

  • options <[Object]>
    • url Источник скрипта для добавления

path Путь к js-файлу для вставки во фрейм. Если path является относительным путем, он разрешается относительно текущего пути. - код content js для внедрения на страницу (т.е.) type Тип скрипта. При внедрении модуля ES6 значением является «модуль». Нажмите сценарий, чтобы просмотреть подробности. - Возврат: > Объект Promise, то есть внедренный тег tag. Когда срабатывает загрузка скрипта или код вводится в фрейм.

Итак, добавляем прямо в код:

await page.addScriptTag({url: 'https://code.jquery.com/jquery-3.2.1.min.js'})

Тогда вы можете летать счастливо!

Способ второй

Если посещаемая веб-страница уже поддерживает jQuery, это еще удобнее!

await page.evaluate(()=> {
    var $ = window.$
})

Просто объявите переменную в pageFunction и присвойте ей значение $ в window.

Расширение заканчивается~

Примечание. В pageFunctin есть экземпляры страниц. Если вы используете такие методы, как document или jquery, в других местах программы, вам будет предложено указать среду документа или напрямую сообщить об ошибке.

(node:3346) UnhandledPromiseRejectionWarning: ReferenceError: document is not defined

Извлечь комментарии для каждой новости

Мало парсить новости, нужны еще топовые комментарии к каждой новости.

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

  1. Проанализировав элемент dom новостного блока, мы имитируем нажатие кнопки комментария.
$('.WB_handle span[node-type=comment_btn_text]').each(async(i, v)=>{
    $(v).trigger('click')
})
  1. Используйте событие события прослушивателя: «ответ» для прослушивания запросов страниц.

event: 'response'

  • <[Response]>

Запускается, когда запрос страницы получает соответствующий [ответ].

Рисунок: Когда мы нажимаем кнопку комментария, браузер отправляет много запросов, наша цель — извлечь запрос комментария.

Нам нужно использовать несколько методов в классе Response, чтобы отслеживать ответ браузера, анализировать и извлекать комментарии.

  • response.url() Contains the URL of the response.
  • response.text()
    • returns: <Promise> Promise which resolves to a text representation of response body.
page.on('response', async(res)=> {
    const url = res.url()
    if (url.indexOf('small') > -1) {
        let text = await res.text()
        var mid = getQueryVariable(res.url(), 'mid');
        var delHtml = delHtmlTag(JSON.parse(text).data.html)
        var matchReg = /\:.*?(?= )/gi;
        var matchRes = delHtml.match(matchReg)
        if (matchRes && matchRes.length) {
            let comment = []
            matchRes.map((v)=> {
                comment.push({mid, content: JSON.stringify(v.split(':')[1])})
            })
            pool.getConnection(function (err, connection) {
                save.comment({"connection": connection, "res": comment}, function () {
                    console.log('insert success')
                })
            })
        }
    }
})
  1. использоватьres.url()Получите URL-адрес ответа и определите, содержит ли строка ключевое слово small.
  2. Перехватите ключ в url: mid, mid используется для того, чтобы различать, к какой новости относится комментарий.
  3. использоватьres.text()Получите тело ответа и удалите html-теги dom в body.data.
  4. Извлеките содержимое комментария, которое нам нужно, в обычный текст.

Сохранить в Mysql

Используйте Mysql для хранения новостей и комментариев

$ npm i mysql -D mysql

Используемый нами mysql — это библиотека на основе node.js. Он написан на JavaScript и не требует компиляции.

  1. Создайте новый файл config.js, создайте подключение к локальной базе данных и экспортируйте конфигурацию.

config.js

var mysql = require('mysql');

var ip = 'http://127.0.0.1:3000';
var host = 'localhost';
var pool = mysql.createPool({
    host:'127.0.0.1',
    user:'root',
    password:'xxxx',
    database:'yuan_place',
    connectTimeout:30000
});

module.exports = {
    ip    : ip,
    pool  : pool,
    host  : host,
}
  1. Добавьте config.js в сканер
page.on('response', async(res)=> {
    ...
    if (matchRes && matchRes.length) {
        let comment = []
        matchRes.map((v)=> {
            comment.push({mid, content: JSON.stringify(v.split(':')[1])})
        })
        pool.getConnection(function (err, connection) {
            save.comment({"connection": connection, "res": comment}, function () {
                console.log('insert success')
            })
        })
    }
    ...
})
const content = await getWeibo(page)

pool.getConnection(function (err, connection) {
    save.content({"connection": connection, "res": content}, function () {
        console.log('insert success')
    })
})
  1. Затем мы пишем save.js для обработки логики вставки данных.

Две таблицы имеют следующую структуру:

Теперь мы можем начать радостно заливать данные в базу данных.

save.js

exports.content = function(list,callback){
    console.log('save news')
    var connection = list.connection
    async.forEach(list.res,function(item,cb){
        debug('save news',JSON.stringify(item));
        var data = [item.tbinfo,item.mid,item.isforward,item.minfo,item.omid,item.text,new Date(parseInt(item.sendAt)),item.cid,item.clink]
        if(item.forward){
            var fo = item.forward
            data = data.concat([fo.name,fo.id,fo.text,new Date(parseInt(fo.sendAt))])
        }else{
            data = data.concat(['','','',new Date()])
        }
        connection.query('select * from sina_content where mid = ?',[item.mid],function (err,res) {
            if(err){
                console.log(err)
            }
            if(res && res.length){
                //console.log('has news')
                cb();
            }else{
                connection.query('insert into sina_content(tbinfo,mid,isforward,minfo,omid,text,sendAt,cid,clink,fname,fid,ftext,fsendAt) values(?,?,?,?,?,?,?,?,?,?,?,?,?)',data,function(err,result){
                    if(err){
                        console.log('kNewscom',err)
                    }
                    cb();
                })
            }
        })
    },callback);
}
//把文章列表存入数据库
exports.comment = function(list,callback){
    console.log('save comment')
    var connection = list.connection
    async.forEach(list.res,function(item,cb){
        debug('save comment',JSON.stringify(item));
        var data = [item.mid,item.content]
        connection.query('select * from sina_comment where mid = ?',[item.mid],function (err,res) {
            if(res &&res.length){
                cb();
            }else{
                connection.query('insert into sina_comment(mid,content) values(?,?)',data,function(err,result){
                    if(err){
                        console.log(item.mid,item.content,item)
                        console.log('comment',err)
                    }
                    cb();
                });
            }
        })
    },callback);
}

Запустите программу и вы обнаружите, что данные уже есть в библиотеке.

Бесполезный и хлопотный аванс для проекта: mock login

Вам не нужно входить сюда, вы уже можете просматривать новости и комментарии. но! Как мы можем остановиться здесь в нашем стремлении к прогрессу. Сделайте несколько виджетов входа, которые бесполезны для проекта! Импортируйте его, если он вам нужен, оставьте как есть, если он вам не нужен.

Добавьте файл creds.js в корневой каталог проекта.

module.exports = {
  username: '<GITHUB_USERNAME>',
  password: '<GITHUB_PASSWORD>'
};
  1. использоватьpage.clickИмитация клика по странице

page.click(selector[, options])

  • selector A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
  • options
    • button left, right, or middle, defaults to left.
    • clickCount defaults to 1. See UIEvent.detail.
    • delay Time to wait between mousedown and mouseup in milliseconds. Defaults to 0.
    • returns: Promise which resolves when the element matching selector is successfully clicked. The Promise will be rejected if there is no element matching selector.

    Поскольку page.click возвращает обещание, используйте await для паузы.

    await page.click('.gn_login_list li a[node-type="loginBtn"]');
    

    1. await page.waitFor(2000) Подождите 2 секунды, пока не отобразится поле ввода.

    3. Используйтеpage.typeВводим логин и пароль (здесь добавляем скорость имитации пользовательского ввода{delay:30}параметры, которые можно изменить в соответствии с реальной ситуацией), а затем имитировать нажатие кнопки входа в систему, чтобы использоватьpage.waitForNavigation()Подождите, пока страница перейдет после успешного входа в систему.

    await page.type('input[name=username]',CREDS.username,{delay:30});
    await page.type('input[name=password]',CREDS.password,{delay:30});
    await page.click('.item_btn a');
    await page.waitForNavigation();
    

    Поскольку тестовая учетная запись, которую я использовал, не привязана к номеру мобильного телефона, описанный выше метод можно использовать для завершения входа в систему. Если вы привязаны к номеру мобильного телефона, вам необходимо использовать клиент для сканирования вторичной аутентификации.

    наконец

    Изображения рептилий:

    Демонстрация сканера здесь:GitHub.com/Wallace мета…

    Справочная документация:

    GitHub.com/Google CHROM… GitHub.com/Google CHROM…

    Если вы думаете, что это весело, пожалуйста, обратите внимание~ Приглашаем всех собраться и написать комментарий~~~

    Объявление о вакансии

    ByteDance набирает сотрудников!

    Описание вакансии: Front-end development (senior) ToB направление - видео облако (База: Шанхай, Пекин)

    1. Отвечает за производство мультимедийных услуг, таких как аудио и видео по запросу / прямая трансляция / общение в реальном времени и создание облачных платформ для бизнеса;

    2. Отвечает за мультимедийную систему качества, эксплуатацию и техническое обслуживание, построение и развитие системы;

    3. Хорошо разбирается в абстрактном дизайне, инженерном мышлении, фокусируется на взаимодействии и создает максимальный пользовательский опыт.

    Профессиональные требования

    1. Компьютеры, связь и электронная информатика и другие смежные специальности являются предпочтительными;

    2. Владение различными фронтенд-технологиями, включая HTML/CSS/JavaScript/Node.js и т. д.;

    3. Глубокое понимание языка JavaScript с использованием основных сред разработки, таких как React или Vue.js;

    4. Приветствуется знание Node.js, понимание Express/KOA и других фреймворков, а также опыт разработки крупномасштабных серверных программ;

    5. Иметь определенное представление о пользовательском опыте, интерактивной работе и анализе потребностей пользователей, а также иметь опыт разработки продуктов или интерфейсов;

    6. Предпочтение отдается участникам со своими собственными техническими продуктами, работами с открытым исходным кодом или активным сообществом с открытым исходным кодом.

    Основные моменты работы

    Команда видеооблачных сервисов опирается на накопленные аудио- и видеотехнологии и базовые ресурсы Douyin, Xigua Video и других продуктов, чтобы предоставлять клиентам универсальные аудио- и видеомультимедийные услуги, включая аудио- и видеопо запросу, прямую трансляцию, реальную связь времени, обработка изображений и т. д. Внутренне, как центр видеотехнологий, он обслуживает внутренний бизнес; снаружи он создает готовые аудио- и видео-мультимедийные сервисные решения для обслуживания пользователей корпоративного уровня.

    У команды есть стандартизированный процесс итерации проекта и идеальная конфигурация ролей проекта; сильная техническая атмосфера, охватывает сообщество открытого исходного кода и регулярно делится, так что каждый может быстро расти вместе с бизнесом и использовать технологии, чтобы изменить мир!

    способ доставки

    Резюме можно отправлять напрямую по адресу:yuanyuan.wallace@bytedance.com

    Вы также можете отсканировать внутренний QR-код для доставки онлайн, с нетерпением жду вашего присоединения! ~