爬取目标为豆瓣电影列表 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))。在考虑自身需求的时候,也需要为别人的服务器所能承受的压力考虑一下。