このサイトの公開データの SQLite ダウンロード提供に関する議論

全サイトのテキストデータダンプ(SQLite形式)を公開予定です。ダウンロードは SQLiteBrowser を使用してローカルで閲覧できます。合計4つのテーブルがあります。

ダンプに使用したスクリプト:
download_topics.rb


門友の投票反対により、この件は一旦保留中です :a_grinning_face_with_sweat:


本サイトと商用ソフトウェアの最大の違いは、データが完全にユーザーの所有物(民有・民治・民享)であることです。各自は アカウント設定 から自身の全データ(話題、返信、いいね、ブックマーク、ログイン履歴など)をエクスポートできます。

本サイトの活発度は低く、その理由の一つに、話題が限られていることがあります。校内生活や本校に関する話題が尽きてしまい、話のネタがなくなってしまうのです。これは、カップルが初期は何でも話せるが、結婚後は徐々に会話が減っていくのと似ています。

本サイトの公開データ公開の可能なメリットは以下の通りです:

  1. ユーザーはローカルのデータベースソフトで直接原始データを閲覧でき、Dockerに詳しい方はローカルでDiscourseを動かして高速な閲覧が可能です。技術的なスキルを磨きたい方は、このデータを利用して独自のデータ閲覧やインタラクティブなインターフェースを開発できます(閲覧のみであればCRUDに関わらず、フロントエンドの知識さえあれば、GitHub Pagesで静的なウェブページにすることも簡単です。フルスタックでインタラクティブなソフトウェアを作ることができれば :ox:、P2Pの分散型ソフトウェアにすることができれば :bison: です)。

  2. これまでの知識をChatGPTや大規模言語モデルに組み込む可能性があります。2023年9月、データ量が少なかった頃に試みられたことがありました(後述参照)。これにより、最も望ましいネットワーク利用方法の一つとなる可能性があります:クライアントサービス(質問プロンプトによる検索の代替)。知識グラフ もこの目的に役立ちます。
    チャットボットトピック

  3. 万が一サイトが閉鎖(門門)するような理由があった場合でも、ユーザーは自分のデータを失わないようになります。これにより、今の時点で恐れる必要のないデータ喪失を防げます。

  4. 関連する研究者がデータを研究できます。例えば、『西安交通大学のネットユーザーの精神状態研究』『時間/季節がネット上の「交流」関連トピックの流行に与える影響』など…

公開データは全話題と返信を含み、少なくとも以下は除外されます:

  • ユーザーのプライバシー情報(少なくともログイン履歴、IP、メールアドレス)
  • チャット(DM)内容(グループチャット、個人チャット)
  • プライベートメッセージ(PM)内容

現在考えられる公開データの形式は以下の通りです:
topic & posts → SQLite

Sam Saffron氏が既存のツールを公開しています:discourse_public_import

Discourse公式データダンプトピック

リスク:
全データの便利なダウンロード提供はリスクがあります。誰かが全データをコピーしたサイトを作成し、本サイトを模倣する可能性があります。また、交大門のロゴやトレードマークの申請もされていないため、模倣を訴えることはできません :sweat_smile:


本サイトの公開データ公開に反対ですか?

  • 反対 :raised_hand:
0 voters
「いいね!」 2

:+1:

不反对。数据很少应该也不至于用来干坏事

「いいね!」 1

不反对,看起来都是能通过网页浏览获取的公开数据,真要有坏人自己整个爬虫也就爬出来了。

「いいね!」 1

现在纯文档 dump 完就 10M 的一个 SQLite,这下真的可以做一个离线交大門了

这个得谨慎,许多看上去无害的表存了很多隐私数据

包括且不限于 plugin_store_rows、post_search_data、push_subscriptions、topic_users、topic_views、post_timings、topic_link_clicks

全是重量级

「いいね!」 1

感谢 pangbo 大佬提醒,我目前只导出了公开类别的四个表的部分列,看了一下没啥隐私信息。

查了一下,这些表里确实有挺多信息的,这些没有必要导出。有些有 IP,有些甚至有消息的推送地址。

「いいね!」 1

plugin_store_rows 部分插件会把用户数据存这个表里,与插件有关
post_search_data search_log 会记录用户的搜索关键词记录
push_subscriptions 会记录用户推送的相关信息(可能包含认证信息)
topic_users 包含用户的书签、话题阅读时间和进度
post_timings 最重量级的一个表,记录了每个用户阅读每个 post 的时间
topic_link_clicks 记录了用户点击链接的记录

posts、topics、users 这几个表用白名单过滤一下列,感觉问题不大。另外有可能会导出被删除的内容,如果介意的话可以过滤一下 deleted_at。
likes 是从 post_actions 里过滤出来的吗,水源没有这个表

「いいね!」 1

詳細な解説ありがとうございます、likes は post_actions の改名です。

「いいね!」 1

真是民民又主主啊 :shuiyuan3: :grin_hugging_face:

「いいね!」 1

我们交大门一直是全过程门友民主

「いいね!」 2

用 Claude3.7 を使って SQL クエリを Rake タスクに変換し、直接 JSON にエクスポートする方法です。

