키워드를 사용하고 컨텐츠를 필터링하는 CLI 웹 스파이더를 작성하는 방법


10

더 이상 사용되지 않는 (사용되지 않는) 문헌 포럼 e-bane.net 에서 기사를 찾고 싶습니다 . 일부 포럼 모듈이 비활성화되어 있으며 작성자가 작성한 기사 목록을 얻을 수 없습니다. 또한 사이트는 검색 엔진에서 Google, Yndex 등으로 색인을 생성하지 않습니다.

내 기사를 모두 찾는 유일한 방법 은 사이트 의 아카이브 페이지 를 여는 것입니다 (그림 1). 그런 다음 특정 연도 및 월을 선택해야합니다 ( 예 : 2013 년 1 월 (그림 1)). 그런 다음 처음에 내 별명 -pa4080 (그림 3)으로 작성되어 있는지 여부를 각 기사 (그림 2)를 검사해야합니다 . 그러나 수천 건의 기사가 있습니다.

여기에 이미지 설명을 입력하십시오

여기에 이미지 설명을 입력하십시오

여기에 이미지 설명을 입력하십시오

다음과 같이 몇 가지 주제를 읽었지만 내 솔루션에 맞는 솔루션은 없습니다.

내 솔루션 을 게시 할 것 입니다. 그러나 나에게는 흥미 롭습니다. 이 작업을 해결하는 더 우아한 방법이 있습니까?

답변:


3

script.py:

#!/usr/bin/python3
from urllib.parse import urljoin
import json

import bs4
import click
import aiohttp
import asyncio
import async_timeout


BASE_URL = 'http://e-bane.net'


async def fetch(session, url):
    try:
        with async_timeout.timeout(20):
            async with session.get(url) as response:
                return await response.text()
    except asyncio.TimeoutError as e:
        print('[{}]{}'.format('timeout error', url))
        with async_timeout.timeout(20):
            async with session.get(url) as response:
                return await response.text()


async def get_result(user):
    target_url = 'http://e-bane.net/modules.php?name=Stories_Archive'
    res = []
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, target_url)
        html_soup = bs4.BeautifulSoup(html, 'html.parser')
        date_module_links = parse_date_module_links(html_soup)
        for dm_link in date_module_links:
            html = await fetch(session, dm_link)
            html_soup = bs4.BeautifulSoup(html, 'html.parser')
            thread_links = parse_thread_links(html_soup)
            print('[{}]{}'.format(len(thread_links), dm_link))
            for t_link in thread_links:
                thread_html = await fetch(session, t_link)
                t_html_soup = bs4.BeautifulSoup(thread_html, 'html.parser')
                if is_article_match(t_html_soup, user):
                    print('[v]{}'.format(t_link))
                    # to get main article, uncomment below code
                    # res.append(get_main_article(t_html_soup))
                    # code below is used to get thread link
                    res.append(t_link)
                else:
                    print('[x]{}'.format(t_link))

        return res


def parse_date_module_links(page):
    a_tags = page.select('ul li a')
    hrefs = a_tags = [x.get('href') for x in a_tags]
    return [urljoin(BASE_URL, x) for x in hrefs]


def parse_thread_links(page):
    a_tags = page.select('table table  tr  td > a')
    hrefs = a_tags = [x.get('href') for x in a_tags]
    # filter href with 'file=article'
    valid_hrefs = [x for x in hrefs if 'file=article' in x]
    return [urljoin(BASE_URL, x) for x in valid_hrefs]


def is_article_match(page, user):
    main_article = get_main_article(page)
    return main_article.text.startswith(user)


def get_main_article(page):
    td_tags = page.select('table table td.row1')
    td_tag = td_tags[4]
    return td_tag


@click.command()
@click.argument('user')
@click.option('--output-filename', default='out.json', help='Output filename.')
def main(user, output_filename):
    loop = asyncio.get_event_loop()
    res = loop.run_until_complete(get_result(user))
    # if you want to return main article, convert html soup into text
    # text_res = [x.text for x in res]
    # else just put res on text_res
    text_res = res
    with open(output_filename, 'w') as f:
        json.dump(text_res, f)


if __name__ == '__main__':
    main()

requirement.txt:

aiohttp>=2.3.7
beautifulsoup4>=4.6.0
click>=6.7

다음은 python3 버전의 스크립트입니다 (Ubuntu 17.10의 python3.5에서 테스트 ).

사용하는 방법:

  • 그것을 사용하려면 두 코드를 모두 파일에 넣으십시오. 예를 들어 코드 파일은 script.py이고 패키지 파일은 requirement.txt입니다.
  • 를 실행하십시오 pip install -r requirement.txt.
  • 스크립트를 예제로 실행 python3 script.py pa4080

여러 라이브러리를 사용합니다.

