网络爬虫的基本工做流程以下:python
首先选取一部分精心挑选的种子 URLgit
将种子 URL 加入任务队列github
从待抓取 URL 队列中取出待抓取的 URL,解析 DNS,而且获得主机的 ip,并将 URL 对应的网页下载下来,存储进已下载网页库中。此外,将这些 URL 放进已抓取 URL 队列。sql
分析已抓取 URL 队列中的 URL,分析其中的其余 URL,而且将 URL 放入待抓取 URL 队列,从而进入下一个循环。数据库
解析下载下来的网页,将须要的数据解析出来。api
数据持久话,保存至数据库中。浏览器
在爬虫系统中,待抓取 URL 队列是很重要的一部分。待抓取 URL 队列中的 URL 以什么样的顺序排列也是一个很重要的问题,由于这涉及到先抓取那个页面,后抓取哪一个页面。而决定这些 URL 排列顺序的方法,叫作抓取策略。下面重点介绍几种常见的抓取策略:微信
深度优先策略(DFS)
深度优先策略是指爬虫从某个 URL 开始,一个连接一个连接的爬取下去,直处处理完了某个连接所在的全部线路,才切换到其它的线路。
此时抓取顺序为:A -> B -> C -> D -> E -> F -> G -> H -> I -> Jcookie
广度优先策略(BFS)
宽度优先遍历策略的基本思路是,将新下载网页中发现的连接直接插入待抓取 URL 队列的末尾。也就是指网络爬虫会先抓取起始网页中连接的全部网页,而后再选择其中的一个连接网页,继续抓取在此网页中连接的全部网页。
此时抓取顺序为:A -> B -> E -> G -> H -> I -> C -> F -> J -> D网络
了解了爬虫的工做流程和爬取策略后,就能够动手实现一个爬虫了!那么在 python 里怎么实现呢?
requests 人性化的请求发送
Bloom Filter 布隆过滤器,用于判重
XPath 解析 HTML 内容
murmurhash
Anti crawler strategy 反爬虫策略
MySQL 用户数据存储
下面是一个伪代码
import Queue initial_page = "https://www.zhihu.com/people/gaoming623" url_queue = Queue.Queue() seen = set() seen.insert(initial_page) url_queue.put(initial_page) while(True): #一直进行 if url_queue.size()>0: current_url = url_queue.get() #拿出队例中第一个的 url store(current_url) #把这个 url 表明的网页存储好 for next_url in extract_urls(current_url): #提取把这个 url 里链向的 url if next_url not in seen: seen.put(next_url) url_queue.put(next_url) else: break
若是你直接加工一下上面的代码直接运行的话,你须要很长的时间才能爬下整个知乎用户的信息,毕竟知乎有 6000 万月活跃用户。更别说 Google 这样的搜索引擎须要爬下全网的内容了。那么问题出如今哪里?
须要爬的网页实在太多太多了,而上面的代码太慢太慢了。设想全网有 N 个网站,那么分析一下判重的复杂度就是 N*log(N),由于全部网页要遍历一次,而每次判重用 set 的话须要 log(N) 的复杂度。OK,我知道 python 的 set 实现是 hash——不过这样仍是太慢了,至少内存使用效率不高。
一般的判重作法是怎样呢?Bloom Filter. 简单讲它仍然是一种 hash 的方法,可是它的特色是,它可使用固定的内存(不随 url 的数量而增加)以 O(1) 的效率断定 url 是否已经在 set 中。惋惜天下没有白吃的午饭,它的惟一问题在于,若是这个 url 不在 set 中,BF 能够 100%肯定这个 url 没有看过。可是若是这个 url 在 set 中,它会告诉你:这个 url 应该已经出现过,不过我有 2%的不肯定性。注意这里的不肯定性在你分配的内存足够大的时候,能够变得很小不多。
# bloom_filter.py BIT_SIZE = 5000000 class BloomFilter: def __init__(self): # Initialize bloom filter, set size and all bits to 0 bit_array = bitarray(BIT_SIZE) bit_array.setall(0) self.bit_array = bit_array def add(self, url): # Add a url, and set points in bitarray to 1 (Points count is equal to hash funcs count.) # Here use 7 hash functions. point_list = self.get_postions(url) for b in point_list: self.bit_array[b] = 1 def contains(self, url): # Check if a url is in a collection point_list = self.get_postions(url) result = True for b in point_list: result = result and self.bit_array[b] return result def get_postions(self, url): # Get points positions in bit vector. point1 = mmh3.hash(url, 41) % BIT_SIZE point2 = mmh3.hash(url, 42) % BIT_SIZE point3 = mmh3.hash(url, 43) % BIT_SIZE point4 = mmh3.hash(url, 44) % BIT_SIZE point5 = mmh3.hash(url, 45) % BIT_SIZE point6 = mmh3.hash(url, 46) % BIT_SIZE point7 = mmh3.hash(url, 47) % BIT_SIZE return [point1, point2, point3, point4, point5, point6, point7]
BF 详细的原理参考我以前写的文章: 布隆过滤器(Bloom Filter) 的原理和实现
用户有价值的信息包括用户名、简介、行业、院校、专业及在平台上活动的数据好比回答数、文章数、提问数、粉丝数等等。
用户信息存储的表结构以下:
CREATE DATABASE `zhihu_user` /*!40100 DEFAULT CHARACTER SET utf8 */; -- User base information table CREATE TABLE `t_user` ( `uid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `brief_info` varchar(400) COMMENT '我的简介', `industry` varchar(50) COMMENT '所处行业', `education` varchar(50) COMMENT '毕业院校', `major` varchar(50) COMMENT '主修专业', `answer_count` int(10) unsigned DEFAULT 0 COMMENT '回答数', `article_count` int(10) unsigned DEFAULT 0 COMMENT '文章数', `ask_question_count` int(10) unsigned DEFAULT 0 COMMENT '提问数', `collection_count` int(10) unsigned DEFAULT 0 COMMENT '收藏数', `follower_count` int(10) unsigned DEFAULT 0 COMMENT '被关注数', `followed_count` int(10) unsigned DEFAULT 0 COMMENT '关注数', `follow_live_count` int(10) unsigned DEFAULT 0 COMMENT '关注直播数', `follow_topic_count` int(10) unsigned DEFAULT 0 COMMENT '关注话题数', `follow_column_count` int(10) unsigned DEFAULT 0 COMMENT '关注专栏数', `follow_question_count` int(10) unsigned DEFAULT 0 COMMENT '关注问题数', `follow_collection_count` int(10) unsigned DEFAULT 0 COMMENT '关注收藏夹数', `gmt_create` datetime NOT NULL COMMENT '建立时间', `gmt_modify` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次编辑', PRIMARY KEY (`uid`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户基本信息表';
网页下载后经过 XPath 进行解析,提取用户各个维度的数据,最后保存到数据库中。
通常网站会从几个维度来反爬虫:用户请求的 Headers,用户行为,网站和数据加载的方式。从用户请求的 Headers 反爬虫是最多见的策略,不少网站都会对 Headers 的 User-Agent 进行检测,还有一部分网站会对 Referer 进行检测(一些资源网站的防盗链就是检测 Referer)。
若是遇到了这类反爬虫机制,能够直接在爬虫中添加 Headers,将浏览器的 User-Agent 复制到爬虫的 Headers 中;或者将 Referer 值修改成目标网站域名。对于检测 Headers 的反爬虫,在爬虫中修改或者添加 Headers 就能很好的绕过。
cookies = { "d_c0": "AECA7v-aPwqPTiIbemmIQ8abhJy7bdD2VgE=|1468847182", "login": "NzM5ZDc2M2JkYzYwNDZlOGJlYWQ1YmI4OTg5NDhmMTY=|1480901173|9c296f424b32f241d1471203244eaf30729420f0", "n_c": "1", "q_c1": "395b12e529e541cbb400e9718395e346|1479808003000|1468847182000", "l_cap_id": "NzI0MTQwZGY2NjQyNDQ1NThmYTY0MjJhYmU2NmExMGY=|1480901160|2e7a7faee3b3e8d0afb550e8e7b38d86c15a31bc", "d_c0": "AECA7v-aPwqPTiIbemmIQ8abhJy7bdD2VgE=|1468847182", "cap_id": "N2U1NmQwODQ1NjFiNGI2Yzg2YTE2NzJkOTU5N2E0NjI=|1480901160|fd59e2ed79faacc2be1010687d27dd559ec1552a" } headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.3", "Referer": "https://www.zhihu.com/" } r = requests.get(url, cookies = cookies, headers = headers)
还有一部分网站是经过检测用户行为,例如同一 IP 短期内屡次访问同一页面,或者同一帐户短期内屡次进行相同操做。
大多数网站都是前一种状况,对于这种状况,使用 IP 代理就能够解决。这样的代理 ip 爬虫常常会用到,最好本身准备一个。有了大量代理 ip 后能够每请求几回更换一个 ip,这在 requests 或者 urllib2 中很容易作到,这样就能很容易的绕过第一种反爬虫。目前知乎已经对爬虫作了限制,若是是单个 IP 的话,一段时间系统便会提示异常流量,没法继续爬取了。所以代理 IP 池很是关键。网上有个免费的代理 IP API: http://api.xicidaili.com/free2016.txt
import requests import random class Proxy: def __init__(self): self.cache_ip_list = [] # Get random ip from free proxy api url. def get_random_ip(self): if not len(self.cache_ip_list): api_url = 'http://api.xicidaili.com/free2016.txt' try: r = requests.get(api_url) ip_list = r.text.split('rn') self.cache_ip_list = ip_list except Exception as e: # Return null list when caught exception. # In this case, crawler will not use proxy ip. print e return {} proxy_ip = random.choice(self.cache_ip_list) proxies = {'http': 'http://' proxy_ip} return proxies
使用日志模块记录爬取日志和错误日志
分布式任务队列和分布式爬虫
爬虫源代码:zhihu-crawler 下载以后经过 pip 安装相关三方包后,运行$ python crawler.py 便可(喜欢的帮忙点个 star 哈,同时也方便看到后续功能的更新)
运行截图:
[1] Python 编写知乎爬虫实践
[2] zhihu-crawler
https://github.com/cpselvis/zhihu-crawler
[3] 如何构建一个分布式爬虫:实战篇
[4] Python爬虫开源项目代码,爬取微信、淘宝、豆瓣、知乎、新浪微博、QQ、去哪网等
https://mp.weixin.qq.com/s/IS9kNt_jqfbjtz48V1zMZA
[5] 什么车最适合跑滴滴——数据化思惟小记
[6] 爬虫平台的架构实现和框架的选型
https://mp.weixin.qq.com/s/hRKHpZFZWwPW8bN5VMWGkw
[7] puppeteer爬虫的奇妙之旅