本文首发于知乎
本文是上一篇文章的续篇,实现基于多线程的 翻页、抓取二级页面。使用豆瓣top250作为例子,为了防止请求过快ip被封,我们每页只抓取5个电影。
爬虫代码如下
import requests
import time
from threading import Thread
from queue import Queue
import json
from bs4 import BeautifulSoup
def run_time(func):
def wrapper(*args, **kw):
start = time.time()
func(*args, **kw)
end = time.time()
print('running', end-start, 's')
return wrapper
class Spider():
def __init__(self):
self.start_url = 'https://movie.douban.com/top250'
self.qurl = Queue()
self.data = list()
self.item_num = 5 # 限制每页提取个数(也决定了二级页面数量)防止对网页请求过多
self.thread_num = 10 # 抓取二级页面线程数量
self.first_running = True
def parse_first(self, url):
print('crawling', url)
r = requests.get(url)
soup = BeautifulSoup(r.content, 'lxml')
movies = soup.find_all('div', class_ = 'info')[:self.item_num]
for movie in movies:
url = movie.find('div', class_ = 'hd').a['href']
self.qurl.put(url)
nextpage = soup.find('span', class_ = 'next').a
if nextpage:
nexturl = self.start_url + nextpage['href']
self.parse_first(nexturl)
else:
self.first_running = False
def parse_second(self):
while self.first_running or not self.qurl.empty():
url = self.qurl.get()
print('crawling', url)
r = requests.get(url)
soup = BeautifulSoup(r.content, 'lxml')
mydict = {}
title = soup.find('span', property = 'v:itemreviewed')
mydict['title'] = title.text if title else None
duration = soup.find('span', property = 'v:runtime')
mydict['duration'] = duration.text if duration else None
time = soup.find('span', property = 'v:initialReleaseDate')
mydict['time'] = time.text if time else None
self.data.append(mydict)
@run_time
def run(self):
ths = []
th1 = Thread(target=self.parse_first, args=(self.start_url, ))
th1.start()
ths.append(th1)
for _ in range(self.thread_num):
th = Thread(target=self.parse_second)
th.start()
ths.append(th)
for th in ths:
th.join()
s = json.dumps(self.data, ensure_ascii=False, indent=4)
with open('top_th1.json', 'w', encoding='utf-8') as f:
f.write(s)
print('Data crawling is finished.')
if __name__ == '__main__':
Spider().run()
复制代码
这里的整体思路和上一篇文章没有什么区别。分配两个队列,一个存储二级页面的URL,一个存储抓取到的数据。一级页面单独开一个线程,将二级页面URL不断填入队列中。解析二级页面URL时开启多个线程提高抓取速度。
除此之外,还需要说明一个地方
我们上一篇文章中,因为URL队列是事先产生的,而不是生产和消耗URL程序同时进行,因此队列一旦为空即结束爬虫。而这里的一级页面和二级页面的解析是同时进行的,也就是说二级页面URL是边生产边消耗的,这时我们就要保证
- 所有页面解析结束可以退出所有线程(如果只是单纯
while True
,URL列表为空时,消耗线程就会永远等下去) - 不会因为二级页面URL消耗太快而使队列提前为空,提早退出爬虫
对于第二点,这里定义了self.first_running
,它如果是True
,则表示一级页面还没运行完成。此时即使二级页面的URL队列已经空了,也要继续等待一级页面解析后产生新的二级页面URL。
另外,由于这次URL队列是典型的生产消费者模式,因此如果不想自己实现Condition锁的话,就用Queue来代替list。
读者也可以试着更改self.thread_num
看爬虫速度有什么改变。
欢迎关注我的知乎专栏
专栏主页:python编程
专栏目录:目录
版本说明:软件及包版本说明