现在很多平台把 Python 当做成功学传播,制作了很多昂贵的 Python 课程,其中还不乏部分粗制滥造的课程。
作为 10 年 Python 使用经验的程序员,觉得有必要告诉大家,Python 入门其实很简单,完全没有必要花大价钱去学习。
本文从比较流行爬虫为例,抛砖引玉,介绍 Python 在公开数据获取上的强大和灵活性。
Python 是荷兰计算机科学家 Guido van Rossum 发明的一款解释型、强类型、动态的、支持对象的高级程序设计语言。
初期 Python 仅仅是个人项目,现在已经发展成了时下最热门的编程语言之一,2020 年初在 TIOBE 榜单稳定排第三。Python 在人工智能的应用领域占领绝对优势。
Python 是解释性语言,非常适合作为入门的程序设计语言,它无须编译,编写完成即可运行。
尽管 Python 是动态语言,但它的数据类型是强类型的,避免了像 JS 这样“过分动态”为初学者带来各种奇怪的困惑。
Python 在支持面向对象编程,在发展过程中不断借鉴其他语言的强项。语言特性非常丰富,功能强大。
Python 语言自带类库足够好用,其生态系统也非常完善。围绕着 Python 生态的类库,领域丰富,质量又非常高。这是非常难得的。相比 npm 管理的库,虽然数量极多,但总体质量就不敢恭维了。
1. Python 入门简单
Python 语法比较简单,核心关键字数量较少,结构清晰。为了达到结构清晰的目的,Python 用代码缩进来表达程序结构,在一般的编程语言,缩进往往只是一种美化代码的方法。这一点非常适合强迫症用户。
2. Python 有丰富的标准库
Python 内置的标准模块非常丰富,可以满足一般科学计算、文本处理、后端服务等需求,Python 甚至内置了一个 Demo 性质 HTTP 服务器。用户可以借助一个丰富的标准库,用较少的代码就可以构建一个规模较大的应用。对语言流行的助力是非常大的。
3. Python 生态优秀
Python 在 Web 框架、网络爬虫、网络内容提取、模板引擎、数据库、数据可视化、图片处理、文本处理、自然语言处理、机器学习、日志、代码分析等领域都有非常高质量的模块和库。使用高质量的库进行开发,系统中的坑自然会少很多。
基于以上原因,使用 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 3 在 Windows 系统下的安装步骤。
1. 打开 Windows 版本下载页
https://www. python.org/downloads/wi ndows/
根据你的 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 程序:
#! 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 的一些基本语法概念。
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 爬虫的实现细节。
因为 HTTP 协议是简单字符协议,实现爬虫可以很简单。但是站长需要保护站内资源,采取各种反爬虫手段和爬虫对抗,真正实用爬虫会比较复杂。用好爬虫,需要对 HTTP 相关协议进行比较深入的了解。
这里不会引入所谓的爬虫框架,框架固然可能比较高效,但它隐藏了细节,学习它无助于我们理解爬虫的原理。手动实现的爬虫更加灵活。
Web 页面的打开,需要浏览器和服务器进行多次的 HTTP 交互。HTTP 协议是一个明文字符协议,对协议的研究非常方便。
HTTP 最常用的方法是 post 和 get,最简单的爬虫就是 get 一个 URL,对返回内容进行解释。
研究 HTTP 请求最方便的办法,是在使用 Chrome 请求 Web 页面的时候,打开 F12,分析 HTTP 的请求/返回。使用 Postman/curl 软件,编辑 HTTP 请求数据包不断测试 Web 服务器的返回。
但 Web 页的交互往往是复杂的,经常遇到的问题可能有:
种种复杂问题,对爬虫的实现者都是挑战。为了更加高效的实现爬虫,建议实用 requests 库。requests 是奇才 kennethreitz 实现的一个 HTTP 访问库,它封装了 HTTP 协议的细节,号称是一个给人类使用的 HTTP 请求类库。
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 拦截了我们的请求,返回了错误服务:
405错误页面
这和浏览器打开访问 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,容易想到是 http://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 设置项:
一个好用的类库,不仅需要入门简单,更需要的是控制的细节足够丰富。requests 库提供了灵活的控制接口,方便各种访问任务,经常需要控制的细节有:
r = requests.get('https://github.com', timeout=(3.05, 27))
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'))
>>> requests.get('https://github.com', verify=False)
具体用法细节可参考:
https:// requests.readthedocs.io /zh_CN/latest/
内容爬回来了,怎样对内容进行解析使用呢?有很多类库可以做 HTML 内容解析,这里推荐 lxml 库对 HTTP 返回内容进行处理。
lxml 库支持使用 xpath 语法来对 HTML 文档进行解析,用 xpath 来解释 HTML 速度非常快。
lxml 的安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml
使用 lxml 的难点是观察文档结构,写好 xpath 查询语句。所以我们从一个简单的 HTML 文档入手:
名字
数量
-
Pyton 进阶
-
Pyton cook book
-
p7.py
#! env python
# -*- coding=utf8 -*-
import lxml
doc_html = '''
名字
数量
-
Pyton 进阶 10
-
Pyton cook book 2
'''
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/i ndex.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 站点是搜索引擎友好的,所有内容都可以简单爬取走。但提供信息查询服务的站点大多不希望自己的宝贵资料被爬走。他们会采取各种办法封锁爬虫,保护数据资源。
无论站点多复杂,只要使用合适的对策,基本没有爬不到的站点。下面尝试应对一些常见的反爬虫策略。既然是对抗,一定需要多次测试方可奏效。
Web 站点对访问的客户 IP 进行计数,如果一段时间访问的次数大于某个值,便把当前 IP 封禁一段时间。
解决的方法有:
代理主要有 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/prox y_pool
不过免费的代理服务器的稳定较差,如果需要稳定的 IP 建议自己购买可换 IP 的 VPS 服务器。在 VPS 上搭建一个代理服务器,定期拨号更换 IP。
另外也有供应商在出售代理 IP 服务,一天可以使用数万的 IP 地址,对于绝大多数场景都是够用的。
很多站点为了防爬,使用 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 需要上 http://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:UsersdevAppDataLocalProgramsPythonPython38-32Libsite-packagespyppeteer
供参考。
编辑方法:找到 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 对象还可以把 Web 页生成 PDF 文档 page.pdf()。
登录图片验证码的破解,只有一个办法就是图片识别。
大致上也有两个实用手段
----------
文章发在本人gitchat平台,需要请前往:
https://gitbook.cn/books/5e3e976f89a91a7be03b9810/index.html