ann_img.py
ann_img.py — 图片处理与 OCR 组件
下载公告图片 → MD5/感知哈希去重 → 二维码识别(纯二维码/混合图/普通图)→ PaddleOCR 文字识别 → 多层黑名单过滤 → 上传华为云 OBS 替换 src。五层流程详解见 parsegpt 主篇。
代码
# -*- coding: utf-8 -*-
"""
图片处理工具模块
功能:下载网页图片 → 本地缓存 → 尺寸检测 → 二维码识别 → OCR文字识别 → 图片上传云存储 → 过滤垃圾图片
适用:爬虫后处理HTML中的图片,清洗、标准化、提取信息
"""
from PIL import Image
from utils import ner_logger
import os
import io
import re
import sys
sys.path.append('../') # 添加上级目录到Python路径,解决模块导入问题
import json
# 工具类导入:日志、MD5、文件下载、后缀获取、文本MD5等
from utils import ner_logger, getMD5Bytes, download_file, get_file_extension, get_md5_clear_text, getMD5Str
# 图片工具:透明度检测、读取图片、图片均值哈希(去重)
from utils_img import check_transparency_ratio, read_image_object, average_hash
# OCR识别接口
from api.ocr_api import Paddle_OCR
# 华为云OBS上传接口
from api.hwcloud_api import upload_file_to_obs
# URL工具:重组URL(标准化URL)
from utils_html import recombine_url
# 二维码检测工具
from utils_img import detect_qr_code
# ====================== 全局初始化 ======================
# 初始化 OCR 解析器(全局单例,避免重复加载)
ocr_parser = Paddle_OCR()
def url_to_base64(image_url, _cache_dir):
"""
根据图片URL下载图片,完成:缓存、尺寸获取、MD5、二维码检测、OCR识别
:param image_url: 图片网络地址
:param _cache_dir: 本地缓存目录
:return: 宽度、高度、二维码标记、图片属性字典、缓存路径、文件名、响应头
"""
# 1. 下载图片内容
_ok, image_content, headers = download_file(image_url)
if not _ok:
ner_logger.info(f"图片下载失败 {image_url}")
return 0, 0, 'N', {}, '', '', ''
# 2. 读取为PIL图片对象
_ok, image = read_image_object(image_url, image_content)
if not _ok:
ner_logger.info(f"图片读取失败 {image_url}")
return 0, 0, 'N', {}, '', '', ''
# 3. 获取图片宽高
width, height = image.size
# 4. 计算图片内容MD5(唯一标识)
img_md5 = getMD5Bytes(image_content)
# 5. 获取图片后缀(.jpg/.png/.gif等)
_ext = get_file_ext(image_url, image)
# 6. 生成本地缓存文件名(MD5命名,避免重复)
_cache_filename = f'{img_md5}_pic{_ext}'
_cachefile = os.path.join(_cache_dir, _cache_filename)
# 7. 写入本地缓存文件
with open(_cachefile, 'wb') as f:
f.write(image_content)
ner_logger.info(f"图片尺寸: {width}x{height}, {image_url}")
# 8. RGBA格式转RGB(统一格式,避免后续处理报错)
if image.mode == 'RGBA':
image = image.convert('RGB')
# ====================== 二维码检测 ======================
_qrcode = 'N' # N=无二维码 H=含二维码 Y=纯二维码
_props = {}
try:
# 仅对大于64*64的图片检测二维码
if width * height > 64 * 64:
_qrcode, _props = detect_qr_code(_cachefile, width, height)
except Exception as e:
ner_logger.info(f"二维码检测失败 {image_url}: {e}")
# ====================== 构建图片基础属性 ======================
_props['img_width'] = width
_props['img_height'] = height
_props['img_md5'] = img_md5
_props['img_similar_md5'] = average_hash(_cachefile) # 均值哈希,用于相似图片去重
_props['img_count'] = 1 # 图片出现次数(去重用)
# ====================== OCR文字识别 ======================
_props['img_ocr'] = ''
ner_logger.info(f"二维码结果:{_qrcode} / {image_url}")
# 非纯二维码才进行OCR(纯二维码无需识别文字)
if _qrcode != 'Y':
try:
# 仅对大于64*64的图片做OCR
if width * height > 64 * 64:
ner_logger.info(f"开始OCR识别 {_cachefile}")
_ocr_text = ocr_parser.ocr_txt_new(_cachefile, width, height)
_props['img_ocr'] = _ocr_text
except Exception as e:
import traceback
error_info = traceback.format_exc()
ner_logger.info(f"OCR识别失败 {image_url}: {e}\n{error_info}")
# 如果图片含二维码,从OCR结果中提取二维码链接
if _qrcode == 'H' and has_qrcode_from_img(_props['img_ocr']):
set_qrcode_from_img(_cachefile, _props)
# 返回所有处理结果
return width, height, _qrcode, _props, _cachefile, _cache_filename, headers
def has_qrcode_from_img(img_ocr):
"""
判断OCR文本中是否包含二维码相关关键词(用于辅助判断二维码)
暂时直接返回True,不做关键词过滤
"""
# 原逻辑:包含'扫描','投递','应聘'等关键词则返回True
return True
def get_file_ext(image_url, image):
"""
获取图片文件后缀(优先从URL获取,失败则从图片格式获取)
:param image_url: 图片URL
:param image: PIL图片对象
:return: 后缀名 .jpg/.png/.gif/.webp
"""
# 从URL提取后缀
_filesuffix = get_file_extension(image_url)
if _filesuffix:
return _filesuffix
ner_logger.info(f"URL无法获取后缀,使用图片格式 {image_url}: {image.format}")
# 从PIL图片格式判断
if image.format == "WEBP":
return ".webp"
if image.format == "PNG":
return ".png"
if image.format == "JPEG":
return ".jpg"
if image.format == "GIF":
return ".gif"
# 默认返回png
return '.png'
def get_img_real_size_config(_data):
"""
加载图片真实显示尺寸配置文件(用于修正图片渲染大小)
:param _data: 配置数据
:return: 图片真实尺寸字典 {url: {rendered_width, rendered_height}}
"""
if 'wx_code_file_config' in _data:
_config_file = _data['wx_code_file_config']
if os.path.exists(_config_file):
with open(_config_file, 'r', encoding='utf-8') as f:
_config = json.load(f)
return _config
return {}
def get_img_size(_rendered_img, _url):
"""
从配置中获取图片实际渲染大小(修正网页显示尺寸)
:param _rendered_img: 尺寸配置字典
:param _url: 图片URL
:return: 是否找到、宽度、高度
"""
_combiled_url = recombine_url(_url)
# 模糊匹配URL
for kurl, size in _rendered_img.items():
_combiled_kurl = recombine_url(kurl)
if kurl in _url or _url in kurl or _combiled_kurl == _combiled_url:
return True, int(float(size['rendered_width'])), int(float(size['rendered_height']))
return False, 0, 0
def replace_img_urls_with_base64(spider_data, _data, soup, _props, _cache_dir):
"""
核心函数:处理HTML中所有<img>标签
流程:下载 → 检测 → 过滤 → 上传云存储 → 替换src → 清洗垃圾图片
:param spider_data: 爬虫数据对象(黑名单校验)
:param _data: 配置数据
:param soup: BeautifulSoup对象
:param _props: 输出属性字典
:param _cache_dir: 缓存目录
:return: 处理是否成功
"""
_props_imgs = {} # 存储所有图片信息
_rendered_imgs = get_img_real_size_config(_data) # 图片真实显示尺寸
_multipics = {} # 记录无文字图片(用于去重删除)
# 遍历HTML中所有img标签
for img_tag in soup.find_all('img'):
img_url = img_tag.get('src')
# 只处理http/https开头的有效图片
if img_url and img_url.startswith(('http://', 'https://')):
ner_logger.info(f"处理图片: {img_url}")
# 下载并解析图片
width, height, _qrcode, _iprops, _cachefile, _cache_filename, headers = url_to_base64(img_url, _cache_dir)
# 下载失败 → 直接删除标签
if width * height == 0:
img_tag.decompose()
continue
# 原始真实尺寸
intrinsic_width = width
intrinsic_height = height
# 使用配置中的渲染尺寸(如果有)
_ok, rendered_width, rendered_height = get_img_size(_rendered_imgs, img_url)
if _ok:
width = rendered_width
height = rendered_height
if width * height != intrinsic_width * intrinsic_height:
ner_logger.info(f"使用渲染尺寸: {width}x{height}, 真实尺寸{intrinsic_width}x{intrinsic_height},{img_url}")
# ====================== 图片黑名单过滤 ======================
# 1. 内容MD5黑名单
if spider_data.check_url_in_blacklist(_iprops['img_md5']):
img_tag.decompose()
ner_logger.info(f"图片MD5在黑名单,已移除 {img_url}")
continue
# 2. 相似哈希MD5黑名单
if spider_data.check_url_in_blacklist(_iprops['img_similar_md5']):
img_tag.decompose()
ner_logger.info(f"图片相似哈希在黑名单,已移除 {img_url},{_iprops['img_similar_md5']}")
continue
# 3. OCR文字内容黑名单
if len(_iprops['img_ocr']) > 30:
_md5txt = get_md5_clear_text(_iprops['img_ocr'])
_mdtv = getMD5Str(_md5txt)
ner_logger.info(f"图片OCR文本MD5 {_mdtv}")
if spider_data.check_md5_txt_in_blacklist(_mdtv):
img_tag.decompose()
ner_logger.info(f"图片OCR文本在黑名单,已移除 {img_url}")
continue
# ====================== 尺寸/质量过滤 ======================
# 过小图片 + 无文字 → 删除
if _qrcode == 'N' and len(_iprops['img_ocr']) < 5 and (
width * height < 120 * 120 or (width < 40 and height < 500) or (width < 500 and height < 40)):
img_tag.decompose()
ner_logger.info(f"图片过小且无文字,已移除 {img_url}")
continue
# 学校就业指导中心垃圾图过滤
if _qrcode in ['H'] and width * height < 300 * 700 and probe_school_blackimg(_iprops['img_ocr']):
img_tag.decompose()
ner_logger.info(f"学校垃圾广告图,已移除 {img_url}")
continue
# 高透明 + 无文字 → 删除
if _qrcode in ['N'] and len(_iprops['img_ocr']) < 5 and check_transparency_ratio(_cachefile):
img_tag.decompose()
ner_logger.info(f"图片透明度过高,已移除 {img_url}")
continue
# 记录无文字图片(后续去重)
if _qrcode == 'N' and len(_iprops['img_ocr']) < 20:
_multipics[img_tag] = img_url
# ====================== 给img标签添加自定义属性 ======================
img_tag['q_width'] = width
img_tag['q_height'] = height
img_tag['q_qrcode'] = _qrcode
img_tag['q_url'] = img_url
# ====================== 上传图片到华为云OBS ======================
_ok, _url = upload_file_to_obs(_cache_filename, _cachefile, headers)
if _ok:
# 替换src为云存储地址
img_tag['src'] = f"{_url}"
# 纯二维码添加尺寸缩放参数
if _qrcode == 'Y' and width > 100:
img_tag['src'] = f"{_url}?x-image-process=image/resize,m_fixed,w_{width},h_{height}"
# 记录云存储地址
_iprops['qz_img_url'] = _url
_iprops['rendered_width'] = width
_iprops['rendered_height'] = height
else:
ner_logger.info(f"OBS上传失败,使用原图 {img_url}")
return False
# ====================== 图片去重计数 ======================
if img_url in _props_imgs:
_props_imgs[img_url]['img_count'] += 1
else:
# 相似图片合并计数
for _k, _v in _props_imgs.items():
if _v['img_similar_md5'] == _iprops['img_similar_md5']:
_v['img_count'] += 1
_iprops['img_count'] += 1
break
_props_imgs[img_url] = _iprops
# 标记超长图片
if width > 500 and height > 3000:
_data["is_large_image"] = 'OK'
# ====================== 移除重复无意义图片 ======================
for img_tag, img_url in _multipics.items():
if img_url in _props_imgs and _props_imgs[img_url]['img_count'] > 1:
img_tag.decompose()
ner_logger.info(f"无文字重复图片,已移除 {img_url}")
# 保存所有图片信息
_props["img_urls"] = _props_imgs
return True
def intit_blacklist():
"""
初始化学校垃圾图片关键词黑名单(从文件加载)
:return: 关键词列表
"""
_blacklist = []
with open("data/black_img_text.txt", encoding="utf-8") as f:
for _line in f.read().splitlines():
_line = _line.strip()
if _line:
_blacklist.append(_line.strip())
return _blacklist
# 全局加载垃圾图片关键词
sch_blacklist = intit_blacklist()
def probe_school_blackimg(_text):
"""
检测图片文字是否为学校垃圾广告(就业中心、推广类)
:param _text: OCR文字
:return: True=垃圾图 False=正常图
"""
if len(_text) > 50:
return False
# 正则匹配关键词
for _blkre in sch_blacklist:
pattern = re.compile(_blkre)
if re.findall(pattern, _text):
return True
return False
def set_qrcode_from_img(_cache_file, _props):
"""
从图片中截取二维码区域 → 生成新图片 → 上传OBS → 记录二维码链接
:param _cache_file: 原图路径
:param _props: 属性字典
"""
_qk = None
_qr = None
_all_qr = {}
# 遍历属性中以http开头的二维码坐标
for _k, _v in _props.items():
if _k.startswith('http') and len(_props[_k]) > 0 and len(_props[_k].split(',')) == 4:
try:
ner_logger.info(f"截取二维码区域 {_cache_file}")
# 解析坐标:左、上、宽、高
left, top, width, height = _props[_k].split(',')
left, top, width, height = int(float(left)), int(float(top)), int(float(width)), int(float(height))
# 打开原图并裁剪
image = Image.open(_cache_file)
cropped_image = image.crop((left, top, left + width, top + height))
# 保存到内存
byte_arr = io.BytesIO()
cropped_image.save(byte_arr, format='PNG')
# 临时保存
_filename = os.path.splitext(_cache_file)
_nfilename = _filename[0] + '_qr.png'
with open(_nfilename, 'wb') as f:
f.write(byte_arr.getvalue())
# 上传OBS
_md5 = getMD5Bytes(byte_arr.getvalue())
_qrfilename = f'{_md5}_qr.png'
_ok, url = upload_file_to_obs(_qrfilename, _nfilename)
if _ok:
if not _qr:
_qk = _k
_qr = url
_all_qr[_k] = url
os.remove(_nfilename) # 删除临时文件
except Exception as e:
ner_logger.info(f"二维码截取失败: {e}")
# 保存最终二维码信息
if _qk:
_props['inside_qr_link'] = _qk
_props['inside_qr_pic'] = _qr
_props['all_qr_pics'] = _all_qr
💬 评论