多线程爬取表情包
有一个网站,叫做“斗图啦”,网址是:https://www.doutula.com/
。这里面包含了许许多多的有意思的斗图图片,还蛮好玩的。有时候为了斗图要跑到这个上面来找表情,实在有点费劲。于是就产生了一个邪恶的想法,可以写个爬虫,把所有的表情都给爬下来。这个网站对于爬虫来讲算是比较友好了,他不会限制你的headers
,不会限制你的访问频率(当然,作为一个有素质的爬虫工程师,爬完赶紧撤,不要把人家服务器搞垮了),不会限制你的 IP 地址,因此技术难度不算太高。但是有一个问题,因为这里要爬的是图片,而不是文本信息,所以采用传统的爬虫是可以完成我们的需求,但是因为是下载图片所以速度比较慢,可能要爬一两个小时都说不准。因此这里我们准备采用多线程爬虫,一下可以把爬虫的效率提高好几倍。
一、分析网站和爬虫准备工作:
构建所有页面 URL 列表:
这里我们要爬的页面不是“斗图啦”首页,而是最新表情页面https://www.doutula.com/photo/list/
,这个页面包含了所有的表情图片,只是是按照时间来排序的而已。我们把页面滚动到最下面,可以看到这个最新表情使用的是分页,当我们点击第二页的时候,页面的URL
变成了https://www.doutula.com/photo/list/?page=2
,而我们再回到第一页的时候,page
又变成了1
,所以这个翻页的URL
其实很简单,前面这一串https://www.doutula.com/photo/list/?page=
都是固定的,只是后面跟的数字不一样而已。并且我们可以看到,这个最新表情总共是有 869 页,因此这里我们可以写个非常简单的代码,来构建一个从 1 到 869 的页面的URL
列表:
1
2
3
4
5
6
|
# 全局变量,用来保存页面的 URL 的
PAGE_URL_LIST
=
[
]
BASE_PAGE_URL
=
'https://www.doutula.com/photo/list/?page='
for
x
in
range
(
1
,
870
)
:
url
=
BASE_PAGE_URL
+
str
(
x
)
PAGE_URL_LIST
.
append
(
url
)
|
获取一个页面中所有的表情图片链接:
我们已经拿到了所有页面的链接,但是还没有拿到每个页面中表情的链接。经过分析,我们可以知道,其实每个页面中表情的HTML
元素构成都是一样的,因此我们只需要针对一个页面进行分析,其他页面按照同样的规则,就可以拿到所有页面的表情链接了。这里我们以第一页为例,跟大家讲解。首先在页面中右键->检查->Elements
,然后点击Elements
最左边的那个小光标,再把鼠标放在随意一个表情上,这样下面的代码就定位到这个表情所在的代码位置了: 可以看到,这个img
标签的class
是等于img-responsive lazy image_dtz
,然后我们再定位其他表情的img
标签,发现所有的表情的img
标签,他的class
都是img-responsive lazy image_dtz
:
因此我们只要把数据从网上拉下来,然后再根据这个规则进行提取就可以了。这里我们使用了两个第三方库,一个是requests
,这个库是专门用来做网络请求的。第二个库是bs4
,这个库是专门用来把请求下来的数据进行分析和过滤用的,如果没有安装好这两个库的,可以使用以下代码进行安装(我使用的是 python2.7 的版本):
1
2
3
4
5
6
|
# 安装 requests
pip
install
requests
# 安装 bs4
pip
install
bs4
# 安装 lxml 解析引擎
pip
install
lxml
|
然后我们以第一个页面为例,跟大家讲解如何从页面中获取所有表情的链接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 导入 requests 库
import
requests
# 从 bs4 中导入 BeautifulSoup
from
bs4
import
BeautifulSoup
# 第一页的链接
url
=
'https://www.doutula.com/photo/list/?page=1'
# 请求这个链接
response
=
requests
.
get
(
url
)
# 使用返回的数据,构建一个 BeautifulSoup 对象
soup
=
BeautifulSoup
(
response
.
content
,
'lxml'
)
# 获取所有 class='img-responsive lazy image_dtz'的 img 标签
img_list
=
soup
.
find_all
(
'img'
,
attrs
=
{
'class'
:
'img-responsive lazy image_dta'
}
)
for
img
in
img_list
:
# 因为 src 属性刚开始获取的是 loading 的图片,因此使用 data-original 比较靠谱
print
img
[
'data-original'
]
|
这样我们就可以在控制台看到本页中所有的表情图片的链接就全部都打印出来了。
下载图片:
有图片链接后,还要对图片进行下载处理,这里我们以一张图片为例:http://ws2.sinaimg.cn/bmiddle/9150e4e5ly1fhpi3ysfocj205i04aa9z.jpg
,来看看Python
中如何轻轻松松下载一张图片:
1
2
3
|
import
urllib
url
=
'http://ws2.sinaimg.cn/bmiddle/9150e4e5ly1fhpi3ysfocj205i04aa9z.jpg'
urllib
.
urlretrieve
(
url
,
filename
=
'test.jpg'
)
|
这样就可以下载一张图片了。
结合以上三部分内容:
以上三部分,分别对,如何构建所有页面的URL
,一个页面中如何获取所有表情的链接以及下载图片的方法。接下来把这三部分结合在一起,就可以构建一个完整但效率不高的爬虫了:
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
|
# 导入 requests 库
import
requests
# 从 bs4 中导入 BeautifulSoup
from
bs4
import
BeautifulSoup
import
urllib
import
os
# 全局变量,用来保存页面的 URL 的
PAGE_URL_LIST
=
[
]
BASE_PAGE_URL
=
'https://www.doutula.com/photo/list/?page='
for
x
in
range
(
1
,
870
)
:
url
=
BASE_PAGE_URL
+
str
(
x
)
PAGE_URL_LIST
.
append
(
url
)
for
page_url
in
PAGE_URL_LIST
:
# 请求这个链接
response
=
requests
.
get
(
page_url
)
# 使用返回的数据,构建一个 BeautifulSoup 对象
soup
=
BeautifulSoup
(
response
.
content
,
'lxml'
)
# 获取所有 class='img-responsive lazy image_dtz'的 img 标签
img_list
=
soup
.
find_all
(
'img'
,
attrs
=
{
'class'
:
'img-responsive lazy image_dta'
}
)
for
img
in
img_list
:
# 因为 src 属性刚开始获取的是 loading 的图片,因此使用 data-original 比较靠谱
src
=
img
[
'data-original'
]
# 有些图片是没有 http 的,那么要加一个 http
if
not
src
.
startswith
(
'http'
)
:
src
=
'http:'
+
src
# 获取图片的名称
filename
=
src
.
split
(
'/'
)
.
pop
(
)
# 拼接完整的路径
path
=
os.path
.
join
(
'images'
,
filename
)
urllib
.
urlretrieve
(
src
,
path
)
|
以上这份代码。可以完整的运行了。但是效率不高,毕竟是在下载图片,要一个个排队下载。如果能够采用多线程,在一张图片下载的时候,就完全可以去请求其他图片,而不用继续等待了。因此效率比较高,以下将该例子改为多线程来实现。
二、多线程下载图片:
这里多线程我们使用的是Python
自带的threading
模块。并且我们使用了一种叫做生产者和消费者的模式,生产者专门用来从每个页面中获取表情的下载链接存储到一个全局列表中。而消费者专门从这个全局列表中提取表情链接进行下载。并且需要注意的是,在多线程中使用全局变量要用锁来保证数据的一致性。以下是多线程的爬虫代码(如果有看不懂的,可以看视频,讲解很仔细):
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
|
#encoding: utf-8
import
urllib
import
threading
from
bs4
import
BeautifulSoup
import
requests
import
os
import
time
# 表情链接列表
FACE_URL_LIST
=
[
]
# 页面链接列表
PAGE_URL_LIST
=
[
]
# 构建 869 个页面的链接
BASE_PAGE_URL
=
'https://www.doutula.com/photo/list/?page='
for
x
in
range
(
1
,
870
)
:
url
=
BASE_PAGE_URL
+
str
(
x
)
PAGE_URL_LIST
.
append
(
url
)
# 初始化锁
gLock
=
threading
.
Lock
(
)
# 生产者,负责从每个页面中提取表情的 url
class
Producer
(
threading
.
Thread
)
:
def
run
(
self
)
:
while
len
(
PAGE_URL_LIST
)
>
0
:
# 在访问 PAGE_URL_LIST 的时候,要使用锁机制
gLock
.
acquire
(
)
page_url
=
PAGE_URL_LIST
.
pop
(
)
# 使用完后要及时把锁给释放,方便其他线程使用
gLock
.
release
(
)
response
=
requests
.
get
(
page_url
)
soup
=
BeautifulSoup
(
response
.
content
,
'lxml'
)
img_list
=
soup
.
find_all
(
'img'
,
attrs
=
{
'class'
:
'img-responsive lazy image_dta'
}
)
gLock
.
acquire
(
)
for
img
in
img_list
:
src
=
img
[
'data-original'
]
if
not
src
.
startswith
(
'http'
)
:
src
=
'http:'
+
src
# 把提取到的表情 url,添加到 FACE_URL_LIST 中
FACE_URL_LIST
.
append
(
src
)
gLock
.
release
(
)
time
.
sleep
(
0.5
)
# 消费者,负责从 FACE_URL_LIST 提取表情链接,然后下载
class
Consumer
(
threading
.
Thread
)
:
def
run
(
self
)
:
print
'%s is running'
%
threading
.
current_thread
while
True
:
# 上锁
gLock
.
acquire
(
)
if
len
(
FACE_URL_LIST
)
==
0
:
# 不管什么情况,都要释放锁
gLock
.
release
(
)
continue
else
:
# 从 FACE_URL_LIST 中提取数据
face_url
=
FACE_URL_LIST
.
pop
(
)
gLock
.
release
(
)
filename
=
face_url
.
split
(
'/'
)
[
-
1
]
path
=
os.path
.
join
(
'images'
,
filename
)
urllib
.
urlretrieve
(
face_url
,
filename
=
path
)
if
__name__
==
'__main__'
:
# 2 个生产者线程,去从页面中爬取表情链接
for
x
in
range
(
2
)
:
Producer
(
)
.
start
(
)
# 5 个消费者线程,去从 FACE_URL_LIST 中提取下载链接,然后下载
for
x
in
range
(
5
)
:
Consumer
(
)
.
start
(
)
|
写在最后:
本教程采用多线程来完成表情的爬取,可以让爬取效率高出很多倍。Python
的多线程虽然有GIL
全局解释器锁,但在网络IO
处理这一块表现还是很好的,不用在一个地方一直等待。以上这个例子就很好的说明了多线程的好处。