数据大作战

      连续一个星期了,一直是在抓数据。个人水平从小初班到了小中班。好歹是积累了些干货。感觉有必要总结总结了。

          在上干货之前,我先说明下提纲。不感兴趣前部分就可以跳过了。

            第一节,普及下网络数据请求过程

            第二节,几种语言抓取数据的方式

            第三节,Python抓取数据的方法和常见问题,包括中文乱码,post传值,域名登录,接口获取,数据解析处理等

一,      网络数据请求过程

我可以做一个这样的比喻,浏览网页就像是在跟另一个人用对讲机通话。是的。虽然我们一提到上网冲浪好像是在看电影似的,完全是坐享其成的感受。但是实际上这是一种错误的认识。上网就是在通话,用户在一头说话,服务器在另一头回应。如果用户没有任何操作,你想要的网页是不会跳出来的。而浏览器就相当于对讲机,展示数据的载体。http协议就相当于通话双方的频段。

再提到域名就不能用对讲机举例了,就要拿电话机来比喻了。我们所有访问的网站,都代表了一个完全独立唯一的个体,就像电话号码。但是电话号码太长了,就需要用另一种容易记忆的方式来标示,这就出现了各种www域名,就像我们手机通讯录存储的姓名。试想一下每次使用百度都输入’61.135.169.121’这样一串数字,谁能吃得消啊。实际上在咱们往浏览器输入简单的www.baidu.com的时候,浏览器仍然在需要在庞大的域名数据库里查询对应的ip地址才能访问到目标网站。比如说你想找北京水利局长,即使你很明确你要找的人是谁也无法和他通话,因为你首先要通过查询找到他的电话号码才能拨通。如果你找不到他的号码,就会得到提示‘查无此人’,这种事情发生在网络上就会出现最经典的404报错。

   假设网站可以正常访问了,相当于电话拨通了。但是并不代表可以正常通话了。因为对方是水利局长每天受到的骚扰多了去了,首先你得表明身份是什么地方的谁谁谁,当电话那头允许了。你这边再说一句‘下面我将表明我的诉求‘,再接下来才是进入网络访问过程。这就是传说中的三次握手。

   既然通话网路已经建立,就进入到了通话过程。一头说,“我想让某某给我唱一首伤心太平洋“”我想让某某某号给我讲一个笑话“,然后对讲机那一头就按你的要求进行回复。即使这个时候也是不能保证有求必应的,比如403错误,你想看看对方老婆照片,对方肯定是不允许的;再比如500错误,对方临时上厕所开小差抽风了没法正常通话了;还有一种属于正常300提示,对方转接到其它线路。

      我所认识的网络请求就是这个样子了。希望能满足诸位某些疑惑,也欢迎提出在下认识中的不足之处。

二,几中编程语言抓取数据的方式

      1)

