用Python實現一個爬蟲爬取ZINC網站進行生物信息學數據分析


  最近接到實驗室的導師交給我的一個任務,就是他們手頭有很多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就會容易得多了。

 

以上,可能是我大學在校期間做的最后一個項目了,四年的大學生活即將結束,感慨頗多。畢業之后即將走上工作崗位,心里既有一份期待也有一些焦慮,程序員這條道路我走得並不容易,希望以后一切順利,共勉!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM