MoreRSS

site iconNekonull | 卢之睿

学生、程序员、设计师,腾讯和商汤实习。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Nekonull | 卢之睿的 RSS 预览

curl 有 LTS 了

2024-11-17 21:27:04

curl 有 5 年 LTS (Long Time Support) 版本了,不过只有签了商业协议的用户才能使用

https://rock-solid.curl.dev/

via: https://daniel.haxx.se/blog/2024/11/07/rock-solid-curl/

Excel 的自动填充实际上是程序生成

2024-11-17 13:36:00

原来 Excel 里自动填充是有论文的,底层是实现了程序生成;另外据说 Google Sheets 的自动填充之所以没有微软的好,是因为微软申请了专利,Google 绕不过去。

Automating String Processing in Spreadsheets Using Input-Output Examples https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/popl11-synthesis.pdf

via https://danluu.com/ballmer/

不要在 gcc 7 里隐式捕获 constexpr 数组

2024-11-02 15:38:00

如果你还没有遇到过 compiler bug,说明你写的代码还不够多。 —— 一位本科同学

虽然经常能看到其他人遇到 compiler 导致的意外现象,自己遇到却还是第一次,因此还是写篇文章小小记录下。

因为一些原因,目前工作中使用的 gcc 版本是 7.4。接到一个业务需求,需要对某些特定的用户打开一个 feature flag,且需要在代码里硬编码。代码很快就写完了,自测的时候却发现不太对劲。相关代码简化脱敏后如下所示。逻辑很简单,就是把可以进入灰度的用户 id 放到了一个 allowlist 里,然后检查当前的请求涉及到的所有用户 id 是否都在这个 allowlist 中。奇怪之处在于,在 gcc 7.4 下,如果用了 std::any_of 来找,即便用户不在灰度列表中,列表成员检查也会返回 true,导致意外的灰度进入;简单的 for 循环则没有这个问题。

#include <cstdint>
#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    // we want to check if all users is in allowlist (something like a feature flag)
    constexpr uint64_t allowlist[] = {1,2,3};
    std::vector<uint64_t> users;
    users.push_back(4);

    bool basic_found = std::find(std::begin(allowlist), std::end(allowlist), 4) != std::end(allowlist);
    std::cout << "outside: basic_found=" << basic_found << std::endl;
    // should be false, as expected

    bool some_found_anyof = std::any_of(users.begin(), users.end(), [&](const uint64_t uid) {
    bool found = std::find(std::begin(allowlist), std::end(allowlist), uid) != std::end(allowlist);
     std::cout << "inside: uid=" << uid <<  ", found=" << found << std::endl;
     return found;
    });
    std::cout << "some_found_anyof=" << some_found_anyof << std::endl;
    // should be false; however in gcc 7.4 this outputs true

    bool some_found_manual = false;
    for (const auto& u: users) {
        if (std::find(std::begin(allowlist), std::end(allowlist), u) != std::end(allowlist)) {
            some_found_manual = true;
            break;
        }
    }
    std::cout << "some_found_manual=" << some_found_manual << std::endl;
    // should be false, as expected
}

