一个前端小白的爬虫初试

news/2024/7/19 9:28:31 标签: 爬虫, 前端, json

前言

八月。透蓝的天空,悬着火球般的太阳,云彩好似被太阳烧化了,也消失得无影无踪。没有一丝风,大地活像一个蒸笼。
好热,好烦躁,好无聊。无意间又打开知乎?,首页冒出一个问题 给好看的女生拍照是种怎样的体验?,齐刷刷一大摞好看的小姐姐,看的人好生陶醉。作为一个曾经的理工屌丝男,我相信此刻你的想法和我一样,要是可以把她们装进那《学习教程》文件夹就好了。

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

"说干就干"

原文地址
源码地址

需求

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

啊呸呸呸,应该是

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

分析

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

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

    这个暂时猜想是使用成熟的库,我只需要传入图片链接地址,以及图片下载到哪个目录就可以完成。如果没找着这样的库,就只能研究原生的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/...

经过代理服务器后变成了(其中xxx.yyy.zzz可以是你自己的代理服务器的域名或者ip加端口)
https://xxx.yyy.zzz/proxy/api...

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

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

结尾

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

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

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

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

原文地址
源码地址


http://www.niftyadmin.cn/n/1379055.html

相关文章

linux安装mysql默认的配置文件_关于Linux安装mysql默认配置文件位置详解

在linux下面安装mysql如果在/etc下面没有存在my.cnf配置文件解决方式如下:1.通过which mysqld命令来查看mysql的安装位置2.通过/usr/local/mysql/bin/mysqld --verbose --help |grep -A 1 "Default options"命令来查看mysql使用的配置文件默认路径,(注意红色标注的是…

Appium python自动化测试系列之Android知识讲解(三)

​3.1 ADB工具讲解 3.1.1 什么是ADB呢&#xff1f; 我们不去解释官方语言的翻译&#xff0c;给大家说一个通熟易懂的说法&#xff0c;ADB我理解为他就是电脑和手机连接的桥梁。此连接不是充电的连接&#xff0c;大家不要混淆&#xff0c;说他是一个调试工具&#xff0c;可能更贴…

mysql 删除重复索引吗_MySQL数据库中删除重复记录的方法总结[推荐]

表结构&#xff1a;mysql> desc demo;-------------------------------------------------------------| Field | Type | Null | Key | Default | Extra |-------------------------------------------------------------| id | int(11) unsigned | NO | PRI | NULL | auto_…

DBCP的使用

DBCP&#xff08;DataBase Connection Pool&#xff09;是由apace提供的数据库连接池组件。 使用过程如下&#xff1a; 1.导入相关的包&#xff0c;注意dbcp2和dbcp1对jdk版本要求是不一样的。除此之外还需要comms-logging包和commons-pool2包&#xff0c;这些包都可以在Apache…

【leetcode】885. Boats to Save People

题目如下&#xff1a; 解题思路&#xff1a;本题可以采用贪心算法&#xff0c;因为每条船最多只能坐两人&#xff0c;所以在选定其中一人的情况下&#xff0c;再选择第二个人使得两人的体重最接近limit。考虑到人的总数最大是50000&#xff0c;而每个人的体重最大是30000&#…

爬格子呀--IEEE极限编程大赛留念

10.14&#xff0c;坐标&#xff1a;电子科技大学 24h&#xff0c;不间断的编程&#xff0c;感觉还是很爽的。 排名一般&#xff0c;但是这是开始&#xff0c;未来还很远。 题目举例1&#xff1a; 广袤的非洲大草原上&#xff0c;狮子居住在一个个的网格里&#xff0c;他们的…

[BZOJ4197][Noi2015]寿司晚宴

4197: [Noi2015]寿司晚宴 Time Limit: 10 Sec Memory Limit: 512 MBSubmit: 412 Solved: 279[Submit][Status][Discuss]Description 为了庆祝 NOI 的成功开幕&#xff0c;主办方为大家准备了一场寿司晚宴。小 G 和小 W 作为参加 NOI 的选手&#xff0c;也被邀请参加了寿司晚宴…

ASP.net Web API允许跨域访问解决办法

来源 http://blog.csdn.net/wxg_kingwolfmsncn/article/details/48545099 遇到此跨域访问问题&#xff0c;解决办法如下&#xff1a;方法一&#xff1a;1. 在web.config中增加customHeaders&#xff0c;如下图&#xff1a;<system.webServer><validation validateInte…