一、网络爬虫

1. 什么是网络爬虫:

  1. 通俗理解:爬虫是一个模拟人类请求网站行为的程序。可以自动请求网页、并数据抓取下来,然后使用一定的规则提取有价值的数据。
  2. 专业介绍:网络爬虫(又称为网页蜘蛛,网络机器人,在 FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的 程序或者 脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

2. 通用爬虫和聚焦爬虫:

  1. 通用爬虫:通用爬虫是搜索引擎抓取系统(百度、谷歌、搜狗等)的重要组成部分。主要是将互联网上的网页下载到本地,形成一个互联网内容的镜像备份。
  2. 聚焦爬虫:是面向特定需求的一种网络爬虫程序,他与通用爬虫的区别在于:聚焦爬虫在实施网页抓取的时候会对内容进行筛选和处理,尽量保证只抓取与需求相关的网页信息。

3. 为什么用Python写爬虫程序:

  1. PHP:PHP是世界是最好的语言,但他天生不是做这个的,而且对多线程、异步支持不是很好,并发处理能力弱。爬虫是工具性程序,对速度和效率要求比较高。
  2. Java:生态圈很完善,是Python爬虫最大的竞争对手。但是Java语言本身很笨重,代码量很大。重构成本比较高,任何修改会导致代码大量改动。爬虫经常要修改采集代码。
  3. C/C++:运行效率是无敌的。但是学习和开发成本高。写个小爬虫程序可能要大半天时间。
  4. Python:语法优美、代码简洁、开发效率高、支持的模块多。相关的HTTP请求模块和HTML解析模块非常丰富。还有Scrapy和Scrapy-redis框架让我们开发爬虫变得异常简单。

4. 准备工具:

  1. Python3.6开发环境。
  2. Pycharm、vscode

二、http协议和Chrome抓包工具

1.什么是http和https协议:

HTTP协议:全称是HyperText Transfer Protocol,中文意思是超文本传输协议,是一种发布和接收HTML页面的方法。服务器端口号是80端口。 HTTPS协议:是HTTP协议的加密版本,在HTTP下加入了SSL层。服务器端口号是443端口。

2. 在浏览器中发送一个http请求的过程:

  1. 当用户在浏览器的地址栏中输入一个URL并按回车键之后,浏览器会向HTTP服务器发送HTTP请求。HTTP请求主要分为“Get”和“Post”两种方法。
  2. 当我们在浏览器输入URL  http://www.baidu.com 的时候,浏览器发送一个Request请求去获取  http://www.baidu.com 的html文件,服务器把Response文件对象发送回给浏览器。
  3. 浏览器分析Response中的 HTML,发现其中引用了很多其他文件,比如Images文件,CSS文件,JS文件。 浏览器会自动再次发送Request去获取图片,CSS文件,或者JS文件。
  4. 当所有的文件都下载成功后,网页会根据HTML语法结构,完整的显示出来了。

3. url详解:

URLUniform Resource Locator的简写,统一资源定位符。 一个URL由以下几部分组成:

scheme://host:port/path/?query-string=xxx#anchor
  • 1.
  • scheme:代表的是访问的协议,一般为http或者https以及ftp等。
  • host:主机名,域名,比如www.baidu.com
  • port:端口号。当你访问一个网站的时候,浏览器默认使用80端口。
  • path:查找路径。比如:www.jianshu.com/trending/now,后面的trending/now就是path
  • query-string:查询字符串,比如:www.baidu.com/s?wd=python,后面的wd=python就是查询字符串。
  • anchor:锚点,后台一般不用管,前端用来做页面定位的。

在浏览器中请求一个url,浏览器会对这个url进行一个编码。除英文字母,数字和部分符号外,其他的全部使用百分号+十六进制码值进行编码。

4. 常用的请求方法:

Http协议中,定义了八种请求方法。这里介绍两种常用的请求方法,分别是get请求和post请求。

  1. get请求:一般情况下,只从服务器获取数据下来,并不会对服务器资源产生任何影响的时候会使用get请求。
  2. post请求:向服务器发送数据(登录)、上传文件等,会对服务器资源产生影响的时候会使用post请求。 以上是在网站开发中常用的两种方法。并且一般情况下都会遵循使用的原则。但是有的网站和服务器为了做反爬虫机制,也经常会不按常理出牌,有可能一个应该使用get方法的请求就一定要改成post请求,这个要视情况而定。

5. 请求头常见参数:

http协议中,向服务器发送一个请求,数据分为三部分,第一个是把数据放在url中,第二个是把数据放在body中(在post请求中),第三个就是把数据放在head中。这里介绍在网络爬虫中经常会用到的一些请求头参数:

  1. User-Agent:浏览器名称。这个在网络爬虫中经常会被使用到。请求一个网页的时候,服务器通过这个参数就可以知道这个请求是由哪种浏览器发送的。如果我们是通过爬虫发送请求,那么我们的User-Agent就是Python,这对于那些有反爬虫机制的网站来说,可以轻易的判断你这个请求是爬虫。因此我们要经常设置这个值为一些浏览器的值,来伪装我们的爬虫。
  2. Referer:表明当前这个请求是从哪个url过来的。这个一般也可以用来做反爬虫技术。如果不是从指定页面过来的,那么就不做相关的响应。
  3. Cookiehttp协议是无状态的。也就是同一个人发送了两次请求,服务器没有能力知道这两个请求是否来自同一个人。因此这时候就用cookie来做标识。一般如果想要做登录后才能访问的网站,那么就需要发送cookie信息了。

6. 常见响应状态码:

  1. 200:请求正常,服务器正常的返回数据。
  2. 301:永久重定向。比如在访问www.jingdong.com的时候会重定向到www.jd.com
  3. 302:临时重定向。比如在访问一个需要登录的页面的时候,而此时没有登录,那么就会重定向到登录页面。
  4. 400:请求的url在服务器上找不到。换句话说就是请求url错误。
  5. 403:服务器拒绝访问,权限不够。
  6. 500:服务器内部错误。可能是服务器出现bug了。

7. Chrome抓包工具:

Chrome浏览器是一个非常亲近开发者的浏览器。可以方便的查看网络请求以及发送的参数。对着网页右键->检查。然后就可以打开开发者选项。以下用图片来说明。

三、urllib库

urllib库是Python中一个最基本的网络请求库。可以模拟浏览器的行为,向指定的服务器发送一个请求,并可以保存服务器返回的数据。

1. urlopen函数:

Python3urllib库中,所有和网络请求相关的方法,都被集到urllib.request模块下面了,以先来看下urlopen函数基本的使用:

from urllib import request
resp = request.urlopen('http://www.baidu.com')
print(resp.read())
  • 1.
  • 2.
  • 3.

实际上,使用浏览器访问百度,右键查看源代码。你会发现,跟我们刚才打印出来的数据是一模一样的。也就是说,上面的三行代码就已经帮我们把百度的首页的全部代码爬下来了。一个基本的url请求对应的python代码真的非常简单。
以下对urlopen函数的进行详细讲解:

  1. url:请求的url。
  2. data:请求的data,如果设置了这个值,那么将变成post请求。
  3. 返回值:返回值是一个http.client.HTTPResponse对象,这个对象是一个类文件句柄对象。有read(size)readlinereadlines以及getcode等方法。

2. urlretrieve函数:

这个函数可以方便的将网页上的一个文件保存到本地。以下代码可以非常方便的将百度的首页下载到本地:

from urllib import request
request.urlretrieve('http://www.baidu.com/','baidu.html')
  • 1.
  • 2.

3. urlencode函数:

用浏览器发送请求的时候,如果url中包含了中文或者其他特殊字符,那么浏览器会自动的给我们进行编码。而如果使用代码发送请求,那么就必须手动的进行编码,这时候就应该使用urlencode函数来实现。urlencode可以把字典数据转换为URL编码的数据。示例代码如下:

from urllib import parse
data = {'name':'爬虫基础','greet':'hello world','age':100}
qs = parse.urlencode(data)
print(qs)
  • 1.
  • 2.
  • 3.
  • 4.

4. parse_qs函数:

可以将经过编码后的url参数进行解码。示例代码如下:

from urllib import parse
qs = "name=%E7%88%AC%E8%99%AB%E5%9F%BA%E7%A1%80&greet=hello+world&age=100"
print(parse.parse_qs(qs))
  • 1.
  • 2.
  • 3.

5. urlparse和urlsplit:

有时候拿到一个url,想要对这个url中的各个组成部分进行分割,那么这时候就可以使用urlparse或者是urlsplit来进行分割。示例代码如下:

from urllib import request,parse

url = 'http://www.baidu.com/s?username=zhiliao'

result = parse.urlsplit(url)
# result = parse.urlparse(url)

print('scheme:',result.scheme)
print('netloc:',result.netloc)
print('path:',result.path)
print('query:',result.query)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

urlparseurlsplit基本上是一模一样的。唯一不一样的地方是,urlparse里面多了一个params属性,而urlsplit没有这个params属性。比如有一个url为:url = 'http://www.baidu.com/s;hello?wd=python&username=abc#1'
那么urlparse可以获取到hello,而urlsplit不可以获取到。url中的params也用得比较少。

6. request.Request类:

如果想要在请求的时候增加一些请求头,那么就必须使用request.Request类来实现。比如要增加一个User-Agent,示例代码如下:

from urllib import request

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
req = request.Request("http://www.baidu.com/",headers=headers)
resp = request.urlopen(req)
print(resp.read())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

7. 内涵段子爬虫实战作业:

  1. url链接: http://neihanshequ.com/bar/1/
  2. 要求:能爬取一页的数据就可以了。

8. ProxyHandler处理器(代理设置)

很多网站会检测某一段时间某个IP的访问次数(通过流量统计,系统日志等),如果访问次数多的不像正常人,它会禁止这个IP的访问。
所以我们可以设置一些代理服务器,每隔一段时间换一个代理,就算IP被禁止,依然可以换个IP继续爬取。
urllib中通过ProxyHandler来设置使用代理服务器,下面代码说明如何使用自定义opener来使用代理:

from urllib import request

# 这个是没有使用代理的
# resp = request.urlopen('http://httpbin.org/get')
# print(resp.read().decode("utf-8"))

# 这个是使用了代理的
handler = request.ProxyHandler({"http":"218.66.161.88:31769"})

opener = request.build_opener(handler)
req = request.Request("http://httpbin.org/ip")
resp = opener.open(req)
print(resp.read())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

常用的代理有:

9. 什么是cookie:

在网站中,http请求是无状态的。也就是说即使第一次和服务器连接后并且登录成功后,第二次请求服务器依然不能知道当前请求是哪个用户。cookie的出现就是为了解决这个问题,第一次登录后服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的cookie数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是哪个了。cookie存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4KB。因此使用cookie只能存储一些小量的数据。

cookie的格式:
Set-Cookie: NAME=VALUE;Expires/Max-age=DATE;Path=PATH;Domain=DOMAIN_NAME;SECURE
  • 1.

参数意义:

  • NAME:cookie的名字。
  • VALUE:cookie的值。
  • Expires:cookie的过期时间。
  • Path:cookie作用的路径。
  • Domain:cookie作用的域名。
  • SECURE:是否只在https协议下起作用。

10. 案例

Cookie 是指网站服务器为了辨别用户身份和进行Session跟踪,而储存在用户浏览器上的文本文件,Cookie可以保持登录信息到用户下次与服务器的会话。
这里以人人网为例。人人网中,要访问某个人的主页,必须先登录才能访问,登录说白了就是要有cookie信息。那么如果我们想要用代码的方式访问,就必须要有正确的cookie信息才能访问。解决方案有两种,第一种是使用浏览器访问,然后将cookie信息复制下来,放到headers中。示例代码如下:

from urllib import request

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
    'Cookie': 'anonymid=jacdwz2x-8bjldx; depovince=GW; _r01_=1; _ga=GA1.2.1455063316.1511436360; _gid=GA1.2.862627163.1511436360; wp=1; JSESSIONID=abczwY8ecd4xz8RJcyP-v; jebecookies=d4497791-9d41-4269-9e2b-3858d4989785|||||; ick_login=884e75d4-f361-4cff-94bb-81fe6c42b220; _de=EA5778F44555C091303554EBBEB4676C696BF75400CE19CC; p=61a3c7d0d4b2d1e991095353f83fa2141; first_login_flag=1; ln_uact=970138074@qq.com; ln_hurl=http://hdn.xnimg.cn/photos/hdn121/20170428/1700/main_nhiB_aebd0000854a1986.jpg; t=3dd84a3117737e819dd2c32f1cdb91d01; societyguester=3dd84a3117737e819dd2c32f1cdb91d01; id=443362311; xnsid=169efdc0; loginfrom=syshome; ch_id=10016; jebe_key=9c062f5a-4335-4a91-bf7a-970f8b86a64e%7Ca022c303305d1b2ab6b5089643e4b5de%7C1511449232839%7C1; wp_fold=0'
}

