Путь обновления сканера узлов для борьбы с монстрами

Node.js рептилия API koa

Я всегда чувствовал, что поисковые роботы — неизбежная вещь для многих веб-разработчиков. Мы также должны коснуться этого аспекта более или менее, потому что мы можем получить некоторые базовые знания в веб-разработке от краулеров. Кроме того, это весело.

Я немного активный пользователь Zhihu. Раньше я написал краулер, который помогает мне сканировать и анализировать его данные. Я думаю, что этот процесс довольно интересен, потому что это процесс постоянного создания и решения проблем для себя. Среди них есть некоторые моменты, которые я подытожу и поделюсь с вами сегодня.

На чем ползает?

Позвольте мне сначала кратко представить мой сканер. Он может регулярно привлекать внимание, просмотры страниц и ответы на вопросы, так что я могу отображать эти данные в диаграммах, чтобы показать его горячие тенденции. Чтобы не позволить мне пропустить некоторые популярные события, он также будет регулярно получать популярные вопросы и ответы по темам, на которые я подписан, и отправлять их в мой почтовый ящик.

Как фронтенд-разработчик, я должен создать интерфейс для этой краулер-системы, который позволяет мне входить в свою учетную запись Zhihu, добавлять темы и темы, представляющие интерес, и просматривать визуализированные данные. Так вот этот краулер тоже имеет функцию входа в Zhihu и поиска тем.

Тогда посмотрите на интерфейс.

Далее следует серьезный разговор о процессе его разработки.

Технический отбор

Благодаря простому и быстрому синтаксису и богатой библиотеке поисковых роботов Python всегда был предпочтительным выбором для разработчиков поисковых роботов. К сожалению, я с ним не знаком. Конечно, самое главное то, что для фронтенд-разработчика, если Node.js может удовлетворить потребности поисковых роботов, естественно, это первый выбор. А с развитием узла появилось много полезных краулерных библиотек, и даже некоторыеpuppeteerТаким образом, запуск инструмента, который может напрямую имитировать доступ Chrome к веб-страницам, узел должен быть в состоянии должным образом удовлетворить все мои потребности сканера с точки зрения сканера.

Поэтому я решил создать сервер на основе koa2 с нуля. Почему бы просто не выбрать более комплексные фреймворки, такие как egg, express, thinkjs? Потому что я люблю бросать. И это тоже процесс обучения. Если вы раньше не знали node и заинтересованы в создании сервера node, вы можете прочитать мою предыдущую статью —Создайте сервер Koa2 с нуля.

я выбрал гусеничныйrequest+cheerio. Хотя React используется во многих местах в Zhihu, большинство страниц по-прежнему отображаются на стороне сервера, поэтому, пока я могу запрашивать веб-страницы и интерфейсы (запрос), анализ страницы (cherrio) может удовлетворить мои потребности сканера.

Остальные не перечислены по порядку, я перечислю стек технологий

Сервер

  1. koajsУ узла сервера;
  2. request + cheerioВыполнять поисковые услуги;
  3. mongodbсделать хранение данных;
  4. node-scheduleпланировать задачи;
  5. nodemailerДелайте рассылку по электронной почте.

клиент

  1. vuejsинтерфейсный фреймворк;
  2. museuiБиблиотека пользовательского интерфейса Material Design;
  3. chart.jsБиблиотека графиков.

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

Как парсить данные сайта?

У Zhihu нет открытого интерфейса для получения пользователями данных, поэтому, если вы хотите получить данные, вам придется самостоятельно сканировать информацию веб-страницы. Мы знаем, что даже веб-страница по сути является интерфейсом GET-запроса, нам нужно только запросить адрес соответствующей веб-страницы на стороне сервера (клиентский запрос будет междоменным), а затем разобрать html-структуру, чтобы получить нужные данные. .

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

Имитация входа

Каждый будет использовать Chrome и другие современные браузеры для просмотра информации о запросе.Мы входим на страницу входа в Zhihu, а затем проверяем полученную информацию об интерфейсе, чтобы знать, что вход в систему — это не что иное, как отправка учетной записи, пароля и другой информации в API входа в систему. , в случае успеха. Сервер установит файл cookie для клиента, который является учетными данными для входа.

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

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

Изюминка заключается в том, что даже если вы отправите правильный код подтверждения, вам все равно будет предложено указать, что код подтверждения неверен. Если мы сами создали систему отправки проверочного кода, мы можем быстро найти причину. Если мы еще этого не сделали, мы снова смотрим на запросы и ответы, участвующие в входе в систему, и мы также можем предположить:

Когда клиент получает код подтверждения, сервер Zhihu также устанавливает новый файл cookie для клиента.При отправке запроса на вход код подтверждения должен быть отправлен вместе с этим файлом cookie, чтобы убедиться, что код подтверждения, отправленный на этот раз, действительно предоставлен код подтверждения пользователя в то время.

