求你别再花大价钱学 Python 之爬虫实战

求你别再花大价钱学 Python 之爬虫实战

引子

现在很多平台把 Python 当做成功学传播,制作了很多昂贵的 Python 课程,其中还不乏部分粗制滥造的课程。

作为 10 年 Python 使用经验的程序员,觉得有必要告诉大家,Python 入门其实很简单,完全没有必要花大价钱去学习。

本文从比较流行爬虫为例,抛砖引玉,介绍 Python 在公开数据获取上的强大和灵活性。

Python 基本概念

Python 是荷兰计算机科学家 Guido van Rossum 发明的一款解释型、强类型、动态的、支持对象的高级程序设计语言。

初期 Python 仅仅是个人项目,现在已经发展成了时下最热门的编程语言之一,2020 年初在 TIOBE 榜单稳定排第三。Python 在人工智能的应用领域占领绝对优势。

Python 是解释性语言,非常适合作为入门的程序设计语言,它无须编译,编写完成即可运行。

尽管 Python 是动态语言,但它的数据类型是强类型的,避免了像 JS 这样“过分动态”为初学者带来各种奇怪的困惑。

Python 在支持面向对象编程,在发展过程中不断借鉴其他语言的强项。语言特性非常丰富,功能强大。

Python 语言自带类库足够好用,其生态系统也非常完善。围绕着 Python 生态的类库,领域丰富,质量又非常高。这是非常难得的。相比 npm 管理的库,虽然数量极多,但总体质量就不敢恭维了。

Python 优势和劣势

优势

1. Python 入门简单

Python 语法比较简单,核心关键字数量较少,结构清晰。为了达到结构清晰的目的,Python 用代码缩进来表达程序结构,在一般的编程语言,缩进往往只是一种美化代码的方法。这一点非常适合强迫症用户。

2. Python 有丰富的标准库

Python 内置的标准模块非常丰富,可以满足一般科学计算、文本处理、后端服务等需求,Python 甚至内置了一个 Demo 性质 HTTP 服务器。用户可以借助一个丰富的标准库,用较少的代码就可以构建一个规模较大的应用。对语言流行的助力是非常大的。

3. Python 生态优秀

Python 在 Web 框架、网络爬虫、网络内容提取、模板引擎、数据库、数据可视化、图片处理、文本处理、自然语言处理、机器学习、日志、代码分析等领域都有非常高质量的模块和库。使用高质量的库进行开发,系统中的坑自然会少很多。

基于以上原因,使用 Python 来进行项目开发的效率是非常高的。成为资料收集、自然语言处理、办公自动化工具、居家旅行必备良品。所谓的 Python 用户哲学:人生苦短,我用 Python。

Python 的劣势

1. Python 2 和 Python 3 的版本不兼容

Python 是一门进化中的语言,Python 2 内部的各版本有轻微的兼容问题,Python 3 在设计的时候为了轻装上阵,干脆不兼容 Python 2。Python 2 版本的程序在 Python 3 下很有可能运行失败,且无法简单 fix。对于初学者,这里建议直接以 Python 3 为学习的对象,同时了解 Python 2 的版本差异。

本文也是以 Python 3 为例子进行讲解的。

2. Python 的性能不够好

Python 程序运行效率慢,一方面因为是动态语言的问题,其次是 GIL(全局解释器锁),让每次解释字节码的时候都需要申请这个全局解释器锁。根据微信团队某大牛举的例子:团队内有人使用 Python 来实现一个重要算法,被主管嫌弃运行太慢。用 C 来实现,性能有数十倍提升。以为是动态语言的问题,后来再用 Perl 实现一次,Perl 版本相较 Python 仍有 10 倍性能提升。

虽然详细内情不得而知,但可以看出 Python 对 CPU 的利用效率的确有限。

其次,Python 程序运行期间难以精确控制内存占用,在使用内置类库来处理大规模数据的时候,占用的内存可能越来大,引起 OOM 问题。

总的来说,Python 是值得深入学习使用的,未来的计算机的算力必然不断进步不断变得更加廉价,人工人力的成本是越来越贵的。

Python 安装设置

这里介绍 Python 3 在 Windows 系统下的安装步骤。

1. 打开 Windows 版本下载页

https://www.python.org/downloads/windows/

根据你的 Windows 版本,选择合适的版本安装包 *executable installer,可直接安装最新的 Python 版本。

2. 安装并设置路径

一步步安装,最后在安装程序中设置好环境变量。

3. 检查 Python 版本

python -V

4. 运行 Python 交互解释器

直接运行 Python 命令,得到一个交互运行 Python 交互解释器,这个很方便对类库进行测试和体验。

python
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 22:39:24) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
\>\>\> import datetime
\>\>\> print( datetime.datetime.now() )
2020-02-07 10:46:49.266547

Python 基本语法

程序例子

通过一个例子看 Python 程序:

\#! env python

\#内置列表数据结构
url_list = [ 'http://ziyuan3721.com',
'https://www.baidu.com',
'ftp://abc.com'
]

\#循环是这样的
for url in url_list:
    \#条件判断
    if url.startswith('http:'):
        port = 80
    elif url.startswith('https:'):
        port = 443
    elif url.startswith('ftp:'):
        port = 21

    print("port for {} is: {}".format( url, port ) )

\#变量的作用域
print( "Last", url, port )

把源码保存为 p1.py,运行:

python p1.py

运行结果:

port for http://ziyuan3721.com is: 80
port for https://www.baidu.com is: 443
port for ftp://abc.com is: 21
Last ftp://abc.com 21

以上例子虽然简单,但演示了 Python 语言的主要特点:

  • Python 的变量是第一次赋值时自动生成,无须声明;
  • Python 内置了很多数据类型;
  • 字符串对象的方法比较丰富;
  • Python 使用缩进控制程序结构;
  • 变量的作用域会适当“提升”。

如果读者有其他程序设计的经验,看完这个程序,可以说已经基本掌握了 Python 脚本写作的所需语法。事实上,很多非专业人员可以用 Python 语法作为“胶水”,把一些标准类库和一些第三方类库功能粘合在一起,构造出非常实用的程序(图片批量去水印、注册机、发贴机之类)。再也不用去学什么易语言之类的。

Python 基本语法

虽然 Python 语法入门是如此简单,但也有必要掌握一下 Python 的一些基本语法概念。

1. 函数/方法

p2.py

\#! env python

def log(s):
    print(s)

log('hello file')

if __name__ == '\_\_main\_\_':

    log('hello main')

用 def 指令,可以定义一个函数/方法。冒号: 缩进下的代码是函数体。缩进在 Python 中是严格限制的,不能使用一些比较落后的文本编辑器对 Python 代码进行格式化,因为它们可能会破坏程序结构。建议使用 3 个或者 4 个空格做代码缩进。

函数自然是相对封闭的,在函数体内创建的变量,只能在函数体内可见。

运行 python p2.py,打印出两行日志:

hello file
hello main

2. 模块

模块是 Python 代码组织单元,使用内置模块 datetime:

import datetime

print( datetime.datetime.now() )

在 Python 交互解释器里面,可以用 dir 方法来列举模块内的所有成员(类/对象/方法):

\>\>> import datetime
\>\>> dir(datetime)
['MAXYEAR', 'MINYEAR', '\_\_doc\_\_', '\_\_file\_\_', '\_\_name\_\_', '\_\_package\_\_', 'date', 'datetime', 'datetime\_CAPI', 'time', 'timedelta', 'tzinfo']

最简单的模块是一个 py 文件,解释器运行,把 p2.py 作为自定义模块引入。