프로그램을 추가로 개발하기 위해 알아야 할 사항 (필수 패키지 문서 제외) :

  • 파이썬 라이브러리 : asyncio, json 및 urllib.parse
  • CSS 선택기 ( mdn web docs ) 및 일부 HTML. 이 기사 와 같은 브라우저에서 CSS 선택기를 사용하는 방법도 참조 하십시오.

작동 방식 :

  • 먼저 간단한 html 다운로더를 만듭니다. aiohttp doc에 제공된 샘플에서 수정 된 버전입니다.
  • 그 후에 사용자 이름과 출력 파일 이름을 허용하는 간단한 명령 줄 파서를 만듭니다.
  • 스레드 링크 및 기본 아티클에 대한 구문 분석기를 작성하십시오. pdb와 간단한 URL 조작을 사용하면 문제가 해결됩니다.
  • 함수를 결합하고 주요 기사를 json에 넣으면 나중에 다른 프로그램에서 처리 할 수 ​​있습니다.

더 발전시킬 수있는 아이디어

  • 날짜 모듈 링크를 승인하는 다른 부속 명령을 작성하십시오. 메소드를 분리하여 날짜 모듈을 자체 기능으로 구문 분석하고이를 새로운 부속 명령과 결합하여 수행 할 수 있습니다.
  • 날짜 모듈 링크 캐싱 : 스레드 링크를 얻은 후 캐시 json 파일을 작성하십시오. 따라서 프로그램은 링크를 다시 구문 분석 할 필요가 없습니다. 또는 전체 스레드 기본 기사가 일치하지 않더라도 전체 캐시

이것은 가장 우아한 대답은 아니지만 bash 답변을 사용하는 것보다 낫다고 생각합니다.

  • 파이썬을 사용하므로 크로스 플랫폼에서 사용할 수 있습니다.
  • 간단한 설치, 모든 필수 패키지는 pip를 사용하여 설치할 수 있습니다
  • 프로그램을 더 잘 읽을 수 있고 더 쉽게 개발할 수 있습니다.
  • 13 분 동안bash 스크립트 와 동일한 작업을 수행합니다 .

좋아, 나는 몇 가지 모듈을 설치했다 : sudo apt install python3-bs4 python3-click python3-aiohttp python3-async. 그러나 어떤 패키지 async_timeout에서 나온 패키지를 찾을 수 없습니까?
pa4080

@ pa4080 pip로 설치하므로 aiohttp에 포함되어야합니다. 처음 2 개의 함수 중 일부는 여기 aiohttp.readthedocs.io/en/stable 에서 수정되었습니다 . 또한 필요한 패키지를 설치하는 지침을 추가합니다
dan

pip를 사용하여 모듈을 성공적으로 설치했습니다. 그러나 다른 오류가 나타납니다 : paste.ubuntu.com/26311694 . 당신이 그렇게 할 때 제발 핑 :)
pa4080

@ pa4080, 오류를 복제 할 수 없으므로 가져 오기 기능을 단순화합니다. 부작용은 두 번째 재 시도가 작동하지 않으면 프로그램이 오류를 던질 수 있다는 것입니다
dan

1
주요 단점은 우분투 17.10에서만 스크립트를 성공적으로 실행할 수 있다는 것입니다. 그러나 내 bash 스크립트보다 5 배 빠르 므로이 답변을 수락하기로 결정했습니다.
pa4080

10

이 작업을 해결하기 위해 주로 CLI 도구를 사용하는 다음 간단한 bash 스크립트를 만들었습니다 wget.

#!/bin/bash

TARGET_URL='http://e-bane.net/modules.php?name=Stories_Archive'
KEY_WORDS=('pa4080' 's0ther')
MAP_FILE='url.map'
OUT_FILE='url.list'

get_url_map() {
    # Use 'wget' as spider and output the result into a file (and stdout) 
    wget --spider --force-html -r -l2 "${TARGET_URL}" 2>&1 | grep '^--' | awk '{ print $3 }' | tee -a "$MAP_FILE"
}

filter_url_map() {
    # Apply some filters to the $MAP_FILE and keep only the URLs, that contain 'article&sid'
    uniq "$MAP_FILE" | grep -v '\.\(css\|js\|png\|gif\|jpg\|txt\)$' | grep 'article&sid' | sort -u > "${MAP_FILE}.uniq"
    mv "${MAP_FILE}.uniq" "$MAP_FILE"
    printf '\n# -----\nThe number of the pages to be scanned: %s\n' "$(cat "$MAP_FILE" | wc -l)"
}

