Photoshop 自动水印脚本

需求:

1、读取相片中的拍摄日期信息,添加水印到相片上,要求模拟胶片水印

2、自动化处理

3、方便后期调整

实现:

查询教程

用Photoshop脚本批量为照片增加拍摄时间水印批量添加时间水印灌进肚里的大老虎的博客-CSDN博客

这种胶片风格的水印照片是用什么软件拍的? – 知乎 (zhihu.com)

技术路线

Photoshop + Javascript

原理:

photoshop具有javascript脚本api,在javascript脚本中可以调用action,也可以在action中记录调用的js脚本

在js脚本中可以获取图片的信息

实现过程:

1、编辑Javascript脚本

Js脚本可以一次运行,批量处理,也可以在一个打开的文件中运行。

2、在打开的文件中运行一次脚本,或者在photoshop启动页打开脚本

调用脚本路径:

File->Scripts->Browse…>打开js文件

脚本内容:

AddDateWatermark.jsx 对打开的文件一次处理:

/*
功能:photoshop脚本从exif获取日期,自动添加日期水印。如没有exif日期信息,则从xmp信息从读取日期。
作者:leongongye, https://github.com/leongongye
参考:laozeng, https://github.com/laozeng1024,感谢!
*/

//自定义字符串,如“@上海”,使用urlencode编码
var customStr = "%40%E4%B8%8A%E6%B5%B7";
//exif中“日期时间”字段名称,urlencode编码
var photoTimeStr = "%E6%97%A5%E6%9C%9F%E6%97%B6%E9%97%B4";
//exif中“日期戳”字段名称,urlencode编码
var photoTimeStr2 = "%E6%97%A5%E6%9C%9F%E6%88%B3";
//"原始日期时间"
var photoTimeStr3 = "%E5%8E%9F%E5%A7%8B%E6%97%A5%E6%9C%9F%E6%97%B6%E9%97%B4";

var actionName = "DateWatermark_film";
var batchMode = false;

if(batchMode){
    batchProcessDoc();
}else{
    var docRef = app.activeDocument;
    addDateTimeWatermark(docRef);
}

function batchProcessDoc(){
    var inputFolder = Folder.selectDialog("选择添加日期水印图片所在文件夹:");
    var outFolder = Folder.selectDialog("选择图片保存输出的文件夹:");
    
    //判断文件夹是否存在
    if (inputFolder != null && inputFolder != null) {
        //获得文件夹下的所有图片
        var fileList = inputFolder.getFiles();
    
        //遍历图片
        for (var i = 0; i < fileList.length; i++){
            //判断图片是否正常文件,并且处于非隐藏状态
            if (fileList[i] instanceof File && fileList[i].hidden == false) {       
                //打开遍历到的图片
                var docRef = open(fileList[i]);
    
                //设置另存路径文件名,重命名为:new_原文件名
                var fileout = new File(outFolder+'/new_'+ basename(fileList[i]))
    
                // // 旋转照片
                // if(docRef.width > docRef.height){
                //     docRef.rotateCanvas(90);
                // }
    
                // 添加水印
                addDateTimeWatermark(docRef);
    
                //另存照片
                saveDocAsCopy(docRef, fileout);
            }
        }
        alert("添加日期水印已处理完成!")
    }
}

function saveDocAsCopy(docRef, fileout) {
    //定义一个变量[asCopy],用来指定图片以副本的方式保存
    var asCopy = true;

    //定义一个变量[extensionType],用来指定图片名称的后缀为小写的.jpg
    var extensionType = Extension.LOWERCASE;

    //定义一个变量[options],用来指定图片保存的格式为JPG。PNG为PNGSaveOptions
    var jpegSaveOptions = JPEGSaveOptions;
    jpegSaveOptions.embedColorProfile = true;  
    jpegSaveOptions.formatOptions = FormatOptions.STANDARDBASELINE;  
    jpegSaveOptions.matte = MatteType.NONE;  
    jpegSaveOptions.quality = 12;

    docRef.saveAs(fileout, jpegSaveOptions, asCopy, extensionType);
    
    //操作完成后,直接关闭文档
    if(batchMode==true){
        docRef.close(SaveOptions.DONOTSAVECHANGES);
    }
}


function addDateTimeWatermark(docRef) {
    //获得exif照片日期,可自行加自定义文字customStr
    //photoTime = getExifData(docRef) + decodeURIComponent(customStr)
    photoTime = getDocCreateTime(docRef)

    //如果exif没有日期数据,从文件名读取
    if (photoTime == 0){
        photoTime = basename(fileList[i]);
        photoTime = photoTime.toString().slice(0, -4);
    }

    //新建图层
    var layerRef = docRef.artLayers.add();

    //设置为文字图层
    layerRef.kind = LayerKind.TEXT;

    //设置图层文字
    layerRef.textItem.contents = "X " + photoTime;
    // gimmePostScriptFontName("asdfasdf");

    //根据图片宽度比例,设置文字大小
    var docMaxSide = Math.min(docRef.width,docRef.height);
    var textSize = docMaxSide/42/(docRef.resolution/72); //默认分辨率72,根据分辨率修改pt
    layerRef.textItem.size = textSize;
    layerRef.textItem.font = "LcdD"; //设置字体

    
    // 设置文本对齐方式
    layerRef.textItem.justification = Justification.RIGHT;

    var textWidthOffset = docRef.width - textSize * 4;
    var textHeightOffset = docRef.height - textSize * 3;
    layerRef.textItem.position = new Array(textWidthOffset,textHeightOffset);


    //定义颜色
    var color = new RGBColor();

    //设置red属性
    color.red = 200;
    //设置green属性
    color.green = 58;
    //设置blue属性
    color.blue = 20;
    
    //定义水印文字的颜色
    var sc = new SolidColor();

    //设置[sc]对象的[rgb]属性的值为变量[color]
    sc.rgb = color;

    //将文本图层的字体颜色设置为变量[sc]
    layerRef.textItem.color = sc;

    //设置文本透明度
    // layerRef.fillOpacity = 74;

    // 设置混合模式
    // layerRef.blendMode = BlendMode.SCREEN;
    // do action
    app.doAction(actionName,"Scripts");

    // 对亮背景做调整
    if(actionName=="DateWatermark_film"){
        var endPixelBrightness = getPixelColorBrightness(docRef, [textWidthOffset, textHeightOffset]);
        if(endPixelBrightness > 65){
            // layerRef.blendMode = BlendMode.HARDLIGHT;
            layerRef.fillOpacity = 100;
        }
        //合并文本图层至背景图层
        layerRef.merge();
    }
}


function getPixelColorBrightness(docRef, position) {
    // position = [new UnitValue( 100,'px'),new UnitValue(100,'px' )]
    var colorSampler = docRef.colorSamplers.add(position);
    var Brightness = Math.round(colorSampler.color.hsb.brightness); //Brightness value
    colorSampler.remove();
    return Brightness;
}


function getCreateDateFromXmp(doc) {
    var ns = "http://ns.adobe.com/xap/1.0/";
    ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
    xmpMeta = new XMPMeta(doc.xmpMetadata.rawData);
    var theValue = xmpMeta.getProperty(ns, "CreateDate");
    return theValue;
}

//获取exif中的日期
function getDocCreateTime(doc) {

    var exifData = doc.info.exif;

    var photoTime = 0

    //1. 优先从 exif 里取
    for(j = 0; j < exifData.length; j++ ) 
    {
        encodeStr = encodeURIComponent(exifData[j][0]);
        switch(encodeStr)
        {   
            //urlencode 中文再判断
            //日期时间
            case photoTimeStr: case photoTimeStr2: case photoTimeStr3:
                photoTime = exifData[j][1];

                p = photoTime.split(" ")
                // 2020:10:11 12:08:33 替换为2020-10-11 12:08:33
                // 格式 2023-01-13 14:23:58
                // photoTime = p[0].replace(/:/g,"-")+" "+p[1]
                photoTime = p[0].replace(/:/g,"-")
                break;
        }

    } 

    if(photoTime==0){
        // 2. 最后从 xmp 数据里取,原始格式 2023-01-14T10:11:07+08:00
        var xmpCreateDateStr = getCreateDateFromXmp(doc)+"";
        var sp = xmpCreateDateStr.split("T");
        // photoTime = sp[0]+" "+sp[1].substring(0,8);
        // photoTime = sp[0].replace(/-/g,"/")
        photoTime = sp[0];

        /*
        if(doc.info.creationDate){
            // 2. 从doc的creationDate 里取,格式 20230113
            // photoTime = doc.info.creationDate+"-D";
            photoTime = getCreateDateFromXmp(doc)+"-X";
        }else{
            // 3. 最后从 xmp 数据里取,原始格式 2023-01-14T10:11:07+08:00
            photoTime = getCreateDateFromXmp(doc)+"-X";
        }
        */
    }

    // alert(photoTime);


    // 调整文字格式 2023.09.23
    var parts = photoTime.split("-"); // 将日期字符串拆分成年、月和日
    var modifiedDate = "'" + parts[0].substring(2) + " " + parts[1] + " " + parts[2]; // 重新构建日期字符串
    return modifiedDate;

}

function ShowTheObject(obj){
  var des = "";
    for(var name in obj){
        // des += name + ";";
        des += name + ":" + obj[name] + ";";
    }
  return des;
}