python
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 22:39:24) [MSC v.1916 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
\>\>> import p2
hello file
\>\>>

看到屏幕只打印了 hello file,和直接运行 python p2.py 不同。

原因是模块内置变量 __name__ 是随着运行环境改变的,当模块 p2 被作为运行入口时,它是的值是 '__main__';当被其他模块引入时,它的取值是 'p2'

可以在解释器中检查:

\>\>\> print(p2.\_\_name\_\_)
p2

dir 查看一下,p2 模块的 log 方法被导出。

\>\>> dir(p2)
['\_\_builtins\_\_', '\_\_cached\_\_', '\_\_doc\_\_', '\_\_file\_\_', '\_\_loader\_\_', '\_\_name\_\_', '\_\_package\_\_', '\_\_spec\_\_', 'log']

可以直接调用 p2.log:

\>\>> dir(p2)
['\_\_builtins\_\_', '\_\_cached\_\_', '\_\_doc\_\_', '\_\_file\_\_', '\_\_loader\_\_', '\_\_name\_\_', '\_\_package\_\_', '\_\_spec\_\_', 'log']
\>\>> p2.log("hi p2")
hi p2

不仅模块方法可以导出,模块变量也可以导出。

模块的更上一层管理结构是包,多个相关的模块可以组成一个包来发布。

3. 类

Python 是支持面向对象的,以非常直观的方法绑定对象的方式来组织面向对象的代码。

用 class 指令来定义类:

class Logger:

    def \_\_init\_\_(self, level):
        self.log_method = print 
        self.level = level

    def log(self, s):
        self.log_method(s)

if __name_\_ == '\_\_main\_\_':

    mylog = Logger(0)
    mylog.log('in main')

_init_ 是构造方法,log 是自定义方法。和 C++/Java 不同,两个方法都必须把方法绑定的对象明显列出,就是上面的 self 对象。self 不是关键字,只是 Python 老铁的一个约定习惯,用 this 和 me 等名字也可以。在调用对象的方法时,对象已经绑定了,参数列表不需要再给出对象。

注意:虽然 Python 可以说一切皆对象,使用 Python 进行编程不强求使用面向对象的思维。可以根据自己的水平和解决问题的类型采取合适的程序架构。

至此,用 Python 来进行实现爬虫的知识准备已经足够。下面进行 Python 爬虫的实现细节。

Python 爬虫实现

因为 HTTP 协议是简单字符协议,实现爬虫可以很简单。但是站长需要保护站内资源,采取各种反爬虫手段和爬虫对抗,真正实用爬虫会比较复杂。用好爬虫,需要对 HTTP 相关协议进行比较深入的了解。

这里不会引入所谓的爬虫框架,框架固然可能比较高效,但它隐藏了细节,学习它无助于我们理解爬虫的原理。手动实现的爬虫更加灵活。

爬虫相关 HTTP 协议概念

Web 页面的打开,需要浏览器和服务器进行多次的 HTTP 交互。HTTP 协议是一个明文字符协议,对协议的研究非常方便。

HTTP 最常用的方法是 post 和 get,最简单的爬虫就是 get 一个 URL,对返回内容进行解释。

研究 HTTP 请求最方便的办法,是在使用 Chrome 请求 Web 页面的时候,打开 F12,分析 HTTP 的请求/返回。使用 Postman/curl 软件,编辑 HTTP 请求数据包不断测试 Web 服务器的返回。

但 Web 页的交互往往是复杂的,经常遇到的问题可能有:

  • 服务器返回的内容是压缩的
  • 服务器返回了 3xx 跳转,需要浏览器进一步处理
  • 服务器返回了 JS 内容,浏览器执行 JS 才能渲染页面
  • 服务器识别客户端浏览器版本,返回不同的内容。甚至拦截某些客户端
  • 服务器要求客户端带 cookie 访问
  • 服务器对用户进行挑战 CAPTCHA 验证

种种复杂问题,对爬虫的实现者都是挑战。为了更加高效的实现爬虫,建议实用 requests 库。requests 是奇才 kennethreitz 实现的一个 HTTP 访问库,它封装了 HTTP 协议的细节,号称是一个给人类使用的 HTTP 请求类库。

requests 库使用

requests 虽然好用,但不是 Python 内置类库,需要安装。

使用 pip 命令,-i 指定国内的 pypi 源,下载 requests 库:

 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests

使用 request 请求网页:

import requests

def get\_url(url):
    \#使用get方法请求url
    res = requests.get(url)
    return res

def write(content):
    fn = './a.html'
    f  = open(fn,'w')
    f.write(content)
    f.close()

if __name__ == '\_\_main\_\_':

    import sys
    url = sys.argv[1]
    res = get_url(url)

    \#输出res内部成员
    print( dir(res) )

    \#打印http相应码
    print( res.status_code )

    \#导出网页源码到 a.html
    write( str(res.content, encoding='utf8') )

运行 python p4.py https://www.qichacha.com

['__attrs__', '__bool__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_content', '_content_consumed', '_next', 'apparent_encoding', 'close', 'connection', 'content', 'cookies', 'elapsed', 'encoding', 'headers', 'history', 'is_permanent_redirect', 'is_redirect', 'iter_content', 'iter_lines', 'json', 'links', 'next', 'ok', 'raise_for_status', 'raw', 'reason', 'request', 'status_code', 'text', 'url']
200

看到返回的 res 对象的成员,最有用的是 status_code (HTTP 状态码)和 content(返回内容)。

打开内容文件 a.html,看到 qichacha 拦截了我们的请求,返回了错误服务:

\<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"\>
\<html xmlns="http://www.w3.org/1999/xhtml"\> 
\<head\>
\<title\>405错误页面\</title\>

这和浏览器打开访问 https://www.qichacha.com 不同,肯定是 requests 库发起的 get 请求的参数和浏览器不同。

再运行 python p4.py https://httpbin.org/get(注 https://httpbin.org/ 是一个 HTTP 调试工具网站),看到返回的 body:

{
  "args": {}, 
  "headers": {
    "Accept": "\*/\*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.22.0", 
    "X-Amzn-Trace-Id": "Root=1-5e3cf28a-447225d89eb73c98965842d8"
  }, 
  "origin": "223.198.155.159", 
  "url": "https://httpbin.org/get"
}

看到默认的 User-Agent 的值是 python-requests/2.22.0,容易想到是 qichacha.com 拦截了未知的 User-Agent。我们可以伪造 Chrome 的请求头进行请求。

用 Chrome 浏览器打开 https://httpbin.org/get(这个接口输出了客户端的 HTTP 请求头):

{
  "args": {}, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,\*/\*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Accept-Language": "zh-CN,zh;q=0.9", 
    "Host": "httpbin.org", 
    "Sec-Fetch-Dest": "document", 
    "Sec-Fetch-Mode": "navigate", 
    "Sec-Fetch-Site": "none", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36", 
    "X-Amzn-Trace-Id": "Root=1-5e3cf674-0f5f7ff1792e4ab6064d2451"
  }, 
  "origin": "223.198.155.159", 
  "url": "https://httpbin.org/get"
}

