爬虫基本步骤:
1.定义全局变量
redis_client = redis.Redis(host='*******',
port=***, password='*****') # 链接redis数据库,存放任务队列和已完成任务
mongo_client = pymongo.MongoClient(host='*****', port=27017) # 连接mongdb数据库
db = mongo_client.msohu
sohu_data_coll = db.webpages
hasher_proto = sha1() # 引入哈希摘要
2.定义一个常量来判断爬虫是否在工作
@unique # 装饰器,限定惟一值
class SpiderStatus(Enum):
'''
枚举,定义常量,作用是用包装器定义状态是唯一的
'''
IDLE = 0
WORKING = 1
这里 0表示爬虫没有工作了
3.写一个通用的解码方法,注意不是每个网页都是utf8编码格式,所以这里传参要传一个元组
def decode_page(page_bytes, charsets=('utf-8',)):
page_html = None
for charset in charsets:
try:
page_html = page_bytes.decode(charset)
break
except UnicodeDecodeError:
pass
return page_html
4.定义一个爬虫类,并写他的行为和属性
class Spider(object):
def __init__(self):
self.status = SpiderStatus.IDLE # 定义初识状态
@Retry() # 这里用到类装饰器,当爬虫获取数据失败时有可能是网络故障,所以多给几次尝试的机会,但是又不想更改爬虫自身的属性,所以我定义一个装饰器来装饰爬虫的行为
def fetch(self, current_url, *, charsets=('utf-8', ),
user_agent=None, proxies=None): # 定义爬虫抓取当前网页的方法
thread_name = current_thread().name # 拿到当前线程的名字,current_url是thread里面的一个方法,.name就是获取name
print(f'[{thread_name}]: {current_url}') # f'{}'是python新版的粘结字符串的方法,和以前的 '%S%S' % (a,b)一个意思
headers = {'user-agent': user_agent} if user_agent else {}# 伪装成别的被承认的spider,比如Baiduspider
resp = requests.get(current_url,
headers=headers, proxies=proxies) # 拿到网页的数据
return decode_page(resp.content, charsets) \
if resp.status_code == 200 else None
# 分析爬取的网页
def parse(self, html_page, *, domain='m.sohu.com'):
# domain表示需要传入的网址参数,更好的是将这个参数提出来,降低耦合
soup = BeautifulSoup(html_page, 'lxml')
# BeatifulSoup解析页面,并按照指定的格式输出,lxml是我们目前很好的选择
for a_tag in soup.body.select('a[href]'): # 拿到所有有href的a标签
parse = urlparse(a_tag.attrs['href']) # urlparse可以将拿到的url分段
netloc = parse.netloc or domain # 拿到关键域名的分段,这里就是 m.sohu.com
scheme = parse.scheme or 'http' # 拿出url的协议的内容
if scheme != 'javascript' and netloc == domain: # 因为之前拿出来有的参数带有javascript,所以在这里将包含这个的数据去掉
path = parse.path # 拿出url的的路径
query = '?' + parse.query if parse.query else ''
# 拿出url的的传入的参数
full_url = f'{scheme}://{netloc}{path}{query}' # 完成网页的拼接,那么为什么要这样做呢,因为这个网站的有的网页是隐藏了协议的,有的网页传入了参数等等,总之要拿到含有netloc的网址
if not redis_client.sismember('visited_urls', full_url):
# 我们将爬取的任务队列放在非关系型数据库redis中,因为我们在爬取数据时如果需要对进程中断,那就下次就要重新爬取已经获取的数据,把任务队列放在redis中,下次操作时如果redis里面保存有将要爬取的数据的信息就不会再次爬取。这里判断如果拼接的网址不在已经爬取的数据库内才进行爬取。
redis_client.rpush('m_sohu_task', full_url)
# rpush是redis中添加数据的一种方法,redis采用先进先出的数据结构,从右边进就要从左边拿,rpush对应lpop,lpush对应rpop
5.定义完爬虫之后还要定义爬虫类里面的retry类装饰器
有关装饰器在另一篇有详细介绍
class Retry(object):
def __init__(self, *, retry_times=3,
wait_secs=5, errors=(Exception, )):
self.retry_times = retry_times
self.wait_secs = wait_secs
self.errors = errors
def __call__(self, fn): # 类装饰器的魔法方法,调用这个类装饰器其实就是调用这个方法
def wrapper(*args, **kwargs):
for _ in range(self.retry_times):
try:
return fn(*args, **kwargs)
except self.errors as e:
logging.info('[Retry]')
sleep((random() + 1) * self.wait_secs + 1)
# 这里如果失败就停歇一段时间再次调用
return None
return wrapper # 返回的也是一个函数
6.定义多线程
class SpiderThread(Thread):
"""
定义线程类
"""
def __init__(self, name, spider):
super().__init__(name=name, daemon=True) # deamon = True将程序设为守护线程,主程序结束后也跟着结束,name是线程名,也可以不写,这里是为了方便查看程序开始时是否启用了多线程
self.spider = spider
def run(self):
while True:
current_url = redis_client.lpop('m_sohu_task')
# 拿到第一个,锁机制(一次只能有一个操作),用put在最后加一个
while not current_url:
current_url = redis_client.lpop('m_sohu_task')
# 这里这样写是确保必须拿到'm_sohu_task'里面的第一个元素,否则程序就不往下执行
self.spider.status = SpiderStatus.WORKING # 程序启起来后爬虫的状态改变
current_url = current_url.decode('utf-8') # 解码
if not redis_client.sismember('visited_urls', current_url):
# 这个语句是判断当前网页不在已经访问过的库中
redis_client.sadd('visited_urls', current_url) # 如果当前网页不在已经访问的库中就加进去,这里的sadd是redis语句
html_page = self.spider.fetch(current_url) # 抓取页面
if html_page not in [None, '']: # 如果页面不为空
hasher = hasher_proto.copy()
# 这里是引入生成哈希摘要,为什么不在这里直接生成呢?因为复制比生成快得多,节约计算机性能
hasher.update(current_url.encode('utf-8'))
# 传入urlbong生成哈希摘要
doc_id = hasher.hexdigest()
# 生成一个doc_id的摘要数据
if not sohu_data_coll.find_one({'_id': doc_id}):
# 如果刚才生成的数据不在MongoDB数据库中那么就进行插入数据行为
sohu_data_coll.insert_one({
'_id': doc_id,
'url': current_url,
'page': Binary(zlib.compress(pickle.dumps(html_page))) # 这个操作时对网页的数据进行压缩
})
self.spider.parse(html_page)
# 如果页面不在redis中保存的库中就继续执行解析操作,因为这是一个while Ttue循环
self.spider.status = SpiderStatus.IDLE
# 更改爬虫状态
7.判断状态
def is_any_alive(spider_threads):
return any([spider_thread.spider.status == SpiderStatus.WORKING
for spider_thread in spider_threads])
visited_urls = set()
8.执行主程序
def main():
if not redis_client.exists('m_sohu_task'):
redis_client.rpush('m_sohu_task', 'http://m.sohu.com/') # 如果任务队列里面没有目标网址就将其加入到任务队列中
spider_threads = [SpiderThread('thread-%d' % i, Spider())
for i in range(10)] # 这里启十个多线程
for spider_thread in spider_threads:
spider_thread.start() # 启动多线程
while redis_client.exists('m_sohu_task') or is_any_alive(spider_threads):
pass
"""
只要队列不为空或者还有爬虫在工作就不停止
"""
print('Over!')
if __name__ == '__main__':
main()