//获取文件名
function basename(str) {
    str = str.toString();
    var idx = str.toString().lastIndexOf('/')
    idx = idx > -1 ? idx : str.lastIndexOf('\\')
    if (idx < 0) {
        return str
    }
    return str.substring(idx + 1);
}

//获取字体
function gimmePostScriptFontName(f)
{
  numOfFonts = app.fonts.length;
  // var s = "";
  for (var i = 0, numOfFonts; i < numOfFonts; i++)
  {
    fnt = app.fonts[i].name;
    // s += app.fonts[i].name + "***" + app.fonts[i].postScriptName + ";\n";

    if (f == fnt)
    {
      return app.fonts[i].postScriptName;
    }
  }
}

AddDateWatermark_batch.jsx批量处理:

/*
功能:photoshop脚本从exif获取日期,自动添加日期水印。如没有exif日期信息,则从xmp信息从读取日期。
作者:leongongye, https://github.com/leongongye
参考:laozeng, https://github.com/laozeng1024,感谢!
*/

//自定义字符串,如“@上海”,使用urlencode编码
var customStr = "%40%E4%B8%8A%E6%B5%B7";
//exif中“日期时间”字段名称,urlencode编码
var photoTimeStr = "%E6%97%A5%E6%9C%9F%E6%97%B6%E9%97%B4";
//exif中“日期戳”字段名称,urlencode编码
var photoTimeStr2 = "%E6%97%A5%E6%9C%9F%E6%88%B3";
//"原始日期时间"
var photoTimeStr3 = "%E5%8E%9F%E5%A7%8B%E6%97%A5%E6%9C%9F%E6%97%B6%E9%97%B4";

var actionName = "DateWatermark_film";
var batchMode = true;

if(batchMode){
    batchProcessDoc();
}else{
    var docRef = app.activeDocument;
    addDateTimeWatermark(docRef);
}

function batchProcessDoc(){
    var inputFolder = Folder.selectDialog("选择添加日期水印图片所在文件夹:");
    var outFolder = Folder.selectDialog("选择图片保存输出的文件夹:");
    
    //判断文件夹是否存在
    if (inputFolder != null && inputFolder != null) {
        //获得文件夹下的所有图片
        var fileList = inputFolder.getFiles();
    
        //遍历图片
        for (var i = 0; i < fileList.length; i++){
            //判断图片是否正常文件,并且处于非隐藏状态
            if (fileList[i] instanceof File && fileList[i].hidden == false) {       
                //打开遍历到的图片
                var docRef = open(fileList[i]);
    
                //设置另存路径文件名,重命名为:new_原文件名
                var fileout = new File(outFolder+'/new_'+ basename(fileList[i]))
    
                // // 旋转照片
                // if(docRef.width > docRef.height){
                //     docRef.rotateCanvas(90);
                // }
    
                // 添加水印
                addDateTimeWatermark(docRef);
    
                //另存照片
                saveDocAsCopy(docRef, fileout);
            }
        }
        alert("添加日期水印已处理完成!")
    }
}

function saveDocAsCopy(docRef, fileout) {
    //定义一个变量[asCopy],用来指定图片以副本的方式保存
    var asCopy = true;

    //定义一个变量[extensionType],用来指定图片名称的后缀为小写的.jpg
    var extensionType = Extension.LOWERCASE;

    //定义一个变量[options],用来指定图片保存的格式为JPG。PNG为PNGSaveOptions
    var jpegSaveOptions = JPEGSaveOptions;
    jpegSaveOptions.embedColorProfile = true;  
    jpegSaveOptions.formatOptions = FormatOptions.STANDARDBASELINE;  
    jpegSaveOptions.matte = MatteType.NONE;  
    jpegSaveOptions.quality = 12;

    docRef.saveAs(fileout, jpegSaveOptions, asCopy, extensionType);
    
    //操作完成后,直接关闭文档
    if(batchMode==true){
        docRef.close(SaveOptions.DONOTSAVECHANGES);
    }
}