参考上面的浏览器设置,去掉 Host 和 X-Amzn-Trace-Id 这些多余的头, 我们可以为 requests 的 get 请求加入 headers 参数,尽量保持和 Chrome 浏览器一致。

p5.py

\#! env python

\# -\*- coding=utf8 -\*- 
\# 指定字符编码为 utf8

import requests

def build\_headers():
    return {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,\*/\*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
    "Accept-Encoding": "gzip, deflate, br", 
    "Accept-Language": "zh-CN,zh;q=0.9", 
    "Sec-Fetch-Dest": "document", 
    "Sec-Fetch-Mode": "navigate", 
    "Sec-Fetch-Site": "none", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36"
  }

def get\_url(url):
    \#实用get方法请求url
    headers = build_headers()
    headers = ''
    res = requests.get(url, headers=headers)
    return res

def write(content):
    fn = './a.html'
    f  = open(fn,'w',encoding='utf8')
    f.write(content)
    f.close()

if __name__ == '\_\_main\_\_':

    import sys
    url = sys.argv[1]
    res = get_url(url)

    \#输出res内部成员
    print( dir(res) )

    \#打印http相应码
    print( res.status_code )

    \#导出网页源码
    write( str(res.content, encoding='utf8') )

运行 python p5.py 前,先更改 cmd codepage 参数,支持 UTF-8:

chcp 65001
python p5.py

输出的内容 a.html 和 Chrome 浏览器看到的源码基本一致。看到请求头已经处理好了。

实用 requests 模块请求,需要灵活设置 headers 参数。常见的 headers 设置项:

  • 设置 cookie:多数网站实用 cookie 来定位用户身份,如果不带 cookie 浏览页面可能出错
  • 设置 Referer:有的网站严格使用 Referer 来做图片防盗链,需要针对性设置 Referer 来突破封禁
  • 设置 proxy_forward_for:伪造代理服务器头,绕过较弱的 IP 频率拦截
  • 设置 User-Agent:可以防止通用的拦截,也可以设置 User-Agent 为手机 agent 专门爬去 Wap H5 格式的内容。Wap 页面内容通常比 Web 更简单,处理起来比较简单高效。

一个好用的类库,不仅需要入门简单,更需要的是控制的细节足够丰富。requests 库提供了灵活的控制接口,方便各种访问任务,经常需要控制的细节有:

  • 控制访问超时
r = requests.get('https://github.com', timeout=(3.05, 27))

  • 自定义身份验证:假设我们有一个 Web 服务,有一个古怪的验证方法,仅在 X-Pizza 头被设置为一个密码值的情况下才会有响应。
from requests.auth import AuthBase

class PizzaAuth(AuthBase):
    """Attaches HTTP Pizza Authentication to the given Request object."""
    def \_\_init\_\_(self, username):
        \# setup any auth-related data here
        self.username = username

    def \_\_call\_\_(self, r):
        \# modify and return the request
        r.headers['X-Pizza'] = self.username
        return r

然后就可以使用我们的 PizzaAuth 来进行网络请求:

\>\>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
<Response [200]>

  • 关掉 HTTPS 证书校验:我们实现的是爬虫,多数情况下没必要校验证书。
\>\>\> requests.get('https://github.com', verify=False)
<Response [200]>

  • 使用代理服务器:这可以隐藏访问者的身份,后面我们提到这一点。

具体用法细节可参考:

https://requests.readthedocs.io/zh_CN/latest/

解释内容 lxml 使用

内容爬回来了,怎样对内容进行解析使用呢?有很多类库可以做 HTML 内容解析,这里推荐 lxml 库对 HTTP 返回内容进行处理。

lxml 库支持使用 xpath 语法来对 HTML 文档进行解析,用 xpath 来解释 HTML 速度非常快。

lxml 的安装:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml

使用 lxml 的难点是观察文档结构,写好 xpath 查询语句。所以我们从一个简单的 HTML 文档入手:

\<html\>
\<head\>
\</head\>
\<body\>
  \<div id="info"\>
    \<div class="name"\>名字\</div\> 
    \<div class="value"\>数量\</div\>
    \<ul\>
       \<li\>
       \<div class="name"\>Pyton 进阶\</div\>
       \</li\>
       \<li\>
       \<div class="value"\>Pyton cook book\</div\>
       \<li\>
    \</ul\>
  \</div\>
\</body\>
\<html\>

p7.py

\#! env python

\# -\*- coding=utf8 -\*- 

import lxml

