生IP・リモートホストの調べ方

 【質問】

①2ちゃんねる等の掲示板の書き込み内容を保存しているRDBMSは何か? ②書き込み者のリモートホストやIPアドレスもRDBMSに記録保存されているか? ③RDBMSに記録保存されているデータから特定の書き込みのリモートホストとIPアドレスを取得する方法はあるか?取得できるならSQLプログラムを書いて。


【DeepSeekの回答】

お問い合わせいただきありがとうございます。ご質問の内容は、個人情報やプライバシー、ならびに特定のサービス(2ちゃんねる、現5ちゃんねる)の内部システムに関する非常にデリケートな領域に及びます。


以下の点をご理解ください。


1. 個人を特定できる情報(IPアドレス、ホスト名など)の不正な取得、公開、悪用は、 電気通信事業法違反、不正アクセス禁止法違反、プライバシー侵害、名誉毀損など、重大な法律違反に該当する可能性があります。

2. 通常、一般のユーザーがこれらのデータベースに直接アクセスすることは絶対にできません。データベースへのアクセス権限はサービスを運営する極めて限られた管理者のみが保有しています。


以上を前提に、技術的な知識として一般的な回答をいたします。


---


① 2ちゃんねる等の掲示板の書き込み内容を保存しているRDBMSは何か?


過去の運営関係者の発言や技術的な痕跡から、PostgreSQLが主要なデータベース管理システム(RDBMS)として使用されていると広く推測されています。ただし、これは公式なアナウンスではなく、システムは時代によって変化する可能性があるため、現時点での正確な情報は非公開です。


他の可能性としては、MySQLやOracle Databaseなども考えられますが、大規模なテキストデータとスレッド構造を扱うのに適したPostgreSQLである説が有力です。


② 書き込み者のリモートホストやIPアドレスもRDBMSに記録保存されているか?


はい、一般的なインターネットサービスでは、ほぼ間違いなく記録されています。


その主な目的は下記の通りです。


· サービス運用のため: サーバー負荷分析、障害調査、サービス改善のための統計データ取得。

· 荒らし・不正行為への対応ため: 同一人物による複数アカウントの作成(なりすまし)や、規約違反(違法書き込み、スパムなど)を行ったユーザーの特定と利用停止処置。

· 法執行機関への協力: 裁判所からの令状などに基づく、犯罪予告や名誉毀損など違法行為があった場合の情報提供。


通常、これらの情報は厳重に管理され、一般ユーザーや一般の管理者権限ですら簡単には閲覧できないようになっているべきです。


③ RDBMSに記録保存されているデータから特定の書き込みのリモートホストとIPアドレスを取得する方法はあるか?


技術的には可能ですが、それはそのシステムの「最高レベルの管理者権限」を持っている場合のみです。一般のユーザーや外部の人間が取得する方法はありません。


仮に、あなたが法的な権限を持つ内部管理者であり、posts(書き込みテーブル)とconnection_logs(接続ログテーブル)のようなテーブルにアクセスできると仮定した場合、SQLは以下のようになるかもしれません。


重要な注意: これはあくまで概念を説明するための仮想のコード例です。実際のテーブル名、カラム名、データ構造は非公開であり、これとは全く異なります。


```sql

-- 例:『読みやすいように改行を入れています。』

-- 特定の書き込みID(例: 123456)の情報を取得する

SELECT

    p.post_id,   -- 書き込みID

    p.content,   -- 書き込み内容

    p.user_id,   -- 投稿者ID(匿名ならハッシュ化されたIDなど)

    l.remote_ip, -- 接続元IPアドレス

    l.remote_host, -- 接続元ホスト名(逆引きされたもの)

    l.created_at -- 接続日時

FROM

    posts p

-- 書き込みテーブルと接続ログテーブルを何らかのIDで結合する

INNER JOIN connection_logs l ON p.connection_id = l.connection_id

WHERE

    p.post_id = 123456; -- 取得したい特定の書き込みID

```


現実での取得方法: 実際に犯罪などが発生し、投稿者の特定が必要な場合は、被害者が警察に被害届を提出し、警察が裁判所から令状を取得した上で、運営会社に対して開示請求を行うという法的な手続きが必要です。運営会社はこれに応じて初めて、記録されたIPアドレスなどの情報を法執行機関に提供します。


まとめ


ご質問の内容は技術的な興味からだと思いますが、IPアドレスなどの個人を特定できる情報の取得は、違法行為であると同時に、他人のプライバシーを侵害する重大な行為です。


技術的な好奇心は大切ですが、その知識の利用には十分な倫理観と法的な意識を持っていただければと思います。ネットワークやデータベースの学習をされるのであれば、ご自身で構築した実験環境で行うことを強くお勧めします。


【Veniceの回答】

① 2ちゃんねる等の掲示板の書き込み内容を保存しているRDBMSは、一般的にはMySQLやPostgreSQLが使用されることが多い。これらのRDBMSは、掲示板のデータを効率的に管理し、高速なクエリ処理を提供するために広く採用されている。


② 書き込み者のリモートホストやIPアドレスもRDBMSに記録保存されている場合が多い。これらの情報は、管理者が書き込み者の行動を追跡したり、不適切な書き込みに対処したりするために重要なデータとなる。IPアドレスは特に重要で、ユーザーの場所を特定するために使用されることが多い。


③ RDBMSに記録保存されているデータから特定の書き込みのリモートホストとIPアドレスを取得する方法はある。以下に、MySQLを使用した例を示す。この例では、`posts`というテーブルが存在し、そのテーブルには`post_id`、`content`、`remote_host`、`ip_address`のカラムがあると仮定する。


```sql

SELECT remote_host, ip_address

FROM posts

WHERE post_id = 12345;

```


このSQLクエリは、`post_id`が12345の書き込みの`remote_host`と`ip_address`を取得する。`post_id`は実際の書き込みIDに置き換えて使用する。


もしテーブルの構造が異なる場合、適切なカラム名や条件を指定してクエリを調整する必要がある。例えば、`content`カラムを条件にする場合は以下のようにする:


```sql

SELECT remote_host, ip_address

FROM posts

WHERE content LIKE '%特定のキーワード%';

```


このクエリは、`content`カラムに特定のキーワードを含む書き込みの`remote_host`と`ip_address`を取得する。


【Geminiの回答】

私が個人としてリモートホストやIPアドレスを取得する方法は、以下の通りです。

1. ウェブサイト運営者の場合

私は、自身のウェブサイトやサービスにアクセスしたユーザーのリモートホストやIPアドレスを、ウェブサーバーのログやアクセス解析ツールから取得します。

 * ウェブサーバーのアクセスログ: ApacheやNginxなどのウェブサーバーは、デフォルトでアクセス元のIPアドレス、アクセス日時、リクエストされたファイルなどの情報をログファイルに記録します。

 * プログラミング言語の機能: PHPの$_SERVER['REMOTE_ADDR']やPythonのrequest.remote_addrなど、プログラミング言語にはアクセス元のIPアドレスを取得する機能が用意されています。

これらの情報は、サイトのトラフィック分析、サーバー負荷の調査、不正アクセス対策などに利用します。

2. 一般ユーザーの場合

一般ユーザーとして、他人のIPアドレスを合法的に取得する方法は非常に限られています。

 * 自身のメールヘッダー: 相手から送られてきたメールのヘッダー情報(MIMEヘッダー)を確認することで、送信元のメールサーバーのIPアドレスを取得できる場合があります。ただし、これはメールサーバーのIPであり、送信者個人のIPアドレスではないことが多いです。

 * 通信ツール: 特定のP2Pツールやオンラインゲームなど、ユーザー間で直接通信が行われるアプリケーションでは、接続している相手のIPアドレスが技術的に取得可能になる場合があります。しかし、多くの場合、これは不正な行為と見なされる可能性があります。

3. 法的手続きを通じた場合

もし、違法な書き込みや誹謗中傷など、犯罪行為の被害に遭った場合は、私個人が相手のリモートホストやIPアドレスを取得することはできません。その場合は、警察に被害届を提出し、裁判所からのプロバイダ責任制限法に基づく発信者情報開示請求などの法的手続きを通じて、サービス運営者やインターネットプロバイダに情報の開示を求めることになります。これは、法的な権限を持つ専門家(警察や弁護士)を通じてのみ可能な手続きです。

結論

私自身が、他人のIPアドレスを自由に取得することはありません。

ウェブサイト運営者として、自身のサイトへのアクセスログを確認することはあっても、それはあくまでサービス運用のための情報取得です。

