阅读视图

发现新文章,点击刷新页面。

如何快速批量检索损坏的图片文件—python开发学习笔记(一)

为了解决几十万量级图片库内异常图片的检测,折腾出了一个基于 python 的图片检测程序。


一、混乱的图片仓库

用前一段发现的本地 AI 图片视频搜索引擎 MaterialSearch 整理十几年间积累的几十万张图片时,遇到了一个令人崩溃的场景:有上百张图片报损坏,经过部分核查,很多文件打开后呈现诡异色块,亦或者只有半截图,还有些文件大小为 0 KB。这些损坏的图片零散的散布在数千个子文件夹中,手动一个一个检查无异于大海捞针,累死也搞不定。于是 VS code 启动!!


二、初版方案

用 Pillow 暴力验证,直接用 Pillow verify()看看是否报错来解决。

代码V1.0方案(基础检测逻辑)

from PIL import Image

def check_img_v1(path):
    try:
        Image.open(path).verify()
        return True
    except:
        return False

V1.0方案的情况
1. 误报文件:很多图片会报损坏,但是用图片浏览器打开却十分正常,经过研究之后才知道,原来大量网站在使用一种叫做渐进式JPEG的技术,通过将图像数据分为多个扫描逐层渲染,可以在网速不好时图片先绘制出低分辨率的模糊轮廓,随着数据被下载逐步变为清晰图像(现代图片编码如WEBP、AVIF也都有类似的渐进式加载机制)。这导致需要完整解码才能验证所有扫描数据。因此被verify()误认为损坏。
2. 漏检文件:未完整下载的图片有时也能通过验证。
3. 性能问题:慢,按照测试计算,10万张图片的检测起码需要耗时4、5小时了。


三、改进方案

经过对 MaterialSearch 日志报错图片的抽查,发现损坏的文件主要是文件不完整导致的半截图,于是我打算改为:先检查文件结尾是否存在结束符来判定图片是否损坏,然后再做进一步检查。

代码V2.0(尾部校验部分代码)

def check_img_v2(path):
    with open(path, 'rb') as f:
        f.seek(-32, 2) #只用获取文件最后32字节就行
        trailer = f.read()
        if path.lower().endswith('.jpg'):
            return trailer.endswith(b'\xff\xd9')  # JPEG的结束符
        elif path.lower().endswith('.png'):
            return trailer.endswith(b'\xaeB`\x82')  # PNG的IEND块

V2.0方案的情况
1. 捕获到了异常文件 :下载一半的文件确实被检测出来了。
2. 检测了个寂寞 :如果图片附加了元数据,图片文件很可能就不是以\xff\xd9结尾了,结果就是1000张的测试图片,在尾部检测部分逻辑,有800多张都报了损坏……想快速检查了个寂寞。


四、再次优化

使用img.load() 强制加载所有数据。对渐进式jpeg图片做特殊处理逻辑。

代码V3.0(验证部分的修改)

def check_img_v3(path):
    try:
        with open(path, 'rb') as f:  
            img = Image.open(f)
            img.load()  # 强制加载完整图片

            # 特殊处理渐进式JPEG
            if img.format == 'JPEG' and 'progressive' in img.info:
                img.tile = []  

            return True
    except Exception as e:
        print(f"损坏文件: {path} | 错误类型: {type(e).__name__}")
        return False

V3.0方案的情况
1. 漏报率下降了很多
2. 渐进式JPEG兼容处理
3. 打印异常类型方便处理
4. 实际代码中自己傻逼了在verify()之后调用load(),导致文件指针不可用,说人话就是:代码逻辑中verify()做完检查后,就把图片文件关闭了,load()啥也获取不到。
5. 性能就很一般了,基本和初版差不多的速度。


五、终局之战

又经过一番研究和查证其实 Pillow verify()对渐进式图片检测是没问题的,误报率并没有我在V1测试时那么高,只是我本地环境的 Pillow 版本不够新而已,但也确实会有漏报。只用load()也会有漏报,有一点误报可以接受,但是漏报就无法接受了,所以还是需要联合检查。

最终决定采用如下逻辑
1. 先检测文件路径是否存在,收集所有路径。
2. img.verify()先上
3. 同一个循环内使用img.load()再来一次检测
4. 并行处理加快处理速度
5. 不在控制台显示扫描 log,毕竟绝大部分图片都是好的,没问题的显示出来无意义,只显示有问题的又很容易看起来像是卡住了,所以用 tqdm 做个进度条。还能大概估计下完成时间。
6. 用 jinja2 做个 html 格式的检测报告,毕竟在终端里复制粘贴起来也不方便。

代码V4.0(完善使用)

实在太长了就放github上了:img_validator.py

命令:python img_validator.py <"目录路径"> [并发数]

ex:python img_validator.py "D:\Download\图片" 8

最终会在脚本的同级目录下生成 html 格式的检测报告image_validation_report.html

  • 路径最好用””框住
  • 并发数可以不填,默认使用CPU全部线程。除非你用非常大的图,不然全线程跑也费不了多少CPU占用。
  • 理论上 10W 张图片在 12 线程下,用时应该半小时都不到,不过实际上受制于仓库盘使用的是 HDD 只能到 20~40files/s,机械硬盘的读出速度跟不上检测速度,特别是到最后20%的时候,速度更是掉到只有个位数了。18W 张图片的文件夹,检测用时接近5个小时,平均速度 ≈ 11 files/s
  • windows 和 unix 系统都可用,代码会自动处理两者的路径差异。

为什么会有这么多图片损坏

自己十年前写的P站抓取代码不完善,如果因为网络超时导致图片下载失败,爬虫会重试,但是之前损坏的图片有可能并不会被正确清除(删除部分代码没有正确处理超长文件名和带特殊符号的文件名),虽然带问题代码只使用了从14年到16年这大约一坤年,但是也积累了接近 600 张问题文件。不过倒是挺奇怪的,抓全年龄的部分出现大量这种问题,抓R18的几乎就没出错过,这是又为什么呢,沉思中…………

The post 如何快速批量检索损坏的图片文件—python开发学习笔记(一) appeared first on 秋风于渭水.

给博客增加 文章归档 和 年度统计 功能

终于在建站11年的当下终于给博客增加了文章归档页。
是基于 Rank Math SEO 插件的 Sitemap Settings 功能实现了一个简单的文章归档页。
顺带在文章归档页内塞了一个额外的网站数据年度统计功能。
大概长这样:文章归档


起因 为什么想要加文章归档页了

起因是在《WindowTabs: 让几乎一切 Windows 窗口支持多标签功能》一文下,博友 ddw2019 在评论中提到我的站点“没有找到 Archive 页面,一个个翻页起来,感觉还是有点点费劲的呀。”

之前我一直感觉文章归档页的感知不强,我认为它的主要意义和好处是:系统化整理文章、提高可发现性、增强用户体验、SEO优化、展示创作历程这 5 个方面。

毕竟现在右侧有文章分类、随机文章、热门文章三个模块。系统化整理文章提高可发现性性上已经足够了。
SEO 优化上文章归档页固然归可以帮助搜索引擎更好地抓取和索引博客内容,不过在已经有 xml 站点地图的时候,文章归档页对于提供博客的 SEO 优化上作用不大,相较这种 html 格式站点地图,xml 格式的站点地图对搜索引擎会更加友好一些。

不过嘛,确实考虑到博客里确实是缺少一个展示创作历程和增强用户体验的东西。于是在2024年的年底,终于在建站 11 年后决定加上这个东西。

思考 实现文章归档页的方案

那么现在问题来了,如何多快好省的实现文章归档页 Archive 呢?
从这个博客的可以称之为极简的风格上估计大家都能看出,我并不太喜欢在美化折腾博客页面上消耗过多精力。

  • 方案一 网友 ddw2019 的博客中写到的方案 年度归档插件
    好处是省事的多,装个插件的事情。缺点是又双叒叕需要多装一个插件了,博客速度又要被拖累。
  • 方案二 利用现有插件 Rank Math SEO 的站点地图功能实现。
    好处是利用了现有插件,少装了一个插件,缺点是样式就简陋的多了,样式的自定义空间也小的多。

  • 方案三 自己实现一个页面
    优点是更加轻量,样式完全可以自定义。缺点是自己写东西需要费更多的时间。