doc_html = ''' \<html\> \<head\> \</head\> \<body\> \<div id="info"\> \<div class="name"\>名字\</div\> \<div class="value"\>数量\</div\> \<ul\> \<li\> \<div class="name"\>Pyton 进阶\</div\> \<div class="value"\>10\</div\> \</li\> \<li\> \<div class="name"\>Pyton cook book\</div\> \<div class="value"\>2\</div\> \</li\> \</ul\> \</div\> \</body\> \<html\> '''

if __name__ == '\_\_main\_\_':

    from lxml import etree

    root = etree.HTML(doc_html)

    \#nodes 是节点列表
    nodes = root.xpath('//div [@id="info"]')
    if nodes :
        node = nodes[0]
        \#print( dir(node) )
        print( "attrib ID " + node.attrib['id'] )
        print( "tag " + node.tag )
        print( "text [" + node.text  + "]")

    nodes = root.xpath('//div [@id="info"]/ul/li')

    for node in nodes :
        names = node.xpath('./div [@class="name"]/text()')
        values = node.xpath('./div [@class="value"]/text()')
        print( "name " + names[0]  + ' value ' + values[0])

运行结果:

attrib ID info
tag div
text [
    ]
name Pyton 进阶 value 10
name Pyton cook book value 2

etree.HTML 返回一个 Element 类型的节点,Element 节点支持 xpath() 方法用 xpath 语法定位文档的其他节点:

  • //div 表示任意级别下的 div 节点
  • ./div 表示当前节点下的 div 节点
  • //div [@id="info"] 只选择属性等于 info 的节点

xpath 的语法可以参考:

https://www.w3school.com.cn/xpath/index.asp

只要细心对页面进行分析,使用 etree 相关接口就可以对所有站内所有页面进行解释,把非结构化的页面转化为结构化的数据。

现在很多站点使用前后端分离的架构(如 Vue),前端使用 AJAX 接口从后端获取 xhr 数据,数据一般都是 JSON 格式的,这样我们不需要使用 lxml 解释这么费劲,直接把 XHR 数据用 Python 的 JSON 模块处理就可以了。

举例:

\>\>> import json
\>\>> xhr='{ "key": "python cookbook" , "value" : "10" }'
\>\>> kv=json.loads(xhr)
\>\>> kv["key"], kv["value"]
('python cookbook', '10')

高级话题

有的 Web 站点是搜索引擎友好的,所有内容都可以简单爬取走。但提供信息查询服务的站点大多不希望自己的宝贵资料被爬走。他们会采取各种办法封锁爬虫,保护数据资源。

无论站点多复杂,只要使用合适的对策,基本没有爬不到的站点。下面尝试应对一些常见的反爬虫策略。既然是对抗,一定需要多次测试方可奏效。

如何应对站点的 IP 频率控制

Web 站点对访问的客户 IP 进行计数,如果一段时间访问的次数大于某个值,便把当前 IP 封禁一段时间。

解决的方法有:

  1. 降低访问频率
  2. 使用 HTTP 代理服务器

代理主要有 HTTP 代理和 Sock5 代理,下面是使用 HTTP 代理的代码:

import requests
proxies = {'http': 'http://127.0.0.1:1080', 'https': 'http://127.0.0.1:1080'}
url = 'http://www.baidu.com'
requests.post(url, proxies=proxies, verify=False) 

http://127.0.0.1:1080 这个代理地址只是一个示范,需要先保证代理地址可用,程序才能正常运行。

HTTP 代理服务器,可以自己搭建,也可以使用网上一些免费使用的服务。

这是一个开源的 HTTP 代理收集接口:

https://github.com/jhao104/proxy_pool

不过免费的代理服务器的稳定较差,如果需要稳定的 IP 建议自己购买可换 IP 的 VPS 服务器。在 VPS 上搭建一个代理服务器,定期拨号更换 IP。

另外也有供应商在出售代理 IP 服务,一天可以使用数万的 IP 地址,对于绝大多数场景都是够用的。

如何应对站点使用 JS 渲染前端

很多站点为了防爬,使用 JS 来渲染 HTML 内容,这种站点一般是不欢迎搜索引擎的。

通常破解方法有:

1. 阅读 JS 代码,找到内容拼接逻辑

如果 JS 逻辑比较简单,这个方法是简单高效的。但现在很多 JS 代码都是高度混淆的,又引入了大量的外部 JS 类库,阅读代码并不容易。

2. 使用 WebDriver 无头浏览器

使 WebDriver 的方式,可以比较完美模仿浏览器,自然可以执行 JS。无论前端多么复杂,WebDriver 都可以渲染出来。

由于方法 1 对技术要求太高,成功率也偏低,我们主要讲怎么使用无头浏览器来实现爬虫。

