Press "Enter" to skip to content

你的日记导出

「你的日记」是一个精致的日记应用。 受启于动画《你的名字》,旨在将二次元的故事带进现实 生活。

你的日记官网:https://nideriji.cn/

你的日记官方导出工具:https://nijiweb.cn/pc/

目前官方版导出工具只能导出为txt且不包含图片。于是用Python写了一下,可以导出自己或者匹配笔友的日记为md,并且包含图片。

Github仓库地址:https://github.com/zanghuaren/nideriji-download

import requests
import os
import re
import base64
from Crypto.Cipher import AES
import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm

EMAIL = "填写你的账号"
PASSWORD = "填写你的密码"
ORDER_OLD_TO_NEW = True
MAX_WORKERS = 5


def login(email, password):
    """登录并获取 token"""
    url = "https://nideriji.cn/api/login/"
    headers = {'User-Agent': 'OhApp/3.6.12 Platform/Android'}
    data = {'email': email, 'password': password}
    r = requests.post(url, headers=headers, data=data)
    r.raise_for_status()
    token = r.json().get('token')
    if not token:
        raise ValueError("登录失败,未获取到 token")
    print("[INFO] 登录成功")
    return token


def clean_unicode(text):
    """清理高代理 Unicode"""
    for prefix in ["\\ud83c", "\\ud83d", "\\ud83e"]:
        text = text.replace(prefix, "")
    return text


def decrypt_privacy(content, user_id):
    """解密隐私日记块"""
    def decrypt_block(match):
        cipher_text = match.group(1)
        key = str(user_id).encode('utf-8')
        aes = AES.new(key.ljust(16, b'\0'), AES.MODE_ECB)
        try:
            decrypted = aes.decrypt(bytes.fromhex(cipher_text))
            return "[隐私区域开始]" + decrypted.decode('utf-8', errors='ignore') + "[隐私区域结束]"
        except:
            try:
                decrypted = aes.decrypt(base64.b64decode(cipher_text))
                return "[隐私区域开始]" + decrypted.decode('utf-8', errors='ignore') + "[隐私区域结束]"
            except:
                return match.group(0)
    pattern = re.compile(
        r"\[以下是隐私区域密文,请不要做任何编辑,否则可能导致解密失败\]([\s\S]+?)\[以上是隐私日记,请不要编辑密文\]")
    return pattern.sub(decrypt_block, content)


def get_diaries_overview(token, partner=False):
    """获取日记列表及图片 ID"""
    headers = {'auth': f'token {token}',
               'User-Agent': 'OhApp/3.6.12 Platform/Android'}
    r = requests.post("https://nideriji.cn/api/v2/sync/",
                      headers=headers,
                      data={'user_config_ts': '0', 'diaries_ts': '0',
                            'readmark_ts': '0', 'images_ts': '0'})
    r.raise_for_status()
    data = r.json()
    # print(data)

    if partner:
        diaries = data.get('diaries_paired', [])
        all_image_ids = [img['image_id']
                         for img in data.get('images_paired', [])]
        user_id = data['user_config']['paired_user_config']['userid']
    else:
        diaries = data.get('diaries', [])
        all_image_ids = [img['image_id'] for img in data.get('images', [])]
        user_id = data['user_config']['userid']

    print(f"[INFO] 获取到 {len(all_image_ids)} 张图片 ID")
    if diaries:
        print(
            f"[INFO] 获取日记总数: {len(diaries)} 篇,日期范围: {diaries[-1]['createddate']} - {diaries[0]['createddate']}")
    return diaries, user_id, all_image_ids


def get_diary_full(token, user_id, diary_id):
    """获取完整日记内容"""
    headers = {'auth': f'token {token}',
               'User-Agent': 'OhApp/3.6.12 Platform/Android'}
    r = requests.post(f"https://nideriji.cn/api/diary/all_by_ids/{user_id}/",
                      headers=headers,
                      data={'diary_ids': diary_id})
    r.raise_for_status()
    data = r.json()
    if 'diaries' in data and data['diaries']:
        d = data['diaries'][0]
        d['content'] = clean_unicode(d.get('content', ''))
        d['content'] = decrypt_privacy(d['content'], user_id)
        return d
    return None


