Anahtar kelimeleri kullanan ve içeriği filtreleyen bir CLI Web Spider'ı nasıl oluştururum?


10

Makalelerimi, kullanımdan kaldırılmış (kullanılmayan) e-bane.net forumunda bulmak istiyorum . Forum modüllerinden bazıları devre dışı bırakıldı ve yazarlarının makalelerinin listesini alamıyorum. Ayrıca site arama motorları tarafından Google, Yndex vb. Olarak dizine eklenmez.

Tüm makalelerimi bulmanın tek yolu sitenin arşiv sayfasını açmaktır (şek.1). O zaman belirli bir yılı ve ayı seçmeliyim - örneğin Ocak 2013 (şek.1). Ve sonra her makaleyi (şek.2) başlangıçta takma adım olup olmadığını kontrol etmeliyim - pa4080 (şek.3). Ancak birkaç bin makale var.

resim açıklamasını buraya girin

resim açıklamasını buraya girin

resim açıklamasını buraya girin

Aşağıdaki gibi birkaç konu okudum, ancak çözümlerin hiçbiri ihtiyaçlarıma uymuyor:

Kendi çözümümü yayınlayacağım . Ama benim için ilginç: Bu görevi çözmenin daha zarif bir yolu var mı?

Yanıtlar:


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

İşte (Ubuntu üzerinde python3.5 üzerinde test script python3 versiyonudur 17.10 ).

Nasıl kullanılır:

  • Kullanmak için her iki kodu da dosyalara koyun. Örneğin, kod dosyası script.pyve paket dosyası requirement.txt.
  • Koş pip install -r requirement.txt.
  • Komut dosyasını örnek olarak çalıştırın python3 script.py pa4080

Birkaç kütüphane kullanır:

Programı daha da geliştirmek için bilinmesi gerekenler (gerekli paketin dokümanı hariç):

  • python kütüphanesi: asyncio, json ve urllib.parse
  • css seçicileri ( mdn web belgeleri ), ayrıca bazı html. ayrıca bu makale gibi tarayıcınızda css seçiciyi nasıl kullanacağınıza bakın

Nasıl çalışır:

  • Önce basit bir html indirici oluşturuyorum. Aiohttp doc'de verilen örnekten değiştirilmiş versiyon.
  • Bundan sonra kullanıcı adı ve çıktı dosya adını kabul eden basit komut satırı ayrıştırıcısı oluşturulur.
  • İş parçacığı bağlantıları ve ana makale için bir ayrıştırıcı oluşturun. PDB ve basit URL manipülasyonu kullanarak iş yapmak gerekir.
  • Fonksiyonu birleştirin ve ana makaleyi json üzerine koyun, böylece diğer program daha sonra işleyebilir.

Bazı fikirler, böylece daha da geliştirilebilir

  • Tarih modülü bağlantısını kabul eden başka bir alt komut oluşturun: tarih modülünü kendi işlevine ayrıştırma yöntemini ayırarak ve yeni alt komutla birleştirerek yapılabilir.
  • Tarih modülü bağlantısını önbelleğe alma: iş parçacığı bağlantısını aldıktan sonra önbellek json dosyası oluşturun. böylece program bağlantıyı tekrar ayrıştırmak zorunda kalmaz. veya eşleşmese bile tüm ana başlık makalesini önbelleğe al

Bu en şık cevap değil, ama bence bash cevabı kullanmaktan daha iyi.

  • Çapraz platformda kullanılabileceği anlamına gelen Python kullanır.
  • Basit kurulum, gerekli tüm paketler pip kullanılarak kurulabilir
  • Daha da geliştirilebilir, program daha okunabilir, daha kolay geliştirilebilir.
  • Bash betiği ile aynı işi sadece 13 dakika boyunca yapar .

Tamam, bazı modüller kurmayı başardım: sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncancak bulamıyorum - hangi paketten async_timeoutgeliyor?
pa4080

@ pa4080 i aiohttp ile dahil edilmelidir böylece pip ile yükleyin. ilk 2 işlevin bölümleri buradan değiştirilir aiohttp.readthedocs.io/en/stable . Ayrıca gerekli paketi yüklemek için talimat ekleyeceğim
dan

Pip kullanarak modülü başarıyla kurdum. Ancak başka bir hata daha görünür: paste.ubuntu.com/26311694 . Bunu yaptığınızda lütfen bana ping
atın

@ pa4080, hatanızı çoğaltamam, bu yüzden getirme işlevini basitleştiririm. yan etki, ikinci yeniden deneme çalışmıyorsa programın hata atabileceğidir
dan

1
Ana eksilerini script sadece Ubuntu 17.10 başarıyla çalıştırmayı başardı. Ancak benim bash senaryomdan 5 kat daha hızlı, bu yüzden bu cevabı kabul etmeye karar verdim.
pa4080

10

Bu görevi çözmek için esas olarak CLI aracını kullanan bir sonraki basit bash betiğini oluşturdum 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

Komut dosyasının üç işlevi vardır:

  • İlk fonksiyon get_url_map()kullanımları wgetolarak --spider(ki bu sadece sayfalar olup olmadığını kontrol edeceği anlamına gelir) ve özyinelemeli yaratacak -rURL'yi $MAP_FILEait $TARGET_URLderinlik seviyesine sahip -l2. (Başka bir örnek burada bulunabilir: Web Sitesini PDF'ye Dönüştür ). Mevcut durumda, $MAP_FILEyaklaşık 20.000 URL içerir.

  • İkinci fonksiyon filter_url_map(), 'in içeriğini basitleştirecektir $MAP_FILE. Bu durumda, yalnızca dizeyi içeren satırlara (URL'lere) ihtiyacımız vardır article&sidve bunlar yaklaşık 3000'dir. Daha Fazla Fikir burada bulunabilir: Bir metin dosyasının satırlarından belirli kelimeler nasıl kaldırılır?

  • Üçüncü işlevi get_key_urls()kullanacaktır wget -qO-(komut olarak curl- örneklerden çıkışına gelen her bir URL'nin içeriğini) $MAP_FILEve herhangi bulmaya çalışacağız $KEY_WORDSiçindeki. Herhangi biri $KEY_WORDSbelirli bir URL'nin içeriğinde kurulmuşsa, bu URL $OUT_FILE.

Çalışma işlemi sırasında komut dosyasının çıktısı bir sonraki resimde gösterildiği gibi görünür. İki anahtar kelime varsa yaklaşık 63 dakika ve yalnızca bir anahtar kelime arandığında 42 dakika sürer .

resim açıklamasını buraya girin


1

@Karel tarafından sağlanan bu cevaba dayanarak senaryomu yeniden oluşturdum . Şimdi betik yerine kullanıyor . Sonuç olarak önemli ölçüde daha hızlı hale gelir.lynxwget

Mevcut sürüm, iki aranan anahtar kelime olduğunda aynı işi 15 dakika ve yalnızca bir anahtar kelime ararsak sadece 8 dakika sürer . @Dan tarafından sağlanan Python çözümünden daha hızlıdır .

Ayrıca lynxlatin olmayan karakterlerin daha iyi işlenmesini sağlar.

#!/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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.