数据采集:多线程+动态IP处理并发爬虫

爬取目标为豆瓣电影列表 https://movie.douban.com/tag/#/?sort=U&range=0,10&tags=电影

在这里插入图片描述
对于每一部电影,分别爬取其中的①电影名称,②导演,③上映日期,④制片国家/地区,⑤片长,⑥评分,⑦类别,⑧评论人数
在这里插入图片描述
对于电影的详情页面,豆瓣是使用了静态加载,所有直接使用requests请求库+正则表达式抓取即可。

import requests
import uagent
import re


def get_page(url):
	headers = {'User-Agent': uagent.get_ua()}
	response = requests.get(url = url, headers = headers)
	page = response.text
	return page


#返回一个列表,电影名称
def name(page):
	pattern = '<span property="v:itemreviewed">(.*?)</span>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,导演
def director(page):
	pattern = 'rel="v:directedBy">(.*?)</a>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,上映日期
def date(page):
	pattern = '<span property="v:initialReleaseDate" content="(.*?)">'
	item = re.findall(pattern, page)
	return item


#返回一个列表,制片国家/地区
def country(page):
	pattern = '<span class="pl">制片国家/地区:</span>(.*?)<br/>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,片长
def mins(page):
	pattern = '<span class="pl">片长:</span> <span property="v:runtime" content=".*">(.*?)</span><br/>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,评分
def score(page):
	pattern = '<strong class="ll rating_num" property="v:average">(.*?)</strong>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,电影类别
def kind(page):
	pattern = '<span property="v:genre">(.*?)</span>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,评论人数
def comments(page):
	pattern = '<a href="collections" class="rating_people"><span property="v:votes">(.*?)</span>人评价</a>'
	item = re.findall(pattern, page)
	return item


def parse(url):
	page 	=	get_page(url)
	return (
		name(page), 
		director(page), 
		date(page), 
		country(page), 
		mins(page), 
		score(page),
		kind(page),
		comments(page)
	)


if __name__ == '__main__':
	url = 'https://movie.douban.com/subject/3878007/'
	data = parse(url)
	print(data)

爬取1部电影之后,第二步是设法爬取所有的电影。现在回到豆瓣电影列表的页面,可以看到,在页面的底部有一个“加载更多“的字样。一般来说,出现这样的情况,网站应该就是Ajax加载,也就是说我们应该从浏览器的开发者工具中找线索。
在这里插入图片描述

通过不断点击“加载更多”,可以从开发者工具中看到不断出现的链接,而且具有一定的规律。
在这里插入图片描述

点击其中一个链接,可以看到,网站返回的是Json数据,而其中的一个“url”字段,对应的正正是电影详情页面的地址。因此,接下来就可以利用刚才发现的链接间的规律以及这个“url”字段,来抓取所有的电影详情页面的地址。
在这里插入图片描述

另外,这些抓取下来的电影详情页面的地址,可以保存到redis的列表当中,在最后抓取电影信息的时候,只需要一个接一个地弹出网址,即使爬虫中途中断,下次启动时也能接着结束的地方开始。

import requests
import uagent
import re
import redis

def get_json(url):
	headers = {'User-Agent': uagent.get_ua()}
	response = requests.get(url = url, headers = headers)
	json = response.json()
	return json


def movie_lists(json):
	items = json['data']
	return (item['url'] for item in items)


def push(r, data):
	for i in data:
		r.lpush('movie_lists', i)
		len = r.llen('movie_lists')
		print(len, 'urls push in redis.')


def main():
	r = redis.StrictRedis(host = 'localhost', port = 6379, db =0)
	base_url = 'https://movie.douban.com/j/new_search_subjects?sort=U&range=0,10&tags=%%E7%%94%%B5%%E5%%BD%%B1&start=%d'
	for i in range(0, 20, 20):
		url = base_url % i
		json = get_json(url)
		data = movie_lists(json)
		push(r, data)