function addDateTimeWatermark(docRef) {
    //获得exif照片日期,可自行加自定义文字customStr
    //photoTime = getExifData(docRef) + decodeURIComponent(customStr)
    photoTime = getDocCreateTime(docRef)

    //如果exif没有日期数据,从文件名读取
    if (photoTime == 0){
        photoTime = basename(fileList[i]);
        photoTime = photoTime.toString().slice(0, -4);
    }

    //新建图层
    var layerRef = docRef.artLayers.add();

    //设置为文字图层
    layerRef.kind = LayerKind.TEXT;

    //设置图层文字
    layerRef.textItem.contents = "X " + photoTime;
    // gimmePostScriptFontName("asdfasdf");

    //根据图片宽度比例,设置文字大小
    var docMaxSide = Math.min(docRef.width,docRef.height);
    var textSize = docMaxSide/42/(docRef.resolution/72); //默认分辨率72,根据分辨率修改pt
    layerRef.textItem.size = textSize;
    layerRef.textItem.font = "LcdD"; //设置字体

    
    // 设置文本对齐方式
    layerRef.textItem.justification = Justification.RIGHT;

    var textWidthOffset = docRef.width - textSize * 4;
    var textHeightOffset = docRef.height - textSize * 3;
    layerRef.textItem.position = new Array(textWidthOffset,textHeightOffset);


    //定义颜色
    var color = new RGBColor();

    //设置red属性
    color.red = 200;
    //设置green属性
    color.green = 58;
    //设置blue属性
    color.blue = 20;
    
    //定义水印文字的颜色
    var sc = new SolidColor();

    //设置[sc]对象的[rgb]属性的值为变量[color]
    sc.rgb = color;

    //将文本图层的字体颜色设置为变量[sc]
    layerRef.textItem.color = sc;

    //设置文本透明度
    // layerRef.fillOpacity = 74;

    // 设置混合模式
    // layerRef.blendMode = BlendMode.SCREEN;
    // do action
    app.doAction(actionName,"Scripts");

    // 对亮背景做调整
    if(actionName=="DateWatermark_film"){
        var endPixelBrightness = getPixelColorBrightness(docRef, [textWidthOffset, textHeightOffset]);
        if(endPixelBrightness > 65){
            // layerRef.blendMode = BlendMode.HARDLIGHT;
            layerRef.fillOpacity = 100;
        }
        //合并文本图层至背景图层
        layerRef.merge();
    }
}


function getPixelColorBrightness(docRef, position) {
    // position = [new UnitValue( 100,'px'),new UnitValue(100,'px' )]
    var colorSampler = docRef.colorSamplers.add(position);
    var Brightness = Math.round(colorSampler.color.hsb.brightness); //Brightness value
    colorSampler.remove();
    return Brightness;
}


function getCreateDateFromXmp(doc) {
    var ns = "http://ns.adobe.com/xap/1.0/";
    ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript');
    xmpMeta = new XMPMeta(doc.xmpMetadata.rawData);
    var theValue = xmpMeta.getProperty(ns, "CreateDate");
    return theValue;
}

//获取exif中的日期
function getDocCreateTime(doc) {

    var exifData = doc.info.exif;

    var photoTime = 0

    //1. 优先从 exif 里取
    for(j = 0; j < exifData.length; j++ ) 
    {
        encodeStr = encodeURIComponent(exifData[j][0]);
        switch(encodeStr)
        {   
            //urlencode 中文再判断
            //日期时间
            case photoTimeStr: case photoTimeStr2: case photoTimeStr3:
                photoTime = exifData[j][1];

                p = photoTime.split(" ")
                // 2020:10:11 12:08:33 替换为2020-10-11 12:08:33
                // 格式 2023-01-13 14:23:58
                // photoTime = p[0].replace(/:/g,"-")+" "+p[1]
                photoTime = p[0].replace(/:/g,"-")
                break;
        }

    } 

    if(photoTime==0){
        // 2. 最后从 xmp 数据里取,原始格式 2023-01-14T10:11:07+08:00
        var xmpCreateDateStr = getCreateDateFromXmp(doc)+"";
        var sp = xmpCreateDateStr.split("T");
        // photoTime = sp[0]+" "+sp[1].substring(0,8);
        // photoTime = sp[0].replace(/-/g,"/")
        photoTime = sp[0];

        /*
        if(doc.info.creationDate){
            // 2. 从doc的creationDate 里取,格式 20230113
            // photoTime = doc.info.creationDate+"-D";
            photoTime = getCreateDateFromXmp(doc)+"-X";
        }else{
            // 3. 最后从 xmp 数据里取,原始格式 2023-01-14T10:11:07+08:00
            photoTime = getCreateDateFromXmp(doc)+"-X";
        }
        */
    }

    // alert(photoTime);


    // 调整文字格式 2023.09.23
    var parts = photoTime.split("-"); // 将日期字符串拆分成年、月和日
    var modifiedDate = "'" + parts[0].substring(2) + " " + parts[1] + " " + parts[2]; // 重新构建日期字符串
    return modifiedDate;

}

function ShowTheObject(obj){
  var des = "";
    for(var name in obj){
        // des += name + ";";
        des += name + ":" + obj[name] + ";";
    }
  return des;
}

//获取文件名
function basename(str) {
    str = str.toString();
    var idx = str.toString().lastIndexOf('/')
    idx = idx > -1 ? idx : str.lastIndexOf('\\')
    if (idx < 0) {
        return str
    }
    return str.substring(idx + 1);
}

