MoreRSS

site iconShadow Walker | 松烟阁

Where other men are limited by morality or law, remember, everything is permitted. I walk in the darkness to serve the light.
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Shadow Walker | 松烟阁的 RSS 预览

用好 AI:Claude 帮我完成 Memos SQLite 到 MySQL 的数据库迁移

2024-11-17 00:16:46

近期阿里云双十一活动 RDS 99/年,非常有性价比,于是我就买了一个香港区的 MySQL RDS 用做我的 Memos 数据服务。这就不可避免的碰到了数据库迁移的问题,即需要将 SQLite 数据库迁移到 MySQL,这种生产环境的数据库迁移一般都非常谨慎,需要严格 SQL 开发和验证,需要花费不少的时间。由于 SQLite 文件有备份,刚刚购买的 MySQL 资源充足,心血来潮之下想玩一波大的 —— 试试 AI 来帮我完成生产数据库的迁移工作。

我完成这些事情是 prompt tune 的思路,主要包括:

  1. 找到当前版本 memos 的库表创建的 sql 文件;
  2. 创建测试用的本地 mysql 数据库服务;
  3. prompt 进行提示词问询,生成 python 迁移工具;
  4. 执行迁移测试,收集脚本报错以及数据库表问题;
  5. 根据测试发现的问题,进行 prompt tuning,生成新的 python 迁移工具;
  6. 根据第1步的 sql 文件完成库表创建,然后执行 python 迁移工具完成生产数据库的迁移;

前期准备

查找库表创建 SQL 文件