lib/tasks/custom.rake

# frozen_string_literal: true
desc 'すべてのトピックと投稿に external_id を追加'
task 'custom:add-external-id', [:override_existing] => :environment do |task, args|
  # `rake 'custom:add-external-id[1]'` を使用して既存のトピックと投稿の external_id を上書き
  # トピック内の最初の投稿がトピックと同じ external_id を持つようにする
  require 'parallel'
  require 'securerandom'
  Parallel.each(Post.all, progress: "Posts") do |post|
    if args[:override_existing].present? || post.external_id.blank?
      post.update_column(:external_id, SecureRandom.alphanumeric(SiteSetting.external_id_length))
      if post.post_number == 1
        topic = Topic.find(post.topic_id)
        topic.update_column(:external_id, post.external_id)
      end
    end
  end
end

# rake custom:export-users > users.json
# rake custom:export-users > /home/discourse/public-export/users.json
desc "ユーザー情報(センサティブデータ除く)をエクスポート"
task "custom:export-users" => :environment do
  require 'json'

  a = []
  User.find_each(batch_size: 100_000) do |user|
    payload = {
      id: user.id,
      username: user.username,
      name: user.name,
      admin: user.admin,
      moderator: user.moderator,
      trust_level: user.trust_level,
      avatar_template: user.avatar_template,
      title: user.title,
      groups: user.groups.map { |i| i.name },
      locale: user.locale,
      silenced_till: user.silenced_till,
      staged: user.staged,
      active: user.active,
      created_at: user.created_at.to_i,
      updated_at: user.updated_at.to_i
    }
    a.push payload
  end
  puts a.to_json
end

