ฉันจะสร้าง CLI Web Spider ที่ใช้คำหลักและตัวกรองเนื้อหาได้อย่างไร


10

ฉันอยากพบบทความของฉันภายใน (เก่า) ฟอรั่มเลิกวรรณกรรมe-bane.net โมดูลฟอรัมบางส่วนถูกปิดใช้งานและฉันไม่สามารถรับรายการบทความโดยผู้เขียน นอกจากนี้เว็บไซต์ยังไม่ได้จัดทำดัชนีโดยเครื่องมือค้นหาเช่น Google, Yndex เป็นต้น

วิธีเดียวในการค้นหาบทความของฉันทั้งหมดคือการเปิดหน้าเก็บถาวรของเว็บไซต์ (รูปที่ 1) จากนั้นฉันจะต้องเลือกปีและเดือนที่แน่นอน - เช่นมกราคม 2013 (รูปที่ 1) และจากนั้นฉันต้องตรวจสอบแต่ละบทความ (รูปที่ 2) ไม่ว่าในตอนแรกจะเขียนชื่อเล่นของฉัน - pa4080 (รูปที่ 3) แต่มีไม่กี่พันบทความ

ป้อนคำอธิบายรูปภาพที่นี่

ป้อนคำอธิบายรูปภาพที่นี่

ป้อนคำอธิบายรูปภาพที่นี่

ฉันได้อ่านหัวข้อดังต่อไปนี้ แต่ไม่มีวิธีใดที่ตรงกับความต้องการของฉัน:

ผมจะโพสต์วิธีการแก้ปัญหาของตัวเอง แต่สำหรับฉันมันน่าสนใจ: มีวิธีที่สง่างามกว่านี้ในการแก้ไขงานนี้หรือไม่?

คำตอบ:


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 (ทดสอบบน python3.5 บน Ubuntu 17.10 )

วิธีใช้:

  • หากต้องการใช้งานให้วางโค้ดทั้งสองไว้ในไฟล์ ตัวอย่างเช่นไฟล์รหัสและแฟ้มแพคเกจscript.pyrequirement.txt
  • pip install -r requirement.txtวิ่ง
  • รันสคริปต์เป็นตัวอย่าง python3 script.py pa4080

มันใช้ห้องสมุดหลายแห่ง:

  • คลิกเพื่อแยกวิเคราะห์อาร์กิวเมนต์
  • beautifulsoupสำหรับ parser html
  • aiohttpสำหรับตัวดาวน์โหลด html

สิ่งที่ควรทราบเพื่อพัฒนาโปรแกรมเพิ่มเติม (นอกเหนือจากเอกสารของแพ็คเกจที่ต้องการ):

  • หลามไลบรารี: asyncio, json และ urllib.parse
  • ตัวเลือก css ( mdn web docs ) รวมทั้ง html ด้วย ดูวิธีการใช้ css selector บนเบราว์เซอร์ของคุณเช่นบทความนี้

มันทำงานอย่างไร:

  • ก่อนอื่นฉันสร้างเครื่องมือดาวน์โหลด HTML แบบง่าย มันเป็นรุ่นที่ปรับเปลี่ยนจากตัวอย่างที่กำหนดใน aiohttp doc
  • หลังจากนั้นสร้างตัวแยกวิเคราะห์บรรทัดคำสั่งอย่างง่ายซึ่งยอมรับชื่อผู้ใช้และชื่อไฟล์เอาต์พุต
  • สร้าง parser สำหรับลิงค์เธรดและบทความหลัก การใช้ pdb และการจัดการ URL อย่างง่ายควรทำงาน
  • รวมฟังก์ชั่นและวางบทความหลักใน json เพื่อให้โปรแกรมอื่นสามารถประมวลผลได้ในภายหลัง