本人是搞PHP的,其它领域就业余很多了。所以先拿php说吧,

      最简单的就是fopen /file_get_contents函数了,这就特别像拿来主意了,相当于网页右键查看源代码再复制下来。但是实际上他的简单也说明了它的局限性,只能抓取现成的页面代码,对于post传值和需要登录的就无能为力了,而且它不能keeplive,就像每次通话都要重新拨号,得到需求后再挂断。有些影响效率,比较适用于文本内容比较简单而且不作处理的页面。

      另一种就是curl模拟提交了,属于伪造浏览器访问。可以设置post参数,甚至可以传输文件,应该范围很广,尤其是公众号开发,第三方接口获取数据。缺点就是配置特别多,所以我猜大部分跟我一样使用复制大法进行套用。另外,curl对于登录验证码这种高难度的操作就不太灵了。

      2)

      再接下来说一说js,jquery,vue.js,jsonp,ajax,这几种不同的名字在获取数据方法其实都是一种方式,就是ajax异步获取。当然其中肯定是有差别的,后面再细说。

      它的优点是不必整个页面重新获取,而是只获取某一个数字或图片或单独一页内容再进行填充,用户体验效果好,并且节省网络资源。缺点是它并非真正意义上的抓取数据,而是类似于接收数据。因为ajax获取的数据及数据格式都是预先定义好的,只需要按照预定参数去获取就好了。

      其中jsonp是较特殊的一种了,优点是可以跨域,就是可以获取不同域名站点的内容。缺点是只能使用get提交。至于使用非jsonp跨域获取数据的ajax类似的在网上还有大把方法可寻。世上无难事,只怕有心人。

      3)

      Node.js,据说这是近两年特别火爆的语言。我就感觉做程序员就是个无底洞,三天两头出来个新语言,新升级,新类库。不学不行,学了也感觉就那样。所以我就看了两个小时的教程,get 了点皮毛,貌似是功能挺强大,操作也挺简单。可以完成各种高难度的数据抓取。下面是一段简单的抓取数据的源码,包含抓取到解析的过程,自行体会吧。

  1 var http = require('http')
  2 
  3 var cheerio = require('./cheerio')
  4 
  5 var url  = 'http://www.imooc.com/learn/348'
  6 
  7 function filterChapter(html)
  8 
  9 {
 10 
 11       var $ = cheerio.load(html)
 12 
 13       chapters = $('.chapter')
 14 
 15       var courseData = []
 16 
 17       chapters.each(function(item){
 18 
 19            var chapter = $(this)
 20 
 21            var chapterTitle = chapter.find('strong').text()
 22 
 23            var videos = chapter.find('.video').children('li')
 24 
 25            var chapterData = {
 26 
 27                  chapterTitle:chapterTitle,
 28 
 29                  videos:[]
 30 
 31            }
 32 
 33            videos.each(function(item){
 34 
 35                  var video = $(this).find('.J-media-item')
 36 
 37                  var videoTitle = video.text()
 38 
 39                  var id = video.attr('href').split('video/')[1]
 40 
 41                  chapterData.videos.push({
 42 
 43                       title:videoTitle,
 44 
 45                       id:id
 46 
 47                  })
 48 
 49            })
 50 
 51            courseData.push(chapterData)
 52 
 53       })
 54 
 55       return courseData
 56 
 57 }
 58 
 59 function printCourseInfo(courseData){
 60 
 61       courseData.forEach(function(item){
 62 
 63            var chapterTitle = item.chapterTitle
 64 
 65            var chapterVideos = item.videos
 66 
 67            console.log(chapterTitle+'\n')
 68 
 69            chapterVideos.forEach(function(video){
 70 
 71                  var videoTitle = video.title
 72 
 73                  var videoId    = video.id
 74 
 75                  console.log('['+videoId+']'+videoTitle)
 76 
 77            })
 78 
 79       })
 80 
 81 }
 82 
 83 http.get(url,function(res){
 84 
 85       var html = ''
 86 
 87       res.on('data',function(data){
 88 
 89            html+=data
 90 
 91       })
 92 
 93       res.on('end',function(){
 94 
 95            courseData = filterChapter(html)
 96 
 97            //console.log(courseData)
 98 
 99            printCourseInfo(courseData)
100 
101       })
102 
103 }).on('error',function(){
104 
105       console.log('获取数据出错')
106 
107 })

 

4)

最后再说一说python了,这门语言算是抓取数据的老牌专业户了,一提到它的名字就会联想到’爬虫’二字。我对它接触了有一个来月吧,就感觉python是个啥也不是,但是啥也能干的玩意。如果不加载模块好像啥也干不了了,但是所有想到的需求只要配合相关模块全都能做。比如能做网站,能做桌面应用程序,也能写用来执行爬虫的页面。

 1 user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
 2 
 3 headers = { 'User-Agent' : user_agent }
 4 
 5 response = urllib2.urlopen(request)
 6 
 7 if response.getcode()!=200:
 8 
 9 return None