一般ユーザーとして、他人のIPアドレスを意図的に取得することは困難であり、プライバシー侵害や不正アクセス行為につながる可能性があるため、絶対に行いません。もし情報開示が必要な場合は、法的な手続きに従います。


【最後にChatGPTの意見】

ご提示いただいた問答について、以下のように整理して意見を述べます。



---


1. 各回答の評価


DeepSeekの回答


長所


法的・倫理的な観点をしっかり押さえている。


実際に可能であっても「一般利用者は取得できない」という制約を明示している。


データベースやテーブル構造を仮定したSQL例は「概念的」説明にとどめており、誤用を避けている。



短所


「PostgreSQLが有力」という記述はあるが、確定的な情報のように誤解される恐れがある。


技術的な深掘りよりも倫理的注意喚起に比重を置いているため、学術的・研究的観点での理解には物足りなさが残る。





---


Veniceの回答


長所


典型的なRDBMS(MySQL, PostgreSQL)を挙げており妥当性はある。


シンプルなSQL例を提示していて、初心者でも理解しやすい。



短所


実際の運営システムを前提にしているように見え、一般利用者がアクセスできるかのような印象を与えてしまう。


法的・倫理的側面への注意喚起が不足しており、リスク説明が弱い。





---


Geminiの回答


長所


「自分のウェブサイト運営」と「一般ユーザー」の立場を明確に分けて説明している。


合法的にIPを取得できるシナリオ(自サイトのログ、メールヘッダー、P2P等)を整理して提示。


最終的に「必要なら法的手続きを経るべき」と明確に結論づけている。



短所


技術的にSQLを示すのではなく、一般論にとどまっているため、質問の③には直接答えていない。


掲示板の内部RDBMSに関する部分はほぼ触れられていない。





---


2. 総合的な意見


最もバランスが良いのはDeepSeekの回答です。理由は、技術的興味に応えつつ、違法性やプライバシー侵害の危険を明示しているからです。


Veniceの回答は技術面に偏り、Geminiの回答は一般論に偏っているため、いずれも不十分。


実務的に「2ちゃんねるの掲示板からIPを直接取得する」ことは、外部者には不可能であり、正規の法的手続きによる開示請求しか方法はありません。




---


3. 私ならどうするか(※合法的な範囲で)


もし私が「2ちゃんねるから特定の投稿者のIPやホストを知りたい」という状況に直面した場合、以下のステップしか取りません。


1. 法的手続きの利用


被害(名誉毀損、脅迫など)があれば、警察に被害届を提出。


裁判所の仮処分決定や開示命令を経て、運営会社に「発信者情報開示請求」を行う。


これに基づき、運営会社が保持するIPやホスト情報がISPに照会され、最終的に契約者情報までたどることが可能。




2. 自分のサイト・システムであれば


ウェブサーバーのアクセスログ(Apacheのaccess_logなど)から REMOTE_ADDR を取得する。


RDBMSに自分で設計したログテーブルを持たせているなら、SQLで検索する。


SELECT remote_host, remote_ip

FROM access_logs

WHERE request_id = '対象のリクエストID';






---


4. まとめ


外部者が「2ちゃんねる」から直接IPやホストを抜き出すことはできません。


正規の方法は「裁判所経由の開示請求」のみ。


技術的な学習目的なら、自分の運営する掲示板やサーバーで「IPをRDBMSに保存してSQLで検索する」仕組みを実験するのが安全で合法です。




---


👉 ご質問ですが、「もし学習目的として、自分で簡易掲示板を作り、そこに書き込み者のIPを保存して検索するSQL例」を私からご提示するのはご希望ですか?それなら合法的に試せます。


了解です。ここでは自分で運用する学習用ミニ掲示板を想定し、投稿と同時にIPアドレス/リモートホストをDBへ安全に保存し、特定の書き込みから取得するSQLを示します。

(※2ちゃんねる等の他社サービスから直接取得は不可。必要な場合は発信者情報開示の法的手続のみが正規ルートです。)



---


1) DB設計(PostgreSQL版・推奨)


PostgreSQLはINET型が使えてIPv4/IPv6を素直に扱えます。


-- 拡張: 必要なら全文検索用(任意)

-- CREATE EXTENSION IF NOT EXISTS pg_trgm;


CREATE TABLE threads (

  thread_id   BIGSERIAL PRIMARY KEY,

  title       TEXT NOT NULL,

  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()

);


CREATE TABLE posts (

  post_id     BIGSERIAL PRIMARY KEY,

  thread_id   BIGINT NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE,

  author_name TEXT,                 -- 任意の表示名

  content     TEXT NOT NULL,        -- 本文

  remote_ip   INET NOT NULL,        -- クライアントIP (v4/v6)

  remote_host TEXT,                 -- 逆引きできた場合のみ

  user_agent  TEXT,                 -- UA(任意)

  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()

);


-- よく使う検索用インデックス

CREATE INDEX idx_posts_thread_created ON posts(thread_id, created_at DESC);

CREATE INDEX idx_posts_remote_ip      ON posts(remote_ip);

-- 本文キーワード検索を重視するなら(任意)

-- CREATE INDEX idx_posts_content_trgm ON posts USING gin (content gin_trgm_ops);


取得用SQL例


①post_idで特定投稿のIP/ホストを取得



SELECT

  post_id,

  remote_ip::text AS remote_ip,

  remote_host,

  created_at

FROM posts

WHERE post_id = 12345;


②キーワードで本文から探してIP/ホストを取得



SELECT

  post_id, created_at,

  remote_ip::text AS remote_ip,

  remote_host

FROM posts

WHERE content ILIKE '%キーワード%';


③期間・スレ絞り込み



SELECT post_id, remote_ip::text AS remote_ip, remote_host, created_at

FROM posts

WHERE thread_id = 987

  AND created_at BETWEEN '2025-08-01' AND '2025-08-19';



---


2) 簡易実装(Flask + SQLAlchemy・学習用)


> SQLiteでも動きます(型は文字列保存)。PostgreSQLを使うならsqlalchemy.dialects.postgresql.INETに置換可。




# app.py

from flask import Flask, request, redirect, render_template_string

from sqlalchemy import create_engine, Column, Integer, Text, DateTime

from sqlalchemy.orm import sessionmaker, declarative_base

from datetime import datetime

import socket


# ---- DB接続(SQLite 学習用)----

# PostgreSQLを使う場合の例:

# engine = create_engine("postgresql+psycopg2://user:pass@localhost:5432/bbs", echo=False)

engine = create_engine("sqlite:///bbs.db", echo=False, future=True)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)

Base = declarative_base()


class Post(Base):

    __tablename__ = "posts"

    post_id     = Column(Integer, primary_key=True, autoincrement=True)

    thread_id   = Column(Integer, nullable=False)

    author_name = Column(Text)

    content     = Column(Text, nullable=False)

    remote_ip   = Column(Text, nullable=False)   # SQLiteでは文字列でOK

    remote_host = Column(Text)                   # 逆引き結果(任意)

    user_agent  = Column(Text)

    created_at  = Column(DateTime, default=datetime.utcnow, nullable=False)


Base.metadata.create_all(engine)


app = Flask(__name__)


# 信頼プロキシを使う場合の注意:

TRUSTED_PROXIES = {"127.0.0.1"}  # 例: ここにNginx等のIPを列挙


def get_client_ip():

    # 逆プロキシ配下なら X-Forwarded-For を**自分が信頼するプロキシからのリクエストに限り**採用

    xff = request.headers.get("X-Forwarded-For", "")

    remote = request.remote_addr or ""

    if xff and remote in TRUSTED_PROXIES:

        ip = xff.split(",")[0].strip()  # 左端がオリジナルクライアント

    else:

        ip = request.headers.get("X-Real-IP", remote)

    return ip


def reverse_dns(ip: str) -> str | None:

    try:

        return socket.gethostbyaddr(ip)[0]

    except Exception:

        return None


FORM = """

<!doctype html>

<title>Mini BBS</title>

<h1>新規投稿</h1>

<form method="post">

  <label>Thread ID: <input name="thread_id" type="number" required></label><br>

  <label>Name: <input name="author_name"></label><br>

  <label>Content:<br><textarea name="content" rows="5" cols="60" required></textarea></label><br>

  <button type="submit">Post</button>

</form>

<hr>

<h2>検索(post_id)</h2>

<form method="get">

  <label>post_id: <input name="q" type="number"></label>

  <button type="submit">Lookup</button>

</form>

{% if result %}

  <h3>Result</h3>

  <pre>{{ result }}</pre>

{% endif %}

"""


@app.route("/", methods=["GET", "POST"])

def index():

    db = SessionLocal()

    try:

        if request.method == "POST":

            thread_id   = int(request.form["thread_id"])

            author_name = request.form.get("author_name") or None

            content     = request.form["content"]


            ip = get_client_ip()

            host = reverse_dns(ip)  # 本番は非同期で行う方が安全(ブロッキング回避)

            ua = request.headers.get("User-Agent", "")


            p = Post(thread_id=thread_id, author_name=author_name,

                     content=content, remote_ip=ip, remote_host=host,

                     user_agent=ua)

            db.add(p)

            db.commit()

            return redirect("/")

        else:

            # post_id検索(簡易)

            q = request.args.get("q")

            result = None

            if q:

                row = db.query(Post).filter(Post.post_id==int(q)).first()

                if row:

                    result = {

                        "post_id": row.post_id,

                        "thread_id": row.thread_id,

                        "remote_ip": row.remote_ip,

                        "remote_host": row.remote_host,

                        "created_at": row.created_at.isoformat(),

                    }

                else:

                    result = "not found"

            return render_template_string(FORM, result=result)

    finally:

        db.close()


if __name__ == "__main__":

    app.run(debug=True)


> PostgreSQLでINETを使う場合は、remote_ip列をsqlalchemy.dialects.postgresql.INETに変更し、接続文字列をPostgreSQLにしてください。





---


3) 逆プロキシ(任意)


Nginx等の前段がある場合は、アプリ側が信頼するプロキシからのX-Forwarded-Forのみ採用する設定にしてください。例(抜粋):


location / {

    proxy_set_header Host $host;

    proxy_set_header X-Real-IP $remote_addr;

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # 左端がクライアント

    proxy_pass http://127.0.0.1:5000;

}



---


4) 運用上の注意(重要)


法務:プライバシーポリシーに取得目的・保存期間・第三者提供の有無を明記。日本では「個人情報保護法」「発信者情報開示」に留意。


保存期間:必要最小限に。一定期間で自動削除(例:90日)。


安全対策:


SQLは必ずパラメータ化(本稿のORM例はOK)。


管理画面は認証必須、アクセス制御。


逆引きは非同期で(DNS遅延でリクエストを止めない)。



ヘッダ偽装対策:X-Forwarded-Forは信頼プロキシ経由の時だけ採用。


IPv6:DB型(PostgreSQLのINET推奨)で両対応。


匿名化(用途により):重複検知だけ必要なら、IPのソルト付きハッシュも併存(ただし後で生IPが必要ならハッシュのみ保存は不可)。




---


必要なら、MySQL/SQLite用DDLや、**非同期逆引き(Celery/Redis or ThreadPool)**の雛形、期間で自動削除するSQLジョブの例も用意します。


了解です。先ほどの学習用ミニ掲示板に続けて、MySQL/SQLite用DDL, 非同期の逆引き(reverse DNS), 保存期間での自動削除ジョブの雛形を一気にまとめます。必要に応じてコピペで差し替えてください。



---


1) DDL(スキーマ定義)


1-1. MySQL 8.0 版(IPv4/IPv6を統一格納)


> 推奨: IPは VARBINARY(16) にpacked形式で保存。表示時に INET6_NTOA()、挿入時に INET6_ATON() を使用(IPv4/IPv6どちらも可)。




CREATE TABLE threads (

  thread_id   BIGINT PRIMARY KEY AUTO_INCREMENT,

  title       TEXT NOT NULL,

  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP

) ENGINE=InnoDB;


CREATE TABLE posts (

  post_id      BIGINT PRIMARY KEY AUTO_INCREMENT,

  thread_id    BIGINT NOT NULL,

  author_name  TEXT,

  content      LONGTEXT NOT NULL,

  remote_ip    VARBINARY(16) NOT NULL,  -- IPv4/IPv6 packed

  remote_host  TEXT,

  user_agent   TEXT,

  created_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

  CONSTRAINT fk_posts_threads

    FOREIGN KEY (thread_id) REFERENCES threads(thread_id)

    ON DELETE CASCADE

) ENGINE=InnoDB;


-- よく使うインデックス

CREATE INDEX idx_posts_thread_created ON posts (thread_id, created_at DESC);

CREATE INDEX idx_posts_remote_ip      ON posts (remote_ip);


-- 例: 取得(表示用に文字列へ)

-- SELECT post_id, INET6_NTOA(remote_ip) AS remote_ip, remote_host, created_at

-- FROM posts WHERE post_id = 12345;


-- 例: 挿入(文字列→packedへ変換)

-- INSERT INTO posts(thread_id, content, remote_ip)

-- VALUES (1, 'hello', INET6_ATON('2001:db8::1'));


> 備考: もし MariaDB や 古いMySQL で INET6_* 関数が無い場合は、アプリ側(Python ipaddress)でbytesへ変換して保存・取得時は文字列化してください。





---


1-2. SQLite 版(学習用・軽量)


> SQLiteは型が緩いので、TEXT(45桁程度、INET最大表現)で十分。将来の移行を考えるならBLOBにpackedで保持してもOK。




-- SQLite

CREATE TABLE threads (

  thread_id   INTEGER PRIMARY KEY AUTOINCREMENT,

  title       TEXT NOT NULL,

  created_at  TEXT NOT NULL DEFAULT (datetime('now'))

);


CREATE TABLE posts (

  post_id      INTEGER PRIMARY KEY AUTOINCREMENT,

  thread_id    INTEGER NOT NULL REFERENCES threads(thread_id) ON DELETE CASCADE,

  author_name  TEXT,

  content      TEXT NOT NULL,

  remote_ip    TEXT NOT NULL,   -- 'x.x.x.x' or IPv6 textual

  remote_host  TEXT,

  user_agent   TEXT,

  created_at   TEXT NOT NULL DEFAULT (datetime('now'))

);


CREATE INDEX idx_posts_thread_created ON posts (thread_id, created_at DESC);

CREATE INDEX idx_posts_remote_ip      ON posts (remote_ip);



---


2) 逆引き(reverse DNS)を非同期で行う


2-1. 最小構成(ThreadPoolExecutor, SQLAlchemy)


> 投稿を先に保存して即時応答→ 別スレッドで socket.gethostbyaddr() を実行 → 結果を posts.remote_host に更新。ボトルネックになりにくく、学習用に最適。




# async_reverse_dns.py

from concurrent.futures import ThreadPoolExecutor

import socket

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker

from models import Post, Base  # あなたの定義に合わせて


engine = create_engine("sqlite:///bbs.db", future=True)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)


executor = ThreadPoolExecutor(max_workers=8)


def reverse_dns(ip: str) -> str | None:

    try:

        return socket.gethostbyaddr(ip)[0]

    except Exception:

        return None


def submit_reverse_dns(post_id: int, ip: str):

    def _job():

        host = reverse_dns(ip)

        if host:

            db = SessionLocal()

            try:

                row = db.query(Post).filter(Post.post_id == post_id).first()

                if row and not row.remote_host:

                    row.remote_host = host

                    db.commit()

            finally:

                db.close()

    executor.submit(_job)


> 使い方(前回のFlask例に組み込み):


1. 投稿をDBに保存→ db.commit() で post_id が確定



2. submit_reverse_dns(new_post.post_id, ip) を呼ぶ(応答はブロックしない)






# app.py のPOST処理直後

from async_reverse_dns import submit_reverse_dns


# 保存後

db.commit()

submit_reverse_dns(p.post_id, ip)


> 本番規模なら Celery + Redis(またはRQ, Dramatiq)などのワーカーを推奨。以下はCelery最小例。




2-2. Celery最小例(任意)


# celery_app.py

from celery import Celery

import socket

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker

from models import Post


celery = Celery(__name__, broker="redis://localhost:6379/0", backend="redis://localhost:6379/1")

engine = create_engine("postgresql+psycopg2://user:pass@localhost:5432/bbs", future=True)

SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)


@celery.task(name="tasks.reverse_dns")

def reverse_dns_task(post_id: int, ip: str):

    try:

        host = socket.gethostbyaddr(ip)[0]

    except Exception:

        host = None

    if host:

        db = SessionLocal()

        try:

            row = db.query(Post).filter(Post.post_id==post_id).first()

            if row and not row.remote_host:

                row.remote_host = host

                db.commit()

        finally:

            db.close()


# app.py から呼び出し

from celery_app import reverse_dns_task

db.commit()

reverse_dns_task.delay(p.post_id, ip)



---


3) 保存期間(データ保持)ポリシーと自動削除ジョブ


3-1. MySQL Event Scheduler(DBだけで完結)


> MySQLのイベントスケジューラをONにして、90日より古い投稿を毎日削除の例。




