Как избавиться от фантомного ряда в массиве? - PullRequest
0 голосов
/ 24 апреля 2020

Я очищаю кучу таблиц с httparty, затем анализирую ответ с помощью nokogiri. Все работает хорошо, но потом я получаю фантомную строку вверху:

require 'nokogiri'
require 'httparty'
require 'byebug'
def scraper
    url = "https://github.com/public-apis/public-apis"
    parsed_page = Nokogiri::HTML(HTTParty.get(url))
    # Get categories from the ul at the top
    categories = parsed_page.xpath('/html/body/div[4]/div/main/div[2]/div/div/div/article/ul/li/a')
    # Get all tables from the page
    tables = parsed_page.xpath('/html/body/div[4]/div/main/div[2]/div/div/div/article/table')
    rows = []
    # Acting on one first for testing before making it dynamic 
    tables[0].search('tr').each do |tr|
        cells = tr.search('td')
        link = ''
        values = []
        row = {
            'name' => '',
            'description' => '',
            'auth' => '',
            'https' => '',
            'cors' => '',
            'category' => '',
            'url' => ''
        }
        cells.css('a').each do |a|
            link += a['href']
        end
        cells.each do |cell|
            values << cell.text
        end
        values << categories[0].text
        values << link
        rows << row.keys.zip(values).to_h
    end
    puts rows
end
scraper

Результат в консоли:

{"name"=>"Animals", "description"=>"", "auth"=>nil, "https"=>nil, "cors"=>nil, "category"=>nil, "url"=>nil}
{"name"=>"Cat Facts", "description"=>"Daily cat facts", "auth"=>"No", "https"=>"Yes", 
...

Откуда берется эта первая строка?

Ответы [ 2 ]

1 голос
/ 24 апреля 2020

Первая строка, которую вы видите, скорее всего, строка заголовка. В строках заголовка используется <th> вместо <td>. Это означает, что cells = tr.search('td') будет пустой коллекцией для строки заголовка.

В большинстве случаев строки заголовка помещаются в <thead>, а строки данных помещаются в <tbody>. Поэтому вместо tables[0].search('tr') вы можете использовать tables[0].search('tbody tr'), который выбирает только строки в теге <tbody>.

0 голосов
/ 25 апреля 2020

Ваш код может быть намного проще и более устойчивым:

Медитируйте на это:

require 'nokogiri'
require 'httparty'

URL = 'https://github.com/public-apis/public-apis'
FIELDS = %w[name description auth https cors category url]

doc = Nokogiri::HTML(HTTParty.get(URL))

category = doc.at('article li a').text

rows = doc.at('article table').search('tr')[1..-1].map { |tr| 
  values = tr.search('td').map(&:text)
  link = tr.at('a')['href']
  Hash[
    FIELDS.zip(values + [category, link])
  ]
}

Что приводит к:

puts rows

# >> {"name"=>"Cat Facts", "description"=>"Daily cat facts", "auth"=>"No", "https"=>"Yes", "cors"=>"No", "category"=>"Animals", "url"=>"https://alexwohlbruck.github.io/cat-facts/"}
# >> {"name"=>"Cats", "description"=>"Pictures of cats from Tumblr", "auth"=>"apiKey", "https"=>"Yes", "cors"=>"Unknown", "category"=>"Animals", "url"=>"https://docs.thecatapi.com/"}
# >> {"name"=>"Dogs", "description"=>"Based on the Stanford Dogs Dataset", "auth"=>"No", "https"=>"Yes", "cors"=>"Yes", "category"=>"Animals", "url"=>"https://dog.ceo/dog-api/"}
# >> {"name"=>"HTTPCat", "description"=>"Cat for every HTTP Status", "auth"=>"No", "https"=>"Yes", "cors"=>"Unknown", "category"=>"Animals", "url"=>"https://http.cat/"}
# >> {"name"=>"IUCN", "description"=>"IUCN Red List of Threatened Species", "auth"=>"apiKey", "https"=>"No", "cors"=>"Unknown", "category"=>"Animals", "url"=>"http://apiv3.iucnredlist.org/api/v3/docs"}
# >> {"name"=>"Movebank", "description"=>"Movement and Migration data of animals", "auth"=>"No", "https"=>"Yes", "cors"=>"Unknown", "category"=>"Animals", "url"=>"https://github.com/movebank/movebank-api-doc"}
# >> {"name"=>"Petfinder", "description"=>"Adoption", "auth"=>"OAuth", "https"=>"Yes", "cors"=>"Yes", "category"=>"Animals", "url"=>"https://www.petfinder.com/developers/v2/docs/"}
# >> {"name"=>"PlaceGOAT", "description"=>"Placeholder goat images", "auth"=>"No", "https"=>"Yes", "cors"=>"Unknown", "category"=>"Animals", "url"=>"https://placegoat.com/"}
# >> {"name"=>"RandomCat", "description"=>"Random pictures of cats", "auth"=>"No", "https"=>"Yes", "cors"=>"Yes", "category"=>"Animals", "url"=>"https://aws.random.cat/meow"}
# >> {"name"=>"RandomDog", "description"=>"Random pictures of dogs", "auth"=>"No", "https"=>"Yes", "cors"=>"Yes", "category"=>"Animals", "url"=>"https://random.dog/woof.json"}
# >> {"name"=>"RandomFox", "description"=>"Random pictures of foxes", "auth"=>"No", "https"=>"Yes", "cors"=>"No", "category"=>"Animals", "url"=>"https://randomfox.ca/floof/"}
# >> {"name"=>"RescueGroups", "description"=>"Adoption", "auth"=>"No", "https"=>"Yes", "cors"=>"Unknown", "category"=>"Animals", "url"=>"https://userguide.rescuegroups.org/display/APIDG/API+Developers+Guide+Home"}
# >> {"name"=>"Shibe.Online", "description"=>"Random pictures of Shibu Inu, cats or birds", "auth"=>"No", "https"=>"Yes", "cors"=>"Yes", "category"=>"Animals", "url"=>"http://shibe.online/"}

Проблемы с вашим кодом являются:

  • Использование search('some selector')[0] такое же, как at('some selector') только вторая чище, что приводит к уменьшению визуального шума.

    Существуют и другие, более тонкие различия в том, что возвращает search по сравнению с at, который описан в документации. Я настоятельно рекомендую прочитать и поэкспериментировать с их примерами, так как вы знаете, какой из них использовать, когда вы можете избавить вас от головной боли.

  • Полагаться на абсолютные селекторы XPath: абсолютный селектор очень равен agile. Любое изменение в HTML будет иметь высокую вероятность взлома. Вместо этого найдите полезные узлы для проверки уникальности и позвольте парсеру найти их.

    С помощью селектора CSS 'article li a' перебирает все узлы, пока не найдет узел «article», заглядывает в него для ребенка "ли" и после "а". Вы можете сделать то же самое с XPath, но визуально шумно. Я большой поклонник того, чтобы мой код был как можно более легким для чтения и понимания.

    Аналогично, at('article table') находит первую таблицу в узле "article", затем search('tr') находит вложенные строки в только этот стол.

    Поскольку вы хотите пропустить заголовок таблицы, [1..-1] нарезает NodeSet и пропускает первую строку.

  • map облегчает задачу чтобы построить структуру:

    rows = doc.at('article table').search('tr')[1..-1].map { |tr| 
    

    назначает поля для rows за один проход через эти l oop строк.

    values назначается с текстом NodeSet каждого текста узла "td".

  • Вы можете легко построить га sh, используя Ha sh '[] конструктор и передача массива пар ключ / значение.

    FIELDS.zip(values + [category, link])
    

    берет значения из ячеек и добавляет второй массив, содержащий категорию и ссылка из строки.

Мой пример кода в основном тот же шаблон каждый раз, когда я очищаю страницу с таблицей. Различия будут незначительными, но это всего лишь 1076 * по таблице, извлекающей ячейки и преобразующей их в га sh. Можно даже на чисто написанной таблице автоматически извлечь ключи ha sh из текста ячейки в первой строке таблицы.

...