今天是大年初一, 首先祝大家新年快乐
不知道大家过年都是怎么过的, 反正栏主是在家睡了一天, 醒来的时候登 QQ 发现有人找我要一份贴吧爬虫的源代码, 想起之前练手的时候写过一个抓取百度贴吧发帖记录中的邮箱与手机号的爬虫, 于是开源分享给大家学习与参考
需求分析:
本爬虫主要是对百度贴吧中各种帖子的内容进行抓取, 并且分析帖子内容将其中的手机号和邮箱地址抓取出来主要流程在代码注释中有详细解释
测试环境:
代码在 Windows7 64bit,python 2.7 64bit(安装 mysqldb 扩展)以及 centos 6.5,python 2.7(带 mysqldb 扩展)环境下测试通过
github:https://github.com/cw1997/get-email-by-tieba/blob/master/get-email-by-tieba-multithreading.py
环境准备:
工欲善其事必先利其器, 大家可以从截图看出我的环境是 Windows 7 + PyCharm 我的 Python 环境是 Python 2.7 64bit 这是比较适合新手使用的开发环境然后我再建议大家安装一个 easy_install, 听名字就知道这是一个安装器, 它是用来安装一些扩展包的, 比如说在 python 中如果我们要操作 mysql 数据库的话, python 原生是不支持的, 我们必须安装 mysqldb 包来让 python 可以操作 mysql 数据库, 如果有 easy_install 的话我们只需要一行命令就可以快速安装号 mysqldb 扩展包, 他就像 php 中的 composer,centos 中的 yum,Ubuntu 中的 apt-get 一样方便
相关工具可在我的 github 中找到: cw1997/python-tools, 其中 easy_install 的安装只需要在 python 命令行下运行那个 py 脚本然后稍等片刻即可, 他会自动加入 Windows 的环境变量, 在 Windows 命令行下如果输入 easy_install 有回显说明安装成功
环境选择的细节说明:
至于电脑硬件当然是越快越好, 内存起码 8G 起步, 因为爬虫本身需要大量存储和解析中间数据, 尤其是多线程爬虫, 在碰到抓取带有分页的列表和详情页, 并且抓取数据量很大的情况下使用 queue 队列分配抓取任务会非常占内存包括有的时候我们抓取的数据是使用 json, 如果使用 mongodb 等 nosql 数据库存储, 也会很占内存
网络连接建议使用有线网, 因为市面上一些劣质的无线路由器和普通的民用无线网卡在线程开的比较大的情况下会出现间歇性断网或者数据丢失, 掉包等情况, 这个我亲有体会
至于操作系统和 python 当然肯定是选择 64 位如果你使用的是 32 位的操作系统, 那么无法使用大内存如果你使用的是 32 位的 python, 可能在小规模抓取数据的时候感觉不出有什么问题, 但是当数据量变大的时候, 比如说某个列表, 队列, 字典里面存储了大量数据, 导致 python 的内存占用超过 2g 的时候会报内存溢出错误原因在我曾经 segmentfault 上提过的问题中依云的回答有解释(java python 只要占用内存达到 1.9G 之后 httplib 模块就开始报内存溢出错误 SegmentFault)
如果你准备使用 mysql 存储数据, 建议使用 mysql5.5 以后的版本, 因为 mysql5.5 版本支持 json 数据类型, 这样的话可以抛弃 mongodb 了(有人说 mysql 会比 mongodb 稳定一点, 这个我不确定)
至于现在 python 都已经出了 3.x 版本了, 为什么我这里还使用的是 python2.7? 我个人选择 2.7 版本的原因是自己当初很早以前买的 python 核心编程这本书是第二版的, 仍然以 2.7 为示例版本并且目前网上仍然有大量的教程资料是以 2.7 为版本讲解, 2.7 在某些方面与 3.x 还是有很大差别, 如果我们没有学过 2.7, 可能对于一些细微的语法差别不是很懂会导致我们理解上出现偏差, 或者看不懂 demo 代码而且现在还是有部分依赖包只兼容 2.7 版本我的建议是如果你是准备急着学 python 然后去公司工作, 并且公司没有老代码需要维护, 那么可以考虑直接上手 3.x, 如果你有比较充裕的时间, 并且没有很系统的大牛带, 只能依靠网上零零散散的博客文章来学习, 那么还是先学 2.7 在学 3.x, 毕竟学会了 2.7 之后 3.x 上手也很快
多线程爬虫涉及到的知识点:
其实对于任何软件项目而言, 我们凡是想知道编写这个项目需要什么知识点, 我们都可以观察一下这个项目的主要入口文件都导入了哪些包
现在来看一下我们这个项目, 作为一个刚接触 python 的人, 可能有一些包几乎都没有用过, 那么我们在本小节就来简单的说说这些包起什么作用, 要掌握他们分别会涉及到什么知识点, 这些知识点的关键词是什么这篇文章并不会花费长篇大论来从基础讲起, 因此我们要学会善用百度, 搜索这些知识点的关键词来自学下面就来一一分析一下这些知识点
HTTP 协议:
我们的爬虫抓取数据本质上就是不停的发起 http 请求, 获取 http 响应, 将其存入我们的电脑中了解 http 协议有助于我们在抓取数据的时候对一些能够加速抓取速度的参数能够精准的控制, 比如说 keep-alive 等
threading 模块(多线程):
我们平时编写的程序都是单线程程序, 我们写的代码都在主线程里面运行, 这个主线程又运行在 python 进程中关于线程和进程的解释可以参考阮一峰的博客: 进程与线程的一个简单解释 阮一峰的网络日志
在 python 中实现多线程是通过一个名字叫做 threading 的模块来实现之前还有 thread 模块, 但是 threading 对于线程的控制更强, 因此我们后来都改用 threading 来实现多线程编程了
关于 threading 多线程的一些用法, 我觉得这篇文章不错: 专题八. 多线程编程之 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:
稍微学过一点英语的人都能够猜出来这个模块用于处理时间, 在这个爬虫中我用它来获取当前时间戳, 然后通过在主线程末尾用当前时间戳减去程序开始运行时的时间戳, 得到程序的运行时间
如图所示, 开 50 个线程抓取 100 页 (每页 30 个帖子, 相当于抓取了 3000 个帖子) 贴吧帖子内容并且从中提取出手机邮箱这个步骤共耗时 330 秒
urllib 和 urllib2:
这两个模块都是用于处理一些 http 请求, 以及 url 格式化方面的事情我的爬虫 http 请求部分的核心代码就是使用这个模块完成的
MySQLdb:
这是一个第三方模块, 用于在 python 中操作 mysql 数据库
这里我们要注意一个细节问题: mysqldb 模块并不是线程安全版本, 意味着我们不能在多线程中共享同一个 mysql 连接句柄所以大家可以在我的代码中看到, 我在每个线程的构造函数中都传入了一个新的 mysql 连接句柄因此每个子线程只会用自己独立的 mysql 连接句柄
cmd_color_printers:
这也是一个第三方模块, 网上能够找到相关代码, 这个模块主要用于向命令行中输出彩色字符串比如说我们通常爬虫出现错误, 要输出红色的字体会比较显眼, 就要使用到这个模块
自动化爬虫的错误处理:
如果大家在网络质量不是很好的环境下使用该爬虫, 会发现有的时候会报如图所示的异常, 这是我为了偷懒并没有写各种异常处理的逻辑
通常情况下我们如果要编写高度自动化的爬虫, 那么就需要预料到我们的爬虫可能会遇到的所有异常情况, 针对这些异常情况做处理
比如说如图所示的错误, 我们就应该把当时正在处理的任务重新塞入任务队列, 否则我们就会出现遗漏信息的情况这也是爬虫编写的一个复杂点
总结:
其实多线程爬虫的编写也不复杂, 多看示例代码, 多自己动手尝试, 多去社区, 论坛交流, 很多经典的书上对多线程编程也有非常详细的解释这篇文章本质上主要还是一篇科普文章, 内容讲解的都不是很深入, 大家还需要课外自己多结合网上各种资料自己学习如果对代码中的逻辑有所不明白可以在评论区下提问, 有空我都会耐心解答
来源: http://www.jqhtml.com/11227.html