10 
11 return response.read()

 

      这一段是基本的抓网页内容的代码,是不是比较牛逼,它的优点是完全伪装成浏览器的样子,连什么样的浏览器,什么样的系统,用什么方式去访问,需不需要ip代理,完全照办。所以我还是推荐使用python来抓取数据,代码简单,模块丰富,功能强悍,linux还自带python,只要花两三个小时就能get到一套受用的抓取方法。每次写好程序看着终端窗口不断跳动抓取过程,爽得不要不要的。

      以上是我所知领域的抓取方式,毕竟也是小白一枚,未见过大世面。大牛大咖们莫要耻笑。

三,python爬虫的常见方法和问题总结

      总算是进入干货阶段了,方法一说出来就很简单了。但是这些方法和问题却是实例开发中遇到且花长时间才找到解决方法的,比如说中文乱码的问题,我在网上查询解决方案,五花八门感觉都说得有理,折腾了三个多小时就是不成功。最终是自己尝试才找到方法,后面会详细说明。这里就不多说了。

      首先说说爬取数据的方法,前面已经贴了一部分。在这里咱们再系统的罗列一遍。基本上用得都是urllib2和urllib模块。

a)最简单的不带参数的爬取。

 f = urllib2.urlopen(url, timeout=5).read()  

b)带header参数的

 1 user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
 2 
 3 headers = { 'User-Agent' : user_agent }
 4 
 5 
 6 
 7 request = urllib2.Request(url, headers = headers)
 8 
 9 response = urllib2.urlopen(request)
10 
11 if response.getcode()!=200:
12 
13 return None
14 
15 return response.read()

 

c)带post参数的

 1 user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
 2 
 3 headers = { 'User-Agent' : user_agent }
 4 
 5 value={
 6 
 7 'name':'BUPT',
 8 
 9 'age':'60',
10 
11 'location':'Beijing'#字典中的内容随意,不影响#
12 
13 }
14 
15 postdata=urllib.urlencode(value)#对value进行编码,转换为标准编码#
16 
17 request = urllib2.Request(url,data=postdata,headers = headers)
18 
19 response = urllib2.urlopen(request)
20 
21 if response.getcode()!=200:
22 
23 return None
24 
25 return response.read()

 

d)需要登录和验证码的页面,利用本地cookie文件爬取,需要使用cookielib模块

 1 cookie=cookielib.MozillaCookieJar()
 2 
 3 cookie.load('cookies.txt',ignore_expires=True,ignore_discard=True)
 4 
 5 req=urllib2.Request('http://bbs.fobshanghai.com/qun.php?subcatid=576')
 6 
 7 opener=urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
 8 
 9 urllib2.install_opener(opener)
10 
11 response=urllib2.urlopen(req)
12 
13 print response.read()

 

PS:cookie文件可以利用firebug工具导出,要特别注意两点,1是在文本第一行加上‘# Netscape HTTP Cookie File’,这样一句话声明这是一个cookie文件,不然会报错,http.cookiejar.LoadError: 'cookies.txt' does not look like a Netscape format cookies file  2.检查一下每行的项目是不是齐全,里面包含了每条cookie的域名,布尔值,适用途径,是否使用安全协议,过期时间,名称,值共7个属性,缺一个就会报错。如果少的话,随便从其它行复制过来就可以。如果再报错,就用一个笨方法。复制一行正确的cookie,然后每个属性值一一复制粘贴过来。关键时刻还就得这样整。

e)获取json数据

按说获取接口的jsono数据是最简单的了,但是习惯使用beautiful模块去解析页面的DOM元素,遇到json数据反而傻眼了。所以在这里特别说明下json数据解析使用到的模块和方法。

解析json数据,我使用的是demjson模块,操作也是很简单的。使用datas = demjson.decode(soup),就可以像使用字典那样去取值了。

f)涉及用户登录及验证码的网页

针对验证码的爬取方法,网上同样有很多教程。但是本人没有成功完成的经验所以就不敢妄言了,目前来看掌握这几种方法就已经够用了。