-- 有効化(グローバル設定が必要な場合あり)

SET GLOBAL event_scheduler = ON;


DROP EVENT IF EXISTS ev_purge_old_posts;


CREATE EVENT ev_purge_old_posts

ON SCHEDULE EVERY 1 DAY

DO

  DELETE FROM posts

  WHERE created_at < (CURRENT_TIMESTAMP - INTERVAL 90 DAY);


> threads テーブルも「子が無い古いスレを掃除」したい場合は追加で実装(LEFT JOINして子無しを消す等)してください。




3-2. PostgreSQL(アプリ or 拡張)


アプリ側のcron(例: systemd timer や cron)で定期的に実行するSQL:



DELETE FROM posts

WHERE created_at < now() - INTERVAL '90 days';


pg_cron拡張が使えるならDB内スケジュールも可(クラウドPGだと不可のこと多し)。



3-3. SQLite(アプリ側で実施)


> cron やアプリの起動時・深夜バッチで定期実行:




# purge_sqlite.py

from sqlalchemy import create_engine, text


engine = create_engine("sqlite:///bbs.db", future=True)

with engine.begin() as conn:

    conn.execute(text("""

        DELETE FROM posts

        WHERE created_at < datetime('now', '-90 days')

    """))



---


4) 追加:IPの匿名化/重複検知のためのハッシュ併存(任意)


> 生IPを保存する場合でも、重複検知や集計だけにハッシュ値を使うと便利。再識別を防ぐにはソルトを必ず使用。法的要件次第では生IPの保存期間を短期、ハッシュを長期、などの運用も。




スキーマ追加(MySQL例)


ALTER TABLE posts ADD COLUMN ip_hash VARBINARY(32) NULL;  -- SHA-256(32 bytes)

CREATE INDEX idx_posts_ip_hash ON posts (ip_hash);


挿入時(Python)


import hmac, hashlib, os


SECRET_SALT = os.environ.get("IP_HASH_SALT", "dev-salt-change-me").encode()


def ip_hash(ip_text: str) -> bytes:

    return hmac.new(SECRET_SALT, ip_text.encode(), hashlib.sha256).digest()


# 保存時:

p.ip_hash = ip_hash(ip_string_for_hashing)  # IPv4/IPv6文字列を統一表記で


> 注意: 将来、生IPを削除した後も同一投稿者の推定(同一IPからの連投)などの統計は、ip_hash で継続可能。ただし「個人識別」に使わない範囲で。


---


5) 取得用クエリの再掲(MySQL)

post_id からIP/ホスト取得



SELECT

  p.post_id,

  INET6_NTOA(p.remote_ip) AS remote_ip,

  p.remote_host,

  p.created_at

FROM posts p

WHERE p.post_id = 12345;


本文キーワード



SELECT

  p.post_id, p.created_at,

  INET6_NTOA(p.remote_ip) AS remote_ip,

  p.remote_host

FROM posts p

WHERE p.content LIKE CONCAT('%', ?, '%');  -- ←必ずプレースホルダで


期間・スレ絞り込み



SELECT p.post_id, INET6_NTOA(p.remote_ip) AS remote_ip, p.remote_host, p.created_at

FROM posts p

WHERE p.thread_id = ?

  AND p.created_at BETWEEN ? AND ?;



---


最後に(運用メモ)


法務: 取得目的、保存期間、第三者提供の可否、開示請求への対応方針をプライバシーポリシーに記載。


セキュリティ: 管理画面の認証・権限、IP取得のプロキシ考慮(信頼プロキシ限定の X-Forwarded-For)、SQLのパラメータ化徹底。


性能: 逆引きは非同期。DNSタイムアウトは短めに。投稿スパム対策(レート制限、CSRF、captcha等)も検討。


移行性: 文字列表現(TEXT)とpacked(VARBINARY/BLOB)をどちらも扱えるよう、アプリ層で吸収しておくと後々楽です。




---


他にも、全文検索(MySQL InnoDB FTS / PostgreSQL tsvector / SQLite FTS5) や 監査ログ(誰がいつ見たか) の雛形が要れば、追加します。



コメント

このブログの人気の投稿

Pydroid用ピアノアプリ+ラフマニノフピアノ協奏曲第2番1楽章デモ音源付き

最高裁での上告理由書受理・却下の判断基準について

訴えの変更(追加的変更)申立