url = 'http://www.renren.com/880151247/profile'

req = request.Request(url,headers=headers)
resp = request.urlopen(req)
with open('renren.html','w') as fp:
    fp.write(resp.read().decode('utf-8'))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

但是每次在访问需要cookie的页面都要从浏览器中复制cookie比较麻烦。在Python处理Cookie,一般是通过http.cookiejar模块和urllib模块的HTTPCookieProcessor处理器类一起使用。http.cookiejar模块主要作用是提供用于存储cookie的对象。而HTTPCookieProcessor处理器主要作用是处理这些cookie对象,并构建handler对象。

http.cookiejar模块:

该模块主要的类有CookieJar、FileCookieJar、MozillaCookieJar、LWPCookieJar。这四个类的作用分别如下:

  1. CookieJar:管理HTTP cookie值、存储HTTP请求生成的cookie、向传出的HTTP请求添加cookie的对象。整个cookie都存储在内存中,对CookieJar实例进行垃圾回收后cookie也将丢失。
  2. FileCookieJar (filename,delayload=None,policy=None):从CookieJar派生而来,用来创建FileCookieJar实例,检索cookie信息并将cookie存储到文件中。filename是存储cookie的文件名。delayload为True时支持延迟访问访问文件,即只有在需要时才读取文件或在文件中存储数据。
  3. MozillaCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与Mozilla浏览器 cookies.txt兼容的FileCookieJar实例。
  4. LWPCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与libwww-perl标准的 Set-Cookie3 文件格式兼容的FileCookieJar实例。
登录人人网:

利用http.cookiejarrequest.HTTPCookieProcessor登录人人网。相关示例代码如下:

from urllib import request,parse
from http.cookiejar import CookieJar

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}

def get_opener():
    cookiejar = CookieJar()
    handler = request.HTTPCookieProcessor(cookiejar)
    opener = request.build_opener(handler)
    return opener

def login_renren(opener):
    data = {"email": "970138074@qq.com", "password": "pythonspider"}
    data = parse.urlencode(data).encode('utf-8')
    login_url = "http://www.renren.com/PLogin.do"
    req = request.Request(login_url, headers=headers, data=data)
    opener.open(req)

def visit_profile(opener):
    url = 'http://www.renren.com/880151247/profile'
    req = request.Request(url,headers=headers)
    resp = opener.open(req)
    with open('renren.html','w') as fp:
        fp.write(resp.read().decode("utf-8"))

if __name__ == '__main__':
    opener = get_opener()
    login_renren(opener)
    visit_profile(opener)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
保存cookie到本地:

保存cookie到本地,可以使用cookiejarsave方法,并且需要指定一个文件名:

from urllib import request
from http.cookiejar import MozillaCookieJar

cookiejar = MozillaCookieJar("cookie.txt")
handler = request.HTTPCookieProcessor(cookiejar)
opener = request.build_opener(handler)

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
req = request.Request('http://httpbin.org/cookies',headers=headers)

resp = opener.open(req)
print(resp.read())
cookiejar.save(ignore_discard=True,ignore_expires=True)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
从本地加载cookie:

从本地加载cookie,需要使用cookiejarload方法,并且也需要指定方法:

from urllib import request
from http.cookiejar import MozillaCookieJar

cookiejar = MozillaCookieJar("cookie.txt")
cookiejar.load(ignore_expires=True,ignore_discard=True)
handler = request.HTTPCookieProcessor(cookiejar)
opener = request.build_opener(handler)

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
}
req = request.Request('http://httpbin.org/cookies',headers=headers)

resp = opener.open(req)
print(resp.read())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

四、requests库

虽然Python的标准库中 urllib模块已经包含了平常我们使用的大多数功能,但是它的 API 使用起来让人感觉不太好,而 Requests宣传是 “HTTP for Humans”,说明使用更简洁方便。

1. 安装和文档地址:

利用pip可以非常方便的安装:

pip install requests
  • 1.

中文文档: http://docs.python-requests.org/zh_CN/latest/index.htmlgithub地址: https://github.com/requests/requests

2. 发送GET请求:

  1. 最简单的发送get请求就是通过requests.get来调用:
response = requests.get("http://www.baidu.com/")
  • 1.

  1. 添加headers和查询参数:
    如果想添加 headers,可以传入headers参数来增加请求头中的headers信息。如果要将参数放在url中传递,可以利用 params 参数。相关示例代码如下:
import requests

 kw = {'wd':'中国'}

 headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"}

 # params 接收一个字典或者字符串的查询参数,字典类型自动转换为url编码,不需要urlencode()
 response = requests.get("http://www.baidu.com/s", params = kw, headers = headers)

 # 查看响应内容,response.text 返回的是Unicode格式的数据
 print(response.text)

 # 查看响应内容,response.content返回的字节流数据
 print(response.content)

 # 查看完整url地址
 print(response.url)

 # 查看响应头部字符编码
 print(response.encoding)

 # 查看响应码
 print(response.status_code)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

3. 发送POST请求:

  1. 最基本的POST请求可以使用post方法:
response = requests.post("http://www.baidu.com/",data=data)
  • 1.

  1. 传入data数据:
    这时候就不要再使用urlencode进行编码了,直接传入一个字典进去就可以了。比如请求拉勾网的数据的代码:
import requests

 url = "https://www.lagou.com/jobs/positionAjax.json?city=%E6%B7%B1%E5%9C%B3&needAddtionalResult=false&isSchoolJob=0"

 headers = {
     'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
     'Referer': 'https://www.lagou.com/jobs/list_python?labelWords=&fromSearch=true&suginput='
 }

 data = {
     'first': 'true',
     'pn': 1,
     'kd': 'python'
 }

 resp = requests.post(url,headers=headers,data=data)
 # 如果是json数据,直接可以调用json方法
 print(resp.json())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

4. 使用代理:

使用requests添加代理也非常简单,只要在请求的方法中(比如get或者post)传递proxies参数就可以了。示例代码如下:

import requests

url = "http://httpbin.org/get"

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
}

proxy = {
    'http': '171.14.209.180:27829'
}

resp = requests.get(url,headers=headers,proxies=proxy)
with open('xx.html','w',encoding='utf-8') as fp:
    fp.write(resp.text)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

5. cookie:

如果在一个响应中包含了cookie,那么可以利用cookies属性拿到这个返回的cookie值:

import requests

url = "http://www.renren.com/PLogin.do"
data = {"email":"970138074@qq.com",'password':"pythonspider"}
resp = requests.get('http://www.baidu.com/')
print(resp.cookies)
print(resp.cookies.get_dict())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

6. session:

之前使用urllib库,是可以使用opener发送多个请求,多个请求之间是可以共享cookie的。那么如果使用requests,也要达到共享cookie的目的,那么可以使用requests库给我们提供的session对象。注意,这里的session不是web开发中的那个session,这个地方只是一个会话的对象而已。还是以登录人人网为例,使用requests来实现。示例代码如下:

import requests

url = "http://www.renren.com/PLogin.do"
data = {"email":"970138074@qq.com",'password':"pythonspider"}
headers = {
    'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
}

# 登录
session = requests.session()
session.post(url,data=data,headers=headers)

# 访问大鹏个人中心
resp = session.get('http://www.renren.com/880151247/profile')

print(resp.text)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

7. 处理不信任的SSL证书:

对于那些已经被信任的SSL整数的网站,比如https://www.baidu.com/,那么使用requests直接就可以正常的返回响应。示例代码如下:

resp = requests.get('http://www.12306.cn/mormhweb/',verify=False)
print(resp.content.decode('utf-8'))
  • 1.
  • 2.

五、XPath语法

1. 什么是XPath?

xpath(XML Path Language)是一门在XML和HTML文档中查找信息的语言,可用来在XML和HTML文档中对元素和属性进行遍历。

2. XPath开发工具

  1. Chrome插件XPath Helper。
  2. Firefox插件Try XPath。

3. XPath语法

选取节点:

XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。

表达式

描述

示例

结果

nodename

选取此节点的所有子节点

bookstore

选取bookstore下所有的子节点

/

如果是在最前面,代表从根节点选取。否则选择某节点下的某个节点

/bookstore

选取根元素下所有的bookstore节点

//

从全局节点中选择节点,随便在哪个位置

//book

从全局节点中找到所有的book节点

@

选取某个节点的属性

//book[@price]

选择所有拥有price属性的book节点

.

当前节点

./a

选取当前节点下的a标签

谓语:

谓语用来查找某个特定的节点或者包含某个指定的值的节点,被嵌在方括号中。
在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:

路径表达式

描述

/bookstore/book[1]

选取bookstore下的第一个子元素

/bookstore/book[last()]

选取bookstore下的倒数第二个book元素。

bookstore/book[position()<3]

选取bookstore下前面两个子元素。

//book[@price]

选取拥有price属性的book元素

//book[@price=10]

选取所有属性price等于10的book元素

通配符

*表示通配符。

通配符

描述

示例

结果

*

匹配任意节点