//获取字体
function gimmePostScriptFontName(f)
{
  numOfFonts = app.fonts.length;
  // var s = "";
  for (var i = 0, numOfFonts; i < numOfFonts; i++)
  {
    fnt = app.fonts[i].name;
    // s += app.fonts[i].name + "***" + app.fonts[i].postScriptName + ";\n";

    if (f == fnt)
    {
      return app.fonts[i].postScriptName;
    }
  }
}

在脚本中调用了Action app.doAction("DateWatermark","Scripts");,在Action set Scripts下有action “DateWatermark”, 因此如要正常运行脚本,需要设置提前action,后期对水印样式修改也可以通过action修改进行。若要完全由脚本控制处理过程,则需要其他工具。

拓展:

Xtools,用于将action文件转为js脚本

Photoshop Scripting Listener,用于将操作过程记录为scripts log文件

Photoshop流程

  1. 字体:LcdD
  2. 图层效果:outer glow + drop shadow
  3. 混合模式:normal
  4. 透明度:74%

效果:

new_20160801-DSC_4086
new_20220504-_DSC2067
new_20220918-_DSC2520
new_20200517-_DSC0340

2022.5.14

玻璃高楼的社区边上,搭起简易的舞台和LED屏,主持人操着东北话讲黄段子,杂技、二人转,在DJ舞曲的背景音中热舞和挑逗,互动,“音乐走起!”,“掌声响起来!”,稀疏的观众,老人和大笑的小孩,主持人说,耽误了一会,节目会持续到凌晨三点。(我胆战心惊观看,害怕窜出个人大喊扰民。)中途时候,孝子上台,摆出遗像,下跪,音乐哀婉,主持人严肃,发抖,流涕,带着哭腔,边说边唱亲人分离的悲凉和痛苦。一阵一阵,回荡至远处的高楼。主持人说,避免扰民,今天的晚会到此结束。难忘今宵的背景音想起,路人往回走,谈论声多起来。

这是喜丧,我在贾樟柯的电影里看到过,在乡下看到过,今日,我在南京城市里第一次看到。

我不能思考所看所听的事物。我只能感受到,每个人都参与了一种现象,它是真实的,也是荒诞的。

一次数据处理心得

处理1.5G的CSV文件,有370万行和16列,做均值、透视、聚合、匹配查找。

我需要统计每个关键词不同时期的排名,然后得出最高排名、最低排名、平均排名。根据关键词、排名、ASIN进行模糊和精确匹配筛选。使用这种方式可以发现数据之间的关联性,比如有四组元素((1,A),(1,D)), ((2,B),(2,A),(2,C)),((3,C),(3,D)),((4,E),(4,F))。有两组元素中都有A,可以发现A与1和2有关联,A,B,C在同一组中,A,B,C存在关联性,由于各自与1,2,3关联,因此1,2,3又相互关联,与4不关联。通过选择匹配方式,可以把1,2,3组单独筛选出来。

image-20220116162026377

一开始的时候想都没想就用字典实现了,也比较容易,匹配速度非常快。但键值对层级多了之后容易搞混。之所以用字典,是因为原来的数据是近似随机分布的,对原数据进行遍历后生成格式化的字典,以便在匹配的时候按需索引。

同时panda也有尝试,但是pandas的处理速度耗时惊人,后来发现是我的思路不正确。最初思路是把表格数据二次展开,但是这样原本300多万行就变成了1000多万,一是消耗数据展开的时间,二是展开会出现重复数据,增加内存消耗,三是匹配数大幅增加,匹配关键词量级从X变成3X。

后来发现字典有一个很大的问题,就是内存占用,一个大型字典是完全载入内存的,原数据有1.5G,载入内存后占用飙升了好几个G。处理一套更复杂的数据时,发现内存占用100%,程序就再也运行不下了。

如何解决内存问题?

一个思路是不要把数据完全载入内存,这样就涉及到内存交换,只在需要时载入部分数据。可以用文件或者数据库实现。但是配置数据库实在太麻烦了,如果是sqlite还好说。此外IO型的数据更新会非常慢,尤其是这几百万离散数据更新。我尝试过sqlitedict,数据处理速度影响可能是光速换步行。

在这中间还有一个小插曲,我想试试协程读文件会不会提高读取速度,比如同时读几个文件,疯狂切换,用asyncioaiofiles实现并测试,发现速度大幅下降。还是老老实实流式读文件最好。异步不要乱用,异步是提高资源利用率,而不是添乱。

回到另一个思路,压缩字典。根据业务需求选择不同的数据载入方式。比如有的数据在全程都不会用到,那么在载入的时候就应该排除掉。另外就是尽可能避免数据冗余。能够折叠的就尽量折叠,比如分层,在一些情况下还可以提高索引效率。但是对于需要完整数据的时候,还是没有办法了。

