首页 > 技术文章 > Python爬虫黑科技(经验)

valorchang 2019-08-15 13:55 原文

"作为一名爬虫工程师,你最需要关注的,是数据的来源"

原文:https://www.jb51.net/article/90114.htm

霍夫曼编码压缩算法

1.最基本的抓站

  1.  
    import urllib2
  2.  
    content = urllib2.urlopen('http://XXXX').read()

2.使用代理服务器

这在某些情况下比较有用,比如IP被封了,或者比如IP访问的次数受到限制等等。

1

2

3

4

5

import urllib2

proxy_support = urllib2.ProxyHandler({'http':'http://XX.XX.XX.XX:XXXX'})

opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)

urllib2.install_opener(opener)

content = urllib2.urlopen('http://XXXX').read()

3.需要登录的情况 cookie 表单

  1.  
    import urllib
  2.  
    postdata=urllib.urlencode({
  3.  
    'username':'XXXXX',
  4.  
    'password':'XXXXX',
  5.  
    'continueURI':'http://www.verycd.com/',
  6.  
    'fk':fk,
  7.  
    'login_submit':'登录'
  8.  
    })

 

3.伪装浏览器

 

  1.  
    import urllib2
  2.  
    import random
  3.  
     
  4.  
    url = "http://www.itcast.cn"
  5.  
    #注意是列表
  6.  
    ua_list = [
  7.  
    "Mozilla/5.0 (Windows NT 6.1; ) Apple.... ",
  8.  
    "Mozilla/5.0 (X11; CrOS i686 2268.111.0)... "
  9.  
    ]
  10.  
    #随机选择
  11.  
    user_agent = random.choice(ua_list)
  12.  
    request = urllib2.Request(url)
  13.  
    #也可以通过调用Request.add_header() 添加/修改一个特定的header
  14.  
    request.add_header("User-Agent", user_agent)
  15.  
    # 第一个字母大写,后面的全部小写
  16.  
    request.get_header("User-agent")
  17.  
     
  18.  
    response = urllib2.urlopen(req)
  19.  
    html = response.read()
  20.  
    print html

 

 

3.反‘反盗链’

某些站点有所谓的反盗链设置,其实说穿了很简单,就是检查你发送请求的header里面,referer站点是不是他自己,所以我们只需要像3.3一样,把headers的referer改成该网站即可,以黑幕著称地cnbeta为例:

1

2

3

headers = {

 'Referer':'http://www.cnbeta.com/articles'

}

4.多线程并发抓取

单线程太慢的话,就需要多线程了,这里给个简单的线程池模板 这个程序只是简单地打印了1-10,但是可以看出是并发地。

队列还要加强学习

  1.  
    from threading import Thread
  2.  
    from Queue import Queue
  3.  
    from time import sleep
  4.  
    #q是任务队列
  5.  
    #NUM是并发线程总数
  6.  
    #JOBS是有多少任务
  7.  
    q = Queue()
  8.  
    NUM = 2
  9.  
    JOBS = 10
  10.  
    #具体的处理函数,负责处理单个任务
  11.  
    def do_somthing_using(arguments):
  12.  
    print arguments
  13.  
    #这个是工作进程,负责不断从队列取数据并处理
  14.  
    def working():
  15.  
    while True:
  16.  
    arguments = q.get()
  17.  
    do_somthing_using(arguments)
  18.  
    sleep(1)
  19.  
    q.task_done()
  20.  
    #fork NUM个线程等待队列
  21.  
    for i in range(NUM):
  22.  
    t = Thread(target=working)
  23.  
    t.setDaemon(True)
  24.  
    t.start()
  25.  
    #把JOBS排入队列
  26.  
    for i in range(JOBS):
  27.  
    q.put(i)
  28.  
    #等待所有JOBS完成
  29.  
    q.join()

5.验证码的处理

碰到验证码咋办?这里分两种情况处理:

     1、google那种验证码,凉拌

     2、简单的验证码:字符个数有限,只使用了简单的平移或旋转加噪音而没有扭曲的,这种还是有可能可以处理的,一般思路是旋转的转回来,噪音去掉,然后划分单个字符,划分好了以后再通过特征提取的方法(例如PCA)降维并生成特征库,然后把验证码和特征库进行比较。这个比较复杂

 

6 gzip/deflate支持

现在的网页普遍支持gzip压缩,这往往可以解决大量传输时间,以 VeryCD 的主页为例,未压缩版本247K,压缩了以后45K,为原来的1/5。这就意味着抓取速度会快5倍。

然而python的urllib/urllib2默认都不支持压缩,要返回压缩格式,必须在request的header里面写明'accept-encoding',然后读取response后更要检查header查看是否有'content-encoding'一项来判断是否需要解码,很繁琐琐碎。如何让urllib2自动支持gzip, defalte呢?

 

7. 更方便地多线程

总结一文的确提及了一个简单的多线程模板,但是那个东东真正应用到程序里面去只会让程序变得支离破碎,不堪入目。在怎么更方便地进行多线程方面我也动了一番脑筋。先想想怎么进行多线程调用最方便呢?

1、用twisted进行异步I/O抓取

事实上更高效的抓取并非一定要用多线程,也可以使用异步I/O法:直接用twisted的getPage方法,然后分别加上异步I/O结束时的callback和errback方法即可。

  1.  
    from twisted.web.client import getPage
  2.  
    from twisted.internet import reactor
  3.  
     
  4.  
    links = [ 'http://www.verycd.com/topics/%d/'%i for i in range(5420,5430) ]
  5.  
     
  6.  
    def parse_page(data,url):
  7.  
    print len(data),url
  8.  
     
  9.  
    def fetch_error(error,url):
  10.  
    print error.getErrorMessage(),url
  11.  
     
  12.  
    # 批量抓取链接
  13.  
    for url in links:
  14.  
    getPage(url,timeout=5) \
  15.  
    .addCallback(parse_page,url) \ #成功则调用parse_page方法
  16.  
    .addErrback(fetch_error,url) #失败则调用fetch_error方法
  17.  
     
  18.  
    reactor.callLater(5, reactor.stop) #5秒钟后通知reactor结束程序

8. 一些琐碎的经验

opener.open和urllib2.urlopen一样,都会新建一个http请求。通常情况下这不是什么问题,因为线性环境下,一秒钟可能也就新生成一个请求;然而在多线程环境下,每秒钟可以是几十上百个请求,这么干只要几分钟,正常的有理智的服务器一定会封禁你的。

然而在正常的html请求时,保持同时和服务器几十个连接又是很正常的一件事,所以完全可以手动维护一个 HttpConnection 的池,然后每次抓取时从连接池里面选连接进行连接即可。

这里有一个取巧的方法,就是利用squid做代理服务器来进行抓取,则squid会自动为你维护连接池,还附带数据缓存功能,而且squid本来就是我每个服务器上面必装的东东,何必再自找麻烦写连接池呢

2、设定线程的栈大小

栈大小的设定将非常显著地影响python的内存占用,python多线程不设置这个值会导致程序占用大量内存,这对openvz的vps来说非常致命。stack_size必须大于32768,实际上应该总要32768*2以上

3、设置失败后自动重试

  1.  
    def get(self,req,retries=3):
  2.  
    try:
  3.  
    response = self.opener.open(req)
  4.  
    data = response.read()
  5.  
    except Exception , what:
  6.  
    print what,req
  7.  
    if retries>0:
  8.  
    return self.get(req,retries-1)
  9.  
    else:
  10.  
    print 'GET Failed',req
  11.  
    return ''
  12.  
    return data

4、设置超时
 

  1.  
    import socket
  2.  
    socket.setdefaulttimeout(10) #设置10秒后连接超时

 

Python爬虫防封杀方法集合

爬虫与浏览器对比

相同点

