Python虽然在爬虫领域占用十分重要的地位,但这时,经常有人会嘲讽到:Python这么慢的语言用来写爬虫一点都不香,Java、Go......哪个不比Python快啊!
虽然Python性能确实不好,但改善优化这方面,其实也是比较简单的。接下来笔者将使用一个常见的设计模式:生产者消费者模式,编写一个博客文章的爬虫。
思路
首先,我们需要先了解一下生产者消费者模式。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题的。简单来讲,就是使用一个线程安全的容器进行多线程的分工操作。也就是说,生产者消费者模式一定是一个异步模式。
关于线程安全的容器,在Python中第一个想到的就是队列,当然,使用redis的数据结构当作该容器也是可以的。
然后,先确定生产者以及消费者。生产者在该爬虫中的任务即请求页面并解析页面,生成url链接,放入到容器中;消费者则是对容器中的链接进行二次请求,提取博客内容以及保存。
本文中将使用redis作为容器,并使用queue作为页码队列。
依赖库
import os
import re
import random
import threading
import requests
import redis
import fake_useragent
from queue import Queue
from bs4 import BeautifulSoup
- os模块主要用于进行文件路径操作与文件保存
- re模块在本文中主要用于替换数据而不是解析页面,当然,也可以用来解析页面
- random模块为随机模块
- threading模块即多线程模块
- requests模块依然用于进行页面请求
- redis模块用于操作redis数据库
- fake_useragent模块用于自动生成user-agent,也可以直接复制浏览器的user-agent
- queue模块即阻塞队列
- bs4模块用于解析页面,提取数据,也可使用xpath语法或者正则
生产者类
首先,生产者顾名思义,是需要生产数据,也就是爬虫中的页面请求部分。
class Producer(threading.Thread):
"""生产者类"""
def __init__(self, cli, queue):
super().__init__()
self.cli = cli
self.queue = queue
def get_html(self):
"""
获取页面源代码
:return: response.text/None
"""
pass
def get_url(self, text):
"""
生产数据(url)
:param text: 页面源代码
:return: None
"""
pass
def run(self):
"""
生产者线程执行函数
:return: None
"""
pass
上述代码为该生产者类的一个简单框架,既然是一个多线程爬虫,自然要继承threading.Thread这个类,再init方法里面也要通过super实现父类的init方法。
在init方法里有两个属性,分别代表了1个redis对象和一个队列对象。redis对象很明显,是用来对redis进行读写的,而这个对列对象,则是1个页码池,出于线程安全考虑,使用了队列。
get_html这个方法比较简单,首先在页码池里面获取1个页码,然后使用requests进行http请求,返回请求页面的源代码,如果池中没有页码了,则返回None。
get_url这个方法需要在页面源代码中提取我们需要的信息,即文章链接。获取到的文章链接将保存在redis中的集合对象中,以免出现重复文章。
run方法是线程的执行方法,也就是该生产者类的”发动机“,详细代码如下。
def run(self):
"""
生产者线程执行函数
:return: None
"""
while True:
# redis集合中的数据大于1000时结束循环(没有实际意义,起到控制程序工作量的作用)
if self.cli.scard('jianshu:start_urls') > 1000:
break
else:
res = self.get_html()
if res is None:
continue
else:
self.get_url(res)
print(f'name:{threading.current_thread().name} is over!!!')
消费者类
消费者,自然要消费数据,当redis中开始出现文章链接时,消费者类就要大显身手了。
class Consumer(threading.Thread):
"""消费者类"""
def __init__(self, cli):
super().__init__()
self.cli = cli
def get_data(self):
"""
二次请求链接
:return: response.text
"""
pass
def analysis(self, text):
"""
获取页面中的需要保存的html
:param text: 页面源代码
:return: section
"""
pass
def write_in(self, section):
"""
将获取到的数据保存到本地
:param section: section标签中的内容
:return: None
"""
pass
def run(self):
"""
消费者线程执行函数
:return: None
"""
pass
在消费者类中,init方法不需要再传入页码了,消费者类的工作基本上都围绕在redis与页面解析上。
get_data方法类似生产者类中的get_html方法,即请求链接,返回页面源代码。
analysis方法则是需要进行页面解析,即获取文章页面的html内容。因为markdown是完全兼容html格式的,所以将获取到的html的文章内容部分保存为markdown,是可以还原的文章格式的。通过解析可看出,只需要保存< section >标签内的内容即可。即该方法返回< section >的soup对象。
write_in方法则是将获取到的html文本保存为markdown的格式,虽然描述起来简单,但这一步是最麻烦的。因为,我们需要去修改文章内的图片链接,将图片下载到本地,每篇文章都需要1个独立文件夹,再将图片链接替换为相对路径即可实现文章的保存。
关于run方法,如下所示。
def run(self):
"""
消费者线程执行函数
:return: None
"""
while True:
text = self.get_data()
if text is None:
continue
else:
section = self.analysis(text)
self.write_in(section)
# 如果redis集合中没有数据则退出循环
if self.cli.scard('jianshu:start_urls') == 0:
break
print('大功告成!!!')
需要注意的是,创建线程时,尽量要保证生产的线程多一点,否则上述代码的消费者if语句会直接退出程序,这个if语句也只是用来控制程序的,可随意增改。
结语
上述两个类的具体实现可以自由发挥,其实说到底,爬虫程序的主观性实在是太大了。。。关键一点在于,不要让别人的代码影响了你的思路,优秀的爬虫工程师往往都能第一时间想到解决问题的思路,然后一步步去解决思路里的麻烦。
爬虫程序并没有特么难的地方,往往都是一些”麻烦“在不断的困扰你,有时候劝退你的往往都是麻烦,这时候只要耐心就好了。
关于该爬虫的具体实现,可参考下面的Github链接。
https://github.com/macxin123/spider/blob/master/jianshu/threading_jianshu.py
由于最近工作比较忙,所以鸽了很久。。。上述的爬虫也已经完成很久了,所以在可用性上也许已经崩了,不过还是那句话,爬虫最重要的还是思路以及愿不愿意去解决麻烦。
爬虫界的最大的麻烦也许就是验证码了,下篇文章笔者将写一篇关于验证码与爬虫的案例,敬请期待!