首页 > 代码库 > python自然语言处理1——从网络抓取数据

python自然语言处理1——从网络抓取数据

python自然语言处理1——从网络抓取数据

写在前面

                    本节学习python2.7 BeautifulSoup库从网络抽取数据的技术,检验之简而言之就是爬虫技术。网络编程是一门复杂的技术,在需要基础的地方,文中给出的链接地址,都是很好的教程,可以参考,我在这里不在重复发明轮子。本节的主旨在于: 帮助快速掌握基本爬虫技术,形成一条主线,能为自己的实验构造基础数据。掌握爬虫技术后,可以从网络抓取符合特定需求的数据供分析,这里学习的爬虫技术适用于数据挖掘、自然语言处理等需要从外部挖掘数据的学科。



1.什么是网络爬虫?

     网络爬虫(Web crawler)也叫蚂蚁(ant),自动检索工具(automatic indexer),简单来说,就是通过不断请求各个网络资源,然后整理它们,形成自己的数据目录。具体的展开叙述可以参考wiki 网络蜘蛛。

  这里用自己的话表达就是:  网络爬虫 = {一组初始url,一组筛选数据规则,一种爬取网络自然的技术}

  简单网络爬虫的算法伪代码如下(参考资stackoverflow):

一个未访问列表
一个已经访问列表 
以及一组判定你对该资源感兴趣的规则
while 未访问url列表不为空:
      从未访问url列表取出一个URL
      记录下URL页面上你感兴趣的东西
      如果它是HTML:
	解析出页面的链接links
	对于每一个链接link:
		如果它符合你的规则,并且不在你的已访问列表中或者未访问列表中:
			将链接添加到未访问列表
对于搜索引擎等对数据要求较高的话,则需要更高级的爬虫技术,这不在本节讨论范围内。

网络爬虫技术必须要注意几点(参考自:A few scraping rules):

  • 重视网站的版权声明。这些数据的所有权是网站,网站管理它们,你应该在爬取一个网站之前查看网站的版权条款。
  • 不要太暴力。机器访问速度远远高于人,不要以粗暴地方式攻击别人的服务器。
  • 爬虫结果受限于网站结构。随着网页改动,网站数据更改和维护,原本工作的代码可能要随之变动。


2.Python爬虫技术主要有哪些?

Python语言中主要的爬虫库包括:

  • Scrapy
  • BeautifulSoup +urllib2
  • Mechanize
  • Twill
  • HarvestMan
  • Ruya

可以参考: stackoverflow获取更多库支持。

我们主要还是关注如何方便我们达到目的即可,本节主要使用BeautifulSoup来展开后面的示例。

BeautifulSoup的中文文档地址:  BeautifulSoup中文.

3. 爬虫技术关键点

3.1 寻找到我们想要的内容

    如何在找到的html或者xml中查找我们感兴趣的内容,主要有两个方面。

   第一如何获取网页中对应的节点。根据不同库的实现不同,稍微有些不同。但是基本上都包括:

    遍历DOM对象,利用标签过滤,例如a,选择所有的链接. DOM以树的形式组织整个HTML文档,例如简单DOM树如下:

  技术分享

    如何学会使用DOM对象,可以参考: HTML DOM.

   利用CSS选择器过滤对象,例如p > span ,选择段落的直接子标签span.可以参考 W3school CSS 选择器.

   利用正则表达式过滤标签内容,从而获得我们感兴趣的文本关于如何使用正则表达式,可以参考:Python正则表达式指南.

3.2 爬虫的一些技巧

 身份伪装

    个别网站不允许我们爬取,需要伪装成浏览器才可以,通过在请求头中添加浏览器和操作系统信息达到伪装目的。例如csdn网站,如果直接爬取,则容易出现403错误。

 数据解压缩

  注意使用伪装手段时,需要查看返回的相应头的编码方式,如果没有对gzip类型进行解压缩,那么通常会发生数据无法读取的错误,或者产生:UnicodeDecodeError: ‘utf8‘ codec can‘t decode byte 0x8b in position 1: invalid start byte错误。

 数据编码

 python2.7内部默认编码采用ASCII方式,str类型即采用改编码方式,当我们读入网页后,需要解析网页头部,识别编码(当然好的库会自动帮我们完成),这里注意str与unicode之间的转换:

u = u'中文' #显示指定unicode类型对象u
str = u.encode('gb2312') #以gb2312编码对unicode对像进行编码
str1 = u.encode('gbk') #以gbk编码对unicode对像进行编码
str2 = u.encode('utf-8') #以utf-8编码对unicode对像进行编码
u1 = str.decode('gb2312')#以gb2312编码对字符串str进行解码,以获取unicode
更多内容,可以参考: cnblog python encode和decode函数说明.

这里给出一个应用了上述三个关键点的,给出一个利用系统自带的urllib2爬取CSDN官方博客类别列表的代码:

# coding: utf-8
# """获取csdn博客类别列表"""

import urllib2
import gzip
import StringIO
import re
from urllib2 import URLError, HTTPError


def read_data(resp):
    # 读取响应内容 如果是gzip类型则解压缩
    if response.info().get('Content-Encoding') == 'gzip':
        buf = StringIO.StringIO(resp.read())
        gzip_f = gzip.GzipFile(fileobj=buf)
        return gzip_f.read()
    else:
        return resp.read()

URL = 'http://blog.csdn.net/blogdevteam/'
HEADERS = {
    'User-Agent': "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:34.0) "
    "Gecko/20100101 Firefox/34.0"
    , 'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
    , 'Accept-Language': "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3"
    , 'Accept-Encoding': "gzip, deflate"
}
req = urllib2.Request(URL, None, HEADERS)  # 通过添加头来达到伪装目的
try:
    response = urllib2.urlopen(req)
    page_content = read_data(response)
    encoding = response.headers['content-type'].split('charset=')[-1]
    p = re.compile(ur'(<a.*?href=http://www.mamicode.com/)(.*?category.*?>)(.*)()')  # 解析类别的正则表达式>


  输出结果为:

 

首页公告栏
专家访谈
使用小技巧
投诉建议
常见问题
网友心声
博客活动
精品下载资源推荐
2011中国移动开发者大会
CSDN官方活动
2011年SD2.0大会
2012 SDCC中国软件开发者大会
2012移动开发者大会
SDCC大会
2013年云计算大会
微软MVP
社区周刊
博客推荐汇总
博文推荐汇总


4.两个爬虫示例

下面示例部分我们给出通过使用BeautifulSoup库抓取数据的实验。

BeautifulSoup获取网页标签的关键技术。

一是使用find,find_all选择标签获取,这个函数的原型为:

find( name , attrs , recursive , text , **kwargs )

可以使用标签名称,属性,以及是否递归搜索,和一些关键字指定的属性。

另一个是通过CSS选择器,select获取标签列表,例如soup.select(‘p‘)获取所有段落。

BeautifulSoup中文文档.,给出了很详细的API说明,可以参考。

获取网页数据的关键点:

  •    分析网页结构,找到我们所需部分的最简单标志,例如CSS选择器标志或者标签标志
  •    利用正则表达式或者标签属性进行过滤
  •    抓取的效率问题,可以采用多线程等技术
  •    正确的序列化抓取的资源,例如写入文件,数据库

4.1 抽取酷狗音乐网站首页音乐排行榜数据

  抓取网页数据的第一步便是分析网页结构,分析网页结构可以借助浏览器(Firefox,Google chrome等)的分析工具,在所需要的趋于右键鼠标[查看元素]即可获取该页面元素的对应的选择器和相应代码。

我们查看下酷狗音乐首页的排行榜div,我们要抓取的三个部分的排行榜div如下图所示:

推荐歌曲排行榜:

  技术分享


Top10排行榜:

技术分享

全球热榜:

技术分享


这三个区域的共同点在于:

 首先一个大div,div下面有一个p,p中包含了这个排行榜里面的小分类,然后下面的div里面有ul具体填写了歌曲信息,你可以通过分析该网页来了解具体细节。

下面是我们的实现代码:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
"""
*********************************************************
获取酷狗音乐首页排行榜列表
依赖于html页面div选择器 如果改动程序可能失效
by wangdq 2015-01-05(http://blog.csdn.net/wangdingqiaoit)
*********************************************************
"""

from bs4 import BeautifulSoup
from urllib2 import urlopen, Request, URLError, HTTPError
import time


def make_soup(url):
    # """打开指定url 获取BeautifulSoup对象"""
    try:
        req = Request(url)
        response = urlopen(req)
        html = response.read()
    except URLError, e:
        if hasattr(e, 'code'):
            print '错误码: ', e.code, ',无法完成请求.'
        elif hasattr(e, 'reason'):
            print '请求失败: ', e.reason, '无法连接服务器'
    else:
        return BeautifulSoup(html)


def get_music(b_soup, sel):
    #""" 获取歌曲榜单"""
    main_div = b_soup.select(sel)[0]
    # 获取类别列表
    sum_category = main_div.select('p > strong > a[title]')[0].string
    titles = [sum_category+' '+a.string for a in main_div.select('p > span > a[title]')]
    index = 0
    song_dict = {}
    # 逐个解析下层的歌单并加入类别:歌单 字典对象
    for div in main_div.find_all('div', recursive=False):   # 这里我们不能递归查找
        part = div.find_all('span', class_='text')
        if part:
            song_dict[titles[index]] = part
            index += 1
    return song_dict


BASE_URL = 'http://www.kugou.com/'

#这是酷狗首页榜单的div选择器
#如果页面变动 需要更改此处
DIV_LIST = [
    'div#single0',   # 推荐歌曲部分div
    'div.clear_fix.hot_top_10',  # 热榜top10部分div
    'div.clear_fix.hot_global.hot_top_10'  # 全球热榜部分div
]


def main():
    soup = make_soup(BASE_URL)
    if soup is None:
        print '抱歉,无法完成抽取任务,即将退出...'
        exit()
    print '获取时间: '+time.strftime("%Y-%m-%d %H:%M:%S")
    for k in DIV_LIST:   # 从歌单div逐个解析
        for category, items in get_music(soup, k).iteritems():  # 打印类别:歌单字典对象内容
            print '*'*20+category+'*'*30
            count = 1
            for song in items:
                print count, song.string
                count += 1
    print '*'*60
    print '获取歌单结束'

if __name__ == "__main__":
    main()

获取的酷狗首页排行榜结果如下(有删节):

获取时间: 2015-01-06 22:29:11
********************推荐单曲 现场******************************
1 韩红 - 天亮了(Live)
2 A-Lin - 给我一个理由忘记(Live)
...省略
********************推荐单曲 华语******************************
1 王力宏 - 就是现在
2 梁静茹 - 在爱里等你【只因单身在一起主题曲】
...省略
********************推荐单曲 日韩******************************
1 孝琳、San E - Coach Me
2 Hello Venus - Wiggle Wiggle
...省略
********************推荐单曲 欧美******************************
1 Glee Cast - Problem
2 Justin Bieber、Lil Twist - Intertwine
...省略
********************热榜TOP10 最新******************************
1 EXO - Machine(Live)
2 庄心妍 - 有爱就不怕
...省略
********************热榜TOP10 最热******************************
1 筷子兄弟 - 小苹果
2 邓紫棋 - 喜欢你
...

获取歌单结束

  

4.2 抽取免费图库网站图片资源

下面学习抓取网络上的图片资源,其他资源同理。这里使用免费网站素材CNN的站点进行抓取。

我们在必要的时候需要使用多线程技术,关于多线程的简单使用,不是很复杂,可以参考: Python 多线程教程。

我们首先分析这个网站的结构,如下:

技术分享


这个网站的搜索关键词就是网页URL中出现的部分,也就是URL = http://so.sccnn.com+儿童+1.html,其中1是当前页号码,图中显示一共有145页,我们需要三个步骤来完成抓取:

第一,获取一个搜索关键词的结果个数包括总数目、页数目;

第二,通过关键词和页数目,拼接URL,解析每个页面包含的图片列表,这个页面很简单,图片主要使用img标签来标注;

第三,当所有图片URL都解析完毕后,开启多个线程下在,并通知用户下载情况。

解析结果数目主要代码如下:

result_string = unicode(result.get_text()).encode('utf-8')  # 例如 共计:2307个 分为145页 每页16条
    p = re.compile(r'\d+')
    count_list = [int(x) for x in p.findall(result_string)]  # 计数例如 ['2307', '145', '16']

线程使用python的threading库,通知客户端下载情况使用progress 1.2,这个库很简单。

具体实现代码如下:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
"""
*********************************************************
多线程下载素材CNN免费图片 该网站网址: http://so.sccnn.com
by wangdq 2015-01-05(http://blog.csdn.net/wangdingqiaoit)
使用方法:
按照提示输入图片保存路径 例如: /home
要查找图片的关键字即可  例如: 宠物
*********************************************************
"""

from bs4 import BeautifulSoup
from progress.bar import Bar
import urllib2
import os
import re
import threading

BASE_URL = "http://so.sccnn.com"


def make_soup(url):
    # """打开指定url 获取BeautifulSoup对象"""
    try:
        req = urllib2.Request(url)
        response = urllib2.urlopen(req)
        html = response.read()
    except urllib2.URLError, e:
        if hasattr(e, 'code'):
            print '错误码%d,无法完成请求' % e.code
        elif hasattr(e, 'reason'):
            print '错误码%d,无法连接服务器' % e.reason
    else:
        return BeautifulSoup(html)


def join_url(keyword, count):
    #"""链接url路径"""
    unicode_url = unicode(BASE_URL+'/search/'+keyword+'/'+str(count)+'.html', 'utf-8')
    return unicode_url.encode('GB2312')


def load_url(keyword, folder):
    #"""搜索关键字 得到图片img元素列表"""
    local_url = join_url(keyword, 1)
    soup = make_soup(local_url)
    img_list = []
    if soup is None:
        return img_list
    result = soup.find('td', style=True)
    if result is None:
        print '没有找到任何关于"%s"的图片,请重试其他关键词' % keyword
        return img_list
    result_string = unicode(result.get_text()).encode('utf-8')  # 例如 共计:2307个 分为145页 每页16条
    p = re.compile(r'\d+')
    count_list = [int(x) for x in p.findall(result_string)]  # 计数例如 ['2307', '145', '16']
    print '已经找到%d张"%s"图片,共%d页' % (count_list[0], keyword,count_list[1])
    url_bar = Bar('正在解析图片地址', max=count_list[1])
    for x in range(count_list[1]):
        page_soup = make_soup(join_url(keyword, x+1))
        images = page_soup.find_all('img', alt=True)
        img_list.extend([img for img in images if img.has_attr('src') and img.has_attr('alt')])
        url_bar.next()
    return img_list


class DownImage(threading.Thread):
    #"""下载图片线程类"""
    def __init__(self, img_list, folder, bar):
        threading.Thread.__init__(self)
        self.img_list = img_list
        self.folder = folder
        self.bar = bar

    def run(self):
            for img in self.img_list:
                photo_url = img['src']
                try:
                    u = urllib2.urlopen(photo_url)
                    # 使用图片说明作为图片文件名
                    with open(os.path.join(self.folder, img['alt']+photo_url.split('.')[-1]), "wb") as local_file:
                        local_file.write(u.read())
                    self.bar.next()
                    u.close()
                except KeyError, urllib2.HTTPError:
                    print '下载图片%s时出错' %  img['alt']
                except KeyboardInterrupt:
                    raise


def main():
    #"""使用新线程开始获取数据"""
    max_thread = 5  # 最多同时开启5个线程下载
    folder = raw_input('请输入图片存储路径:')
    if not os.path.exists(folder):
        print '文件夹"%s"不存在' % folder
        exit()
    image_list = []
    while not image_list:
        key = raw_input('请输入搜索图片的关键词:')
        image_list = load_url(key, folder)
    try:
        down_count = len(image_list)
        bar = Bar('正在下载图片', max=down_count)
        threads = []
        unit_size = len(image_list) / max_thread    # 每个线程承担任务量
        for i in xrange(0, len(image_list), unit_size):
            thread = DownImage(image_list[i:i+unit_size], folder, bar)
            thread.start()
            threads.append(thread)
        for thread in threads:
            thread.join()
    except threading.ThreadError:
        print '抱歉,无法启动'
    except KeyboardInterrupt:
        print '用户已取消下载'
    print '\n下载已完成!'

if __name__ == "__main__":
    print __doc__
    main()

运行情况如下图所示:

技术分享


下载后的图片如下图所示:

技术分享



小节

  通过使用BeautifulSoup库,我们熟悉了一般爬虫技术,通过本节的两个示例,我相信可以拓展到一般情况下的抓取,只是具体问题要具体分析,在规则、效率等方面要仔细考虑。

python自然语言处理1——从网络抓取数据