2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-07-22 03:29:04 +08:00
|
|
|
module RetrieveTitle
|
2018-01-29 12:36:52 +08:00
|
|
|
CRAWL_TIMEOUT = 1
|
2023-12-01 15:03:06 +08:00
|
|
|
UNRECOVERABLE_ERRORS = [
|
|
|
|
Net::ReadTimeout,
|
|
|
|
FinalDestination::SSRFError,
|
|
|
|
FinalDestination::UrlEncodingError,
|
|
|
|
]
|
2017-07-22 03:29:04 +08:00
|
|
|
|
2022-05-23 18:52:06 +08:00
|
|
|
def self.crawl(url, max_redirects: nil, initial_https_redirect_ignore_limit: false)
|
|
|
|
fetch_title(
|
|
|
|
url,
|
|
|
|
max_redirects: max_redirects,
|
|
|
|
initial_https_redirect_ignore_limit: initial_https_redirect_ignore_limit,
|
|
|
|
)
|
2023-12-01 15:03:06 +08:00
|
|
|
rescue *UNRECOVERABLE_ERRORS
|
|
|
|
# ¯\_(ツ)_/¯
|
2017-07-22 03:29:04 +08:00
|
|
|
end
|
|
|
|
|
2021-01-05 03:32:08 +08:00
|
|
|
def self.extract_title(html, encoding = nil)
|
2017-07-22 03:29:04 +08:00
|
|
|
title = nil
|
2021-09-03 15:45:58 +08:00
|
|
|
return nil if html =~ /<title>/ && html !~ %r{</title>}
|
2017-07-22 03:29:04 +08:00
|
|
|
|
2022-08-23 13:03:57 +08:00
|
|
|
doc = nil
|
|
|
|
begin
|
|
|
|
doc = Nokogiri.HTML5(html, nil, encoding)
|
|
|
|
rescue ArgumentError
|
2022-08-23 13:14:24 +08:00
|
|
|
# invalid HTML (Eg: too many attributes, status tree too deep) - ignore
|
|
|
|
# Error in nokogumbo is not specialized, uses generic ArgumentError
|
|
|
|
# see: https://www.rubydoc.info/gems/nokogiri/Nokogiri/HTML5#label-Error+reporting
|
2022-08-23 13:03:57 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if doc
|
2017-08-03 02:27:21 +08:00
|
|
|
title = doc.at("title")&.inner_text
|
|
|
|
|
2017-09-28 21:29:50 +08:00
|
|
|
# A horrible hack - YouTube uses `document.title` to populate the title
|
|
|
|
# for some reason. For any other site than YouTube this wouldn't be worth it.
|
|
|
|
if title == "YouTube" && html =~ /document\.title *= *"(.*)";/
|
2023-01-21 02:52:49 +08:00
|
|
|
title = Regexp.last_match[1].sub(/ - YouTube\z/, "")
|
2017-09-28 21:29:50 +08:00
|
|
|
end
|
|
|
|
|
2017-08-03 02:27:21 +08:00
|
|
|
if !title && node = doc.at('meta[property="og:title"]')
|
2017-07-22 03:29:04 +08:00
|
|
|
title = node["content"]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if title.present?
|
|
|
|
title.gsub!(/\n/, " ")
|
|
|
|
title.gsub!(/ +/, " ")
|
|
|
|
title.strip!
|
|
|
|
return title
|
|
|
|
end
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def self.max_chunk_size(uri)
|
2022-02-10 05:53:27 +08:00
|
|
|
# Exception for sites that leave the title until very late.
|
2023-01-21 02:52:49 +08:00
|
|
|
if uri.host =~
|
|
|
|
/(^|\.)amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)\z/
|
2022-02-10 05:53:27 +08:00
|
|
|
return 500
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2023-01-21 02:52:49 +08:00
|
|
|
return 300 if uri.host =~ /(^|\.)youtube\.com\z/ || uri.host =~ /(^|\.)youtu\.be\z/
|
|
|
|
return 50 if uri.host =~ /(^|\.)github\.com\z/
|
2017-07-22 03:29:04 +08:00
|
|
|
|
2021-09-03 15:45:58 +08:00
|
|
|
# default is 20k
|
|
|
|
20
|
2017-07-22 03:29:04 +08:00
|
|
|
end
|
2018-01-29 12:36:52 +08:00
|
|
|
|
|
|
|
# Fetch the beginning of a HTML document at a url
|
2022-05-23 18:52:06 +08:00
|
|
|
def self.fetch_title(url, max_redirects: nil, initial_https_redirect_ignore_limit: false)
|
|
|
|
fd =
|
|
|
|
FinalDestination.new(
|
|
|
|
url,
|
|
|
|
timeout: CRAWL_TIMEOUT,
|
|
|
|
stop_at_blocked_pages: true,
|
|
|
|
max_redirects: max_redirects,
|
|
|
|
initial_https_redirect_ignore_limit: initial_https_redirect_ignore_limit,
|
2023-12-04 09:36:42 +08:00
|
|
|
headers: {
|
|
|
|
Accept: "text/html,*/*",
|
|
|
|
},
|
2022-05-23 18:52:06 +08:00
|
|
|
)
|
2018-01-29 12:36:52 +08:00
|
|
|
|
|
|
|
current = nil
|
2017-07-22 03:29:04 +08:00
|
|
|
title = nil
|
2021-01-05 03:32:08 +08:00
|
|
|
encoding = nil
|
2018-01-29 12:36:52 +08:00
|
|
|
|
|
|
|
fd.get do |_response, chunk, uri|
|
2021-06-24 22:23:39 +08:00
|
|
|
unless Net::HTTPRedirection === _response
|
2022-03-23 02:13:27 +08:00
|
|
|
throw :done if uri.blank?
|
|
|
|
|
2021-06-24 22:23:39 +08:00
|
|
|
if current
|
|
|
|
current << chunk
|
|
|
|
else
|
|
|
|
current = chunk
|
|
|
|
end
|
2018-01-29 12:36:52 +08:00
|
|
|
|
2021-06-24 22:23:39 +08:00
|
|
|
if !encoding && content_type = _response["content-type"]&.strip&.downcase
|
|
|
|
if content_type =~ /charset="?([a-z0-9_-]+)"?/
|
|
|
|
encoding = Regexp.last_match(1)
|
|
|
|
encoding = nil if !Encoding.list.map(&:name).map(&:downcase).include?(encoding)
|
2021-01-05 03:32:08 +08:00
|
|
|
end
|
|
|
|
end
|
2018-06-07 13:28:18 +08:00
|
|
|
|
2021-06-24 22:23:39 +08:00
|
|
|
max_size = max_chunk_size(uri) * 1024
|
|
|
|
title = extract_title(current, encoding)
|
|
|
|
throw :done if title || max_size < current.length
|
|
|
|
end
|
2017-07-22 03:29:04 +08:00
|
|
|
end
|
2019-11-15 04:10:51 +08:00
|
|
|
title
|
2018-06-07 13:28:18 +08:00
|
|
|
end
|
2017-07-22 03:29:04 +08:00
|
|
|
end
|