说完了获取接下来就谈一谈解析了,毕竟拿到手的不是json数据而是一堆零乱的html文件,怎么取自己想的数据还要有很多路要走。我经常使用的beautiful模块,下面也是就这个模块来进行讲解。

a),获取id,class,以及标签的DOM元素

cont = soup.find('div',id="d_list")   #获取id名称为‘d_list’的div元素

link.find('span',class_="c_tit")     #获取class名称为’c_tit’的span元素,注意class_不同于’class’

b)获取元素的文本内容和属性值

soup.find('a').get_text()     #获取a标签的文本内容

soup.find('a')['href']          #获取a标签的链接地址

soup.find('img')['src']          #获取图片地址

c)去除DOM元素中的部分元素

cont = soup.find('div',id="d_list")

cont.find('div',class_="ppp").extract()    #去除id为’d_list’元素中的class为‘ppp’的div元素

d)去除a标签的href属性

del link.find('a')[‘href’]    #实际开发中需要遍历操作

e)去除某DOM元素中的第N个某标签元素,或倒数第N个某标签元素

看似伤脑筋的题目,其实只要循环遍历其中所有元素,然后按照下标做选择判断就可以了。如果是去除倒数第N个元素,可以循环两次,第一次取到元素总数,第二次再相应操作就可以了。对于这种问题关键是思路。

f)图片地址问题

因为爬取数据的同时,也需要把图片抓取到本地,这就面临一个图片地址的问题。页面内容的图片地址和获取到自己服务器上的图片地址必须相对应。我在这里提供两个思路,一是如果抓取的网站里面的图片都是放在一个服务器上,那么就远程获取图片下载到自己服务器的相同目录下。这样图片抓到了,内容也抓到了,还不影响页面展示。所以把页面里的所有图片地址传到一个列表里,再一一远程下载到本地服务器。二是面对要抓取的网站里的图片来自不同服务器,这就有些棘手。即使是抓取到自己服务器相同的目录,也要使用正则表达把页面里图片的地址替换掉。我估计大部门程序员对正则还是有些怵的。所以我就采用了迂回战术,遍历出页面里所有的图片地址,分别下载到本地服务器后分别替换新地址。代码比较长就不在这里展示了,放个github库地址给大家观摩吧,https://github.com/zuoshoupai/code/tree/master/wxfunyparse

      常见的招数也就这些了,最后再说一说令人头疼的中文乱码问题。

      网上对于乱码的说法有很多,大概也是由于造成乱码的原因有很多吧。不能全部适用。我就先把一般出现的原因罗列一下,a)页面本身是非utf8编码 ;b)程序开发环境也就是编辑器非utf8编码  c)服务响应的Accept-Encoding采用了gzip压缩 d)其它原因,比如cmd不支持显示中文或者设置不正确

      因为造成乱码的原因有很多,所以我们首先要排查是哪种问题造成的。我的建议是在获取到页面的内容后马上输出,并导出到本地文件,也就是response.read()后马上打印。如果输出显示或是本地文件显示正常,就表示页面编码是没有问题的,问题出在解析上。所以使用response.read().encode('utf-8')解决问题。我上次遇到的问题很是奇葩,页面获取内容后打印输入正常,然后解析处理后就乱码了,我就使用各种decode,encode咋都不好使。最终没辙了,使用chardet模块把每一步的编码格式都打印出来,发现在传输过程中,gbk变成了utf8,也就是说明明是gbk的文本当成是utf8展示,而且还转不过来。最后我把最开始获取数据的地方转换成utf8就OK了。

希望这个小例子能给你带来一些启发,遇到bug千万不能乱,必须一步一步整理思路,确定问题所在,才能一击而中,成功解决。

      好了,关于爬数据的知识总结就到这了。现在突然有种顿悟,所谓编程就是数据的交互,正确的传给别人和正确的拿到别人的。说来简单,中间的过程就是千辛万苦了。

 

posted @ 2017-06-03 22:08  左手拍拍  阅读(283)  评论(0编辑  收藏  举报