人物・会社・取引のIDと関連を保持して持ち出す図

CRMを解約する前に:エクスポートCSVのID・関連付けを照合する

CSVが同じ件数でも、顧客の欠落や会社との誤関連は残ります。CRMの乗換え前に持ち出す範囲を決め、Python標準ライブラリでIDと関連付けを照合する方法を架空データで検証しました。

CRMを乗り換えるとき、「CSVをダウンロードできた」「画面と同じ2件だった」だけで元のアカウントを閉じると、移行先で必要な情報が足りないことがあります。別の2人が入っていても件数は同じですし、顧客の会社IDだけが入れ替わっていても行数は変わりません。

この記事は、小規模チームの移行担当者が持ち出したファイルを検収するための手順です。対象範囲の台帳とCSVを照合し、元の環境へのアクセスを終える前に何を確かめるかを決めます。新しいCRMへ取り込む前の工程で、CSVインポートの受入チェックとは役割を分けています。

検証したのはPythonの照合コードと架空CSVだけです。PipedriveやHubSpotの実アカウントからエクスポートしていません。アカウント全体のバックアップ、添付ファイルの回収、移行の成功を保証するものではありません。

「何を出したか」をファイルと一緒に残す

先に、対象・条件・確認者を1枚にまとめます。比較元と出力の間にデータが更新されると、欠落ではなく時点の違いを検出することもあります。操作時刻と絞り込み条件をそろえ、差分を説明できる状態にしてください。

  • 対象:顧客、会社、商談、活動、メモ、添付など。今回含めないものも書く。
  • 範囲:全件か特定の担当者・期間・状態か。削除済み・アーカイブ済みの扱いも別に決める。
  • 権限:誰が出力したか、その担当者に見える範囲で十分か。
  • 形式:文字コード、列名、ID、関連先のID、複数関連の表現方法。
  • 保管:許可された保存先、確認用コピー、保管期限、確認者。個人情報を公開の検証サービスへ送らない。

Pipedriveの公式エクスポート手順は、出力権限と可視範囲を区別しています。一覧の出力ではフィルター・表示列の確認が必要で、活動・メモ・ファイルは別途出力する対象です。Google Drive保存ファイルの扱いにも例外があります。顧客CSVが取れたことを「関連資料も回収できた」と読み替えないでください。

HubSpotの公式レコード出力手順では、現在のプロパティ値と関連付けを出力します。ビューの選択、含めるプロパティ・関連付けの設定、活動データの別の出力方法を確認します。通常のレコード出力を、すべての変更履歴や活動の保存と同一視しないことが大切です。

製品仕様の確認日:2026年10月11日。権限・プラン・出力方式は実際の利用環境で確認してください。この記事では解約操作や契約条件の判定は行いません。

件数を合わせる前に、照合するキーを決める

ここでは「担当者1人につき会社1社」という小さな検証用モデルに限定します。以下の3ファイルを用意します。列名はこの記事の検証用であり、製品の出力列をそのまま使える共通仕様ではありません。必要なら確認用コピーで列を対応付け、元ファイルは保持します。

  • expected.csv:出力対象として合意した担当者IDと会社IDの台帳。
  • exported.csv:持ち出した担当者データ。
  • companies.csv:持ち出した会社データのID一覧。

expected.csvを検査対象のexported.csvから作ってはいけません。同じ欠落を両方に写すと検出できないからです。元環境の対象一覧など、別途確かめた基準を使います。元環境のIDと移行先が新規発行するIDは別物なので、IDが変わる場合は別途対応表が必要です。

最小の例は次のとおりです。実際の顧客情報は使っていません。

person_id,company_id
001,A
002,B

会社一覧は次の2行です。先頭ゼロのあるIDは数値に変換せず文字列として扱います。

company_id
A
B

Pythonで「同じ2行」の中身を確かめる

以下をcheck_export.pyとして保存します。Python 3の標準ライブラリのみで動き、ネットワーク送信やファイルの書き換えは行いません。出力するのは不一致の件数だけです。

import csv
import json
import sys
from collections import Counter

def read_rows(path, required):
    with open(path, encoding='utf-8-sig', newline='') as handle:
        reader = csv.DictReader(handle)
        fields = reader.fieldnames or []
        if not fields or any(not name.strip() for name in fields):
            raise ValueError(f'{path}: blank column name')
        if len(fields) != len(set(fields)):
            raise ValueError(f'{path}: duplicate column names')
        if not set(required).issubset(fields):
            raise ValueError(f'{path}: required columns missing')
        rows = list(reader)
    if any(None in row or any(row[k] is None for k in required) for row in rows):
        raise ValueError(f'{path}: malformed row')
    return rows