if __name__ == '__main__':
	main()

在爬取的过程中发现,豆瓣很容易会封禁爬虫。因此需要作一些处理。
在这里插入图片描述

我的处理方法是使用动态代理IP。此时需要修改get_page函数,我在这里使用了付费代理IP,1小时1块钱。通过添加一个proxies字典,就能够使用动态IP了。proxyHost,proxyPort,proxyPass,proxyPass变量是代理商提供的接口。


def get_page(url):
	proxyHost = "http-dyn.abuyun.com" 
	proxyPort = "9020"
	proxyUser = "xxxxxxxxxxxxxxxx"
	proxyPass = "xxxxxxxxxxxxxxxx"
	proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
		"host" : proxyHost,
		"port" : proxyPort,
		"user" : proxyUser,
		"pass" : proxyPass,
	}
	proxies = {
		"http"  : proxyMeta,
		"https" : proxyMeta,
	}
	headers = {'User-Agent': uagent.get_ua()}
	response = requests.get(url = url, headers = headers, proxies = proxies, timeout = 10)
	page = response.text
	return page

现在爬虫已经具备了一定的反爬能力了,但是还有一个问题,就是效率非常低,通过测试可以发现(求5次爬取20页电影详情页面的平均时间)
测试代码如下:

def test():
	start = time.time()
	for i in range(20):
		url = r.rpop('movie_lists')
		result = parse(url)
		print(result)
	print(time.time() - start)

if __name__ == '__main__':
	r = redis.StrictRedis(host = 'localhost', port = 6379, db = 0)
	for i in range(5):
		test()

第一次抓取20页用时:75.6673276424408
第二次抓取20页用时:75.86633944511414
第三次抓取20页用时:76.40337014198303
第四次抓取20页用时:78.29247784614563
第五次抓取20页用时:77.42842864990234
爬取20页电影详情页面平均大约需要76.73s
豆瓣开放的数据当中有9980部电影的详情页面,那么如果要全部抓取下来,需要用时大约就是
9980 / 20 * 76.73 = 38288.27s = 638.14mins = 10.4h

在爬虫脚本当中,效率低下的主要原因就是IO阻塞,其中包括了
请求的间隔时间,
网络IO请求阻塞时间,
脚本与redis的IO时间,
如果还要保存数据的话,譬如保存到csv文件当中,又会增加IO时间。

这个时候可以利用Python的多线程来优化爬虫,在Python解释器中存在一个GIL(全局解释锁),每个线程在执行的时候必须要取得GIL,但是当出现IO阻塞的时候,线程就会释放GIL,其他线程就能够取得该GIL,进而开始工作,简单来说,就是在脚本IO阻塞期间,可以做其他事情,这样就节约了时间。

利用threading模块,我开了10个线程来进行抓取

import threading

#多线程提高爬虫效率
def main():
	start = time.time()
	ts = [threading.Thread(target = spider) for i in range(10)] #spider是爬虫的主逻辑函数,该函数不需要提供参数。
	for t in ts:
		t.start()
	for t in ts:
		t.join()
	print(time.time() - start)
	print('finish.')



if __name__ == '__main__':
	main()

另外我添加了一个保存数据的函数,如下:

import csv


#用于保存数据到csv文件中
def save(data):
	with open('movies.csv', 'a', newline = '', encoding = 'GB18030') as c:
		writer = csv.writer(c)
		writer.writerow(data)

完整代码如下:

import requests
import uagent
import re
import redis
import threading
import csv
import time


lock = threading.Lock()


def get_page(url):
	proxyHost = "http-dyn.abuyun.com"
	proxyPort = "9020"
	proxyUser = "H9WTY5I0HPE335UD"
	proxyPass = "2694DBD4B6D9DB1E"
	proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
		"host" : proxyHost,
		"port" : proxyPort,
		"user" : proxyUser,
		"pass" : proxyPass,
	}
	proxies = {
		"http"  : proxyMeta,
		"https" : proxyMeta,
	}
	headers = {'User-Agent': uagent.get_ua()}
	response = requests.get(url = url, headers = headers, proxies = proxies, timeout = 10)
	page = response.text
	return page