Описание языка немного запутано, и я представляю весь процесс запроса входа в систему в виде диаграммы.

Примечание. Когда я писал поисковый робот, Zhihu также частично использовал коды проверки символов изображения, и теперь все они были изменены на форму «щелкнуть перевернутый текст». Это делает отправку правильной капчи более сложной, но не невозможной. После получения изображения,искусственныйОпределите и щелкните перевернутый текст и отправьте координаты нажатия на интерфейс входа. Конечно, заинтересованные и способные студенты могут также написать свой собственный алгоритм для идентификации кода проверки.

Сканировать данные

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

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

После того, как основы сканера выполнены, вы можете получить нужные данные. Мое требование - знать горячую тенденцию определенной проблемы Zhihu. Во-первых, используйте браузер, чтобы увидеть, какие данные находятся на проблемной странице, которые я могу просканировать и проанализировать. Например, такой как этот вопрос:Какие есть удивительные мосты рассуждений?.

После открытия ссылки самый прямой дисплей на страницеПоследователи,просмотрел,1хххх Ответы, а также несколько ответов с высоким лайком и количество лайков и комментариев, отображаемых по умолчанию. Щелкните правой кнопкой мыши, чтобы просмотреть исходный код веб-сайта и подтвердить, что данные отображаются сервером.Мы можем запросить веб-страницу через запрос, а затем использовать селектор css, чтобы найти узел данных через cherrio, получить и сохранить его. Пример кода выглядит следующим образом:

async getData (cookie, qid) {
  const options = {
    url: `${zhihuRoot}/question/${qid}`,
    method: 'GET',
    headers: {
      'Cookie': cookie,
      'Accept-Encoding': 'deflate, sdch, br' // 不允许gzip,开启gzip会开启知乎客户端渲染,导致无法爬取
    }
  }
  const rs = await this.request(options)
  if (rs.error) {
    return this.failRequest(rs)
  }
  const $ = cheerio.load(rs)
  const NumberBoard = $('.NumberBoard-item .NumberBoard-value')
  const $title = $('.QuestionHeader-title')
  $title.find('button').remove()
  return {
    success: true,
    title: $title.text(),
    data: {
      qid: qid,
      followers: Number($(NumberBoard[0]).text()),
      readers: Number($(NumberBoard[1]).text()),
      answers: Number($('h4.List-headerText span').text().replace(' 个回答', ''))
    }
  }
}

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

Итак, вопрос в том, как выполнить это запланированное задание?

задача на время

я использовалnode-scheduleЗаймитесь планированием задач. Если вы уже выполняли временные задачи раньше, вы можете быть знакомы с его синтаксисом, подобным cron, но не имеет значения, если вы не знакомы с ним.Он предоставляет не похожие на cron, более интуитивно понятные настройки для настройки задач, можно получить общее представление, прочитав документацию.

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

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

В системе также есть две другие временные задачи: одна — регулярно сканировать популярные ответы на тему, которая беспокоит пользователей, а другая — отправлять популярные ответы на эту тему соответствующим пользователям. Эти две задачи примерно такие же, как и вышеперечисленные, и подробно обсуждаться не будут.

Однако, когда мы выполняем временные задачи, возникает детальная проблема, то есть как контролировать проблему параллелизма во время сканирования. Например, если запросы сканера слишком одновременны, Zhihu может ограничить доступ к этому IP-адресу, поэтому нам нужно позволить сканеру запрашивать один за другим или несколько за другим.

Для простоты возьмем цикл await. Недолго думая, я написал следующий код:

// 爬虫方法
async function getQuestionData () {
  // do spider action
}

// questions为获取到的关注问答
questions.forEach(await getQuestionData)

Однако после выполнения мы обнаружим, что на самом деле это выполняется одновременно.Почему? На самом деле, хорошенько подумайте. forEach — это просто синтаксический сахар для циклов, если у вас нет этого метода, пусть вы реализуете его, как бы вы его написали? Вы, наверное, тоже писали:

Array.prototype.forEach = function (callback) {
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this)
  }
}

несмотря на то чтоforEachЭто немного сложнее само по себе, но это все. На этот раз мы передаем асинхронный метод в качестве параметраcallbackПередайте его, а затем выполните в цикле. Это выполнение по-прежнему параллельное, а не синхронное.

Поэтому, если мы хотим реализовать настоящий синхронный запрос, нам все равно нужно использовать цикл for для его выполнения, как показано ниже:

async function getQuestionData () {
  // do spider action
}
for (let i = 0; i < questions.length; i++) {
  await getQuestionData()
}

Кроме цикла for можно использовать и for-of, если вам интересен этот аспект, то можете подробнее узнать о нескольких методах обхода массива, и, кстати, изучить итераторы ES6Iterator.

На самом деле, если объем бизнеса большой, даже этого недостаточно. Также необходимо дополнительно разделить детализацию задач и даже добавить IP-адрес прокси для распределения запросов.

Разумная конструкция сервера

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

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

Разумное расслоение

Я видел некоторые нод-сервисы, написанные студентами фронтенда, и они часто пишут все интерфейсы (роутерные действия) системы в один файл, лучше, они будут разбиты на несколько файлов по модулю.

Но если мы столкнулись с другими зрелыми серверными фреймворками или получили некоторые знания J2EE в университете, мы инстинктивно выполним некоторые уровни:

  1. modelслой данных. Ответственность за сохранение данных, вообще говоря, заключается в подключении к базе данных, соответствующей модели данных сущности таблицы базы данных;
  2. serviceслой бизнес-логики. Как следует из названия, он отвечает за реализацию различной бизнес-логики.
  3. controllerконтроллер. Вызывайте службы бизнес-логики, реализуйте передачу данных и возвращайте клиентские представления или данные.

Конечно, некоторые фреймворки или люди будут использовать бизнес-логику.serviceреализован вcontrollerв, илиmodelв слое. Я лично считаю, что слегка сложный проект должен быть изолирован от абстрактной бизнес-логики.

Например, в моей системе обхода я нажимаю операцию добавления, удаления, изменения и проверки базы данных.modelСоответствующее извлечение слояservice, а затем извлеките службу сканирования страниц, службу отправки почты и службу аутентификации пользователей в соответствующиеservice.

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

Практика наслоения на коа

Если мы напрямую используем зрелую внутреннюю структуру, нам не нужно слишком много думать о многоуровневости. Существуют также такие фреймворки, как node, который я представил ранее на нашей фабрике, с открытым исходным кодом.api-mockerусыновленныйegg.js, а также помогли нам сделать разумную стратификацию.

Но если вы создадите сервер с нуля на основе koa, вы столкнетесь с некоторыми неудачами в этом отношении. Логика самого koa очень проста: для обработки запроса вызывается ряд промежуточных программ (то есть одна функция). предоставлено официальнымkoa-router, то есть помочь нам определить путь запроса, а затем загрузить соответствующий метод интерфейса.

Чтобы различать бизнес-модули, мы напишем некоторые методы интерфейса в одном и том жеcontroller, такой как мойquestionControllerОтвечает за обработку интерфейсов, связанных с проблемами;topicControllerОтвечает за обработку тематических интерфейсов.

Затем мы могли бы написать файл маршрутизации следующим образом:

const Router = require('koa-router')
const router = new Router()

const question = require('./controller/question')
const topic = require('./controller/topic')

router.post('/api/question', question.create)
router.get('/api/question', question.get)

router.get('/api/topic', topic.get)
router.post('/api/topic/follow', topic.follow)

module.exports = router

Мой файл вопросов может быть записан так:

class Question {
  async get () {
    // return data
  }
  async create () {
    // create question and return data
  }
}

module.exports = new Question()

Значит проблема на подходе.

Невозможно записать его в объектно-ориентированной форме, просто написав такcontrollerиз. Зачем?

Поскольку мы передали метод свойства объекта вопроса в качестве промежуточного программного обеспечения дляkoa-router, затем поkoaНижний уровень для включения этих методов промежуточного программного обеспечения, передаваемых в качестве параметров вhttp.createServerВ способе он окончательно вызывается, когда запрос прослушивается узлом underlere. что этоthisКто в конце концов будет, без отладки, или увидеть koa и исходный код узла, неизвестно. Но в любом случае этот вызывающий метод определенно не является самим объектом (на самом деле, это был быundefined).

То есть мы не можем пройтиthisчтобы получить свойства или методы самого объекта.

тогда что нам делать? Некоторые учащиеся могут решить написать некоторые из своих собственных общедоступных методов непосредственно вclassвнешний или написанный наutilsФайл, а затем используйте его в интерфейс. Например:


const error = require('utils/error')

const success = (ctx, data) => {
  ctx.body = {
    success: true,
    data: data
  }
}

class Question {
  async get () {
    success(data)
  }
  async create () {
    error(result)
  }
}

module.exports = new Question()

Это действительно хорошо, но появятся новые проблемы - эти методы не являются свойствами самой объекта, и не существует способа унаследовать подклассы.

Зачем вам наследование? Потому что иногда мы хотим чего-то другогоcontrollerИмейте общедоступные методы или свойства, например: я хочу, чтобы все мои успехи или неудачи были в этом формате:

{
  success: false,
  message: '对应的错误消息'
}
{
  success: true,
  data: '对应的数据'
}

