「Python」爬虫自然语言清洗组件 v1.0.0
2018-05-10 本文已影响0人
HughDong
公告:博主因使用魔理沙的扫把表达清洗,已被车万粉拉去祭天。
设计思路
我认为从网站上爬取下来的内容要清洗的有两大块:通用清洗和规则清洗,换句话说就是可复用的和不可复用的。
通用清洗是每个爬虫常见的问题,比如特殊编码、html标签、换行空格符等。
特殊清洗是在通用清洗的基础上,网站结构产生的特殊问题,比如多余的固定字符等。
通用清洗
通用清洗涵盖以下几个方面:
空字段补全
筛选附件和图片
特殊Unicode符号
HTML标签注释
其他字符(\r\n\t...)
通用清洗要注意顺序,否则会引起不必要的麻烦
空字段补全
优先把空内容(null/None/NaN)转换成空字符串(""),这样后续String类型操作不会报出TypeError。
不同网页间爬取下来的字段有些微差别,也将不存在的的字段进行了补全
筛选附件和图片
我爬正文数据下来时是整个正文元素直接获取的,所以也就在这里筛选出正文中的附件和图片
<a href="...">
<img src="...">
通过正则匹配到后判断链接是以http还是./ /开头,如果没有域名则添加该站的域名
最重要的一步是对图片链接进行清洗,如果只是提取链接的话会出现很多小图标
所以我在类规则中图片的deny和access规则,列表中存放的是正则表达式
rules = {
'spider1': {
'img': {
'access': [],
'deny': ['icon_.*?\.gif', '\.gif'],
},
'file': {
'access': [],
'deny': ['docid'],
},
},
}
优先排除不需要的图片和保留一定需要的图片,剩下的部分使用String.BytesIO()判断图片尺寸,
保留长宽像素相乘>10000的图片
特殊Unicode符号
HTML标签注释
使用正则删除掉 <> /**/ 中的内容
re.compile(r'\<.*?\>|\/\*.*?\*\/').sub(' ', str)
其他字符
前面的清洗完成后基本还剩下换行符和标识符,使用str.replace()替换即可
str.replace('\\n', '') \
.replace('\\r', '') \
.replace('\\t', '') \
.replace('\\xa0', '') \
.replace('\\xc2', '') \
.replace('\\u3000', '')
规则清洗
通用的清洗后会有一些特殊数据残留,我将特殊规则写在类中,根据具体规则实现字符串替换等操作
rules = {
'spider1': {
'content': {
'replace': ["\',\'"],
},
},
}
方法链调用
为了使用方便,封装在一条方法链中,清洗时只需要依次根据需求调用即可
Clear_Data(item) \
.empty_key() \
.catch_file_img() \
.unicode_char('content') \
.unicode_char('title') \
.html_label('content') \
.word_wrap('content') \
.special_rules()
完整代码
clear.py
update /18.03.12.1
import pymongo
import re
from io import BytesIO
from PIL import Image
import requests
class Clear_Data():
rules = {
'spider1': {
'img': {
'access': [],
'deny': ['icon_.*?\.gif', '\.gif'],
},
'file': {
'access': [],
'deny': ['docid'],
},
'content': {
'replace': ["\',\'"]
}
},
'spider2': {
'img': {
'access': ['_upload\/article'],
'deny': ['icon_.*?\.gif', '\.gif'],
},
'file': {
'access': [],
'deny': [],
}
},
'spider3': {
'img': {
'access': [],
'deny': ['comm_20\.jpg', 'doc\.gif', 'arrow3\.gif', 'icon_.*?\.gif', '\.gif'],
},
'file': {
'access': [],
'deny': [],
}
}
}
def __init__(self, item):
item.pop('_id')
self.item = item
def rep(self):
return self.item
def empty_key(self):
fields = ['title', 'url', 'date', 'content', 'category', 'index', 'classify', 'institution',
'abstract', 'license', 'source', 'file', 'img']
for key, value in self.item.items():
if value == None: # 清除空字段
if key == 'file' or key == 'img':
self.item[key] = []
else:
self.item[key] = ''
for field in fields: # 补全字段
if not field in self.item:
if field == 'file' or field == 'img':
self.item[field] = []
else:
self.item[field] = ''
return self
def word_wrap(self, key): # 去除换行空格
self.item[key] = self.item[key] \
.replace('\\n', '') \
.replace('\\r', '') \
.replace('\\t', '') \
.replace('\\xa0', '') \
.replace('\\xc2', '') \
.replace('\\u3000', '')
return self
def html_label(self, key): # 清除html标签
self.item[key] = re.compile(r'\<.*?\>').sub(' ', self.item[key])
return self
def unicode_char(self, key): # 清除unicode异常字符
self.item[key] = re \
.compile( \
u"[^"
u"\u4e00-\u9fa5"
u"\u0041-\u005A"
u"\u0061-\u007A"
u"\u0030-\u0039"
u"\u3002\uFF1F\uFF01\uFF0C\u3001\uFF1B\uFF1A\u300C\u300D\u300E\u300F\u2018\u2019\u201C\u201D\uFF08\uFF09\u3014\u3015\u3010\u3011\u2014\u2026\u2013\uFF0E\u300A\u300B\u3008\u3009"
u"\!\@\#\$\%\^\&\*\(\)\-\=\[\]\{\}\\\|\;\'\:\"\,\.\/\<\>\?\/\*\+"
u"]+") \
.sub('', self.item[key])
return self
def catch_file_img(self):
file = []
img = []
img_access_rule = '|'.join(self.rules[self.item['source']]['img']['access'])
img_deny_rule = '|'.join(self.rules[self.item['source']]['img']['deny'])
file_access_rule = '|'.join(self.rules[self.item['source']]['file']['access'])
file_deny_rule = '|'.join(self.rules[self.item['source']]['file']['deny'])
domain_name = re \
.search(r'(?i)https?:\/\/.*?\/', self.item['url']) \
.group()
for content in re.findall(re.compile(r'\<img.*?src=.*?\>'), self.item['content']):
if re.search(r'(?i)gif|jpg|png|psd|swf|bmp|emf|gif|webp', content):
_url_ = re \
.search(r'(?i)src=[\'\"].*?[\'\"]', content) \
.group()[5:-1]
if re.search(r'(?i)^http', _url_): # 链接头部没有域名则添加
img_url = _url_
else:
img_url = domain_name + _url_
if img_deny_rule and re.search('(?i)' + img_deny_rule, img_url): # 匹配deny规则丢弃
continue
elif img_access_rule and re.search('(?i)' + img_access_rule,
img_url): # 匹配access规则丢弃
img.append(img_url)
else: # 其他判断图片尺寸
try:
requests.adapters.DEFAULT_RETRIES = 5
r = requests.get(img_url)
tmp_im = BytesIO(r.content)
im = Image.open(tmp_im)
except OSError:
pass
else:
if im.size[0] * im.size[1] > 10000:
img.append(img_url)
for content in re.findall(re.compile(r'\<a.*?href=.*?\>'), self.item['content']):
if re.search(r'(?i)doc|docx|pdf|xlsx|xls|csv|txt|ppt|pptx|zip|rar|7z', content):
_url_ = re \
.search(r'(?i)href=[\'\"].*?[\'\"]', content) \
.group()[6:-1]
if re.search(r'(?i)^http', _url_):
file_url = _url_
else:
file_url = domain_name + _url_
if file_deny_rule and re.search('(?i)' + file_deny_rule, file_url): # 匹配deny规则丢弃
continue
elif file_access_rule and re.search('(?i)' + file_access_rule,
file_url): # 匹配access规则丢弃
file.append(file_url)
else: # 未匹配则加入
print(file_url)
file.append(file_url)
self.item['file'] = file
self.item['img'] = img
return self
def special_rules(self):
content_replace_rule = '|'.join(self.rules[self.item['source']]['content']['replace'])
item['content'] = item['content'].replace(content_replace_rule, '')
return self
count = 10000 # 分页请求数据
page = 0
while True:
page = page + 1
skip = (page - 1) * count
items = list(db['data_raw'].find({}).skip(skip).limit(count))
for item in items:
Clear_Data(item) \ # 清洗过程
.empty_key() \
.catch_file_img() \
.unicode_char('content') \
.unicode_char('title') \
.html_label('content') \
.word_wrap('content') \
.special_rules()
db['data_value'].insert(items) # 批量插入
print(str(page))
if len(items) < count:
break