def filter_diaries_by_date(diaries, start_date, end_date):
    """按日期筛选日记"""
    filtered = [d for d in diaries if start_date <= datetime.datetime.strptime(
        d['createddate'], "%Y-%m-%d").date() <= end_date]
    filtered.sort(key=lambda x: x['createddate'], reverse=not ORDER_OLD_TO_NEW)
    print(f"[INFO] {len(filtered)} 篇日记在指定范围内")
    return filtered


def save_diary(diary):
    """保存 Markdown 日记"""
    diary_date = datetime.datetime.strptime(
        diary['createddate'], "%Y-%m-%d").date()
    folder_name = f"{diary_date.year}-{diary_date.month:02d}"
    os.makedirs(folder_name, exist_ok=True)
    filename = os.path.join(folder_name, f"{diary_date}.md")
    with open(filename, 'w', encoding='utf-8') as f:
        weekday = diary.get('weekday', '')
        f.write(f"=={diary['createddate']} {weekday}==\n")
        f.write(diary.get('title', '') + '\n')
        f.write(diary.get('content', ''))


def download_all_images(image_ids, user_id, headers):
    """下载所有图片"""
    os.makedirs("Pictures", exist_ok=True)
    print(f"[INFO] 开始下载 {len(image_ids)} 张图片")
    for image_id in tqdm(image_ids, desc="下载图片", unit="img"):
        url = f"https://f.nideriji.cn/api/image/{user_id}/{image_id}/"
        try:
            r = requests.get(url, headers=headers, timeout=15)
            r.raise_for_status()
            with open(os.path.join("Pictures", f"{image_id}.jpg"), 'wb') as f:
                f.write(r.content)
        except Exception as e:
            print(f"[WARN] 下载图片 {image_id} 失败: {e}")


def replace_images_in_markdown(diaries):
    """替换 Markdown 中图片路径"""
    print("[INFO] 替换 Markdown 中图片路径...")
    md_files = []
    for d in diaries:
        diary_date = datetime.datetime.strptime(
            d['createddate'], "%Y-%m-%d").date()
        folder_name = f"{diary_date.year}-{diary_date.month:02d}"
        md_files.append(os.path.join(folder_name, f"{diary_date}.md"))
    for md_file in tqdm(md_files, desc="替换图片路径", unit=""):
        with open(md_file, 'r', encoding='utf-8') as f:
            content = f.read()
        content = re.sub(
            r'\[图(\d+)\]', lambda m: f"![img](../Pictures/{m.group(1)}.jpg)", content)
        with open(md_file, 'w', encoding='utf-8') as f:
            f.write(content)


if __name__ == "__main__":
    email = input(f"请输入邮箱:") or EMAIL
    password = input(f"请输入密码: ") or PASSWORD
    token = login(email, password)

    # 新增选择:自己日记或搭档日记
    choice = input("输入1保存自己的日记,输入2保存搭档的日记:") or "1"
    partner = choice == "2"

    diaries_overview, user_id, all_image_ids = get_diaries_overview(
        token, partner)

    first_date = datetime.datetime.strptime(
        diaries_overview[-1]['createddate'], "%Y-%m-%d").year
    now = datetime.datetime.now()
    start_year = int(input(f"请输入起始年份 [{first_date}]: ") or first_date)
    start_month = int(input("请输入起始月份 [1]: ") or 1)
    end_year = int(input(f"请输入结束年份 [{now.year}]: ") or now.year)
    end_month = int(input(f"请输入结束月份 [{now.month}]: ") or now.month)
    start_date = datetime.date(start_year, start_month, 1)
    end_day = (datetime.date(end_year, end_month + 1, 1) -
               datetime.timedelta(days=1)).day if end_month < 12 else 31
    end_date = datetime.date(end_year, end_month, end_day)

    filtered_diaries = filter_diaries_by_date(
        diaries_overview, start_date, end_date)

    headers = {'auth': f'token {token}',
               'User-Agent': 'OhApp/3.6.12 Platform/Android'}

    print("[INFO] 下载并保存日记...")
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        futures = [executor.submit(
            get_diary_full, token, user_id, d['id']) for d in filtered_diaries]
        full_diaries = []
        for future in tqdm(as_completed(futures), total=len(futures), desc="下载日记", unit=""):
            diary = future.result()
            if diary:
                save_diary(diary)
                full_diaries.append(diary)

    download_all_images(all_image_ids, user_id, headers)
    replace_images_in_markdown(full_diaries)

    print("[INFO] 日记及图片按月份导出完成!")

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注