ความคิดบางอย่างเพื่อให้สามารถพัฒนาต่อไป

  • สร้างคำสั่งย่อยอื่นที่ยอมรับการเชื่อมโยงโมดูลวันที่: สามารถทำได้โดยการแยกวิธีการแยกวิเคราะห์โมดูลวันที่เพื่อฟังก์ชั่นของตัวเองและรวมกับคำสั่งย่อยใหม่
  • การแคชลิงก์โมดูลวันที่: สร้างไฟล์ json แคชหลังจากรับลิงก์เธรด ดังนั้นโปรแกรมไม่จำเป็นต้องแยกวิเคราะห์ลิงก์อีกครั้ง หรือแม้กระทั่งเพียงแค่แคชบทความหลักทั้งกระทู้แม้ว่าจะไม่ตรงกัน

นี่ไม่ใช่คำตอบที่หรูหราที่สุด แต่ฉันคิดว่ามันดีกว่าการใช้คำตอบ bash

  • มันใช้ Python ซึ่งหมายความว่ามันสามารถใช้ข้ามแพลตฟอร์มได้
  • ติดตั้งง่ายแพคเกจที่จำเป็นทั้งหมดสามารถติดตั้งได้โดยใช้ pip
  • สามารถพัฒนาเพิ่มเติมอ่านโปรแกรมได้ง่ายขึ้นสามารถพัฒนาได้ง่ายขึ้น
  • มันไม่งานเดียวกันเป็นสคริปต์ทุบตีเท่านั้นสำหรับ13 นาที

ตกลงฉันจัดการติดตั้งโมดูลบางตัว: sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncแต่ฉันหาไม่ได้ - แพคเกจใดasync_timeoutมาจากไหน
pa4080

@ pa4080 ฉันติดตั้งด้วย pip ดังนั้นควรรวมกับ aiohttp ส่วนของฟังก์ชั่น 2 ครั้งแรกที่มีการแก้ไขจากที่นี่aiohttp.readthedocs.io/en/stable นอกจากนี้ฉันจะเพิ่มคำแนะนำในการติดตั้งแพคเกจที่ต้องการ
แดน

ฉันติดตั้งโมดูลสำเร็จโดยใช้ pip แต่บางคนปรากฏข้อผิดพลาดอื่น ๆ : paste.ubuntu.com/26311694 โปรด ping ฉันเมื่อคุณทำเช่นนั้น :)
pa4080

@ pa4080 ฉันไม่สามารถทำซ้ำข้อผิดพลาดของคุณได้ดังนั้นฉันจึงลดความซับซ้อนของฟังก์ชั่นการดึงข้อมูล ผลข้างเคียงที่เป็นที่โปรแกรมอาจโยนความผิดพลาดถ้าลองใหม่อีกครั้งที่สองไม่ทำงาน
แดน

1
ข้อเสียเปรียบหลักคือฉันได้พยายามที่จะเรียกใช้สคริปต์บน Ubuntu 17.10 ได้สำเร็จ อย่างไรก็ตามมันเร็วกว่าสคริปต์ทุบตีของฉันถึง 5 เท่าฉันเลยตัดสินใจตอบคำถามนี้
pa4080

10

เพื่อแก้ปัญหางานนี้เราได้สร้างที่อยู่ถัดง่ายwgetทุบตีสคริปต์ที่ส่วนใหญ่ใช้เครื่องมือ CLI

#!/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(ซึ่งหมายความว่ามันก็จะตรวจสอบว่าหน้าจะมี) และจะสร้าง recursive -rURL $MAP_FILEของที่มีระดับความลึก$TARGET_URL -l2(สามารถดูตัวอย่างอื่นได้ที่นี่: แปลงเว็บไซต์เป็น PDF ) ในกรณีปัจจุบัน$MAP_FILEมี URL ประมาณ 20,000 รายการ

  • ฟังก์ชั่นที่สองจะลดความซับซ้อนของเนื้อหาของ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 ตอนนี้สคริปต์ที่ใช้แทนlynx wgetผลที่ตามมาก็จะกลายเป็นเร็วขึ้นอย่างมีนัยสำคัญ

เวอร์ชันปัจจุบันทำงานเหมือนกันเป็นเวลา 15 นาทีเมื่อมีคำค้นหาสองคำและเพียง8 นาทีถ้าเราค้นหาคำหลักเพียงคำเดียว นั่นคือเร็วกว่างูหลามวิธีการแก้ปัญหาให้โดย@dan

นอกจากนี้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.