자유게시판

웹서버를 만들고 그누보드를 설치하고, 크롤러는 클라이언트에서 만들어서 글쓰기를 하려고하는 기획단계

작성자 정보

  • 삼둡 작성
  • 작성일

컨텐츠 정보

본문

멋진 프로젝트입니다 — 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 / 이미지 처리 포함)를 만들어 드립니다.

원하시는 작업을 하나 골라 주세요 — 선택하시면 바로 파일/코드까지 한꺼번에 드릴게요. (질문은 피하고 바로 실행 가능한 결과물을 드립니다.)

 

 

 

 

관련자료

댓글 0
등록된 댓글이 없습니다.

인기검색어


최근글


새댓글