老舗BBSサービス「teacup」が2022年8月1日にサービスを終了した。
1997年のサービス開始から実に25年だ。
これで黒字確保できていた理由は3名の担当者という少数で運営しているためらしい。
サービスが終了したら、通常は次の引越し先を考える。
ただし最近は無料掲示板は減り続け、FC2程度しか思いつかない。
レンタルサーバー上にCGIで掲示板を用意する……という手段もあるけれど、そもそも無料掲示板を使っている訳なので、技術も知識も予算も無いのが現状だ。
掲示板だけでなく、ブログやサイト移動の場合でも次のような手順を踏む。
勿論、過去のデータを移行しない場合は何もしなくても良い。
ただし、上記手順が使えない場合は多々ある。
- そもそもエクスポート機能が存在しない
- 掲示板の管理人パスワードを忘れてしまった
- エクスポートしたファイル形式が新規掲示板のエクスポート形式と異なる
この場合は、ひと手間加える必要がある。
今回、私自身が管理人でない掲示板の移行をしてみたので、参考になればと思い記事にしておく。
実際には「teacup掲示板」から「FC2掲示板」へデータ移動を行った。
なお、FC2掲示板はアカウント作成して既に新規作成されている前提。
①② Pythonによるデータ抽出と形式変換
手動でやるなら一記事毎にコピーしてテキストに貼り付ける。
今回の記事数は5,000件以上あったので、スクレイピングの技術を使った。
PythonでSeleniumを使ってChromeを操作する。
そして該当箇所を抽出した後、移行先の掲示板のエクスポート形式に変換する。
FC2掲示板の投稿記事をエクスポートすると次のような形式だった。
POSTED BY: 投稿者名
SUBJECT: タイトル
EMAIL:
SITE: http://
DATE: 2022-07-23 09:38:07
USER AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
IP: 211.XXX.XXX.XXX
ICON: 0
COLOR: 1
AUTHORIZED: 1こんちにわ。
初投稿です。
———-
掲示板形式に関する情報はFC2オフィシャルサイトには存在しなかった。
が、ブログ形式は存在したので、何となく形式は理解できた。
で実装コードは汚いけど、こんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
def convert_date(self, string): pattern = r'(\d+)年(\s|)(\d+)月(\s|)(\d+)日\(\S\)(\d+)時(\d+)分(\d+)秒' prog = re.compile(pattern) result = prog.match(string) if result: date = result.group(1) + "-" + f'{int(result.group(3)):02}' + "-" + f'{int(result.group(5)):02}' + \ " " + f'{int(result.group(6)):02}' + ":" + f'{int(result.group(7)):02}' + ":" + f'{int(result.group(8)):02}' return date return "" def get_bbs_url(self, url, m): self._br.get(url) html = self._br.page_source reg_obj = re.compile(r"<[^>]*?>") # 切り出す result = html.split('Kiji_Title">') for i in range(1, len(result)): title = result[i].split('',1) tmp = result[i].split('Kiji_Author">',1) name = tmp[1].split('',1) tmp = result[i].split('投稿日:',1) date = tmp[1].split(' <!--',1) data1 = self.convert_date(reg_obj.sub("", date[0])) tmp = result[i].split('Remote Host: ',1) ip = tmp[1].split(', Time:',1) tmp = result[i].split('sans-serif; font-size:medium;">',1) content = tmp[1].split('', 1) with open('export' + str(m) + '.txt', 'a', encoding='UTF-8', newline='\n') as f: f.write("POSTED BY: " + reg_obj.sub("", name[0]) + "\n" + \ "SUBJECT: " + reg_obj.sub("", title[0]) + "\n" + \ "EMAIL: " + "\n" + \ "SITE: http://" + "\n" + \ "DATE: " + data1 + "\n" + \ "USER AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" + "\n" + \ "IP: " + "123.456.789.012\n" + \ "ICON: 0" + "\n" + \ "COLOR: 1" + "\n" + \ "AUTHORIZED: 1" + "\n" + \ "" + "\n" + \ # reg_obj.sub("", content[0]).replace('\n', '\r\n') + "\n" + \ "----------" + "\n") return True |
IPアドレスやブラウザ情報なんて分かるわけないので適当に全員同じとしている。
改行コードを合わせる
テキストデータの抽出までは上手く行ったけど、FC2でインポートできなかったりして、5時間ぐらいかかってしまった……。
一番時間がかかったのは
- ファイル形式は「UTF-8」で改行コードは「\n」
- 書き込みメッセージの改行コードは「CR + LF(\r\n)」
手動で対応しても形式が異なるので、TeraPadが勝手に改行コードを変えてしまっていた。
サクラエディタを使ってみると改行コードの違いによって表示が異なるので判別できる。
改行コード | サクラエディタでの表示 |
---|---|
LF | ↓ |
CR | ← |
CR+LF | ↵ |
また保存時に注意ダイアログが表示されることで気づいた。
と簡単に書いてるけど、ここまで分かるのに時間がかかった……。
③エクスポート
FC2用のインポート形式にできたら、「ファイル選択」→「インポート」という形で進めていく。
が、20MB以上になったファイルをインポートしてみた。
しばらく待って次のエラー
413 Request Entity Too Large
nginx
調べてみるとデフォルトのnginx設定では、
最大アップロードが1MB
になっているらしい。
ユーザには変更権限が無いので、小さなファイル単位にして都度インポートしていくしかない。
が、これを手動でやるのはバカバカしい。
Seleniumを使ってファイルをエクスポートする
FC2のインポート画面より、ファイル選択してパソコン上のローカルファイルを選択し、1MBを超えないように細切れにしたファイルをアップロードさせていく。
XPATHを使って指定することで可能っぽい。
1 2 3 4 5 6 7 8 |
def upload(self, str): elements = self._br.find_element(By.XPATH, "//input[@name='txt'][@type='file']") test_data = "C:\\Users\\Hoge\\Desktop\\ファイル" + str + ".txt" elements.send_keys(test_data) fimport = self._br.find_element_by_name('import') fimport.click() sleep(1) return True |
初めて作ってみたけど、簡単だった。
これでサクサクインポートは終了した。
おわりに
背景は、たまたま、まったく見も知らないシニアの方々のTeacupの掲示板利用グループを見たこと。
シニアA「掲示板の設置方法分からないので、これで解散です」
シニアB「寂しい。最後の1ヶ月毎日投稿するからね。」
私「通りすがりの者です。チャッチャッと新規掲示板作っちゃったよー。使ってねーーー」
とTeacup終了一週間前に自慢気に書き込んでリンクを貼り、過去記事も移行しておいた。
結果、
毎日投稿されていた掲示板への書き込みがピタッと止まって、8月1日が過ぎたww
何かの詐欺だと思ったんだろうね。
イラストにするとこんな感じwww
だって、台湾のコロナでホテル監禁中だったから、やることが無かったんだもん!!
情けは人のためならず
※ 「情けは人のためでなく、自分のため」という意味。日本人の誤用率の高い言葉。
ソースコード
Teacupからのテキストファイルの抽出とFC2形式への変換
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
import re import sys import csv import time import urllib from datetime import datetime # for debug import requests from selenium import webdriver from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import Select import functools print = functools.partial(print, flush=True) _BASE_URL = "https://8311.teacup.com/XXX/bbs" class Teacup: """ Teacupに存在する掲示板情報のパース """ def __init__(self, proxy=None, proxy_user=None, proxy_password=None): """ コンストラクタ :param str proxy: プロキシ :param str proxy_user: プロキシのユーザ名 :param str proxy_password: プロキシのパスワード """ self._proxy = proxy self._proxy_user = proxy_user self._proxy_password = proxy_password # open browser self._br = self._open_browser(_BASE_URL) def __del__(self): """ デストラクタ """ self._br.close() self._br.quit() def _add_url_param(self, url, params): return url + '?' + urllib.parse.urlencode(params) def _get_authorized_webdriver(self, url): """ Go though azure certification :param str url: Opened url(str :return Instance of selenium """ options = webdriver.ChromeOptions() options.add_argument('--headless') options.add_argument('--log-level=3') options.add_argument('--no-sandbox') # Bypass OS security model driver = webdriver.Chrome('chromedriver.exe', options=options) driver.get(url) while True: # print(driver.current_url) if url in driver.current_url: break time.sleep(1) return driver def _open_browser(self, page): """ Open url on selenium :param str url: Opened url(str :return Instance of selenium """ # print(page) driver = self._get_authorized_webdriver(page) session = requests.session() for cookie in driver.get_cookies(): session.cookies.set(cookie["name"], cookie["value"]) return driver # 2022年 7月 9日(土)21時11分35秒 # 2022-07-23 09:38:07 def convert_date(self, string): pattern = r'(\d+)年(\s|)(\d+)月(\s|)(\d+)日\(\S\)(\d+)時(\d+)分(\d+)秒' prog = re.compile(pattern) result = prog.match(string) if result: date = result.group(1) + "-" + f'{int(result.group(3)):02}' + "-" + f'{int(result.group(5)):02}' + \ " " + f'{int(result.group(6)):02}' + ":" + f'{int(result.group(7)):02}' + ":" + f'{int(result.group(8)):02}' return date return "" def get_bbs_url(self, url, m): self._br.get(url) html = self._br.page_source reg_obj = re.compile(r"<[^>]*?>") # 切り出す result = html.split('Kiji_Title">') for i in range(1, len(result)): title = result[i].split('',1) tmp = result[i].split('Kiji_Author">',1) name = tmp[1].split('',1) tmp = result[i].split('投稿日:',1) date = tmp[1].split(' <!--',1) data1 = self.convert_date(reg_obj.sub("", date[0])) tmp = result[i].split('Remote Host: ',1) ip = tmp[1].split(', Time:',1) tmp = result[i].split('sans-serif; font-size:medium;">',1) content = tmp[1].split('', 1) with open('export' + str(m) + '.txt', 'a', encoding='UTF-8', newline='\n') as f: f.write("POSTED BY: " + reg_obj.sub("", name[0]) + "\n" + \ "SUBJECT: " + reg_obj.sub("", title[0]) + "\n" + \ "EMAIL: " + "\n" + \ "SITE: http://" + "\n" + \ "DATE: " + data1 + "\n" + \ "USER AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" + "\n" + \ "IP: " + "211.125.140.87\n" + \ "ICON: 0" + "\n" + \ "COLOR: 1" + "\n" + \ "AUTHORIZED: 1" + "\n" + \ "" + "\n" + \ # reg_obj.sub("", content[0]).replace('\n', '\r\n') + "\n" + \ "----------" + "\n") return True # 購入銘柄毎に購入処理 def main(): start = time.time() for i in range(0, 384): print(i, flush=True) f = open('export' + str(i) + '.txt', 'w', encoding='UTF-8', newline='\n') f.write('') # 何も書き込まなくてファイルは作成されました f.close() # サイトアクセス bbs = Teacup() # BBSデータ入手 url_list = bbs.get_bbs_url(_BASE_URL + "?page=" + str(i+1), i) del bbs elapsed_time = time.time() - start print ("elapsed_time:{0}".format(elapsed_time) + "[sec]") if __name__ == '__main__': main() |