Извлечение текста с веб-сайта с помощью goquery

У меня есть HTML примерно выглядит так:

<h4>Movies</h4>
    <h5><a href="external_link" target="_blank"> A Song For Jenny</a> (2015)</h5>
    Rating: PG<br/>
    Running Time (minutes): 77<br/>
    Description: This Drama, based on real life events, tells the story of a family affected directly by the 7/7 London bombings.  It shows love, loss, heartache and  ...<br/>
    <a href="/bmm/shop/Movie_Detail?movieid=2713288">More about  A Song For Jenny</a><br/>
        <a href="/bmm/shop/Edit_Movie?movieid=2713288">Edit  A Song For Jenny</a><br/>
    <br/>
    <h5><a href="link" target="_blank">#RealityHigh</a> (2017)</h5>
    Rating: PG<br/>
    Running Time (minutes): 99<br/>
    Description: High-achieving high-school senior Dani Barnes dreams of getting into UC Davis, the world's top  veterinary school. Then a glamorous new friend draws  ...<br/>
    <a href="/bmm/shop/Movie_Detail?movieid=4089906">More about #RealityHigh</a><br/>
        <a href="/bmm/shop/Edit_Movie?movieid=4089906">Edit #RealityHigh</a><br/>
    <br/>
    <h5><a href="link" target="_blank">1 Night</a> (2016)</h5>
    Rating: PG<br/>
    Running Time (minutes): 80<br/>
    Description: Bea, a worrisome teenager, reconnects with her introverted childhood friend, Andy. The two  overcome their differences in social status one night aft ...<br/>
    <a href="/bmm/shop/Movie_Detail?movieid=3959071">More about 1 Night</a><br/>
        <a href="/bmm/shop/Edit_Movie?movieid=3959071">Edit 1 Night</a><br/>
    <br/>
    <h5><a href="link" target="_blank">10 Cloverfield Lane</a> (2016)</h5>
    Rating: PG<br/>
    Running Time (minutes): 104<br/>
    Description: Soon after leaving her fiancé Michelle is involved in a car accident. She awakens
to find herself sharing an underground bunker with Howard and Emme ...<br/>
    <a href="/bmm/shop/Movie_Detail?movieid=3052189">More about 10 Cloverfield Lane</a><br/>
        <a href="/bmm/shop/Edit_Movie?movieid=3052189">Edit 10 Cloverfield Lane</a><br/>
    <br/>

Мне нужно использовать goquery, чтобы получить как можно больше информации с этой страницы. Я знаю, как извлечь внешние ссылки, замененные словом "ссылка" в этом фрагменте, я знаю, как получить ссылки с более подробной информацией, но я также хочу извлечь информацию, содержащуюся только в тексте, то есть год (в заголовках), время выполнения, сокращенное описание и рейтинг PG. Я не мог понять, как это сделать в goquery, потому что этот текст не окружен ни div, ни другими тегами. Я попытался найти теги h5 и затем вызвать.Next() для них, но я мог только найти <br> теги, а не текст между ними. Как я могу это сделать? Если есть лучший способ сделать это, чем использовать goquery, я в порядке. Мой код выглядит так.

// Retrieve the page count:
    res, err = http.Get("myUrlAddress")
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }
    doc, err = goquery.NewDocumentFromResponse(res)
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }
    links := doc.Find(`a[href*="pageIndex"]`)
    fmt.Println(links.Length()) // Output page count
s := doc.Find("h5").First().Next() // I expect it to be the text after the heading.
fmt.Println(s.Text()) // But it's empty and if I check the node type it says br

2 ответа

Решение

Мне почему-то не нравится идея использовать регулярные выражения для разбора HTML. Я чувствую, что он слишком хрупок против незначительных изменений, таких как порядок тегов или что-то в этом роде.

Я думаю, что лучше всего воспользоваться html.Node(golang.org/x/net/html), на котором основан запрос. Идея состоит в том, чтобы перебрать братьев и сестер, пока он не закончится, или следующий h5 встречается. Работать со ссылками или любыми другими тегами элементов может быть небольшой проблемой, так как html.Node предоставляет довольно недружественный API в отношении атрибутов, но переключение обратно на goquery из этого является еще большей проблемой.

package main

import (
    "fmt"
    "github.com/PuerkitoBio/goquery"
    "golang.org/x/net/html"
    "golang.org/x/net/html/atom"
    "os"
    "strings"
)

type Movie struct {
}

func (m Movie) addTitle(s string) {
    fmt.Println("Title", s)
}

func (m Movie) addProperty(s string) {
    if s == "" {
        return
    }
    fmt.Println("Property", s)
}

var M []*Movie

func parseMovie(i int, s *goquery.Selection) {
    m := &Movie{}
    m.addTitle(s.Text())

loop:
    for node := s.Nodes[0].NextSibling; node != nil; node = node.NextSibling {
        switch node.Type {
        case html.TextNode:
            m.addProperty(strings.TrimSpace(node.Data))
        case html.ElementNode:
            switch node.DataAtom {
            case atom.A:
                //link, do something. You may want to transfer back to go query
                fmt.Println(node.Attr)
            case atom.Br:
                continue
            case atom.H5:
                break loop
            }
        }
    }

    M = append(M, m)
}

func main() {
    r, err := os.Open("movie.html")
    if err != nil {
        panic(err)
    }
    doc, err := goquery.NewDocumentFromReader(r)
    if err != nil {
        panic(err)
    }

    doc.Find("h5").Each(parseMovie)
}

К сожалению, из-за структуры этой HTML-страницы не похоже, что goquery окажет большую помощь после того, как вы определили раздел страницы, который содержит списки фильмов в вашем примере, потому что интересующие вас данные не изолированы в элементы, которые могут быть нацелены goquery.

Тем не менее, детали могут быть легко проанализированы с помощью регулярных выражений, которые, конечно, могут быть изменены по мере необходимости (особенно если / когда исходная страница меняет свою HTML-структуру).

type Movie struct {
    Title          string
    ReleaseYear    int
    Rating         string
    RuntimeMinutes int
    Description    string
}

var movieregexp = regexp.MustCompile(`` +
    `<h5><a.*?>\s*(.*?)\s*</a>\s*\((\d{4})\)</h5>` + // Title and release year
    `[\s\S]*?Rating: (.*?)<` +
    `[\s\S]*?Running Time \(minutes\): (\d{1,3})` +
    `[\s\S]*?Description: ([\s\S]*?)<`)

// Returns a slice of movies parsed from the given string, possibly empty.
func ParseMovies(s string) []Movie {
    movies := []Movie{}
    groups := movieregexp.FindAllStringSubmatch(s, -1)

    if groups != nil {
        for _, group := range groups {
            // We know these integers parse correctly because of the regex.
            year, _ := strconv.Atoi(group[2])
            runtime, _ := strconv.Atoi(group[4])
            // Append the new movie to the list.
            movies = append(movies, Movie{
                Title:          group[1],
                ReleaseYear:    year,
                Rating:         group[3],
                RuntimeMinutes: runtime,
                Description:    group[5],
            })
        }
    }

    return movies
}
Другие вопросы по тегам