本质上都是通过 http/https 协议请求互联网数据

不同点

  1. 爬虫一般为自动化程序,无需用用户交互,而浏览器不是

  2. 运行场景不同;浏览器运行在客户端,而爬虫一般都跑在服务端

  3. 能力不同;浏览器包含渲染引擎、javascript 虚拟机,而爬虫一般都不具备这两者。

编码

其实编码问题很好搞定,只要记住一点:
####任何平台的任何编码 都能和 Unicode 互相转换
UTF-8 与 GBK 互相转换,那就先把UTF-8转换成Unicode,再从Unicode转换成GBK,反之同理。

decode的作用是将其他编码的字符串转换成 Unicode 编码
encode的作用是将 Unicode 编码转换成其他编码的字符串
一句话:UTF-8是对Unicode字符集进行编码的一种编码方式

  1.  
    #coding=utf-8
  2.  
    # 这是一个 UTF-8 编码的字符串
  3.  
    utf8Str = "你好地球"
  4.  
    print utf8Str
  5.  
    # 1. 将 UTF-8 编码的字符串 转换成 Unicode 编码
  6.  
    unicodeStr = utf8Str.decode("UTF-8")
  7.  
    print unicodeStr
  8.  
    # 2. 再将 Unicode 编码格式字符串 转换成 GBK 编码
  1.  
    gbkData = unicodeStr.encode("GBK")
  2.  
    print gbkData
  3.  
    # 1. 再将 GBK 编码格式字符串 转化成 Unicode
  4.  
    unicodeStr = gbkData.decode("gbk")
  5.  
     
  6.  
    # 2. 再将 Unicode 编码格式字符串转换成 UTF-8
  7.  
    utf8Str = unicodeStr.encode("UTF-8")
  8.  
    print utf8Str
  9.  
     
  10.  
    你好地球
  11.  
    你好地球
  12.  
    ��õ���
  13.  
    你好地球

 

 

网页内容

内容一般分为两部分,非结构化的数据 和 结构化的数据。

  • 非结构化数据:先有数据,再有结构,
  • 结构化数据:先有结构、再有数据

非结构化的数据处理

文本、电话号码、邮箱地址

  • 正则表达式

HTML 文件

  • 正则表达式
  • XPath
  • CSS选择器

结构化的数据处理

JSON 文件

  • JSON Path
  • 转化成Python类型进行操作(json类)

XML 文件

  • 转化成Python类型(xmltodict)
  • XPath
  • CSS选择器
  • 正则表达式

 

态度:

让编程改变世界

Change the world by program

 

 

虚拟环境

  1.  
     
  2.  
    创建虚拟环境 pip install virtualenv
  3.  
     
  4.  
    新建虚拟环境 virtualenv -p "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\python.exe" env1
  5.  
     
  6.  
    虚拟环境位置:
  7.  
    New python executable in C:\Users\Administrator\env1\Scripts\python.exe
  8.  
     
  9.  
     
  10.  
    激活虚拟环境
  11.  
    1、进入虚拟环境: cd env1
  12.  
    2、进入脚本目录: cd Scripts
  13.  
    3、运行activate.bat activate.bat
  14.  
    4、退出虚拟环境 deactivate.bat
  15.  
     
  16.  
     
  17.  
    虚拟环境高级版
  18.  
     
  19.  
    安装 virtualenvwrapper
  20.  
    创建 mkvirtualenv env2
  21.  
    列出所有虚拟环境 lsvirtualenv
  22.  
    激活虚拟环境 workon env2 
  23.  
    进入虚拟环境目录 cdvirtualenv
  24.  
    进入虚拟环境的site-packages目录 cdsitepackages
  25.  
    列出site-packages目录的所有软件包 lssitepackages
  26.  
    停止虚拟环境 deactivate
  27.  
    删除虚拟环境 rmvitualenv env2
  28.  
     
  29.  
    总结:
  30.  
    创建:mkvirtualenv [虚拟环境名称]
  31.  
    删除:rmvirtualenv [虚拟环境名称]
  32.  
    进入:workon [虚拟环境名称]
  33.  
    退出:deactivate
  34.  
     
  35.  
    1、冻结环境
  36.  
    所谓冻结(freeze) 环境,就是将当前环境的软件包等固定下来:
  37.  
    pip freeze >packages.txt  # 安装包列表保存到文件packages.txt中 
  38.  
     
  39.  
    2、重建环境
  40.  
    重建(rebuild) 环境就是在部署的时候,在生产环境安装好对应版本的软件包,不要出现版本兼容等问题:
  41.  
    pip install -r packages.txt
  42.  
    配合pip,可以批量安装对应版本的软件包,快速重建环境,完成部署。
  43.  
     
  44.  
     
  45.  
    位置:
  46.  
    New python executable in C:\Users\Administrator\Envs\env2\Scripts\python.exe
  47.  
     
  48.  
    打包应用:

然后自动生成和安装requirements.txt依赖

生成requirements.txt文件

pip freeze > requirements.txt

安装requirements.txt依赖

pip install -r requirements.txt

文件编码

input文件(gbk, utf-8...)   ----decode----->   unicode  -------encode------> output文件(gbk, utf-8...)

代替这繁琐的操作就是codecs.open,例如

文件读尽量用下面方法:

>>> import codecs
>>> fw = codecs.open('test1.txt','a','utf-8')
>>> fw.write(line2)

开源许可证

大概有上百种。很少有人搞得清楚它们的区别。即使在最流行的六种----GPLBSDMITMozillaApacheLGPL

模块(Module)和包(Package)

在Python中,一个.py文件就称之为一个模块

如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。(包由很多模块组成,包就是命名空间)

  每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码

  1.  
    mycompany #包
  2.  
    ├─ __init__.py
  3.  
    ├─ abc.py #模块
  4.  
    └─ xyz.py

 

if __name__ == '__main__'详解

其中 if __name__ =='__main__': 确保服务器只会在该脚本被 Python 解释器直接执行的时候才会运行,而不是作为模块导入的时候。

其中__name__属性的意思:

1、__name__是一个变量。前后加了双下划线是因为是因为这是系统定义的名字。普通变量不要使用此方式命名变量。

2、__name__就是标识模块的名字的一个系统变量。这里分两种情况:假如当前模块是主模块(也就是调用其他模块的模块),那么此模块名字就是__main__,通过if判断这样就可以执行“__mian__:”后面的主函数内容;假如此模块是被import的,则此模块名字为文件名字(不加后面的.py),通过if判断这样就会跳过“__mian__:”后面的内容。

通过上面方式,python就可以分清楚哪些是主函数,进入主函数执行;并且可以调用其他模块的各个函数等等。

神级总结:

one.py

  1.  
    #coding=utf-8
  2.  
    # file one.py
  3.  
    # 在使用自身的时候,就是main,比如你执行:
  4.  
    # python one.py
  5.  
    # 此时在one.py里面的name就是main
  6.  
    # 如果你在two中import one,那么name就是文件名
  7.  
    def func():
  8.  
    print("func() in one.py")
  9.  
     
  10.  
    print("top-level in one.py")
  11.  
     
  12.  
    if __name__ == "__main__":
  13.  
    print("one.py is being run directly")
  14.  
    else: #其他导入会执行,类似测试吧
  15.  
    print("one.py is being imported into another module")

two.py

  1.  
    #coding=utf-8
  2.  
    # file two.py
  3.  
    import one #导入就会自动执行,知道是当前的还是以前的
  4.  
     
  5.  
    print("top-level in two.py")
  6.  
    one.func()
  7.  
     
  8.  
    if __name__ == "__main__":
  9.  
    print("two.py is being run directly")
  10.  
    else:
  11.  
    print("two.py is being imported into another module")

所有规范就多写函数,变量别乱放。

推荐阅读