数据挖掘第一步--爬网站

Jul. 25th, 2016


scrapy python 爬虫

最近开始看数据挖掘和机器学习之类的东西,于是开始看Python,之前一直听说Python效率低、运行得慢,虽然也感觉Python用途很广,但是也没有需求去学习它。从这几天的了解,瞬间就被Python圈粉了,相比于基本察觉不到的低效率,简单的语法和超级多的包简直让Python在各方面都很有用。前几天准备数模一直在熟悉Matlab的语法,用了好几天还是不太适应那种矩阵运算的语法,然后就发现了一套Python的科学计算工具包SciPy,包含了目前在Matlab里用到的好多函数,于是就直接转投Python了。

这次的需求是爬一些网站的数据,具体需求是这样的:

  1. 中国证监会并购重组反馈意见上把所有的公司名和反馈意见的日期及内容都提取下来。
  2. 通过公司全名搜索得到各公司对应的股票代码。
  3. 通过股票代码从新浪财经(例)中找到各公司对于反馈的回复及日期。
  4. 中国证监会重组委公告把各“工作会议公告”文章里涉及的公司和发文日期记录下来。
  5. 中国证监会重组委公告中把各“审核结果公告”文章里涉及的公司的审核结果和发文日期记录下来。
  6. 最终为每一个公司产生一个公司名称+股票代码+反馈意见日期(列表)+回复日期(列表)+会议公告日期+审核结果+审核公告日期的表项。

先在网上查了查,据说Scrapy是个很有名的爬虫包,就决定用它来试试。第一步首先熟悉API,照着官方toturial里的例程看了一下,就直接一步一步开始摸索着写了。

先是第一步,因为文章的标题里就含有需要的公司名称和日期,通过xpath和正则表达式re模块很容易获取得到,关键代码如下:

for sel in response.xpath('//ul[@id="myul"]/li'):
    name=sel.xpath('a/text()').re(u'(?<=关于).*?公司')
    date=sel.xpath('span/text()').extract()

然后就遇到了第一个问题,这些字段的存储问题。刚开始是用Scrapy内置的items来存储,但是在后面找股票代码的时候发现没有办法将代码字段添加到相应公司名所在的items对象里,于是就转而使用Python自带的dict数据类型来存储了。在这里,字典的键是公司的名称,值是反馈日期的列表(因为部分公司存在'二次回复'),上面代码中的namedate本身就是列表,只要将其遍历之后存在字典里即可,一共需要6个字典:

dic_feedback_date=dict()#存放(公司名:反馈日期列表)
dic_feedback_content=dict()#存放(公司名:反馈文章内容)
dic_stock=dict()#存放(公司名:股票代码)
dic_reply_date=dict()#存放(公司名:反馈回复日期列表)
dic_meet_date=dict()#存放(公司名:会议公告日期)
dic_result=dict()#存放(公司名:会议审核结果及日期)

但是由于Scrapy是异步抓取网页的,因此如果一些公司存在两次反馈,那么获取到的日期的先后顺序是不确定的,为了方便之后的处理,需要把顺序调整一下,这就需要在__init__函数中使用signal关联一个函数,以便在程序爬取完毕后可以调用,然后对数据进行统一处理,关键代码如下:

from scrapy import signals
from scrapy.xlib.pydispatch import dispatcher

def __init__(self):
    dispatcher.connect(self.spider_closed_dic,signals.spider_closed)

def spider_closed_dic(self,spider):
    for k,v in self.dic_feedback_date.iteritems():
        if len(v)>=2:
            v.sort()
            if len(v)>2:#如果反馈次数多于两次,只取最近的两次
                v=v[:2]

取完第一组数据之后打算把字典存到文件里看下结果,就遇到了第二个问题,中文的编码问题。爬下来的数据都是unicode编码,直接写到文件里就是一堆\u开头的数字集,后来发现存到字典中之后,通过遍历每一个键值对进行写入的方式可以避免这个问题,另外在网上找到了另一个解决方案,关键代码如下:

self.file0=codecs.open('date.json','wb',encoding='utf-8')
    #取下数据
for sel in select:
    name=sel.xpath('a/text()').re(u'(?<=关于).*?公司')
    date=sel.xpath('span/text()').extract()

    #存入字典中时使用'utf-8'编码
    ne=name[-1].encode('utf-8')
    de=date[-1].encode('utf-8')
    self.dic_feedback.setdefault(ne,[]).append(de)

    #以行为单位写入数据到json文件中
    line=json.dumps(ne+' '+''.join(self.dic_feedback[ne]))+'\n'
    self.file0.write(line.decode('unicode_escape'))    

在取文章内容的时候,因为不同时间的文章的html文件里的标签名称和位置都不一样,所以就要做很多适配的工作,查找逻辑是这样的:

for sel in response.xpath('//div[@class="content"]'):
    # 网站div标签加的千奇百怪,差评!
    segment = sel.xpath('div[@class="Custom_UnionStyle"]/div/p').extract()
    #每一个segment标签是文章中由<p>标签分割的一段
    if segment==[]:
        segment = sel.xpath('div[@class="Custom_UnionStyle"]//p').extract()
    if segment==[]:
        segment = sel.xpath('div[@class="Custom_UnionStyle"]/p').extract()
    if segment == []:
        segment = sel.xpath('//p').extract()
    if segment==[]:
        segment=sel.xpath('p').extract()

接下来是第二步,通常财经类网站都会给出公司的股票代码以及简称,如果知道了公司的简称,很容易就能获取到股票代码,但是全称就行不通了,只能用别的方法,这里就直接使用百度来搜了,通过下面三重循环进行过滤。后来为了验证股票代码找的是否准确,我又通过新浪财经通过股票代码找到各公司的简称,也加到dic_stock字典中,发现还挺准确。关键代码如下:

result=response.xpath('//div[@class="result c-container "]')
for sel in result:
    stock=sel.xpath('h3/a/text()').re(u'(?<=\()[0-9]{6}(?=\))')
    #首先在查询到的每条结果的主体中寻找像 (000892) 这样的数字
for sel in result:
    stock=sel.xpath('div[@class="c-abstract"]/text()').re(u'(?<=:)[0-9]{6}(?=[^0-9])')
    #如果找不到,在每条结果的附加部分(小字部分)中寻找像 :000892 这样的数字
for sel in result:
    stock=sel.xpath('div[@class="c-abstract"]/text()').re(u'[0-9]{6}(?=[^0-9])')
    #如果还是找不到,就找像 000892 这样的数,这里出现错误的几率就比较大了

每个for循环中,一旦找到符合要求的数字,就在字典dict_stock中存储公司名:股票代码的键值对。这里还存在一个问题,就是参数传递的问题。公司名称在函数中作为查询参数请求一个特定的url,接收到response之后调用一个回调函数处理得到的html数据,这时无法直接向回调函数中传递参数,根据官方文档,数据要这样进行传递:

def parse():
    #先通过处理别的数据获得公司名称 name
    ...
    url='http://www.baidu.com/s?wd='+name+'%20'+u'股票代码'
    request=scrapy.Request(url,callback=self.parse_content)
    request.meta['name']=name #要传递的变量
    yield request
def parse_content():
    name=response.meta['name']
    ...

第三步是获取各公司回复反馈的日期,这里同样存在可能有两次回复的情况,再加之每个公司回复的标题都不尽相同,导致准确提取那些反馈有一些问题,而且有很多干扰项要一一排除,最后的结果不很完善,会遗漏掉一部分回复。关键代码是这样的:

    #content为文章标题的列表,date为对应日期的列表
    for i in xrange(len(content)):
        sec = re.findall(u'.*关于.*中国证监会.*二次.*反馈意见.*[回|答]复.*', content[i])
        sec=self.check(sec)
        if sec == []:
            sec = re.findall(u'.*发行股份.*募集配套资金.*二次.*[(反馈)|(审核)]意见.*[回|答]复.*', content[i])
            sec = self.check(sec)
        if sec == []:
            sec = re.findall(u'.*[回|答]复.*发行股份.*募集配套资金.*二次.*[(反馈)|(审核)]意见.*', content[i])
            sec = self.check(sec)
        if sec != []:
            self.dic_reply_date.setdefault(name, []).append(date[i].encode('utf-8'))
            continue
        ...#同理去掉'二次'关键字再进行查找,如果找到就写入字典,并返回
    ...#如果没找到,或只找到'二次反馈',则跳到下一页继续寻找

def check(self,res):
    if res != []:
        for j in xrange(len(res)):
            if re.match(u'.*公告.*', res[j]) or re.match(u'.*有限.*公司.*', res[j]) or re.match(u'.*事务所.*',res[j]):
                res.pop(j)
    return res    

第四部和第五步几乎一样,都是要通过文章标题的链接,进入到文章的页面,把文章的内容提取出来,并在文章中找到公司名,与发文的日期联系起来。在第五步时,除了找到公司名,还需要找到审核的结果,因为html文件的格式都很一致,所以找起来没有什么难度,第四步的关键代码如下:

def parse_meet_content(self,response):
    for sel in response.xpath('//div[@class="row"]'):
        meet = sel.xpath('li[@class="mc"]/div/a/text()').re(u'并购重组委.*工作会议公告.*')
        date = sel.xpath('li[@class="fbrq"]/text()').extract()
        link = sel.xpath('li[@class="mc"]/div/a/@href').extract()
        url = response.urljoin(link[0])
        if meet != []:
            request = scrapy.Request(url, callback=self.parse_record_content)
            request.meta["date"] = date[0]
            yield request

def parse_record_content(self,response):
    date = response.meta["date"].encode('utf-8')
    for sel in response.xpath('//div[@class="content"]/div'):
        cont = sel.xpath('p').extract()
        dr = re.compile(r'<[^>]+>', re.S)
        for i in cont:
            dd = dr.sub('', i)
            res = re.findall(u'[\u4e00-\u9fa5|\uff08|\uff09]+(?=(发行)', dd)
            if res != []:
                name = res[0].encode('utf-8')
                self.dic_meet_date[name] = date    

还是因为之前一直写C++的缘故,而且用的还都是很基础的语法,所以对整个程序的实现都很清楚,而Python众多的包封装了一堆功能很强大的对象和方法,用起来还不太适应,就像Scrapy自带的Items对象,据说很好用的样子。

接下来是要对那些反馈的内容做分词和特征选择等等,未完待续~