这里主要是确认当前生产数据创建了哪些库表,有两种方法:

  1. 去 SQLite 数据库文件中通过 SQL 命令查询:show create table ${table name}
  2. 去 memos sql 文件中查找,edonyzpc/memos/store/migration/sqlite/prod/LATEST.sql(如果非自定义镜像可以去 usememos 查找

本地 mysql 测试环境

1. 准备 init.sql 文件

CREATE DATABASE IF NOT EXISTS memos-migrate-test;

2. 准备 docker compose 文件

version: '3'
services:
  db:
    image: mysql:latest
    ports:
      - 3306:3306
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password
    volumes:
      - ./data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

3. 启动本地测试 mysql

docker pull mysql

docker compose up -d

prompt 初始化

I have a notes service and the data is stored on sqlite database, now i want to migrate all the data to mysql, please help me to write a script in python. Here is the schema definition of the sqlite database
<<<LATEST.sql>>>

注意:替换 LATEST.sql 的内容。

Prompt Tuning

当前具备 code 分析能力的 LLM 均可以用来做迁移工具的 prompt tune,由于 Claude 在代码分析和生成等领域有非常不错的口碑,所以我这次使用了 Claude 3.5 Sonnet 作为调优模型。

1. 初始化 prompt

Here's a Python script that can help you migrate data from SQLite to MySQL based on the schema you provided:

\`\`\`python
import sqlite3
import mysql.connector
import json

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

Before running this script, make sure to:

1. Install required packages: `pip install mysql-connector-python`
2. Replace `'path_to_your_sqlite_database.db'` with the actual path to your SQLite database file.
3. Update the MySQL connection details (host, user, password, database) with your actual MySQL server information.

This script does the following:

1. Connects to both SQLite and MySQL databases.
2. Iterates through each table in the given list.
3. Fetches all data from the SQLite table.
4. Prepares an insert query for MySQL.
5. Inserts the data into the corresponding MySQL table, handling JSON fields if necessary.
6. Commits the changes and moves to the next table.

Note that this script assumes that you have already created the tables in MySQL with the same schema as in SQLite. If you haven't, you'll need to create the tables in MySQL first using the CREATE TABLE statements you provided.

Also, be aware that this script does not handle auto-increment fields specially. If you have auto-increment fields and want to preserve the exact IDs from SQLite, you may need to modify the script to handle this (e.g., by temporarily disabling auto-increment in MySQL during the migration).

2. prompt tuning

prompt tuning 就是在测试环境中执行 Claude 生成的工具,并将测试的报错内容反馈给 Claude 让它继续进行优化调整,并重新生成迁移工具。

库表没有创建

测试初始化 prompt 步骤中的迁移工具,发现有如下报错:

  • prompt tuning: 这一步不需要 prompt 优化,只需要在测试的 MySQL 中执行建表语句,完成库表的创建(LATEST.sql

时间戳表列类型不对

进行测试发现有时间戳相关报错:

  • prompt tuningmigrate error: Incorrect datetime value: '1698320364' for column 'created_ts'
  • prompt result
I apologize for the oversight. The error you're encountering is due to a difference in how SQLite and MySQL handle datetime values. SQLite often stores timestamps as Unix timestamps (seconds since epoch), while MySQL expects a formatted datetime string.

To resolve this issue, we need to convert the Unix timestamp to a datetime format that MySQL can understand. Here's an updated version of the script that handles this conversion:

\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

The main changes in this updated script are:

1. We've added a list of `timestamp_columns` that need to be converted.
2. We've added a `convert_timestamp` function that converts Unix timestamps to MySQL datetime format.
3. In the data processing loop, we now check if a column is in the `timestamp_columns` list and convert it if necessary.

This script should handle the timestamp conversion correctly. If you encounter any other issues or if some tables have different datetime formats, you may need to adjust the `convert_timestamp` function or add more specific handling for certain tables or columns.

Also, make sure that your MySQL tables are set up to accept NULL values for timestamp columns if applicable, as some of your SQLite data might contain NULL values for these fields.

关键字语法错误

再次进行测试发现有保留关键字导致语法相关的报错:

  • prompt tuningError inserting into user_setting: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'key, value) VALUES (1, 'USER_SETTING_ACCESS_TOKENS', '{\"accessTokens\":[{\"acce' at line 1
  • prompt result
I apologize for the oversight. The error is occurring because 'blob' and 'type' are also reserved keywords in MySQL. We need to escape these column names as well. Here's an updated version of the script that handles this issue:

\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

def escape_column_names(columns):
    return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Escape column names if necessary
    escaped_columns = escape_column_names(columns)
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(escaped_columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            print(f"Problematic row: {processed_row}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

The main changes in this updated script are:

1. We've added 'blob' and 'type' to the list of reserved keywords that need to be escaped.
2. The `escape_column_names` function now checks against this expanded list of reserved keywords.
3. I've added a line to print the problematic row when an error occurs, which can help with debugging if you encounter any other issues.

This change should resolve the SQL syntax error you encountered with the `resource` table. The script now properly escapes the 'blob' and 'type' column names, which should allow the insertion into the `resource` table to proceed without errors.

If you encounter any other issues, please let me know, and I'll be happy to help further. Also, remember that if your `blob` column contains large binary data, you might need to

3. 完成 prompt tuning

调优完成之后,就可以向正式的 MySQL 数据库实例进行数据迁移了,主要注意点的:

  • RDS 中创建好数据库实例之后,需要用 LATEST.sql 进行初始化库表
  • prompt tuning 步骤在测试环境迁移完成之后,最好用指定 driver 的方式测试一下 memos 服务是否正常,参考:Database Drivers - Memos
  • 还可以让 Claude 帮你增加建库表的过程,省去了自己初始化库表的事情了

附录

import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('~/.memos/memos_prod.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="xxxxxx.rds.aliyuncs.com",
    user="xxxxxxxxxxxxxxxxx",
    password="xxxxxxxxxxxxx",
    database="xxxxxxxxxxxxx"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

def escape_column_names(columns):
    return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]

for table in tables:
    print(f"Migrating table: {table}")

    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()

    if not rows:
        print(f"No data in table {table}")
        continue

    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]

    # Escape column names if necessary
    escaped_columns = escape_column_names(columns)

    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(escaped_columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"

    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)

        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            print(f"Problematic row: {processed_row}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")

References

  1. Local Storage - Memos

我的消费决策模型

2024-11-10 00:17:32

双十一了,好物清单、什么值得买之类的内容铺天盖地,到底我们在消费的时候受到哪些东西影响,我觉得需要研究一下消费决策模型。根据恩格尔(Engel)等提出的 EBK 模型可以看出消费时原来是这么样复杂,哪怕买瓶水、买个包子都无形中被这么多因素影响着。为了自己的钱包,我觉得很有必要建立属于自己的消费决策模型。

消费是如何产生的

根据 EKB 模型,消费决策是由五步构成的:

1. 认识问题

当人们认为理想与实际之间有差距时,便产生了问题,问题产生后则整个系统(内外)便开始运作,目标也化成了具体的行动。

例如:工作了半天感觉肚子饿了,继续饿着就没力气干活了,看来需要找点吃的了。

2. 收集信息

当人们发现了问题的存在,便会去搜寻相关的信息,信息搜寻又可分为内部搜寻与外部搜寻两种,所谓内部搜寻是指在其现有资料或过去的购买经验中去寻找。当内部搜寻无法满足其需要时,便会转由外部寻找,所谓的外部搜寻诸如大众传播媒体、行销人员及亲友等。

例如:

  • 上次朋友推荐说办公室楼下的面条,赶紧去尝尝解决一下饿肚子问题;
  • 办公的地点人生地不熟的,不知道去哪里找吃的,群里问问有没有人知道或者直接美团上搜搜看;

3. 评估方案

收集到信息之后就需要判断一下哪个更好,哪个跟适合。

例如:

  • 面馆人太多了,要排队等很久,包子铺人少而且味道不错;
  • 不行,最近工作太幸苦了,需要好好补补,奢侈一下买份豪华牛排套餐犒劳一下自己;
  • ......

4. 选择执行

当评估了各种可能的方案后,便会选择一个最能解决原始问题的方案并采取消费行为。

例如:

  • 买份豪华牛排套餐加上星巴克咖啡;
  • 买份性价比高的大排面;

5. 结果反馈

当消费并使用了某产品后,可能发生满意或购买认知失调两种结果,并存储于记忆当中。

例如:

  • 楼下的西餐店的牛排太难吃了,而且等餐化了很长时间,下次再也不去了;
  • 大排面量大、料足,味道出奇的好,还能加面,下次还去;

我的消费决策模型

知彼

商家为了刺激消费,常常会巧妙地运用心理学知识。这些心理技巧潜移默化地影响着我们的消费决策。下面就来详细介绍一些常见的商家常用的心理学知识:

1. 稀缺性原理:

  • 限时限量: 创建一种紧迫感,让人们觉得如果不马上购买就会错过。
  • 独家产品: 强调产品的独特性和稀缺性,让人们产生“拥有感”。
  • 库存告急: 通过展示库存数量,营造抢购氛围。

2. 社会认同感:

  • 明星代言: 利用名人效应,让人们相信产品值得信赖。
  • 用户评价: 展示其他用户的正面评价,增加产品的可信度。
  • 从众心理: 强调产品受欢迎程度,让人们跟风购买。

3. 锚定效应:

  • 原价标注: 先展示一个高价,再标出折扣价,让人觉得捡了便宜。
  • 对比效应: 将产品与更昂贵的同类产品进行比较,凸显其性价比。

4. 损失厌恶:

  • 免费试用: 提供免费试用,让人们产生使用习惯,一旦停止使用就会感到损失。
  • 退货保证: 提供无风险购买,降低购买门槛。

5. 互惠原则:

  • 赠品: 提供小礼品或赠品,让人们产生回报的心理。
  • 免费试用: 通过免费试用建立关系,为后续销售做铺垫。

6. 喜欢原则:

  • 相似性: 强调产品与消费者之间的相似性,增加好感度。
  • 赞美: 给予消费者赞美,让他们产生愉悦感。

7. 权威原则:

  • 专家背书: 引用专家意见,增加产品的权威性。
  • 认证标志: 使用各种认证标志,提高产品的可信度。

8. 承诺一致性:

  • 小额承诺: 先让消费者做出一个小小的承诺,再引导他们做出更大的承诺。
  • 公开承诺: 让消费者公开自己的决定,增加其坚持的可能性。

知己

理解消费决策的心理过程并据此做出明智的消费选择是至关重要的。下面提供了一个优化后、更具可操作性的消费决策分析和思考方法,整合了心理学原理和实践指导:

1. 内在需求与外部影响分析:

  • 识别核心需求:深入分析自己寻求解决的是哪一种内在需求,如基本需求、社交需求还是个人成长需求。需求是指那些必须满足以维持基本生活水平的必需品,而欲望则更多关联于提升生活质量的选择性购买。
  • 评估外部因素:反思购买决策是否受到外部因素如广告、同伴压力或社会趋势的影响。

2. 信息搜集与处理:

  • 全面信息搜集:通过多种渠道搜集关于产品或服务的全面信息,包括正反两面的评价和数据。
  • 理性评估:比对不同的选项,从功能性、性价比、可持续性等多个角度进行客观评估。

3. 情绪因素与理性平衡:

  • 情绪自我检查:认知到目前的情绪状态,并审视这些情绪是否可能影响消费决策。学会管理这些情绪,避免它们在决策过程中起重要作用。
  • 避免冲动消费:实施“冷却期”原则,给自己一段时间(例如24小时)来考虑是否真的需要购买。

4. 财务评估与计划:

  • 制定严格预算:确保消费决策符合个人或家庭的长期财务规划与预算。
  • 成本效益分析:从长期角度考虑购买的成本与其带来的效益和价值。

5. 反思、学习与调整:

  • 后置反馈循环:消费后,进行深度反思,比如满意度、是否满足了预期的需求等。
  • 持续信息更新与学习:定期回顾和更新自己对市场的理解以及消费决策的影响因素。

消费决策检查清单

最后分享一个我自己正在尝试使用的简单消费决策逻辑:

使用说明:

  • 在每次消费前,逐条认真思考并诚实回答。
  • 如果有一条或多条无法勾选,说明你可能需要重新考虑这个消费决定。
  • 对于大额消费,建议多次重复几次。
  • 建立一个消费日记,记录每次购买的物品、金额、原因以及是否满意。
  • 了解基本的财务规划和投资知识,帮助你做出更明智的消费决策。
  • 这份检查清单可以根据个人需求进行调整和扩展。 例如,如果你非常关注可持续发展,可以增加一条“这个产品是否环保?”;如果你对健康生活方式很在意,可以增加一条“这个产品是否健康?”
  • 不要被情绪冲昏头脑,多问自己是否真的需要。
  • 不要只看一家,多比较几家,选择最适合自己的。免费的东西往往伴随着隐藏的成本。

References

  1. 消费者购买决策理论 - MBA智库百科

关于付航,让我想起的一些句子

2024-11-03 22:27:45

真诚的人从不公开表达自己的情感,真正的情感总是蒙了一层面纱。只有虚伪的人才用情感作秀,为了给大家看他们有多敏感。

—— 安德烈·塔可夫斯基《时光中的时光》

我本性不善言辞,却忙于人际周旋;我本喜欢独处,却被生活推着奔波前行。

而所做的一切,都是为了得到那解万千惆怅的碎银几两。

原来,这个世界不允许我们内向。

大家只在意结果,没人关心你独撑的过程。

时常心累,却只能一笑而过。

有些伤痛,何可言、何能言、何处言;

有些经历,不可说、不能说、不必说。

沉默并非无话可说,只是难以启齿,一言难尽。

—— 贾平凹《自在独行》

人们总是把幸福解读为:“有”

有房,有车,有钱,有权,

但幸福其实是“无”,

无忧,无虑,无病,无灾,

“有”多半是给别人看的,“无”才是你自己的。

—— 稻盛和夫

你所有的压力,都是因为你太想要了,你所有的痛苦,都是因为你太较真了。有些事不能尽你心意,就在提醒你该转弯了。

如果事事都是如意,那就不叫生活了,所以睡前原谅一切,醒来不问过往,珍惜所有的不期而遇,看淡所有的不辞而别。

人生,一站有一站的风景,一岁有一岁的味道,你的年龄应该成为你生命的勋章而不是你伤感的理由。

生活嘛,慢慢来,你又不差。

—— 稻盛和夫《人生忠告》

人生至福,就是确信有人爱你,有人为你的现状而爱你,说得更准确些,有人不问你如何就爱你。

—— 雨果《悲惨世界》

耳朵是从不犯错误的,惹祸的都是嘴。

—— 冯骥才

关于付航,如果没看懂其实挺好的

2024-10-26 23:21:38

《喜剧之王单口季》总决赛上付航的表演,后劲有点大,忍不住想说点什么……

总决赛上杨天真说会写一个 repo 给付航,于是顺手搜索了一下付航相关的信息,当我设置了 site:douban.com 之后,我震惊地发现清一色的都是男女对立的点评,此类评论大多认为大家叫好、有共鸣是因为付航段子是男性视角的爽文 —— 贤妻扶我凌云志,我还贤妻万两金。因为我没有国内的社交媒体账号,豆瓣、微博上这样的「口诛笔伐」我不会经常看见也挺好的,只是希望付航会继续勇敢地做自己。

19年开始关注脱口秀(standup comedy),第一次看懂文本结构是杨波的 one-linar,第一次看懂脱口秀内核是付航的 passion。所有人都认为脱口秀是冒犯的艺术,所以花样百出的冒犯别人、冒犯世界,付航让我明白好笑的脱口秀不一定需要冒犯别人。

现今的付航早已不是微末,并不需要我的任何非专业解读和夸赞,他给我带来了真切的快乐就很好了。马东曾经在十三邀里面谈起自己的父亲,他是这样说的:被误会是表达者的命运,我继承了我父亲的性格,面对误会选择沉默

付航最后一段结尾的原文是:「十多年啦!所有人都说我是废物,但是只有你说我是天才。你教会了我人生之中最重要的一件事:不管你的出生、学历、家庭背景,只要你敢勇敢地做自己,就总有人会爱你」。如果付航感谢的人是自己的好哥们、好朋友、家人,他的表达也是成立,只是他很幸运正好是爱自己的女生,我相信这个世界上一定有另外一个“付航”正在因为被他的家人、朋友爱着而勇敢的生活做自己,所以这并不是爽文。

「你必须成为佼佼者才能拥有美好的爱情,体己的朋友,成功的人生,爱你的家人」,诸如此类的社会共识好像也并不完全正确。

总决赛作品里的付航与电影《喜剧之王》里的周星驰在某种程度上是重合的,他们都在一遍遍地强调自己,他们都是小人物,被人轻视,但他们都保留着那份坚韧,最终因梦想而不平凡。但是付航跟周星驰的底我觉得还是有点不一样的,周星驰是小人物经历心酸然后变强赢得结果和尊重。付航是小人物经历心酸也获得了快乐幸福,领悟到人类社会存在的不平等的荒谬性,遇到了一个看到和接纳平凡又不平凡的爱自己的人。

We are all in the gutter, but some of us are looking at the stars. -- Oscar Wilde

经历这些心酸、不公、歧视、谩骂,需要付出多少的努力才能换来跟世界、跟自己的妥协与和解,需要吞下多少苦累才能说出那句“嘿嘿,爷爷在此”和“你在我的剧本里杀青了”。一年一度喜剧大赛中,面对老师好(刘旸教主团队)的越努力越心酸的喜剧表达时,舒淇红着眼睛跟李诞说“我没有很努力”,其实我很共情,就像麦子的那句话说的“我用十八年的努力换来跟你一起喝咖啡的资格”(我奋斗了18年才和你坐在一起喝咖啡-麦子-故事文摘),我身边大多都是“三清”(本、硕、博都是清华)的人,一个小小 C9 硕士能跟他们一起工作、竞争或多或少都需要一些不足为人道的心酸和努力,有关于资源的、也有关于天赋的。

李诞的解释是对的,努力是因为我想努力,不能有悲壮感,不能去苛求公平 —— 努力就有回报!道理是没有错的,我努力是因为我想努力,羞于努力是不可取的。可是匆匆行走的人们何时看到过脚底板下的尘埃,大家幸酸的不是努力而不得的悲壮感,而是我的每份努力都伴随一种沉重的责任感 —— 这个世界上公平是相对的,不公平是绝对的,对不公平已经存在可以视而不见的永远不可能是在努力的那一个。

付航最后有一段对自己的心酸历程的解释:

很多人看到我的东西,说我底色悲凉

真没有

为什么有一些朋友说其实我的底色悲凉,只不过因为我讲的都是我小时候经历的这些普通人都会经历到的事情,如果这样都算悲凉的话,那大家其实都悲凉,我希望传递的是 Passion

朋友们其实不是你不够好,就是因为他不喜欢你,总有人会爱你

我觉得这是付航打破社会和自己对自己的一次次审视的压抑之后练就的钝感力,他用耍宝搞怪去消解敏感者的纠结和窘迫,钝感力背后的幸酸和对世界的善意,让我看到了付航的温柔,如果对一个人温柔,那么对世界温柔那不就是慈悲吗!

如果你觉得大喊大叫、上串下跳的表演浮夸、肤浅,如果你觉得农夫、猴子、马的故事一点也不高级,如果你觉得最后的那句话老套煽情,如果没有看懂他的这些,其实挺好,这说明你一切顺遂,没有经历那份「始于微末」的不足为外人道。

关于计算机安全的一些碎碎念

2024-10-20 16:26:21

我很少谈论自己所在行业,"Security Is Our Top Priority" is BS 这篇文章与我对安全领域的理解不谋而合,作为一个计算机安全行业的从业者,我想简单分享一下自己对于计算机安全的思考:

  • 当其他人在询问你做某件事情的理由时,如果你的回答仅仅是这么做更安全,你需要停下思考一下自己这么做除了帮助别人解决问题之外有多少是为了「保护自己」,同时还要诚实地告诉自己 —— 结果高于自我;
  • 我一直觉得安全是个不讨喜的行业,做得最优秀的安全一定是默默无闻不被感知的,一旦安全被感知了,安全人员可能就要「背锅」了;
  • 安全之所以被划分为「非功能性需求」,跟管理学逻辑是类似的,当资源有限的情况下安全需要进行合理的管理 —— 将安全纳入到一个优先级框架中进行风险控制;
  • 安全一定是有边界的,明确的安全边界假设除了降低解决问题的复杂度,更重要的是降低管理「成本」,因为管控安全边界的成本一定低于管控安全的成本;
  • 安全是无止境,对安全从业者一定是的,对除此以外其他人一定不是的;
  • 安全是平衡的「艺术」,它真正的价值是解决用户的信任问题。举个例子,汽车都会有安全问题,作为汽车安全从业者,你不需要把它设计成「无坚不摧」的坦克才会有用户去买,你只需要设计好安全带、防撞梁、安全气囊,并且做好安全测试保证风险控制有效,就能让用户信任并购买;

最后附上博客原文翻译:

几年前,我被邀请去做一个关于软件安全的会议演讲。其实,我并没有真正被邀请,而是我的公司购买了一个包含演讲席位的赞助包,我回复了一封内部邮件,自愿参与了这个活动 🤣 无论如何,在准备我的演讲过程中,我意识到了几个关于安全的重要观点,这些观点自此一直萦绕在我的心头:

1. **安全是无止境的**。你总是可以投入更多努力来提高安全性。质量和安全、员工满意度等也是如此。
2. 安全的需求与**便捷的用户体验**需求**相冲突**。增强一个方面往往会损害另一个方面。

现在,有些组织宣称“安全是我们的首要任务”。真的吗?你想要将一个没有上限的事情作为你的首要任务?我的意思是,安全是一件好事,但这听起来是不是有点太简单了?实际上,像这样的空洞营销声明可能会让我有点生气。在这篇文章中,我将帮助你理解如何解读这样的声明,以及在现实生活中如何处理安全问题。我将涵盖:

*   一个哲学性的介绍
*   “安全是我们的首要任务”实际上意味着什么?
*   你应该在多大程度上重视安全?
*   公司应该怎么说替代这句话?

## 安全性与用户体验平衡的哲学思考

安全的无限性以及它与用户体验(UX)之间的平衡让我想起了某件事。我不得不停下来思考了一会儿,然后我想起来了。大约在我19或20岁的时候,我发现了GK Chesterton。我非常喜欢他,因为他让我意识到实际上有非常了不起的基督教思想家,而在我那种多少算是福音派的背景中,这样的人并不多。不幸的是,尽管有Chesterton的影响,但与20岁时的我希望的相反,我个人的信仰并没有坚持下去。不过,我还是从他的著作中学到了很多至今仍感激的东西。这里有一段与我们讨论的话题相关的引用:

> “现代社会并不邪恶;在某些方面,现代社会太过美好了。它充满了狂野而被浪费的美德。当一个宗教体系被粉碎(正如基督教在宗教改革中被粉碎那样),被释放的不仅仅是恶习。恶习确实被释放了,它们四处游荡并造成破坏。但美德也被释放了;美德游荡得更加疯狂,美德造成的破坏更加可怕。现代社会充满了旧基督教美德的疯狂。美德之所以疯狂,是因为它们彼此孤立,独自游荡。因此,有些科学家只关心真理;而他们的真理是无情的。因此,有些人文主义者只关心怜悯;而他们的怜悯(很遗憾地说)往往是不真实的。”——GK Chesterton,《正统》,1908年

Chesterton的意思是,好的事物(美德)如果被过分追求或者脱离了其他好的事物,就可能变成坏事。这听起来似乎是显而易见的,确实如此。但是一旦你掌握了这个模式,就会发现它一直在发生。在这篇博文中,我们谈论的是那些忽视了UX的安全狂热者,但在我们的政治世界中也是如此。有些人将多样性、公平性和包容性(DEI)推行到极致,以至于将白人男性妖魔化。有些人将家庭价值观推崇到极致,让其他人感到格格不入。家庭价值观和包容性都是好东西,但如果它们被孤立对待,就可能走向极端,并在群体之间造成分裂。

因此,我们需要在所有美好的事物之间找到一个健康的平衡,不让任何一个走向极端而忽视其他。这并不容易!如果你是一个安全极端主义者,即使你还是被黑客攻击了,至少可以说你已经竭尽全力来提高安全性。另一方面,如果你在安全性和UX之间取得了平衡,并为了更好的UX而做出了一些降低安全性的决定,然后遭到了黑客攻击。你如何为自己辩护?说“现实是复杂的,我们在这种情况下决定优先考虑UX”并不是一个听起来很好的回答。捍卫非极端立场需要勇气,因为极端主义者总是有更有力的口号。

## “安全是我们的首要任务”实际上意味着什么?

安全真的是无限的吗?我认为是的。实际上,银行最安全的运营方式可能是关闭他们的在线业务,购买一个大保险库,并在门外部署一支小型军队。即便如此,安全检查的强度和军队的规模也可以无限增加。然而,大多数人还是更愿意通过银行应用程序的面部识别来转账,并认为这种安全程度已经足够好。

公司真的会这么说吗?是的。比如微软的“[将安全置于一切之上](https://blogs.microsoft.com/blog/2024/05/03/prioritizing-security-above-all-else/)”,AWS的“[在AWS,云安全是最高优先级](https://docs.aws.amazon.com/whitepapers/latest/aws-overview/security-and-compliance.html)”,Meta的“[保护您的数据是我们的最高优先级](https://about.meta.com/actions/protecting-privacy-and-security/)”,还有很多其他例子。

这听起来不错,但在实践中,这是否意味着只要有人提出以牺牲UX、消费者价格等为代价来提高安全性的想法,你就会实施?因为这似乎是它的意思。当然,答案是否定的,所以我对这些声明并不太认真,尽管我理解这给客户带来了一种温暖而模糊的感觉。我更希望组织能够坦诚和清晰,但我只能梦想在一个这样的世界里。

实际上,这些公司所表达的可能是:

> “我们有一个平衡的优先级框架,我们对问题进行分类并给予每个问题加权得分。我们给UX问题分配20%,安全25%,技术债务10%,新功能20%,等等。正如你所见,风险是首要优先级,因为25%高于20%。”

或者他们可能意味着:

> “安全(达到我们行业中标准水平的安全性)是我们的首要任务。一旦我们实施了足够的安全措施,我们就会专注于其他问题。”

如果你有完全诚实的安全声明的例子,比如“我们关心安全,因为我们需要你信任我们,因此它是我们的首要任务之一”,我会非常感兴趣。请在评论中分享!

## 安全性应该做到什么程度?

当我在2018年做那次演讲时,我还不清楚到底应该在何处划线。自那以后,我开始使用ISO 14971和ISO 27001这样的风险框架进行工作,现在我已经有了一些工具来帮助我处理这个问题。

一切始于一个评分机制:弄清楚我们需要保护的是什么,存在哪些风险,以及这些风险发生的可能性和严重程度如何。你将可能性和严重性相乘,为每个风险得出一个风险分数,然后将这些分数映射到低、中、高三个等级。

- 如果风险分数是低的,你可以安全地接受这个风险。
- 如果是中等,你应该考虑风险控制,除非有正当理由不这么做。
- 如果是高风险,你必须实施风险控制。

然后,一旦风险控制措施到位,验证它们是否真正减轻了风险,剩余风险是否现在变低或不存在。这样你就完成了。

听起来简单,但低、中、高的界限我们应该怎么划分呢?我总是把一些决策者聚在一起,给他们提供可能出错的情况的例子,然后请他们提出一个评分系统。接着询问他们愿意接受哪些类型的风险,同时考虑到如果应用了风险控制,产品或流程会是什么样子。

这个简单的评分系统包括风险矩阵、风险偏好/风险接受政策和风险清单。这是一个简单的工具,可以帮助你做出决策。

## 我们应该怎么说?

几十年来,全球都在以成熟的方式处理安全、质量和安全问题。让我们推广那些实际这么做的公司,而不是说这是他们的首要任务。这是一个没有意义的声明。

在我理想的世界里,公司会说:“我们维护着最先进的安全体系,因为没有客户的信任,我们作为公司就没有存在的权利。因此,这是我们工作中最重要的事情之一,我们投入了大量的精力。”这可能对大多数人来说听起来没有那么好,但这样说的公司肯定会给我留下深刻的印象。