实验原因:

目前有一个医疗百科检索项目,该项目中对关键词进行检索后,返回的结果很多,可惜结果的排序很不好,影响用户体验。简单来说,搜索出来的所有符合疾病中,有可能是最不常见的疾病是排在第一个的,而最有可能的疾病可能需要翻很多页才能找到。

 

实验目的:

为了优化对搜索结果的排序,想到了利用百度搜索后有显示搜索到多少词条,利用这个词条数,可以有效的对疾病排名进行一个优化。从一方面看,某一个疾病在百度的搜索词条数目越多,表示这个词条的信息特别丰富,侧面反映了搜索这个词条的人特别多,从而可以推出这个疾病在人群中可能是发生概率较高的。反过来看,如果一个疾病很罕见,人们只有很低的概率会患这种疾病,那么相应的搜索这个词条的人也就会少,相应的网页也就少,因此搜索引擎搜索出来的词条数目也就会少。

 

实验过程:

 

第一阶段:从数据库中获取疾病名称

  这一阶段涉及到如何利用python从数据库中提取数据,我利用的是MySQLdb库,通过利用以下代码建立连接提取数据:

 

db = MySQLdb.connect('localhost', 'root', '', 'medical_app', charset = 'utf8')
cu = db.cursor()
cu.execute('select * from table order by id')

 

  其中在connect函数中我设置了charset属性,这是因为如不这样做python读取出来的数据库中文信息会变成乱码。

 

  在这一阶段,我编写了一个dbmanager类,主要负责数据库的读取和插入工作,刚才介绍的已经可以完成一个读取任务了,那么怎么利用python对数据进行添加呢?

cu.execute('insert into table(id,name) value(?, ?)',[a,b])

 

第二阶段:完成数据的爬取

  最初我尝试了利用python的urllib来对百度网页进行爬取,发现这条路是走不通的,百度发现是机器爬取后会返回错误的网页代码。

  于是就想到了模拟浏览器的方式来对百度网页进行互动,爬取网页的内容。找到了mechanize库,表示这个库非常好用,使用非常简单。除了能够模仿浏览器读取网页以外,还可以和网页进行交互操作,然后还可以设置机器人选项,通过这个选项可以读取屏蔽机器人的网页,比如说百度。

 
br = mechanize.Browser()
br.open("http://www.example.com/")
# follow second link with element text matching regular expression
response1 = br.follow_link(text_regex=r"cheese\s*shop", nr=1)
assert br.viewing_html()
print br.title()
print response1.geturl()
print response1.info()  # headers
print response1.read()  # body

br.select_form(name="order")
# Browser passes through unknown attributes (including methods)
# to the selected HTMLForm.
br["cheeses"] = ["mozzarella", "caerphilly"]  # (the method here is __setitem__)
# Submit current form.  Browser calls .close() on the current response on
# navigation, so this closes response1
response2 = br.submit()
 

以上是简单的mechanize的使用说明。以下是官网对mechanize的简单说明,不得不说,这个配合HTML解析器用起来太方便了。

  • mechanize.Browser and mechanize.UserAgentBase implement the interface of urllib2.OpenerDirector, so:

    • any URL can be opened, not just http:

    • mechanize.UserAgentBase offers easy dynamic configuration of user-agent features like protocol, cookie, redirection and robots.txt handling, without having to make a new OpenerDirector each time, e.g. by callingbuild_opener().

  • Easy HTML form filling.

  • Convenient link parsing and following.

  • Browser history (.back() and .reload() methods).

  • The Referer HTTP header is added properly (optional).

  • Automatic observance of robots.txt.

  • Automatic handling of HTTP-Equiv and Refresh.

  关于BeautifulSoup,这是一个在数据挖掘领域非常好用的HTML解析器,他将网页解析成有标签所构成的树形字典结构,想要找到网页中的某一元素用find函数非常轻松的就可以找到。

  

html = urllib.open('http://mypage.com')
soup = BeautifulSoup(html.read())
soup.find('div', {'class':'nums'})

  上面这句话的意思是,找到网页中class属性为nums的div标签内容。

 

第三阶段:异常的捕获和超时设置

  爬取网页内容的爬虫已经写好,拿出来跑一跑,发现百度经常性的会返回一些错误的网页导致网页无法正确填充表格或者分析,这个时候我们需要抓取这里面的异常让程序能够持续运行,让抓取出现异常的疾病名称放回队列稍后再进行爬取。关于异常捕获,代码如下:

  

 
 1 try:
 2    br = mechanize.Browser()
 3    br.set_handle_robots(False)
 4    br.open(URL)
 5    br.select_form('f')
 6    br['wd'] = name[1].encode('utf8')
 7    response = br.submit()
 8    #print 'form submitted, waiting result...'
 9    #分析网页,有可能百度返回错误页面
10    soup = BeautifulSoup(response.read())
11    # text = soup.find('div', {'class':'nums'}).getText()
12    if soup.find('div', {'class':'nums'}):
13       text = soup.find('div', {'class':'nums'}).getText()
14    else:
15       print '$Return page error,collect again...'
16       self.manual.push_record(name)
17       continue
18             except socket.timeout:
19                 print '$there is an error occur.it will check later...'
20                 self.manual.push_record(name)
21                 print name[1],' pushed into the list.'
22                 continue
 

  这里可以看到为了提高检索效率,设置了一个超时异常,利用socket组件中的timeout。在这段代码之前我们需要设置下超时时间

 
1 import socket
2 
3 socket.setdefaulttimeout(5)
4 
5 try:
6     ...
7 except socket.timeout :
8     print 'timeout'
 

  这段示例代码设置的是5秒钟的超时时间。

  

  到目前为止,利用这些知识,我已经完成了一个在爬取过程中不会出错的一个单线程爬虫模块,显然这个爬虫爬取内容的效率是非常慢的。我决定用多线程来让它快起来。

 

第四阶段:多线程的爬虫控制

  这一阶段,我们需要设计一个可以进行多线程网页爬取的爬虫设计。这里面我们主要考虑两点:1,如何实现多线程;2,关于公共变量的同步读取问题如何实现。

  对于第一个问题,在python中多线程的实现有两种方式:

    第一种是函数式:

函数式:调用thread模块中的start_new_thread()函数来产生新线程。语法如下:

thread.start_new_thread ( function, args[, kwargs] )

参数说明:

  • function - 线程函数。
  • args - 传递给线程函数的参数,他必须是个tuple类型。
  • kwargs - 可选参数。

 

   第二种是线程模块:

Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。

thread 模块提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start():启动线程活动。
  • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

  之前尝试了利用第一种方式来实现函数,发现这样实现出来的代码结构很不清晰,在变量同步的时候回会显得非常混乱。于是采用了第二种方法来实现这个函数:

 
class myThread (threading.Thread):   #继承父类threading.Thread

    def __init__(self, threadID, name, Manual):
        threading.Thread.__init__(self)
        self.lock = thread.allocate_lock()
        self.threadID = threadID
        self.name = name
        self.manual = Manual

    def run(self):                   #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
        print "Starting " + self.name
        self.get_rank()
        print "Exiting " + self.name

    def get_rank(self): #爬虫代码,持续获取疾病得分
        ...
        ...
 

  如何实现线程的运作呢?代码如下:

 
for i in xrange(thread_count): #建立线程
    mythread = myThread(i,'thread-'+str(i),m)
    thread_queue.append(mythread)
for i in xrange(thread_count): #启动线程
    thread_queue[i].start()
for i in xrange(thread_count): #结束线程
    thread_queue[i].join()
 

  现在我们已经可以通过利用线程来对疾病进行爬取了,可是在对爬取结果怎么进行同步存储,怎么对疾病名称进行同步读取呢?

 

第四阶段:变量的同步操作

  这一阶段我们需要设计好如何才能够可行的对同步变量进行操作,这一方面是比较烧脑的。。。设计如下:

  利用python爬取海量疾病名称百度搜索词条目数的爬虫实现

  其中B类是线程类,A类是同步变量控制类,A类的主要功能是提供变量V1,V2的同步操作,包括读取写入之类的。B类是线程类,负责爬取网页的数据。

  B类在上面已经说过了,A类的实现如下:

 
class Manual: #同步变量控制

    def __init__(self, names):
        self.names = names
        self.results = []
        self.lock = threading.RLock()

    def get_name(self): #获得疾病名称
        self.lock.acquire()
        if len(self.names):
            name = self.names.pop()
            #print 'name get'
            self.lock.release()
            return name
        else:
            self.lock.release()
            return None

    def put_result(self, result): #存放得分
        self.lock.acquire()
        self.results.append(result)
        print '(%d/6811)' %len(self.results)
        self.lock.release()

    def push_record(self, name): #放回获取失败的疾病名
        self.lock.acquire()
        self.names.append(name)
        self.lock.release()
 

 

  最后,所有部分都已实现,就差组装起来跑动啦。目前在实验室苦逼的跑啊跑中。。网不给力,拿了4个线程跑,保守目测得4个小时。

 

【原创-Blaxon】