挖一个大坑,我是来观光的。
坑还没填完。
BOP 2016 SEMI Rank#11 团队 yaoyao 代码。
基于 Python 2.7 ,使用 flask 提供 Web API 终结点。665行的人生。
使用的库
- gevent :协程。
REST API 入口:cca
求解主入口:get_all_possible_ans
TOC
cca
[挖坑]
get_all_possible_ans
#@profile def get_all_possible_ans(Id1,Id2): entities = get_entity(expr='Or(Or(Or(Composite(AA.AuId=%s),Composite(AA.AuId=%s)),Id=%s),Id=%s)' % (Id1, Id2, Id1, Id2))
与 Elecky 类似,在查询 Id 类型的同时,将作者的所有论文全部下载下来,然后判断节点对的类型,分别使用 get_Id_Id
、 get_AuId_Id
、 get_Id_AuId
和 get_AuId_AuId
子过程解决问题。
get_entity
#@profile def get_entity(expr,attr='Id,AA.AuId,AA.AfId,J.JId,C.CId,RId,F.FId,CC',thl=[]): t_l = [] q = Queue.Queue() entities = [] pe_l = [] try: for i in xrange(2): t_l.append(threading.Thread(target=get_entity_one,args=(expr,q,i,attr))) t_l[i].start() gevent.joinall(thl) ans = q.get() entities = json.loads(ans)['entities'] except: para['expr'] = expr para['attributes'] = attr ans = get_ans_http(para) entities = json.loads(ans)['entities'] for en in entities: pe = Paper_Entity(en) pe_l.append(pe) return pe_l #@profile def get_entity_one(expr,q,i,attr='Id,AA.AuId,AA.AfId,J.JId,C.CId,RId,F.FId,CC'): para['expr'] = expr para['attributes'] = attr ans = get_ans(para,i) q.put(ans)
#167:这就是“同一查询,两次请求”的实现。注意到 get_entity_one
会将下载下来的论文信息放到 q
中。
#170:只是为了等待其它的 greenlet
。目前使用的都是默认值(空)。
#171:注意到线程安全队列的 Queue.get
在队列为空时会阻塞调用方。此处十分巧妙地利用了这一特点,如果 t_l
中有任意一次请求先完成,那么阻塞就会解除, ans
马上被赋值。
加速的方法:3.一个语句同时查询两次(单次查询时间波动较大,两次查询同时进行,并且使用先返回的结果,会更稳定) [1]
随后就是 JSON 解析和返回了。
#176:可能作者也觉得强行 socket (参阅下文)有点丧心病狂,所以如果在用 socket 的时候出现任何问题,就回退到 get_ans_http
,使用 httplib.HTTPConnection
(对应 Python 3 的 http.client.HTTPConnection
)进行连接。
get_ans
def get_ans(para, i=0): global host, port, domain try: sock_c = s_q.get(block=False) except: sock_c = socket_class() sock = sock_c.get_sock() para = '&'.join(['='.join(item) for item in para.items()]) request_str = '''GET /%s?%s HTTP/1.1\r\nHost: %s\r\n\r\n''' %(domain, para, host) sock.send(request_str) total_data=[] #sock.settimeout(5) data = sock.recv(150) length = int(data.split('\n')[1].split(' ')[1]) header_len = len(data.split('\r\n\r\n')[0]) total_data.append(data[header_len+4:]) length = length-(len(data) - header_len - 4) while 1: if length>0: data = sock.recv(min(length,8192)) length -= len(data) total_data.append(data) else: break ans = ''.join(total_data) s_q.put(sock_c) return ans for i in range(total_sock): s_q.put(socket_class())
其中,para
是一个字典,包含了URL请求的查询属性和值;i
不知道是干什么的,可能是之前向控制台输出以区分 `get_entity` 的两次连接的。
#95:socket pool 不多说。
加速的方法: 1.socket 长连接 [1]
#99:将字典转换为完整的 URL 。
#101:好吧,强行 socket
requests > urllib2 >httplib > socket长连接 时间消耗依次减少 [1]
那么先把 HTTP 1.1 完整的请求和回复放在这里备用吧。例如
GET https://oxfordhk.azure-api.net/academic/v1.0/evaluate?expr=Or(Id=2147152072,Composite(AA.AuId=2147152072))&model=latest&count=2&offset=0&orderby=&attributes=Id,Y,AA.AuId,AA.AfId,F.FId,J.JId,C.CId,RId,CC&subscription-key=f7cc29509a8443c5b3a5e56b0e38b5a6 HTTP/1.1 Host: oxfordhk.azure-api.net
HTTP/1.1 200 OK Content-Length: 683 Content-Type: application/json X-Powered-By: ASP.NET Date: Sat, 14 May 2016 07:32:12 GMT { "expr" : "Or(Id=2147152072,Composite(AA.AuId=2147152072))", "entities" : [{ "logprob":-15.440, "Id":2147152072, "Y":1990, "CC":5293, "RId":[2151561903, 1965061793, 2019911971, 2114804204, 2000215628, 2043343585, 134022301, 2158495995, 96199841, 1602667807, 35738896, 2041565863, 2072371179, 124362989, 2107827038, 1991631392, 71581993], "AA":[{"AuId":2033059188,"AfId":40347166}, {"AuId":676500258}, {"AuId":2019832499}, {"AuId":2114332599}, {"AuId":2004554093,"AfId":125749732}], "F":[{"FId":170133592}, {"FId":56666940}, {"FId":22789450}, {"FId":10879293}, {"FId":186311912}, {"FId":124246873}, {"FId":23123220}, {"FId":105795698}, {"FId":41008148}], "J":{"JId":135954941} }] }
#105, #113:注意 socket.recv
是阻塞函数。不过前面的调用方基本上都是在工作线程调用此函数的,所以也就无谓了。
#106-#109:为了从回复中套出 Content-Length
标头的值。
#113:随后就可以开始愉快地接收数据了。缓冲区大小 8KB 。
数据收完之后就可以返回了。
#123:在程序一开始运行的时候,就向池中填满新鲜的 sockets 。
heart_beat
在此函数中生成了一个简单的请求,然后把 s_q
socket 池中的 socket 取出来,一个一个地使用 get_ans
向外发送请求。函数保证了每一个 socket 在一分钟内会被使用一次。
threading.Thread(target=heart_beat)#.start()
然而这里的 start
被注释掉了……什么状况……
get_Id_Id
if (Id1_en.AuId or Id1_en.entity.get('RId',[])) and (Id2_en.AuId or Id2_en.entity.get('RId',[])): print 'Id-Id' if Id2_en.entity.get('CC',0)>1000: print 'CC>1000' ans+=get_Id_Id(Id1, Id2, Id1_en, Id2_en) else: print 'CC<1000' ans += get_Id_Id_CC(Id1, Id2, Id1_en, Id2_en)
与 Elecky 类似,如果 Id2
的被引次数大于1000,则使用 get_Id_Id
从左向右搜索;否则使用 get_Id_Id_CC
从右向左搜索。
#@profile def get_Id_Id(Id1, Id2, Id1_en, Id2_en): ans = []
这里 Id1_en
、 Id2_en
分别对应了 Id1
、 Id2
的论文详细信息。
对于从左向右搜索的情况
#266:先把 Id1
引用的所有文献(潜在的 Id3
)下载下来,存入 Id1_RId_en_l
中,然后按顺序考虑以下情况
Id1 - AuId/FId/CId/JId3 - Id4 - Id2
(分支):构造查询表达式
查找(包含 Id1.AuId/FId/JId/CId
或位于 Id3.RId
列表中)且引用了 Id2
的论文
很明显,这就是为了查找 Id4
。顺带一提,Or_expr_Id
实现了按照允许的最大表达式长度对查询进行拆分的功能。
#275:作者应该是 JId/CId 不分的,统统称为 CId
。
#286:然后把拆分后的查询表达式丢给 `gevent.spawn` ,在工作线程中进行下载。
Id - Id
:这个很明显。
Id1 - Id3 - Id2
:在 Id_RId_Id
子过程中,由于此时已经把所有可能的 Id3
下载下来了,所以直接判断其 RId
中是否包含 Id2
即可。
Id1 - CId/JId/FId/AuId3 - Id2
:这个也很明显。
Id1 - Id3 - CId/JId/FId/AuID4 - Id2
:这个也很明显,考虑到所有的 Id3
都被下载下来了。
Id1 - AuId/FId/CId/JId3 - Id4 - Id2
(汇合):等待前面 spawn
出来的线程工作完毕,然后查一下返回的 Id4
论文的属性,就可以一条一条地返回了。
对于从右向左搜索的情况
[未完待续]