最近接到实验室的导师交给我的一个任务,就是他们手头有很多smile表达式,格式类似这种:C(=C(c1ccccc1)c1ccccc1)c1ccccc1(这是生物信息学中表达小分子结构的一种常用表达式),他们需要对每个smile表达式在ZINC网站(生物信息学数据网站)上进行搜索,然后找到对应的ZINC号、小分子供应商、构象预测等信息。基本步骤如下:
点击查找之后网页就会跳转到详细信息,我们需要获取它的ZINC号、小分子供应商、构象预测、CAS号等信息,如下:
这一套流程要是靠人工手动完成的话有点不太现实,毕竟他们有一千多个这样的smile号,要是一个一个搞还不要累死,于是他们想到了我,想让我写一个爬虫来自动化提取出这些信息,一劳永逸。说实话我当时接到这个任务的时候毫不犹豫答应了下来,一来是我之前确实写过类似的程序,二来自己最近确实很闲,毕业论文搞得差不多了,每天没啥事打打王者一天的时间就浪费掉了,还不如接个任务,也算是练练手。废话不多说,直接开干。
在撸代码之前我们要先搞清楚几个问题,不能蛮干。首先我们要知道在我们输入smile号并点击搜索的时候,这个时候前端和后端服务器交互的过程是什么样的,也就是说前端到底给后端发送了什么样的HTTP请求。要知道我们一开始可是只输入了一个smile号,网页就直接跳转到了http://zinc15.docking.org/substances/ZINC000001758809/,这肯定是后台根据smile号查出ZINC号之后回应了一个重定向请求,猜想是这样,我们来看看实际情况。在浏览器中右键点击检查,查看在我们操作的过程中浏览器到底向后台发送了哪些请求。
从上图可以看到,我们一旦键入smile表达式之后,浏览器立马给后台发送了一个请求,然后网页显示出一个小分子的图像,很显然这个请求是为了获取小分子构象信息然后生成图片的,这个流程我们不做深究,我们要知道到底发送什么请求才能获得重定向后的地址,并拿到真正有用的网页。我们点击搜索,接着往下看:
在接下来的请求中,我们发现了一个关键请求(上图标红处),这个请求的响应体返回的是一个序列号,如下图:
不要小看这个序列号,虽然我也不知道它具体代表什么意思,但是后面的请求充分向我们说明了这个序列号的重要性,即后面需要smile表达式带上这个序列号一起发送一个HTTP请求,才能获取到那个关键的重定向网页,如下图:
到目前为止,这个网页的请求逻辑已经很清楚了,我们只需要利用python模仿浏览器发送同样的请求,首先获取这个inchikey序列号,然后通过这个序列号和smile表达式再次发起请求就能得到重定向的网址了,然后对这个重定向网址发起请求就能获得我们所需要的关键网页了,我们所需要的全部信息都包含在这个重定向后的网页里,然后只要解析这个html网页,从中提取出我们想要的信息就行了。思路已经很清晰了,可以撸代码了,具体Python代码如下:
1 #coding=utf-8
2
3 '''
4 @Author: jeysin@qq.com
5 @Date: 2019-6-1
6 @Description:
7 1、程序运行环境为python2.7,在python3中不能运行。
8 2、程序只会读取当前目录下名字为SMILE.txt的文件,运行前请先将包含smile表达式的文件命名为SMILE.txt后放在与本程序相同的目录下。
9 3、程序执行结果会以当前时间命名放在当前目录下。
10 4、程序运行快慢取决于SMILE.txt文件大小和当前网络状态,如果网络不通畅,程序会不断自动重试,请耐心等待。
11 5、如果当前网络状态不通畅,重试50次之后仍不通会跳到下一个smile分析,并在另一个文件里记录下当前失败的smile,该文件命名为当前时间+unfinished。
12 '''
13
14 import os,sys
15 import urllib
16 import urllib2
17 import json
18 import time
19 import re
20 from HTMLParser import HTMLParser
21 from datetime import datetime
22
23 headers = {
24 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
25 "Accept-Encoding": "gzip, deflate",
26 "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
27 "Host": "zinc15.docking.org",
28 "Referer": "http://zinc15.docking.org/substances/home/",
29 "Upgrade-Insecure-Requests": "1",
30 "Cookie": "_ga=GA1.2.1842270709.1559278006; _gid=GA1.2.1095204289.1559278006; _gat=1; session=.eJw9zLEKgzAQANBfKTd3qcRFcEgJBIdLQE7hbhFqW6pRC20hGPHf26nvA94G3XCFYoPDBQoQCjlm7YCzTDLWk6N2xBSi2CoKcXSzjGJ0zqkvYT9C_37du88z3JZ_gXP98MTJWY6eesXUKG85RwonTs3q6BzEyOQMrmirzCWtUJe_bv8CllwtkQ.D9Kh8w.M2p5DfE-_En2mAGby_xvS01rLiU",
31 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36"
32 }
33
34 #解析SEA Predictions
35 def getSeaPredictions(html):
36 seaPrediction = []
37 begin = html.find("SEA Predictions", 0)
38 if(begin == -1):
39 return seaPrediction
40 end = html.find("</tbody>", begin)
41 left = html.find("<td>", begin)
42 right = html.find("</td>", left)
43 pattern = re.compile('>(.*?)<')
44 while(right < end):
45 str = html[left:right+5]
46 str = str.replace("\n", "").replace("\t", "")
47 str = ''.join(pattern.findall(str))
48 str = str.strip()
49 seaPrediction.append(' '.join(str.split()))
50 left = html.find("<td>", right)
51 right = html.find("</td>", left)
52 if(left == -1 or right == -1):
53 break
54 return seaPrediction
55
56
57 #解析CAS numbers
58 def getCasNum(html):
59 result = re.search("<dt>CAS Numbers</dt>", html)
60 if(result == None):
61 return "None"
62 begin = result.span()[1]
63 begin = html.find("<dd>", begin, len(html))
64 begin = begin + 4
65 end = html.find("</dd>", begin, len(html))
66 if(begin + 1 >= end):
67 return "None"
68 str = html[begin:end]
69 casNumList = re.findall('[0-9]+-[0-9]+-[0-9]+', str)
70 if(casNumList == None):
71 return "None"
72 casNumStr = ""
73 for i in range(len(casNumList)):
74 casNumStr = casNumStr + casNumList[i]
75 if(i != len(casNumList)-1):
76 casNumStr = casNumStr + ","
77 return casNumStr
78
79 #定义一个新的Response类,不同的是,它的read方法是非阻塞的
80 class Response:
81 html = ''
82 newUrl = ''
83 def __init__(self, html, newUrl):
84 self.html = html
85 self.newUrl = newUrl
86 def read(self):
87 return self.html
88 def geturl(self):
89 return self.newUrl
90
91 #重写urlopen(),增加超时重试机制,以免网络不好程序卡死
92 def myUrlOpen(url):
93 failCount = 0
94 request = urllib2.Request(url, headers = headers)
95 while True:
96 try:
97 if(failCount > 50):
98 return Response(None, None)
99 print "Request: " + url
100 response = urllib2.urlopen(request, None, 10)
101 html = response.read()
102 #获取重定向url,因为有的函数要用到,所以这里统一获取一下,虽然并非所有网页都是重定向的,非重定向网页获取到的是原来的url
103 newUrl = response.geturl()
104 return Response(html, newUrl)
105
106 except:
107 failCount = failCount + 1
108 time.sleep(2)
109 print "Retry, ",
110 else:
111 break
112
113 #解析Vendors
114 def getVendors(html):
115 #获取More about字段结束的位置列表,在其附近查找Vendors
116 indexList = []
117 begin = 0
118 index = html.find("More about ", begin)
119 while(index != -1):
120 indexList.append(index + 11)
121 begin = index + 11
122 index = html.find("More about ", begin)
123
124 vendors = []
125 pattern = re.compile('>(.*?)<')
126 for i in range(len(indexList)):
127 begin = indexList[i]
128 end = html.find('">', begin)
129 vendors.append(html[begin:end])
130
131 begin = html.find("<td>", end)
132 end = html.find("</td>", begin)
133 str = html[begin:end+5]
134 vendors.append(''.join(pattern.findall(str)))
135
136 return vendors
137
138
139 #从url中提取出zincNum
140 def getZincNumFromUrl(url):
141 begin = url.find("ZINC", 0)
142 end = url.find("/", begin)
143 return url[begin:end]
144
145 #解析ZINC号
146 def getZincNumFromHtml(html):
147 result = re.search("Permalink", html)
148 if result is None:
149 return None
150 else:
151 begin = result.span()[1]
152 while(html[begin] != '\n'):
153 begin = begin +1
154 begin = begin + 1
155 end = begin
156 while(html[end] != '\n'):
157 end = end + 1
158 zincNum = html[begin:end]
159 return zincNum.strip()
160
161 def getVendorsPage(url):
162 response = myUrlOpen(url)
163 html = response.read()
164 return html
165
166 def getRedirectionUrlAndHtml(smile):
167 #获取Inchikey
168 encodeSmile = urllib.quote(smile, 'utf-8')
169 url = 'http://zinc15.docking.org/apps/mol/convert?from=' + encodeSmile + '&to=inchikey&onfail=error'
170 response = myUrlOpen(url)
171 inchikey = response.read()
172 if(inchikey == None):
173 return None, None
174
175 #获取重定向网址和网页
176 url = 'http://zinc15.docking.org/substances/?highlight=' + encodeSmile + '&inchikey=' + inchikey + '&onperfect=redirect'
177 response = myUrlOpen(url)
178 newUrl = response.geturl()
179 html = response.read()
180 return newUrl, html
181
182 #解析网页数据并写入文件
183 def parseHtmlAndWriteToFile(smile, zincNum, html1, html2, output):
184 print "ZINC number: " + zincNum
185 output.write("SMILE:\t" + smile + "\n")
186 output.write("ZINC number:\t" + zincNum + '\n')
187
188 casNum = getCasNum(html1)
189 print "CAS numbers: " + casNum
190 output.write("CAS numbers:\t" + casNum + '\n')
191
192 output.write('\n')
193
194 vendors = getVendors(html2)
195 if(0 == len(vendors)):
196 print "Vendors:\tNone"
197 output.write("Vendors:\tNone\n")
198 else:
199 print "Vendors:\t"+str(len(vendors)/2)+" total"
200 output.write("Vendors:\t"+str(len(vendors)/2)+" total\n")
201 i = 0
202 while(i < len(vendors)-1):
203 output.write(vendors[i]+" | "+vendors[i+1]+"\n")
204 i = i + 2
205
206 output.write('\n')
207
208 seaPrediction = getSeaPredictions(html1)
209 if(0 == len(seaPrediction)):
210 print "SEA Prediction:\tNone"
211 output.write("SEA Prediction:\tNone\n")
212 else:
213 print "SEA Prediction:\t"+str(len(seaPrediction)/5)+" total"
214 output.write("SEA Prediction:\t"+str(len(seaPrediction)/5)+" total\n")
215 i = 0
216 while(i < len(seaPrediction)-4):
217 output.write(seaPrediction[i] + " | " + seaPrediction[i+1] + " | " + seaPrediction[i+2] + " | " + seaPrediction[i+3] + " | " + seaPrediction[i+4] +"\n")
218 i = i + 5
219 output.write('\n\n\n')
220
221 def main():
222 inputFilename = "SMILE.txt"
223 currentTime = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
224 outputFilename = currentTime + ".txt"
225 outputFilename2 = currentTime + "-unfinished.txt"
226 with open(inputFilename, "r") as input, open(outputFilename, "w") as output, open(outputFilename2, "w") as unfinished:
227 for line in input.readlines():
228 smile = line.strip()
229 print "SMILE:\t" + smile
230
231 #获取重定向后的url和页面
232 newUrl, html1 = getRedirectionUrlAndHtml(smile)
233 if(newUrl == None or html1 == None):
234 print "SMILE:\t" + smile + ", unfinished\n"
235 unfinished.write(line)
236 continue
237
238 print newUrl
239
240 zincNum = getZincNumFromHtml(html1)
241 if(zincNum == None):
242 print "ZINC number:\tNone\n"
243 output.write("SMILE:\t" + smile + "\n")
244 output.write("ZINC number:\tNone\n")
245 continue
246
247 #获取包含Vendors的网页html2
248 url = newUrl + "catitems/subsets/for-sale/table.html"
249 html2 = getVendorsPage(url)
250 if(html2 == None):
251 print "SMILE:\t" + smile + ", unfinished\n"
252 unfinished.write(line)
253 continue
254
255 #当html1和html2都获取到的时候,开始解析它们并将数据写入文件
256 parseHtmlAndWriteToFile(smile, zincNum, html1, html2, output)
257
258 print "\n"
259
260
261 if __name__ == "__main__":
262 main()
以下是程序运行时截图:
最终抓取到的文本信息截图:
完美解决问题!!!
这里总结一下写爬虫需要注意哪些问题:
1、要摸清楚网站前后端交互的逻辑,要明确知道你的爬虫需要哪些网页,哪些网页是包含关键信息的网页,我们应该怎样构造请求获取它们。逻辑清晰了,思路就有了,代码写起来就快了。
2、解析html文件的时候思路灵活一点,各种正则表达式和查询过滤操作可以混着来,解析html文件归根到底还是对于字符串的处理,尤其是不规范的html文件,更能考验编程功底。当然了,如果学习过xpath的话,那解析html就会容易得多了。
以上,可能是我大学在校期间做的最后一个项目了,四年的大学生活即将结束,感慨颇多。毕业之后即将走上工作岗位,心里既有一份期待也有一些焦虑,程序员这条道路我走得并不容易,希望以后一切顺利,共勉!