Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一个前端小白的"爬虫"初试 #26

Open
qianlongo opened this issue Aug 5, 2018 · 2 comments
Open

一个前端小白的"爬虫"初试 #26

qianlongo opened this issue Aug 5, 2018 · 2 comments
Labels

Comments

@qianlongo
Copy link
Owner

qianlongo commented Aug 5, 2018

前言

八月。透蓝的天空,悬着火球般的太阳,云彩好似被太阳烧化了,也消失得无影无踪。没有一丝风,大地活像一个蒸笼。

好热,好烦躁,好无聊。无意间又打开知乎😋,首页冒出一个问题给好看的女生拍照是种怎样的体验?,齐刷刷一大摞好看的小姐姐,看的人好生陶醉。作为一个曾经的理工屌丝男,我相信此刻你的想法和我一样,要是可以把她们装进那《学习教程》文件夹就好了。

怎么办?一张张图片右键保存吗?不不不,效率低,鼠标按多了“右手”还疼😈。差点忘了,我特么是个程序员啊!程序员啊!程序员啊!这种事难道不应该交给程序去干嘛。

"说干就干"

原文地址
源码地址

需求

需求很简单:希望知乎APP自适应用户手机壳颜色。

啊呸呸呸,应该是

需求很简单:实现自动下载知乎某个帖子下所有回答的图片到本地

分析

需求很明确,所以我想只要知道了以下两点基本就能够完成了。

  1. 图片链接

能够获取帖子下面答题者上传的图片链接,至于所有图片,那就是所有回答者上传的图片链接了

  1. 下载图片

这个暂时猜想是使用成熟的库,我只需要传入图片链接地址,以及图片下载到哪个目录就可以完成。如果没找着这样的库,就只能研究原生的nodejs如何做了。

针对1,我们打开chrome浏览器的控制台,发现页面一打开的时候会有很多个请求发出,但是有一个带"answers"请求很可疑,是不是它负责返回答题者的答案呢?

在验证这个想法之前,我们先不去看这个请求的具体响应内容。我们先点击一下页面上的查看全部 948 个回答按钮,如果猜的对,"answers"请求应该会再次发出,并且返回答题者的答案。

点击按钮之后发现,“answers”确实再次发出了,并且查看其响应内容大体如下

{
  data: [
    {
      // xxx
      // 答题者的信息
      author: {
        // ...
      },
      // 答题内容
      content: '"就是觉得太美好了呀<br><br><figure><noscript><img data-rawheight="1080" src="https://pic4.zhimg.com/v2-a7da381efb1775622c497fb07cc40957_b.jpg" data-rawwidth="720" class="origin_image zh-lightbox-thumb" width="720" data-original="https://pic4.zhimg.com/v2-a7da381efb1775622c497fb07cc40957_r.jpg"></noscript></figure>',
      // 帖子描述
      question: {}
      // xxx 等等
    },
    {
      /// xxx
    }
  ],
  paging: {
    // 是否结束
    is_end:false,
    // 是否是刚开始
    is_start:false,
    // 查看下一页内容的api地址
    next: "https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=8&sort_by=default",
    // 上一页内容的api地址
    previous: "https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=0&sort_by=default",
    // 总回答数
    totals: 948
  }
}

从响应中我们拿到总的回答数量,以及当前请求返回的答题者的内容也就是content字段,我们要的图片地址就在noscript标签下的img标签的data-original属性中。所以针对要求1,我们似乎已经拿到了50%的信息,还有另一半的信息是,我们如何获取所有答题者的内容?,别忘了刚才的响应中还有paging字段,其中。

// 是否结束
is_end:false,
// 查看下一页内容的api地址
next: 'https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=8&sort_by=default"',
// 总回答数
totals: ''

再结合"answers"这个请求的路径

https://www.zhihu.com/api/v4/questions/49364343/answers?include=data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,relevant_info,question,excerpt,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp;data[*].mark_infos[*].url;data[*].author.follower_count,badge[?(type=best_answerer)].topics&offset=3&limit=5&sort_by=default

其中路径部分
https://www.zhihu.com/api/v4/questions/49364343/answers,49364343应该就是帖子的id

query请求部分总共有三个参数