согласно сkoaОсновная идея этого общего преобразования формата должна заключаться в том, чтобы специально написать промежуточное программное обеспечение, после промежуточного программного обеспечения маршрутизации (то есть после выполнения метода в контроллере), выполнить специальную обработку и ответить.

Однако это приведет к необходимости добавления промежуточного программного обеспечения для каждого общедоступного метода. а такжеcontrollerсама потеряла контроль над этими методами. Выполняется ли это промежуточное ПО само или напрямуюnext()будет очень сложно судить.

Если он извлечен вutilsНет ничего невозможного в том, чтобы снова сослаться на метод, то есть, если методов много, объявление ссылки чуть более хлопотно, да и смысла в абстрактном классе нет.

Более идеальное состояние должно быть, как только что сказано, каждый наследует абстрактный родительский класс, а затем вызывает общедоступный соответствующий метод родительского класса, например:

class AbstractController {
  success (ctx, data) {
    ctx.body = {
      success: true,
      data: data
    }
  }
  error (ctx, error) {
    ctx.body = {
      success: false,
      msg: error
    }
  }
}
class Question extends AbstractController {
  async get (ctx) {
    const data = await getData(ctx.params.id)
    return super.success(ctx, data)
  }
}

Это гораздо удобнее, но у людей, написавших коа, могут быть такие заморочки, контекстctxВсегда передавайте его как параметр. Например, все промежуточные методы вышеуказанного контроллера должны проходитьctxParameters, при вызове метода родительского класса вам придется передавать его заново, что также приведет к тому, что метод потеряет некоторую читабельность.

Итак, резюмируя, у нас есть следующие вопросы:

  1. controllerМетод in не может вызывать другие методы и свойства самого себя;
  2. При вызове метода родительского класса необходимо передать параметр контекстаctx.

почини это

На самом деле решение очень простое, нужно только найти способ сделатьcontrollerЭто в методе указывает на сам экземпляр объекта, а затемctxдержись за этоthisна.

Как это сделать? Нам просто нужно перепаковатьkoa-routerВот так, выглядит это:

const Router = require('koa-router')
const router = new Router()
const question = require('./controller/question')
const topic = require('./controller/topic')

const routerMap = [
  ['post', '/api/question', question, 'create'],
  ['get', '/api/question', question, 'get'],
  ['get', '/api/topic', topic, 'get'],
  ['post', '/api/topic/follow', topic, 'follow']
]

routerMap.map(route => {
  const [ method, path, controller, action ] = route

  router[method](path, async (ctx, next) =>
    controller[action].bind(Object.assign(controller, { ctx }))(ctx, next)
  )
})

module.exports = router

Эффект состоит в том, чтобы пройти маршрутизациюcontrollerметод,controllerсебя иctxКомбинированный,bindопределяет методthis. чтобы мы могли пройтиthisполучить методcontrollerдругие методы объекта. Кроме того, методы подкласса и методы суперкласса также могут передаваться.this.ctxчтобы получить объект контекстаctx.

ноbindНа самом деле мы должны рассмотреть следующее перед тем, будут ли другое промежуточное ПО и сам koa делать подобные вещи и изменять значение this. Как судить, есть два пути:

  1. отладка. прежде чем мыbindПрежде распечатайте его в методе промежуточного программного обеспеченияthis,ДаundefinedЕстественно, не привязано.
  2. См. исходный код koa-router/koa/node.

Правда в том, что природы не существует. Тогда мы можем быть увереныbindБар.

напиши в конце

Вышеизложенное, вероятно, является некоторыми из моментов, возникших при написании этого гаджета, которые, как мне кажется, можно обобщить. Технических сложностей нет, но вы можете получить некоторые сопутствующие знания, включая безопасность веб-сайтов, сканирование и анти-сканирование, основные принципы koa и так далее.

Сам инструмент очень индивидуален и может не удовлетворить потребности каждого. И написано это было полгода назад, но я недавно выкопал себе могилу, чтобы подвести итог. И только когда я собирался закончить писать статью, я обнаружил, что Zhihu указал, что мой аккаунт небезопасен. Я предполагаю, что это потому, что один и тот же IP-адрес и одна и та же учетная запись инициируют слишком много сетевых запросов.IP-адрес моего сервера уже считается небезопасным IP-адресом, и учетным записям, вошедшим в него, будет предложено быть небезопасным. Поэтому я не рекомендую вам использовать его напрямую.

Конечно, если он вам все еще интересен, не составляет большой проблемы протестировать его локально или научиться им пользоваться. Или, если вам больше интересно, вы можете попробовать обойти политику безопасности Zhihu самостоятельно.

последний последний прикрепленныйАдрес проекта на GitHub

--читать оригинал

--Пожалуйста, получите мое разрешение перед перепечаткой.