经典无头浏览器有 PhantomJS、Selenium,但我们介绍一种更加高效的 Pyppeteer。Pyppeteer 实际上是 Puppeteer 在 Python 的一个封装库,Pyppeteer 的核心是 Google Chromium 浏览器,所以渲染的效果堪称完美。Pyppeteer 使用异步 IO 的方式控制 Chromium 浏览器,所以性能是有保证的。

下面我们详细讲解如何安装和使用 Pyppeteer。

自动安装 Pyppeteer 需要上 google.com,很多人不具备这个条件,就算有条件速度也很慢。我们选择手动安装,安装过程略为复杂。

1. 下载 Chromium

国内下载地址为:

https://npm.taobao.org/mirrors/chromium-browser-snapshots/

选择符合 Windows 的 Chromium 版本并下载,解压在 C:\chrome-win 目录。

2. 安装 Pyppeteer

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple  pyppeteer

3. 编辑 Pyppeteer 安装包

在 Python 安装目录下找到第三方包的安装目录,再找到 Pyppeteer 包目录的 chromium_downloader.py 文件,让它找到我们下载的 Chromium。

我的路径是:

C:\\Users\\dev\\AppData\\Local\\Programs\\Python\\Python38-32\\Lib\\site-packages\\pyppeteer

供参考。

编辑方法:找到 chromiumExecutable 设置,修改 Chromium 路径,我的是 Win32 系统,所以我的修改是:

'win32': DOWNLOADS_FOLDER/REVISION/'chrome-win32'/'chrome.exe'

=>

'win32': DOWNLOADS_FOLDER/'c:/chrome-win/chrome.exe'

修改后:

chromiumExecutable = {
    'linux': DOWNLOADS_FOLDER/REVISION/'chrome-linux'/'chrome',
    'mac': (DOWNLOADS_FOLDER/REVISION/'chrome-mac'/'Chromium.app' /
            'Contents'/'MacOS'/'Chromium'),
    \#'win32': DOWNLOADS\_FOLDER/REVISION/'chrome-win32'/'chrome.exe',
    'win32': DOWNLOADS_FOLDER/'c:/chrome-win/chrome.exe',
    'win64': DOWNLOADS_FOLDER/REVISION/'chrome-win32'/'chrome.exe',
}

再更新一下 WebSockets 版本:

pip uninstall websockets

pip  install -i https://pypi.tuna.tsinghua.edu.cn/simple  websockets==6.0

写一个测试脚本看看 Pyppeteer 工作是否正常:

import asyncio
from pyppeteer import launch

async def get\_url(fn, url):

    print ("GO FOR ", url , " w ", fn)
    \#browser = await launch( headless = False)
    \#browser = await launch( )
    \#browser = await launch( {'args':['--no-sandbox']} )
    \#启动浏览器,可见模式
    browser = await launch( headless = False)
    \#新建页面
    page = await browser.newPage()
    await page.setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36')
    \#转到url, 关闭超时
    await page.goto(url, { 'timeout': 0 })
    \#等待加载
    await page.waitFor(2000)
    content = await page.content()
    cookies = await page.cookies()
    \#输出截屏
    await page.screenshot( {'path': fn } )
    \#关闭
    await browser.close()

if __name__ == '\_\_main\_\_':
    \#异步运行,等待返回
    asyncio.get_event_loop().run_until_complete( 
          get_url('qichacha.png','https://www.qichacha.com')
    )

如果运行无出错,当前目录有 qichacha.png 的截屏文件。

  • page.content() 是渲染后的 HTML 内容。HTML 内容可以使用 lxml 来处理。
  • page.cookies() 是浏览器得到的 cookie。获取到 cookie 后,也许就不需要使用 pyppeteer 处理后续的爬取任务,把 cookie 放在 headers 里面用 requests 处理往往更加方便。

page 对象还可以把 Web 页生成 PDF 文档 page.pdf()。

如何应对登录图片验证码

登录图片验证码的破解,只有一个办法就是图片识别。

大致上也有两个办法:

1. 接入打码平台

市面上有所谓的接码平台,后台的验证码全是人肉识别,验证通过率高。但需要付费。

2. OCR 图片自动识别

我们讲讲如何使用 OCR 识别破解图片验证码。

比较值得研究的是 Pytesseract 库。虽然一次性的识别率不是非常高,但这个库可以自行训练不断提高识别强度。

这是简单图片识别的代码:

from PIL import Image
import pytesseract 
def ocr(image\_file):

        \# 打开图片
        image = Image.open(image_file)

        \# 转为灰度图
        image_grey = image.convert('L')

        \# 图片二值化
        table = []
        for i in range(256):
            if i < 140:
                table.append(0)
            else:
                table.append(1)
        image_bi = image_grey.point(table, '1')
        \# 识别验证码
        verify_code = pytesseract.image_to_string(image_bi)

        return verify_code

