返回介绍

3.12 内容延伸:非结构化数据的预处理

发布于 2024-01-27 22:54:28 字数 25785 浏览 0 评论 0 收藏 0

3.12.1 网页数据解析

本节通过一个稍微复杂一点的示例,来演示如何抓取并解析网页数据。之所以说复杂,是因为本节中会出现几个本书中未曾提及的知识和方法,从代码数量来看也会比之前的示例稍微长一点。

本示例中,将使用requests、bs4、re、time库进行网页数据读取、解析和相关处理。

示例的目标是抓取亚马逊中国网站苹果手机和配件的价格,用于做竞争对手的标杆商品价格监控。注意:本示例仅做学习之用。

在抓取和解析网页数据之前,首先要做的是做网页内容分析,包括:

要抓取的内容格式:文本、图像还是其他文件。

是否存在重定向:重定向往往根据User-Agent来判断,例如手机端、电脑端所看到的页面信息不同。

是否需要验证:很多网页爬虫都需要用户登录、验证码等。

目标数据是否具有统一标签规则:要爬取的数据是否具有统一的HTML标签,这决定了后期处理的便捷性。

2大多数情况下网页爬取都不是只有一个页面,而是多个页面,因此需要了解不同页面的URL规则,尤其是带有条件查询的,需要了解具体参数。

2根据实际要爬取的数据,分析可能会产生哪些字段,会有哪些冲突、包含关系及关联性影响等。

点击亚马逊中国网站左侧进入二级导航“手机通讯”中的Apple Phone:https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=1&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051

1.要抓取的内容格式分析

笔者的浏览器是Chrome,在当前页面中按快捷键F12,打开开发者工具(或者点击右上角,在弹出的菜单中选择更多工具-开发者工具),点击切换到查看页面元素视图。点击开发者工具栏左侧的,然后点击页面中的商品价格、标题等,多测试几个商品,发现价格是文本格式(不是调用的外部图像)。同样的方法点击商品描述,分析商品标题的特点。商品标题用来存储与某个价格对应的商品,该标题中的关键字可用于识别具体商品型号并与企业自身商品做比较,如图3-5所示。

图3-5 要抓取的内容格式分析

2.是否存在重定向分析

点击开发者工具左侧的第二个图表,通过浏览器模拟移动设备环境,然后按F5刷新该页面。原来适配到电脑上的页面现在改为适配移动端,然后按照刚才的方法查看要抓取的数据格式是否仍然相同;除了显示的样式不同可能导致的页面展示和规则不同外,不同平台的商品价格可能存在不同(例如移动端比电脑端便宜10元),如果需要区分平台,那么可以分开抓取。本节仅以电脑上的可视网页为例进行抓取。相关操作如图3-6所示。

3.是否需要验证分析

由于页面中浏览商品时没有任何需要登录、注册等验证信息,因此无须验证可直接访问。

4.目标数据是否具有统一标签规则

通过查看源代码,发现不同的商品通过li标签进行列表展示,每个li下对应一个商品。在li子层级的标签中,类为"a-size-base a-color-price s-price a-text-bold"的span标签包含了价格信息,h2标签总则包含了商品标题,如图3-7所示。因此我们只需要找到这两个特征的标签即可解析出目标数据。

图3-6 重定向分析

图3-7 主要商品信息所在的标签

相关知识点:HTML标签

超文本标记语言(HTML)的标签是HTML语言的基本单位,也是设计网页的基本元素。我们查看源代码(在任意页面右击,在弹出的菜单中点击“查看页面源代码”,或使用快捷键Ctrl+U),都能发现HTML标签构成了所有代码的“骨架”。不同的HTML标签的作用不同。我们在爬虫网页内容时“看到”(指的是前台展示的信息)的内容通常都是在body标签里面的。上述示例中几个HTML标签的基本含义:

li:HTML中的列表,用来展示多个并列的项目信息。

h2:HTML中的二级标题,一般表示强调,呈现字体加粗增大的效果。

span:定义一个文字段落。

上面这些不同的标签以及效果(HTML里面被称为样式),可以通过多种方法在多个地方定义和引用,不同的定义之间会有覆盖效果。因此,标签本身的默认效果可能被更高优先级的样式覆盖而无法显示出来。

5.URL规则分析

在URL规则部分,URL中https://www.amazon.cn/s/page=1&ie=UTF8&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051所包含的page=*用于控制页码,其中*是指示不同的页码,因此我们只需要获得总页面数量,然后依次循环读取数据即可。通过观察页面分析,发现页面中并没有直接显示页面总数量的信息,不过我们可以通过页面左上角的总返回结果数计算得出。默认我们打开的是第一页,因此有关返回结果的三个数据分布表示第一页商品的起止数以及总商品数,我们用(总商品数/第一页商品数)+1便可以得出总页面数。例如(81/24)+1=4。关于页面这三个数字可以通过“要抓取的内容格式分析”的方法找到页面数量的信息,位于id值为s-result-count的h2标签内。相关页面如图3-8所示。

图3-8 商品数量标签

6.业务常识性分析

本节示例中的商品没有苹果手机的统一型号编码,只有标题的描述信息,因此后期还需要跟实际苹果手机进行匹配,该工作需建立一个匹配表,后期定期维护和增量更新即可。

到此为止我们基本确定了抓取思路:先从打开页面计算得到总页面数量,然后循环读出不同的页面信息;接着在每个页面找到每个商品的标题和价格,并把数据保存到本地文件。

清楚了上述基本情况后,我们开始编写代码,完整代码如下:

# 导入库
import requests  # 用于请求
from bs4 import BeautifulSoup  # 用于HTML格式化处理
import re  # 用于HTML配合查找条件
import time  # 用于文件名保存

# 获取总页面数量
def get_total_page_number():
    user_agent = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36'
    headers = {'User-Agent': user_agent}  # 定义头信息
    url = 'https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=1&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051'  # 寻找页码的URL
    res = requests.get(url, headers=headers)  # 发送请求
    html = res.text  # 获得请求中的返回文本信息
    html_soup = BeautifulSoup(html)  # 建立soup对象,用于处理HTML
    page_number_span = html_soup.find('h2', id="s-result-count")  # 查找id="s-result-count"的h2标签
    page_number_code = page_number_span.text  # 读出该标签的文本信息
    number_list = re.findall(r'(\w*[0-9]+)\w*', page_number_code)  # 使用正则表达式解析出文本中的3个数字
    total_page_number = (int(number_list[-1]) / int(number_list[-2])) + 1  # 计算得出总页码
    return int(total_page_number)  # 返回页面数字

# 解析单页面
def parse_single_page(i):
    url_part1 = 'https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie= UTF8&page=%d' % i  # 定义URL动态前半部分
    url_part2 = '&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051'  # 定义URL静态后半部分
    url = url_part1 + url_part2  # 拼接成完整URL
    print ('parse url: %s' % url)  # 输出URL信息
    user_agent = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36'
    headers = {'User-Agent': user_agent}  # 定义头信息,用于发送请求
    res = requests.get(url, headers=headers)  # 发送请求
    html = res.text  # 获得请求中的返回文本信息
    html_soup = BeautifulSoup(html)  # 建立soup对象,用于处理HTML
    tag_list = html_soup.find_all('li', id=re.compile('^result.*'))  # 查找id以result开始的li标签,返回列表
    for tag_info in tag_list:  # 读取列表中每一个标签(一个标签对应一个商品)
        # 解析价格
        # print (tag_info)
        price_code = tag_info.find('span', class_="a-size-base a-color-price s-price a-text-bold")  # 查找价格标签
        if price_code != None:  # 如果非空则继续
            price = price_code.text  # 取出价格标签文字
            # 解析商品标题
            title_code = tag_info.find('h2')  # 查找标题标签
            title = title_code.text  # 取出标题标签文字
            write_data(title, price)  # 每次解析完成写入文件

# 将数据写入文件
def write_data(title, price):
    file_date = time.strftime('%Y-%m-%d', time.localtime(time.time()))  # 当前日期,用于文件命名
    fn = open('%s.txt' % file_date, 'a+')  # 新建文件对象,以追加模式打开
    content = title + '\t' + price + '\n'  # 写内容,标题和价格以tab分割,末尾增加换行符
    fn.write(content)  # 写入文件
    fn.close()  # 关闭文件对象

# 解析多页面并写文件
def main():
    total_page_number = get_total_page_number()  # 获得总页面数
    for i in range(1, int(total_page_number) + 1):  # 循环读出每个页面
        parse_single_page(i)

main()

上述代码用空行分为6个部分。

第一个部分导入库,具体用途在注释中已经注明。

第二个部分开始我们每个功能都定义为一个函数模块,用于在不同场景下引用。get_total_page_number模块用来计算页面数量。

第三个部分定义了一个用于解析单个URL的函数模块,在定义URL的部分,由于原始URL中有%,这会导致我们新增占位符做外部数字引用时报错,因此应将其分开定义再组合。在解析不同的标签(包括后面的模块)时,我们用到了正则表达式模块,可以非常容易地解析出目标字符。

第四个部分是将每次解析的标题和价格写入文件。文件以追加模式打开,这样每次的数据都会追加到文件尾,而不会覆盖之前的数据,类似于数据库的追加模式操作。在写文件内容时,末尾需要有换行符,否则所有数据都会合并到一行。

第五个部分是通过一个循环来调用并执行多个页面的解析。

第六个部分函数用来执行所有的操作。

上述代码执行后返回结果如下。

1)调试窗口输出信息:

parse url: https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=1&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051
parse url: https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=2&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051
parse url: https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=3&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051
parse url: https://www.amazon.cn/s/ref=sa_menu_digita_l3_siphone?ie=UTF8&page=4&rh=n%3A665002051%2Cp_89%3AApple%2Cn%3A664978051

2)程序执行目录下产生一个跟运行时间相同的数据文件,图3-9所示是文件中的部分数据。

图3-9 爬取数据结果

上述的抓取执行过程中,“遭遇”到了亚马逊的反爬虫应对措施。经过测试,连续执行2次代码,第二次会返回HTML代码,但没有目标数据;连续第3次,HTTP请求直接无法返回数据。

大多数情况下,通过网络爬虫获取数据都是作为辅助方式,原因是现在几乎所有的网站都有防爬虫的意识和方式,这导致数据爬取会受到外部很多因素的影响而导致数据质量低下。基于爬虫的主要工作内容包括舆情监测、市场口碑、用户情绪、市场营销等方面,属于外部属性较强的“附加”工作。这些工作其实都不是公司的核心运营内容,这就会导致这些工作看似有趣并且有价值,但真正对企业来讲价值很难实际体现。

相关知识点:函数

函数是用来形成一个功能的代码段,函数可以用来被其他应用调用。使用函数的好处很多:

利于维护:代码中有变更时,只需要对特定代码段进行修改,而无须全部修订。

复用:当某个功能会被很多应用调用时,通过函数可实现一次撰写多次使用的目的。

清晰化功能设计:当设计功能时,不同的函数模块类似于功能主题,同时基于函数又可以派生库、类、包,通常函数是模块划分的基本单位。

递归:使用函数可以实现一种特殊的功能——递归,函数内部的功能调用函数自身,实现自循环,这种功能经常被用于有固定规律的场景下,例如求阶乘。

定义函数使用def[函数名]即可,函数可用来执行单独的任务,也可以通过return返回执行结果,用来与其他功能做交互使用。同时,不同的函数间可以通过赋值进行参数传递和调度之用。

上述过程中,需要考虑的关键点是:如何根据不同网页的实际特点,尤其是对于反爬虫的应对来正确读取网页源代码,读取源代码之后的解析往往不是主要问题。

本小节示例中,主要用了如下几个知识点:

通过requests库发送带有自定义head信息的网络请求;

通过requests返回对象的text方法获取源代码文本信息;

使用bs4的BeautifulSoup库配合find方法进行目标标签查找和解析,并通过其text方法获得标签文本信息;

通过re的正则表达式功能,实现对于特定数字规律的查找;

通过定义function函数来实现特定功能或返回特定结果;

通过for循环读取数据列表;

通过if条件判断对符合条件的记录进行处理;

对文本文件的读写操作;

使用time的localtime、time、strftime方法进行日期获取以及格式化操作。

3.12.2 网络用户日志解析

网络用户日志属于非结构化数据的一种,其解析方法根据配合的服务器和跟踪实施的不同而需要自定义模块,本节将用一个示例来演示如何进行日志解析。

本示例中,将使用正则表达式配合自定义函数模块实现日志解析功能。数据源文件traffic_log_for_dataivy位于“附件-chapter3”中,默认工作目录为“附件-chapter3”(如果不是,请切换到该目录下,否则会报错“IOError:File traffic_log_for_dataivy does not exist”)。

背景:我们在这个网站上部署了Google Analytics的代码用来监测用户的行为,由于该工具是SAAS工作模式,数据采集之后直接发送到Google云端服务器。我们通过一定的方式将每次发送给谷歌的数据同时“备份”了一条保存到本地服务器日志。其中日志请求内容的部分以“GET/__ua.gif?”开头便是日志记录。我们的目标是找到这些日志,然后对日志做初步的解析,并存放到本地文件便于后期做进一步数据和应用。

要实现这一目标,基本思路是:先从日志文件夹中读取日志文件列表(本示例中仅有1个文件,因此省略该步骤);然后依次读取每个日志文件中的数据;接着对日志文件中的每条数据进行判断,符合条件才能成为我们要的目标数据;最后将数据做初步解析并写到文件里面。

本节我们依然以函数的方式来撰写各个功能模块,如下是完整代码。

# 去除爬虫功能
def remove_spider_data(single_log):
    # 定义排除爬虫规则集
    exclude_set = [
        'AhrefsBot',
        'archive.org_bot',
        'baiduspider',
        'Baiduspider',
        'bingbot',
        'DeuSu',
        'DotBot',
        'Googlebot',
        'iaskspider',
        'MJ12bot',
        'msnbot',
        'Slurp',
        'Sogou web spider',
        'Sogou Push Spider',
        'SputnikBot',
        'Yahoo! Slurp China',
        'Yahoo! Slurp',
        'YisouSpider',
        'YodaoBot',
        'bot.html'
    ]
    count = 0  # 初始计数用于计算日志中是否包含爬虫
    for spider in exclude_set:  # 循环读取每个爬虫
        if single_log.find(spider) != -1:  # 如果爬虫出现在日志里面
            count += 1  # count + 1
    if count > 0:  # 如果结果不为0,意味着日志中有爬虫
        return 0  # 返回 0
    else:  # 否则
        return 1  # 返回1

# 读取日志数据
def get_raw_log(file):
    fn_read = open(file, 'r')  # 打开要读取的日志文件对象
    content = fn_read.readlines()  # 以列表形式读取日志数据
    fn_read.close()  # 关闭文件对象
    for single_log in content:  # 循环判断每天记录
        rule1 = single_log.find('GET /__ua.gif?') != -1  # 定义日志规则:含ua.gif的请求
        rule2 = remove_spider_data(single_log)
        if rule1 == True and rule2 == True:  # 如果同时符合2条规则,则执行
            fn_write = open('ua_data.txt', 'a+')  # 打开要保存的ua日志文件对象
            fn_write.writelines(single_log)  # 写入符合规则的日志
            fn_write.close()  # 关闭文件对象

# 解析每条日志数据
def split_ua(line):
    import re
    # 定义不同日志分割的正则表达式
    ip_rule = r'[\d.]*'  # 定义IP规则,例如203.208.60.230
    time_rule = r'\[[^\[\]]*\]'  # 定义时间规则,例如[02/Mar/2016:14:00:23 +0800]
    request_rule = r'\"[^\"]*\"'  # 定义请求规则
    status_rule = r'\d+'  # 定义返回的状态码规则,例如200
    bytes_rule = r'\d+'  # 返回的字节数,例如326
    refer_rule = r'\"[^\"]*\"'  # 定义refer规则
    user_agent_rule = r'\"[^\"]*\"'  # 定义user agnet规则
    # 原理:主要通过空格和-来区分各不同项目,在各项目内部写各自的匹配表达式
    log_re_pattern = re.compile(r'(%s)\ -\ -\ (%s)\ (%s)\ (%s)\ (%s)\ (%s)\ (%s)' % (
        ip_rule, time_rule, request_rule, status_rule, bytes_rule, refer_rule, user_agent_rule), re.VERBOSE)  # 完整表达式模式
    matchs = log_re_pattern.match(line)  # 匹配
    if matchs != None:  # 如果不为空
        allGroups = matchs.groups()  # 获得所有匹配的列表
        ip_info = allGroups[0]  # IP信息
        time_info = allGroups[1]  # 时间信息
        request_info = allGroups[2]  # 请求信息
        status_info = allGroups[3]  # 状态信息
        bytes_info = allGroups[4]  # 字节数信息
        refer_info = allGroups[5]  # refer信息
        user_agent_info = allGroups[6]  # user agent信息
        return ip_info, time_info, request_info, status_info, bytes_info, refer_info, user_agent_info

# 主程序
def get_final_data():
    file = 'traffic_log_for_dataivy'  # 定义原始日志
    get_raw_log(file)  # 读取非结构化文本数据
    fn_r = open('ua_data.txt', 'r')  # 打开要读取的ua日志文件
    content = fn_r.readlines()  # 读取ua所有日志记录
    fn_r.close()  # 关闭ua文件对象
    fn_w = open('final_data.txt', 'a+')  # 打开要写入的格式化文件
    for line in content:  # 按行循环
        ip_info, time_info, request_info, status_info, bytes_info, refer_info, user_agent_info = split_ua(
            line)  # 获得分割后的数据
        log_line = ip_info + '!' + time_info + '!' + request_info + '!' + status_info + '!' + bytes_info + '!' + refer_info \
                   + '!' + user_agent_info  # 按指定规则组合写入文件
        fn_w.writelines(log_line)  # 写入文件
        fn_w.writelines('\n')  # 写入换行符用于换行
    fn_w.close()  # 关闭写入的文件对象

# 执行程序
get_final_data()  # 执行所有程序

上述代码用空行分为5个部分。

第一部分去除爬虫功能。

该函数块的功能是从日志中去除属于爬虫产生的数据。在日志中,很多搜索引擎公司的网络爬虫在抓取页面时会产生日志数据,除了谷歌外,还百度、搜狗、雅虎、有道、必应等都有类似的爬虫(或者称为机器人)。

代码中先定义了一个爬虫列表,所有我们怀疑为爬虫的关键字字段都可加到列表里面,用来做爬虫记录过滤;然后我们通过for循环读取爬虫列表的每个字段,并在每行日志里通过find方法确认爬虫是否存在,如果存在则在计数器上加1(count+=1),一旦count大于0,我们就认为在日志记录里面至少包含1个爬虫信息。最终的返回结果为0代表有爬虫;1代表没有爬虫。

第二部分读取日志数据。

先通过open方法以只读模式打开日志文件,然后通过readlines方法以列表的形式读取日志记录,读取完成之后关闭日志文件对象。

通过一个for循环将列表中的每条日志读取出来,然后定义两条规则用于判断日志是否符合条件:一条规则是日志中必须包含'GET/__ua.gif?'字符串,这是我们定义Google Analytics日志的标志,另一条规则是日志中不能包含爬虫数据,直接使用在remove_spider_data功能模块的返回值做判断。当在if条件语句中同时满足上述2个条件时,将该条日志记录添加到名为ua_data.txt的新文件中。

该部分代码执行后,会在Python程序当前工作目录下产生一个名为ua_data.txt的新文件。

第三部分解析每条日志数据。

该模块定义了日志下所有字段的分割规则,首先我们针对日志记录中要解析的每个数据字段定义一个正则表达式构成的规则集,以用于目标数据的解析;接着将分散的规则通过re库的compile方法合成一个匹配模式;然后基于匹配模式使用match方法匹配每条数据记录,并由此返回匹配的字段值列表,最后解析出所有定义的字段值并返回给其他函数使用。

第四部分是主要程序模块。

这部分功能的意义是将前文提到的功能整合到该模块中。

先是定义了一个日志文件用于读取日志数据,通过之前定义的get_raw_log模块获取符合要求的日志数据并保存为单独的数据文件。再读取上述保存的日志数据并按行循环,在循环中,我们将每条日志进行分割并以“!”作为最终分隔符写入目标文件。最后执行程序。

上述过程目的是提高每个模块的可读性和可理解性,我们按照功能单独定义,因此“忽略”了总体考虑和最优化规则,此代码有很多地方可以优化:

import re应该放到全局里面导入。

从get_raw_log获得的数据,其实不必先写入文件再从文件读取,该过程可以省略(本节之所以先存后读是为了便于读者理解过程),所有程序最终可以只有1个结果文件。

日志被分割后又进行组合并再次写入文件,实际中不会这样执行,因为已经格式化的数据可以放到数据库里面。

分割后的request_info模块的数据还没有解析,原因是没有应用场景需求做支撑,所以暂时不做处理。该字段中每个请求的数据主体(URL中?之后的部分)都是以key-value形式存在的键值对,解析起来非常容易。但是,由于不同的请求里面包含的key(即参数)可能不同,而且系统预定义的key的值域数量非常大(例如il<listIndex>pi<productIndex>cm<metricIndex>的最大笛卡儿为200*200*200=8000000),因此这些数据无法直接分列存储到一张关系型数据库表里面。解决思路有两种:一是将字段按主题拆分,形成数据仓库或数据集市;二是使用非关系型数据仓库,直接支持海量数据(行和列)的扩展。目前这两种方式都有公司在实行。

上述过程中,需要考虑的关键点是:

如何根据不同的服务器日志配置以及前端代码跟踪实施的具体情况,编写日志过滤规则。

有关爬虫数据的排除,也是需要额外注意的信息点。

本小节示例中,主要用了以下几个知识点:

对文本文件的读写操作;

通过find方法查找符合条件的字符串;

通过if做多条件判断,并对符合条件的记录做处理;

通过re的正则表达式功能,实现对于特定字段的查找和匹配;

通过定义function函数来实现特定功能或返回特定结果;

通过for循环读取数据列表。

3.12.3 图像的基本预处理

本示例中,将使用OpenCV来做图像基本预处理操作,基本处理内容包括图像缩放、平移、旋转、透视变换、图像色彩模式转换、边缘检测、二值化操作、平滑处理、形态学处理。

数据源文件sudoku.png、j.png位于“附件-chapter3”中,默认工作目录为“附件-chapter3”(如果不是,请切换到该目录下,否则会报类似错“IOError:File sudoku.png does not exist”)。完整代码如下:

import cv2  # 导入图像处理库
import numpy as np  # 导入numpy库
from matplotlib import pyplot as plt  # 导入展示库

# 展示图像模块
def img_show(img_name, img):
    cv2.imshow(img_name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 原始图像
img_file = 'sudoku.png'  # 定义原始数据文件
img = cv2.imread(img_file)  # 以彩色模式读取图像文件
rows, cols, ch = img.shape  # 获取图像形状
img_show('raw img', img)  # 展示彩色图像

# 图像缩放
img_scale = cv2.resize(img, None, fx=0.6, fy=0.6, interpolation=cv2.INTER_CUBIC)  # 图像缩放
img_show('scale img', img_scale)  # 展示缩放后的图像

# 图像平移
M = np.float32([[1, 0, 100], [0, 1, 50]])  # 定义平移中心
img_transform = cv2.warpAffine(img, M, (cols, rows))  # 平移图像
img_show('transform img', img_transform)  # 展示平移后的图像

# 图像旋转
M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 0.6)  # 定义旋转中心
img_rotation = cv2.warpAffine(img, M, (cols, rows))  # 第一个参数为旋转中心,第二个为旋转角度,第三个为旋转后的缩放因子
img_show('rotation img', img_rotation)  # 展示旋转后的图像

# 透视变换
pts1 = np.float32([[76, 89], [490, 74], [37, 515], [520, 522]])  # 定义变换前的四个校准点
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])  # 定义变换后的四个角点
M = cv2.getPerspectiveTransform(pts1, pts2)  # 定义变换中心点
img_perspective = cv2.warpPerspective(img, M, (300, 300))  # 透视变换
img_show('perspective img', img_perspective)  # 展示透视变换后的图像

# 转换为灰度图像
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 图像转灰度
img_show('gray img', img_gray)  # 展示灰度图像

# 边缘检测
img_edges = cv2.Canny(img, 100, 200)  # 检测图像边缘
img_show('edges img', img_edges)  # 展示图像边缘

# 图像二值化
ret, th1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  # 简单阈值
th2 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)  # 自适应均值阈值
th3 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)  # 自适应高斯阈值
titles = ['Gray Image', 'Global Thresholding (v = 127)',
          'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']  # 定义图像标题
images = [img_gray, th1, th2, th3]  # 定义图像集
for i in xrange(4):
    plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')  # 以灰度模式展示每个子网格的图像
    plt.title(titles[i])  # 设置每个自网格标题
    plt.xticks([]), plt.yticks([])  # 设置x轴和y轴标题
plt.show()  # 展示图像

# 图像平滑
kernel = np.ones((5, 5), np.float32) / 25  # 设置平滑内核大小
img_smoth_filter2D = cv2.filter2D(img, -1, kernel)  # 2D卷积法
img_smoth_blur = cv2.blur(img, (5, 5))  # 平均法
img_smoth_gaussianblur = cv2.GaussianBlur(img, (5, 5), 0)  # 高斯模糊
img_smoth_medianblur = cv2.medianBlur(img, 5)  # 中值法
titles = ['filter2D', 'blur', 'GaussianBlur', 'medianBlur']  # 定义标题集
images = [img_smoth_filter2D, img_smoth_blur, img_smoth_gaussianblur, img_smoth_medianblur]  # 定义图像集
for i in xrange(4):
    plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')  # 以灰度模式展示每个子网格的图像
    plt.title(titles[i])  # 设置每个自网格标题
    plt.xticks([]), plt.yticks([])  # 设置x轴和y轴标题
plt.show()  # 展示全部图像

# 形态学处理
img2 = cv2.imread('j.png', 0)  # 以灰度模式读取图像
kernel = np.ones((5, 5), np.uint8)  # 设置形态学处理内核大小
erosion = cv2.erode(img2, kernel, iterations=1)  # 腐蚀
dilation = cv2.dilate(img2, kernel, iterations=1)  # 膨胀
plt.subplot(1, 3, 1), plt.imshow(img2, 'gray')  # 设置自网格1图像
plt.subplot(1, 3, 2), plt.imshow(erosion, 'gray')  # 设置自网格2图像
plt.subplot(1, 3, 3), plt.imshow(dilation, 'gray')  # 设置自网格3图像
plt.show()  # 展示全部图像

上述代码以空行分为11个部分,涵盖了日常图像处理的常用操作。

第一部分为导入库,本代码中除了OpenCV库外,还有用于定义图像处理的内核的Numpy、用于展示多图图像的Matplotlib。

第二部分定义了一个函数,用来做单图像展示。下面的每个功能模块,当只有一个图像做展示会直接调用该模块,而无须重复写展示功能代码。cv2.show()方法必须与cv2.waitKey()、cv2.destroyAllWindows()一起使用才能保证图像正常展示及关闭。

第三部分读取原始图像并展示。通过cv2的imread方法以彩色模式读取图像,然后获得彩色图像的长、高和通道形状,最终调用img_show做图像展示,结果如图3-10中所示的①。

第四部分图像缩放处理。直接使用cv2.resize方法设置缩放比例、方法等并展示输出为原图像60%的新图像,结果如图3-10所示的②。

第五部分图像平移处理。先定义图像平移中心,然后使用cv2.warpAffine方法根据平移中心移动图像,移动后的图像,结果如图3-10所示的③。

第六部分图像旋转处理。与图像平移类似,先定义旋转中心,然后使用cv2.warpAffine进行旋转,同时设置旋转角度45度、缩放因子为0.6,结果如图3-10所示的④。

第七部分透视变换处理。先定义变换前的四个校准点,然后定义变换后的四个角点,可用来控制图像大小,接着定义变换中心点并应用cv2.warpPerspective进行透视变换,结果如图3-10所示的⑤。

第八部分转换为灰度图像。使用cv2.cvtColor将BRG模式转为GRAY模式,结果如图3-10所示的⑥。

图3-10 原始图像和部分处理后的图像

第九部分边缘检测处理。使用cv2.Canny检测图像边缘,结果如图3-11所示。

第十部分图像二值化处理。这里分别应用了简单阈值、自适应均值阈值、自适应高斯阈值三种方法做二值化处理,并使用Matplotlib做多网格图像,同时展示原始图像和三种阈值下二值化图像处理结果,如图3-12所示。

第十一部分图像平滑处理。先设置平滑内核大小,然后分别使用cv2.filter2D(3D卷积)、cv2.blur(平均法)、cv2.GaussianBlur(高斯模糊)、cv2.medianBlur(中值法)进行平滑结果对比,如图3-13所示。

第十二部分形态学处理。这里重新以灰度模式读取一个图像,定义处理内核之后,通过cv2.erode和cv2.dilate分别实现腐蚀和膨胀操作。原图和腐蚀、膨胀处理后的图像对比如图3-14所示。

图3-11 边缘检测后的图像

图3-12 原图和多种阈值控制下的二值化结果

图3-13 多种平滑效果对比

图3-14 原图和腐蚀、膨胀处理效果

上述过程中,需要考虑的关键点是:如何根据不同的图像处理需求,实现图像的基本预处理任务,尤其是对于每种方法下参数的具体设置,需要根据实际情况加以选择。另外,在程序自动化过程中,是不可能依靠人工参与每次边界调整、阈值优化等具体过程的,这往往通过一定专用的参数优化模型来实现。例如,对于透视图像的处理,首先要做的是识别出透视图像的矫正参照点,而该过程就是一个融合了业务场景、图像处理技术、数学知识和计算方法等多学科知识的建模过程。

本小节示例中,主要用了几个知识点:

通过cv2.imread对图像文件的数据进行读取,并分别以彩色和灰度模式读取图像;

通过cv2.imshow、cv2.waitKey()、cv2.destroyAllWindows()实现图像展示;

通过Matplotlib库实现多子自网格图的展示;

通过cv2.resize实现图像缩放;

通过cv2.warpAffine实现图像平移;

通过cv2.warpAffine和cv2.getRotationMatrix2D实现图像旋转;

通过cv2.warpPerspective和cv2.getPerspectiveTransform实现图像透视变换;

通过cv2.cvtColor实现图像颜色模式转换;

通过cv2.Canny实现图像边缘检测;

通过cv2.threshold实现图像二值化处理,并通过简单阈值、自适应均值阈值、自适应高斯阈值等方法寻找最佳阈值;

通过cv2.filter2D、cv2.blur、cv2.GaussianBlur、cv2.medianBlur等方法实现图像平滑处理;

通过for循环做数据循环输出;

通过cv2.erode、cv2.dilate等方法实现图像腐蚀、膨胀等形态学处理。

3.12.4 自然语言文本预处理

与数据库中的结构化数据相比,文本具有有限的结构,某些类型的数据源甚至没有数据结构。因此,文本预处理就是要对半结构化或非结构化的文本进行格式和结构的转换、分解和预处理等,以得到能够用于进一步处理的基础文本。不同环境下,文本所需的预处理工作内容也有所不同,大体上分为以下几个部分:

1.基本处理

根据不同的文本数据来源,可能涉及的基本文本处理包括去除无效标签、编码转换、文档切分、基本纠错、去除空白、大小写统一、去标点符号、去停用词、保留特殊字符等。

去除无效标签:例如从网页源代码获取的文本信息中包含HTML标签,此时要提取特定标签内容并去掉标签。

编码转换:不同编码转换对于中文处理具有较大影响,例如UTF-8、UTF-16、GBK、GB2312等之间的转换。

文档切分:如果要获得单个文档中包含多个文件,需要进行单独切分以将不同的文档拆分出来。

基本纠错:对于文本中明显的人名、地名等常用语和特定场景用语的错误进行纠正。

去除空白:文本中可能包含的大量空格、空行等需要去除。

大小写统一:将文本中的英文统一为大写或小写。

去标点符号:去除句子中的标点符号、特殊符号等。

去停用词:常见的停用词包括the、a、an、and、this、those、over、under、above、on等。

保留特殊字符:某些场景下可能需要只针对汉字、英文或数字进行处理,其他字符都需要过滤掉。

2.分词

分词是将一系列连续的字符串按照一定逻辑分割成单独的词。在英文中,单词之间是以空格作为自然分界符的;而中文只有字、句和段能通过明显的分界符来简单划界,而作为词是没有形式上的分界符的。因此,中文分词要比英语等语种分词困难和复杂。对于复杂的中文分词而言,常用的分词方法包括最大匹配法、逆向最大匹配法、双向匹配法、最佳匹配法、联想-回溯法等。

3.文本转向量(word to vector)

人们通常采用向量空间模型来描述文本向量,即将文档作为行,将分词后得到的单词(单词在向量空间模型里面被称为向量,又称特征、维度或维)作为列,而矩阵的值则是通过词频统计算法得到的值。这种空间向量模型又称文档特征矩阵。其表示方法如表3-4所示。

表3-4 空间向量模型示例

本示例中,将仅对自然语言文本做分词和word to vector处理,更多有关文本分析的内容,例如词性标注、关键字提取、词频统计、文本聚类、相似关键字分析等会在第4章中介绍。数据源文件text.txt位于“附件-chapter3”中,默认工作目录为“附件-chapter3”(如果不是,请切换到该目录下,否则会报“IOError:File text.txt does not exist”)。完整代码如下:

# 导入库
import pandas as pd
import jieba  # 结巴分词
from sklearn.feature_extraction.text import TfidfVectorizer  # 基于TF-IDF的词频转向量库

# 分词函数
def jieba_cut(string):
    word_list = []  # 建立空列表用于存储分词结果
    seg_list = jieba.cut(string)  # 精确模式分词
    for word in seg_list:  # 循环读取每个分词
        word_list.append(word)  # 分词追加到列表
    return word_list

# 读取自然语言文件
fn = open('text.txt')
string_lines = fn.readlines()
fn.close()

# 中文分词
seg_list = []  # 建立空列表,用于存储所有分词结果
for string_line in string_lines:  # 读取每行数据
    each_list = jieba_cut(string_line)  # 返回每行的分词结果
    seg_list.append(each_list)  # 分词结果添加到结果列表
for i in range(5):  # 打印输出第一行的前5条数据
    print (seg_list[1][i])

# word to vector
stop_words = [u'\n', u'/', u'“', u'“', u'”', u'的', u',', u'和', u'是', u'随着', u'对于', u'对', u'等', u'能', u'都', u'。', u'、',
              u'中', u'与', u'在', u'其']  # 自定义要去除的无用词
vectorizer = TfidfVectorizer(stop_words=stop_words, tokenizer=jieba_cut)  # 创建词向量模型
X = vectorizer.fit_transform(string_lines)  # 将文本数据转换为向量空间模型
vector = vectorizer.get_feature_names()  # 获得词向量
vector_value = X.toarray()  # 获得词向量值
vector_pd = pd.DataFrame(vector_value, columns=vector)  # 创建用于展示的数据框
print (vector_pd.head(1))  # 打印输出第一条数据

上述代码用空行分为5个部分。

第一部分导入库。本代码中用到jieba作为中文分词,sklearn用于word to vector转换,Pandas用于格式化输出。

第二部分建立一个分词函数。该函数用于下面的中文分词。先建立一个空列表用于存储分词结果;使用jieba.cut做中文分词;循环读取每个分词结果并存储到列表中,最后返回。

第三部分读取自然语言文件。使用Python标准方法open读取文本文件,使用read-lines方法读取为列表。

第四部分中文分词。新建一个空列表,用于存储每次分词返回的结果;使用for循环将原文本数据按行读取,并调用分词函数做中文分词,并将结果追加到列表中。最后通过循环打印输出前5条数据,具体如下:

对于

数据

运营

第六部分word to vector。先定义一个要去除的停用词库,然后使用TfidfVectorizer方法词向量模型,将使用fit_transform方法对输入的分词后的列表做转换。最后通过词向量的get_feature_names获得向量名称,通过转换后的向量使用toarray方法将向量结果转换为数组,再通过数据框做数据格式化。打印输出第一条数据如下:

   python    上    下        不断   不曾   专业         业   业务   两个        严峻 ...   \
0     0.0  0.0  0.0  0.204648  0.0  0.0  0.204648  0.0  0.0  0.204648 ...    
     非   非常  非常简单   预测   领域    高   高于    (    )    ;  
0  0.0  0.0   0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  
[1 rows x 198 columns]

在本示例中,没有涉及更多的自然语言文本预处理环节,例如无效标签、编码转换、文档切分、基本纠错、去除空白、大小写统一、去标点符号、去停用词、保留特殊字符等,原因是这些内容都是针对不同案例展开的,而本示例仅做功能演示,应对不同文本时,很难具有通用性和可复制性。另外,由于测试文件为本书的部分文字内容,文本规模本身有限,因此难以提取真正有价值的数据和规律出来,尤其是基于向量化的分词,由于数据量的限制以及作为扩展内容,无法做更多有价值的探索。

上述过程中,需要考虑的关键点是:如何根据不同自然语言的来源特点、应用场景、语言语法、目标应用做综合的文本处理?文本处理中用到的都是针对文字内容的过滤、筛选、去除、替换等主要基于字符串的操作。

本小节示例中,主要用了如下几个知识点:

对文本文件的读写操作;

通过join方法实现多字符串的拼接;

通过encode方法做字符编码转换;

使用jieba.cut做中文分词,并可设置不同的分词模式;

通过append方法对列表追加元素;

通过for循环读取数据列表;

通过if语句进行条件判断;

通过sklearn.feature_extraction.text的TfidfVectorizer方法做word to vector处理;

通过词向量的get_feature_names获得向量名称;

通过转换后的向量使用toarray方法将向量结果转换为数组;

使用pandas.DataFrame建立数组并通过head方法输出前n条数据。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文