웹서버를 만들고 그누보드를 설치하고, 크롤러는 클라이언트에서 만들어서 글쓰기를 하려고하는 기획단계
작성자 정보
- 삼둡 작성
- 작성일
컨텐츠 정보
- 12 조회
- 목록
본문
멋진 프로젝트입니다 — ac***xa.net에 **그누보드5(또는 REST API 가능한 최신 브랜치)**를 올리고, 로컬(또는 VM) Ubuntu에서 파이썬 크롤러로 특정 한의원 글들을 주기적으로 수집해 자동으로 올리는 전 과정을 단계별로 정리하고, 실제로 바로 쓸 수 있는 예제 코드까지 드립니다. (긴 글이지만 전부 한 번에 제공합니다 — 바로 복사/붙여넣기 하셔도 됩니다.)
요약 아키텍처
도메인(www.ac***xa.net) → CentOS 서버(Apache/Nginx + PHP + MariaDB) : 그누보드(g5) 호스팅
Ubuntu VM (파이썬) : 크롤러 → 변환/중복 제거 → 그누보드 REST API (권장) 또는 폼 작성(대체)로 게시글 업로드
1) 사전준비 · 핵심 권장사항
그누보드5는 리눅스 + Apache/Nginx + PHP + MySQL(MariaDB) 조합에서 동작합니다. 최신권장 환경(예: PHP 8.x, MariaDB 10.5+/MySQL 8)으로 세팅하세요. 설치·환경 관련 문서(요구 버전/필수 확장 등)는 공식 매뉴얼을 참고하세요. 그누보드
그누보드 최신 소스/릴리스 및 업데이트는 공식 GitHub에서 관리됩니다 — 설치 시 공식 repo/릴리스 사용을 권합니다. GitHub
REST API 사용 권장 — 그누보드(최신/개발 브랜치)는 JWT 기반 REST API를 지원합니다. API를 활성화하면 인증(JWT) → 게시글 등록이 훨씬 안정적이고 보안적입니다. (아래에 예제 포함) 그누보드
보안 주의: 오래된 그누보드는 알려진 취약점이 있으니(예: CVE 등) 설치 후 반드시 최신 패치/업데이트 적용하세요. 자동 백업·업데이트 계획을 권장합니다. NVD
크롤링은 robots.txt 및 사이트 이용약관을 존중해야 합니다. 수집 대상 사이트의 robots 규칙과 크롤링 정책을 확인하고, 과도한 요청(서버 부하)을 주지 않도록 지연, 헤더, rate-limit를 적용하세요. Google for Developers
2) 서버(CentOS)에 그누보드 설치 — 빠른 가이드 (명령 예)
(아래는 CentOS 8/Stream 계열을 기준으로 한 빠른 설치 흐름 — 버전/OS에 따라 패키지명 차이 있을 수 있음.)
# 1. 시스템 업데이트 sudo dnf update -y # 2. Apache, MariaDB, PHP 및 필수 확장 설치 sudo dnf install -y httpd mariadb-server php php-cli php-fpm php-mysqlnd php-gd php-mbstring php-xml php-json php-curl unzip git # 3. 서비스 시작 및 자동실행 sudo systemctl enable --now httpd mariadb php-fpm # 4. MariaDB 보안 설정 (root 비밀번호 등) sudo mysql_secure_installation # 5. DB 생성 (예) mysql -u root -p -e "CREATE DATABASE a***xa CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'acumo'@'localhost' IDENTIFIED BY 'StrongPasswordHere'; GRANT ALL ON acumoxa.* TO 'acumo'@'localhost'; FLUSH PRIVILEGES;"
소스 배치 & 권한:
cd /var/www/html sudo git clone https://github.com/gnuboard/gnuboard5.git g5 cd g5 sudo mkdir -p data sudo chmod 707 data # 설치문서 권장(설치 중 생성한 data 폴더 퍼미션 참고). :contentReference[oaicite:5]{index=5} sudo chown -R apache:apache /var/www/html/g5 # CentOS Apache user는 보통 'apache'
Apache vhost 예 (/etc/httpd/conf.d/ac***xa.conf):
<VirtualHost *:80> ServerName acumoxa.net ServerAlias www.ac***xa.net DocumentRoot /var/www/html/g5 <Directory /var/www/html/g5> AllowOverride All Require all granted </Directory> ErrorLog /var/log/httpd/acumoxa-error.log CustomLog /var/log/httpd/acumoxa-access.log combined </VirtualHost>
HTTPS: certbot으로 Let's Encrypt 인증서 발급 후 Apache 플러그인으로 설정하세요.
설치/초기 화면은 브라우저로 접속하면 웹 설치 마법사가 안내합니다(매뉴얼 참고). 그누보드
3) REST API 활성화 (권장)
그누보드의 REST API를 활성화하면 /api/v1/... 형태로 JWT 인증을 받고 게시글을 등록할 수 있습니다. 일반 절차:
서버의 API 설정(.env 또는 api/settings.py)을 편집: USE_API=True, JWT SECRET 키 생성(예: openssl rand -hex 32) 등.
서버 재시작 후 Swagger UI(예: https://your-site/api/docs 또는 /docs)에서 엔드포인트와 파라미터 확인.
API로 token 발급(/api/v1/token), 이후 Authorization: Bearer {access_token} 헤더로 게시글 등록 API 호출. 그누보드
(실제 엔드포인트/필드 이름은 서버 버전/설치 옵션에 따라 달라질 수 있으니, Swagger UI로 정확한 스펙을 확인하세요.)
4) 크롤러(파이썬) 설계 — 핵심 원칙
합법성/예의: robots.txt 확인 → 이용약관/저작권 확인 → 출처 표기 또는 원문 링크 제공.
중복 방지: 수집 결과를 SQLite(또는 Redis)로 해시·저장(예: SHA256(title+content)) — 이미 수집된 경우 게시하지 않음.
폴리티니스: 사이트당 지연(예: 1~5초 랜덤), 병렬은 매우 조심.
이미지 처리: 이미지 직접 다운로드 후 gnuBoard에 업로드(또는 콘텐츠에 원문 링크 포함). 핫링크는 권장하지 않음.
관리자 승인 큐: 자동으로 바로 공개하기보단 임시대기(관리자 검토) 옵션을 두는 것을 권장합니다(법적·윤리적 이유).
로그/알림: 실패시 이메일/슬랙 알림 및 로깅.
5) 파이썬 예제 코드 (실전 사용 가능한 템플릿)
아래는 최소한의 완전한 예제(REST API 우선, 없으면 폼 방식으로 대체).
설치 라이브러리:
python3 -m venv venv && source venv/bin/activate pip install requests beautifulsoup4 lxml pyyaml python-dotenv bleach
config.yaml (예: 수집 대상을 선언)
base_url: "https://www.ac***xa.net" # gnuBoard 사이트 주소 api: enable: true user: "auto_poster" password: "strongpw" scraper: user_agent: "AcumoBot/1.0 (+https://ac***a.net/bot)" timeout: 10 sites: - name: saengmyeongmaru list_url: "https://www.saengmyeongmaru.co.kr/board/list" article_link_selector: ".post-list a" # 각 사이트에 맞게 조정 title_selector: "h1.title" content_selector: "div.content" rate_limit_seconds: 2 bo_table: "clinic_news" # gnuBoard 게시판 ID
scraper.py (요약된 전체 흐름 — 실제 사용시 CSS 셀렉터/사이트 구조를 config에 맞춰 수정)
#!/usr/bin/env python3 import time, hashlib, sqlite3, yaml, os, logging from urllib.parse import urljoin, urlparse from urllib.robotparser import RobotFileParser import requests from bs4 import BeautifulSoup from dotenv import load_dotenv import bleach load_dotenv() # for secrets if used LOG = logging.getLogger("scraper") logging.basicConfig(level=logging.INFO) # DB (단순 sqlite for dedupe) DB = 'posts.db' conn = sqlite3.connect(DB) conn.execute('''CREATE TABLE IF NOT EXISTS posted ( id INTEGER PRIMARY KEY, source_url TEXT UNIQUE, content_hash TEXT, posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') conn.commit() def can_fetch(url, user_agent): parsed = urlparse(url) robots = f"{parsed.scheme}://{parsed.netloc}/robots.txt" rp = RobotFileParser() rp.set_url(robots) try: rp.read() except Exception: return True return rp.can_fetch(user_agent, url) def fetch(url, headers, timeout=10): r = requests.get(url, headers=headers, timeout=timeout) r.raise_for_status() return r.text def make_hash(title, content): h = hashlib.sha256() h.update((title + "\n" + content).encode('utf-8')) return h.hexdigest() # REST API auth def get_api_token(base_url, user, pw, session, headers): token_url = f"{base_url.rstrip('/')}/api/v1/token" data = {'grant_type':'password','username':user,'password':pw} r = session.post(token_url, data=data, headers=headers, timeout=10) r.raise_for_status() return r.json()['access_token'] def post_via_api(base_url, token, bo_table, title, content, session): # 실제 엔드포인트/필드 이름은 swagger에서 확인하세요. url = f"{base_url.rstrip('/')}/api/v1/boards/{bo_table}/posts" headers = {'Authorization': f'Bearer {token}', 'Content-Type':'application/json'} payload = { "wr_subject": title, "wr_content": content, "html": "1" # html 여부 } r = session.post(url, json=payload, headers=headers, timeout=15) r.raise_for_status() return r def fallback_post_form(base_url, bo_table, title, content, session, headers): # write 페이지에서 숨겨진 토큰(?) 및 form action 추출 -> 그 값으로 POST write_url = f"{base_url.rstrip('/')}/bbs/write.php?bo_table={bo_table}" r = session.get(write_url, headers=headers, timeout=10) r.raise_for_status() soup = BeautifulSoup(r.text, 'lxml') form = soup.find('form', {'name':'fwrite'}) or soup.find('form') hidden = {} for inp in form.find_all('input', {'type':'hidden'}): name = inp.get('name') if name: hidden[name] = inp.get('value', '') data = hidden data.update({ 'wr_subject': title, 'wr_content': content, 'html': '1', 'w': '' }) action = form.get('action') or '/bbs/write_update.php' if not action.startswith('http'): action = urljoin(base_url, action) r2 = session.post(action, data=data, headers=headers, timeout=15) r2.raise_for_status() return r2 def main(): with open('config.yaml','r') as f: cfg = yaml.safe_load(f) sess = requests.Session() headers = {'User-Agent': cfg['scraper']['user_agent']} base_url = cfg['base_url'] token = None if cfg.get('api', {}).get('enable'): try: token = get_api_token(base_url, cfg['api']['user'], cfg['api']['password'], sess, headers) LOG.info("API token acquired") except Exception as e: LOG.warning("API auth failed: %s", e) token = None for site in cfg['sites']: list_url = site['list_url'] if not can_fetch(list_url, cfg['scraper']['user_agent']): LOG.warning("robots.txt disallows %s, skipping", list_url) continue html = fetch(list_url, headers, timeout=cfg['scraper'].get('timeout',10)) soup = BeautifulSoup(html, 'lxml') links = [urljoin(list_url, a.get('href')) for a in soup.select(site['article_link_selector'])] for link in links: if not can_fetch(link, cfg['scraper']['user_agent']): LOG.info("robots denied %s", link); continue # 이미 수집했는지 체크 cur = conn.execute("SELECT 1 FROM posted WHERE source_url = ?", (link,)).fetchone() if cur: LOG.info("already processed %s", link); continue art_html = fetch(link, headers, timeout=cfg['scraper'].get('timeout',10)) a_soup = BeautifulSoup(art_html, 'lxml') title = a_soup.select_one(site['title_selector']).get_text(strip=True) content_node = a_soup.select_one(site['content_selector']) content_html = str(content_node) # sanitize safe_html = bleach.clean(content_html, tags=bleach.ALLOWED_TAGS+['img','p','br','div','ul','li'], strip=True) h = make_hash(title, safe_html) # 중복(해시) 체크 dup = conn.execute("SELECT 1 FROM posted WHERE content_hash = ?", (h,)).fetchone() if dup: LOG.info("duplicate content for %s", link) conn.execute("INSERT OR IGNORE INTO posted(source_url, content_hash) VALUES (?,?)",(link,h)) conn.commit() continue # 포스트 전송 (API 우선) try: if token: r = post_via_api(base_url, token, site['bo_table'], title, safe_html, sess) else: r = fallback_post_form(base_url, site['bo_table'], title, safe_html, sess, headers) LOG.info("posted %s => %s", link, r.status_code) conn.execute("INSERT OR IGNORE INTO posted(source_url, content_hash) VALUES (?,?)",(link,h)) conn.commit() except Exception as e: LOG.exception("failed to post %s : %s", link, e) time.sleep(site.get('rate_limit_seconds', 2)) if __name__ == "__main__": main()
위 예제는 템플릿입니다. 실제 사이트 구조(셀렉터), API 엔드포인트, 파라미터명은 여러분 서버의 Swagger/문서와 대상 사이트 구조를 확인해 맞춰야 합니다. REST API 활성화 시 토큰 발급 경로(/api/v1/token)와 게시글 등록 경로는 반드시 swagger에서 확인하세요. 그누보드
6) 스케줄링(자동화)
간단: cron에 넣기
*/15 * * * * /home/ubuntu/venv/bin/python /home/ubuntu/scraper/scraper.py >> /var/log/acumo_scraper.log 2>&1
권장(더 견고): systemd 서비스 + timer로 관리 (재시작/로그 관리 용이).
7) 이미지 업로드 전략
간단: 원문 이미지를 <img src="원문URL">로 넣는 방법 — 핫링크(권장 X, 저작권/서버부하 문제).
권장: 이미지 다운로드 → GnuBoard에 파일 첨부(REST API의 파일 업로드 엔드포인트 또는 form multipart)로 업로드 → 업로드된 파일 URL을 본문에 포함. (파일 업로드는 multipart/form-data로 bf_file[] 같은 이름을 쓰는 경우가 많음 — 서버측 스펙 확인 필요)
이미지 저장 시 파일명 충돌/용량제한/확장자 검증 필요.
8) 운영·보안 체크리스트 (중요)
gnuBoard와 플러그인/테마를 주기적으로 업데이트하고, 보안 공지(CVE 등)를 모니터링하세요. NVD
DB 정기 백업(일/주), g5/data 폴더(첨부파일) 백업.
HTTPS 적용(모든 API 호출에 TLS 사용).
크롤러에 API/DB 비밀번호를 .env나 Linux systemd 환경변수로 보관(코드 내 평문 금지).
크롤링 시 반드시 robots.txt를 체크하고, 과도한 병렬 요청은 피하세요(서버 부하 방지). Google for Developers
9) 법적·윤리적 고려사항 (반드시 읽기)
다른 병원/사이트 글을 무단으로 전재(전문 복제) 하면 저작권 문제가 발생할 수 있습니다. 가능하면 (a) 원문 링크 + 요약(내 말로 요약) + 출처 표기, 또는 (b) 사전 동의(또는 RSS 제공/신디케이션 허가)를 받는 것이 안전합니다.
개인정보(환자 정보 등)가 포함된 게시물은 절대 수집·게시하지 마세요.
10) 테스트 · 배포 권장 순서 (권장)
로컬 개발환경(또는 별도 staging 서브도메인)에서 gnuBoard 설치 및 API 활성화.
크롤러를 --dry-run 모드로 돌려서 어떤 데이터를 추출하는지 확인.
관리자 승인(임시대기) 게시판으로 먼저 올려 운영자가 확인 → 이상 없으면 자동 공개로 전환.
운영 모드로 이관 시 모니터링/로그 알림을 켜두세요.
11) 다음 단계 제안 (원하시면 제가 바로 해드립니다)
(A) CentOS 서버 접속 정보(포트/권한 없이 민감정보는 제외) 없이도, 제가 당장 사용하실 수 있는 설치 스크립트(Ansible playbook 또는 셸 스크립트)와 Apache vhost 파일, certbot 설치 스텝을 파일 형태로 만들어 드릴게요.
(B) 또는 config.yaml에 대상 사이트(생명마루한의원 등)의 실제 URL과, 그 사이트의 list/article 셀렉터를 알려주시면 제가 해당 사이트에 맞춘 크롤러 파싱 코드(정밀한 CSS/XPath 포함)를 만들어 드리겠습니다.
원하시면 어느 쪽(A 또는 B)을 바로 만들어 드릴까요? (선택하시면 해당 파일/스크립트/코드를 바로 드리겠습니다.)
참고(주요 문서)
그누보드 GitHub (소스 & 릴리스). GitHub
그누보드 설치 가이드(매뉴얼) — 설치 환경 · data 권한 등. 그누보드
그누보드 REST API 시작 가이드 (JWT 인증, Swagger 등). 그누보드
알려진 취약점(CVE) · 보안 패치 확인 중요성. NVD
robots.txt(크롤링 규칙) 기본 안내(Google Search Central). Google for Developers
원하시면 지금 바로:
CentOS 설치/Apache vhost/SELinux/Let’s Encrypt 자동화 스크립트 파일 하나 만들어 드리거나,
대상 사이트(예: 생명마루한의원)의 구체 URL을 주시면 그 사이트 맞춤형 파싱 코드(정확한 CSS selector / 이미지 처리 포함)를 만들어 드립니다.
원하시는 작업을 하나 골라 주세요 — 선택하시면 바로 파일/코드까지 한꺼번에 드릴게요. (질문은 피하고 바로 실행 가능한 결과물을 드립니다.)
관련자료
-
이전
-
다음