值得注意的是,图片二值化这个步骤很重要,否则成功率很低。

如何应对登录滑动验证码

举一个例子应对 qichacha.com 的登录滑动条。

思路:

  1. 用 Pyppeteer 打开 https://www.qichcha.com
  2. 鼠标右键“检查”,查看“登录”按钮 selector 定位
  3. 点击登录按钮,弹起登录浮层。
  4. 加载完滑动条后,鼠标右键“检查”,获取滑块的坐标和大小,计算需要向右滑动的举例。
  5. 编写代码,把 1、2、3、4 步骤用 page 的各方法去实现滑块移动。

具体步骤:

1. peppeteer 打开 URL,并长期 sleep,我们需要 F12 调试。

    \#转到url, 关闭超时
    await page.goto(url, { 'timeout': 0 })
    \#等待加载
    await page.waitFor(600 * 1000)

2. 定位登录按钮

代码运行到调用起 Chromium 后,获取登录按钮元素的 selector 路径。

得到的路径为:

'body \> header \> div \> ul \> li:nth-child(14) \> a \> span'

3. 弹起浮层

这个代码可以实现元素的点击,点击后 JS 被执行,浮层弹起。

    slt_login = 'body \> header \> div \> ul \> li:nth-child(14) \> a \> span'
    await page.click(slt_login)

4. 获取浮层的相关元素 selector 路径,还有滑块活动长度.

#nc_1_n1z,滑动长度可以从 HTML 属性中获取。

以下函数实现拖动滑块去做验证。

\#distance=368 是计算的滑动总长度
async def try\_validation(page, distance=368):

    \#nc\_1\_n1z
    distance1 = distance - 20
    distance2 = 20
    \#获取滑块的位置的大小
    btn_position = await page.evaluate(''' () =\>{ return { x: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().x, y: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().y, width: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().width, height: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().height }} ''')
    x = btn_position['x'] + btn_position['width']/2
    y = btn_position['y'] + btn_position['height']/2
    \#移动鼠标到滑块开始位置
    await page.mouse.move(x, y)
    \#鼠标点击
    await page.mouse.down()
    \#拖动完第一部分
    await page.mouse.move(x + distance1, y+8, {'steps': 60})
    await page.waitFor(800)
    \#拖动完第二部分
    await page.mouse.move(x + distance1 + distance2, y-4, {'steps': 30})
    await page.waitFor(800)

总的来说,代码如下:

p9.py

import asyncio
from pyppeteer import launch

async def call\_js(page):
    js1 = '''() =\>{ Object.defineProperties(navigator,{ webdriver:{ get: () =\> false } }) }'''

    js2 = '''() =\> { window.navigator.chrome = { runtime: {}, // etc. }; }'''

    js3 = '''() =\>{ Object.defineProperty(navigator, 'languages', { get: () =\> ['en-US', 'en'] }); }'''

    js4 = '''() =\>{ Object.defineProperty(navigator, 'plugins', { get: () =\> [1, 2, 3, 4, 5,6], }); }'''
    await page.evaluate(js1)
    await page.evaluate(js2)
    await page.evaluate(js3)
    await page.evaluate(js4)

async def try\_validation(page, distance=258):

    \#nc\_1\_n1z
    distance1 = distance - 20
    distance2 = 20
    \#获取滑块的位置的大小
    btn_position = await page.evaluate(''' () =\>{ return { x: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().x, y: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().y, width: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().width, height: document.querySelector('\#nc\_1\_n1z').getBoundingClientRect().height }} ''')
    x = btn_position['x'] + btn_position['width']/2
    y = btn_position['y'] + btn_position['height']/2
    \#移动鼠标到滑块开始位置
    await page.mouse.move(x, y)
    \#鼠标点击
    await page.mouse.down()
    \#拖动完第一部分
    await page.mouse.move(x + distance1, y+8, {'steps': 60})
    await page.waitFor(800)
    \#拖动完第二部分
    await page.mouse.move(x + distance1 + distance2, y-4, {'steps': 30})
    await page.waitFor(800)

async def get\_url(fn, url):

    print ("GO FOR ", url , " w ", fn)
    \#browser = await launch( headless = False)
    \#browser = await launch( )
    \#browser = await launch( {'args':['--no-sandbox']} )
    \#启动浏览器,可见模式
    browser = await launch( headless = False)
    \#新建页面
    page = await browser.newPage()
    await page.setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36')
    \#转到url, 关闭超时
    await page.goto(url, { 'timeout': 0 })

    \#调用js
    await call_js(page)
    \#等待加载
    await page.waitFor(2000)

    \#点击登录 
    slt_login = 'body \> header \> div \> ul \> li:nth-child(14) \> a \> span'
    await page.click(slt_login)

    \#等待加载
    await page.waitFor(10000)

    \#滑块加载完成后,尝试去拖动滑块

    await try_validation(page, 368)
    \#关闭
    await browser.close()