最后只能寄托于pandas了,之前说过pandas运算速度太慢,但pandas可以解决内存占用的问题,至少不会因为内存爆满而无法继续运行。用时间换空间是可以理解的。

时隔几天又重新写了一个同样功能的程序,只不过是pandas方法实现的,测试后发现耗时从原来的的50秒变成了5分钟。显然我是不甘心的。我需要即时生成数据,5分钟实在没有耐心,耗时主要花在了匹配上,二次展开后的源数据有1000多万行,与字典的一百万条左右相比,匹配时间真是无法比。分析数据后,发现是我的思路不对,源数据没必要二次展开,因为同时损失了空间和运算时间。

read_csv时,明确dtype=strusecols=range(x)engine='c'进一步提升数据加载速度。

在关键词匹配的时候,先用groupby将关键词分组,进一步提升匹配速度。

同理用groupby分组运算最大、最小,transform生成列,然后匹配。

把匹配结果放入字典,再做二次运算,与原来的字典相比,体积大幅降低。

测试运行时间字典方式与pandas方式不相上下。

最后我将纯python和pandas方式统一了,写了一个新的匹配引擎。

让人惊讶的是新引擎的python方式比原来的还要快,虽然方法都差不多,还加了引擎判断。

提高性能的一些tips

判断尽量在循环外。

及时中断,避免重复循环。

可以避免的运算,请放在判断中或循环外。

复杂运算在筛选后进行。

提高pandas性能的一些tips

能用itertuples的时候就不要用iterrows()

合理使用groupby整理数据,然后使用aggtransform运算列。

合理使用apply运算数据。

避免数据冗余。

读取文件时选择性载入数据,明确dtype

这次的程序让我同时加深了:unittestpandasasyncio,以及工厂函数的构建思路。

稍微有点理解了接口统一性。因为结构不变,但是支持两种引擎操作。

Python KMP算法

"""
思路:
1、建立next_list,next_list[i]表示当pattern[i]不匹配时,跳转的pattern最大公共区域的下一级值。
"""

s = 'abdabdabababaaababac'
p = 'ababaaababac'
lens, lenp = len(s), len(p)


def build_next():
    next_list = [0] * lenp
    for i in range(1, lenp):
        if p[i] == p[next_list[i - 1]]:
            next_list[i] = next_list[i - 1] + 1
    return next_list


def kmp_match():
    i = 0
    j = 0
    next_list = build_next()
    while i < len(s):
        if lens - i >= lenp - j:
            if s[i] == p[j]:
                i += 1
                j += 1
            elif j == 0:
                i += 1
            else:
                j = next_list[j - 1]
        else:
            return -1
    return lens - j


print('next_list: ', build_next())
print('match_at: ', kmp_match())

Python 异步下载

def async_down_image(urls_dict, save_dirpath):
    async def down_image(client, url, save_path):
        root, name = os.path.split(save_path)
        if os.path.exists(save_path):
            return True
        try_count = 0
        while try_count < 3:
            try:
                async with client.get(url) as resp:
                    if resp.status == 200:
                        content = await resp.read()
                        print(f"Downloading {name}")
                        with open(save_path, 'wb') as fp:
                            fp.write(content)
                        return True
                    else:
                        try_count += 1
                        await asyncio.sleep(try_count)
            except:
                try_count += 1
                await asyncio.sleep(try_count)
        return False

    async def main(urls):
        async with aiohttp.ClientSession() as client:
            tasks = []
            for filename, url in urls.items():
                save_path = os.path.join(save_dirpath, filename)
                tasks.append(
                    asyncio.create_task(down_image(client, url, save_path=save_path))
                )
            await asyncio.wait(tasks)

    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main(urls_dict))

12.08

好似自我和解的内核,世界是无意义的,所有的东西都是无意义的,是尼采的虚无主义,是存在主义的虚无。

但是他们又说意义由自己创造,他们说有积极的虚无主义,可以自主选择以什么样的姿态面对虚无。

我对“积极”充满怀疑,因为虚伪的积极无意识地暗藏了期待,期待带来的幻灭只会加剧悲观。探索意义在我看来是徒劳的,在虚无的前提下,对结果的追寻也只有虚无,注定是失望。在期待与幻灭的矛盾中无限循环,是使人绝望和焦虑的根源。

“答案是一种决定,而不是一种说辞…我们必须决定是放弃,还是继续前进。”

对我来说,在虚无中带领我前进的是一种“勇气”的姿态,视角,决定,或者什么其他东西。这是今年里我最大的认知收获,是我看了N多动画片体会到的。

