用python抓一点数据回来

python基础

浏览数:175

2019-4-27

AD:资源代下载服务

概要

  • 背景描述

  • 网站和http请求分析

  • IP受限的问题

1. 背景描述

大为软件公司于2001年9月在保定国家高新技术产业开发区注册,公司致力于中国、日本知识产权软件的研究开发,立志成为新兴的中国知识产权信息服务业一流服务供应商,以专业化、国际化的形象服务于中国市场,为企业、大学、科研院所、知识产权代理机构等用户提供国际一流品质的知识产权信息技术服务。

朋友希望获取A股上市公司(约3000余家)2014-2016年的专利数量,包括发明专利/实用新型/外观专利和外国授权四类。人们使用爬虫从公开网络获取数据,核心要解决的问题就是效率,通过程序模拟http请求,获取数据并解析得到目标数据。3500家公司3年的数据累计1万次请求,初听来难点在于可能遇到的动态网页/网页解析和其他可能遇到的反爬虫机制上,但最终却栽在了1万次请求上(IP限制)。一般数据服务公司出于商业考量,公共试用服务都会限制请求次数,希望无限制访问则需要付费升级为vip。

经典意义上的爬虫流程是,定义一个起始网址(start_url),请求该网址并获取reponse,用lxml或bs4解析网址获取目标数据,同时利用网页之间的外链关系将解析得到的url压栈得到下一步需要爬取的网址,不断的递归上述过程,直到达到约定的条件停止,这种方法时非常暴力的,需要解决url去重和排序的问题,工程量非常大,一般见于淘宝/京东等网站信息的爬取。当目标资源数量有限,且通过分页列表的方式组织时,有一种相对优雅的方式去组织爬虫程序,可以通过循环的方式分别请求目标数据,很多时候都可以通过直接拼url的方式解决。我面临的问题有别于经典问题,是一个query问题,这一万次请求之间没有任何关联,虽然如此,只要两个循环(对股票的循环和对时间的循环)即可解决。背景分析至此,下一步要解决的问题就是完成一次独立的请求,并解析得到目标数据。

2. 网站和http请求分析

上图即目标网站截图,其中右侧红色方框中选中部分的四个数字就是我的目标。上方地址栏的网址是请求的入口,中间圆角方框中的query格式(”天津 and ADY=2016“)则是请求参数。

目前需要解决两个问题:1,如何将目标网页load到本地,即将网页爬下来;2,从爬到的网页中将目标数据解析出来。又以第一个问题最为紧要,且第一个问题还有一个附带的小问题,即目标网页是否是动态网页。动态网页即动态加载的页面,一个完整的网络请求包括load网页框架和ajax异步请求数据并填充渲染。动态网页对爬虫的影响是巨大的,requests或者urllib2等程序发起的http请求只能完成第一步工作,后续的ajax异步请求数据将不被执行,结果是我们将拿不到任何有效信息,一般我们关注的信息都在动态加载部分,除非我们要的是模版。直觉分析,专利数据应该是不断更新的,这种信息一般的处理方式都是动态从数据库中检索获得,所以极有可能是动态网页。动态网页的识别和解决办法按下不表,我们先解决模拟请求的问题。

2.1 模拟请求

经典爬虫是不需要传参的,我们只需要用requests等包请求目标网址即可,且一般是post方法。我们面临的问题稍有区别,需要传一个query字符串来表明请求的目标。

http协议中,一般建议通过POST方法的body传递参数给服务端,但是许多网站由于不规范或者出于安全考虑等原因,通过GET方法方法传参数的情况也不鲜见,将query字符串藏在header里面或者拼接在url中都可以实现该目的。

首先,我尝试了一种最质朴和直接的拼接url的思路,即上图红色标注的链接,很可惜并未能获取到检索结果。to young to naive.

正规军的办法是使用chrome的开发者工具。如上面截图,在输入query字符串之后,我们观察右侧的network目录,一般在XHR下面会有目标的AJAX请求,我的运气比较好,目标网站比较简单,只有一个类似请求,进入请求详情如下图。

当我看到request payload中的“天津 and ADY=2016”时,非常开心,因为我找到了请求的入口。general部分标示了目标的网址,且表明这是一个POST请求。如果愿意,我们可以带上requests header部分,目标网站不需要登陆,所以这一部分处理是否带上看情怀。

到现在,我们已经把网站请求分析清楚了,剩下要做的就是找一个python的包来实现上面的POST请求。

这里一定要吐槽下,作为python的三大主流应用(网站框架/数据挖掘/爬虫)之一,python内置的urllib/urllib2是我见过最混乱和不pythonic的。简单区分下,不用scrapy等爬虫框架的背景下,大家一般会用urllib.urlencode()来拼接带query的url,然后用urllib2来生成一个Request对象,并用urllib2.urlopen打开该Request,或者直接打开一个url,相关信息参考如下链接

简单比较了下urllib2和requests之后,我迅速决定使用reuqests包,因为它提供了非常简洁的post方法,而且接口十分简洁优雅,真心非常对得起它的slogon – http for humans. 这个包也真是没谁了,logo长得漂亮,api写的优雅,而且也足够屌,简介中有一段warning,极尽鄙视其他http包,并威胁如果用其他的包可能导致死亡,吐血拜服。

有一小点需要说明,requests的post方法通过payload参数传入body部分,document中说明,同时接受字典和json字符串,我一开始传入python字典却总是请求失败,后来在小象学院杨真老师的提示下,改传入json字符串方能成功。后期复盘,发现虽然post能同时接受字典和json字符串,但却是生成不同的请求body,传入字典生成的body部分时一个form表单,而传入json字符串则对应一个字符串。

2.2 解析爬虫结果

关于动态网页的判别,我并没有直接的判断,在分析网站的请求的过程中就已经知道该请求为动态加载。对于一般的GET请求,则可以通过urllib2.open(target_url).read()的方法,然后用bs4或者lxml,甚至正则的方法去寻找目标对象, 如果是动态网页,则一般找不到或者只能找到空的tag。

解决动态网页爬去的思路典型有两种,一种是模拟浏览的行为执行完整的请求过程,甚至操作一个浏览器完成完整的请求和渲染过程,推荐的组合是SeleniumPhantomJS,其中Selenium提供了操作web server的python api,PhantomJS则是一个headless的浏览器。第二种相对轻便的解决办法就是如我们上述使用,分析网络请求流程,找出其中请求目标数据的AJAX请求的url,并模拟该请求。

解析数据部分直接用的resp.json(),不赘述了。

3. IP限制

IP限制几乎时任何正式爬虫都会遇到的问题,所以我们也遇到了,还好我们只有一万次请求,最差也可以人工分一周的时间搞定。

为了解决这个问题,我做了如下的尝试:

  1. 代理IP。GitHub上找到一个项目提供了很优美的解决方案。作者写了一个爬虫去IP代理池网站爬取代理IP,并用该IP去访问任意网站(默认是百度,可以考虑用目标网站),如果可以ping通,则将该IP写入数据库备用。最优雅的部分在于,可以用flask等轻量的网站框架写一个网络服务,暴露一个get方法来给其他应用使用。可惜的是,该方法爬到的IP在爬取目标网址时非常慢,且大部分不可用,最后这个项目默认的数据库是ssdb,它莫名奇怪的挂掉了。

  2. 人肉,他们提到的方案包括插拔家里的路由器,家里和公司的ip每天都刷一遍,找朋友帮忙,骑着单车逛四五个北京的星巴克。

附上项目的github链接

作者:guojw