def audit(expected_path, exported_path, companies_path):
    expected = read_rows(expected_path, ['person_id', 'company_id'])
    exported = read_rows(exported_path, ['person_id', 'company_id'])
    companies = read_rows(companies_path, ['company_id'])
    expected_ids = [r['person_id'] for r in expected]
    if any(not value for value in expected_ids) or len(set(expected_ids)) != len(expected_ids):
        raise ValueError('expected manifest needs unique, nonempty person_id')
    wanted = {r['person_id']: r['company_id'] for r in expected}
    company_ids = {r['company_id'] for r in companies if r['company_id']}
    counts = Counter(r['person_id'] for r in exported if r['person_id'])
    seen = set(counts)
    checks = {
        'rows': len(exported),
        'blank_person_ids': sum(not r['person_id'] for r in exported),
        'duplicate_rows_by_id': sum(n - 1 for n in counts.values()),
        'missing_ids': len(set(wanted) - seen),
        'unexpected_ids': len(seen - set(wanted)),
        'unknown_company_refs': sum(bool(r['company_id']) and r['company_id'] not in company_ids for r in exported),
        'changed_company_refs': sum(r['person_id'] in wanted and r['company_id'] != wanted[r['person_id']] for r in exported)
    }
    checks['pass'] = all(value == 0 for key, value in checks.items() if key != 'rows')
    return checks

if __name__ == '__main__':
    if len(sys.argv) != 4:
        print('usage: check_export.py expected.csv exported.csv companies.csv', file=sys.stderr)
        sys.exit(2)
    try:
        checks = audit(*sys.argv[1:])
    except (ValueError, OSError, UnicodeError, csv.Error) as error:
        print(f'input error: {error}', file=sys.stderr)
        sys.exit(2)
    print(json.dumps(checks, ensure_ascii=False, indent=2))
    sys.exit(0 if checks['pass'] else 1)

実行例:

python3 check_export.py expected.csv exported.csv companies.csv

2026年10月16日訂正: 同名の列が複数あるCSVを旧コードに与えると、後ろの列だけで照合して合格するケースがありました。現在のコードは3ファイルすべてで列名の重複・空欄を検出して停止します。元CSVを保持したまま出力設定や列の対応付けを確認し、どの列か不明なまま削除して通さないでください。

Python公式のDictReader説明のとおり、行の値は列名をキーにした辞書へ対応付けられます。この照合では、その前提となる列名の一意性を先に検査します。

終了コードは、0=この照合で不一致なし、1=照合した結果に不一致あり、2=入力・読み込みの問題で判定できないです。以前はpass: falseでも終了コードが0でした。シェルや後続処理で使う場合はJSONだけでなく終了コードも確認し、1や2で移行完了扱いにしないでください。0でも、以下に挙げる検査対象外の確認は必要です。Pythonの終了処理を使って区別しています。

pass: trueは、この小さなモデルのIDと会社関連について不一致を検出しなかった、という意味です。会社一覧自体の完全性、連絡先の氏名・住所の正しさ、メール履歴、複数社への関連付け、添付内容は判定しません。業務上必要な項目は別の照合に追加してください。台帳側の会社IDが正しいかも先に確認します。

架空データで確認できたこと

2026年10月11日、macOS上で5ケースを実行しました。どのケースも出力CSVは2行です。正常ケースにはカンマ・改行を含む引用符付きフィールド、UTF-8 BOM、先頭ゼロのIDを含め、単純な行分割を使わず読み取れることも確認しています。

スマートフォンでは表を横にスクロールできます。

出力側に入れた差分検出結果合否
基準と同じID・会社関連不一致0件合格
002を003に置換欠落ID 1、予定外ID 1不合格
001を2回入れるID重複行 1、欠落ID 1不合格
002の会社を存在しないZにする不明な会社参照 1、関連変更 1不合格
002の会社を存在するAに変える関連変更 1不合格

必須のperson_id列がないファイルも別途与え、合格扱いではなくエラーで停止することを確認しました。「会社IDが会社一覧に存在する」だけでは最後のケースを見逃すため、基準台帳との組合せまで照合しています。

元環境を閉じる判断は、照合結果より広い

不一致があったら、すぐ出力を上書きせず、比較した2つのファイルと対象条件を残します。原因を「出力範囲」「権限」「元データの更新」「列の対応付け」「検証コードの前提」に分けると、ベンダーへの質問も具体的になります。

ID照合が通った後も、営業担当者と一緒にサンプルの顧客を開き、本文のあるメモ・活動日時・添付ファイルの実体まで業務上必要な形で読めるかを確認してください。ファイルへのリンクだけを保存した場合、元のサービスへアクセスできなくなっても読めるとは限りません。

完了条件は「このコードが合格」ではなく、次の3点を担当者が説明できることです。

  1. 必要なデータの種類と範囲を持ち出せたか。持ち出せない対象を把握したか。
  2. 移行先または保存先で必要な情報を読み、関連付けを復元できるか。
  3. 最終差分の回収・保管・不一致修正を誰がいつまで担当するか。

新しいCRMを契約する前なら、この検収条件を候補の窓口に渡し、必要なエクスポート機能・権限が見積もりに含まれるか確認できます。Pipedriveの公式プランとHubSpot Sales Hubの公式プランは、その条件を確認する入口です。安さや「CSV対応」という表示だけで持ち出し可能性を判定せず、未確認項目が残るなら契約・切替の判断材料として明示してください。

検収で見つかった制約は、候補CRMを選ぶ条件に戻しましょう。たとえば「活動・添付を別途回収する手間」や「必要な出力権限を使えるプラン」を比較表の確認欄に残せば、機能名や月額だけでは見えない移行の負担を判断できます。スタートアップ向けCRMの比較を読むときも、この確認欄と営業体制を合わせて候補を絞り、未確認の項目は候補サービスへ質問してください。

← 記事一覧に戻る