勇气不需要意义,它是无中生有的精神来源,虚无带来无限的自由,勇气只是一种自由的表现,是对虚无的接受和行动。

Document 7_5
Document 7_3
Document 7_4
Document 7_6
Document 7_2
Screenshot_20211208-230954
Screenshot_20211208-231227

Power Query 扩充行数的方法

参考:

Insert X Rows to Expand Table – Excel University (excel-university.com)

一张出运明细表中含有装箱率、件数、箱数,现在需要用Bartender制作外箱贴,外箱贴是有序号的,需要正好符合总外箱数。

在安排编号的时候并不会手动设定每一张外箱贴编号,我需要根据外箱数及不同SKU,自动生成外箱编号。举例:A有10箱,B有8箱。

SKU箱数装箱率
A102
B82

需要自动转化出另一张表:

编号SKU装箱率
1A2
2A2
3A2
4A2
5A2
6A2
7A2
8A2
9A2
10A2
11B2
12B2
13B2
14B2
15B2
16B2
17B2
18B2

我有预感Power Query能实现扩充行数。

实现方式为添加List列。

  = {1..[箱数]}

然后 Expand to new rows

image-20211202230714581

然后添加索引列即可。

具体代码:

let
    Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"SKU", type text}, {"EAN", type text}, {"UNIT", Int64.Type}, {"CTNS", Int64.Type}, {"PCS", Int64.Type}, {"PCStoPrint", Int64.Type}, {"CTNStoPrint", Int64.Type}, {"CTN_NUM_F", type any}, {"CTN_NUM_T", type any}}),
    #"Added Custom" = Table.AddColumn(#"Changed Type", "CTINS_LIST", each {1..[CTNS]}),
    #"Expanded CTINS_LIST" = Table.ExpandListColumn(#"Added Custom", "CTINS_LIST"),
    #"Added Index" = Table.AddIndexColumn(#"Expanded CTINS_LIST", "CTIN_ID", 1, 1, Int64.Type),
    #"Removed Columns" = Table.RemoveColumns(#"Added Index",{"CTINS_LIST"}),
    #"Reordered Columns" = Table.ReorderColumns(#"Removed Columns",{"CTIN_ID", "SKU", "EAN", "UNIT", "CTNS", "PCS", "PCStoPrint", "CTNStoPrint", "CTN_NUM_F", "CTN_NUM_T", "RANGE"})
in
    #"Reordered Columns"

Excel VBA代码找回

开启了宏命令的Excel一不小心就会出现莫名奇妙的问题,轻则VBA代码丢失,重则工作薄损坏,无法打开或保存。确实很恶心。

在中文语言的Excel中新建了宏,在英文语言的Excel中很有可能就打不开了,甚至中文Excel保存后再次打开也会提示文件损坏,辛辛苦苦写的VBA代码就那样没了。

修复是不可能真正修复的,顶多就删除vbaProject.bin这样子,那么写的VBA代码就直接放弃了吗?显然不应该。

xlsx/xlsb实际上是一个压缩包文件,将其后缀改为zip解压,获得大致如下的目录结构。vbaProject.bin就存储了VBA工程文件,包括VBA代码。vbaProject.bin实际上也是一个压缩文件,还可以再解压,但是不能使用常规方式提取出代码。

