2018.06.01*****************************************************************************
author:wills
网络爬虫(web crawler),以前经常称之为网络蜘蛛(spider),是按照一定的规则自动浏览万维网并获取信息的机器人程序(或脚本),曾经被广泛的应用于互联网搜索引擎。使用过互联网和浏览器的人都知道,网页中除了供用户阅读的文字信息之外,还包含一些超链接。网络爬虫系统正是通过网页中的超链接信息不断获得网络上的其它页面。正因如此,网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游,所以才被形象的称为网络爬虫或者网络蜘蛛。
在理想的状态下,所有ICP(Internet Content Provider)都应该为自己的网站提供API接口来共享它们允许其他程序获取的数据,在这种情况下爬虫就不是必需品,国内比较有名的电商平台(如淘宝、京东等)、社交平台(如腾讯微博等)等网站都提供了自己的Open API,但是这类Open API通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言,及时的获取行业相关数据是企业生存的重要环节之一,然而大部分企业在行业数据方面的匮乏是其与生俱来的短板,合理的利用爬虫来获取数据并从中提取出有价值的信息是至关重要的。当然爬虫还有很多重要的应用领域,以下列举了其中的一部分:
1.1.1. 搜索引擎
1.1.2. 新闻聚合
1.1.3. 社交应用
1.1.4. 舆情监控
1.1.5. 行业数据
2.1.1. 网络爬虫领域目前还属于拓荒阶段,虽然互联网世界已经通过自己的游戏规则建立起一定的道德规范(Robots协议,全称是“网络爬虫排除标准”),但法律部分还在建立和完善中,也就是说,现在这个领域暂时还是灰色地带。
2.1.2. “法不禁止即为许可”,如果爬虫就像浏览器一样获取的是前端显示的数据(网页上的公开信息)而不是网站后台的私密敏感信息,就不太担心法律法规的约束,因为目前大数据产业链的发展速度远远超过了法律的完善程度。
2.1.3. 在爬取网站的时候,需要限制自己的爬虫遵守Robots协议,同时控制网络爬虫程序的抓取数据的速度;在使用数据的时候,必须要尊重网站的知识产权(从Web 2.0时代开始,虽然Web上的数据很多都是由用户提供的,但是网站平台是投入了运营成本的,当用户在注册和发布内容时,平台通常就已经获得了对数据的所有权、使用权和分发权)。如果违反了这些规定,在打官司的时候败诉几率相当高。
大多数网站都会定义robots.txt文件,下面以淘宝的robots.txt文件为例,看看该网站对爬虫有哪些限制。
User-agent: Baiduspider
Allow: /article
Allow: /oshtml
Disallow: /product/
Disallow: /
注意上面robots.txt第一段的最后一行,通过设置“Disallow: /”禁止百度爬虫访问除了“Allow”规定页面外的其他所有页面。因此当你在百度搜索“淘宝”的时候,搜索结果下方会出现:“由于该网站的robots.txt文件存在限制指令(限制搜索引擎抓取),系统无法提供该页面的内容描述”。百度作为一个搜索引擎,至少在表面上遵守了淘宝网的robots.txt协议,所以用户不能从百度上搜索到淘宝内部的产品信息。
我们在网页上看到的内容通常是浏览器执行HTML语言得到的结果,而HTTP就是传输HTML数据的协议。HTTP是构建于TCP(传输控制协议)之上应用级协议,它利用TCP提供的可靠的传输服务实现Web应用的数据交换。
设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法,也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。
3.2.1. Chrome Developer Tools
3.2.2. POSTMAN
3.2.3. HTTPie
3.2.4. BuiltWith:识别网站使用的技术
import builtwith
print(builtwith.parse('http://www.bootcss.com/'))
# {'web-servers': ['Nginx'], 'font-scripts': ['Font Awesome'], 'javascript-frameworks': ['Lo-dash', 'Underscore.js', 'Vue.js', 'Zepto', 'jQuery'], 'web-frameworks': ['Twitter Bootstrap']}
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
print(builtwith.parse('https://www.jianshu.com/'))
# {'web-servers': ['Tengine'], 'web-frameworks': ['Twitter Bootstrap', 'Ruby on Rails'], 'programming-languages': ['Ruby']}
3.2.5. python-whois:查询网站的所有者
import whois
whois.whois('baidu.com')
# {'domain_name': ['BAIDU.COM', 'baidu.com'],'registrar': 'MarkMonitor, Inc.', 'whois_server': 'whois.markmonitor.com', 'referral_url': None, }
3.2.6. robotparser:解析robots.txt的工具
from urllib import robotparser
parser = robotparser.RobotFileParser()
parser.set_url('https://www.taobao.com/robots.txt')
parser.read()
parser.can_fetch('Hellokitty', 'http://www.taobao.com/article')
# False
parser.can_fetch('Baiduspider', 'http://www.taobao.com/article')
# True
parser.can_fetch('Baiduspider', 'http://www.taobao.com/product')
# False
一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容,当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术,其中可能还包括调度器和后台管理程序(监控爬虫的工作状态以及检查数据抓取的结果)。
4.1.1 使用python原生的办法
from urllib.request import urlopen
sohu_html = urlopen('http://www.baidu.com').read().decode('utf-8')
4.1.2. 使用第三方库requests
1). GET请求和POST请求。
2). URL参数和请求头。
3). 复杂的POST请求(文件上传)。
4). 操作Cookie。
5). 设置代理服务器。
6). 超时设置。
import requests
url= 'http://www.baidu.com'
headers = {'user-agent': 'Baidusprider'} # URL参数和请求头
proxies = {'http': '61.135.217.7:80'} # 设置代理服务器
resp = requests.get(url, headers=headers, proxies=proxies)
说明:关于requests的详细用法可以参考它的官方文档。
from random import random
def retry(fn, retry_times=3, wait_secs=3):
def wrapper(*args, **kwargs):
"""包装器,让被包装的函数执行3次,每次至少相隔wait_secs = 3s
"""
for _ in range(retry_times):
try:
return fn(*args, **kwargs)
except Exception as e:
# 访问失败的避让时间
sleep((random() + 1) * wait_secs)
return None
return wrapper
@retry
def fetch(self, current_url, *, user_agent=None, proxies=None):
"""抓取页面
"""
headers = {'user-agent': user_agent} if user_agent else {}
resp = requests.get(current_url, headers=headers, proxies=proxies)
return decode_page(resp.content) if resp.status_code == 200 else None
# 实例1:
def get_html(url):
# 这里我们给url请求,伪装了一个请求头,表示我是一个浏览器(也可以伪装成移动端)
resp = requests.get(url,headers={
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'})
return resp
常见的伪装‘user-agent’:
常见的User Agent
1.Android
* Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19
* Mozilla/5.0 (Linux; U; Android 4.0.4; en-gb; GT-I9300 Build/IMM76D) * * * AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
* Mozilla/5.0 (Linux; U; Android 2.2; en-gb; GT-P1000 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
2.Firefox
* Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
* Mozilla/5.0 (Android; Mobile; rv:14.0) Gecko/14.0 Firefox/14.0
3.Google Chrome
* Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36
* Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19
4.iOS
* Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3
* Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A101a Safari/419.3
#实例2:
def get_html(url):
# 这里我们的url请求,伪装成百度的爬虫
resp = requests.get(url,headers={
'user-agent': 'Baiduspider'})
return resp
# 实例3:
def main():
这里我使用了代理的IP,隐藏了自己的真实ip
headers = {'user-agent': 'Baiduspider'}
# 隐藏自己的身份IP
proxies = {
# 代理IP 'http': '36.22.76.233:35390',
'http': '61.135.217.7:80'
}
base_url = 'https://www.zhihu.com'
seed_url = urljoin(base_url, 'explore')
resp = requests.get(seed_url, headers=headers, proxies=proxies)
def decode_page(page_bytes, charsets=('utf-8', 'gbk', 'gb2312')):
"""对已经获取的页面进行解码,默认尝试用三种不同的编码进行解码
"""
html_page = ''
for charset in charsets:
try:
html_page = page_bytes.decode(charset)
# 如果解码成功跳出循环
break
except Exception as e:
pass
return html_page
from re import findall
# html_page是解码后的页面
link_regex = re.compile(r']+test=a\s[^>]*href=["\'](\S*)[\']{1}', re.IGNORECASE)
link_list = findall(link_regex, html_page)
# 使用re.findall()返回的结果是一个列表
for link in link_list:
redis_cli.rpush('link_list', link)
4.7.1 使用redis数据库持久化数据
import redis
# 创建redis连接对象
redis_cli = redis.Redis(host='47.106.185.12', port='6379', password='123456')
for link in link_list:
redis_cli.rpush('link_list', link)
redis_cli插入数据的办法和redis数据库的操作方法基本一样,详情见官方文档
4.7.2 使用mongodb数据库持久化数据
import pymongo
# 创建redis连接对象
mongo_cli = pymongo.MongoClient(host='96.106.188.164', port=27017)
# 创建数据库 database_motto 创建集合 mottos
db = mongo_obj.database_motto.mottos
for link in link_list:
db.insert_one({'_id': 1, 'url_list': link})
# 也可以同时插入多个数据
db.insert_many([
{'_id': 11, 'url_list': 'http://www.xxx.com'},
{'_id': 22, 'url_list': 'http://www.yyy.com'},
...
])
4.7.3 使用mysql数据库持久化数据
使用mysql时需要先在数据库创建对应的表
drop database if exists tb_carwler;
create database tb_crawler default charset utf8;
use database crawler;
-- blob - longblob 二进制大对象
-- clob - longtext 字符大对象
-- fastDFS 分布式文件存储系统
-- engine=innodb 引擎
create table tb_crawler
(
resultid integer not null auto_increment,
rurl varchar(128) not null,
rpage longtext not null,
rdate timestamp default now(),
PRIMARY key(resultid)
);
import re
import pymysql
import hashlib
def main():
...
# 创建一个mysql的连接对象connect,db表示数据库的名称
connect = pymysql.connect(user='root', host='localhost',
db='crawler', passwd='123456',
port=3306, charset='utf8mb4')
...
try:
for link in link_list:
if link not in visited_list:
# 获取页面
html1 = get_page_code(link, 2)
# 获取页面中需要的信息
title = findall(title_regex, html1)[0]
# 这里%s不表示字符串,表示占位符
with connect.cursor() as cursor:
cursor.execute('insert into tb_crawler (rurl, rpage)values(%s, %s)', (title, link))
connect.commit()
finally:
connect.close()
# 上面的办法每循环一次就要对数据库进行一次访问,性能低下,可以把需要插入的信息处理完成,一次性插入数据库
try:
with conn.cursor() as cursor:
param_list = []
for link in links_list:
param_list.append((args1, link))
cursor.executemany('insert into tb_result values (default, %s, %s)',
param_list)
conn.commit()
except Error:
pass
finally:
conn.close()
print('执行完成')
序列化 / 反序列化
pickle / json / shelve
pickle: --
json: dump / dumps(序列化) || load / loads(反序列化)
序列化 - 把对象变成字符,或者字节序列
反序列化 - 把字节或者字符序列还原为对象
import zlib, pickle, hashlib
# 数据摘要
hasher_proto = hashlib.md5()
hasher = hasher_proto.copy()
hasher.update(link.encode('utf-8'))
link_digest = hasher.hexdigest()
# zlib: 压缩 compress/解压缩decompress
zipped_title = zlib.compress(pickle.dumps(headings[0]))
# 解压缩,反序列化
aipped_title = zlib.decompress(pickle.loads(headings[0]))
总结
1. 下载数据,获取页面的方式 - urllib / requests / aiohttp。
2. 解析数据 - re / lxml / beautifulsoup4(bs4)/ pyquery。
3. 持久化(数据最好序列化压缩) - pymysql / redis(高速缓存) / sqlalchemy / peewee / pymongo(结构不严谨的廉价数据,但是数据可能非常庞大)。
4. 调度器 - 进程 / 线程 / 协程。
SQL数据库中事务的隔离级别:
1. UNcommitted read 读未提交,并发性好 -但是会出现读脏数据Dirty Read
2. read Committed - 可以避免脏读,但是不可以重复读, - UNrepeatable read
3. repeatable read 可以重复读 – 但是会出现Phantom Read 幻读
4. serializable -串行,没有并发了
永久更改事务隔离级别: set global transaction isolation level read committed;
临时更改事务隔离级别: set session transaction isolation level serializable;
为什么使用非关系型数据库 redis/MongoDB
CAP理论
对于分布式计算系统,不可能同时满足一下三点:
1. 一致性(consistency)(所有节点在同一时间具有相同的数据)
2. 可用性(availability)(保证每个请求不管成功还是失败都有响应)
3. 分割容忍(Partitiontolerance)(系统中任意信息的丢失或者失败不会影响系统继续运行)
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
* CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
* CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
* AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
有的网页需要进行证书验证,我们的requests请求已经自带了很多证书,但是有的网页还是不行,因此需要导入ssl模块,创建一个默认的上下文content,并且将其置为unverified,即不需要验证
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
抓取方法 | 速度 | 使用难度 | 备注 |
---|---|---|---|
正则表达式 | 快 | 困难 | 常用正则表达式 在线正则表达式测试 |
lxml | 快 | 一般 | 需要安装C语言依赖库 唯一支持XML的解析器 |
Beautiful | 快/慢(取决于解析器) | 简单 | |
PyQuery | 较快 | 简单 | Python版的jQuery |
说明:Beautiful的解析器包括:Python标准库(html.parser)、lxml的HTML解析器、lxml的XML解析器和html5lib。
soup = BeautifulSoup(html, 'lxml')
from bs4 import BeautifulSoup
import re
def main():
# 这里为了方便演示,直接获取了一个html的页面源码,并且赋值给html
html = """
首页
Hello, world!
这是一个神奇的网站!
腾讯网
姓名
上场时间
得分
篮板
助攻
"""
# 通过BeautifulSoup构建一个DOM对象,(可通过参数指定解析器,解析器优劣见上文)
# 之后我们就可以对soup对象进行各种寻根问祖的节点操作
soup = BeautifulSoup(html, 'lxml')
# 类似于JavaScript中 - document.title
# 遍历文档树的办法
print(soup.title) # 可以直接获取title标签
# 首页
# 类似于JavaScript中 - document.body.h1
print(soup.p) # 获取第一个p标签,只能获取一个
# 这是一个神奇的网站!
print(soup.body.p.text) # 获取body标签下p标签的内容text
# 这是一个神奇的网站!
print(soup.body.p.contents) # 获取body标签下p标签的所有直接子节点并且以列表形式返回
# ['这是一个', 神奇, '的网站!']
# .children方法获取了p标签的所有直接子节点
for p_child in soup.body.p.children:
print(p_child)
# .contents与.children方法类似,只是.contents返回列表,.children返回一个生成器,需要进行遍历才能拿到相应的数据。.descendants则拿到的是所有的子孙节点
print(len([elem for elem in soup.body.children])) # 获取body下所有直接子节点数量
# 19
print(len([elem for elem in soup.body.descendants]))# 获取body下所有的子节点的数量
# 65
print([elem for elem in soup.body.descendants]) # 获取body下所有的子节点
####
# 使用find与find_all()函数的办法
# --正则表达式--
# findAll与find_all都是相同的函数,只是写法不同,find函数找到的是一个对象,
print(soup.findAll(re.compile(r'^h[1-6]'))) # 获取所有的h1-h6的标签
print(soup.body.find_all(r'^h')) # 获取body标签下所有以h打头的标签
print(soup.body.div.find_all(re.compile(r'^h'))) # 获取body标签下第一个div标签中所有的h打头的标签
print(soup.find_all(re.compile(r'r$'))) # 获取所有以r结尾的标签
# 获取所有标签为‘img’,‘src’属性符合正则表达式(r'\./img/\w+.png')的标签
print(soup.find_all('img', {'src':re.compile(r'\./img/\w+.png')}))
# --函数/lambda--
# 获取有两个属性attrs的所有标签
print(soup.find_all(lambda x: len(x.attrs) == 2))
# 获取所有的foo标签
print(soup.find_all(foo))
# 获取所有class=foo的p标签
print(soup.find_all('p', {'class': 'foo'}))
# 获取所有的a标签的href属性,并对其进行遍历
for elem in soup.select('a[href]'):
print(elem.attrs['href'])
# 获取class=”hello1“的div 下的所有a标签 的所有p标签
print(soup.select('div[class="hello1"] a p'))
# [疑似地上霜
, 低头思故乡
]
# 获取class=”hello1“的div, 结果是一个列表【***】
print(soup.select('div[class="hello1"]'))
# 获取class=”hello1“的div 下的所有a标签
print(soup.select('div[class="hello1"] a'))
# [低头思故乡
]
# 获取class=”hello1“的div 下的所有a标签
print(soup.select_one('div[class="hello1"] a'))
# 低头思故乡
def foo(elem):
return len(elem.attrs) == 2
if __name__ == '__main__':
main()
说明:更多内容可以参考BeautifulSoup的官方文档。
下面是一个面向对象爬虫的代码,这里使用了多线程模式
from enum import Enum, unique
from threading import Thread
@unique
class SpiderStatus(Enum):
"""枚举爬虫的状态
"""
IDLE = 0
WORKING = 1
class Spider(object):
"""爬虫类
"""
def __init__(self):
self.status = SpiderStatus.IDLE
def fetch(self, current_url):
"""抓取页面
"""
pass
def parse(self, html_page):
"""解析页面
"""
pass
def store(self, data_dict):
"""保存数据
"""
pass
def extract(self, html_page):
"""抽取需要的数据
"""
pass
class SpiderThread(Thread):
def __init__(self, spider, tasks):
super().__init__(daemon=True)
self.spider = spider
self.tasks = tasks
def run(self):
while True:
pass
def main():
pass
if __name__ == '__main__':
main()