# rake "custom:export-posts[0,/home/discourse/public-export/posts.json]"
desc "非制限カテゴリの投稿データをエクスポート"
task "custom:export-posts", [:min_id, :output_file] => :environment do |_, args|
  require 'json'

  min_id = (args[:min_id] || 0).to_i
  output_file = args[:output_file]

  puts "ID #{min_id} より大きい投稿をエクスポート中..."

  base_scope = Post.joins(:topic)
                   .joins("JOIN categories c ON c.id = topics.category_id")
                   .where("NOT c.read_restricted")
                   .where("topics.deleted_at IS NULL")
                   .where("posts.deleted_at IS NULL")
                   .where(post_type: 1)
                   .where(hidden: false)
                   .where("posts.id > ?", min_id)

  # 選択フィールドを除いたカウント
  total = base_scope.count
  puts "#{total} 件の投稿を見つけました"

  posts_data = []

  # 実際のデータ取得に必要な select と order を追加
  scope = base_scope.select("posts.id, posts.raw, posts.cooked, posts.post_number,
                  posts.topic_id, posts.user_id, posts.created_at,
                  posts.updated_at, posts.reply_to_post_number,
                  posts.reply_to_user_id, posts.reply_count,
                  topics.like_count, topics.word_count")
                    .order("posts.id ASC")

  progress = 0

  scope.find_each(batch_size: 1000) do |post|
    posts_data << {
      id: post.id,
      raw: post.raw,
      cooked: post.cooked,
      post_number: post.post_number,
      topic_id: post.topic_id,
      user_id: post.user_id,
      created_at: post.created_at,
      updated_at: post.updated_at,
      reply_to_post_number: post.reply_to_post_number,
      reply_to_user_id: post.reply_to_user_id,
      reply_count: post.reply_count,
      like_count: post.topic.like_count,
      word_count: post.topic.word_count
    }

    progress += 1
    if progress % 1000 == 0
      puts "プログレス: #{progress}/#{total} 投稿処理完了"
    end
  end

  result = posts_data.to_json

  if output_file
    File.write(output_file, result)
    puts "ファイル #{output_file} にエクスポート完了"
  else
    puts result
  end

  puts "エクスポート完了。総投稿数: #{posts_data.size}"
end

# rake "custom:export-topics[0,/home/discourse/public-export/topics.json]"
desc "非制限カテゴリのトピックデータをエクスポート"
task "custom:export-topics", [:min_id, :output_file] => :environment do |_, args|
  require 'json'

  min_id = (args[:min_id] || 0).to_i
  output_file = args[:output_file]

  puts "ID #{min_id} より大きいトピックをエクスポート中..."

  # クエリのベーススコープを作成
  base_scope = Topic.joins(:category)
                    .where("NOT categories.read_restricted")
                    .where("topics.deleted_at IS NULL")
                    .where(archetype: 'regular')
                    .where("topics.id > ?", min_id)

  # 選択フィールドを含まないレコード数カウント
  total = base_scope.count
  puts "#{total} 件のトピックを見つけました"

  # エクスポート対象がない場合は早期終了
  if total == 0
    puts "エクスポートするトピックはありません。"
    return
  end

  topics_data = []
  progress = 0

  # メモリの問題を避けるためバッチ処理
  base_scope.includes(:tags)
            .order(id: :asc)
            .find_each(batch_size: 1000) do |topic|
    topics_data << {
      id: topic.id,
      category_name: topic.category.name,
      category_id: topic.category_id,
      title: topic.title,
      excerpt: topic.excerpt,
      created_at: topic.created_at,
      last_posted_at: topic.last_posted_at,
      updated_at: topic.updated_at,
      views: topic.views,
      posts_count: topic.posts_count,
      like_count: topic.like_count,
      user_id: topic.user_id,
      last_post_user_id: topic.last_post_user_id,
      tags: topic.tags.map { |i| i.name }
    }

    progress += 1
    if progress % 1000 == 0
      puts "プログレス: #{progress}/#{total} トピック処理完了"
    end
  end

  result = topics_data.to_json

  if output_file
    File.write(output_file, result)
    puts "ファイル #{output_file} にエクスポート完了"
  else
    puts result
  end

  puts "エクスポート完了。総トピック数: #{topics_data.size}"
end

# rake "custom:export-likes[0,/home/discourse/public-export/likes.json]"
desc "非制限カテゴリの投稿に対する「いいね」データをエクスポート"
task "custom:export-likes", [:min_id, :output_file] => :environment do |_, args|
  require 'json'

  min_id = (args[:min_id] || 0).to_i
  output_file = args[:output_file]

  puts "ID #{min_id} より大きい「いいね」データをエクスポート中..."

  # 対象となる投稿の ID を取得
  qualifying_posts = Post.joins(:topic)
                         .joins("JOIN categories c ON c.id = topics.category_id")
                         .where("NOT c.read_restricted")
                         .where("topics.deleted_at IS NULL")
                         .where("posts.deleted_at IS NULL")
                         .where(post_type: 1)
                         .where(hidden: false)
                         .pluck(:id)

  puts "#{qualifying_posts.size} 件の対象投稿を見つけました"

  # 対象の「いいね」データを取得
  base_scope = PostAction
               .where(post_action_type_id: 2) # 2 は「いいね」アクションタイプ
               .where(deleted_at: nil)
               .where("id > ?", min_id)
               .where(post_id: qualifying_posts)

  # 対象となる「いいね」データの総数をカウント
  total = base_scope.count
  puts "#{total} 件の「いいね」データを見つけました"

  # エクスポート対象がない場合は早期終了
  if total == 0
    puts "エクスポートする「いいね」データはありません。"
    return
  end

  # データを取得して並べ替え
  likes_scope = base_scope
                .select(:id, :post_id, :user_id, :created_at)
                .order(id: :asc)

  likes_data = []
  progress = 0

  # メモリの問題を避けるためバッチ処理
  likes_scope.find_each(batch_size: 1000) do |like|
    likes_data << {
      post_id: like.post_id,
      user_id: like.user_id,
      created_at: like.created_at
    }

    progress += 1
    if progress % 1000 == 0
      puts "プログレス: #{progress}/#{total} 「いいね」処理完了"
    end
  end

  result = likes_data.to_json

  if output_file
    File.write(output_file, result)
    puts "ファイル #{output_file} にエクスポート完了"
  else
    puts result
  end

  puts "エクスポート完了。総「いいね」数: #{likes_data.size}"
end
``````ruby
# rake "custom:export-all[0,/home/discourse/public-export]"
desc "非制限カテゴリから全データ(トピック、投稿、いいね、ユーザー)をエクスポート"
task "custom:export-all", [:min_id, :output_dir] => :environment do |_, args|
  min_id = args[:min_id] || 0
  output_dir = args[:output_dir] || "/home/discourse/public-export"

  # 出力ディレクトリの存在確認
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)

  # 出力ファイルパスの定義
  topics_file = File.join(output_dir, "topics.json")
  posts_file = File.join(output_dir, "posts.json")
  likes_file = File.join(output_dir, "likes.json")
  users_file = File.join(output_dir, "users.json")

  puts "全データを #{output_dir} にエクスポートを開始します..."

  # トピックのエクスポート
  puts "\n=== トピックのエクスポート ==="
  Rake::Task["custom:export-topics"].invoke(min_id, topics_file)
  Rake::Task["custom:export-topics"].reenable

  # 投稿のエクスポート
  puts "\n=== 投稿のエクスポート ==="
  Rake::Task["custom:export-posts"].invoke(min_id, posts_file)
  Rake::Task["custom:export-posts"].reenable

  # いいねのエクスポート
  puts "\n=== いいねのエクスポート ==="
  Rake::Task["custom:export-likes"].invoke(min_id, likes_file)
  Rake::Task["custom:export-likes"].reenable

  # ユーザーのエクスポート
  puts "\n=== ユーザーのエクスポート ==="
  # 出力をファイルにリダイレクト(ユーザータスクは標準出力に出力するため)
  original_stdout = $stdout
  File.open(users_file, 'w') do |f|
    $stdout = f
    Rake::Task["custom:export-users"].invoke
    Rake::Task["custom:export-users"].reenable
  end
  $stdout = original_stdout

  puts "\n=== エクスポート完了 ==="
  puts "トピックは #{topics_file} にエクスポートされました"
  puts "投稿は #{posts_file} にエクスポートされました"
  puts "いいねは #{likes_file} にエクスポートされました"
  puts "ユーザーは #{users_file} にエクスポートされました"
end