开博第一篇:DHT 爬虫的学习记录
经过一段时间的研究和学习,大致了解了DHT网络的一些信息,大部分还是参会别人的相关代码,一方面主要对DHT爬虫原理感兴趣,最主要的是为了学习python,大部分是别人的东西原理还是引用别人的吧
DHT网络爬虫的实现 | 学步园 http://www.xuebuyuan.com/1287052.html
DHT协议原理以及一些重点分析:
要做DHT的爬虫,首先得透彻理解DHT,这样才能知道在什么地方究竟该应用什么算法去解决问题。关于DHT协议的细节以及重要的参考文章,请参考文末1
DHT协议作为BT协议的一个辅助,是非常好玩的。它主要是为了在BT正式下载时得到种子或者BT资源。传统的网络,需要一台中央服务器存放种子或者BT资源,不仅浪费服务器资源,还容易出现单点的各种问题,而DHT网络则是为了去中心化,也就是说任意时刻,这个网络总有节点是亮的,你可以去询问问这些亮的节点,从而将自己加入DHT网络。
要实现DHT协议的网络爬虫,主要分3步,第一步是得到资源信息(infohash,160bit,20字节,可以编码为40字节的十六进制字符串),第二步是确认这些infohash是有效的,第三步是通过有效的infohash下载到BT的种子文件,从而得到对这个资源的完整描述。
其中第一步是其他节点用DHT协议中的get_peers方法向爬虫发送请求得到的,第二步是其他节点用DHT协议中的announce_peer向爬虫发送请求得到的,第三步可以有几种方式得到,比如可以去一些保存种子的网站根据infohash直接下载到,或者通过announce_peer的节点来下载到,具体如何实现,可以取决于你自己的爬虫。
DHT协议中的主要几个操作:
主要负责通过UDP与外部节点交互,封装4种基本操作的请求以及相应。
ping:检查一个节点是否“存活”
在一个爬虫里主要有两个地方用到ping,第一是初始路由表时,第二是验证节点是否存活时
find_node:向一个节点发送查找节点的请求
在一个爬虫中主要也是两个地方用到find_node,第一是初始路由表时,第二是验证桶是否存活时
get_peers:向一个节点发送查找资源的请求
在爬虫中有节点向自己请求时不仅像个正常节点一样做出回应,还需要以此资源的info_hash为机会尽可能多的去认识更多的节点。如图,get_peers实际上最后一步是announce_peer,但是因为爬虫不能announce_peer,所以实际上get_peers退化成了find_node操作。
announce_peer:向一个节点发送自己已经开始下载某个资源的通知
爬虫中不能用announce_peer,因为这就相当于通报虚假资源,对方很容易从上下文中判断你是否通报了虚假资源从而把你禁掉
DHT协议中有几个重点的需要澄清的地方:
1. node与infohash同样使用160bit的表示方式,160bit意味着整个节点空间有2^160 = 730750818665451459101842416358141509827966271488,是48位10进制,也就是说有百亿亿亿亿亿个节点空间,这么大的节点空间,是足够存放你的主机节点以及任意的资源信息的。
2. 每个节点有张路由表。每张路由表由一堆K桶组成,所谓K桶,就是桶中最多只能放K个节点,默认是8个。而桶的保存则是类似一颗前缀树的方式。相当于一张8桶的路由表中最多有160-4个K桶。
3. 根据DHT协议的规定,每个infohash都是有位置的,因此,两个infohash之间就有距离一说,而两个infohash的距离就可以用异或来表示,即infohash1 xor infohash2,也就是说,高位一样的话,他们的距离就近,反之则远,这样可以快速的计算两个节点的距离。计算这个距离有什么用呢,在DHT网络中,如果一个资源的infohash与一个节点的infohash越近则该节点越有可能拥有该资源的信息,为什么呢?可以想象,因为人人都用同样的距离算法去递归的询问离资源接近的节点,并且只要该节点做出了回应,那么就会得到一个announce信息,也就是说跟资源infohash接近的节点就有更大的概率拿到该资源的infohash
4. 根据上述算法,DHT中的查询是跳跃式查询,可以迅速的跨越的的节点桶而接近目标节点桶。之所以在远处能够大幅度跳跃,而在近处只能小幅度跳跃,原因是每个节点的路由表中离自身越接近的节点保存得越多,如下图
5. 在一个DHT网络中当爬虫并不容易,不像普通爬虫一样,看到资源就可以主动爬下来,相反,因为得到资源的方式(get_peers, announce_peer)都是被动的,所以爬虫的方式就有些变化了,爬虫所要做的事就是像个正常节点一样去响应其他节点的查询,并且得到其他节点的回应,把其中的数据收集下来就算是完成工作了。而爬虫唯一能做的,是尽可能的去多认识其他节点,这样,才能有更多其他节点来向你询问。
6. 有人说,那么我把DHT爬虫的K桶中的容量K增大是不是就能增加得到资源的机会,其实不然,之前也分析过了,DHT爬虫最重要的信息来源全是被动的,因为你不能增大别人的K,所以距离远的节点保存你自身的概率就越小,当然距离远的节点去请求你的概率相对也比较小。
一些主要的组件(实际实现更加复杂一些,有其他的模块,这里仅列举主要几个):
DHT crawler:
这个就是DHT爬虫的主逻辑,为了简化多线程问题,跟server用了生产者消费者模型,负责消费,并且复用server的端口。
主要任务就是负责初始化,包括路由表的初始化,以及初始的请求。另外负责处理所有进来的消息事件,由于生产者消费者模型的使用,里面的操作都基本上是单线程的,简化了不少问题,而且相信也比上锁要提升速度(当然了,加锁这步按理是放到了queue这里了,不过对于这种生产者源源不断生产的类型,可以用ring-buffer大幅提升性能)。
DHT server:
这里是DHT爬虫的服务器端,DHT网络中的节点不单是client,也是server,所以要有server担当生产者的角色,最初也是每个消费者对应一个生产者,但实际上发现可以利用IO多路复用来达到消息事件的目的,这样一来大大简化了系统中线程的数量,如果client可以的话,也应该用同样的方式来组织,这样系统的速度应该会快很多。(尚未验证)
DHT route table:
主要负责路由表的操作。
路由表有如下操作:
init:刚创建路由表时的操作。分两种情况:
1. 如果之前已经初始化过,并且将上次路由表的数据保存下来,则只需要读入保存数据。
2. 如果之前没有初始化过,则首先应当初始化。
首先,应当有一个接入点,也就是说,你要想加进这个网络,必须认识这个网络中某个节点i并将i加入路由表,接下来对i用find_node询问自己的hash_info,这里巧妙的地方就在于,理论上通过一定数量的询问就会找到离自己距离很近的节点(也就是经过一定步骤就会收敛)。find_node目的在于尽可能早的让自己有数据,并且让网络上别的节点知道自己,如果别人不认识你,就不会发送消息过来,意味着你也不能获取到想要的信息。
search:比较重要的方法,主要使用它来定位当前infohash所在的桶的位置。会被其他各种代理方法调用到。
findNodes:找到路由表中与传入的infohash最近的k个节点
getPeer:找到待查资源是否有peer(即是否有人在下载,也就是是否有人announce过)
announcePeer:通知该资源正在被下载
DHT bucket:
acitiveNode:逻辑比较多,分如下几点。
1. 查找所要添加的节点对应路由表的桶是否已经满,如果未满,添加节点
2. 如果已经满,检查该桶中是否包含爬虫节点自己,如果不包含,抛弃待添加节点
3. 如果该桶中包含本节点,则平均分裂该桶
其他的诸如locateNode,
replaceNode, updateNode,
removeNode,就不一一说明了
DHT torrent parser:
主要从bt种子文件中解析出以下几个重要的信息:name,size,file list(sub file name, sub file size),比较简单,用bencode方向解码就行了
Utils:
distance:计算两个资源之间的距离。在kad中用a xor b表示
为了增加难度,选用了不太熟悉的语言python,结果步步为营,但是也感慨python的简洁强大。在实现中,也碰到很多有意思的问题。比如如何保存一张路由表中的所有桶,之前想出来几个办法,甚至为了节省资源,打算用bit数组+dict直接保存,但是因为估计最终的几个操作不是很方便直观容易出错而放弃,选用的结构就是前缀树,操作起来果然是没有障碍;
在超时问题上,比如桶超时和节点超时,一直在思考一个高效但是比较优雅的做法,可以用一个同步调用然后等待它的超时,但是显然很低效,尤其我没有用更多线程的情况,一旦阻塞了就等于该端口所有事件都被阻塞了。所以必须用异步操作,但是异步操作很难去控制它的精确事件,当然,我可以在每个事件来的时候检查一遍是否超时,但是显然也是浪费和低效。那么,剩下的只有采用跟tomcat类似的方式了,增加一个线程来监控,当然,这个监控线程最好是全局的,能监控所有crawler中所有事务的超时。另外,超时如果控制不当,容易导致内存没有回收以至于内存泄露,也值得注意。超时线程是否会与其他线程互相影响也应当仔细检查。
最初超时的控制没处理好,出现了ping storm,运行一定时间后大多数桶已经满了,如果按照协议中的方式去跑的话会发现大量的事件都是在ping以确认这个节点是否ok以至于大量的cpu用于处理ping和ping响应。深入理解后发现,检查节点状态是不需要的,因为节点状态只是为了提供给询问的人一些好的节点,既然如此,可以将每次过来的节点替换当前桶中最老的节点,如此一来,我们将总是保存着最新的节点。
搜索算法也是比较让我困惑的地方,简而言之,搜索的目的并不是真正去找资源,而是去认识那些能够保存你的节点。为什么说是能够保存你,因为离你越远,桶的数量越少,这样一来,要想进他们的桶中去相对来说就比较困难,所以搜索的目标按理应该是附近的节点最好,但是不能排除远方节点也可能保存你的情况,这种情况会发生在远方节点初始化时或者远方节点的桶中节点超时的时候,但总而言之,概率要小些。所以搜索算法也不应该不做判断就胡乱搜索,但是也不应该将搜索的距离严格限制在附近,所以这是一个权衡问题,暂时没有想到好的方式,觉得暂时让距离远的以一定概率发生,而距离近的必然发生
还有一点,就是搜索速度问题,因为DHT网络的这种结构,决定了一个节点所认识的其他节点必然是有限的附近节点,于是每个节点在一定时间段内能拿到的资源数必然是有限的,所以应当分配多个节点同时去抓取,而抓取资源的数量很大程度上就跟分配节点的多少有关了。
最后一个值得优化的地方是findnodes方法,之前的方式是把一个桶中所有数据拿出来排序,然后取其中前K个返回回去,但是实际上我们做了很多额外的工作,这是经典的topN问题,使用排序明显是浪费时间的,因为这个操作非常频繁,所以即便所有保存的节点加起来很少((160 - 4) * 8),也会一定程度上增加时间。而采用的算法是在一篇论文《可扩展的DHT网络爬虫设计和优化》中找到的,基本公式是IDi = IDj xor 2 ^(160 - i),这样,已知IDi和i就能知道IDj,若已知IDi和IDj就能知道i,通过这种方式,可以快速的查找该桶A附近的其他桶(显然是离桶A层次最近的桶中的节点距离A次近),比起全部遍历再查找效率要高不少。
dht协议http://www.bittorrent.org/beps/bep_0005.html 及其翻译http://gobismoon.blog.163.com/blog/static/5244280220100893055533/
爬虫源码参考别人的,非原创,只为学习
#encoding: utf-8 from hashlib import sha1
from random import randint
from struct import unpack, pack
from socket import inet_aton, inet_ntoa
from bisect import bisect_left
from threading import Timer
from time import sleep from bencode import bencode, bdecode BOOTSTRAP_NODES = [
("router.bittorrent.com", 6881),
("dht.transmissionbt.com", 6881),
("router.utorrent.com", 6881)
]
TID_LENGTH = 4
KRPC_TIMEOUT = 10
REBORN_TIME = 5 * 60
K = 8 def entropy(bytes):
s = ""
for i in range(bytes):
s += chr(randint(0, 255))
return s # """把爬虫"伪装"成正常node, 一个正常的node有ip, port, node ID三个属性, 因为是基于UDP协议,
# 所以向对方发送信息时, 即使没"明确"说明自己的ip和port时, 对方自然会知道你的ip和port,
# 反之亦然. 那么我们自身node就只需要生成一个node ID就行, 协议里说到node ID用sha1算法生成,
# sha1算法生成的值是长度是20 byte, 也就是20 * 8 = 160 bit, 正好如DHT协议里说的那范围: 0 至 2的160次方,
# 也就是总共能生成1461501637330902918203684832716283019655932542976个独一无二的node.
# ok, 由于sha1总是生成20 byte的值, 所以哪怕你写SHA1(20)或SHA1(19)或SHA1("I am a 2B")都可以,
# 只要保证大大降低与别人重复几率就行. 注意, node ID非十六进制,
# 也就是说非FF5C85FE1FDB933503999F9EB2EF59E4B0F51ECA这个样子, 即非hash.hexdigest(). """
def random_id():
hash = sha1()
hash.update( entropy(20) )
return hash.digest() def decode_nodes(nodes):
n = []
length = len(nodes)
if (length % 26) != 0:
return n
for i in range(0, length, 26):
nid = nodes[i:i+20]
ip = inet_ntoa(nodes[i+20:i+24])
port = unpack("!H", nodes[i+24:i+26])[0]
n.append( (nid, ip, port) )
return n def encode_nodes(nodes):
strings = []
for node in nodes:
s = "%s%s%s" % (node.nid, inet_aton(node.ip), pack("!H", node.port))
strings.append(s) return "".join(strings) def intify(hstr):
#"""这是一个小工具, 把一个node ID转换为数字. 后面会频繁用到."""
return long(hstr.encode('hex'), 16) #先转换成16进制, 再变成数字 def timer(t, f):
Timer(t, f).start() class BucketFull(Exception):
pass class KRPC(object):
def __init__(self):
self.types = {
"r": self.response_received,
"q": self.query_received
}
self.actions = {
"ping": self.ping_received,
"find_node": self.find_node_received,
"get_peers": self.get_peers_received,
"announce_peer": self.announce_peer_received,
} self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.bind(("0.0.0.0", self.port)) def response_received(self, msg, address):
self.find_node_handler(msg) def query_received(self, msg, address):
try:
self.actions[msg["q"]](msg, address)
except KeyError:
pass def send_krpc(self, msg, address):
try:
self.socket.sendto(bencode(msg), address)
except:
pass class Client(KRPC):
def __init__(self, table):
self.table = table timer(KRPC_TIMEOUT, self.timeout)
timer(REBORN_TIME, self.reborn)
KRPC.__init__(self) def find_node(self, address, nid=None):
nid = self.get_neighbor(nid) if nid else self.table.nid
tid = entropy(TID_LENGTH) msg = {
"t": tid,
"y": "q",
"q": "find_node",
"a": {"id": nid, "target": random_id()}
}
self.send_krpc(msg, address) def find_node_handler(self, msg):
try:
nodes = decode_nodes(msg["r"]["nodes"])
for node in nodes:
(nid, ip, port) = node
if len(nid) != 20: continue
if nid == self.table.nid: continue
self.find_node( (ip, port), nid )
except KeyError:
pass def joinDHT(self):
for address in BOOTSTRAP_NODES:
self.find_node(address) def timeout(self):
if len( self.table.buckets ) < 2:
self.joinDHT()
timer(KRPC_TIMEOUT, self.timeout) def reborn(self):
self.table.nid = random_id()
self.table.buckets = [ KBucket(0, 2**160) ]
timer(REBORN_TIME, self.reborn) def start(self):
self.joinDHT() while True:
try:
(data, address) = self.socket.recvfrom(65536)
msg = bdecode(data)
self.types[msg["y"]](msg, address)
except Exception:
pass def get_neighbor(self, target):
return target[:10]+random_id()[10:] class Server(Client):
def __init__(self, master, table, port):
self.table = table
self.master = master
self.port = port
Client.__init__(self, table) def ping_received(self, msg, address):
try:
nid = msg["a"]["id"]
msg = {
"t": msg["t"],
"y": "r",
"r": {"id": self.get_neighbor(nid)}
}
self.send_krpc(msg, address)
self.find_node(address, nid)
except KeyError:
pass def find_node_received(self, msg, address):
try:
target = msg["a"]["target"]
neighbors = self.table.get_neighbors(target) nid = msg["a"]["id"]
msg = {
"t": msg["t"],
"y": "r",
"r": {
"id": self.get_neighbor(target),
"nodes": encode_nodes(neighbors)
}
}
self.table.append(KNode(nid, *address))
self.send_krpc(msg, address)
self.find_node(address, nid)
except KeyError:
pass def get_peers_received(self, msg, address):
try:
infohash = msg["a"]["info_hash"] neighbors = self.table.get_neighbors(infohash) nid = msg["a"]["id"]
msg = {
"t": msg["t"],
"y": "r",
"r": {
"id": self.get_neighbor(infohash),
"nodes": encode_nodes(neighbors)
}
}
self.table.append(KNode(nid, *address))
self.send_krpc(msg, address)
self.master.log(infohash)
self.find_node(address, nid)
except KeyError:
pass def announce_peer_received(self, msg, address):
try:
infohash = msg["a"]["info_hash"]
nid = msg["a"]["id"] msg = {
"t": msg["t"],
"y": "r",
"r": {"id": self.get_neighbor(infohash)}
} self.table.append(KNode(nid, *address))
self.send_krpc(msg, address)
self.master.log(infohash)
self.find_node(address, nid)
except KeyError:
pass
# 该类只实例化一次.
class KTable(object):
# 这里的nid就是通过node_id()函数生成的自身node ID. 协议里说道, 每个路由表至少有一个bucket,
# 还规定第一个bucket的min=0, max=2^160次方, 所以这里就给予了一个buckets属性来存储bucket, 这个是列表.
def __init__(self, nid):
self.nid = nid
self.buckets = [ KBucket(0, 2**160) ] def append(self, node):
index = self.bucket_index(node.nid)
try:
bucket = self.buckets[index]
bucket.append(node)
except IndexError:
return
except BucketFull:
if not bucket.in_range(self.nid):
return
self.split_bucket(index)
self.append(node) # 返回与目标node ID或infohash的最近K个node. # 定位出与目标node ID或infohash所在的bucket, 如果该bucuck有K个节点, 返回.
# 如果不够到K个节点的话, 把该bucket前面的bucket和该bucket后面的bucket加起来, 只返回前K个节点.
# 还是不到K个话, 再重复这个动作. 要注意不要超出最小和最大索引范围.
# 总之, 不管你用什么算法, 想尽办法找出最近的K个节点.
def get_neighbors(self, target):
nodes = []
if len(self.buckets) == 0: return nodes
if len(target) != 20 : return nodes index = self.bucket_index(target)
try:
nodes = self.buckets[index].nodes
min = index - 1
max = index + 1 while len(nodes) < K and ((min >= 0) or (max < len(self.buckets))):
if min >= 0:
nodes.extend(self.buckets[min].nodes) if max < len(self.buckets):
nodes.extend(self.buckets[max].nodes) min -= 1
max += 1 num = intify(target)
nodes.sort(lambda a, b, num=num: cmp(num^intify(a.nid), num^intify(b.nid)))
return nodes[:K] #K是个常量, K=8
except IndexError:
return nodes def bucket_index(self, target):
return bisect_left(self.buckets, intify(target)) # 拆表 # index是待拆分的bucket(old bucket)的所在索引值.
# 假设这个old bucket的min:0, max:16. 拆分该old bucket的话, 分界点是8, 然后把old bucket的max改为8, min还是0.
# 创建一个新的bucket, new bucket的min=8, max=16.
# 然后根据的old bucket中的各个node的nid, 看看是属于哪个bucket的范围里, 就装到对应的bucket里.
# 各回各家,各找各妈.
# new bucket的所在索引值就在old bucket后面, 即index+1, 把新的bucket插入到路由表里.
def split_bucket(self, index):
old = self.buckets[index]
point = old.max - (old.max - old.min)/2
new = KBucket(point, old.max)
old.max = point
self.buckets.insert(index + 1, new)
for node in old.nodes[:]:
if new.in_range(node.nid):
new.append(node)
old.remove(node) def __iter__(self):
for bucket in self.buckets:
yield bucket class KBucket(object):
__slots__ = ("min", "max", "nodes") # min和max就是该bucket负责的范围, 比如该bucket的min:0, max:16的话,
# 那么存储的node的intify(nid)值均为: 0到15, 那16就不负责, 这16将会是该bucket后面的bucket的min值.
# nodes属性就是个列表, 存储node. last_accessed代表最后访问时间, 因为协议里说到,
# 当该bucket负责的node有请求, 回应操作; 删除node; 添加node; 更新node; 等这些操作时,
# 那么就要更新该bucket, 所以设置个last_accessed属性, 该属性标志着这个bucket的"新鲜程度". 用linux话来说, touch一下.
# 这个用来便于后面说的定时刷新路由表. def __init__(self, min, max):
self.min = min
self.max = max
self.nodes = [] # 添加node, 参数node是KNode实例. # 如果新插入的node的nid属性长度不等于20, 终止.
# 如果满了, 抛出bucket已满的错误, 终止. 通知上层代码进行拆表.
# 如果未满, 先看看新插入的node是否已存在, 如果存在, 就替换掉, 不存在, 就添加,
# 添加/替换时, 更新该bucket的"新鲜程度".
def append(self, node):
if node in self:
self.remove(node)
self.nodes.append(node)
else:
if len(self) < K:
self.nodes.append(node)
else:
raise BucketFull def remove(self, node):
self.nodes.remove(node) def in_range(self, target):
return self.min <= intify(target) < self.max def __len__(self):
return len(self.nodes) def __contains__(self, node):
return node in self.nodes def __iter__(self):
for node in self.nodes:
yield node def __lt__(self, target):
return self.max <= target class KNode(object):
# """
# nid就是node ID的简写, 就不取id这么模糊的变量名了. __init__方法相当于别的OOP语言中的构造方法,
# 在python严格来说不是构造方法, 它是初始化, 不过, 功能差不多就行.
# """
__slots__ = ("nid", "ip", "port") def __init__(self, nid, ip, port):
self.nid = nid
self.ip = ip
self.port = port def __eq__(self, other):
return self.nid == other.nid #using example
class Master(object):
def __init__(self, f):
self.f = f def log(self, infohash):
self.f.write(infohash.encode("hex")+"\n")
self.f.flush()
try:
f = open("infohash.log", "a")
m = Master(f)
s = Server(Master(f), KTable(random_id()), 8001)
s.start()
except KeyboardInterrupt:
s.socket.close()
f.close()
种子从迅雷下,初期为学习从http://torrage.com/sync/下的infohash,去重用了别人写的Bloom Filter算法,数据库用Mysql,建表语句如下,其中uinthash是根据infohash的头四个字节和最后四个字节组成的一个int整数,先这样设计,看后期查询的时候用得到不,总觉得用infohash来查很慢
CREATE TABLE `torrentinfo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`infohash` char(40) NOT NULL DEFAULT '',
`filename` varchar(128) DEFAULT NULL,
`filelength` bigint(11) DEFAULT NULL,
`recvtime` datetime DEFAULT NULL,
`filecontent` text,
`uinthash` int(11) unsigned NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `uinthash_index` (`uinthash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Thunder.py
# _*_ coding: utf-8 _*_
import socket
import os,glob
import time as time_p
import requests
from bencode import bdecode, BTL
from torrent import *
import threading, signal
import MySQLdb
from BloomFilter import * class Thunder(object):
def __init__(self):
self.connstr={'host':'127.0.0.1','user':'root','passwd':'','port':3306,'charset':"UTF8"}
def download(self, infohash):
try:
tc = self._download(infohash)
if(tc==-1):
return
tc = bdecode(tc)
info = torrentInfo(tc)
# print info['name']
# print info['length']
# print info['files']
uint=int(infohash[:4]+infohash[-4:],16)
time_now=time_p.strftime('%Y-%m-%d %H:%M:%S',time_p.localtime(time_p.time()))
sql="insert into torrentinfo(infohash,filename,filelength,recvtime,filecontent,uinthash) values('%s','%s','%d','%s','%s','%d')"%(infohash,MySQLdb.escape_string(info['name']),info['length'],time_now,MySQLdb.escape_string(info['files']),uint)
self.executeSQL(sql)
except Exception,e:
print e
pass def openConnection(self):
try:
self.conn=MySQLdb.connect(**self.connstr)
self.cur=self.conn.cursor()
self.conn.select_db('dht')
except MySQLdb.Error,e:
print 'mysql error %d:%s'%(e.args[0],e.args[1]) def executeSQL(self,sql):
try:
self.cur.execute(sql)
self.conn.commit()
except MySQLdb.Error,e:
print 'mysql error %d:%s'%(e.args[0],e.args[1])
def closeConnection(self):
try:
self.cur.close()
self.conn.close()
except MySQLdb.Error,e:
print 'mysql error %d:%s'%(e.args[0],e.args[1]) def _download(self, infohash):
infohash = infohash.upper()
start = infohash[0:2]
end = infohash[-2:]
url = "http://bt.box.n0808.com/%s/%s/%s.torrent" % (start, end, infohash)
headers = {
"Referer": "http://bt.box.n0808.com"
}
try:
r = requests.get(url, headers=headers, timeout=10)
if r.status_code == 200:
# f=open("d:\\"+infohash+'.torrent','wb')
# f.write(r.content)
# f.close()
return r.content
except (socket.timeout, requests.exceptions.Timeout), e:
pass
return -1 class torrentBean(object):
"""docstring for torrentBean"""
__slots__=('infohash','filename','recvtime','filecontent','uinthash') def __init__(self, infohash,filename,recvtime,filecontent,uinthash):
super(torrentBean, self).__init__()
self.infohash = infohash
self.filename = filename
self.recvtime = recvtime
self.filecontent = filecontent
self.uinthash = uinthash bf = BloomFilter(0.001, 1000000)
a=Thunder()
a.openConnection()
# info_hash="a02d2735e6e1daa6f7d58f21bd7340a7b7c4b7a5"
# info_hash='cf3a6a4f07da0b90beddae838462ca0012bef285'
# a.download('cf3a6a4f07da0b90beddae838462ca0012bef285') files=glob.glob('./*.txt')
for fl in files:
print os.path.basename(fl)
f=open(fl,'r')
for line in f:
infohash=line.strip('\n')
if not bf.is_element_exist(infohash):
bf.insert_element(infohash)
a.download(infohash)
a.closeConnection()
torrent种子文件经过bencode解析,获取key为info对应value值,种子大致的格式如下,有乱码,不影响观看
{
'files': [{
'path': ['PGD660.avi'],
'length': 1367405512,
'filehash': 'J\xef\xfe\xb3K\xd4g\x8d\x07m\x03\xbb\xb3\xadt\xa1\xa0\xf0\xec\xab',
'ed2k': '/\xfb\xe55#n\xbd1\xb6\x1c\x0f\xf3\xe4\x9dP\xfb',
'path.utf-8': ['PGD660.avi']
}, {
'path': ['PGD660B.jpg'],
'length': 135899,
'filehash': '*$O\x17w\xe9E\x95>O\x1f\xfb\x0e\x9b\x16\x15B\\Q\x9d',
'ed2k': 'T/L*\xbb\x8e.\xe2d\xddu\nR\x07\xca\x19',
'path.utf-8': ['PGD660B.jpg']
}, {
'path': ['yoy123@\xe8\x8d\x89\xe6\xa6\xb4\xe7\xa4\xbe\xe5\x8c\xba@\xe6\x9c\x80\xe6\x96\xb0\xe5\x9c\xb0\xe5\x9d\x80.mht'],
'length': 472,
'filehash': '&\xa92\xb7\xdd8\xeel3\xcc-S\x07\xb5e\xd35\xc0\xb7r',
'ed2k': '\x13\xd2 a\x0cA\xb4\xf2X\x12\xea\xd4\xe8\xac`\x92',
'path.utf-8': ['yoy123@\xe8\x8d\x89\xe6\xa6\xb4\xe7\xa4\xbe\xe5\x8c\xba@\xe6\x9c\x80\xe6\x96\xb0\xe5\x9c\xb0\xe5\x9d\x80.mht']
}, {
'path': ['yoy123@\xe8\x8d\x89\xe6\xa6\xb4\xe7\xa4\xbe\xe5\x8c\xba\xe5\xae\xa3\xe4\xbc\xa0.txt'],
'length': 363,
'filehash': '\x96nA*\xe2\xb6Y+[\xe3\xaf\xd4\x14A\x94\xf5@\xcd\xc1\x91',
'ed2k': '8V\xa6X\xd9\x82l\xdbNO8\xe8D\xe9E\xed',
'path.utf-8': ['yoy123@\xe8\x8d\x89\xe6\xa6\xb4\xe7\xa4\xbe\xe5\x8c\xba\xe5\xae\xa3\xe4\xbc\xa0.txt']
}, {
'path': ['\xe2\x98\x85\xe5\xb0\x91\xe5\xa6\x87 \xe8\xae\xba\xe5\x9d\x9b \xe9\x99\x90\xe9\x87\x8f\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe3\x80\x82\xe3\x80\x82.mht'],
'length': 475,
'filehash': '\xec\xde\xeb-6\x86\x1avB\xdd\xd8q\x8b\x8f\xc06\xf0XX\x0e',
'ed2k': '\xa7\x8dU\xfd\xfc=\x12\x15>yE\x8f&A\xc2u',
'path.utf-8': ['\xe2\x98\x85\xe5\xb0\x91\xe5\xa6\x87 \xe8\xae\xba\xe5\x9d\x9b \xe9\x99\x90\xe9\x87\x8f\xe5\xbc\x80\xe6\x94\xbe\xe4\xb8\xad\xe3\x80\x82\xe3\x80\x82.mht']
}, {
'path': ['\xe6\x9f\x8f\xe6\x8b\x89\xe5\x9c\x96\xe7\xa7\x98\xe5\xaf\x86\xe8\x8a\xb1\xe5\x9c\x92.mht'],
'length': 478,
'filehash': "\xe4\xb5'Td\x0b=P\xc0\x9aG\xa2\xd7\xfapg\xc6.\x8e\xa7",
'ed2k': '\xdd\x8d\xbb\x0b\x04\xcb\x03O\xb1\x18"\x03\xb1\x1d\xba\x08',
'path.utf-8': ['\xe6\x9f\x8f\xe6\x8b\x89\xe5\x9c\x96\xe7\xa7\x98\xe5\xaf\x86\xe8\x8a\xb1\xe5\x9c\x92.mht']
}, {
'path': ['\xe7\xbe\x8e\xe5\xa5\xb3\xe4\xb8\x8a\xe9\x96\x80\xe6\x8f\xb4\xe4\xba\xa4\xe6\x9c\x8d\xe5\x8b\x99.mht'],
'length': 478,
'filehash': "\xe4\xb5'Td\x0b=P\xc0\x9aG\xa2\xd7\xfapg\xc6.\x8e\xa7",
'ed2k': '\xdd\x8d\xbb\x0b\x04\xcb\x03O\xb1\x18"\x03\xb1\x1d\xba\x08',
'path.utf-8': ['\xe7\xbe\x8e\xe5\xa5\xb3\xe4\xb8\x8a\xe9\x96\x80\xe6\x8f\xb4\xe4\xba\xa4\xe6\x9c\x8d\xe5\x8b\x99.mht']
}],
'publisher': 'yoy123',
'piece length': 524288,
'name': 'PGD660 \xe6\x83\xb3\xe8\xa9\xa6\xe8\x91\x97\xe5\x85\xa8\xe5\x8a\x9b\xe6\x93\x8d\xe6\x93\x8d\xe7\x9c\x8b\xe9\x80\x99\xe5\x80\x8b\xe6\xb7\xab\xe8\x95\xa9\xe7\xbe\x8e\xe5\xa5\xb3\xe5\x97\x8e \xe5\xb0\x8f\xe5\xb7\x9d\xe3\x81\x82\xe3\x81\x95\xe7\xbe\x8e',
'publisher.utf-8': 'yoy123',
}
解析代码torrent.py
# _*_ coding: utf-8 _*_
from time import time def torrentInfo(torrentContent):
metadata = torrentContent["info"]
print metadata
info = {
"name": getName(metadata),
"length": calcLength(metadata),
"timestamp": getCreateDate(torrentContent),
"files": extraFiles(metadata)
}
return info def calcLength(metadata):
length = 0
try:
length = metadata["length"]
except KeyError:
try:
for file in metadata["files"]:
length += file["length"]
except KeyError:
pass
return length def extraFiles(metadata):
files = []
try:
for file in metadata["files"]:
path = file["path.utf-8"]
size=file['length']
if len(path) > 1:
main = path[0]
for f in path[1:2]:
files.append("%s/%s %d bytes" % (main, f,size))
else:
files.append("%s %d bytes" % (path[0],size) )
if files:
return '\r\n'.join(files)
else:
return getName(metadata)
except KeyError:
return getName(metadata) def getName(metadata):
try:
name = metadata["name.utf-8"]
if name.strip()=="":
raise KeyError
except KeyError:
try:
name = metadata["name"]
if name.strip()=="":
raise KeyError
except KeyError:
name = getMaxFile(metadata) return name
def getMaxFile(metadata):
try:
maxFile = metadata["files"][0]
for file in metadata["files"]:
if file["length"] > maxFile["length"]:
maxFile = file
name = maxFile["path"][0]
return name
except KeyError:
return "" def getCreateDate(torrentContent):
try:
timestamp = torrentContent["creation date"]
except KeyError:
timestamp = int( time() )
return timestamp
最后还有别人写的BloomFilter代码
#encoding: utf-8
'''
Created on 2012-11-7 @author: palydawn
'''
import cmath
from BitVector import BitVector class BloomFilter(object):
def __init__(self, error_rate, elementNum):
#计算所需要的bit数
self.bit_num = -1 * elementNum * cmath.log(error_rate) / (cmath.log(2.0) * cmath.log(2.0)) #四字节对齐
self.bit_num = self.align_4byte(self.bit_num.real) #分配内存
self.bit_array = BitVector(size=self.bit_num) #计算hash函数个数
self.hash_num = cmath.log(2) * self.bit_num / elementNum self.hash_num = self.hash_num.real #向上取整
self.hash_num = int(self.hash_num) + 1 #产生hash函数种子
self.hash_seeds = self.generate_hashseeds(self.hash_num) def insert_element(self, element):
for seed in self.hash_seeds:
hash_val = self.hash_element(element, seed)
#取绝对值
hash_val = abs(hash_val)
#取模,防越界
hash_val = hash_val % self.bit_num
#设置相应的比特位
self.bit_array[hash_val] = 1 #检查元素是否存在,存在返回true,否则返回false
def is_element_exist(self, element):
for seed in self.hash_seeds:
hash_val = self.hash_element(element, seed)
#取绝对值
hash_val = abs(hash_val)
#取模,防越界
hash_val = hash_val % self.bit_num #查看值
if self.bit_array[hash_val] == 0:
return False
return True #内存对齐
def align_4byte(self, bit_num):
num = int(bit_num / 32)
num = 32 * (num + 1)
return num #产生hash函数种子,hash_num个素数
def generate_hashseeds(self, hash_num):
count = 0
#连续两个种子的最小差值
gap = 50
#初始化hash种子为0
hash_seeds = []
for index in xrange(hash_num):
hash_seeds.append(0)
for index in xrange(10, 10000):
max_num = int(cmath.sqrt(1.0 * index).real)
flag = 1
for num in xrange(2, max_num):
if index % num == 0:
flag = 0
break if flag == 1:
#连续两个hash种子的差值要大才行
if count > 0 and (index - hash_seeds[count - 1]) < gap:
continue
hash_seeds[count] = index
count = count + 1 if count == hash_num:
break
return hash_seeds def hash_element(self, element, seed):
hash_val = 1
for ch in str(element):
chval = ord(ch)
hash_val = hash_val * seed + chval
return hash_val def SaveBitToFile(self,f):
self.bit_array.write_bits_to_fileobject(f)
pass
表内容见下图
开博第一篇:DHT 爬虫的学习记录的更多相关文章
- 开博第一篇,学习markdown
Markdown学习 其实之前自己也一直有记录,不过是Evernote记录,没有分享出来,最近看了一些牛人博客,觉得也应该分享出来.和别人多交流,多学习.所以花了几小时学了一下Markdown语法,现 ...
- 初学C++,开博第一篇
几个维度相同的数组转置算法,这两种完全相同://注意:如果维度不同,转置会出错误,因为下标会越界...解决办法是把数组的维度调齐,或者是写到另一新数组中. for(int i=0;i<row;i ...
- 开园第一篇---有关tensorflow加载不同模型的问题
写在前面 今天刚刚开通博客,主要想法跟之前某位博主说的一样,希望通过博客园把每天努力的点滴记录下来,也算一种坚持的动力.我是小白一枚,有啥问题欢迎各位大神指教,鞠躬~~ 换了新工作,目前手头是OCR项 ...
- 第一篇 入门必备 (Android学习笔记)
第一篇 入门必备 第1章 初识Android 第2章 搭建你的开发环境 第3章 创建第一个程序--HelloWorld 第4章 使用Android工具 ●Android之父 Android安迪·罗 ...
- 开园第一篇 - 论移动开发环境 IOS与Android的差异
首先,在真正写技术之前做个自我简介.本人08年开始学c语言 一年后,转vc++.开始接触MFC MFC做了两年.转眼11年了我考上了一个不知名的大专.搞C++发现没有市场了因为当时酷狗腾讯的软件已经日 ...
- 开博第二篇:记一个利用JavaScript,编写PS脚本,开发图片量产工具
背景:身在一个有实业的电商公司,设计部的妹子们总是会有做不完的商品图片,当然了,要是做点有技术含量的美化工作也罢,但是最近她们很是无聊,总是要做一些重复性的工作,就比如如题所说的,图片量产,量产什么呢 ...
- 【Python Learning第一篇】Linux命令学习及Vim命令的使用
学了两天,终于把基本命令学完了,掌握以后可以当半个程序员了♪(^∇^*) 此文是一篇备忘录或者查询笔记,如果哪位大佬看上了并且非常嫌弃的话,还请大佬不吝赐教,多多包涵 以下是我上课做的一些笔记,非常的 ...
- 轻量级ORM框架——第一篇:Dapper快速学习
我们都知道ORM全称叫做Object Relationship Mapper,也就是可以用object来map我们的db,而且市面上的orm框架有很多,其中有一个框架 叫做dapper,而且被称为th ...
- 爬虫第一篇:爬虫详解之urllib.request模块
我将urllib.request 的GET请求和POST请求两种方法做了总结 GET请求 GET请求爬取: import urllib.request import urllib.parse head ...
随机推荐
- jdbc连接池中c3p0的配置文件的详解以及在在java中如何使用
<c3p0-config> <!-- 默认配置,如果没有指定则使用这个配置 --> <default-config> <property name=" ...
- Composer PHP 依赖管理工具
composer 是 PHP 用来管理依赖(dependency)关系的工具.你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件. 依赖管理 ...
- python爬虫爬取全球机场信息
--2013年10月10日23:54:43 今天需要获取机场信息,发现一个网站有数据,用爬虫趴下来了所有数据: 目标网址:http://www.feeyo.com/airport_code.asp?p ...
- java笔记00-目录
--2013年7月26日17:49:59 学习java已久,趁最近有空,写一个总结: java笔记01-反射:
- [转]Web性能监控自动化探索之路–初识WebPageTest
本文转自:http://www.webryan.net/2013/01/use-webpagetest-to-analyze-web-performance/ 无论是从Velocity 2012还是在 ...
- 关于Linux系统和Windows系统中文件夹的命名规范
Windows系统中. 1.在创建文件夹的时候不能以"."开头(但是文件以多个点开头并且还有其他合法字符的话就是合法的) 但是在windows系统中确实见过以一个点".& ...
- IOS下视频监控项目总结
一.项目说明 二.技术点 1.数据缓存 2.storyboard的使用 3.h264码流数据的解码 4.UDP通讯 三.解决方案 四.总结 五.相关资料
- EWM Matrai B2B管理平台
该应用是一款企业管理的app,可以通过“分享”.“工作分派”.“审批”.“业务”.“工作计划”.“日程”等功能得到有效的管控.该项目主要分为5大模块,分别是近期动态,任务,日程,我,在线聊天.
- Servlet & JSP - Cookie
关于 Cookie 的内容,参考 HTTP - Cookie 机制 获取来自客户端的 cookie request.getCookies 方法可以获取来自 HTTP 请求的 cookie,返回的是 j ...
- Nginx - Rewrite Module
Initially, the purpose of this module (as the name suggests) is to perform URL rewriting. This mecha ...