搜索引擎与爬虫

爬虫系列:数据清洗

上一期我们讲解了使用 Python 读取 CSV、PDF、Word 文档相关内容。

前面我们已经介绍了网络数据采集的一些基础知识,现在我们将进入高级数据采集部分。到目前为止,我们创建的网络爬虫都不是特别给力,如果网络服务器不能立即提供样式规范的信息,爬虫就不能采集正确的数据。如果爬虫只能采集那些显而易见的信息,不经过处理就存储起来,那么迟早要被登录表单、网页交互以及 Javascript 困住手脚。总之,目前爬虫还没有足够的实力去采集各种数据,只能处理那些愿意被采集的信息。

高级数据采集部分就是要帮你分析原始数据,获取隐藏在数据背后的故事——网站的真实故事其实都隐藏在 Javascript、登录表单和网站反爬措施背后。

数据清洗

到目前为止,我们都没有处理过那些样式不规范的数据,要么使用的是样式规范的数据源,要么就是放弃样式不符合我们预期的数据。但在网络数据采集中,你通常无法对采集的数据样式太挑剔。

由于错误的标点符号、大小写字母不一致、断行和拼写错误等问题,凌乱的数据(dirty data)是网络中的大问题。下面我们就通过工具和技术,通过改变代码的编写方式,帮你从源头控制数据凌乱的问题,并且对已经入库的数据经行清洗。

编写代码清洗数据

和编写异常处理代码一样,你应该学会编写预防型代码来处理意外情况。

在语言学中有一个模型叫 n-gram,表示文本或语言中的 n 个连续的单词组成的序列。再进行自然语言分析时,用 n-gram 或者寻找常用词组,就可以很容易的把一句话分成若干个文字片段。

在接下来的内容我们将重点介绍如何获取格式合理的 n-gram。

下面的代码返回维基百科词条“Python programming language”的 2-gram 列表:

from utils import connection_util


class DataCleaning(object):
    def __init__(self):
        self._target_url = 'https://en.wikipedia.org/wiki/python_(programming_language)'
        self._init_connection = connection_util.ProcessConnection()

    @staticmethod
    def ngrams(input_text, n):
        split_result = input_text.split(' ')
        output = []
        for i in range(len(split_result) - n + 1):
            output.append(split_result[i:i + n])
        return output

    def get_result(self):
        # 连接目标网站,获取内容
        get_content = self._init_connection.init_connection(self._target_url)
        if get_content:
            content = get_content.find("div", {"id": "mw-content-text"}).get_text()
            ngrams = self.ngrams(content, 2)
            print(ngrams)
            print("2-grams count is: " + str(len(ngrams)))


if __name__ == '__main__':
    DataCleaning().get_result()

ngrams 函数把一个待处理的字符串分成单词序列(假设所有单词按照空格分开),然后增加到 n-gram 模型形成以每个单词开始的二元数组。

运行程序之后,会有一些凌乱的数据,例如:

['web', 'frameworks\nBottle\nCherryPy\nCubicWeb\nDjango\nFastAPI\nFlask\nGrok\nNagare\nNevow\nPylons\nPyramid\nQuixote\nTACTIC\nTornado\nTurboGears\nTwistedWeb\nWebware\nweb2py\nZope']

另外,应为每个单词(除了最后一个单词)都要创建一个 2-gram 序列,所以这个词条里共有 11680 个 2-gram 序列。这并不是一个非常便于管理的数据集!

我们首先使用一些正则表达式来移除转义字符(\n),再把 Unicode 字符过滤掉。我们可以通过下面的函数对之前输出的内容经行清理:

import re

from utils import connection_util


class DataCleaning(object):
    def __init__(self):
        self._target_url = 'https://en.wikipedia.org/wiki/python_(programming_language)'
        self._init_connection = connection_util.ProcessConnection()

    @staticmethod
    def ngrams(input, n):
        input = re.sub('\n+', " ", input)
        input = re.sub(' +', " ", input)
        input = bytes(input, "UTF-8")
        input = input.decode("ascii", "ignore")
        print(input)
        input = input.split(' ')
        output = []
        for i in range(len(input) - n + 1):
            output.append(input[i:i + n])
        return output

    def get_result(self):
        # 连接目标网站,获取内容
        get_content = self._init_connection.init_connection(self._target_url)
        if get_content:
            content = get_content.find("div", {"id": "mw-content-text"}).get_text()
            ngrams = self.ngrams(content, 2)
            print(ngrams)
            print("2-grams count is: " + str(len(ngrams)))


if __name__ == '__main__':
    DataCleaning().get_result()

上面的代码首先将内容中的换行符(或者多个换行符)替换成空格,然后把连续的多个空格替换成一个空格,确保所有单词之间只有一个空格。最后,把内容转换成 UTF-8 格式以消除转义字符。

通过上面的几步,我们已经可以大大改善输出结果了,但是还是有一些问题:

ALGOL 68,[13] APL,[14] C,[15] C++,[16] CLU,[17] Dylan,[18] Haskell,[19] Icon,[20] Java,[21] Lisp,[22] Modula-3,[16] Perl, Standard ML[14]InfluencedApache Groovy

因此,需要增加一些规则来处理数据。我们可以定制一些规则让数据变得更规范:

  • 剔除单字符的“单词”,除非这个单词是“a”或“i”;

  • 剔除维基百科的引用标记(方括号包裹的数字,入[1])

  • 剔除标点符号

现在“清洗任务”列表变得越来越长,让我们把规则都移出来,新建一个函数:

import re
import string

def ngrams(self, input, n):
    input = self.clean_input(input)
    output = []
    for i in range(len(input) - n + 1):
        output.append(input[i:i + n])
    return output

@staticmethod
def clean_input(input):
    input = re.sub('\n+', " ", input)
    input = re.sub('\[[0-9]*\]', "", input)
    input = re.sub(' +', " ", input)
    input = bytes(input, "UTF-8")
    input = input.decode("ascii", "ignore")
    input = input.split(' ')
    clean_input = []
    for item in input:
        # string.punctuation 获取所有的标点符号
        item = item.strip(string.punctuation)
        if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
            clean_input.append(item)
    return clean_input

这里用 import string 和 string.punctuation 来获取 Python 所有的标点符号。我们可以在 Python 命令行里面查看标点符号有哪些:

import string
print(string.punctuation)
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

在循环体中用item.strip(string.punctuation)对内容中的所有单词进行清洗,单词两端的任何标点符号都会被去掉,但带连字符的单词(连字符在单词内部)任然会保留。

本期关于数据清洗就是如上内容,在接下来的内容中我会讲解数据标准化,以及存储的数据如何清洗。

以上的演示源代码托管于 Gihub,地址:https://github.com/sycct/Scrape_1_1.git

如果有任何问题,欢迎大家 issue。