日期:2016年10月05日
标题:HTTP请求,hash_table,bloomFilter和python并发编程
编号:2
一.HTTP协议
- HTTP请求
-
- 本质上说,一个HTTP请求起始于用户端向HTTP服务器发送一个URL请求,一个标准的HTTP请求由以下几个部分组成:
-
- <request-line> 请求行,用来说明请求类型,URL和HTTP版本
- <headers> 头部信息,用来说明服务器要使用的附加信息
- <CRLF> 回车换行符(/r/n),用于表明头部信息的结束
- [<request-body><CRLF>] 主题数据,可不添加
- HTTP的数据交互方式
-
- GET(查):用于获取信息,不会修改服务器上的数据
- POST(改):向服务器发送修改请求,从而修改服务器。例如在论坛上回帖,在博客上评论,也可以仅用来获取数据
- DELETE(删):可以通过GET/POST等价实现
- PUT(加):增加,放置数据,也可以通过GET/POST来实现
- GET和POST的区别: GET请求的数据是放在URL之后的,例如login.action?name=hyddd&password=idontknow&verify=%E4%BD%E5%A5%BD(a.以?来分割 URL 和 GET 的数据 b.不同数据之间通过&连接 c.遇到 中文使用 BASE64 编码) 而POST的数据放在HTTP的正文之中,GET能提交的数据要比POST少,且GET的数据是明文传输的,安全性低
- 利用python来模拟HTTP请求
-
- cookielib:cookielib模块的主要作用是提供可存储cookie的对象,以便于与urllib2模块配合使用来访问Internet资源。还可以用来处理包含cookie数据的文件。例如可以利用本模块的CookieJar类的对象来捕获cookie并在后续连接请求时重新发送。
- urllib2模块的 OpenerDirector 操作类。这是一个管理很多处理类(Handler)的类。而所有这些 Handler 类都对应处理相应的协议,或者特殊功能。
- cookielib模块一般与urllib2模块配合使用,主要用在urllib2.build_oper()函数中作为urllib2.HTTPCookieProcessor()的参数。
import urllib,urllib2,cookielib //需要使用cookielib来存储发送给服务器的信息
from bs4 import BeautifulSoup
def bbs_set(id,pw,text):
cj = cookielib.CookieJar(); //创建一个存储cookie的cookiejar对象
//urllib2.urlopen()函数不支持验证、cookie或者其它HTTP高级功能。要支持这些功能,必须使用build_opener()函数创建自定义Opener对象。
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) //利用HTTPCookieProcessor(cookie处理器)处理cookiejar对象,以此为初值定义opener对象
urllib2.install_opener(opener) //安装opener,在安装之后调用urlopen()都会使用安装过的opener对象
postdata = urllib.urlencode({ //使用字典表示要发送的POST信息,接着用urllib.urlencode将post信息编码为服务器所能识别的格式
'id':id,
'pw':pw,
'submit':'login' //?有疑问 此处的key全是HTML的input tag中的name属性,点按钮的动作通过给input按钮的name赋值HTML上的value值来实现,例如
<input type="submit" name="submit" class="button" value="login" id="Submit1" />
})
//通过Request的方式打开url
req = urllib2.Request(url='https://bbs.sjtu.edu.cn/bbslogin',data=postdata)
response = urllib2.urlopen(req)
postdata2 = urllib.urlencode({
'text':text,
'type':'update'
})
req = urllib2.Request(url='https://bbs.sjtu.edu.cn/bbsplan',data = postdata2)
response2=urllib2.urlopen(req)
content = urllib2.urlopen('https://bbs.sjtu.edu.cn/bbsplan').read()
soup = BeautifulSoup(content)
print str(soup.find('textarea').string).strip().decode('utf-8')
text='shanghai'
_id='crawler'
pw='515030910612'
bbs_set(_id,pw,text)
二.HASH散列
- hash table
- 将字符串根据规则(hash function)放在b个不同的bucket中,这样查找只需要查找对应的bucket,将搜索范围缩短为原来的1/b
- hash function :
- 好的hash function应该能够将keywords按照规则平均地分配到b个bucket中
- 参考(General Purpose Hash Function Algorithms)中给出了几种Hash Function的写法(GeneralHashFunctions_-_Python.zip文件)。使用时可以选择多个哈希函数。也可以选择其中一种,给定不同的seed生成多个哈希函数。
def simple_hash_string(keyword,b):
if keyword!='':
return ord(keyword[0]) % b
else :
return 0
def better_hash_function(keyword,b):
if keyword!='':
return (ord(keyword[0])+ord(keyword[-1])) % b
else :
return 0
def hashtable_get_bucket(table,keyword):
if keyword!='':
bucket_num = (ord(keyword[0])+ord(keyword[-1]))%len(table)
else:
bucket_num = 0
return table[bucket_num]
def hashtable_lookup(table,keyword):
bucket_num = better_hash_function(keyword,len(table))
if keyword in table[bucket_num]:
return True
else:
return False
def make_hashtable(b):
try:
if b==0:
table = []
else:
table = [[]]*b
return table
except:
print "b must be greater than 0!\n"
def hashtable_add(table,keyword):
bucket_num = better_hash_function(keyword,len(table))
table[bucket_num].append(keyword)
def test_hash_function(func,size,filename):
words=[]
f = open(filename,'r')
for line in f.readlines():
for word in line.strip().split(' '):
words.append(word)
f.close()
results = [0]*size
words_used = []
for word in words:
if word not in words_used:
bucket = func(word,size)
results[bucket]+=1
words_used.append(word)
return results
三.
BloomFilter
- 背景:
- 在爬虫中,用HashTable记录已爬URL太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。
- Gmail等Email提供商,需要过滤垃圾邮件。一个办法就是记录下那些发垃圾邮件的 email 地址。每存储一亿个 email 地址, 就需要 1.6GB 的内存。而全世界至少有几十亿个发垃圾邮件的地址。
- 在上述应用中,需要快速判断某个元素是否属于集合,但是并不严格要求100%正确。例如将未爬网页误判为已爬网页的代价只是少爬几个网页;将正常邮件的地址误判为垃圾邮件地址,可以用通过建立白名单(存储那些可能别误判的邮件地址)的方式补救。另外,我们不关心集合里具体有哪些元素。例如我们不关心垃圾邮件集合里具体有哪些垃圾邮件地址。
- bit-map:
- 建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。 初始状态时,BitSet是一个包含m位的位数组,每一位都置0。添加元素x时,通过哈希函数将x映射到0~m-1的某个位置h(x),将该位置置1。查找元素y时,对y用哈希函数,如果h(y)位置为1,则很有可能y属于集合。如果h(y)位置为0,则y肯定不属于集合。
- Bit-Map相当于没有bucket,而且m>>n的HashTable。由于没有存储keyword的bucket,bitmap更节省空间。若要降低冲突发生的概率到1%,就要将BitSet的长度m设置为keyword个数n的100倍。
- 缺点:冲突率高
- BloomFilter:
- 为了降低冲突的概念,Bloom Filter使用了k个哈希函数,而不是一个。
- 假设有k个哈希函数,位数组大小为m,加入keyword的数量为n。 k个哈希函数记为h1, h2, …, hk, 对于keyword e,分别计算h1(e),h2(e),…,hk(e),然后将BitSet的h1(e),h2(e),…,hk(e)位设为1。这样就将keyword e映射到BitSet中的k个二进制位了。
- 检查keyword str是否存在的过程 :对于keyword str,分别计算h1(str),h2(str),h3(str),h4(str)。然后检查BitSet的第h1(str),h2(str),h3(str),h4(str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。 若一个字符串对应的Bit不全为1,则可以肯定该字符串一定没有被BloomFilter记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了) 但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被BloomFilter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为false positive 。 不同于其他结构,BloomFilter中的keyword加入了就被不能删除了,因为删除会影响到其他keyword 。 BloomFilter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
- BloomFilter的在线演示 http://billmill.org/bloomfilter-tutorial/ http://www.jasondavies.com/bloomfilter/
- 哈希函数个数k、位数组大小m选择 :哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以(BloomFilters- the math)。该文献证明了对于给定的m、n,当 k = ln(2)* m/n 时出错的概率是最小的。 同时该文献还给出特定的k,m,n的出错概率。例如:根据(BloomFilters- the math) 1,哈希函数个数k取10,位数组大小m设为字符串个数n的20倍时,false positive发生的概率是0.0000889 ,这个概率基本能满足爬虫的需求了。
四.并发编程
- Python中并发编程可以通过threading和Queue函数实现。Threading用来操作线程,Queue用来维护任务队列。 (PPT上使用了比较低级的Thread,和Threading是两个不一样的类,Threading使用OO方法,不需要指定target函数)
- Queue:
-
- 三个子类:Queue(队列),LifoQueue(栈),PriorityQueue(优先级队列)
- q=Queue.Queue(size) 初始化队列,size可选
- q.put(obj) (对于Queue)将obj放入末尾
- q.get()从队列首pop出一个元素
- q.size():返回队列大小
- q.empty():判断是否为空
- q.full():判断队列是否达到最大数
- q.get_nowait():如果队列阻塞就不等待直接取出
- q.unfinished_tasks():表示q中未完成的工作(元素)数量
- q.task_done():完成一项工作之后向队列发出一个信号,同时q.unfinished_tasks()减一
- q.join():等到队列空了之后再执行别的工作
- thread:
def working():
while True:
arguments = q.get() #获取任务
do_something_using(arguments) #处理任务
q.task_done() #任务结束
for i in range(NUM): #生成NUM个线程等待队列
t = Thread(target=working) #每个线程的工作进程为working()函数
t.setDaemon(True)
t.start()
for i in range(JOBS):
q.put(i) #将任务放入队列
q.join() #阻塞,等待所有任务完成
- Threading: