使用python编写多线程爬虫抓取百度贴吧邮箱与手机号

不知道大家过年都是怎么过的,反正栏主是在家睡了一天,醒来的时候登qq发现有人找我要一份贴吧爬虫的源代码,想起之前练手的时候写过一个抓取百度贴吧发帖记录中的邮箱与手机号的爬虫,于是开源分享给大家学习与参考。

需求分析:

本爬虫主要是对百度贴吧中各种帖子的内容进行抓取,并且分析帖子内容将其中的手机号和邮箱地址抓取出来。主要流程在代码注释中有详细解释。

测试环境:

代码在windows7 64bit,python 2.7 64bit(安装mysqldb扩展)以及centos 6.5,python 2.7(带mysqldb扩展)环境下测试通过

现在来看一下我们这个项目,作为一个刚接触python的人,可能有一些包几乎都没有用过,那么我们在本小节就来简单的说说这些包起什么作用,要掌握他们分别会涉及到什么知识点,这些知识点的关键词是什么。这篇文章并不会花费长篇大论来从基础讲起,因此我们要学会善用百度,搜索这些知识点的关键词来自学。下面就来一一分析一下这些知识点。

http协议:

我们的爬虫抓取数据本质上就是不停的发起http请求,获取http响应,将其存入我们的电脑中。了解http协议有助于我们在抓取数据的时候对一些能够加速抓取速度的参数能够精准的控制,比如说keep-alive等。

threading模块(多线程):

我们平时编写的程序都是单线程程序,我们写的代码都在主线程里面运行,这个主线程又运行在python进程中。关于线程和进程的解释可以参考阮一峰的博客:进程与线程的一个简单解释 – 阮一峰的网络日志

在python中实现多线程是通过一个名字叫做threading的模块来实现。之前还有thread模块,但是threading对于线程的控制更强,因此我们后来都改用threading来实现多线程编程了。

关于threading多线程的一些用法,我觉得这篇文章不错:[python] 专题八.多线程编程之thread和threading
大家可以参考参考。

简单来说,使用threading模块编写多线程程序,就是先自己定义一个类,然后这个类要继承threading.thread,并且把每个线程要做的工作代码写到一个类的run方法中,当然如果线程本身在创建的时候如果要做一些初始化工作,那么就要在他的__init__方法中编写好初始化工作所要执行的代码,这个方法就像php,java中的构造方法一样。

这里还要额外讲的一点就是线程安全这个概念。通常情况下我们单线程情况下每个时刻只有一个线程在对资源(文件,变量)操作,所以不可能会出现冲突。但是当多线程的情况下,可能会出现同一个时刻两个线程在操作同一个资源,导致资源损坏,所以我们需要一种机制来解决这种冲突带来的破坏,通常有加锁等操作,比如说mysql数据库的innodb表引擎有行级锁等,文件操作有读取锁等等,这些都是他们的程序底层帮我们完成了。所以我们通常只要知道那些操作,或者那些程序对于线程安全问题做了处理,然后就可以在多线程编程中去使用它们了。而这种考虑到线程安全问题的程序一般就叫做“线程安全版本”,比如说php就有ts版本,这个ts就是thread safety线程安全的意思。下面我们要讲到的queue模块就是一种线程安全的队列数据结构,所以我们可以放心的在多线程编程中使用它。

最后我们就要来讲讲至关重要的线程阻塞这个概念了。当我们详细学习完threading模块之后,大概就知道如何创建和启动线程了。但是如果我们把线程创建好了,然后调用了start方法,那么我们会发现好像整个程序立马就结束了,这是怎么回事呢?其实这是因为我们在主线程中只有负责启动子线程的代码,也就意味着主线程只有启动子线程的功能,至于子线程执行的那些代码,他们本质上只是写在类里面的一个方法,并没在主线程里面真正去执行他,所以主线程启动完子线程之后他的本职工作就已经全部完成了,已经光荣退场了。既然主线程都退场了,那么python进程就跟着结束了,那么其他线程也就没有内存空间继续执行了。所以我们应该是要让主线程大哥等到所有的子线程小弟全部执行完毕再光荣退场,那么在线程对象中有什么方法能够把主线程卡住呢?thread.sleep嘛?这确实是个办法,但是究竟应该让主线程sleep多久呢?我们并不能准确知道执行完一个任务要多久时间,肯定不能用这个办法。所以我们这个时候应该上网查询一下有什么办法能够让子线程“卡住”主线程呢?“卡住”这个词好像太粗鄙了,其实说专业一点,应该叫做“阻塞”,所以我们可以查询“python 子线程阻塞主线程”,如果我们会正确使用搜索引擎的话,应该会查到一个方法叫做join(),没错,这个join()方法就是子线程用于阻塞主线程的方法,当子线程还未执行完毕的时候,主线程运行到含有join()方法的这一行就会卡在那里,直到所有线程都执行完毕才会执行join()方法后面的代码。

queue模块(队列):

假设有一个这样的场景,我们需要抓取一个人的博客,我们知道这个人的博客有两个页面,一个list.php页面显示的是此博客的所有文章链接,还有一个view.php页面显示的是一篇文章的具体内容。

如果我们要把这个人的博客里面所有文章内容抓取下来,编写单线程爬虫的思路是:先用正则表达式把这个list.php页面的所有链接a标签的href属性抓取下来,存入一个名字叫做article_list的数组(在python中不叫数组,叫做list,中文名列表),然后再用一个for循环遍历这个article_list数组,用各种抓取网页内容的函数把内容抓取下来然后存入数据库。

如果我们要编写一个多线程爬虫来完成这个任务的话,就假设我们的程序用10个线程把,那么我们就要想办法把之前抓取的article_list平均分成10份,分别把每一份分配给其中一个子线程。

但是问题来了,如果我们的article_list数组长度不是10的倍数,也就是文章数量并不是10的整数倍,那么最后一个线程就会比别的线程少分配到一些任务,那么它将会更快的结束。

如果仅仅是抓取这种只有几千字的博客文章这看似没什么问题,但是如果我们一个任务(不一定是抓取网页的任务,有可能是数学计算,或者图形渲染等等耗时任务)的运行时间很长,那么这将造成极大地资源和时间浪费。我们多线程的目的就是尽可能的利用一切计算资源并且计算时间,所以我们要想办法让任务能够更加科学合理的分配。

并且我还要考虑一种情况,就是文章数量很大的情况下,我们要既能快速抓取到文章内容,又能尽快的看到我们已经抓取到的内容,这种需求在很多cms采集站上经常会体现出来。

比如说我们现在要抓取的目标博客,有几千万篇文章,通常这种情况下博客都会做分页处理,那么我们如果按照上面的传统思路先抓取完list.php的所有页面起码就要几个小时甚至几天,老板如果希望你能够尽快显示出抓取内容,并且尽快将已经抓取到的内容展现到我们的cms采集站上,那么我们就要实现一边抓取list.php并且把已经抓取到的数据丢入一个article_list数组,一边用另一个线程从article_list数组中提取已经抓取到的文章url地址,然后这个线程再去对应的url地址中用正则表达式取到博客文章内容。如何实现这个功能呢?

我们就需要同时开启两类线程,一类线程专门负责抓取list.php中的url然后丢入article_list数组,另外一类线程专门负责从article_list中提取出url然后从对应的view.php页面中抓取出对应的博客内容。

但是我们是否还记得前面提到过线程安全这个概念?前一类线程一边往article_list数组中写入数据,另外那一类的线程从article_list中读取数据并且删除已经读取完毕的数据。但是python中list并不是线程安全版本的数据结构,因此这样操作会导致不可预料的错误。所以我们可以尝试使用一个更加方便且线程安全的数据结构,这就是我们的子标题中所提到的queue队列数据结构。