这篇文章说明了office三件套的文件格式实际是ole文件(oledump.py | Didier Stevens

DESKTOP-K65CV8V :: Users/Desktop/book % tree -L 3
.
├── [Content_Types].xml
├── _rels
├── customXml
│   ├── _rels
│   │   └── item1.xml.rels
│   ├── item1.xml
│   └── itemProps1.xml
├── docProps
│   ├── app.xml
│   └── core.xml
└── xl
    ├── _rels
    │   └── workbook.bin.rels
    ├── calcChain.bin
    ├── connections.bin
    ├── metadata.bin
    ├── printerSettings
    │   └── printerSettings1.bin
    ├── queryTables
    │   └── queryTable1.bin
    ├── sharedStrings.bin
    ├── styles.bin
    ├── tables
    │   ├── _rels
    │   ├── table1.bin
    │   ├── table2.bin
    │   └── table3.bin
    ├── theme
    │   └── theme1.xml
    ├── vbaProject.bin
    ├── workbook.bin
    └── worksheets
        ├── _rels
        ├── binaryIndex1.bin
        ├── binaryIndex2.bin
        ├── binaryIndex3.bin
        ├── sheet1.bin
        ├── sheet2.bin
        └── sheet3.bin

借助该文提供的oledump.py(oledump.py | Didier Stevens)可对ole文件进行分析。

以下简单介绍利用oledump.py提取VBA代码的用法。

1、查看vbaProject.bin结构

image-20211118222429078

确认模块位置-以Module或模块开头的编号。

查看模块代码内容:

image-20211118222621902

Python about the metaclass

花了好多天去理解unittest.main()和Base.metadata.create_all()的实现方式,如何在不实例化的情况下知道子类的信息?

以下为实验代码,详细参考《Python Cookbook(第3版)中文版》

class Metaclass(type):
    childs = []
    def __init__(self, clsname, bases, clsdict):
        super().__init__(clsname, bases, clsdict)
        self.childs.append(clsname)

class Base(metaclass=Metaclass):
    pass


class Tree1(Base):
    pass

class Tree2(Base):
    pass

if __name__ == '__main__':
    for c in Base.childs:
        print(c)

输出结果:

Base
Tree1
Tree2

如何用Excel实现多重条件匹配与分段计价

有一个需求,需要对不同规格的包裹进行定价,在Excel里输入长宽高和重量,即可自动计算出定价信息。

但是包裹的尺寸和重量同时决定了定价。定价是分段计算的,包裹将根据尺寸和重量范围分为不同的包裹类型,比如有信件包裹、标准包裹、大尺寸包裹等。在每一个包裹类型下,对于不同的包裹重量,又有不同的定价标准。

Continue reading “如何用Excel实现多重条件匹配与分段计价”

Kindle进阶使用思路

有网友私信我Kindle使用的问题,不过小新对公众号疏于打理,几天后才看到消息,因此没有回复的机会了。与Kindle使用相关的经验其实老想分享,但是我懒,就一直搁置了起来,今天借着网友的提醒,决定弥补一下,可能会遗漏一些重要的内容,小新将会在以后补充,如有问题直接在本文留言。

Continue reading “Kindle进阶使用思路”

技术与人文

最近三天都宅在家里,睡觉、写了一些代码,博客网站修补了一下,更简洁了一些。

今天去看了看别人的博客。到了2021年,独立博客仍然存在,但大都是计算机专业学生、前端工程师的玩具,网站尽管花哨,却很少有意思的内容。技术与人文往往存在这样的矛盾:有内容,却缺少恰当的形式和载体;载体被打造得极为功能强劲,却几乎没有内容。

独立博客在今天,技术的展示比内容的展示更常见,这是没有问题的,其意义取决于我们如何看待这个对象,我们的目的。

Continue reading “技术与人文”

社交牛逼症

“社交牛逼症”这个词不知怎么就火了,到处都能听到、看到。查了一下,百度热词和微信指数都显示该词在8.25日热度从0陡然上升,但其实这个词被传播的时间还要更早一点。在B站搜索,最广泛传播的是8月1日的“社交牛逼症”视频,主角是“马牛逼”,视频主角与抖音账号“明星双喜哥”是同一人(即马牛逼),此后“社交牛逼症”相关的视频多数都与“马牛逼”有关。

Continue reading “社交牛逼症”

闲读记录

为什么卡夫卡的小说读起来老是像做梦 9.11

年纪大了,对恋爱电影无感。看了一下《花束般的恋爱》,没有撑到30分钟,就跳到了结局。9.12

我好焦虑。我希望对所有的一切都宽容一点。

\

在二楼南书房看完了《上海百年建筑史》,很不错,一口气看完。

现在南书房重新开放了,也不收费了。9.12

\9.13

世界没有意义,意义是自己赋予的。

8.25 我的三个人格

结合目前的沟通和表现方式我总结了自己的三个人格,工作上是看淡一切、说话娘炮温和的小白工具人人格,互联网上是不知道什么人格,在家里是社恐二次元宅男人格。要维持各自人格是很不容易的,边界是气球的薄膜,只要在家里收到一条钉钉消息就能刺破一切,使自我短暂崩溃。我可以认为第三种人格接近自我,第二种人格代表着自我的虚构,第一种人格是为了在“外部”社会生存而做出的小小牺牲。

All in all,我的自我斗争意识越来越薄弱了,这是一种对“内在冲突”的规避,因为我讨厌矛盾,以及由此引发的焦虑。我在寻求一种“接受一切又否定一切”的状态,有点像佛教的“是是非非”和“否定之否定”。对我来说,我不会再因为抗争和突破而退缩,但也完全接受了全身而退的心理准备。 我迟早要出家。

刚刚我意识到,微博和豆瓣一类的平台正在被我当作日记本使用,但显然这是一种公开的日记本,与私密性是矛盾的,人类在公开的场合很难主动展现不完美,在这个场合下,无论如何展现自我的真实,说是真实,莫如是说自我对真实的想象和理解,也有限制。

所以公开地展现、确认自我是没有意义的,做得越多越是一种限制,与真实也越纠缠不清,我们能做得只有尽力以观察者的角度评论和记录。