权衡利弊后,我决定先用方案二顶上,先解决有无问题再说。

开干 实现文章归档

以下的必要前提是:你要有装 Rank Math SEO 这个插件,并且开启了他的 Sitemap 组件。
这里我只介绍必要的操作,如果想了解更多一点,这里有篇不错的文章 《家庭数据中心系列Rank Math SEO设置与优化-无敌的个人博客》

  1. 新建一个页面,起名叫文章归档并把固定链接设置为ex.your.domain/archives并发布。
    (是的,内容为空就行)
  2. 进入博客后台,左侧找到 Rank Math SEO 的 Sitemap Settings 部分,切换到 HTML Sitemap 标签,并开启开关。

  3. 将 Display Format (显示模式)设置为 Page 也就是页面模式。如果你想在文章归档页放入额外的东西,也可以设置为 Shortcode 模式,这样上一步时,需要在你想要插入文章归档页的地方写短代码[rank_math_html_sitemap] 即可。

  4. Sort by (排序方式),既然是文章归档页,那自然是选 Published Date (基于发表日期)啦。并且开启下边的 Show Dates (显示日期),这样文章后面就会显示发布日期了。

  5. Item Titles (项目标题),作为文章归档页,建议选 Item Titles (文章标题)而不是 SEO Titles (SEO用标题)这两者的区别是:SEO用标题是给搜索引擎和搜索结果用户看的,可能会比实际的文章标题会有些奇妙的拉长和缩短。比如文章实际标题很长,但是 SEO 标题可能会为了能保证在搜索结果页把关键词凸显出来会相对减少次要描述。也就是说 Item Titles (文章标题)更能反映文章内容。

  6. 最后保存即可,现在前边设定的固定链接看到一个比较简陋的,文章归档页了。
    类似本站这样:文章归档 (https://www.tjsky.net/archives)

增强 增加博客年度数据统计功能

本来事情就该结束了,结果中午的时候,我又在博友圈里看到了 ts小陈 的文章:《为WordPress网站添加年度数据汇总
我发现,“诶,这个东西不错嘛,可以很直观的展现每年博客的发展数据。”
随后尝试直接套用代码,然后果不其然的发现:事情并没有那么简单。
毕竟我现在用的主题上次更新已经是2022年了,主题并没有完美支持页面模板php文件。
并不能像他原文介绍的那样:

  • 将php文件上传到你主题的页面模板文件夹(pages文件夹)
  • 创建新页面选择该模板-年度数据
  • 发布页面,搞定。

于是我又研究了一下WordPress这个页面模板php文件应该怎么写,以及他代码具体是做了什么。
重新写了适配自己主题的页面模板文件。主要修改点是三个

  • 修改一下样式适配博客主题
    主要是套用原有page页的代码,改一个新的模板文件。还有些小修改,比如表格做成圆角啊,表格改个主题色啊,属于毫无技术含量的修改。
  • 修改其中统计字数的函数str_word_count()mb_strlen()
    中文不用空格来分隔单词,对于中文字符串,str_word_count()会将两个标点之间的整个字符串都视为一个单词,mb_strlen()则会根据当前PHP设定的编码自适应用基于UTF8或gbk等编码正确统计字数。所以改一下得到准确的字数统计。

//修改前的代码,在原文文件的第79行
word_count = str_word_count(strip_tags(content)); // 去除HTML标签后计算字数

//修改后的代码
word_count = mb_strlen(strip_tags(content)); // 去除HTML标签后计算中文字数

大部分面板和一键脚本部署的 PHP 应该默认就开了 mb_strlen 这个函数,不过对于用虚拟主机的可能需要注意下,这个 PHP 函数不一定被启用了。

  • 把缓存改成1天,30天有些长了。

原文写的是”数据使用 Memcached 缓存”,让人以为只能用于 Memcached。不过实际上,如果你用 Redis 或者其他什么缓存,只要你装了对应的缓存插件,比如用 Redis 并且装了 Redis Object Cache。可以保证wp_cache_get()wp_cache_set() 函数能正常工作就不需要注释掉缓存代码,不然每次都现场计算一次,这性能浪费不小。

最终效果如下图

很直观的数据
2017年上一个博客被我rm -rf */干没了之后,在 17 年 12月 底重建,18 年还在保持正常的更新,可惜博客没啥人看(看评论数量),外在驱动力不足 导致 19 ~ 21 年我基本上没更新过博客,21 年底,因为一些机缘巧合博客收获了一波较大流量,加之因为这个机缘巧合重新拾起写代码的乐趣,重新开始恢复正常的更新频率。

The post 给博客增加 文章归档 和 年度统计 功能 appeared first on 秋风于渭水.

Linux重装与dotfile整理分享

最近把电脑上面的Linux系统给重装了,同时呢也要配置新的MacBook,就整理了一个个人的dotfile,这里分享一下linux上的我主要使用的软件,以及我的dotfile内容。

什么是Dotfile

dotfile字面意思就是以.开头的文件,在Linux当中就是隐藏文件,我们大家说的一般指的就是配置文件,比如shell的.bashrc.profile文件等。我在整理自己的dotfile的时候参考了一些网上大神的dotfile文件,这里我主要是包含我的shell的一些配置文件,vimgitrime相关的文件。

我的 Dotfile

为了保持Linux和Mac系统的统一, 我将Linux的Shell也换成了zsh,同时为了简单并没有使用oh-my-zsh,只是添加了一些自己常用的aliases

而Vim则使用neovim,它相当于是重新开发的,我想比vim应该代码上面更加高效,毕竟少了很多的历史包袱。另外它的配置文件可以使用Lua进行编写,而不是使用vim script,这样也是它的一大优点。

除了配置之外,还增加了脚本用用于将这些配置文件自动拷贝到对应的目录,使用以下代码判断是Linux系统还是Mac系统:

1
2
3
4
5
if [ "$(uname -s)" == "Darwin" ]; then
 //action for mac
else
 //action for linux
fi

另外呢,对于Mac系统,在初始化脚本中还添加了homebrew的安装,并且通过使用Brewfile在定义需要安装的一些软件,这样在执行brew bundle的时候可以把这些软件都安装上去。

对于Linux的目前还没做啥,都是通过自己手动安装的,不过一些操作也记录到了shell文件当中了。

Linux上的软件

既然写了文章,就顺便分享一下我的Linux上面还在用的软件吧。 首先是Shell,为了跟Mac保持统一,也改用了zsh,如果你也想要设置zsh为你的默认shell,可以执行如下命令并重启(前提你已经安装的zsh):

1
 sudo chsh -s $(which zsh) $USER

编辑器目前在用的有三款,主要在用neovim,同时代码文件还会使用vscode,因为有些场景neovim操作比较麻烦(对于快捷键不太熟悉),最近也在使用阮一峰老师之前推荐过的zed,据说比vscode性能更高,目前体验是对于很多语言的支持是已经内置了,不需要在安装插件,这点是好评的。

输入法在使用Fcitx5,输入方案则是使用了Rime,Rime的配置则是参考了雾凇拼音,而我主要使用小鹤双拼。

其他还在使用的软件包括:

项目开发: Android studio

截图软件:Flameshot

启动器: ULauncher, 使用简单,支持的插件数量也比较多

文档搜索: Zeal doc,mac 上面dash的window和linux平台开源版本,支持dash的文档。

文件同步: Syncthing

局域网文件传输: LocalSend

聊天软件: Weixin, telegram

文档和博客编辑: Obsidian

网页浏览器: Edge

Linux 开启zram

我的电脑已经有32G的内存了,大部分时候是够用的,但是编译Android系统的时候就不够用了。因此需要想办法,一种方式是弄一个swap空间,但是swap的速度不是很快,经过查询资料了解到现在linux已经有了一种新的虚拟内存技术,也就是zram,它主要功能是虚拟内存压缩,它是通过在RAM中压缩设备的分页,避免在磁盘分页,从而提高性能。

而想要启用它其实很简单,在我们的Ubuntu中,只需要首先关闭原先的swap空间,编辑/etc/fstab文件,将其中的swapfile条目注释掉。之后调用如下命令:

1
sudo swapoff /swapfile

如果你本来就没有设置swap,那就不需要做上面的操作,直接安装zram-config:

1
2
3
sudo apt install zram-config
systemctl enable zram-config //设置开机启动开启zram的服务
systemctl start zram-config //启动zram服务

之后可以调用如下命令验证:

1
cat /proc/swaps

我们在系统监控里面也能看到,不过还是swap。 以上方式开启的zram为物理内存的一半大小,当然也是可以修改的。 修改/usr/bin/init-zram-swapping文件中的mem大小即可。

如果对于我的dotfile感兴趣,可以查看我的repo, 地址为: https://github.com/sangmingming/dotfiles,其中我提到的初始化脚本为script/bootstrap文件。

看完评论一下吧

CloudCone VPS 2024年黑五闪购即将开始,新老用户可领取积分兑换奖励

这就是个年更贴

CloudCone 是一家提供各种虚拟私人服务器(VPS)托管服务的公司,主要特点是

便宜(总体来说挺便宜的)

IP干净(可以解锁奈菲、油管、迪士尼+等各种流媒体)

支持支付宝 (总有人没信用卡嘛)

cloudcone


注册 CloudCone

注册地址:https://app.cloudcone.com (可能需要魔法上网才能打开)
备用直连注册地址:https://app.cloudcone.com.cn

注册时资料信息尽量不要乱填,请符合你当时的浏览环境,以免触发风控!!!

新老用户积分领取

CloudCone 黑五 VPS 闪购促销

目前 CloudCone 黑五闪购还未开始,建议提前注册账号,并收藏活动页面:https://app.cloudcone.com/events/blackfriday/,准点的时候去刷新页面,有各种不同的闪购促销,预计会有10~12$左右的年费VPS

2023年 CloudCone 黑五“攒积分兑礼品“活动答案

30:In which year was CloudCone launched? (CloudCone 是哪一年推出的?)
回答:2017

31:Which year was our One-click Applications introduced?  (我们的One-click应用程序是哪一年推出的?)
回答:2020


32:How many layers does CloudCone DDoS protection cover? ?(我们的DDOS提供几层防护 )
回答:7

33:How many disk bays does the $249/mo Dedicated Server plan support?  (249刀/月计划的专用服务器服务器,可以提供几个硬盘托架)
回答:

34:How many domains are available with the 'Startup' subscription of our Hosted Email service?  (我们的托管电子邮件服务的“Startup”订阅可以使用多少个域?  )
回答:10

35:What is the hourly cost of the SC2 Business Plan?  (SC2 商业计划的每小时费用是多少?)
回答:

结语

总的来说呢,CloudCone 的 VPS 价格实惠,售后响应迅速。如果您需要性价比高的美国解锁流媒体 VPS ,可以考虑关注一下。此外,在BLACK FRIDAY 2024 期间,您可以领取积分并兑换奖励。这个活动对新老用户都开放,简单凑一下可以换1刀出来。建议提前注册账号,并等待黑五闪购促销。

我主要是买来做流媒体解锁,放一点需要美国IP的小工具的。他家母鸡的CPU占用有点高,邻居有时候闹幺蛾子,连带你的小鸡也卡。主要优势是便宜。

CloudCone 注册地址:https://app.cloudcone.com (可能需要魔法上网)
CloudCone 备用直连注册地址:https://app.cloudcone.com.cn

The post CloudCone VPS 2024年黑五闪购即将开始,新老用户可领取积分兑换奖励 appeared first on 秋风于渭水.

解决Ubuntu下Sublime text 3中文输入的问题

好久之前便听朋友说起Sublime Text这款软件很好用,终于这几天有空折腾,把软件给装起来了。用起来确实很不错,写代码很爽。
但是用了一段时间之后,我需要输入中文了,无论怎么切换输入法,都无法切换到中文。

网上搜索了一下,原来这是Bug。找解决方法吧。下面介绍我的解决方案,是大神cjacker解决成功的啦,我只是copy一下,方便大家在遇到这个问题的时候可以方便解决。

 我的系统:ubuntu 13.04
我的输入法:fcitx
sublime版本:3059

理论上支持 sublime text2/3

1.保存代码sublime-imfix.c

/*
sublime-imfix.c
Use LD_PRELOAD to interpose some function to fix sublime input method support for linux.
By Cjacker Huang <jianzhong.huang at i-soft.com.cn>
gcc -shared -o libsublime-imfix.so sublime_imfix.c `pkg-config --libs --cflags gtk+-2.0` -fPIC
LD_PRELOAD=./libsublime-imfix.so sublime_text
*/
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
typedef GdkSegment GdkRegionBox;
struct _GdkRegion
{
long size;
long numRects;
GdkRegionBox *rects;
GdkRegionBox extents;
};
GtkIMContext *local_context;
void
gdk_region_get_clipbox (const GdkRegion *region,
GdkRectangle *rectangle)
{
g_return_if_fail (region != NULL);
g_return_if_fail (rectangle != NULL);
rectangle->x = region->extents.x1;
rectangle->y = region->extents.y1;
rectangle->width = region->extents.x2 - region->extents.x1;
rectangle->height = region->extents.y2 - region->extents.y1;
GdkRectangle rect;
rect.x = rectangle->x;
rect.y = rectangle->y;
rect.width = 0;
rect.height = rectangle->height;
//The caret width is 2;
//Maybe sometimes we will make a mistake, but for most of the time, it should be the caret.
if(rectangle->width == 2 && GTK_IS_IM_CONTEXT(local_context)) {
gtk_im_context_set_cursor_location(local_context, rectangle);
}
}
//this is needed, for example, if you input something in file dialog and return back the edit area
//context will lost, so here we set it again.
static GdkFilterReturn event_filter (GdkXEvent *xevent, GdkEvent *event, gpointer im_context)
{
XEvent *xev = (XEvent *)xevent;
if(xev->type == KeyRelease && GTK_IS_IM_CONTEXT(im_context)) {
GdkWindow * win = g_object_get_data(G_OBJECT(im_context),"window");
if(GDK_IS_WINDOW(win))
gtk_im_context_set_client_window(im_context, win);
}
return GDK_FILTER_CONTINUE;
}
void gtk_im_context_set_client_window (GtkIMContext *context,
GdkWindow *window)
{
GtkIMContextClass *klass;
g_return_if_fail (GTK_IS_IM_CONTEXT (context));
klass = GTK_IM_CONTEXT_GET_CLASS (context);
if (klass->set_client_window)
klass->set_client_window (context, window);
if(!GDK_IS_WINDOW (window))
return;
g_object_set_data(G_OBJECT(context),"window",window);
int width = gdk_window_get_width(window);
int height = gdk_window_get_height(window);
if(width != 0 && height !=0) {
gtk_im_context_focus_in(context);
local_context = context;
}
gdk_window_add_filter (window, event_filter, context);
}

2.安装C/C++的编译环境和gtk libgtk2.0-dev

sudo apt-get install build-essential
sudo apt-get install libgtk2.0-dev

3.编译共享内存

gcc -shared -o libsublime-imfix.so sublime_imfix.c `pkg-config --libs --cflags gtk+-2.0` -fPIC

4.启动测试

LD_PRELOAD = ./libsublime-imfix.so sublime_text

正常的话这样是没有问题的。

然后我们在修改我们的desktop文件,使图标也可以使用

sudo vi /usr/share/applications/sublime-text.desktop

先将so文件移动到sublime text的目录

然后按照如下替换(主要是每次执行之前,去预加载我们的libsublime-imfix.so库)

[Desktop Entry]
Version=1.0
Type=Application
Name=Sublime Text
GenericName=Text Editor
Comment=Sophisticated text editor for code, markup and prose
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' %F
Terminal=false
MimeType=text/plain;
Icon=sublime-text
Categories=TextEditor;Development;
StartupNotify=true
Actions=Window;Document;
[Desktop Action Window]
Name=New Window
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' -n
OnlyShowIn=Unity;
[Desktop Action Document]
Name=New File
Exec=bash -c 'LD_PRELOAD=/opt/sublime_text/libsublime-imfix.so /opt/sublime_text/sublime_text' --command new_file
OnlyShowIn=Unity;

看完评论一下吧

❌