因为需求要的比较着急,就先用 for 循环替换了。后面有空的时候尝试继续简化代码,可以得到如下的最小复现 POC:定义一个 constexpr 数组,创建一个 lambda 表达式,用[&]隐式捕获这个数组,最后在闭包中多次获取其地址;会惊喜的发现,每次获取的地址是不一样的!(可以在 Compiler Explorer 自己试试:https://godbolt.org/z/qdTfP81xn

#include <iostream>

int main()
{
    constexpr int arr[] = {1, 2, 3};
    std::cout << "outside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;
    std::cout << "outside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;
    std::cout << "outside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;

    auto a = [&]() {
        std::cout << "inside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;
        std::cout << "inside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;
        std::cout << "inside: addr=" << reinterpret_cast<const void*>(arr) << std::endl;
    };

    a();
}

输出如下:

outside: addr=0x7ffc4b13dc24
outside: addr=0x7ffc4b13dc24
outside: addr=0x7ffc4b13dc24
inside: addr=0x7ffc4b13dbec
inside: addr=0x7ffc4b13dbf8
inside: addr=0x7ffc4b13dc04

从生成的汇编中可以发现,在闭包里每次访问 arr 时候,其实都在栈上临时创建了一个副本,因此每次获取到的 arr 地址都是不一致的。

        mov     DWORD PTR [rbp-36], 1
        mov     DWORD PTR [rbp-32], 2
        mov     DWORD PTR [rbp-28], 3
        lea     rax, [rbp-36]
	    ...ignored...
		mov     DWORD PTR [rbp-24], 1
        mov     DWORD PTR [rbp-20], 2
        mov     DWORD PTR [rbp-16], 3
        lea     rax, [rbp-24]

这也就能解释为什么之前在 std::any_of 闭包里用 std::find 会有问题了:其中作为参数的 std::begin(allowlist), std::end(allowlist) 和最后一个用于比较的 std::end(allowlist),三个 allowlist 的地址都不一样,因此这个 != 判定始终会成功,表现上就是会认为用户总是在灰度列表中了。

这是不是一个 compiler bug 呢?正如文章标题,我只是把这作为一个 unexpected behavior,因为编译器可以自行选择对 constexpr 的优化方式。我也尝试在 gcc bug report 数据库里进行了查找,但看起来没有人反馈过这个问题。

最后是可能的绕过方式:

  1. 升级 gcc 版本(gcc 8 及以上版本无此问题,7.5 及以下版本才有此问题)
  2. 换用其他 compiler:clang, msvc 都无此问题
  3. 不要用 constexpr,改用一般的 const
  4. [&arr] 显式捕获 constexpr 数组

祝读到这里的各位以后都能少踩这类奇怪的坑吧。

工具小站

2024-10-07 14:03:00

参考 tools.simonwillison.net,决定把所有单页纯前端应用全部放到到一个 repo 里,并且分配一个子域名;这样不用每个小工具都申请独立 repo + 配置 Github Actions 了。

虽然目前还是没什么东西,但是还是欢迎来玩:pages.nekonull.me

基于 WebRecorder 和 MitmProxy 的图片手动抓取探索

2024-10-07 13:58:00

最近收到任务,需要从某个站点下载一系列图片。首先当然是 F12 看下网络请求。这是一个无限滚动的瀑布流,图片地址本身是随机的(看起来文件名像是 uuid),所以没法直接遍历图片地址;此外每次滚动到底,都会触发一个获取下一页图片地址的请求,参数传的还是游标而不是页数,这又断绝了直接构造分页请求(例如 page=1)获取所有图片地址,再逐个拉取的念头。好在总图片数是有限的,大概在 2k 左右,每次下拉能拉回 50 条,所以手动抓取也不是不能接受。以下是两种手动抓取的思路。注:这里的手动抓取,指的是在浏览器前端通过模拟人类行为,无侵入且不对网站发起额外请求的抓取方式。

前置准备:AHK 翻页器

一个简单的小工具,每隔一段时间自动按 page down 翻页(建议提前 zoom out 调小页面比例,这样滚动起来效率更高)

AHK 翻页器代码
#Requires AutoHotkey v2.0

; 创建一个 GUI
MyGui := Gui()
MyGui.Add("Text", "x10 y10", "Auto Page Down:")
toggle := MyGui.Add("Checkbox", "x10 y30 vToggleState", "Enable")
MyGui.OnEvent("Close", (*) => ExitApp())
MyGui.Show()

; 设置全局变量以跟踪定时器状态
global keyHeld := false

; 定时器:每0.1秒检查一次开关状态
SetTimer(CheckToggleState, 100)

; 主循环
CheckToggleState()
{
    global keyHeld  ; 声明 keyHeld 为全局变量

    if (toggle.Value) {
        if (!keyHeld) {
            ; 如果勾选了启用选项,并且哔哩哔哩窗口处于活动状态
            RandomDelayAndSendPageDown()
            keyHeld := true
        }
    } else {
        if (keyHeld) {
            ; 如果没有勾选启用选项,停止按键操作
            keyHeld := false
        }
    }
}

; 生成随机延迟并发送 Page Down 键
RandomDelayAndSendPageDown()
{
    ; 生成 500 到 1000 毫秒之间的随机延迟
    RandomDelay := Random(500, 1000)
    SetTimer(PressPageDown, RandomDelay)
}

; 按下 Page Down 键
PressPageDown()
{
    global keyHeld  ; 声明 keyHeld 为全局变量
    
    if (toggle.Value) {
        Send("{PgDn}")
        RandomDelayAndSendPageDown()  ; 继续下一次按键
    } else {
        keyHeld := false
    }
}

思路1:WebRecorder

概述:用 WebRecorder 插件,录制网络请求(WebRecorder 插件会 hook 浏览器的 XMLHttpRequest 机制),dump 出 warc (Web Archive)文件,然后再解析文件,从中提取 mime 类型为图片的文件(或者知道 URL 格式的话也可以用 URL 格式匹配)。

限制:导出的时候存在文件大小限制(Chrome 下似乎是 2G,也取决于可用内存大小),总文件大小太大的话可能会有问题。

步骤:

  1. 安装 Chrome 插件 Webrecorder ArchiveWeb.page
  2. 浏览器插件栏找到 Webrecorder,点击 Start Archiving
  3. 跳转到需要抓取的网页
  4. 打开翻页器,一直往下滚动
  5. 完成后打开插件,点击 Stop Archiving
  6. 点击插件界面内的 Home 按钮,找到刚才的 Archiving Session,点击 Download,下载一个 wacz 文件到本地
  7. 修改扩展名为 zip,将其中的 archive/data.warc.gz 文件解压出来,得到 data.warc 文件
  8. 运行如下 Python 脚本,解析 warc 文件,并从中提取图片内容
Python 解析 WARC 代码
from warcio.archiveiterator import ArchiveIterator
import os
import re

# 定义一个函数过滤掉不支持的字符
def sanitize_filename(filename):
    # 定义不允许的字符
    invalid_chars = r'[\\/:*?"<>|]'
    # 替换这些字符为空字符,或者可以替换为下划线 "_"
    sanitized_filename = re.sub(invalid_chars, '_', filename)
    return sanitized_filename

# 提取 WARC 文件中的图片、视频资源
def extract_resources_from_warc(warc_file, output_dir):
    with open(warc_file, 'rb') as stream:
        for record in ArchiveIterator(stream):
            if record.http_headers:
                content_type = record.http_headers.get_header('Content-Type')
                if content_type and 'image' in content_type:
                    # 获取资源文件的 URL
                    url = record.rec_headers.get_header('WARC-Target-URI')
                    print(url)

                    # 获取资源文件内容
                    content = record.content_stream().read()
                    
                    # 确定文件扩展名
                    ext = 'jpg' if 'image' in content_type else 'mp4' if 'video' in content_type else ''
                    if ext:
                        # 生成文件名
                        sanitized_url = sanitize_filename(os.path.basename(url))
                        filename = os.path.join(output_dir, sanitized_url) + '.' + ext
                        print("filename", filename)
                        # 保存文件
                        with open(filename, 'wb') as f:
                            f.write(content)
                        print(f"Extracted: {filename}")

# 示例用法
warc_file = 'data.warc'
output_dir = 'extracted_resources'
os.makedirs(output_dir, exist_ok=True)
extract_resources_from_warc(warc_file, output_dir)

思路2:MitmProxy

概述:安装 mitmproxy,设置浏览器代理指向 proxy,浏览时从响应中直接过滤出图片内容并保存到本地

限制:似乎没什么限制

步骤:

  1. 安装 mitmproxy
  2. 保存以下 Python 脚本为 save.py
mitmproxy save.py
import os
import re
from mitmproxy import ctx, http

# 定义文件保存目录
save_dir = "saved_media"

# 确保目录存在
if not os.path.exists(save_dir):
    os.makedirs(save_dir)

# 定义一个正则表达式,匹配Windows非法的文件名字符
def sanitize_filename(filename: str) -> str:
    # 替换非法字符为下划线
    return re.sub(r'[<>:"/\\|?*]', '_', filename)

def response(flow: http.HTTPFlow):
    # 获取响应的Content-Type
    content_type = flow.response.headers.get("Content-Type", "")

    # 判断是否为图片或视频类型
    if "image" in content_type:
        # 获取文件扩展名
        ext = content_type.split("/")[-1]

        # 从请求路径中尝试提取文件名,如果路径中没有文件名,则使用flow.id
        url_path = flow.request.path.split("/")[-1]
        if "." in url_path:  # 判断路径中是否包含文件扩展名
            raw_filename = url_path
        else:
            raw_filename = f"{flow.request.host}_{flow.id}.{ext}"

        # 对文件名进行清理,确保符合Windows的文件名要求
        safe_filename = sanitize_filename(raw_filename)

        # 确定完整的保存路径
        filepath = os.path.join(save_dir, safe_filename)

        # 记录日志,打印保存的文件名和URL
        ctx.log.info(f"Saving file: {filepath} from URL: {flow.request.url}")

        # 保存文件内容
        with open(filepath, "wb") as f:
            f.write(flow.response.content)

        # 打印成功保存的日志
        ctx.log.info(f"Saved {filepath}")
  1. 运行 mitmproxy
# 需要先 cd 到 save.py 所在的目录
# 可以添加 --mode upstream:http://localhost:7890 来指定上游代理
mitmweb -s save.py
  1. 设置浏览器,指向 mitmproxy 代理(可能你需要 proxy switchyomega
  2. 跳转到需要抓取的网页,打开翻页器
  3. 滚动到底部,且所有图片加载完成后,需要抓取的图片应该都已经保存到本地了