#返回一个列表,电影名称
def name(page):
	pattern = '<span property="v:itemreviewed">(.*?)</span>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,导演
def director(page):
	pattern = 'rel="v:directedBy">(.*?)</a>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,上映日期
def date(page):
	pattern = '<span property="v:initialReleaseDate" content="(.*?)">'
	item = re.findall(pattern, page)
	return item


#返回一个列表,制片国家/地区
def country(page):
	pattern = '<span class="pl">制片国家/地区:</span>(.*?)<br/>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,片长
def mins(page):
	pattern = '<span class="pl">片长:</span> <span property="v:runtime" content=".*">(.*?)</span><br/>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,评分
def score(page):
	pattern = '<strong class="ll rating_num" property="v:average">(.*?)</strong>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,电影类别
def kind(page):
	pattern = '<span property="v:genre">(.*?)</span>'
	item = re.findall(pattern, page)
	return item


#返回一个列表,评论人数
def comments(page):
	pattern = '<a href="collections" class="rating_people"><span property="v:votes">(.*?)</span>人评价</a>'
	item = re.findall(pattern, page)
	return item


def parse(page):
	return (
		name(page), 
		director(page), 
		date(page), 
		country(page), 
		mins(page), 
		score(page),
		kind(page),
		comments(page)
	)


#用于保存数据到csv文件中
def save(data):
	with open('movies.csv', 'a', newline = '', encoding = 'GB18030') as c:
		writer = csv.writer(c)
		writer.writerow(data)


#爬虫的主逻辑
def spider():
	r = redis.StrictRedis(host = 'localhost', port = 6379, db = 0)
	while True:
	#这里要添加一个锁,因为对于数据库的读写需要同步操作,不然会出现读写错误。注意这个锁不是GIL,是threading中创建的锁,它不属于某个特定的线程。
		lock.acquire() 
		if r.llen('movie_lists'):
			url = r.rpop('movie_lists')
			#数据库读写操作结束,可以释放锁,让其他线程去竞争该锁
			lock.release()
			#处理异常,如果出现异常,就把该url重新放入redis的列表当中,然后重新运行该函数,异常出现的因为可能是因为代理IP不是100%可用。
			try: 
				page = get_page(url)
				result = parse(page)
				save(result)
				print(result)
			except: 
				r.lpush('movie_lists', url)
				spider()
		#如果在redis列表当中长度为0,也就是说所有电影信息都抓取下来了,所以可以直接释放锁,并退出while循环。
		else:
			lock.release()
			break


#多线程提高爬虫效率
def main():
	start = time.time()
	ts = [threading.Thread(target = spider) for i in range(10)]
	for t in ts:
		t.start()
	for t in ts:
		t.join()
	print(time.time() - start) #计算整个爬虫运行所花费的时间。
	print('finish.')



if __name__ == '__main__':
	main()

下图是该爬虫运行的时间:3392s = 56.54mins,不到1小时。在这个例子中,和单线程相比,理论上快了10倍多。
在这里插入图片描述

总结:利用Python的threading模块创建多线程比较简单,主要写好抓取的逻辑,然后直接利用threading.Thread创建多线程即可,其中如果涉及到读写操作的时候,要注意加锁。
特别是针对爬虫,多线程爬虫是一个并发爬虫,对目标网站同一时刻作出大量的请求,很有可能会被服务器所封禁,此时配合动态IP+多线程是一个不错的解决办法,如果有需要,可以在请求之间添加时间间隔(譬如time.sleep(x))。在考虑自身需求的时候,也需要为别人的服务器所能承受的压力考虑一下。

猜你喜欢

转载自blog.csdn.net/eighttoes/article/details/86159500
今日推荐