/bookstore/*

选取bookstore下的所有子元素。

@*

匹配节点中的任何属性

//book[@*]

选取所有带有属性的book元素。

选取多个路径:

通过在路径表达式中使用“|”运算符,可以选取若干个路径。
示例如下:

//bookstore/book | //book/title
# 选取所有book元素以及book元素下所有的title元素
  • 1.
  • 2.

六、lxml库

lxml 是 一个HTML/XML的解析器,主要的功能是如何解析和提取 HTML/XML 数据。

lxml和正则一样,也是用 C 实现的,是一款高性能的 Python HTML/XML 解析器,我们可以利用之前学习的XPath语法,来快速的定位特定元素以及节点信息。

lxml python 官方文档: http://lxml.de/index.html

需要安装C语言库,可使用 pip 安装:pip install lxml

1. 基本使用:

我们可以利用他来解析HTML代码,并且在解析HTML代码的时候,如果HTML代码不规范,他会自动的进行补全。示例代码如下:

# 使用 lxml 的 etree 库
from lxml import etree 

text = '''
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a> # 注意,此处缺少一个 </li> 闭合标签
     </ul>
 </div>
'''

#利用etree.HTML,将字符串解析为HTML文档
html = etree.HTML(text) 

# 按字符串序列化HTML文档
result = etree.tostring(html) 

print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

输入结果如下:

<html><body>
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html">third item</a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
 </div>
</body></html>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

可以看到。lxml会自动修改HTML代码。例子中不仅补全了li标签,还添加了body,html标签。

2. 从文件中读取html代码:

除了直接使用字符串进行解析,lxml还支持从文件中读取内容。我们新建一个hello.html文件:

<!-- hello.html -->
<div>
    <ul>
         <li class="item-0"><a href="link1.html">first item</a></li>
         <li class="item-1"><a href="link2.html">second item</a></li>
         <li class="item-inactive"><a href="link3.html"><span class="bold">third item</span></a></li>
         <li class="item-1"><a href="link4.html">fourth item</a></li>
         <li class="item-0"><a href="link5.html">fifth item</a></li>
     </ul>
 </div>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

然后利用etree.parse()方法来读取文件。示例代码如下:

from lxml import etree

# 读取外部文件 hello.html
html = etree.parse('hello.html')
result = etree.tostring(html, pretty_print=True)

print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

输入结果和之前是相同的。

3. 在lxml中使用XPath语法:

  1. 获取所有li标签:
from lxml import etree

 html = etree.parse('hello.html')
 print type(html)  # 显示etree.parse() 返回类型

 result = html.xpath('//li')

 print(result)  # 打印<li>标签的元素集合
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

  1. 获取所有li元素下的所有class属性的值:
from lxml import etree

 html = etree.parse('hello.html')
 result = html.xpath('//li/@class')

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

  1. 获取li标签下href为www.baidu.com的a标签:
from lxml import etree

 html = etree.parse('hello.html')
 result = html.xpath('//li/a[@href="www.baidu.com"]')

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

  1. 获取li标签下所有span标签:
from lxml import etree

 html = etree.parse('hello.html')

 #result = html.xpath('//li/span')
 #注意这么写是不对的:
 #因为 / 是用来获取子元素的,而 <span> 并不是 <li> 的子元素,所以,要用双斜杠

 result = html.xpath('//li//span')

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

  1. 获取li标签下的a标签里的所有class:
from lxml import etree

 html = etree.parse('hello.html')
 result = html.xpath('//li/a//@class')

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

  1. 获取最后一个li的a的href属性对应的值:
from lxml import etree

 html = etree.parse('hello.html')

 result = html.xpath('//li[last()]/a/@href')
 # 谓语 [last()] 可以找到最后一个元素

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

  1. 获取倒数第二个li元素的内容:
from lxml import etree

 html = etree.parse('hello.html')
 result = html.xpath('//li[last()-1]/a')

 # text 方法可以获取元素内容
 print(result[0].text)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

  1. 获取倒数第二个li元素的内容的第二种方式:
from lxml import etree

 html = etree.parse('hello.html')
 result = html.xpath('//li[last()-1]/a/text()')

 print(result)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

4. 使用requests和xpath爬取电影天堂

示例代码如下:

import requests
from lxml import etree

BASE_DOMAIN = 'http://www.dytt8.net'
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
    'Referer': 'http://www.dytt8.net/html/gndy/dyzz/list_23_2.html'
}

def spider():
    url = 'http://www.dytt8.net/html/gndy/dyzz/list_23_1.html'
    resp = requests.get(url,headers=HEADERS)
    # resp.content:经过编码后的字符串
    # resp.text:没有经过编码,也就是unicode字符串
    # text:相当于是网页中的源代码了
    text = resp.content.decode('gbk')
    # tree:经过lxml解析后的一个对象,以后使用这个对象的xpath方法,就可以
    # 提取一些想要的数据了
    tree = etree.HTML(text)
    # xpath/beautifulsou4
    all_a = tree.xpath("//div[@class='co_content8']//a")
    for a in all_a:
        title = a.xpath("text()")[0]
        href = a.xpath("@href")[0]
        if href.startswith('/'):
            detail_url = BASE_DOMAIN + href
            crawl_detail(detail_url)
            break

def crawl_detail(url):
    resp = requests.get(url,headers=HEADERS)
    text = resp.content.decode('gbk')
    tree = etree.HTML(text)
    create_time = tree.xpath("//div[@class='co_content8']/ul/text()")[0].strip()
    imgs = tree.xpath("//div[@id='Zoom']//img/@src")
    # 电影海报
    cover = imgs[0]
    # 电影截图
    screenshoot = imgs[1]
    # 获取span标签下所有的文本
    infos = tree.xpath("//div[@id='Zoom']//text()")
    for index,info in enumerate(infos):
        if info.startswith("◎年  代"):
            year = info.replace("◎年  代","").strip()

        if info.startswith("◎豆瓣评分"):
            douban_rating = info.replace("◎豆瓣评分",'').strip()
            print(douban_rating)

        if info.startswith("◎主  演"):
            # 从当前位置,一直往下面遍历
            actors = [info]
            for x in range(index+1,len(infos)):
                actor = infos[x]
                if actor.startswith("◎"):
                    break
                actors.append(actor.strip())
            print(",".join(actors))


if __name__ == '__main__':
    spider()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.

七、BeautifulSoup4库

和 lxml 一样,Beautiful Soup 也是一个HTML/XML的解析器,主要的功能也是如何解析和提取 HTML/XML 数据。
lxml 只会局部遍历,而Beautiful Soup 是基于HTML DOM(Document Object Model)的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多,所以性能要低于lxml。
BeautifulSoup 用来解析 HTML 比较简单,API非常人性化,支持CSS选择器、Python标准库中的HTML解析器,也支持 lxml 的 XML解析器。
Beautiful Soup 3 目前已经停止开发,推荐现在的项目使用Beautiful Soup 4。

1. 安装和文档:

  1. 安装:pip install bs4
  2. 中文文档: https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html

2.几大解析工具对比:

解析工具

解析速度

使用难度

BeautifulSoup

最慢

最简单

lxml

简单

正则

最快

最难

3. 简单使用:

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

#创建 Beautiful Soup 对象
# 使用lxml来进行解析
soup = BeautifulSoup(html,"lxml")

print(soup.prettify())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

4. 常用的对象:

Beautiful Soup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳为4种:

  1. Tag
  2. NavigatableString
  3. BeautifulSoup
  4. Comment
1. Tag:

Tag 通俗点讲就是 HTML 中的一个个标签。示例代码如下:

from bs4 import BeautifulSoup

html = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title" name="dromouse"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1"><!-- Elsie --></a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""

#创建 Beautiful Soup 对象
soup = BeautifulSoup(html,'lxml')


print soup.title
# <title>The Dormouse's story</title>

print soup.head
# <head><title>The Dormouse's story</title></head>

print soup.a
# <a class="sister" href="http://example.com/elsie" id="link1"><!-- Elsie --></a>

print soup.p
# <p class="title" name="dromouse"><b>The Dormouse's story</b></p>

print type(soup.p)
# <class 'bs4.element.Tag'>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

我们可以利用 soup 加标签名轻松地获取这些标签的内容,这些对象的类型是bs4.element.Tag。但是注意,它查找的是在所有内容中的第一个符合要求的标签。如果要查询所有的标签,后面会进行介绍。
对于Tag,它有两个重要的属性,分别是name和attrs。示例代码如下:

print soup.name
# [document] #soup 对象本身比较特殊,它的 name 即为 [document]

print soup.head.name
# head #对于其他内部标签,输出的值便为标签本身的名称

print soup.p.attrs
# {'class': ['title'], 'name': 'dromouse'}
# 在这里,我们把 p 标签的所有属性打印输出了出来,得到的类型是一个字典。

print soup.p['class'] # soup.p.get('class')
# ['title'] #还可以利用get方法,传入属性的名称,二者是等价的

soup.p['class'] = "newClass"
print soup.p # 可以对这些属性和内容等等进行修改
# <p class="newClass" name="dromouse"><b>The Dormouse's story</b></p>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
2. NavigableString:

如果拿到标签后,还想获取标签中的内容。那么可以通过tag.string获取标签中的文字。示例代码如下:

print soup.p.string
# The Dormouse's story

print type(soup.p.string)
# <class 'bs4.element.NavigableString'>thon
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
3. BeautifulSoup:

BeautifulSoup 对象表示的是一个文档的全部内容.大部分时候,可以把它当作 Tag 对象,它支持 遍历文档树 和 搜索文档树 中描述的大部分的方法.
因为 BeautifulSoup 对象并不是真正的HTML或XML的tag,所以它没有name和attribute属性.但有时查看它的 .name 属性是很方便的,所以 BeautifulSoup 对象包含了一个值为 “[document]” 的特殊属性 .name

soup.name
# '[document]'
  • 1.
  • 2.
4. Comment:

Tag , NavigableString , BeautifulSoup 几乎覆盖了html和xml中的所有内容,但是还有一些特殊对象.容易让人担心的内容是文档的注释部分:

markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Comment 对象是一个特殊类型的 NavigableString 对象:

comment
# 'Hey, buddy. Want to buy a used parser'
  • 1.
  • 2.

5.遍历文档树:

1. contents和children:
html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc,'lxml')

head_tag = soup.head
# 返回所有子节点的列表
print(head_tag.contents)

# 返回所有子节点的迭代器
for child in head_tag.children:
    print(child)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
2. strings 和 stripped_strings

如果tag中包含多个字符串 [2] ,可以使用 .strings 来循环获取:

for string in soup.strings:
    print(repr(string))
    # u"The Dormouse's story"
    # u'\n\n'
    # u"The Dormouse's story"
    # u'\n\n'
    # u'Once upon a time there were three little sisters; and their names were\n'
    # u'Elsie'
    # u',\n'
    # u'Lacie'
    # u' and\n'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'\n\n'
    # u'...'
    # u'\n'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

输出的字符串中可能包含了很多空格或空行,使用 .stripped_strings 可以去除多余空白内容:

for string in soup.stripped_strings:
    print(repr(string))
    # u"The Dormouse's story"
    # u"The Dormouse's story"
    # u'Once upon a time there were three little sisters; and their names were'
    # u'Elsie'
    # u','
    # u'Lacie'
    # u'and'
    # u'Tillie'
    # u';\nand they lived at the bottom of a well.'
    # u'...'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

6. 搜索文档树:

1. find和find_all方法:

搜索文档树,一般用得比较多的就是两个方法,一个是find,一个是find_allfind方法是找到第一个满足条件的标签后就立即返回,只返回一个元素。find_all方法是把所有满足条件的标签都选到,然后返回回去。使用这两个方法,最常用的用法是出入name以及attr参数找出符合要求的标签。

soup.find_all("a",attrs={"id":"link2"})
  • 1.

或者是直接传入属性的的名字作为关键字参数:

soup.find_all("a",id='link2')
  • 1.
2. select方法:

使用以上方法可以方便的找出元素。但有时候使用css选择器的方式可以更加的方便。使用css选择器的语法,应该使用select方法。以下列出几种常用的css选择器方法:

(1)通过标签名查找:
print(soup.select('a'))
  • 1.
(2)通过类名查找:

通过类名,则应该在类的前面加一个.。比如要查找class=sister的标签。示例代码如下:

print(soup.select('.sister'))
  • 1.
(3)通过id查找:

通过id查找,应该在id的名字前面加一个#号。示例代码如下:

print(soup.select("#link1"))
  • 1.
(4)组合查找:

组合查找即和写 class 文件时,标签名与类名、id名进行的组合原理是一样的,例如查找 p 标签中,id 等于 link1的内容,二者需要用空格分开:

print(soup.select("p #link1"))
  • 1.

直接子标签查找,则使用 > 分隔:

print(soup.select("head > title"))
  • 1.
(5)通过属性查找:

查找时还可以加入属性元素,属性需要用中括号括起来,注意属性和标签属于同一节点,所以中间不能加空格,否则会无法匹配到。示例代码如下:

print(soup.select('a[href="http://example.com/elsie"]'))
  • 1.
(6)获取内容

以上的 select 方法返回的结果都是列表形式,可以遍历形式输出,然后用 get_text() 方法来获取它的内容。

soup = BeautifulSoup(html, 'lxml')
print type(soup.select('title'))
print soup.select('title')[0].get_text()

for title in soup.select('title'):
    print title.get_text()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

八、正则表达式和re模块:

1. 什么是正则表达式:

通俗理解:按照一定的规则,从某个字符串中匹配出想要的数据。这个规则就是正则表达式。
标准答案:https://baike.baidu.com/item/正则表达式/1700215?fr=aladdin

2. 正则表达式常用匹配规则:

匹配某个字符串:
text = 'hello'
ret = re.match('he',text)
print(ret.group())
>> he
  • 1.
  • 2.
  • 3.
  • 4.

以上便可以在hello中,匹配出he

点(.)匹配任意的字符:
text = "ab"
ret = re.match('.',text)
print(ret.group())
>> a
  • 1.
  • 2.
  • 3.
  • 4.

但是点(.)不能匹配不到换行符。示例代码如下:

text = "ab"
ret = re.match('.',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute 'group'
  • 1.
  • 2.
  • 3.
  • 4.
\d匹配任意的数字:
text = "123"
ret = re.match('\d',text)
print(ret.group())
>> 1
  • 1.
  • 2.
  • 3.
  • 4.
\D匹配任意的非数字:
text = "a"
ret = re.match('\D',text)
print(ret.group())
>> a
  • 1.
  • 2.
  • 3.
  • 4.

而如果text是等于一个数字,那么就匹配不成功了。示例代码如下:

text = "1"
ret = re.match('\D',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute 'group'
  • 1.
  • 2.
  • 3.
  • 4.
\s匹配的是空白字符(包括:\n,\t,\r和空格):
text = "\t"
ret = re.match('\s',text)
print(ret.group())
>> 空白
  • 1.
  • 2.
  • 3.
  • 4.
\w匹配的是a-zA-Z以及数字和下划线:
text = "_"
ret = re.match('\w',text)
print(ret.group())
>> _
  • 1.
  • 2.
  • 3.
  • 4.

而如果要匹配一个其他的字符,那么就匹配不到。示例代码如下:

text = "+"
ret = re.match('\w',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute
  • 1.
  • 2.
  • 3.
  • 4.
\W匹配的是和\w相反的:
text = "+"
ret = re.match('\W',text)
print(ret.group())
>> +
  • 1.
  • 2.
  • 3.
  • 4.

而如果你的text是一个下划线或者英文字符,那么就匹配不到了。示例代码如下:

text = "_"
ret = re.match('\W',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute
  • 1.
  • 2.
  • 3.
  • 4.
[]组合的方式,只要满足中括号中的某一项都算匹配成功:
text = "0731-88888888"
ret = re.match('[\d\-]+',text)
print(ret.group())
>> 0731-88888888
  • 1.
  • 2.
  • 3.
  • 4.

之前讲到的几种匹配规则,其实可以使用中括号的形式来进行替代:

  • \d:[0-9]
  • \D: 0-9
  • \w:[0-9a-zA-Z_]
  • \W:[^0-9a-zA-Z_]
匹配多个字符:
  1. *:可以匹配0或者任意多个字符。示例代码如下:
text = "0731"
 ret = re.match('\d*',text)
 print(ret.group())
 >> 0731
  • 1.
  • 2.
  • 3.
  • 4.

以上因为匹配的要求是\d,那么就要求是数字,后面跟了一个星号,就可以匹配到0731这四个字符。

  1. +:可以匹配1个或者多个字符。最少一个。示例代码如下:
text = "abc"
 ret = re.match('\w+',text)
 print(ret.group())
 >> abc
  • 1.
  • 2.
  • 3.
  • 4.

因为匹配的是\w,那么就要求是英文字符,后面跟了一个加号,意味着最少要有一个满足\w的字符才能够匹配到。如果text是一个空白字符或者是一个不满足\w的字符,那么就会报错。示例代码如下:

text = ""
 ret = re.match('\w+',text)
 print(ret.group())
 >> AttributeError: 'NoneType' object has no attribute
  • 1.
  • 2.
  • 3.
  • 4.

  1. ?:匹配的字符可以出现一次或者不出现(0或者1)。示例代码如下:
text = "123"
 ret = re.match('\d?',text)
 print(ret.group())
 >> 1
  • 1.
  • 2.
  • 3.
  • 4.

  1. {m}:匹配m个字符。示例代码如下:
text = "123"
 ret = re.match('\d{2}',text)
 print(ret.group())
 >> 12
  • 1.
  • 2.
  • 3.
  • 4.

  1. {m,n}:匹配m-n个字符。在这中间的字符都可以匹配到。示例代码如下:
text = "123"
 ret = re.match('\d{1,2}',text)
 prit(ret.group())
 >> 12
  • 1.
  • 2.
  • 3.
  • 4.

如果text只有一个字符,那么也可以匹配出来。示例代码如下:

text = "1"
 ret = re.match('\d{1,2}',text)
 prit(ret.group())
 >> 1
  • 1.
  • 2.
  • 3.
  • 4.

3.小案例:

  1. 验证手机号码:手机号码的规则是以1开头,第二位可以是34587,后面那9位就可以随意了。示例代码如下:
text = "18570631587"
 ret = re.match('1[34587]\d{9}',text)
 print(ret.group())
 >> 18570631587
  • 1.
  • 2.
  • 3.
  • 4.

而如果是个不满足条件的手机号码。那么就匹配不到了。示例代码如下:

text = "1857063158"
 ret = re.match('1[34587]\d{9}',text)
 print(ret.group())
 >> AttributeError: 'NoneType' object has no attribute
  • 1.
  • 2.
  • 3.
  • 4.

  1. 验证邮箱:邮箱的规则是邮箱名称是用数字、数字、下划线组成的,然后是@符号,后面就是域名了。示例代码如下:
text = "hynever@163.com"
 ret = re.match('\w+@\w+\.[a-zA-Z\.]+',text)
 print(ret.group())
  • 1.
  • 2.
  • 3.

  1. 验证URL:URL的规则是前面是http或者https或者是ftp然后再加上一个冒号,再加上一个斜杠,再后面就是可以出现任意非空白字符了。示例代码如下:
text = "http://www.baidu.com/"
 ret = re.match('(http|https|ftp)://[^\s]+',text)
 print(ret.group())
  • 1.
  • 2.
  • 3.

  1. 验证身份证:身份证的规则是,总共有18位,前面17位都是数字,后面一位可以是数字,也可以是小写的x,也可以是大写的X。示例代码如下:
text = "3113111890812323X"
 ret = re.match('\d{17}[\dxX]',text)
 print(ret.group())
  • 1.
  • 2.
  • 3.

^(脱字号):表示以...开始:
text = "hello"
ret = re.match('^h',text)
print(ret.group())
  • 1.
  • 2.
  • 3.

如果是在中括号中,那么代表的是取反操作.

$:表示以...结束:
# 匹配163.com的邮箱
text = "xxx@163.com"
ret = re.search('\w+@163\.com$',text)
print(ret.group())
>> xxx@163.com
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
|:匹配多个表达式或者字符串:
text = "hello|world"
ret = re.search('hello',text)
print(ret.group())
>> hello
  • 1.
  • 2.
  • 3.
  • 4.
贪婪模式和非贪婪模式:

贪婪模式:正则表达式会匹配尽量多的字符。默认是贪婪模式。
非贪婪模式:正则表达式会尽量少的匹配字符。
示例代码如下:

text = "0123456"
ret = re.match('\d+',text)
print(ret.group())
# 因为默认采用贪婪模式,所以会输出0123456
>> 0123456
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

可以改成非贪婪模式,那么就只会匹配到0。示例代码如下:

text = "0123456"
ret = re.match('\d+?',text)
print(ret.group())
  • 1.
  • 2.
  • 3.
案例:匹配0-100之间的数字:
text = '99'
ret = re.match('[1-9]?\d$|100$',text)
print(ret.group())
>> 99
  • 1.
  • 2.
  • 3.
  • 4.

而如果text=101,那么就会抛出一个异常。示例代码如下:

text = '101'
ret = re.match('[1-9]?\d$|100$',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute 'group'
  • 1.
  • 2.
  • 3.
  • 4.
转义字符和原生字符串:

在正则表达式中,有些字符是有特殊意义的字符。因此如果想要匹配这些字符,那么就必须使用反斜杠进行转义。比如$代表的是以...结尾,如果想要匹配$,那么就必须使用\$。示例代码如下:

text = "apple price is \$99,orange paice is $88"
ret = re.search('\$(\d+)',text)
print(ret.group())
>> $99
  • 1.
  • 2.
  • 3.
  • 4.

原生字符串:
在正则表达式中,\是专门用来做转义的。在Python中\也是用来做转义的。因此如果想要在普通的字符串中匹配出\,那么要给出四个\。示例代码如下:

text = "apple \c"
ret = re.search('\\\\c',text)
print(ret.group())
  • 1.
  • 2.
  • 3.

因此要使用原生字符串就可以解决这个问题:

text = "apple \c"
ret = re.search(r'\\c',text)
print(ret.group())
  • 1.
  • 2.
  • 3.

九、re模块中常用函数:

1. match:

从开始的位置进行匹配。如果开始的位置没有匹配到。就直接失败了。示例代码如下:

text = 'hello'
ret = re.match('h',text)
print(ret.group())
>> h
  • 1.
  • 2.
  • 3.
  • 4.

如果第一个字母不是h,那么就会失败。示例代码如下:

text = 'ahello'
ret = re.match('h',text)
print(ret.group())
>> AttributeError: 'NoneType' object has no attribute 'group'
  • 1.
  • 2.
  • 3.
  • 4.

如果想要匹配换行的数据,那么就要传入一个flag=re.DOTALL,就可以匹配换行符了。示例代码如下:

text = "abc\nabc"
ret = re.match('abc.*abc',text,re.DOTALL)
print(ret.group())
  • 1.
  • 2.
  • 3.

2. search:

在字符串中找满足条件的字符。如果找到,就返回。说白了,就是只会找到第一个满足条件的。

text = 'apple price $99 orange price $88'
ret = re.search('\d+',text)
print(ret.group())
>> 99
  • 1.
  • 2.
  • 3.
  • 4.

3. 分组:

在正则表达式中,可以对过滤到的字符串进行分组。分组使用圆括号的方式。

  1. group:和group(0)是等价的,返回的是整个满足条件的字符串。
  2. groups:返回的是里面的子组。索引从1开始。
  3. group(1):返回的是第一个子组,可以传入多个。
    示例代码如下:
text = "apple price is $99,orange price is $10"
ret = re.search(r".*(\$\d+).*(\$\d+)",text)
print(ret.group())
print(ret.group(0))
print(ret.group(1))
print(ret.group(2))
print(ret.groups())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

4. findall:

找出所有满足条件的,返回的是一个列表。

text = 'apple price $99 orange price $88'
ret = re.findall('\d+',text)
print(ret)
>> ['99', '88']
  • 1.
  • 2.
  • 3.
  • 4.

5. sub:

用来替换字符串。将匹配到的字符串替换为其他字符串。

text = 'apple price $99 orange price $88'
ret = re.sub('\d+','0',text)
print(ret)
>> apple price $0 orange price $0
  • 1.
  • 2.
  • 3.
  • 4.

sub函数的案例,获取拉勾网中的数据:

html = """
<div>
<p>基本要求:</p>
<p>1、精通HTML5、CSS3、 JavaScript等Web前端开发技术,对html5页面适配充分了解,熟悉不同浏览器间的差异,熟练写出兼容各种浏览器的代码;</p>
<p>2、熟悉运用常见JS开发框架,如JQuery、vue、angular,能快速高效实现各种交互效果;</p>
<p>3、熟悉编写能够自动适应HTML5界面,能让网页格式自动适应各款各大小的手机;</p>
<p>4、利用HTML5相关技术开发移动平台、PC终端的前端页面,实现HTML5模板化;</p>
<p>5、熟悉手机端和PC端web实现的差异,有移动平台web前端开发经验,了解移动互联网产品和行业,有在Android,iOS等平台下HTML5+CSS+JavaScript(或移动JS框架)开发经验者优先考虑;6、良好的沟通能力和团队协作精神,对移动互联网行业有浓厚兴趣,有较强的研究能力和学习能力;</p>
<p>7、能够承担公司前端培训工作,对公司各业务线的前端(HTML5\CSS3)工作进行支撑和指导。</p>
<p><br></p>
<p>岗位职责:</p>
<p>1、利用html5及相关技术开发移动平台、微信、APP等前端页面,各类交互的实现;</p>
<p>2、持续的优化前端体验和页面响应速度,并保证兼容性和执行效率;</p>
<p>3、根据产品需求,分析并给出最优的页面前端结构解决方案;</p>
<p>4、协助后台及客户端开发人员完成功能开发和调试;</p>
<p>5、移动端主流浏览器的适配、移动端界面自适应研发。</p>
</div>
"""

ret = re.sub('</?[a-zA-Z0-9]+>',"",html)
print(ret)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

6. split:

使用正则表达式来分割字符串。

text = "hello world ni hao"
ret = re.split('\W',text)
print(ret)
>> ["hello","world","ni","hao"]
  • 1.
  • 2.
  • 3.
  • 4.

7. compile:

对于一些经常要用到的正则表达式,可以使用compile进行编译,后期再使用的时候可以直接拿过来用,执行效率会更快。而且compile还可以指定flag=re.VERBOSE,在写正则表达式的时候可以做好注释。示例代码如下:

text = "the number is 20.50"
r = re.compile(r"""
                \d+ # 小数点前面的数字
                \.? # 小数点
                \d* # 小数点后面的数字
                """,re.VERBOSE)
ret = re.search(r,text)
print(ret.group())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

十、json文件处理:

1. 什么是json:

JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。更多解释请见: https://baike.baidu.com/item/JSON/2462549?fr=aladdin

2. JSON支持数据格式:

  1. 对象(字典)。使用花括号。
  2. 数组(列表)。使用方括号。
  3. 整形、浮点型、布尔类型还有null类型。
  4. 字符串类型(字符串必须要用双引号,不能用单引号)。

多个数据之间使用逗号分开。
注意:json本质上就是一个字符串。

3. 字典和列表转JSON:

import json

books = [
    {
        'title': '钢铁是怎样练成的',
        'price': 9.8
    },
    {
        'title': '红楼梦',
        'price': 9.9
    }
]

json_str = json.dumps(books,ensure_ascii=False)
print(json_str)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

因为jsondump的时候,只能存放ascii的字符,因此会将中文进行转义,这时候我们可以使用ensure_ascii=False关闭这个特性。
Python中。只有基本数据类型才能转换成JSON格式的字符串。也即:intfloatstrlistdicttuple

4. 将json数据直接dump到文件中:

json模块中除了dumps函数,还有一个dump函数,这个函数可以传入一个文件指针,直接将字符串dump到文件中。示例代码如下:

books = [
    {
        'title': '钢铁是怎样练成的',
        'price': 9.8
    },
    {
        'title': '红楼梦',
        'price': 9.9
    }
]
with open('a.json','w') as fp:
    json.dump(books,fp)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

5. 将一个json字符串load成Python对象:

json_str = '[{"title": "钢铁是怎样练成的", "price": 9.8}, {"title": "红楼梦", "price": 9.9}]'
books = json.loads(json_str,encoding='utf-8')
print(type(books))
print(books)
  • 1.
  • 2.
  • 3.
  • 4.

6. 直接从文件中读取json:

import json
with open('a.json','r',encoding='utf-8') as fp:
    json_str = json.load(fp)
    print(json_str)
  • 1.
  • 2.
  • 3.
  • 4.

十一、csv文件处理

1. 读取csv文件:

import csv

with open('stock.csv','r') as fp:
    reader = csv.reader(fp)
    titles = next(reader)
    for x in reader:
        print(x)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样操作,以后获取数据的时候,就要通过下表来获取数据。如果想要在获取数据的时候通过标题来获取。那么可以使用DictReader。示例代码如下:

import csv

with open('stock.csv','r') as fp:
    reader = csv.DictReader(fp)
    for x in reader:
        print(x['turnoverVol'])
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

2. 写入数据到csv文件:

写入数据到csv文件,需要创建一个writer对象,主要用到两个方法。一个是writerow,这个是写入一行。一个是writerows,这个是写入多行。示例代码如下:

import csv

headers = ['name','age','classroom']
values = [
    ('zhiliao',18,'111'),
    ('wena',20,'222'),
    ('bbc',21,'111')
]
with open('test.csv','w',newline='') as fp:
    writer = csv.writer(fp)
    writer.writerow(headers)
    writer.writerows(values)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

也可以使用字典的方式把数据写入进去。这时候就需要使用DictWriter了。示例代码如下:

import csv

headers = ['name','age','classroom']
values = [
    {"name":'wenn',"age":20,"classroom":'222'},
    {"name":'abc',"age":30,"classroom":'333'}
]
with open('test.csv','w',newline='') as fp:
    writer = csv.DictWriter(fp,headers)
    writer = csv.writeheader()
    writer.writerow({'name':'zhiliao',"age":18,"classroom":'111'})
    writer.writerows(values)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

十二、MySQL数据库操作

1. 安装mysql:

  1. 在官网: https://dev.mysql.com/downloads/windows/installer/5.7.html
  2. 如果提示没有.NET Framework框架。那么就在提示框中找到下载链接,下载一个就可以了。
  3. 如果提示没有Microsoft Virtual C++ x64(x86),那么百度或者谷歌这个软件安装即可。
  4. 如果没有找到。那么私聊我。

2. navicat:

navicat是一个操作mysql数据库非常方便的软件。使用他操作数据库,就跟使用excel操作数据是一样的。

4. 安装驱动程序:

Python要想操作MySQL。必须要有一个中间件,或者叫做驱动程序。驱动程序有很多。比如有mysqldbmysqlclientpymysql等。在这里,我们选择用pymysql。安装方式也是非常简单,通过命令pip install pymysql即可安装。

5. 数据库连接:

数据库连接之前。首先先确认以下工作完成,这里我们以一个pymysql_test数据库.以下将介绍连接mysql的示例代码:

import pymysql

    db = pymysql.connect(
        host="127.0.0.1",
        user='root',
        password='root',
        database='pymysql_test',
        port=3306
    )
    cursor = db.cursor()
    cursor.execute("select 1")
    data = cursor.fetchone()
    print(data)
    db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

6. 插入数据:

import pymysql

db = pymysql.connect(
    host="127.0.0.1",
    user='root',
    password='root',
    database='pymysql_test',
    port=3306
)
cursor = db.cursor()
sql = """
insert into user(
    id,username,gender,age,password
  ) 
  values(null,'abc',1,18,'111111');
"""
cursor.execute(sql)
db.commit()
db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

如果在数据还不能保证的情况下,可以使用以下方式来插入数据:

sql = """
insert into user(
    id,username,gender,age,password
  ) 
  values(null,%s,%s,%s,%s);
"""

cursor.execute(sql,('spider',1,20,'222222'))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

7. 查找数据:

使用pymysql查询数据。可以使用fetch*方法。

  1. fetchone():这个方法每次之获取一条数据。
  2. fetchall():这个方法接收全部的返回结果。
  3. fetchmany(size):可以获取指定条数的数据。
    示例代码如下:
cursor = db.cursor()

sql = """
select * from user
"""

cursor.execute(sql)
while True:
    result = cursor.fetchone()
    if not result:
        break
    print(result)
db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

或者是直接使用fetchall,一次性可以把所有满足条件的数据都取出来:

cursor = db.cursor()

sql = """
select * from user
"""

cursor.execute(sql)
results = cursor.fetchall()
for result in results:
    print(result)
db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

或者是使用fetchmany,指定获取多少条数据:

cursor = db.cursor()

sql = """
select * from user
"""

cursor.execute(sql)
results = cursor.fetchmany(1)
for result in results:
    print(result)
db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

8. 删除数据:

cursor = db.cursor()

sql = """
delete from user where id=1
"""

cursor.execute(sql)
db.commit()
db.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

9. 更新数据:

conn = pymysql.connect(host='localhost',user='root',password='root',database='pymysql_demo',port=3306)
cursor = conn.cursor()

sql = """
update user set username='aaa' where id=1
"""
cursor.execute(sql)
conn.commit()

conn.close()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

十三、多线程爬虫

有些时候,比如下载图片,因为下载图片是一个耗时的操作。如果采用之前那种同步的方式下载。那效率肯会特别慢。这时候我们就可以考虑使用多线程的方式来下载图片。

1. 多线程介绍:

多线程是为了同步完成多项任务,通过提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也可以有多节车厢。多线程的出现就是为了提高效率。同时它的出现也带来了一些问题。更多介绍请参考:https://baike.baidu.com/item/多线程/1190404?fr=aladdin

2.threading模块介绍:

threading模块是python中专门提供用来做多线程编程的模块。threading模块中最常用的类是Thread。以下看一个简单的多线程程序:

import threading
import time

def coding():
    for x in range(3):
        print('%s正在写代码' % x)
        time.sleep(1)

def drawing():
    for x in range(3):
        print('%s正在画图' % x)
        time.sleep(1)


def single_thread():
    coding()
    drawing()

def multi_thread():
    t1 = threading.Thread(target=coding)
    t2 = threading.Thread(target=drawing)

    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

3.查看线程数:

使用threading.enumerate()函数便可以看到当前线程的数量。

4.查看当前线程的名字:

使用threading.current_thread()可以看到当前线程的信息。

5.继承自threading.Thread类:

为了让线程代码更好的封装。可以使用threading模块下的Thread类,继承自这个类,然后实现run方法,线程就会自动运行run方法中的代码。示例代码如下:

import threading
import time

class CodingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print('%s正在写代码' % threading.current_thread())
            time.sleep(1)

class DrawingThread(threading.Thread):
    def run(self):
        for x in range(3):
            print('%s正在画图' % threading.current_thread())
            time.sleep(1)

def multi_thread():
    t1 = CodingThread()
    t2 = DrawingThread()

    t1.start()
    t2.start()

if __name__ == '__main__':
    multi_thread()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

6. 多线程共享全局变量的问题:

多线程都是在同一个进程中运行的。因此在进程中的全局变量所有线程都是可共享的。这就造成了一个问题,因为线程执行的顺序是无序的。有可能会造成数据错误。比如以下代码:

import threading

tickets = 0

def get_ticket():
    global tickets
    for x in range(1000000):
        tickets += 1
    print('tickets:%d'%tickets)

def main():
    for x in range(2):
        t = threading.Thread(target=get_ticket)
        t.start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

以上结果正常来讲应该是6,但是因为多线程运行的不确定性。因此最后的结果可能是随机的。

7. 锁机制:

为了解决以上使用共享全局变量的问题。threading提供了一个Lock类,这个类可以在某个线程访问某个变量的时候加锁,其他线程此时就不能进来,直到当前线程处理完后,把锁释放了,其他线程才能进来处理。示例代码如下:

import threading

VALUE = 0

gLock = threading.Lock()

def add_value():
    global VALUE
    gLock.acquire()
    for x in range(1000000):
        VALUE += 1
    gLock.release()
    print('value:%d'%VALUE)

def main():
    for x in range(2):
        t = threading.Thread(target=add_value)
        t.start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  1. Lock版本生产者和消费者模式:

生产者和消费者模式是多线程开发中经常见到的一种模式。生产者的线程专门用来生产一些数据,然后存放到一个中间的变量中。消费者再从这个中间的变量中取出数据进行消费。但是因为要使用中间变量,中间变量经常是一些全局变量,因此需要使用锁来保证数据完整性。以下是使用threading.Lock锁实现的“生产者与消费者模式”的一个例子:

import threading
import random
import time

gMoney = 1000
gLock = threading.Lock()
# 记录生产者生产的次数,达到10次就不再生产
gTimes = 0

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gLock
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gLock.acquire()
            # 如果已经达到10次了,就不再生产了
            if gTimes >= 10:
                gLock.release()
                break
            gMoney += money
            print('%s当前存入%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gLock.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gLock
        global gTimes
        while True:
            money = random.randint(100, 500)
            gLock.acquire()
            if gMoney > money:
                gMoney -= money
                print('%s当前取出%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
                time.sleep(0.5)
            else:
                # 如果钱不够了,有可能是已经超过了次数,这时候就判断一下
                if gTimes >= 10:
                    gLock.release()
                    break
                print("%s当前想取%s元钱,剩余%s元钱,不足!" % (threading.current_thread(),money,gMoney))
            gLock.release()

def main():
    for x in range(5):
        Consumer(name='消费者线程%d'%x).start()

    for x in range(5):
        Producer(name='生产者线程%d'%x).start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  1. Condition版的生产者与消费者模式:

Lock版本的生产者与消费者模式可以正常的运行。但是存在一个不足,在消费者中,总是通过while True死循环并且上锁的方式去判断钱够不够。上锁是一个很耗费CPU资源的行为。因此这种方式不是最好的。还有一种更好的方式便是使用threading.Condition来实现。threading.Condition可以在没有数据的时候处于阻塞等待状态。一旦有合适的数据了,还可以使用notify相关的函数来通知其他处于等待状态的线程。这样就可以不用做一些无用的上锁和解锁的操作。可以提高程序的性能。首先对threading.Condition相关的函数做个介绍,threading.Condition类似threading.Lock,可以在修改全局数据的时候进行上锁,也可以在修改完毕后进行解锁。以下将一些常用的函数做个简单的介绍:

  1. acquire:上锁。
  2. release:解锁。
  3. wait:将当前线程处于等待状态,并且会释放锁。可以被其他线程使用notifynotify_all函数唤醒。被唤醒后会继续等待上锁,上锁后继续执行下面的代码。
  4. notify:通知某个正在等待的线程,默认是第1个等待的线程。
  5. notify_all:通知所有正在等待的线程。notifynotify_all不会释放锁。并且需要在release之前调用。

Condition版的生产者与消费者模式代码如下:

import threading
import random
import time

gMoney = 1000
gCondition = threading.Condition()
gTimes = 0
gTotalTimes = 5

class Producer(threading.Thread):
    def run(self):
        global gMoney
        global gCondition
        global gTimes
        while True:
            money = random.randint(100, 1000)
            gCondition.acquire()
            if gTimes >= gTotalTimes:
                gCondition.release()
                print('当前生产者总共生产了%s次'%gTimes)
                break
            gMoney += money
            print('%s当前存入%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
            gTimes += 1
            time.sleep(0.5)
            gCondition.notify_all()
            gCondition.release()

class Consumer(threading.Thread):
    def run(self):
        global gMoney
        global gCondition
        while True:
            money = random.randint(100, 500)
            gCondition.acquire()
            # 这里要给个while循环判断,因为等轮到这个线程的时候
            # 条件有可能又不满足了
            while gMoney < money:
                if gTimes >= gTotalTimes:
                    gCondition.release()
                    return
                print('%s准备取%s元钱,剩余%s元钱,不足!'%(threading.current_thread(),money,gMoney))
                gCondition.wait()
            gMoney -= money
            print('%s当前取出%s元钱,剩余%s元钱' % (threading.current_thread(), money, gMoney))
            time.sleep(0.5)
            gCondition.release()

def main():
    for x in range(5):
        Consumer(name='消费者线程%d'%x).start()

    for x in range(2):
        Producer(name='生产者线程%d'%x).start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

10. Queue线程安全队列:

在线程中,访问一些全局变量,加锁是一个经常的过程。如果你是想把一些数据存储到某个队列中,那么Python内置了一个线程安全的模块叫做queue模块。Python中的queue模块中提供了同步的、线程安全的队列类,包括FIFO(先进先出)队列Queue,LIFO(后入先出)队列LifoQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么都做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。相关的函数如下:

  1. 初始化Queue(maxsize):创建一个先进先出的队列。
  2. qsize():返回队列的大小。
  3. empty():判断队列是否为空。
  4. full():判断队列是否满了。
  5. get():从队列中取最后一个数据。
  6. put():将一个数据放到队列中。

11. 使用生产者与消费者模式多线程下载表情包:

import threading
import requests
from lxml import etree
from urllib import request
import os
import re
from queue import Queue

class Producer(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(Producer, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue


    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            self.parse_page(url)

    def parse_page(self,url):
        response = requests.get(url,headers=self.headers)
        text = response.text
        html = etree.HTML(text)
        imgs = html.xpath("//div[@class='page-content text-center']//a//img")
        for img in imgs:
            if img.get('class') == 'gif':
                continue
            img_url = img.xpath(".//@data-original")[0]
            suffix = os.path.splitext(img_url)[1]
            alt = img.xpath(".//@alt")[0]
            alt = re.sub(r'[,。??,/\\·]','',alt)
            img_name = alt + suffix
            self.img_queue.put((img_url,img_name))

class Consumer(threading.Thread):
    def __init__(self,page_queue,img_queue,*args,**kwargs):
        super(Consumer, self).__init__(*args,**kwargs)
        self.page_queue = page_queue
        self.img_queue = img_queue

    def run(self):
        while True:
            if self.img_queue.empty():
                if self.page_queue.empty():
                    return
            img = self.img_queue.get(block=True)
            url,filename = img
            request.urlretrieve(url,'images/'+filename)
            print(filename+'  下载完成!')

def main():
    page_queue = Queue(100)
    img_queue = Queue(500)
    for x in range(1,101):
        url = "http://www.doutula.com/photo/list/?page=%d" % x
        page_queue.put(url)

    for x in range(5):
        t = Producer(page_queue,img_queue)
        t.start()

    for x in range(5):
        t = Consumer(page_queue,img_queue)
        t.start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.

12. GIL全局解释器锁:

Python自带的解释器是CPythonCPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。因为CPython解释器的内存管理不是线程安全的。当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,见下面:

  1. Jython:用Java实现的Python解释器。不存在GIL锁。更多详情请见: https://zh.wikipedia.org/wiki/Jython
  2. IronPython:用.net实现的Python解释器。不存在GIL锁。更多详情请见: https://zh.wikipedia.org/wiki/IronPython
  3. PyPy:用Python实现的Python解释器。存在GIL锁。更多详情请见: https://zh.wikipedia.org/wiki/PyPyGIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程。

13. 多线程下载百思不得姐段子作业:

import requests
from lxml import etree
import threading
from queue import Queue
import csv


class BSSpider(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }
    def __init__(self,page_queue,joke_queue,*args,**kwargs):
        super(BSSpider, self).__init__(*args,**kwargs)
        self.base_domain = 'http://www.budejie.com'
        self.page_queue = page_queue
        self.joke_queue = joke_queue

    def run(self):
        while True:
            if self.page_queue.empty():
                break
            url = self.page_queue.get()
            response = requests.get(url, headers=self.headers)
            text = response.text
            html = etree.HTML(text)
            descs = html.xpath("//div[@class='j-r-list-c-desc']")
            for desc in descs:
                jokes = desc.xpath(".//text()")
                joke = "\n".join(jokes).strip()
                link = self.base_domain+desc.xpath(".//a/@href")[0]
                self.joke_queue.put((joke,link))
            print('='*30+"第%s页下载完成!"%url.split('/')[-1]+"="*30)

class BSWriter(threading.Thread):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'
    }

    def __init__(self, joke_queue, writer,gLock, *args, **kwargs):
        super(BSWriter, self).__init__(*args, **kwargs)
        self.joke_queue = joke_queue
        self.writer = writer
        self.lock = gLock

    def run(self):
        while True:
            try:
                joke_info = self.joke_queue.get(timeout=40)
                joke,link = joke_info
                self.lock.acquire()
                self.writer.writerow((joke,link))
                self.lock.release()
                print('保存一条')
            except:
                break

def main():
    page_queue = Queue(10)
    joke_queue = Queue(500)
    gLock = threading.Lock()
    fp = open('bsbdj.csv', 'a',newline='', encoding='utf-8')
    writer = csv.writer(fp)
    writer.writerow(('content', 'link'))

    for x in range(1,11):
        url = 'http://www.budejie.com/text/%d' % x
        page_queue.put(url)

    for x in range(5):
        t = BSSpider(page_queue,joke_queue)
        t.start()

    for x in range(5):
        t = BSWriter(joke_queue,writer,gLock)
        t.start()

if __name__ == '__main__':
    main()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.

十四、动态网页数据抓取

1. 什么是AJAX:

AJAX(Asynchronouse JavaScript And XML)异步JavaScript和XML。过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用Ajax)如果需要更新内容,必须重载整个网页页面。因为传统的在传输数据格式方面,使用的是XML语法。因此叫做AJAX,其实现在数据交互基本上都是使用JSON。使用AJAX加载的数据,即使使用了JS,将数据渲染到了浏览器中,在右键->查看网页源代码还是不能看到通过ajax加载的数据,只能看到使用这个url加载的html代码。

2. 获取ajax数据的方式:

  1. 直接分析ajax调用的接口。然后通过代码请求这个接口。
  2. 使用Selenium+chromedriver模拟浏览器行为获取数据。

方式

优点

缺点

分析接口

直接可以请求到数据。不需要做一些解析工作。代码量少,性能高。

分析接口比较复杂,特别是一些通过js混淆的接口,要有一定的js功底。容易被发现是爬虫。

selenium

直接模拟浏览器的行为。浏览器能请求到的,使用selenium也能请求到。爬虫更稳定。

代码量多。性能低。

3.Selenium+chromedriver获取动态数据:

Selenium相当于是一个机器人。可以模拟人类在浏览器上的一些行为,自动处理浏览器上的一些行为,比如点击,填充数据,删除cookie等。chromedriver是一个驱动Chrome浏览器的驱动程序,使用他才可以驱动浏览器。当然针对不同的浏览器有不同的driver。以下列出了不同浏览器及其对应的driver:

  1. Chrome: https://sites.google.com/a/chromium.org/chromedriver/downloads
  2. Firefox: https://github.com/mozilla/geckodriver/releases
  3. Edge: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
  4. Safari: https://webkit.org/blog/6900/webdriver-support-in-safari-10/

4. 安装Selenium和chromedriver:

  1. 安装
Selenium
  • 1.

Selenium
  • 1.

有很多语言的版本,有java、ruby、python等。我们下载python版本的就可以了。

pip install selenium
  • 1.

  1. 安装chromedriver:下载完成后,放到不需要权限的纯英文目录下就可以了。

5. 快速入门:

现在以一个简单的获取百度首页的例子来讲下Seleniumchromedriver如何快速入门:

from selenium import webdriver

# chromedriver的绝对路径
driver_path = r'D:\ProgramApp\chromedriver\chromedriver.exe'

# 初始化一个driver,并且指定chromedriver的路径
driver = webdriver.Chrome(executable_path=driver_path)
# 请求网页
driver.get("https://www.baidu.com/")
# 通过page_source获取网页源代码
print(driver.page_source)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

6. selenium常用操作:

更多教程请参考: http://selenium-python.readthedocs.io/installation.html#introduction

关闭页面:
  1. driver.close():关闭当前页面。
  2. driver.quit():退出整个浏览器。
定位元素:
find_element_by_id
  • 1.

:根据id来查找某个元素。等价于:

submitTag = driver.find_element_by_id('su')
 submitTag1 = driver.find_element(By.ID,'su')
  • 1.
  • 2.

find_element_by_class_name
  • 1.

:根据类名查找元素。 等价于:

submitTag = driver.find_element_by_class_name('su')
 submitTag1 = driver.find_element(By.CLASS_NAME,'su')
  • 1.
  • 2.

find_element_by_name
  • 1.

:根据name属性的值来查找元素。等价于:

submitTag = driver.find_element_by_name('email')
 submitTag1 = driver.find_element(By.NAME,'email')
  • 1.
  • 2.

find_element_by_tag_name
  • 1.

:根据标签名来查找元素。等价于:

submitTag = driver.find_element_by_tag_name('div')
 submitTag1 = driver.find_element(By.TAG_NAME,'div')
  • 1.
  • 2.

find_element_by_xpath
  • 1.

:根据xpath语法来获取元素。等价于:

submitTag = driver.find_element_by_xpath('//div')
 submitTag1 = driver.find_element(By.XPATH,'//div')
  • 1.
  • 2.

  1. find_element_by_css_selector:根据css选择器选择元素。等价于:
submitTag = driver.find_element_by_css_selector('//div')
 submitTag1 = driver.find_element(By.CSS_SELECTOR,'//div')
  • 1.
  • 2.

要注意,find_element是获取第一个满足条件的元素。find_elements是获取所有满足条件的元素。

操作表单元素:
  1. 操作输入框:分为两步。第一步:找到这个元素。第二步:使用send_keys(value),将数据填充进去。示例代码如下:
inputTag = driver.find_element_by_id('kw')
 inputTag.send_keys('python')
  • 1.
  • 2.

使用clear方法可以清除输入框中的内容。示例代码如下:

inputTag.clear()
  • 1.

  1. 操作checkbox:因为要选中checkbox标签,在网页中是通过鼠标点击的。因此想要选中checkbox标签,那么先选中这个标签,然后执行click事件。示例代码如下:
rememberTag = driver.find_element_by_name("rememberMe")
 rememberTag.click()
  • 1.
  • 2.

  1. 选择select:select元素不能直接点击。因为点击后还需要选中元素。这时候selenium就专门为select标签提供了一个类selenium.webdriver.support.ui.Select。将获取到的元素当成参数传到这个类中,创建这个对象。以后就可以使用这个对象进行选择了。示例代码如下:
from selenium.webdriver.support.ui import Select
 # 选中这个标签,然后使用Select创建对象
 selectTag = Select(driver.find_element_by_name("jumpMenu"))
 # 根据索引选择
 selectTag.select_by_index(1)
 # 根据值选择
 selectTag.select_by_value("http://www.95yueba.com")
 # 根据可视的文本选择
 selectTag.select_by_visible_text("95秀客户端")
 # 取消选中所有选项
 selectTag.deselect_all()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

  1. 操作按钮:操作按钮有很多种方式。比如单击、右击、双击等。这里讲一个最常用的。就是点击。直接调用click函数就可以了。示例代码如下:
inputTag = driver.find_element_by_id('su')
 inputTag.click()
  • 1.
  • 2.

行为链:

有时候在页面中的操作可能要有很多步,那么这时候可以使用鼠标行为链类ActionChains来完成。比如现在要将鼠标移动到某个元素上并执行点击事件。那么示例代码如下:

inputTag = driver.find_element_by_id('kw')
submitTag = driver.find_element_by_id('su')

actions = ActionChains(driver)
actions.move_to_element(inputTag)
actions.send_keys_to_element(inputTag,'python')
actions.move_to_element(submitTag)
actions.click(submitTag)
actions.perform()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

还有更多的鼠标相关的操作。

Cookie操作:
  1. 获取所有的
cookie
  • 1.

for cookie in driver.get_cookies():
     print(cookie)
  • 1.
  • 2.

  1. 根据cookie的key获取value:
value = driver.get_cookie(key)
  • 1.

  1. 删除所有的cookie:
driver.delete_all_cookies()
  • 1.

  1. 删除某个
cookie
  • 1.

driver.delete_cookie(key)
  • 1.

页面等待:

现在的网页越来越多采用了 Ajax 技术,这样程序便不能确定何时某个元素完全加载出来了。如果实际页面等待时间过长导致某个dom元素还没出来,但是你的代码直接使用了这个WebElement,那么就会抛出NullPointer的异常。为了解决这个问题。所以 Selenium 提供了两种等待方式:一种是隐式等待、一种是显式等待。

  1. 隐式等待:调用driver.implicitly_wait。那么在获取不可用的元素之前,会先等待10秒中的时间。示例代码如下:
driver = webdriver.Chrome(executable_path=driver_path)
driver.implicitly_wait(10)
# 请求网页
driver.get("https://www.douban.com/")
  • 1.
  • 2.
  • 3.
  • 4.

  1. 显示等待:显示等待是表明某个条件成立后才执行获取元素的操作。也可以在等待的时候指定一个最大的时间,如果超过这个时间那么就抛出一个异常。显示等待应该使用selenium.webdriver.support.excepted_conditions期望的条件和selenium.webdriver.support.ui.WebDriverWait来配合完成。示例代码如下:
from selenium import webdriver
 from selenium.webdriver.common.by import By
 from selenium.webdriver.support.ui import WebDriverWait
 from selenium.webdriver.support import expected_conditions as EC

 driver = webdriver.Firefox()
 driver.get("http://somedomain/url_that_delays_loading")
 try:
     element = WebDriverWait(driver, 10).until(
         EC.presence_of_element_located((By.ID, "myDynamicElement"))
     )
 finally:
     driver.quit()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

  1. 一些其他的等待条件:
  • presence_of_element_located:某个元素已经加载完毕了。
  • presence_of_all_emement_located:网页中所有满足条件的元素都加载完毕了。
  • element_to_be_cliable:某个元素是可以点击了。
    更多条件请参考: http://selenium-python.readthedocs.io/waits.html
切换页面:

有时候窗口中有很多子tab页面。这时候肯定是需要进行切换的。selenium提供了一个叫做switch_to_window来进行切换,具体切换到哪个页面,可以从driver.window_handles中找到。示例代码如下:

# 打开一个新的页面
self.driver.execute_script("window.open('"+url+"')")
# 切换到这个新的页面中
self.driver.switch_to_window(self.driver.window_handles[1])
  • 1.
  • 2.
  • 3.
  • 4.
设置代理ip:

有时候频繁爬取一些网页。服务器发现你是爬虫后会封掉你的ip地址。这时候我们可以更改代理ip。更改代理ip,不同的浏览器有不同的实现方式。这里以Chrome浏览器为例来讲解:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--proxy-server=http://110.73.2.248:8123")
driver_path = r"D:\ProgramApp\chromedriver\chromedriver.exe"
driver = webdriver.Chrome(executable_path=driver_path,chrome_options=options)

driver.get('http://httpbin.org/ip')
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
WebElement元素:

from selenium.webdriver.remote.webelement import WebElement类是每个获取出来的元素的所属类。
有一些常用的属性:

  1. get_attribute:这个标签的某个属性的值。
  2. screentshot:获取当前页面的截图。这个方法只能在driver上使用。
    driver的对象类,也是继承自WebElement
    更多请阅读相关源代码。

十五、图形验证码识别技术:

阻碍我们爬虫的。有时候正是在登录或者请求一些数据时候的图形验证码。因此这里我们讲解一种能将图片翻译成文字的技术。将图片翻译成文字一般被成为光学文字识别(Optical Character Recognition),简写为OCR。实现OCR的库不是很多,特别是开源的。因为这块存在一定的技术壁垒(需要大量的数据、算法、机器学习、深度学习知识等),并且如果做好了具有很高的商业价值。因此开源的比较少。这里介绍一个比较优秀的图像识别开源库:Tesseract。

1. Tesseract:

Tesseract是一个OCR库,目前由谷歌赞助。Tesseract是目前公认最优秀、最准确的开源OCR库。Tesseract具有很高的识别度,也具有很高的灵活性,他可以通过训练识别任何字体。

2. 安装:

Windows系统:

在以下链接下载可执行文件,然后一顿点击下一步安装即可(放在不需要权限的纯英文路径下):
 https://github.com/tesseract-ocr/

Linux系统:

可以在以下链接下载源码自行编译。
 https://github.com/tesseract-ocr/tesseract/wiki/Compiling或者在ubuntu下通过以下命令进行安装:

sudo apt install tesseract-ocr
  • 1.
Mac系统:

Homebrew即可方便安装:

brew install tesseract
  • 1.
设置环境变量:

安装完成后,如果想要在命令行中使用Tesseract,那么应该设置环境变量。MacLinux在安装的时候就默认已经设置好了。在Windows下把tesseract.exe所在的路径添加到PATH环境变量中。

还有一个环境变量需要设置的是,要把训练的数据文件路径也放到环境变量中。
在环境变量中,添加一个TESSDATA_PREFIX=C:\path_to_tesseractdata\teseractdata

3. 在命令行中使用tesseract识别图像:

如果想要在cmd下能够使用tesseract命令,那么需要把tesseract.exe所在的目录放到PATH环境变量中。然后使用命令:tesseract 图片路径 文件路径
示例:

tesseract a.png a
  • 1.

那么就会识别出a.png中的图片,并且把文字写入到a.txt中。如果不想写入文件直接想显示在终端,那么不要加文件名就可以了。

4. 在代码中使用tesseract识别图像:

Python代码中操作tesseract。需要安装一个库,叫做pytesseract。通过pip的方式即可安装:

pip install pytesseract
  • 1.

并且,需要读取图片,需要借助一个第三方库叫做PIL。通过pip list看下是否安装。如果没有安装,通过pip的方式安装:

pip install PIL
  • 1.

使用pytesseract将图片上的文字转换为文本文字的示例代码如下:

# 导入pytesseract库
import pytesseract
# 导入Image库
from PIL import Image

# 指定tesseract.exe所在的路径
pytesseract.pytesseract.tesseract_cmd = r'D:\ProgramApp\TesseractOCR\tesseract.exe'

# 打开图片
image = Image.open("a.png")
# 调用image_to_string将图片转换为文字
text = pytesseract.image_to_string(image)
print(text)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

5. 用pytesseract处理拉勾网图形验证码:

import pytesseract
from urllib import request
from PIL import Image
import time


pytesseract.pytesseract.tesseract_cmd = r"D:\ProgramApp\TesseractOCR\tesseract.exe"


while True:
    captchaUrl = "https://passport.lagou.com/vcode/create?from=register&refresh=1513081451891"
    request.urlretrieve(captchaUrl,'captcha.png')
    image = Image.open('captcha.png')
    text = pytesseract.image_to_string(image,lang='eng')
    print(text)
    time.sleep(2)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

十六、Scrapy框架架构

1. Scrapy框架介绍:

写一个爬虫,需要做很多的事情。比如:发送网络请求、数据解析、数据存储、反反爬虫机制(更换ip代理、设置请求头等)、异步请求等。这些工作如果每次都要自己从零开始写的话,比较浪费时间。因此Scrapy把一些基础的东西封装好了,在他上面写爬虫可以变的更加的高效(爬取效率和开发效率)。因此真正在公司里,一些上了量的爬虫,都是使用Scrapy框架来解决。

2. Scrapy框架模块功能:

  1. Scrapy Engine(引擎)Scrapy框架的核心部分。负责在SpiderItemPipelineDownloaderScheduler中间通信、传递数据等。
  2. Spider(爬虫):发送需要爬取的链接给引擎,最后引擎把其他模块请求回来的数据再发送给爬虫,爬虫就去解析想要的数据。这个部分是我们开发者自己写的,因为要爬取哪些链接,页面中的哪些数据是我们需要的,都是由程序员自己决定。
  3. Scheduler(调度器):负责接收引擎发送过来的请求,并按照一定的方式进行排列和整理,负责调度请求的顺序等。
  4. Downloader(下载器):负责接收引擎传过来的下载请求,然后去网络上下载对应的数据再交还给引擎。
  5. Item Pipeline(管道):负责将Spider(爬虫)传递过来的数据进行保存。具体保存在哪里,应该看开发者自己的需求。
  6. Downloader Middlewares(下载中间件):可以扩展下载器和引擎之间通信功能的中间件。
  7. Spider Middlewares(Spider中间件):可以扩展引擎和爬虫之间通信功能的中间件。

3. Scrapy安装和文档:

  1. 安装:通过pip install scrapy即可安装。
  2. Scrapy官方文档: http://doc.scrapy.org/en/latest
  3. Scrapy中文文档: http://scrapy-chs.readthedocs.io/zh_CN/latest/index.html

注意:

  1. ubuntu上安装scrapy之前,需要先安装以下依赖:
    sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev,然后再通过pip install scrapy安装。
  2. 如果在windows系统下,提示这个错误ModuleNotFoundError: No module named 'win32api',那么使用以下命令可以解决:pip install pypiwin32

4. Scrapy快速入门:

创建项目:

要使用Scrapy框架创建项目,需要通过命令来创建。首先进入到你想把这个项目存放的目录。然后使用以下命令创建:

scrapy startproject [项目名称]
  • 1.
目录结构介绍:


以下介绍下主要文件的作用:

  1. items.py:用来存放爬虫爬取下来数据的模型。
  2. middlewares.py:用来存放各种中间件的文件。
  3. pipelines.py:用来将items的模型存储到本地磁盘中。
  4. settings.py:本爬虫的一些配置信息(比如请求头、多久发送一次请求、ip代理池等)。
  5. scrapy.cfg:项目的配置文件。
  6. spiders包:以后所有的爬虫,都是存放到这个里面。
使用Scrapy框架爬取糗事百科段子:
使用命令创建一个爬虫:
scrapy gensipder qsbk "qiushibaike.com"
  • 1.

创建了一个名字叫做qsbk的爬虫,并且能爬取的网页只会限制在qiushibaike.com这个域名下。

爬虫代码解析:
import scrapy

class QsbkSpider(scrapy.Spider):
    name = 'qsbk'
    allowed_domains = ['qiushibaike.com']
    start_urls = ['http://qiushibaike.com/']

    def parse(self, response):
        pass
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

其实这些代码我们完全可以自己手动去写,而不用命令。只不过是不用命令,自己写这些代码比较麻烦。
要创建一个Spider,那么必须自定义一个类,继承自scrapy.Spider,然后在这个类中定义三个属性和一个方法。

  1. name:这个爬虫的名字,名字必须是唯一的。
  2. allow_domains:允许的域名。爬虫只会爬取这个域名下的网页,其他不是这个域名下的网页会被自动忽略。
  3. start_urls:爬虫从这个变量中的url开始。
  4. parse:引擎会把下载器下载回来的数据扔给爬虫解析,爬虫再把数据传给这个parse方法。这个是个固定的写法。这个方法的作用有两个,第一个是提取想要的数据。第二个是生成下一个请求的url。
修改settings.py代码:

在做一个爬虫之前,一定要记得修改setttings.py中的设置。两个地方是强烈建议设置的。

  1. ROBOTSTXT_OBEY设置为False。默认是True。即遵守机器协议,那么在爬虫的时候,scrapy首先去找robots.txt文件,如果没有找到。则直接停止爬取。
  2. DEFAULT_REQUEST_HEADERS添加User-Agent。这个也是告诉服务器,我这个请求是一个正常的请求,不是一个爬虫。
完成的爬虫代码:
  1. 爬虫部分代码:
import scrapy
 from abcspider.items import QsbkItem

 class QsbkSpider(scrapy.Spider):
     name = 'qsbk'
     allowed_domains = ['qiushibaike.com']
     start_urls = ['https://www.qiushibaike.com/text/']

     def parse(self, response):
         outerbox = response.xpath("//div[@id='content-left']/div")
         items = []
         for box in outerbox:
             author = box.xpath(".//div[contains(@class,'author')]//h2/text()").extract_first().strip()
             content = box.xpath(".//div[@class='content']/span/text()").extract_first().strip()
             item = QsbkItem()
             item["author"] = author
             item["content"] = content
             items.append(item)
         return items
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

  1. items.py部分代码:
import scrapy
 class QsbkItem(scrapy.Item):
     author = scrapy.Field()
     content = scrapy.Field()
  • 1.
  • 2.
  • 3.
  • 4.

  1. pipeline部分代码:
import json

 class AbcspiderPipeline(object):
     def __init__(self):

         self.items = []

     def process_item(self, item, spider):
         self.items.append(dict(item))
         print("="*40)
         return item

     def close_spider(self,spider):
         with open('qsbk.json','w',encoding='utf-8') as fp:
             json.dump(self.items,fp,ensure_ascii=False)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

运行scrapy项目:

运行scrapy项目。需要在终端,进入项目所在的路径,然后scrapy crawl [爬虫名字]即可运行指定的爬虫。如果不想每次都在命令行中运行,那么可以把这个命令写在一个文件中。以后就在pycharm中执行运行这个文件就可以了。比如现在新创建一个文件叫做start.py,然后在这个文件中填入以下代码:

from scrapy import cmdline

cmdline.execute("scrapy crawl qsbk".split())
  • 1.
  • 2.
  • 3.

5. Scrapy Shell

我们想要在爬虫中使用xpath、beautifulsoup、正则表达式、css选择器等来提取想要的数据。但是因为scrapy是一个比较重的框架。每次运行起来都要等待一段时间。因此要去验证我们写的提取规则是否正确,是一个比较麻烦的事情。因此Scrapy提供了一个shell,用来方便的测试规则。当然也不仅仅局限于这一个功能。

打开Scrapy Shell:

打开cmd终端,进入到Scrapy项目所在的目录,然后进入到scrapy框架所在的虚拟环境中,输入命令scrapy shell [链接]。就会进入到scrapy的shell环境中。在这个环境中,你可以跟在爬虫的parse方法中一样使用了。

十七、CrawlSpider

在上一个糗事百科的爬虫案例中。我们是自己在解析完整个页面后获取下一页的url,然后重新发送一个请求。有时候我们想要这样做,只要满足某个条件的url,都给我进行爬取。那么这时候我们就可以通过CrawlSpider来帮我们完成了。CrawlSpider继承自Spider,只不过是在之前的基础之上增加了新的功能,可以定义爬取的url的规则,以后scrapy碰到满足条件的url都进行爬取,而不用手动的yield Request

1. 创建CrawlSpider爬虫:

之前创建爬虫的方式是通过scrapy genspider [爬虫名字] [域名]的方式创建的。如果想要创建CrawlSpider爬虫,那么应该通过以下命令创建:

scrapy genspider -c crawl [爬虫名字] [域名]
  • 1.

2. LinkExtractors链接提取器:

使用LinkExtractors可以不用程序员自己提取想要的url,然后发送请求。这些工作都可以交给LinkExtractors,他会在所有爬的页面中找到满足规则的url,实现自动的爬取。以下对LinkExtractors类做一个简单的介绍:

class scrapy.linkextractors.LinkExtractor(
    allow = (),
    deny = (),
    allow_domains = (),
    deny_domains = (),
    deny_extensions = None,
    restrict_xpaths = (),
    tags = ('a','area'),
    attrs = ('href'),
    canonicalize = True,
    unique = True,
    process_value = None
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

主要参数讲解:

  • allow:允许的url。所有满足这个正则表达式的url都会被提取。
  • deny:禁止的url。所有满足这个正则表达式的url都不会被提取。
  • allow_domains:允许的域名。只有在这个里面指定的域名的url才会被提取。
  • deny_domains:禁止的域名。所有在这个里面指定的域名的url都不会被提取。
  • restrict_xpaths:严格的xpath。和allow共同过滤链接。

3. Rule规则类:

定义爬虫的规则类。以下对这个类做一个简单的介绍:

class scrapy.spiders.Rule(
    link_extractor, 
    callback = None, 
    cb_kwargs = None, 
    follow = None, 
    process_links = None, 
    process_request = None
)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

主要参数讲解:

  • link_extractor:一个LinkExtractor对象,用于定义爬取规则。
  • callback:满足这个规则的url,应该要执行哪个回调函数。因为CrawlSpider使用了parse作为回调函数,因此不要覆盖parse作为回调函数自己的回调函数。
  • follow:指定根据该规则从response中提取的链接是否需要跟进。
  • process_links:从link_extractor中获取到链接后会传递给这个函数,用来过滤不需要爬取的链接。

十八、redis教程:

1. 概述

redis是一种支持分布式的nosql数据库,他的数据是保存在内存中,同时redis可以定时把内存数据同步到磁盘,即可以将数据持久化,并且他比memcached支持更多的数据结构(string,list列表[队列和栈],set[集合],sorted set[有序集合],hash(hash表))。相关参考文档: http://redisdoc.com/index.html

2. redis使用场景:

  1. 登录会话存储:存储在redis中,与memcached相比,数据不会丢失。
  2. 排行版/计数器:比如一些秀场类的项目,经常会有一些前多少名的主播排名。还有一些文章阅读量的技术,或者新浪微博的点赞数等。
  3. 作为消息队列:比如celery就是使用redis作为中间人。
  4. 当前在线人数:还是之前的秀场例子,会显示当前系统有多少在线人数。
  5. 一些常用的数据缓存:比如我们的BBS论坛,板块不会经常变化的,但是每次访问首页都要从mysql中获取,可以在redis中缓存起来,不用每次请求数据库。
  6. 把前200篇文章缓存或者评论缓存:一般用户浏览网站,只会浏览前面一部分文章或者评论,那么可以把前面200篇文章和对应的评论缓存起来。用户访问超过的,就访问数据库,并且以后文章超过200篇,则把之前的文章删除。
  7. 好友关系:微博的好友关系使用redis实现。
  8. 发布和订阅功能:可以用来做聊天软件。

3.redismemcached的比较:

memcached

redis

类型

纯内存数据库

内存磁盘同步数据库

数据类型

在定义value时就要固定数据类型

不需要

虚拟内存

不支持

支持

过期策略

支持

支持

存储数据安全

不支持

可以将数据同步到dump.db中

灾难恢复

不支持

可以将磁盘中的数据恢复到内存中

分布式

支持

主从同步

订阅与发布

不支持

支持

4. redisubuntu系统中的安装与启动

  1. 安装:
sudo apt-get install redis-server
  • 1.

  1. 卸载:
sudo apt-get purge --auto-remove redis-server
  • 1.

  1. 启动:redis安装后,默认会自动启动,可以通过以下命令查看:
ps aux|grep redis
  • 1.

如果想自己手动启动,可以通过以下命令进行启动:

sudo service redis-server start
  • 1.

  1. 停止:
sudo service redis-server stop
  • 1.

5. redis在windows系统中的安装与启动:

  1. 下载:redis官方是不支持windows操作系统的。但是微软的开源部门将redis移植到了windows上。因此下载地址不是在redis官网上。而是在github上:https://github.com/MicrosoftArchive/redis/releases。
  2. 安装:点击一顿下一步安装就可以了。
  3. 运行:进入到redis安装所在的路径然后执行redis-server.exe redis.windows.conf就可以运行了。
  4. 连接:redismysql以及mongo是一样的,都提供了一个客户端进行连接。输入命令redis-cli(前提是redis安装路径已经加入到环境变量中了)就可以连接到redis服务器了。

6. 其他机器访问本机redis服务器:

想要让其他机器访问本机的redis服务器。那么要修改redis.conf的配置文件,将bind改成bind [自己的ip地址或者0.0.0.0],其他机器才能访问。
注意:bind绑定的是本机网卡的ip地址,而不是想让其他机器连接的ip地址。如果有多块网卡,那么可以绑定多个网卡的ip地址。如果绑定到额是0.0.0.0,那么意味着其他机器可以通过本机所有的ip地址进行访问。

7. 对redis的操作

redis的操作可以用两种方式,第一种方式采用redis-cli,第二种方式采用编程语言,比如PythonPHPJAVA等。

  1. 使用redis-cliredis进行字符串操作:
  2. 启动redis
sudo service redis-server start
  • 1.

  1. 连接上
redis-server
  • 1.

redis-cli -h [ip] -p [端口]
  • 1.

  1. 添加:
set key value
  如:
  set username xiaotuo
  • 1.
  • 2.
  • 3.

将字符串值value关联到key。如果key已经持有其他值,set命令就覆写旧值,无视其类型。并且默认的过期时间是永久,即永远不会过期。

  1. 删除:
del key
  如:
  del username
  • 1.
  • 2.
  • 3.

  1. 设置过期时间:
expire key timeout(单位为秒)
  • 1.

也可以在设置值的时候,一同指定过期时间:

set key value EX timeout
  或:
  setex key timeout value
  • 1.
  • 2.
  • 3.

  1. 查看过期时间:
ttl key
  如:
  ttl username
  • 1.
  • 2.
  • 3.

  1. 查看当前redis中的所有key
keys *
  • 1.

  1. 列表操作:
  • 在列表左边添加元素:
lpush key value
  • 1.

将值value插入到列表key的表头。如果key不存在,一个空列表会被创建并执行lpush操作。当key存在但不是列表类型时,将返回一个错误。

  • 在列表右边添加元素:
rpush key value
  • 1.

将值value插入到列表key的表尾。如果key不存在,一个空列表会被创建并执行RPUSH操作。当key存在但不是列表类型时,返回一个错误。

  • 查看列表中的元素:
lrange key start stop
  • 1.

返回列表key中指定区间内的元素,区间以偏移量startstop指定,如果要左边的第一个到最后的一个lrange key 0 -1

  • 移除列表中的元素:
  • 移除并返回列表
key
  • 1.

的头元素:

lpop key
  • 1.

  • 移除并返回列表的尾元素:
rpop key
  • 1.

  • 移除并返回列表key的中间元素:
lrem key count value
  • 1.

将删除key这个列表中,count个值为value的元素。

  • 指定返回第几个元素:
lindex key index
  • 1.

将返回key这个列表中,索引为index的这个元素。

  • 获取列表中的元素个数:
llen key
  如:
  llen languages
  • 1.
  • 2.
  • 3.

  • 删除指定的元素:
lrem key count value
  如:
  lrem languages 0 php
  • 1.
  • 2.
  • 3.

根据参数 count 的值,移除列表中与参数 value 相等的元素。count的值可以是以下几种:

  • count > 0:从表头开始向表尾搜索,移除与value相等的元素,数量为count
  • count < 0:从表尾开始向表头搜索,移除与 value相等的元素,数量为count的绝对值。
  • count = 0:移除表中所有与value 相等的值。
  1. set集合的操作:
  • 添加元素:
sadd set value1 value2....
  如:
  sadd team xiaotuo datuo
  • 1.
  • 2.
  • 3.

  • 查看元素:
smembers set
  如:
  smembers team
  • 1.
  • 2.
  • 3.

  • 移除元素:
srem set member...
  如:
  srem team xiaotuo datuo
  • 1.
  • 2.
  • 3.

  • 查看集合中的元素个数:
scard set
  如:
  scard team1
  • 1.
  • 2.
  • 3.

  • 获取多个集合的交集:
sinter set1 set2
  如:
  sinter team1 team2
  • 1.
  • 2.
  • 3.

  • 获取多个集合的并集:
sunion set1 set2
  如:
  sunion team1 team2
  • 1.
  • 2.
  • 3.

  • 获取多个集合的差集:
sdiff set1 set2
如:
sdiff team1 team2
  • 1.
  • 2.
  • 3.

  1. hash哈希操作:
  • 添加一个新值:
hset key field value
  如:
  hset website baidu baidu.com
  • 1.
  • 2.
  • 3.

将哈希表key中的域field的值设为value
如果key不存在,一个新的哈希表被创建并进行 HSET操作。如果域 field已经存在于哈希表中,旧值将被覆盖。

  • 获取哈希中的field对应的值:
hget key field
  如:
  hget website baidu
  • 1.
  • 2.
  • 3.

  • 删除field中的某个field
hdel key field
  如:
  hdel website baidu
  • 1.
  • 2.
  • 3.

  • 获取某个哈希中所有的fieldvalue
hgetall key
  如:
  hgetall website
  • 1.
  • 2.
  • 3.

  • 获取某个哈希中所有的field
hkeys key
  如:
  hkeys website
  • 1.
  • 2.
  • 3.

  • 获取某个哈希中所有的值:
hvals key
如:
hvals website
  • 1.
  • 2.
  • 3.

  • 判断哈希中是否存在某个field
hexists key field
如:
hexists website baidu
  • 1.
  • 2.
  • 3.

  • 获取哈希中总共的键值对:
hlen field
如:
hlen website
  • 1.
  • 2.
  • 3.

  1. 事务操作:Redis事务可以一次执行多个命令,事务具有以下特征:
  • 隔离操作:事务中的所有命令都会序列化、按顺序地执行,不会被其他命令打扰。
  • 原子操作:事务中的命令要么全部被执行,要么全部都不执行。
  • 开启一个事务:
multi
  • 1.

以后执行的所有命令,都在这个事务中执行的。

  • 执行事务:
exec
  • 1.

会将在multiexec中的操作一并提交。

  • 取消事务:
discard
  • 1.

会将multi后的所有命令取消。

  • 监视一个或者多个key
watch key...
  • 1.

监视一个(或多个)key,如果在事务执行之前这个(或这些) key被其他命令所改动,那么事务将被打断。

  • 取消所有key的监视:
unwatch
  • 1.

  1. 发布/订阅操作:
  • 给某个频道发布消息:
publish channel message
  • 1.

  • 订阅某个频道的消息:
subscribe channel
  • 1.

十九、Scrapy-Redis分布式爬虫组件

Scrapy是一个框架,他本身是不支持分布式的。如果我们想要做分布式的爬虫,就需要借助一个组件叫做Scrapy-Redis,这个组件正是利用了Redis可以分布式的功能,集成到Scrapy框架中,使得爬虫可以进行分布式。可以充分的利用资源(多个ip、更多带宽、同步爬取)来提高爬虫的爬行效率。

1. 分布式爬虫的优点:

  1. 可以充分利用多台机器的带宽。
  2. 可以充分利用多台机器的ip地址。
  3. 多台机器做,爬取效率更高。

2. 分布式爬虫必须要解决的问题:

  1. 分布式爬虫是好几台机器在同时运行,如何保证不同的机器爬取页面的时候不会出现重复爬取的问题。
  2. 同样,分布式爬虫在不同的机器上运行,在把数据爬完后如何保证保存在同一个地方。

3. 安装:

通过pip install scrapy-redis即可安装。

4. Scrapy-Redis架构:

Scrapy架构图:

Scrapy-Redis架构图:

分布式爬虫架构图:

以上两个图片对比我们可以发现。Item Pipeline在接收到数据后发送给了RedisScheduler调度器调度数据也是从Redis中来的、并且其实数据去重也是在Redis中做的。

5. 编写Scrapy-Redis分布式爬虫:

要将一个Scrapy项目变成一个Scrapy-redis项目只需修改以下三点就可以了:

  1. 将爬虫的类从scrapy.Spider变成scrapy_redis.spiders.RedisSpider;或者是从scrapy.CrawlSpider变成scrapy_redis.spiders.RedisCrawlSpider
  2. 将爬虫中的start_urls删掉。增加一个redis_key="xxx"。这个redis_key是为了以后在redis中控制爬虫启动的。爬虫的第一个url,就是在redis中通过这个发送出去的。
  3. 在配置文件中增加如下配置:
# Scrapy-Redis相关配置
    # 确保request存储到redis中
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"

    # 确保所有爬虫共享相同的去重指纹
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

    # 设置redis为item pipeline
    ITEM_PIPELINES = {
        'scrapy_redis.pipelines.RedisPipeline': 300
    }

    # 在redis中保持scrapy-redis用到的队列,不会清理redis中的队列,从而可以实现暂停和恢复的功能。
    SCHEDULER_PERSIST = True

    # 设置连接redis信息
    REDIS_HOST = '127.0.0.1'
    REDIS_PORT = 6379
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  1. 运行爬虫:
  1. 在爬虫服务器上。进入爬虫文件所在的路径,然后输入命令:scrapy runspider [爬虫名字]
  2. Redis服务器上,推入一个开始的url链接:redis-cli> lpush [redis_key] start_url开始爬取。