if __name__ == '\_\_main\_\_':
    \#异步运行,等待返回
    asyncio.get_event_loop().run_until_complete( 
          get_url('qichacha.png','https://www.qichacha.com')
    )

其中有一个 call_js 函数,是在 Chromium 中 调用 JS,避免被站点识别是模拟浏览器。

以上代码,在当前时间,可以通过 qichacha.com 的滑动验证。

如何抽取正文内容去广告

我们爬取新闻、博客、CMS 等页面时,页面上可能有一些页眉页脚、广告等不属于正文的元素。

如果每爬取一个页面,都需要用 lxml 等解析器去针对性处理,就没有办法实现通用的正文爬取爬虫。要达到通用型,需要引入自动正文抽取库。

正文抽取库的选择比较多,实现的原理也各有差异,有的是基于规则的,有的基于简单统计的,有的是基于大规模语料统计的。安装难度和适用范围都有一定的差异。

newspaper 库安装:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple newspaper3k

示例代码 p12.py

\# -\*- coding: utf-8 -\*-
import newspaper
url =  'https://www.jianshu.com/p/4e93b48f9f63'
a = newspaper.Article(url,language='zh')
a.download()
a.parse()
print(a.text)

看打印出来的内容,已经基本满足需求。

Mac 版 Navicat Premium 下载地址(请使用分享链接下载):

英文版:https://pan.baidu.com/s/1hzEoEHmaOJ5EFc7Bpr6tDQ

提取码: 9eq9

在线生成非对称加密公钥私钥对:密钥是 2048 位的,PKCS#8 格式。

本人使用

公钥:

...
readability

安装:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple readability-lxml

使用例子:p13.py

import requests
from readability import Document

headers = {}
headers['User-Agent'] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_13\_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36";
url = 'https://www.jianshu.com/p/4e93b48f9f63'
response = requests.get(url, headers=headers)
html = response.content
doc = Document(html)
print('title:', doc.title())
print('content:', doc.summary(html\_partial=True))

title: 2019-01-11亲测Navicat Premium for Mac破解 - 简书
content: \<div\>\<article class="\_2rhmJa"\>\<p\>Mac版 Navicat Premium 下载地址(请使用分享链接下载):\<br/\>
[英文版](链接: \<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fpan.baidu.com%2Fs%2F1hzEoEHmaOJ5EFc7Bpr6tDQ" target="\_blank"\>https://pan.baidu.com/s/1hzEoEHmaOJ5EFc7Bpr6tDQ\</a\>)\<br/\>
提取码: 9eq9\</p\>
\<p\>\<a href="https://links.jianshu.com/go?to=http%3A%2F%2Fweb.chacuo.net%2Fnetrsakeypair" target="\_blank"\>在线生成非对称 加密公钥私钥对\</a\> -- 密钥是2048位的,PKCS#8格式\</p\>
\<p\>本人使用:\<br/\>
公钥:\</p\>
\<pre\>\<code\>-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvc8jdtI4y68rvFvRULCY
hprieJINbeOkzUBoQVRl2o2VNE5qWy9lNmS7reCfCwqq/YQpbKH2dHrhiICviNiM
DKRLw1NH+fGDzje3qCHm8tG5EHZQSTyqDe7rI8UDN1W3vk28Snwz97XQ+toVfiA3
4zGNbWYsKmEBjxXR502ZLwf2oCx64zFZLNJeub0UVrZMLOTSnClPHT0cfFvRdzHB
qDGx8KuOUgKBzuPyrUYwF8t5byXdxWwPOaNQu/aoEecZX0wbxvu06LmKxfJ6kaUE
hoe9ztH4XQNcpxF68O3Z7BNsitkDEzV8G40t/uLoE09WHtOD/YEW0zLCOlSb74pw
twIDAQAB
-----END PUBLIC KEY-----
\</code\>\</pre\>

newspaper 提取了纯文本,readability 保留了 HTML tag 供参考。

建议同时使用多个正文提取库,综合其优点灵活使用。

简单总结

结语:本文简要介绍了 Python 作为爬虫的相关技术,从这里可以看到使用 Python 做项目,诀窍是尽量使用成熟的模块类库,这样减少代码,达到事半功倍的效果。把精力放在真正的业务上。

值得注意的是,爬虫运行需要注意风险,如果资料并非公开发布,原则上是不能爬取的,爬取敏感资料有法律风险;另外,也有遵守爬虫的规则,不可以频繁爬取对方网站,给对方网站造成服务压力。


欢迎关注我的公众号,回复关键字“大礼包” ,将会有大礼相送!!! 祝各位面试成功!!!

发布了112 篇原创文章 · 获赞 2 · 访问量 5542

猜你喜欢

转载自blog.csdn.net/weixin_41818794/article/details/104524056