get_key_urls() {
    counter=1
    # Do this for each line in the $MAP_FILE
    while IFS= read -r URL; do
        # For each $KEY_WORD in $KEY_WORDS
        for KEY_WORD in "${KEY_WORDS[@]}"; do
            # Check if the $KEY_WORD exists within the content of the page, if it is true echo the particular $URL into the $OUT_FILE
            if [[ ! -z "$(wget -qO- "${URL}" | grep -io "${KEY_WORD}" | head -n1)" ]]; then
                echo "${URL}" | tee -a "$OUT_FILE"
                printf '%s\t%s\n' "${KEY_WORD}" "YES"
            fi
        done
        printf 'Progress: %s\r' "$counter"; ((counter++))
    done < "$MAP_FILE"
}

# Call the functions
get_url_map
filter_url_map
get_key_urls

이 스크립트에는 세 가지 기능이 있습니다.

  • 첫 번째 함수 get_url_map()wget로 사용 하고 --spider(이것은 페이지가 있는지 확인 함을 의미 함) with with level 의 재귀 -rURL $MAP_FILE을 만듭니다 . (또 다른 예는 웹 사이트를 PDF로 변환) 에서 찾을 수 있습니다 . 현재의 경우 약 20 000 개의 URL을 포함합니다.$TARGET_URL-l2$MAP_FILE

  • 두 번째 함수 filter_url_map()는의 내용을 단순화합니다 $MAP_FILE. 이 경우 문자열을 포함하는 행 (URL) 만 필요하며 article&sid약 3000 개입니다. 자세한 아이디어는 여기에서 찾을 수 있습니다 . 텍스트 파일의 행에서 특정 단어를 제거하는 방법?

  • 세 번째 기능은 get_key_urls()사용합니다 wget -qO-(명령으로 curl- 출력에 각 URL의 내용을) $MAP_FILE과 중 하나를 찾기 위해 노력할 것입니다 $KEY_WORDS그 안에. $KEY_WORDS특정 URL의 콘텐츠 내에이 중 하나가 있으면 해당 URL은에 저장됩니다 $OUT_FILE.

작업 과정에서 스크립트의 출력은 다음 이미지에 표시된 것처럼 보입니다. 두 개의 키워드가 있으면 완료하는 데 약 63 분이 걸리고 하나의 키워드 만 검색하면 42 분이 걸립니다 .

여기에 이미지 설명을 입력하십시오


1

@karel에서 제공 한이 답변을 기반으로 스크립트 를 다시 작성 했습니다 . 이제 스크립트 대신을 사용 합니다 . 결과적으로 훨씬 빨라집니다.lynxwget

현재 버전은 검색된 키워드가 두 개인 경우 15 분 동안 동일한 작업을 수행하고 하나의 키워드 만 검색하면 8 분만 동일한 작업을 수행 합니다. @dan에서 제공 하는 Python 솔루션 보다 빠릅니다 .

또한 lynx라틴 이외 문자를보다 잘 처리 할 수 ​​있습니다.

#!/bin/bash

TARGET_URL='http://e-bane.net/modules.php?name=Stories_Archive'
KEY_WORDS=('pa4080')  # KEY_WORDS=('word' 'some short sentence')
MAP_FILE='url.map'
OUT_FILE='url.list'

get_url_map() {
    # Use 'lynx' as spider and output the result into a file 
    lynx -dump "${TARGET_URL}" | awk '/http/{print $2}' | uniq -u > "$MAP_FILE"
    while IFS= read -r target_url; do lynx -dump "${target_url}" | awk '/http/{print $2}' | uniq -u >> "${MAP_FILE}.full"; done < "$MAP_FILE"
    mv "${MAP_FILE}.full" "$MAP_FILE"
}

filter_url_map() {
    # Apply some filters to the $MAP_FILE and keep only the URLs, that contain 'article&sid'
    uniq "$MAP_FILE" | grep -v '\.\(css\|js\|png\|gif\|jpg\|txt\)$' | grep 'article&sid' | sort -u > "${MAP_FILE}.uniq"
    mv "${MAP_FILE}.uniq" "$MAP_FILE"
    printf '\n# -----\nThe number of the pages to be scanned: %s\n' "$(cat "$MAP_FILE" | wc -l)"
}

get_key_urls() {
    counter=1
    # Do this for each line in the $MAP_FILE
    while IFS= read -r URL; do
        # For each $KEY_WORD in $KEY_WORDS
        for KEY_WORD in "${KEY_WORDS[@]}"; do
            # Check if the $KEY_WORD exists within the content of the page, if it is true echo the particular $URL into the $OUT_FILE
            if [[ ! -z "$(lynx -dump -nolist "${URL}" | grep -io "${KEY_WORD}" | head -n1)" ]]; then
                echo "${URL}" | tee -a "$OUT_FILE"
                printf '%s\t%s\n' "${KEY_WORD}" "YES"
            fi
        done
        printf 'Progress: %s\r' "$counter"; ((counter++))
    done < "$MAP_FILE"
}

# Call the functions
get_url_map
filter_url_map
get_key_urls
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.