同样queue也有一个join()方法,这个join()方法其实和上一个小节所讲到的threading中join()方法差不多,只不过在queue中,join()的阻塞条件是当队列不为空空的时候才阻塞,否则继续执行join()后面的代码。在这个爬虫中我便使用了这种方法来阻塞主线程而不是直接通过线程的join方式来阻塞主线程,这样的好处是可以不用写一个死循环来判断当前任务队列中是否还有未执行完的任务,让程序运行更加高效,也让代码更加优雅。

还有一个细节就是在python2.7中队列模块的名字是queue,而在python3.x中已经改名为queue,就是首字母大小写的区别,大家如果是复制网上的代码,要记得这个小区别。

getopt模块:

如果大家学过c语言的话,对这个模块应该会很熟悉,他就是一个负责从命令行中的命令里面提取出附带参数的模块。比如说我们通常在命令行中操作mysql数据库,就是输入mysql -h127.0.0.1 -uroot -p,其中mysql后面的“-h127.0.0.1 -uroot -p”就是可以获取的参数部分。

我们平时在编写爬虫的时候,有一些参数是需要用户自己手动输入的,比如说mysql的主机ip,用户名密码等等。为了让我们的程序更加友好通用,有一些配置项是不需要硬编码在代码里面,而是在执行他的时候我们动态传入,结合getopt模块我们就可以实现这个功能。

hashlib(哈希):

哈希本质上就是一类数学算法的集合,这种数学算法有个特性就是你给定一个参数,他能够输出另外一个结果,虽然这个结果很短,但是他可以近似认为是独一无二的。比如说我们平时听过的md5,sha-1等等,他们都属于哈希算法。他们可以把一些文件,文字经过一系列的数学运算之后变成短短不到一百位的一段数字英文混合的字符串。

python中的hashlib模块就为我们封装好了这些数学运算函数,我们只需要简单的调用它就可以完成哈希运算。

为什么在我这个爬虫中用到了这个包呢?因为在一些接口请求中,服务器需要带上一些校验码,保证接口请求的数据没有被篡改或者丢失,这些校验码一般都是hash算法,所以我们需要用到这个模块来完成这种运算。

json:

很多时候我们抓取到的数据不是html,而是一些json数据,json本质上只是一段含有键值对的字符串,如果我们需要提取出其中特定的字符串,那么我们需要json这个模块来将这个json字符串转换为dict类型方便我们操作。

re(正则表达式):

有的时候我们抓取到了一些网页内容,但是我们需要将网页中的一些特定格式的内容提取出来,比如说电子邮箱的格式一般都是前面几位英文数字字母加一个@符号加http://xxx.xxx的域名,而要像计算机语言描述这种格式,我们可以使用一种叫做正则表达式的表达式来表达出这种格式,并且让计算机自动从一大段字符串中将符合这种特定格式的文字匹配出来。

sys:

这个模块主要用于处理一些系统方面的事情,在这个爬虫中我用他来解决输出编码问题。

time:

稍微学过一点英语的人都能够猜出来这个模块用于处理时间,在这个爬虫中我用它来获取当前时间戳,然后通过在主线程末尾用当前时间戳减去程序开始运行时的时间戳,得到程序的运行时间。

如果大家在网络质量不是很好的环境下使用该爬虫,会发现有的时候会报如图所示的异常,这是我为了偷懒并没有写各种异常处理的逻辑。

通常情况下我们如果要编写高度自动化的爬虫,那么就需要预料到我们的爬虫可能会遇到的所有异常情况,针对这些异常情况做处理。

比如说如图所示的错误,我们就应该把当时正在处理的任务重新塞入任务队列,否则我们就会出现遗漏信息的情况。这也是爬虫编写的一个复杂点。

总结:

其实多线程爬虫的编写也不复杂,多看示例代码,多自己动手尝试,多去社区,论坛交流,很多经典的书上对多线程编程也有非常详细的解释。这篇文章本质上主要还是一篇科普文章,内容讲解的都不是很深入,大家还需要课外自己多结合网上各种资料自己学习。

以上就是使用 python 编写多线程爬虫抓取百度贴吧邮箱与手机号 的详细内容,更多请关注 第一php社区 其它相关文章!

Posted in 未分类

发表评论