{
  include: 'xxxx', // 这个参数可能是知乎后台要做的各种验证吧
  offset: 3, // 页码
  limit: 5, // 每页内容数量
  sort_by: 'default' // 排序方式
}

所以看起来,咱们把offset设置为0,limit设置为totals的值,是不是就可以拿到所有数据了呢?尝试之后发现,最多只能拿到20个答题者的数据,所以我们还是根据is_end以及next两个响应值,多次请求,逐步获取所有数据吧。

针对2. 最后一顿google搜索发现还真有这么一个库request,比如要下载一张在线的图片到本地只需要些如下代码

const request = require('request)

request('http://google.com/doodle.png')
  .pipe(fs.createWriteStream('doodle.png'))

到这里1和2两个条件都具备了,接下来要做的就是撸起来,写代码实现了。

预览

在说代码实现之前,我们先看一个录制的gif,以及如何使用crawler.js

点击查看gif

使用

require('./crawler')({
  dir: './imgs', // 图片存放位置
  questionId: '34078228', // 知乎帖子id,比如https://www.zhihu.com/question/49364343/answer/157907464,输入49364343即可
  proxyUrl: 'https://www.zhihu.com' // 当请求知乎的数量达到一定的阈值的时候,会被知乎认为是爬虫(好像是封ip),这时如果你如果有一个代理服务器来转发请求数据,便又可以继续下载了。
})

proxyUrl先不关注,后面会仔细说明这个字段的作用

源码实现

点击查看crawler.js

let path = require('path')
let fs = require('fs')
let rp = require('request-promise')
let originUrl = 'https://www.zhihu.com'

class Crawler {
  constructor (options) {
    // 构造函数中主要是一些属性的初始化
    const { dir = './imgs', proxyUrl = originUrl, questionId = '49364343', offset = 0, limit = 100, timeout = 10000 } = options
    // 非代理模式下请求知乎的原始url默认是 https://www.zhihu.com
    this.originUrl = originUrl
    // 代理模式下请求的实际路径, 这里默认也是https://www.zhihu.com
    // 当你的电脑ip被封了之后,可以通过代理服务器,请求知乎,而我们是向代理服务器获取数据
    this.proxyUrl = proxyUrl
    // 请求的最终url
    this.uri = `${proxyUrl}/api/v4/questions/${questionId}/answers?limit=${limit}&offset=${offset}&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&sort_by=default`
    // 是否已经是最后的数据
    this.isEnd = false
    // 知乎的帖子id
    this.questionId = questionId
    // 设置请求的超时时间(获取帖子答案和下载图片的超时时间目前相同)
    this.timeout = timeout
    // 解析答案后获取的图片链接
    this.imgs = []
    // 图片下载路径的根目录
    this.dir = dir
    // 根据questionId和dir拼接的最终图片下载的目录
    this.folderPath = ''
    // 已下载的图片的数量
    this.downloaded = 0
    // 初始化方法
    this.init()
  }

  async init () {
    if (this.isEnd) {
      console.log('已经全部下载完成, 请欣赏')
      return
    }
    // 获取帖子答案
    let { isEnd, uri, imgs, question } = await this.getAnswers()

    this.isEnd = isEnd
    this.uri = uri
    this.imgs = imgs
    this.downloaded = 0
    this.question = question
    console.log(imgs, imgs.length)
    // 创建图片下载目录
    this.createFolder()
    // 遍历下载图片
    this.downloadAllImg(() => {
      // 当前请求回来的所有图片都下载完成之后,继续请求下一波数据
      if (this.downloaded >= this.imgs.length) {
        setTimeout(() => {
          console.log('休息3秒钟继续下一波')
          this.init()
        }, 3000)
      }
    })
  }
  // 获取答案
  async getAnswers () {
    let { uri, timeout } = this
    let response = {}

    try {
      const { paging, data } = await rp({ uri, json: true, timeout })
      const { is_end: isEnd, next } = paging
      const { question } = Object(data[0])
      // 将多个答案聚合到content中
      const content = data.reduce((content, it) => content + it.content, '')
      // 匹配content 解析图片url
      const imgs = this.matchImg(content)

      response = { isEnd, uri: next.replace(originUrl, this.proxyUrl), imgs, question }
    } catch (error) {
      console.log('调用知乎api出错,请重试')
      console.log(error)
    }

    return response
  }
  // 匹配字符串,从中找出所有的图片链接
  matchImg (content) {
    let imgs = []
    let matchImgOriginRe = /<img.*?data-original="(.*?)"/g

    content.replace(matchImgOriginRe, ($0, $1) => imgs.push($1))

    return [ ...new Set(imgs) ]
  }
  // 创建文件目录
  createFolder () {
    let { dir, questionId } = this
    let folderPath = `${dir}/${questionId}`
    let dirs = [ dir, folderPath ]

    dirs.forEach((dir) => !fs.existsSync(dir) && fs.mkdirSync(dir))

    this.folderPath = folderPath
  }
  // 遍历下载图片
  downloadAllImg (cb) {
    let { folderPath, timeout } = this
    this.imgs.forEach((imgUrl) => {
      let fileName = path.basename(imgUrl)
      let filePath = `${folderPath}/${fileName}`

      rp({ uri: imgUrl, timeout })
        .on('error', () => {
          console.log(`${imgUrl} 下载出错`)
          this.downloaded += 1
          cb()
        })
        .pipe(fs.createWriteStream(filePath))
        .on('close', () => {
          this.downloaded += 1
          console.log(`${imgUrl} 下载完成`)
          cb()
        })
    })
  }
}

module.exports = (payload = {}) => {
  return new Crawler(payload)
}

源码实现基本上很简单,大家看注释就可以很快明白。

ip被封

正当我用写好的crawler.js下载多个帖子下面的图片的时候,程序报了一个这个提示。

系统检测到您的帐号或IP存在异常流量,请进行验证用于确认这些请求不是自动程序发出的"

完蛋了,知乎不让我请求了😭😭😭。

完蛋了,知乎不让我请求了😭😭😭。

完蛋了,知乎不让我请求了😭😭😭。

折腾了半天,最后被当做爬虫给封了。网上找了一些解决方法,例如爬虫怎么解决封IP?

基本上是两个思路

1、放慢抓取速度,减小对于目标网站造成的压力。但是这样会减少单位时间类的抓取量。

2、第二种方法是通过设置代理IP等手段,突破反爬虫机制继续高频率抓取。但是这样需要多个稳定的代理IP。

继续用本机并且在ip没有发生变化的情况下,直接请求知乎是不可能了,不过我们可以尝试一下2.使用代理服务器。突然想起自己去年在搬瓦工买了一个服务器,😀。平时除了用它作为vpn存在访问一些被墙的网站外,就只放了一个resume-native程序。虽然没法做到像上面两张图一样,哪个代理服务被封,及时再切换另一个代理服务器。但是至少可以通过代理服务器再次下载图片,撸起来。。。

另找出路

代理程序proxy.js运行在服务器上,会监测路径为/proxy*的请求,请求到来的时候通过自己以前写的请求转发httpProxy中间件去知乎拉取数据,再返回响应给我们本地。用一张图表示如下

所以我们原来的请求路径是(为了简化把include这个很长的query参数去除了)
https://www.zhihu.com/api/v4/questions/49364343/answers?offset=3&limit=5&sort_by=default

经过代理服务器后变成了(其中xxx.yyy.zzz可以是你自己的代理服务器的域名或者ip加端口)
https://xxx.yyy.zzz/proxy/api/v4/questions/49364343/answers?offset=3&limit=5&sort_by=default

点击查看代理模式gif图左侧是服务器上打印的信息,右侧是本地打印的信息

这样我们间接地绕过了知乎封ip的尴尬,不过这只是临时方案,终究代理服务器也会被封ip。

结尾

好快,一眨眼就下午5点了。这个简单的"爬虫"初试,或者根本就算不上什么爬虫,也有许多不完善的地方,就先放一放啦。天气稍微凉爽了些,该出去走走了。

如果你喜欢,请点一颗星星噢😯

如果你喜欢,请点一颗星星噢😯

如果你喜欢,请点一颗星星噢😯

原文地址
源码地址

@qop
Copy link

qop commented Aug 6, 2018

最后的链接,少了一个《学习教程》文件夹地址啊

@qianlongo
Copy link
Owner Author

@qop 哈哈哈哈

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants