relaton 2.2.0.pre.alpha.1 → 3.0.0.pre.alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/{docs/README.adoc → README.adoc} +165 -38
- data/bin/console +0 -1
- data/lib/relaton/3gpp/bibdata.rb +9 -0
- data/lib/relaton/3gpp/bibitem.rb +9 -0
- data/lib/relaton/3gpp/bibliography.rb +123 -0
- data/lib/relaton/3gpp/data_fetcher.rb +303 -0
- data/lib/relaton/3gpp/docidentifier.rb +114 -0
- data/lib/relaton/3gpp/doctype.rb +9 -0
- data/lib/relaton/3gpp/ext.rb +31 -0
- data/lib/relaton/3gpp/item.rb +18 -0
- data/lib/relaton/3gpp/item_data.rb +15 -0
- data/lib/relaton/3gpp/parser.rb +400 -0
- data/lib/relaton/3gpp/processor.rb +71 -0
- data/lib/relaton/3gpp/release.rb +34 -0
- data/lib/relaton/3gpp/util.rb +8 -0
- data/lib/relaton/3gpp.rb +29 -0
- data/lib/relaton/adobe/bibdata.rb +8 -0
- data/lib/relaton/adobe/bibitem.rb +8 -0
- data/lib/relaton/adobe/bibliography.rb +92 -0
- data/lib/relaton/adobe/docidentifier.rb +49 -0
- data/lib/relaton/adobe/doctype.rb +14 -0
- data/lib/relaton/adobe/ext.rb +32 -0
- data/lib/relaton/adobe/item.rb +15 -0
- data/lib/relaton/adobe/item_base.rb +18 -0
- data/lib/relaton/adobe/item_data.rb +6 -0
- data/lib/relaton/adobe/processor.rb +45 -0
- data/lib/relaton/adobe/util.rb +8 -0
- data/lib/relaton/adobe.rb +37 -0
- data/lib/relaton/bib/converter/asciibib/to_asciibib.rb +663 -0
- data/lib/relaton/bib/converter/asciibib.rb +13 -0
- data/lib/relaton/bib/converter/bibtex/from_bibtex.rb +245 -0
- data/lib/relaton/bib/converter/bibtex/to_bibtex.rb +341 -0
- data/lib/relaton/bib/converter/bibtex.rb +23 -0
- data/lib/relaton/bib/converter/bibxml/from_rfcxml.rb +386 -0
- data/lib/relaton/bib/converter/bibxml/from_rfcxml_referencegroup.rb +71 -0
- data/lib/relaton/bib/converter/bibxml/to_rfcxml.rb +308 -0
- data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb +52 -0
- data/lib/relaton/bib/converter/bibxml.rb +51 -0
- data/lib/relaton/bib/hash_parser_v1.rb +767 -0
- data/lib/relaton/bib/item_data.rb +229 -0
- data/lib/relaton/bib/model/abstract.rb +16 -0
- data/lib/relaton/bib/model/address.rb +22 -0
- data/lib/relaton/bib/model/affiliation.rb +16 -0
- data/lib/relaton/bib/model/bibdata.rb +11 -0
- data/lib/relaton/bib/model/bibdata_shared.rb +12 -0
- data/lib/relaton/bib/model/bibitem.rb +11 -0
- data/lib/relaton/bib/model/bibitem_shared.rb +12 -0
- data/lib/relaton/bib/model/contact.rb +18 -0
- data/lib/relaton/bib/model/contribution_info.rb +15 -0
- data/lib/relaton/bib/model/contributor.rb +29 -0
- data/lib/relaton/bib/model/copyright.rb +27 -0
- data/lib/relaton/bib/model/date.rb +31 -0
- data/lib/relaton/bib/model/depiction.rb +16 -0
- data/lib/relaton/bib/model/docidentifier.rb +49 -0
- data/lib/relaton/bib/model/doctype.rb +14 -0
- data/lib/relaton/bib/model/edition.rb +14 -0
- data/lib/relaton/bib/model/ext.rb +39 -0
- data/lib/relaton/bib/model/extent.rb +16 -0
- data/lib/relaton/bib/model/formattedref.rb +43 -0
- data/lib/relaton/bib/model/full_name_type.rb +64 -0
- data/lib/relaton/bib/model/fullname.rb +11 -0
- data/lib/relaton/bib/model/ics.rb +49 -0
- data/lib/relaton/bib/model/image.rb +28 -0
- data/lib/relaton/bib/model/item.rb +96 -0
- data/lib/relaton/bib/model/item_base.rb +20 -0
- data/lib/relaton/bib/model/item_shared.rb +88 -0
- data/lib/relaton/bib/model/keyword.rb +30 -0
- data/lib/relaton/bib/model/locality.rb +18 -0
- data/lib/relaton/bib/model/locality_stack.rb +14 -0
- data/lib/relaton/bib/model/localized_string.rb +48 -0
- data/lib/relaton/bib/model/localized_string_attrs.rb +24 -0
- data/lib/relaton/bib/model/logo.rb +14 -0
- data/lib/relaton/bib/model/medium.rb +22 -0
- data/lib/relaton/bib/model/note.rb +16 -0
- data/lib/relaton/bib/model/organization.rb +13 -0
- data/lib/relaton/bib/model/organization_type.rb +42 -0
- data/lib/relaton/bib/model/person.rb +36 -0
- data/lib/relaton/bib/model/phone.rb +14 -0
- data/lib/relaton/bib/model/place.rb +33 -0
- data/lib/relaton/bib/model/price.rb +14 -0
- data/lib/relaton/bib/model/relation.rb +43 -0
- data/lib/relaton/bib/model/series.rb +34 -0
- data/lib/relaton/bib/model/size.rb +23 -0
- data/lib/relaton/bib/model/source_locality_stack.rb +14 -0
- data/lib/relaton/bib/model/status.rb +27 -0
- data/lib/relaton/bib/model/structured_identifier.rb +49 -0
- data/lib/relaton/bib/model/subdivision.rb +16 -0
- data/lib/relaton/bib/model/title.rb +56 -0
- data/lib/relaton/bib/model/type/plain_date.rb +16 -0
- data/lib/relaton/bib/model/type/string_date.rb +48 -0
- data/lib/relaton/bib/model/uri.rb +18 -0
- data/lib/relaton/bib/model/validity.rb +16 -0
- data/lib/relaton/bib/model/version.rb +43 -0
- data/lib/relaton/bib/namespace_helper.rb +21 -0
- data/lib/relaton/bib/sanitizer.rb +264 -0
- data/lib/relaton/bib/util.rb +18 -0
- data/lib/relaton/bib/versions.json +35 -0
- data/lib/relaton/bib.rb +46 -0
- data/lib/relaton/bipm/bibliography.rb +231 -0
- data/lib/relaton/bipm/converter/asciibib.rb +64 -0
- data/lib/relaton/bipm/data_fetcher.rb +81 -0
- data/lib/relaton/bipm/data_outcomes_parser.rb +656 -0
- data/lib/relaton/bipm/id_parser.rb +278 -0
- data/lib/relaton/bipm/item_data.rb +47 -0
- data/lib/relaton/bipm/model/bibdata.rb +9 -0
- data/lib/relaton/bipm/model/bibitem.rb +9 -0
- data/lib/relaton/bipm/model/comment_period.rb +13 -0
- data/lib/relaton/bipm/model/doctype.rb +12 -0
- data/lib/relaton/bipm/model/ext.rb +38 -0
- data/lib/relaton/bipm/model/item.rb +11 -0
- data/lib/relaton/bipm/model/structured_identifier.rb +36 -0
- data/lib/relaton/bipm/processor.rb +69 -0
- data/lib/relaton/bipm/rawdata_bipm_metrologia/affiliations.rb +111 -0
- data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +172 -0
- data/lib/relaton/bipm/rawdata_bipm_metrologia/niso_jats_parser.rb +353 -0
- data/lib/relaton/bipm/si_brochure_parser.rb +188 -0
- data/lib/relaton/bipm/util.rb +8 -0
- data/lib/relaton/bipm.rb +38 -0
- data/lib/relaton/bsi/bibliography.rb +196 -0
- data/lib/relaton/bsi/hit.rb +28 -0
- data/lib/relaton/bsi/hit_collection.rb +113 -0
- data/lib/relaton/bsi/item_data.rb +13 -0
- data/lib/relaton/bsi/model/bibdata.rb +8 -0
- data/lib/relaton/bsi/model/bibitem.rb +8 -0
- data/lib/relaton/bsi/model/docidentifier.rb +107 -0
- data/lib/relaton/bsi/model/doctype.rb +14 -0
- data/lib/relaton/bsi/model/ext.rb +17 -0
- data/lib/relaton/bsi/model/item.rb +17 -0
- data/lib/relaton/bsi/model/item_base.rb +22 -0
- data/lib/relaton/bsi/model/relation.rb +9 -0
- data/lib/relaton/bsi/processor.rb +43 -0
- data/lib/relaton/bsi/schema.json +24882 -0
- data/lib/relaton/bsi/scraper.rb +288 -0
- data/lib/relaton/bsi/util.rb +8 -0
- data/lib/relaton/bsi.rb +25 -0
- data/lib/relaton/calconnect/bibliography.rb +93 -0
- data/lib/relaton/calconnect/data_fetcher.rb +174 -0
- data/lib/relaton/calconnect/docidentifier.rb +80 -0
- data/lib/relaton/calconnect/hit.rb +14 -0
- data/lib/relaton/calconnect/hit_collection.rb +92 -0
- data/lib/relaton/calconnect/item_data.rb +12 -0
- data/lib/relaton/calconnect/model/bibdata.rb +8 -0
- data/lib/relaton/calconnect/model/bibitem.rb +8 -0
- data/lib/relaton/calconnect/model/doctype.rb +11 -0
- data/lib/relaton/calconnect/model/ext.rb +11 -0
- data/lib/relaton/calconnect/model/item.rb +20 -0
- data/lib/relaton/calconnect/processor.rb +72 -0
- data/lib/relaton/calconnect/scraper.rb +87 -0
- data/lib/relaton/calconnect/util.rb +8 -0
- data/lib/relaton/calconnect.rb +36 -0
- data/lib/relaton/ccsds/bibliography.rb +63 -0
- data/lib/relaton/ccsds/data/fetcher.rb +252 -0
- data/lib/relaton/ccsds/data/iso_references.rb +30 -0
- data/lib/relaton/ccsds/data/parser.rb +194 -0
- data/lib/relaton/ccsds/hit.rb +24 -0
- data/lib/relaton/ccsds/hit_collection.rb +47 -0
- data/lib/relaton/ccsds/item_data.rb +9 -0
- data/lib/relaton/ccsds/model/bibdata.rb +8 -0
- data/lib/relaton/ccsds/model/bibitem.rb +8 -0
- data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
- data/lib/relaton/ccsds/model/doctype.rb +9 -0
- data/lib/relaton/ccsds/model/ext.rb +19 -0
- data/lib/relaton/ccsds/model/item.rb +15 -0
- data/lib/relaton/ccsds/processor.rb +68 -0
- data/lib/relaton/ccsds/util.rb +10 -0
- data/lib/relaton/ccsds.rb +33 -0
- data/lib/relaton/cen/bibliography.rb +149 -0
- data/lib/relaton/cen/committees.yaml +66 -0
- data/lib/relaton/cen/hit.rb +31 -0
- data/lib/relaton/cen/hit_collection.rb +116 -0
- data/lib/relaton/cen/item_data.rb +7 -0
- data/lib/relaton/cen/model/bibdata.rb +8 -0
- data/lib/relaton/cen/model/bibitem.rb +8 -0
- data/lib/relaton/cen/model/docidentifier.rb +100 -0
- data/lib/relaton/cen/model/ext.rb +11 -0
- data/lib/relaton/cen/model/item.rb +14 -0
- data/lib/relaton/cen/model/structured_identifier.rb +9 -0
- data/lib/relaton/cen/processor.rb +45 -0
- data/lib/relaton/cen/scraper.rb +218 -0
- data/lib/relaton/cen/util.rb +8 -0
- data/lib/relaton/cen.rb +30 -0
- data/lib/relaton/cie/bibdata.rb +8 -0
- data/lib/relaton/cie/bibitem.rb +8 -0
- data/lib/relaton/cie/bibliography.rb +31 -0
- data/lib/relaton/cie/data_fetcher.rb +540 -0
- data/lib/relaton/cie/ext.rb +7 -0
- data/lib/relaton/cie/item.rb +11 -0
- data/lib/relaton/cie/item_data.rb +6 -0
- data/lib/relaton/cie/processor.rb +68 -0
- data/lib/relaton/cie/scrapper.rb +52 -0
- data/lib/relaton/cie/util.rb +8 -0
- data/lib/relaton/cie.rb +29 -0
- data/lib/relaton/core/array_wrapper.rb +20 -0
- data/lib/relaton/core/data_fetcher.rb +244 -0
- data/lib/relaton/core/date_parser.rb +42 -0
- data/lib/relaton/core/governor.rb +320 -0
- data/lib/relaton/core/hash_keys_sybolizer.rb +19 -0
- data/lib/relaton/core/hit.rb +49 -0
- data/lib/relaton/core/hit_collection.rb +118 -0
- data/lib/relaton/core/pacer.rb +134 -0
- data/lib/relaton/core/processor.rb +67 -0
- data/lib/relaton/core/request_error.rb +14 -0
- data/lib/relaton/core/workers_pool.rb +45 -0
- data/lib/relaton/core.rb +12 -0
- data/lib/relaton/db/registry.rb +44 -4
- data/lib/relaton/db.rb +0 -1
- data/lib/relaton/doi/crossref.rb +89 -0
- data/lib/relaton/doi/parser.rb +921 -0
- data/lib/relaton/doi/processor.rb +65 -0
- data/lib/relaton/doi/util.rb +8 -0
- data/lib/relaton/doi.rb +20 -0
- data/lib/relaton/easc/bibdata.rb +8 -0
- data/lib/relaton/easc/bibitem.rb +8 -0
- data/lib/relaton/easc/bibliography.rb +95 -0
- data/lib/relaton/easc/docidentifier.rb +100 -0
- data/lib/relaton/easc/doctype.rb +14 -0
- data/lib/relaton/easc/ext.rb +44 -0
- data/lib/relaton/easc/item.rb +13 -0
- data/lib/relaton/easc/item_base.rb +18 -0
- data/lib/relaton/easc/item_data.rb +6 -0
- data/lib/relaton/easc/processor.rb +46 -0
- data/lib/relaton/easc/util.rb +8 -0
- data/lib/relaton/easc.rb +35 -0
- data/lib/relaton/ecma/bibdata.rb +8 -0
- data/lib/relaton/ecma/bibitem.rb +8 -0
- data/lib/relaton/ecma/bibliography.rb +149 -0
- data/lib/relaton/ecma/data_fetcher.rb +162 -0
- data/lib/relaton/ecma/data_parser.rb +49 -0
- data/lib/relaton/ecma/docidentifier.rb +124 -0
- data/lib/relaton/ecma/edition_parser.rb +80 -0
- data/lib/relaton/ecma/ext.rb +7 -0
- data/lib/relaton/ecma/item.rb +13 -0
- data/lib/relaton/ecma/item_data.rb +6 -0
- data/lib/relaton/ecma/memento_parser.rb +60 -0
- data/lib/relaton/ecma/page_fetcher.rb +39 -0
- data/lib/relaton/ecma/parser_common.rb +33 -0
- data/lib/relaton/ecma/processor.rb +69 -0
- data/lib/relaton/ecma/standard_parser.rb +134 -0
- data/lib/relaton/ecma/util.rb +8 -0
- data/lib/relaton/ecma.rb +33 -0
- data/lib/relaton/etsi/bibdata.rb +10 -0
- data/lib/relaton/etsi/bibitem.rb +10 -0
- data/lib/relaton/etsi/bibliography.rb +111 -0
- data/lib/relaton/etsi/data_fetcher.rb +167 -0
- data/lib/relaton/etsi/data_parser.rb +208 -0
- data/lib/relaton/etsi/doctype.rb +30 -0
- data/lib/relaton/etsi/ext.rb +31 -0
- data/lib/relaton/etsi/item.rb +15 -0
- data/lib/relaton/etsi/item_data.rb +6 -0
- data/lib/relaton/etsi/processor.rb +71 -0
- data/lib/relaton/etsi/pubid.rb +37 -0
- data/lib/relaton/etsi/status.rb +13 -0
- data/lib/relaton/etsi/util.rb +8 -0
- data/lib/relaton/etsi.rb +26 -0
- data/lib/relaton/gb/bibdata.rb +8 -0
- data/lib/relaton/gb/bibitem.rb +8 -0
- data/lib/relaton/gb/bibliography.rb +171 -0
- data/lib/relaton/gb/ccs.rb +14 -0
- data/lib/relaton/gb/committee.rb +13 -0
- data/lib/relaton/gb/docidentifier.rb +72 -0
- data/lib/relaton/gb/doctype.rb +9 -0
- data/lib/relaton/gb/ext.rb +36 -0
- data/lib/relaton/gb/gb_scraper.rb +61 -0
- data/lib/relaton/gb/gb_type.rb +20 -0
- data/lib/relaton/gb/hit.rb +48 -0
- data/lib/relaton/gb/hit_collection.rb +19 -0
- data/lib/relaton/gb/item.rb +13 -0
- data/lib/relaton/gb/item_data.rb +6 -0
- data/lib/relaton/gb/processor.rb +49 -0
- data/lib/relaton/gb/project_number.rb +38 -0
- data/lib/relaton/gb/scraper.rb +221 -0
- data/lib/relaton/gb/sec_scraper.rb +92 -0
- data/lib/relaton/gb/stage_name.rb +13 -0
- data/lib/relaton/gb/structured_identifier.rb +26 -0
- data/lib/relaton/gb/t_scraper.rb +126 -0
- data/lib/relaton/gb/util.rb +8 -0
- data/lib/relaton/gb/yaml/prefixes.yaml +200 -0
- data/lib/relaton/gb.rb +33 -0
- data/lib/relaton/gost/bibdata.rb +8 -0
- data/lib/relaton/gost/bibitem.rb +8 -0
- data/lib/relaton/gost/bibliography.rb +107 -0
- data/lib/relaton/gost/docidentifier.rb +80 -0
- data/lib/relaton/gost/doctype.rb +16 -0
- data/lib/relaton/gost/ext.rb +46 -0
- data/lib/relaton/gost/item.rb +15 -0
- data/lib/relaton/gost/item_base.rb +18 -0
- data/lib/relaton/gost/item_data.rb +6 -0
- data/lib/relaton/gost/processor.rb +49 -0
- data/lib/relaton/gost/util.rb +8 -0
- data/lib/relaton/gost.rb +36 -0
- data/lib/relaton/iala/bibdata.rb +8 -0
- data/lib/relaton/iala/bibitem.rb +8 -0
- data/lib/relaton/iala/bibliography.rb +146 -0
- data/lib/relaton/iala/docidentifier.rb +89 -0
- data/lib/relaton/iala/doctype.rb +18 -0
- data/lib/relaton/iala/ext.rb +32 -0
- data/lib/relaton/iala/item.rb +21 -0
- data/lib/relaton/iala/item_base.rb +18 -0
- data/lib/relaton/iala/item_data.rb +6 -0
- data/lib/relaton/iala/processor.rb +43 -0
- data/lib/relaton/iala/relation.rb +7 -0
- data/lib/relaton/iala/util.rb +8 -0
- data/lib/relaton/iala.rb +35 -0
- data/lib/relaton/iana/bibdata.rb +8 -0
- data/lib/relaton/iana/bibitem.rb +8 -0
- data/lib/relaton/iana/bibliography.rb +100 -0
- data/lib/relaton/iana/data_fetcher.rb +101 -0
- data/lib/relaton/iana/item.rb +7 -0
- data/lib/relaton/iana/item_data.rb +6 -0
- data/lib/relaton/iana/parser.rb +146 -0
- data/lib/relaton/iana/processor.rb +70 -0
- data/lib/relaton/iana/util.rb +8 -0
- data/lib/relaton/iana.rb +40 -0
- data/lib/relaton/iec/bibliography.rb +283 -0
- data/lib/relaton/iec/data_fetcher.rb +222 -0
- data/lib/relaton/iec/data_parser.rb +391 -0
- data/lib/relaton/iec/hit.rb +26 -0
- data/lib/relaton/iec/hit_collection.rb +138 -0
- data/lib/relaton/iec/item_data.rb +7 -0
- data/lib/relaton/iec/model/bibdata.rb +8 -0
- data/lib/relaton/iec/model/bibitem.rb +8 -0
- data/lib/relaton/iec/model/docidentifier.rb +135 -0
- data/lib/relaton/iec/model/doctype.rb +12 -0
- data/lib/relaton/iec/model/ext.rb +53 -0
- data/lib/relaton/iec/model/item.rb +20 -0
- data/lib/relaton/iec/model/item_base.rb +12 -0
- data/lib/relaton/iec/model/relation.rb +7 -0
- data/lib/relaton/iec/model/stage_name.rb +13 -0
- data/lib/relaton/iec/processor.rb +74 -0
- data/lib/relaton/iec/statuses.yml +199 -0
- data/lib/relaton/iec/util.rb +8 -0
- data/lib/relaton/iec.rb +98 -0
- data/lib/relaton/ieee/balloting_group.rb +13 -0
- data/lib/relaton/ieee/bibdata.rb +8 -0
- data/lib/relaton/ieee/bibitem.rb +8 -0
- data/lib/relaton/ieee/bibliography.rb +73 -0
- data/lib/relaton/ieee/converter/bibxml/from_rfcxml.rb +10 -0
- data/lib/relaton/ieee/converter/bibxml/from_rfcxml_referencegroup.rb +10 -0
- data/lib/relaton/ieee/converter/bibxml.rb +20 -0
- data/lib/relaton/ieee/data_fetcher.rb +771 -0
- data/lib/relaton/ieee/doctype.rb +9 -0
- data/lib/relaton/ieee/editorial_group.rb +19 -0
- data/lib/relaton/ieee/ext.rb +36 -0
- data/lib/relaton/ieee/idams_parser.rb +330 -0
- data/lib/relaton/ieee/item.rb +11 -0
- data/lib/relaton/ieee/item_data.rb +7 -0
- data/lib/relaton/ieee/processor.rb +71 -0
- data/lib/relaton/ieee/rawbib_id_parser.rb +753 -0
- data/lib/relaton/ieee/util.rb +8 -0
- data/lib/relaton/ieee.rb +30 -0
- data/lib/relaton/ietf/bibdata.rb +8 -0
- data/lib/relaton/ietf/bibitem.rb +8 -0
- data/lib/relaton/ietf/bibliography.rb +35 -0
- data/lib/relaton/ietf/bibxml_parser.rb +232 -0
- data/lib/relaton/ietf/data_fetcher.rb +561 -0
- data/lib/relaton/ietf/doctype.rb +9 -0
- data/lib/relaton/ietf/ext.rb +63 -0
- data/lib/relaton/ietf/item.rb +16 -0
- data/lib/relaton/ietf/item_base.rb +18 -0
- data/lib/relaton/ietf/item_data.rb +6 -0
- data/lib/relaton/ietf/processing_instructions.rb +79 -0
- data/lib/relaton/ietf/processor.rb +72 -0
- data/lib/relaton/ietf/relation.rb +9 -0
- data/lib/relaton/ietf/rfc/abstract.rb +19 -0
- data/lib/relaton/ietf/rfc/author.rb +21 -0
- data/lib/relaton/ietf/rfc/entry.rb +446 -0
- data/lib/relaton/ietf/rfc/entry_date.rb +21 -0
- data/lib/relaton/ietf/rfc/format.rb +19 -0
- data/lib/relaton/ietf/rfc/index.rb +46 -0
- data/lib/relaton/ietf/rfc/is_also.rb +21 -0
- data/lib/relaton/ietf/rfc/keywords.rb +19 -0
- data/lib/relaton/ietf/rfc/rfc_index_namespace.rb +11 -0
- data/lib/relaton/ietf/scraper.rb +101 -0
- data/lib/relaton/ietf/util.rb +8 -0
- data/lib/relaton/ietf/wg_name_resolver.rb +42 -0
- data/lib/relaton/ietf.rb +30 -0
- data/lib/relaton/iho/bibdata.rb +8 -0
- data/lib/relaton/iho/bibitem.rb +8 -0
- data/lib/relaton/iho/bibliography.rb +114 -0
- data/lib/relaton/iho/comment_period.rb +13 -0
- data/lib/relaton/iho/docidentifier.rb +31 -0
- data/lib/relaton/iho/doctype.rb +10 -0
- data/lib/relaton/iho/ext.rb +20 -0
- data/lib/relaton/iho/hash_parser_v1.rb +144 -0
- data/lib/relaton/iho/item.rb +20 -0
- data/lib/relaton/iho/item_base.rb +18 -0
- data/lib/relaton/iho/item_data.rb +6 -0
- data/lib/relaton/iho/processor.rb +54 -0
- data/lib/relaton/iho/relation.rb +9 -0
- data/lib/relaton/iho/structured_identifier.rb +20 -0
- data/lib/relaton/iho/util.rb +8 -0
- data/lib/relaton/iho.rb +27 -0
- data/lib/relaton/index/config.rb +52 -0
- data/lib/relaton/index/file_io.rb +305 -0
- data/lib/relaton/index/file_storage.rb +66 -0
- data/lib/relaton/index/pool.rb +46 -0
- data/lib/relaton/index/shard_source.rb +201 -0
- data/lib/relaton/index/type.rb +210 -0
- data/lib/relaton/index/util.rb +18 -0
- data/lib/relaton/index.rb +61 -0
- data/lib/relaton/isbn/isbn.rb +61 -0
- data/lib/relaton/isbn/open_library.rb +43 -0
- data/lib/relaton/isbn/parser.rb +88 -0
- data/lib/relaton/isbn/processor.rb +48 -0
- data/lib/relaton/isbn/util.rb +8 -0
- data/lib/relaton/isbn.rb +19 -0
- data/lib/relaton/iso/bibliography.rb +412 -0
- data/lib/relaton/iso/data_fetcher.rb +338 -0
- data/lib/relaton/iso/data_parser.rb +458 -0
- data/lib/relaton/iso/hash_parser_v1.rb +177 -0
- data/lib/relaton/iso/hit.rb +57 -0
- data/lib/relaton/iso/hit_collection.rb +196 -0
- data/lib/relaton/iso/item_data.rb +50 -0
- data/lib/relaton/iso/model/bibdata.rb +10 -0
- data/lib/relaton/iso/model/bibitem.rb +8 -0
- data/lib/relaton/iso/model/contributor.rb +6 -0
- data/lib/relaton/iso/model/contributor_info.rb +9 -0
- data/lib/relaton/iso/model/docidentifier.rb +146 -0
- data/lib/relaton/iso/model/doctype.rb +13 -0
- data/lib/relaton/iso/model/ext.rb +35 -0
- data/lib/relaton/iso/model/item.rb +17 -0
- data/lib/relaton/iso/model/item_base.rb +22 -0
- data/lib/relaton/iso/model/organization.rb +9 -0
- data/lib/relaton/iso/model/project_number.rb +22 -0
- data/lib/relaton/iso/model/relation.rb +9 -0
- data/lib/relaton/iso/model/stagename.rb +14 -0
- data/lib/relaton/iso/model/structured_identifier.rb +31 -0
- data/lib/relaton/iso/processor.rb +85 -0
- data/lib/relaton/iso/scraper.rb +635 -0
- data/lib/relaton/iso/type/pubid.rb +50 -0
- data/lib/relaton/iso/util.rb +8 -0
- data/lib/relaton/iso.rb +30 -0
- data/lib/relaton/itu/bibliography.rb +95 -0
- data/lib/relaton/itu/data_crawler_r.rb +664 -0
- data/lib/relaton/itu/data_fetcher.rb +550 -0
- data/lib/relaton/itu/data_merge_r.rb +149 -0
- data/lib/relaton/itu/data_parser_r.rb +214 -0
- data/lib/relaton/itu/data_parser_t.rb +228 -0
- data/lib/relaton/itu/family_cache.rb +177 -0
- data/lib/relaton/itu/governor.rb +56 -0
- data/lib/relaton/itu/hit.rb +28 -0
- data/lib/relaton/itu/hit_collection.rb +299 -0
- data/lib/relaton/itu/item_data.rb +6 -0
- data/lib/relaton/itu/model/approval_stage.rb +13 -0
- data/lib/relaton/itu/model/bibdata.rb +8 -0
- data/lib/relaton/itu/model/bibitem.rb +8 -0
- data/lib/relaton/itu/model/docidentifier.rb +75 -0
- data/lib/relaton/itu/model/doctype.rb +13 -0
- data/lib/relaton/itu/model/ext.rb +47 -0
- data/lib/relaton/itu/model/item.rb +13 -0
- data/lib/relaton/itu/model/meeting.rb +13 -0
- data/lib/relaton/itu/model/meeting_date.rb +15 -0
- data/lib/relaton/itu/model/question.rb +13 -0
- data/lib/relaton/itu/model/recommendation_status.rb +11 -0
- data/lib/relaton/itu/model/structured_identifier.rb +38 -0
- data/lib/relaton/itu/processor.rb +72 -0
- data/lib/relaton/itu/pubid.rb +199 -0
- data/lib/relaton/itu/radio_regulations_parser.rb +70 -0
- data/lib/relaton/itu/recommendation_fields.rb +334 -0
- data/lib/relaton/itu/recommendation_parser.rb +30 -0
- data/lib/relaton/itu/scraper.rb +203 -0
- data/lib/relaton/itu/util.rb +8 -0
- data/lib/relaton/itu.rb +34 -0
- data/lib/relaton/jcgm/bibdata.rb +8 -0
- data/lib/relaton/jcgm/bibitem.rb +8 -0
- data/lib/relaton/jcgm/bibliography.rb +97 -0
- data/lib/relaton/jcgm/data_fetcher.rb +81 -0
- data/lib/relaton/jcgm/docidentifier.rb +102 -0
- data/lib/relaton/jcgm/doctype.rb +12 -0
- data/lib/relaton/jcgm/ext.rb +23 -0
- data/lib/relaton/jcgm/item.rb +20 -0
- data/lib/relaton/jcgm/item_base.rb +18 -0
- data/lib/relaton/jcgm/item_data.rb +6 -0
- data/lib/relaton/jcgm/meetings_parser.rb +175 -0
- data/lib/relaton/jcgm/processor.rb +71 -0
- data/lib/relaton/jcgm/relation.rb +9 -0
- data/lib/relaton/jcgm/structured_identifier.rb +40 -0
- data/lib/relaton/jcgm/util.rb +8 -0
- data/lib/relaton/jcgm.rb +24 -0
- data/lib/relaton/jis/bibdata.rb +8 -0
- data/lib/relaton/jis/bibitem.rb +8 -0
- data/lib/relaton/jis/bibliography.rb +73 -0
- data/lib/relaton/jis/data_fetcher.rb +183 -0
- data/lib/relaton/jis/docidentifier.rb +120 -0
- data/lib/relaton/jis/doctype.rb +9 -0
- data/lib/relaton/jis/ext.rb +13 -0
- data/lib/relaton/jis/hit.rb +88 -0
- data/lib/relaton/jis/hit_collection.rb +128 -0
- data/lib/relaton/jis/item.rb +22 -0
- data/lib/relaton/jis/item_base.rb +26 -0
- data/lib/relaton/jis/item_data.rb +8 -0
- data/lib/relaton/jis/processor.rb +55 -0
- data/lib/relaton/jis/relation.rb +11 -0
- data/lib/relaton/jis/scraper.rb +232 -0
- data/lib/relaton/jis/structured_identifier.rb +13 -0
- data/lib/relaton/jis/util.rb +8 -0
- data/lib/relaton/jis.rb +32 -0
- data/lib/relaton/logger/channels/gh_issue.rb +143 -0
- data/lib/relaton/logger/config.rb +34 -0
- data/lib/relaton/logger/formatter_json.rb +8 -0
- data/lib/relaton/logger/formatter_string.rb +12 -0
- data/lib/relaton/logger/log.rb +64 -0
- data/lib/relaton/logger/log_device.rb +27 -0
- data/lib/relaton/logger/pool.rb +24 -0
- data/lib/relaton/logger.rb +18 -0
- data/lib/relaton/nist/bibdata.rb +8 -0
- data/lib/relaton/nist/bibitem.rb +8 -0
- data/lib/relaton/nist/bibliography.rb +191 -0
- data/lib/relaton/nist/comment_period.rb +15 -0
- data/lib/relaton/nist/data_fetcher.rb +134 -0
- data/lib/relaton/nist/date.rb +7 -0
- data/lib/relaton/nist/docidentifier.rb +165 -0
- data/lib/relaton/nist/doctype.rb +7 -0
- data/lib/relaton/nist/ext.rb +16 -0
- data/lib/relaton/nist/hit.rb +18 -0
- data/lib/relaton/nist/hit_collection.rb +362 -0
- data/lib/relaton/nist/item.rb +19 -0
- data/lib/relaton/nist/item_base.rb +16 -0
- data/lib/relaton/nist/item_data.rb +6 -0
- data/lib/relaton/nist/mods_parser.rb +302 -0
- data/lib/relaton/nist/processor.rb +69 -0
- data/lib/relaton/nist/pubs_export.rb +69 -0
- data/lib/relaton/nist/relation.rb +10 -0
- data/lib/relaton/nist/scraper.rb +293 -0
- data/lib/relaton/nist/series.yaml +49 -0
- data/lib/relaton/nist/util.rb +8 -0
- data/lib/relaton/nist.rb +34 -0
- data/lib/relaton/oasis/bibdata.rb +8 -0
- data/lib/relaton/oasis/bibitem.rb +8 -0
- data/lib/relaton/oasis/bibliography.rb +201 -0
- data/lib/relaton/oasis/browser_agent.rb +71 -0
- data/lib/relaton/oasis/data_fetcher.rb +130 -0
- data/lib/relaton/oasis/data_parser.rb +305 -0
- data/lib/relaton/oasis/data_parser_utils.rb +330 -0
- data/lib/relaton/oasis/data_part_parser.rb +239 -0
- data/lib/relaton/oasis/docidentifier.rb +54 -0
- data/lib/relaton/oasis/doctype.rb +7 -0
- data/lib/relaton/oasis/ext.rb +19 -0
- data/lib/relaton/oasis/item.rb +14 -0
- data/lib/relaton/oasis/item_data.rb +6 -0
- data/lib/relaton/oasis/processor.rb +74 -0
- data/lib/relaton/oasis/util.rb +8 -0
- data/lib/relaton/oasis.rb +41 -0
- data/lib/relaton/ogc/bibdata.rb +8 -0
- data/lib/relaton/ogc/bibitem.rb +8 -0
- data/lib/relaton/ogc/bibliography.rb +64 -0
- data/lib/relaton/ogc/data_fetcher.rb +131 -0
- data/lib/relaton/ogc/docidentifier.rb +111 -0
- data/lib/relaton/ogc/doctype.rb +12 -0
- data/lib/relaton/ogc/ext.rb +15 -0
- data/lib/relaton/ogc/hit.rb +18 -0
- data/lib/relaton/ogc/hit_collection.rb +111 -0
- data/lib/relaton/ogc/item.rb +13 -0
- data/lib/relaton/ogc/item_data.rb +6 -0
- data/lib/relaton/ogc/processor.rb +59 -0
- data/lib/relaton/ogc/scraper.rb +225 -0
- data/lib/relaton/ogc/util.rb +8 -0
- data/lib/relaton/ogc.rb +30 -0
- data/lib/relaton/oiml/bibdata.rb +8 -0
- data/lib/relaton/oiml/bibitem.rb +8 -0
- data/lib/relaton/oiml/bibliography.rb +169 -0
- data/lib/relaton/oiml/docidentifier.rb +48 -0
- data/lib/relaton/oiml/doctype.rb +13 -0
- data/lib/relaton/oiml/ext.rb +42 -0
- data/lib/relaton/oiml/item.rb +20 -0
- data/lib/relaton/oiml/item_base.rb +18 -0
- data/lib/relaton/oiml/item_data.rb +6 -0
- data/lib/relaton/oiml/processor.rb +54 -0
- data/lib/relaton/oiml/relation.rb +9 -0
- data/lib/relaton/oiml/util.rb +8 -0
- data/lib/relaton/oiml.rb +24 -0
- data/lib/relaton/omg/bibdata.rb +8 -0
- data/lib/relaton/omg/bibitem.rb +8 -0
- data/lib/relaton/omg/bibliography.rb +31 -0
- data/lib/relaton/omg/docidentifier.rb +67 -0
- data/lib/relaton/omg/ext.rb +7 -0
- data/lib/relaton/omg/item.rb +9 -0
- data/lib/relaton/omg/item_data.rb +6 -0
- data/lib/relaton/omg/processor.rb +35 -0
- data/lib/relaton/omg/scraper.rb +186 -0
- data/lib/relaton/omg/util.rb +8 -0
- data/lib/relaton/omg.rb +25 -0
- data/lib/relaton/plateau/bibdata.rb +8 -0
- data/lib/relaton/plateau/bibitem.rb +8 -0
- data/lib/relaton/plateau/bibliography.rb +31 -0
- data/lib/relaton/plateau/data_fetcher.rb +176 -0
- data/lib/relaton/plateau/doctype.rb +7 -0
- data/lib/relaton/plateau/ext.rb +23 -0
- data/lib/relaton/plateau/handbook_parser.rb +102 -0
- data/lib/relaton/plateau/hit.rb +21 -0
- data/lib/relaton/plateau/hit_collection.rb +79 -0
- data/lib/relaton/plateau/item.rb +12 -0
- data/lib/relaton/plateau/item_data.rb +6 -0
- data/lib/relaton/plateau/parser.rb +101 -0
- data/lib/relaton/plateau/processor.rb +51 -0
- data/lib/relaton/plateau/technical_report_parser.rb +98 -0
- data/lib/relaton/plateau/util.rb +8 -0
- data/lib/relaton/plateau.rb +29 -0
- data/lib/relaton/sdo/config.rb +34 -0
- data/lib/relaton/sdo/fetcher.rb +52 -0
- data/lib/relaton/sdo/logo.rb +95 -0
- data/lib/relaton/sdo/name.rb +26 -0
- data/lib/relaton/sdo/organization.rb +71 -0
- data/lib/relaton/sdo/store.rb +49 -0
- data/lib/relaton/sdo.rb +29 -0
- data/lib/relaton/un/bibdata.rb +8 -0
- data/lib/relaton/un/bibitem.rb +8 -0
- data/lib/relaton/un/bibliography.rb +48 -0
- data/lib/relaton/un/doctype.rb +10 -0
- data/lib/relaton/un/ext.rb +30 -0
- data/lib/relaton/un/hit.rb +56 -0
- data/lib/relaton/un/hit_collection.rb +61 -0
- data/lib/relaton/un/item.rb +11 -0
- data/lib/relaton/un/item_data.rb +6 -0
- data/lib/relaton/un/parser.rb +108 -0
- data/lib/relaton/un/processor.rb +38 -0
- data/lib/relaton/un/session.rb +25 -0
- data/lib/relaton/un/token_generator.rb +105 -0
- data/lib/relaton/un/util.rb +8 -0
- data/lib/relaton/un/wasm/decoder.rb +434 -0
- data/lib/relaton/un/wasm/instance.rb +101 -0
- data/lib/relaton/un/wasm/interpreter.rb +613 -0
- data/lib/relaton/un/wasm/memory.rb +112 -0
- data/lib/relaton/un/wasm/module.rb +47 -0
- data/lib/relaton/un/wasm.rb +17 -0
- data/lib/relaton/un/wasm_v_bg.wasm +0 -0
- data/lib/relaton/un.rb +28 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibdata.rb +8 -0
- data/lib/relaton/w3c/bibitem.rb +8 -0
- data/lib/relaton/w3c/bibliography.rb +174 -0
- data/lib/relaton/w3c/data_fetcher.rb +487 -0
- data/lib/relaton/w3c/data_parser.rb +379 -0
- data/lib/relaton/w3c/docidentifier.rb +48 -0
- data/lib/relaton/w3c/doctype.rb +7 -0
- data/lib/relaton/w3c/ext.rb +11 -0
- data/lib/relaton/w3c/governor.rb +32 -0
- data/lib/relaton/w3c/item.rb +15 -0
- data/lib/relaton/w3c/item_data.rb +6 -0
- data/lib/relaton/w3c/processor.rb +49 -0
- data/lib/relaton/w3c/pubid.rb +85 -0
- data/lib/relaton/w3c/safe_realize.rb +148 -0
- data/lib/relaton/w3c/util.rb +8 -0
- data/lib/relaton/w3c.rb +36 -0
- data/lib/relaton/xsf/bibdata.rb +8 -0
- data/lib/relaton/xsf/bibitem.rb +8 -0
- data/lib/relaton/xsf/bibliography.rb +84 -0
- data/lib/relaton/xsf/data_fetcher.rb +106 -0
- data/lib/relaton/xsf/docidentifier.rb +46 -0
- data/lib/relaton/xsf/hit.rb +17 -0
- data/lib/relaton/xsf/hit_collection.rb +52 -0
- data/lib/relaton/xsf/item.rb +13 -0
- data/lib/relaton/xsf/item_data.rb +6 -0
- data/lib/relaton/xsf/processor.rb +51 -0
- data/lib/relaton/xsf/util.rb +9 -0
- data/lib/relaton/xsf.rb +34 -0
- data/lib/relaton-core.rb +1 -0
- data/lib/relaton.rb +85 -0
- metadata +775 -161
- data/.github/workflows/rake.yml +0 -14
- data/.github/workflows/release.yml +0 -24
- data/.github/workflows/rubocop.yml +0 -48
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/CLAUDE.md +0 -78
- data/Gemfile +0 -29
- data/Rakefile +0 -6
- data/docs/CHANGELOG.adoc +0 -16
- data/docs/VERSIONING_POLICY.adoc +0 -38
- data/docs/navigation.adoc +0 -6
- data/lib/relaton/db/version.rb +0 -5
- data/relaton.gemspec +0 -72
- data/spec/relaton/config_spec.rb +0 -10
- data/spec/relaton/db_cache_spec.rb +0 -51
- data/spec/relaton/db_spec.rb +0 -567
- data/spec/relaton/registry_spec.rb +0 -178
- data/spec/relaton/util_spec.rb +0 -3
- data/spec/relaton_meta_spec.rb +0 -25
- data/spec/relaton_spec.rb +0 -800
- data/spec/spec_helper.rb +0 -45
- data/spec/support/gb_t_20223_2006.xml +0 -33
- data/spec/support/iso_111111119115_1.xml +0 -0
- data/spec/support/iso_19115_1.xml +0 -115
- data/spec/support/iso_19115_2.xml +0 -95
- data/spec/support/rfc_8341.xml +0 -46
- data/spec/vcr_cassetes/api_relaton_org.yml +0 -121
- data/spec/vcr_cassetes/api_relaton_org_unavailable.yml +0 -140
- data/spec/vcr_cassetes/cc_dir_10005_2019.yml +0 -202
- data/spec/vcr_cassetes/cipm_meeting_43.yml +0 -1505
- data/spec/vcr_cassetes/gb_t_20223_2006.yml +0 -729
- data/spec/vcr_cassetes/iso_111111119115_1.yml +0 -12951
- data/spec/vcr_cassetes/iso_19115_1.yml +0 -17334
- data/spec/vcr_cassetes/iso_19115_1_2.yml +0 -322
- data/spec/vcr_cassetes/iso_19115_1_std.yml +0 -17334
- data/spec/vcr_cassetes/iso_19115_all_parts.yml +0 -185
- data/spec/vcr_cassetes/iso_19133_2005.yml +0 -144
- data/spec/vcr_cassetes/iso_combined_applied.yml +0 -318
- data/spec/vcr_cassetes/iso_combined_included.yml +0 -318
- data/spec/vcr_cassetes/ogc_19_025r1.yml +0 -374
- data/spec/vcr_cassetes/omg_ami4ccm_1_0.yml +0 -317
- data/spec/vcr_cassetes/rfc_8341.yml +0 -1278
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
require "mechanize"
|
|
2
|
+
require_relative "../itu"
|
|
3
|
+
require_relative "../core/pacer"
|
|
4
|
+
require_relative "data_parser_r"
|
|
5
|
+
require_relative "governor"
|
|
6
|
+
|
|
7
|
+
module Relaton
|
|
8
|
+
module Itu
|
|
9
|
+
# The ITU-R harvester (issue #75) — the replacement for the decommissioned
|
|
10
|
+
# RunSearch bulk enumeration.
|
|
11
|
+
#
|
|
12
|
+
# ITU-R metadata is still fully server-rendered under `/pub` + `/rec`, in
|
|
13
|
+
# three levels — Recommendations:
|
|
14
|
+
#
|
|
15
|
+
# /pub/R-REC/en -> the 16 series
|
|
16
|
+
# /rec/R-REC-BO/en -> one row per document (54 for BO)
|
|
17
|
+
# /rec/R-REC-BO.1130/en -> one row per edition (Main + Previous)
|
|
18
|
+
# /rec/R-REC-BO.1130-5-202602-I/en -> approval day + the PDF href
|
|
19
|
+
#
|
|
20
|
+
# and Reports, the same shape under a different root (see FAMILIES):
|
|
21
|
+
#
|
|
22
|
+
# /pub/R-REP/en -> the 14 report series
|
|
23
|
+
# /pub/R-REP-BO/en -> one row per document (36 for BO)
|
|
24
|
+
# /pub/R-REP-BO.1227/en -> one row per edition
|
|
25
|
+
# /pub/R-REP-BO.1227-2-1998/en -> the files' posted date + PDF href
|
|
26
|
+
#
|
|
27
|
+
# `DataFetcher#fetch_publications` drives it, requiring this file lazily so
|
|
28
|
+
# an ITU-T-only crawl never loads it. Standalone use — note the write goes
|
|
29
|
+
# through DataMergeR, never straight to #write_file, so a partial harvest
|
|
30
|
+
# can't overwrite what it cannot see:
|
|
31
|
+
#
|
|
32
|
+
# require "relaton/itu/data_merge_r"
|
|
33
|
+
# fetcher = Relaton::Itu::DataFetcher.new("data", "yaml")
|
|
34
|
+
# crawler = Relaton::Itu::DataCrawlerR.new
|
|
35
|
+
# items = crawler.harvest("BO") + crawler.harvest("BO", family: "R-REP")
|
|
36
|
+
# Relaton::Itu::DataMergeR.write_all items, fetcher
|
|
37
|
+
# fetcher.index.save
|
|
38
|
+
#
|
|
39
|
+
# **Politeness is not this class's `sleep` any more.** Levels 2 and 3 run on
|
|
40
|
+
# a small worker pool (`#in_parallel`, one Mechanize agent per thread), and
|
|
41
|
+
# the rate is set by a shared `Relaton::Core::Pacer` — `delay:` is the
|
|
42
|
+
# minimum gap between request *starts*, so ITU's own latency counts toward it
|
|
43
|
+
# instead of being added on top. A `Relaton::Itu::Governor` gives the pool one
|
|
44
|
+
# shared cooldown for a WAF block. See `#get` and lib/relaton/itu/CLAUDE.md.
|
|
45
|
+
# The crawl is parallel **within** a series only: `DataFetcher#harvest_family`
|
|
46
|
+
# rescues per series and `DataMergeR` writes per series, and the data repo's
|
|
47
|
+
# collapse guard is calibrated on that granularity.
|
|
48
|
+
#
|
|
49
|
+
class DataCrawlerR
|
|
50
|
+
DOMAIN = "https://www.itu.int".freeze
|
|
51
|
+
|
|
52
|
+
# Both families are the same three-level tree with different URL roots and
|
|
53
|
+
# a different date field. `date:` is the substantive difference, not a
|
|
54
|
+
# detail: a Recommendation page publishes only its **approval** date,
|
|
55
|
+
# while a Report page carries the "Posted" date of its files — which is
|
|
56
|
+
# the **publication** date the preserved records hold. So a harvested
|
|
57
|
+
# Report reproduces the published `date:`, and a harvested Recommendation
|
|
58
|
+
# cannot (see DataMergeR, which is why the merge never rewrites one).
|
|
59
|
+
FAMILIES = {
|
|
60
|
+
"R-REC" => {
|
|
61
|
+
index: "#{DOMAIN}/pub/R-REC/en",
|
|
62
|
+
page: "#{DOMAIN}/rec/%<id>s/en",
|
|
63
|
+
pdf: "#{DOMAIN}/dms_pubrec/itu-r/rec/%<series>s/%<id>s!!PDF-E.pdf",
|
|
64
|
+
date: :approved,
|
|
65
|
+
group: :series,
|
|
66
|
+
}.freeze,
|
|
67
|
+
"R-REP" => {
|
|
68
|
+
index: "#{DOMAIN}/pub/R-REP/en",
|
|
69
|
+
page: "#{DOMAIN}/pub/%<id>s/en",
|
|
70
|
+
pdf: "#{DOMAIN}/dms_pub/itu-r/opb/rep/%<id>s-PDF-E.pdf",
|
|
71
|
+
date: :posted,
|
|
72
|
+
group: :series,
|
|
73
|
+
}.freeze,
|
|
74
|
+
# Questions nest one level deeper: the family index lists **study
|
|
75
|
+
# groups** (R-QUE-SG01), not series letters. No `pdf:` template — a
|
|
76
|
+
# Question edition page usually offers no PDF of its own (verified on
|
|
77
|
+
# R-QUE-SG01.202-2-2002), and deriving a URL that 404s is worse than
|
|
78
|
+
# leaving the record sourceless.
|
|
79
|
+
"R-QUE" => {
|
|
80
|
+
index: "#{DOMAIN}/pub/R-QUE/en",
|
|
81
|
+
page: "#{DOMAIN}/pub/%<id>s/en",
|
|
82
|
+
date: :posted,
|
|
83
|
+
group: :sub_index,
|
|
84
|
+
}.freeze,
|
|
85
|
+
# Handbooks are flat like Resolutions, but they publish less per page
|
|
86
|
+
# than any other family, so two things move: the **date** comes from the
|
|
87
|
+
# id (an edition page carries no posted date, and the displayed codes
|
|
88
|
+
# disagree with each other — `R-HDB-43-2013` renders "2014" against its
|
|
89
|
+
# siblings' "2026" and "Edition of 2002"), and the **title** comes from
|
|
90
|
+
# the level-1 row, the only place ITU prints it.
|
|
91
|
+
"R-HDB" => {
|
|
92
|
+
index: "#{DOMAIN}/pub/R-HDB/en",
|
|
93
|
+
page: "#{DOMAIN}/pub/%<id>s/en",
|
|
94
|
+
pdf: "#{DOMAIN}/dms_pub/itu-r/opb/hdb/%<id>s-PDF-E.pdf",
|
|
95
|
+
date: :id,
|
|
96
|
+
title: :document,
|
|
97
|
+
group: :flat,
|
|
98
|
+
}.freeze,
|
|
99
|
+
# Resolutions are **flat**: /pub/R-RES/en links all 75 documents
|
|
100
|
+
# directly, with no grouping level at all.
|
|
101
|
+
"R-RES" => {
|
|
102
|
+
index: "#{DOMAIN}/pub/R-RES/en",
|
|
103
|
+
page: "#{DOMAIN}/pub/%<id>s/en",
|
|
104
|
+
pdf: "#{DOMAIN}/dms_pub/itu-r/opb/res/%<id>s-PDF-E.pdf",
|
|
105
|
+
date: :posted,
|
|
106
|
+
group: :flat,
|
|
107
|
+
}.freeze,
|
|
108
|
+
}.freeze
|
|
109
|
+
DEFAULT_FAMILY = "R-REC".freeze
|
|
110
|
+
|
|
111
|
+
# Every level is addressable by its id. The pages' own links are
|
|
112
|
+
# `./recommendation.asp?…&parent=<id>` / `publications.aspx?…&parent=<id>`,
|
|
113
|
+
# which resolve against the series directory and do NOT reach the
|
|
114
|
+
# canonical page — so the id is extracted from `parent=` and the URL
|
|
115
|
+
# rebuilt, never followed.
|
|
116
|
+
PARENT_RE = /[?&]parent=(?<id>[^"&\s]+)/.freeze
|
|
117
|
+
APPROVED_RE = /Approved in\s*(\d{4}-\d{2}(?:-\d{2})?)/.freeze
|
|
118
|
+
# A Report edition page dates each downloadable file ("Posted"). The day is
|
|
119
|
+
# the file's posting stamp; the corpus records these at month precision.
|
|
120
|
+
POSTED_RE = /\b((?:19|20)\d{2}-\d{2})-\d{2}\b/.freeze
|
|
121
|
+
# "R-REC-BO.1130-3-200007-S" -> "2000-07"
|
|
122
|
+
ID_DATE_RE = /-(\d{4})(\d{2})-[A-Za-z]+\z/.freeze
|
|
123
|
+
# "R-REP-BO.1227-2-1998" -> "1998"
|
|
124
|
+
ID_YEAR_RE = /-((?:19|20)\d{2})\z/.freeze
|
|
125
|
+
# Where a throttled /pub request lands instead of erroring.
|
|
126
|
+
NOTFOUND = "notfound.aspx".freeze
|
|
127
|
+
RETRIES = 3
|
|
128
|
+
RETRY_BACKOFF = 5 # seconds, multiplied by the attempt number
|
|
129
|
+
|
|
130
|
+
# The `/pub` soft block: a 302 to notfound.aspx, which Mechanize follows
|
|
131
|
+
# to a perfectly good 200. It stays a `Mechanize::ResponseCodeError`
|
|
132
|
+
# subclass so #get's rescue list is unchanged, and it mixes in the
|
|
133
|
+
# governor's marker so a rate limit can be told apart from the 404 it
|
|
134
|
+
# otherwise looks exactly like — which is the whole point: a 404 must cost
|
|
135
|
+
# one document, a soft block must pause the pool.
|
|
136
|
+
class SoftBlock < Mechanize::ResponseCodeError
|
|
137
|
+
include Governor::SoftBlock
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Worker threads for the two request-heavy levels of the walk. Deliberately
|
|
141
|
+
# small, and deliberately not the same knob as ITU-T's: the two halves have
|
|
142
|
+
# different bottlenecks. ITU-T is latency-bound. ITU-R is *pacer*-bound —
|
|
143
|
+
# throughput is min(1/delay, concurrency/latency), so at the 1 s default
|
|
144
|
+
# and ITU's ~1.1 s latency two workers already saturate the contract and
|
|
145
|
+
# four is only headroom for a slow page. More would buy nothing and only
|
|
146
|
+
# widen the burst the F5 WAF sees.
|
|
147
|
+
DEFAULT_CONCURRENCY = 4
|
|
148
|
+
|
|
149
|
+
# Each worker owns its agent, stashed here rather than passed down: every
|
|
150
|
+
# level of the walk calls #get, and threading an agent through
|
|
151
|
+
# #series/#documents/#editions/#edition would touch every signature for a
|
|
152
|
+
# value only #get uses.
|
|
153
|
+
AGENT_KEY = :relaton_itu_r_agent
|
|
154
|
+
|
|
155
|
+
# @param agent [Mechanize] one agent per crawler (Mechanize is not
|
|
156
|
+
# thread-safe, so a parallel harvester needs one per worker)
|
|
157
|
+
# @param delay [Numeric] politeness pause before each request — www.itu.int
|
|
158
|
+
# sits behind an F5 WAF that throttles, and a full crawl is ~10^4 pages
|
|
159
|
+
# @param concurrency [Integer] worker threads for the level-2/3 fetches
|
|
160
|
+
# @param pace_mode [Symbol] :slot (default) or :fixed — see Core::Pacer
|
|
161
|
+
# @param pacer [Relaton::Core::Pacer, nil] shared request pacer
|
|
162
|
+
# @param governor [Relaton::Itu::Governor, nil] pool-wide back-pressure
|
|
163
|
+
def initialize(agent: nil, delay: 0.5, concurrency: 1,
|
|
164
|
+
pace_mode: Core::Pacer::DEFAULT_MODE, pacer: nil, governor: nil)
|
|
165
|
+
@agent = agent
|
|
166
|
+
@delay = delay
|
|
167
|
+
@concurrency = [concurrency.to_i, 1].max
|
|
168
|
+
# The pacer, not the pool, sets the rate: `delay` is the minimum gap
|
|
169
|
+
# between request *starts*, shared by every worker, so ITU's own latency
|
|
170
|
+
# counts toward it instead of being added on top.
|
|
171
|
+
@pacer = pacer || Core::Pacer.new(gap: delay, mode: pace_mode)
|
|
172
|
+
@governor = governor || Governor.new
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# This thread's agent: a pool worker's own, or the crawler's shared one for
|
|
176
|
+
# the serial walk (and for every spec and direct #get). Built lazily so a
|
|
177
|
+
# crawler that only ever runs in workers never makes one.
|
|
178
|
+
def agent
|
|
179
|
+
Thread.current[AGENT_KEY] || (@agent ||= self.class.agent)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [Integer] rate-limit responses seen this crawl, for the summary
|
|
183
|
+
def throttle_count
|
|
184
|
+
@governor.throttle_count
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Boolean] the crawl was declared rate-limited and gave up
|
|
188
|
+
def abandoned?
|
|
189
|
+
@governor.exhausted?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Release the pool's Mechanize agents and their persistent connections.
|
|
193
|
+
# The pool is memoised for the whole crawl (deliberately — rebuilding it
|
|
194
|
+
# per series would churn ~300 agents), so nothing else can close them;
|
|
195
|
+
# DataFetcher#fetch_publications calls this in an ensure, mirroring what
|
|
196
|
+
# #fetch_recommendations already does for the ITU-T pool.
|
|
197
|
+
def shutdown
|
|
198
|
+
@worker_agents&.each(&:shutdown)
|
|
199
|
+
@worker_agents = nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Same hardening as DataFetcher#rec_agent: the browser UA is mandatory (the
|
|
203
|
+
# WAF rejects non-browser clients) and max_history keeps a long crawl from
|
|
204
|
+
# retaining every page it has read.
|
|
205
|
+
def self.agent
|
|
206
|
+
Mechanize.new.tap do |a|
|
|
207
|
+
a.user_agent_alias = "Mac Safari"
|
|
208
|
+
a.max_history = 1
|
|
209
|
+
a.open_timeout = 15
|
|
210
|
+
a.read_timeout = 60
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The grouping level between a family index and its documents. Three
|
|
215
|
+
# topologies, all real:
|
|
216
|
+
#
|
|
217
|
+
# :series R-REC/R-REP — 16 and 14 series letters (BO, BR, …)
|
|
218
|
+
# :sub_index R-QUE — 6 study groups (SG01, SG03, …)
|
|
219
|
+
# :flat R-RES — none; the index links all 75 documents
|
|
220
|
+
#
|
|
221
|
+
# A flat family yields a single nil group so the walk below stays one
|
|
222
|
+
# shape: index -> group -> document -> edition.
|
|
223
|
+
#
|
|
224
|
+
# @param family [String]
|
|
225
|
+
# @return [Array<String, nil>]
|
|
226
|
+
def series(family = DEFAULT_FAMILY)
|
|
227
|
+
conf = config(family)
|
|
228
|
+
return [nil] if conf[:group] == :flat
|
|
229
|
+
|
|
230
|
+
pattern = conf[:group] == :sub_index ? "(SG\\d+)" : "([A-Z]+)"
|
|
231
|
+
found = get(conf[:index]).search("//a").filter_map do |a|
|
|
232
|
+
a[:href].to_s[%r{/(?:rec|pub)/#{Regexp.escape family}-#{pattern}/en\z}, 1]
|
|
233
|
+
end.uniq
|
|
234
|
+
warn_if_empty found, "#{family} groups"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Level 1 — every document in a series.
|
|
238
|
+
#
|
|
239
|
+
# @param series [String] e.g. "BO"
|
|
240
|
+
# @param family [String] "R-REC" or "R-REP"
|
|
241
|
+
# @return [Array<Hash>] { id:, code:, title: }
|
|
242
|
+
def documents(series, family: DEFAULT_FAMILY)
|
|
243
|
+
conf = config(family)
|
|
244
|
+
# A flat family lists its documents on the family index itself; the
|
|
245
|
+
# others list them on the group page.
|
|
246
|
+
url, prefix = series ? [page_url("#{family}-#{series}"), "#{family}-#{series}."] : [conf[:index], "#{family}-"]
|
|
247
|
+
found = rows(url).filter_map do |id, anchor, cells|
|
|
248
|
+
# Every one of these pages links itself (parent=R-REC-BO,
|
|
249
|
+
# parent=R-QUE-SG01) for the language switcher, and a flat index links
|
|
250
|
+
# only documents — so the prefix test is what separates the two.
|
|
251
|
+
next unless id.start_with?(prefix) && id != prefix.chomp(".")
|
|
252
|
+
|
|
253
|
+
# The Handbook index lists *titles* where every other family lists
|
|
254
|
+
# numbers — its number is in the id alone and its title cell is empty.
|
|
255
|
+
name = squish(anchor.text)
|
|
256
|
+
{ id: id, code: name, title: conf[:title] == :document ? name : title_text(cells[1]) }
|
|
257
|
+
end
|
|
258
|
+
warn_if_empty found, "documents in #{series ? "#{family}-#{series}" : family}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Level 2 — every edition of a document ("Main" first, then "Previous
|
|
262
|
+
# versions"; the page is already newest-first).
|
|
263
|
+
#
|
|
264
|
+
# @param doc_id [String] e.g. "R-REC-BO.1130"
|
|
265
|
+
# @return [Array<Hash>] { id:, code:, title:, status: }
|
|
266
|
+
def editions(doc_id)
|
|
267
|
+
number = doc_id[/\AR-[A-Z]+-(.+)\z/, 1]
|
|
268
|
+
found = rows(page_url(doc_id)).filter_map do |id, anchor, cells|
|
|
269
|
+
next unless id.start_with? "#{doc_id}-" # skip the page's self-link
|
|
270
|
+
|
|
271
|
+
code = squish(anchor.text)
|
|
272
|
+
# An edition's code must be the document's own number. ITU lists a
|
|
273
|
+
# second row for some editions under a `…-P` id whose displayed Number
|
|
274
|
+
# is the associated *Question* (`/rec/R-REC-M.2083/en` shows both
|
|
275
|
+
# "M.2083-0 (09/2015)" and "M.5/BL/22 (09/2015)" for one edition) —
|
|
276
|
+
# taking that at face value mints a Recommendation docid out of a
|
|
277
|
+
# question number. Only the `/rec` pages carry those rows, and only
|
|
278
|
+
# there does the code start with the document number: a Resolution
|
|
279
|
+
# renders "Res.1-9 (2023)" against the id `R-RES-R.1-9-2023`.
|
|
280
|
+
next if family_of(doc_id) == "R-REC" && !code.start_with?(number)
|
|
281
|
+
|
|
282
|
+
{ id: id, code: code, title: title_text(cells[1]), status: squish(cells[2]&.text) }
|
|
283
|
+
end
|
|
284
|
+
# A Handbook edition is listed twice — once with an empty code, once with
|
|
285
|
+
# its year — so keep the richer row per id rather than harvesting the
|
|
286
|
+
# same edition twice.
|
|
287
|
+
found = found.group_by { |e| e[:id] }.map { |_, dupes| dupes.max_by { |e| e[:code].to_s.size } }
|
|
288
|
+
warn_if_empty found, "editions of #{doc_id}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Level 3 — the per-edition page. One request per edition, so this is what
|
|
292
|
+
# makes a full crawl expensive; it buys the day-precision approval date and
|
|
293
|
+
# a PDF href read off the page instead of derived from the id.
|
|
294
|
+
#
|
|
295
|
+
# @param edition_id [String] e.g. "R-REC-BO.1130-5-202602-I"
|
|
296
|
+
# @return [Hash] { date:, pdf: }
|
|
297
|
+
def edition(edition_id)
|
|
298
|
+
page = get(page_url(edition_id))
|
|
299
|
+
# Anchored on the edition id, which is what makes "no PDF ⇒ no source"
|
|
300
|
+
# true. The link shape differs per family ("…!!PDF-E.pdf" for a rec,
|
|
301
|
+
# "…-PDF-E.pdf" for a report) and every /pub page carries a QUICK LINKS
|
|
302
|
+
# sidebar whose Publication Catalogue entry is itself a `…-PDF-E.pdf` —
|
|
303
|
+
# an unanchored match would hand that catalogue URL to any report
|
|
304
|
+
# edition that has no English PDF of its own. Resolved against the page,
|
|
305
|
+
# not concatenated: ITU serves root-relative hrefs today, but an
|
|
306
|
+
# absolute one would give "https://www.itu.inthttps://…".
|
|
307
|
+
# `.pdf`-terminated, because /pub pages also carry an "add to cart"
|
|
308
|
+
# control whose href is `javascript:addcart(…,'R-REP-BT.2526-1-2024-PDF-E',…)`
|
|
309
|
+
# — it contains both the edition id and PDF-E, and feeding it to
|
|
310
|
+
# URI#merge raises URI::InvalidURIError, which once killed a whole series.
|
|
311
|
+
href = page.search("//a[contains(@href,'#{edition_id}') and contains(@href,'PDF-E')]")
|
|
312
|
+
.map { |a| a[:href].to_s.strip }
|
|
313
|
+
.find { |h| h.end_with? ".pdf" }
|
|
314
|
+
{ date: page_date(page, edition_id), pdf: href && page.uri.merge(href).to_s }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# End-to-end: a series -> parsed ItemData, one per edition.
|
|
318
|
+
#
|
|
319
|
+
# @param series [String] e.g. "BO"
|
|
320
|
+
# @param family [String] "R-REC" or "R-REP"
|
|
321
|
+
# @param only [Array<String>, nil] restrict to these document ids (specs,
|
|
322
|
+
# sampling runs)
|
|
323
|
+
# @param deep [Boolean] fetch level 3. false costs `1 + documents` requests
|
|
324
|
+
# and yields month-precision dates and a derived PDF URL; true adds one
|
|
325
|
+
# request per edition for the page's own date and PDF href.
|
|
326
|
+
# @param errors [Hash] shared error tally, as DataParserR expects
|
|
327
|
+
# @return [Array<Relaton::Itu::ItemData>]
|
|
328
|
+
def harvest(series, family: DEFAULT_FAMILY, only: nil, deep: true, errors: Hash.new(true), skip: nil)
|
|
329
|
+
docs = documents(series, family: family)
|
|
330
|
+
docs = docs.select { |d| only.include? d[:id] } if only
|
|
331
|
+
# A Handbook edition inherits the document's title (`title: :document`):
|
|
332
|
+
# its own row shows a year and nothing else, so this is the only chance
|
|
333
|
+
# to carry the title down — the level-3 fetch never sees the index.
|
|
334
|
+
inherited = config(family)[:title] == :document
|
|
335
|
+
# Level 2 — one request per document. Parallel, but only *within* this
|
|
336
|
+
# series: #harvest_family rescues per series and DataMergeR writes per
|
|
337
|
+
# series, and the data repo's guard_itur_harvest is calibrated on that
|
|
338
|
+
# granularity, so the series loop above stays strictly serial.
|
|
339
|
+
editions = in_parallel(docs) do |d|
|
|
340
|
+
editions(d[:id]).map do |ed|
|
|
341
|
+
ed.merge(family: family_of(ed[:id]), **(inherited ? { title: d[:title] } : {}))
|
|
342
|
+
end
|
|
343
|
+
end.flatten(1)
|
|
344
|
+
# `skip` is what makes a top-up cheap: it decides from the level-2 row,
|
|
345
|
+
# before the per-edition page — the expensive half — is ever requested.
|
|
346
|
+
# It also needs the complete level-2 set, which is why the two levels are
|
|
347
|
+
# two phases with a barrier rather than one pool.
|
|
348
|
+
editions = editions.reject { |ed| skip.call ed } if skip
|
|
349
|
+
# Level 3 — one request per edition, ~70% of a full crawl.
|
|
350
|
+
#
|
|
351
|
+
# Each parse gets its OWN errors hash, folded on this thread after the
|
|
352
|
+
# join. `errors[k] &&= v` from four workers is exactly the hazard
|
|
353
|
+
# DataParserT documents at data_parser_t.rb:33-36; folding afterwards
|
|
354
|
+
# makes the tally deterministic and needs no lock.
|
|
355
|
+
parsed = in_parallel(editions) do |ed|
|
|
356
|
+
own = Hash.new(true)
|
|
357
|
+
[DataParserR.parse(row(ed, deep: deep), own), own]
|
|
358
|
+
end
|
|
359
|
+
parsed.each { |_, own| own.each { |k, v| errors[k] &&= v } }
|
|
360
|
+
parsed.filter_map(&:first)
|
|
361
|
+
end
|
|
362
|
+
private
|
|
363
|
+
|
|
364
|
+
# Map `items` through the block on the worker pool, results index-aligned
|
|
365
|
+
# with the input.
|
|
366
|
+
#
|
|
367
|
+
# Serial and thread-free at concurrency 1 or on a one-item list, so every
|
|
368
|
+
# existing single-threaded caller — and every cassette-backed spec — stays
|
|
369
|
+
# on exactly the code path it is on today.
|
|
370
|
+
#
|
|
371
|
+
# Failure semantics are deliberately today's: the first error aborts the
|
|
372
|
+
# whole call, because #harvest already loses the series on a raise and the
|
|
373
|
+
# data repo's guard_itur_harvest is calibrated on that. Returning the
|
|
374
|
+
# partial result instead would shrink a collapse below its 50% threshold
|
|
375
|
+
# and publish the hole the guard exists to catch. What changes is only the
|
|
376
|
+
# blast radius — the producer stops enqueueing at the first error, so at
|
|
377
|
+
# most `concurrency` further requests go out rather than a whole queue's.
|
|
378
|
+
#
|
|
379
|
+
# @param items [Array]
|
|
380
|
+
# @return [Array] one entry per item, in input order
|
|
381
|
+
def in_parallel(items)
|
|
382
|
+
return items.map { |i| yield i } if @concurrency < 2 || items.size < 2
|
|
383
|
+
|
|
384
|
+
# Pre-sized, so workers only ever assign to distinct existing indices —
|
|
385
|
+
# no lock, and no chance of racing a resize. That is the one thing that
|
|
386
|
+
# makes Core::WorkersPool unusable here.
|
|
387
|
+
results = Array.new(items.size)
|
|
388
|
+
error = nil
|
|
389
|
+
guard = Mutex.new
|
|
390
|
+
queue = SizedQueue.new(@concurrency * 2)
|
|
391
|
+
workers = worker_agents.map do |worker_agent|
|
|
392
|
+
Thread.new do
|
|
393
|
+
Thread.current[AGENT_KEY] = worker_agent
|
|
394
|
+
while (item = queue.pop)
|
|
395
|
+
value, idx = item
|
|
396
|
+
# `next`, never `break`: a worker that stopped popping would let
|
|
397
|
+
# the SizedQueue fill and deadlock the producer on its poison
|
|
398
|
+
# pills — a hung CI job, the worst failure available here.
|
|
399
|
+
next if guard.synchronize { !error.nil? }
|
|
400
|
+
|
|
401
|
+
begin
|
|
402
|
+
results[idx] = yield value
|
|
403
|
+
rescue StandardError => e
|
|
404
|
+
guard.synchronize { error ||= e }
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
items.each_with_index do |item, idx|
|
|
410
|
+
break if guard.synchronize { !error.nil? }
|
|
411
|
+
|
|
412
|
+
queue << [item, idx]
|
|
413
|
+
end
|
|
414
|
+
@concurrency.times { queue << nil }
|
|
415
|
+
workers.each(&:join)
|
|
416
|
+
raise error if error
|
|
417
|
+
|
|
418
|
+
results
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# One agent per worker, built once for the crawler rather than per phase:
|
|
422
|
+
# Mechanize is not thread-safe, and a per-worker agent also keeps one
|
|
423
|
+
# worker's cookie and history state out of another's.
|
|
424
|
+
def worker_agents
|
|
425
|
+
@worker_agents ||= Array.new(@concurrency) { self.class.agent }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# In deep mode the edition page is the **authority**: an edition that
|
|
429
|
+
# offers no `!!PDF-E.pdf` (older ones are Word-only) gets no source rather
|
|
430
|
+
# than a guessed URL that 404s, and a page that came back empty — ITU does
|
|
431
|
+
# serve truncated 200s — is logged instead of being silently backfilled
|
|
432
|
+
# with shallow data, which would make a degraded deep crawl look like a
|
|
433
|
+
# successful one. The derived values are the *shallow* mode's answer.
|
|
434
|
+
#
|
|
435
|
+
# @param ed [Hash] a level-2 row
|
|
436
|
+
# @return [Hash] the normalized row DataParserR consumes
|
|
437
|
+
def row(ed, deep:)
|
|
438
|
+
return ed.merge(base_row(ed), date: id_date(ed[:id]), pdf: pdf_url(ed[:id])) unless deep
|
|
439
|
+
|
|
440
|
+
detail = edition(ed[:id])
|
|
441
|
+
# A family whose date lives in the id (Handbooks) is expected to find
|
|
442
|
+
# none on the page; for the others a missing date means a degraded fetch.
|
|
443
|
+
if detail[:date].nil? && config(family_of(ed[:id]))[:date] != :id
|
|
444
|
+
Util.warn "No date on #{page_url ed[:id]} — falling back to the id's date"
|
|
445
|
+
end
|
|
446
|
+
ed.merge(base_row(ed), date: detail[:date] || id_date(ed[:id]), pdf: detail[:pdf])
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# @param ed [Hash]
|
|
450
|
+
# @return [Hash]
|
|
451
|
+
def base_row(ed)
|
|
452
|
+
{ family: family_of(ed[:id]), url: page_url(ed[:id]) }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# @param family [String]
|
|
456
|
+
# @return [Hash] the family's URL templates and date field
|
|
457
|
+
# @raise [ArgumentError] for a family this crawler doesn't implement
|
|
458
|
+
def config(family)
|
|
459
|
+
FAMILIES[family] or
|
|
460
|
+
raise ArgumentError, "unknown ITU-R family #{family.inspect} (known: #{FAMILIES.keys.join ', '})"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Every id names its own family, so the level-2/3 methods take an id alone.
|
|
464
|
+
#
|
|
465
|
+
# @param id [String] e.g. "R-REP-BO.1227-2-1998"
|
|
466
|
+
# @return [String] e.g. "R-REP"
|
|
467
|
+
def family_of(id)
|
|
468
|
+
id.to_s[/\AR-[A-Z]+/].to_s
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# @param id [String]
|
|
472
|
+
# @return [String]
|
|
473
|
+
def page_url(id)
|
|
474
|
+
format config(family_of(id))[:page], id: id
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Recommendation pages carry an approval date; report pages date each
|
|
478
|
+
# downloadable file instead ("Posted"), which is the publication date the
|
|
479
|
+
# corpus records — at month precision, so the day is dropped.
|
|
480
|
+
#
|
|
481
|
+
# @param page [Mechanize::Page]
|
|
482
|
+
# @param id [String]
|
|
483
|
+
# @return [String, nil]
|
|
484
|
+
def page_date(page, id)
|
|
485
|
+
case config(family_of(id))[:date]
|
|
486
|
+
when :approved then page_text(page)[APPROVED_RE, 1]
|
|
487
|
+
when :posted then posted_date(page, id)
|
|
488
|
+
# :id — the page has no date to offer, so #row falls back to the id's
|
|
489
|
+
# year without treating it as a degraded crawl.
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# A report's files can be re-posted after publication, in which case the
|
|
494
|
+
# posted date is no longer the publication date the id names. Say so
|
|
495
|
+
# rather than quietly dating the record by a re-upload.
|
|
496
|
+
#
|
|
497
|
+
# @param page [Mechanize::Page]
|
|
498
|
+
# @param id [String]
|
|
499
|
+
# @return [String, nil]
|
|
500
|
+
def posted_date(page, id)
|
|
501
|
+
date = page_text(page)[POSTED_RE, 1]
|
|
502
|
+
year = id[ID_YEAR_RE, 1]
|
|
503
|
+
if date && year && date[0, 4] != year
|
|
504
|
+
Util.warn "Posted date #{date} on #{page_url id} disagrees with the id's year #{year}"
|
|
505
|
+
end
|
|
506
|
+
date
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# The decoded document text minus scripts: the raw body is the response's
|
|
510
|
+
# bytes (Latin-1 here, ASCII-8BIT when replayed from a cassette), and the
|
|
511
|
+
# SharePoint chrome embeds JavaScript full of date literals that a bare
|
|
512
|
+
# date match would find first.
|
|
513
|
+
#
|
|
514
|
+
# @param page [Mechanize::Page]
|
|
515
|
+
# @return [String]
|
|
516
|
+
def page_text(page)
|
|
517
|
+
doc = page.parser.dup
|
|
518
|
+
doc.search("script", "style").each(&:remove)
|
|
519
|
+
squish doc.text
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# An ITU restyle would leave every level matching nothing, and the crawl
|
|
523
|
+
# would return an empty corpus that looks like a successful run — the
|
|
524
|
+
# silent failure ITU_R_DISABLED exists to prevent for RunSearch. Say so.
|
|
525
|
+
#
|
|
526
|
+
# @param found [Array] the level's rows
|
|
527
|
+
# @param what [String] what was being enumerated
|
|
528
|
+
# @return [Array] found, unchanged
|
|
529
|
+
def warn_if_empty(found, what)
|
|
530
|
+
Util.warn "ITU-R crawl: no #{what} found — the page layout may have changed" if found.empty?
|
|
531
|
+
found
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Shallow mode's date. A Recommendation id ends in YYYYMM (the displayed
|
|
535
|
+
# code renders pre-2000 editions with a two-digit year, so the id is the
|
|
536
|
+
# only unambiguous source); a Report id ends in the publication year, which
|
|
537
|
+
# gives no month.
|
|
538
|
+
#
|
|
539
|
+
# @param id [String]
|
|
540
|
+
# @return [String, nil]
|
|
541
|
+
def id_date(id)
|
|
542
|
+
m = id.match(ID_DATE_RE)
|
|
543
|
+
return "#{m[1]}-#{m[2]}" if m
|
|
544
|
+
|
|
545
|
+
id[ID_YEAR_RE, 1]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# nil rather than a malformed URL when the series can't be read off the id
|
|
549
|
+
# — DataParserR then records the missing source in the error tally instead
|
|
550
|
+
# of writing `…/rec//R-REC-…` into the data file.
|
|
551
|
+
#
|
|
552
|
+
# @param id [String]
|
|
553
|
+
# @return [String, nil]
|
|
554
|
+
def pdf_url(id)
|
|
555
|
+
template = config(family_of(id))[:pdf]
|
|
556
|
+
return nil unless template # R-QUE editions rarely have one; never guess
|
|
557
|
+
return format(template, id: id) unless template.include? "%<series>s"
|
|
558
|
+
|
|
559
|
+
series = id[/-([A-Za-z]+)\./, 1]
|
|
560
|
+
series && format(template, series: series.downcase, id: id)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Every level's table rows share one shape: a `parent=`-linked anchor in
|
|
564
|
+
# the first cell, the title in the second.
|
|
565
|
+
#
|
|
566
|
+
# @param url [String]
|
|
567
|
+
# @return [Array<Array(String, Nokogiri::XML::Element, Nokogiri::XML::NodeSet)>]
|
|
568
|
+
def rows(url)
|
|
569
|
+
get(url).search("//a[contains(@href,'parent=R-')]").filter_map do |a|
|
|
570
|
+
id = a[:href].to_s[PARENT_RE, :id]
|
|
571
|
+
tr = a.ancestors("tr").first
|
|
572
|
+
[id, a, tr.search("td")] if id && tr
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# The title cell carries an optional red annotation ("Note - Suppressed on
|
|
577
|
+
# …"), which is not part of the title.
|
|
578
|
+
#
|
|
579
|
+
# @param cell [Nokogiri::XML::Element, nil]
|
|
580
|
+
# @return [String]
|
|
581
|
+
def title_text(cell)
|
|
582
|
+
return "" unless cell
|
|
583
|
+
|
|
584
|
+
cell = cell.dup
|
|
585
|
+
cell.search("font").each(&:remove)
|
|
586
|
+
squish(cell.text)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# The cells are padded with ` `, which `\s` does not match — so the
|
|
590
|
+
# non-breaking spaces are folded in before collapsing. `scrub` first: the
|
|
591
|
+
# /rec pages declare no charset in Content-Type, so a page whose encoding
|
|
592
|
+
# Nokogiri sniffs as UTF-8 can hand back Latin-1 bytes, and `gsub` on an
|
|
593
|
+
# invalid-encoding String raises — which would kill the whole series.
|
|
594
|
+
#
|
|
595
|
+
# @param text [String, nil]
|
|
596
|
+
# @return [String]
|
|
597
|
+
def squish(text)
|
|
598
|
+
text.to_s.scrub.tr(" ", " ").gsub(/\s+/, " ").strip
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Retried, because a corpus-scale crawl *will* be throttled: measured over
|
|
602
|
+
# a 49-minute full run, ITU rate-limits `/rec` with HTTP 503 and `/pub`
|
|
603
|
+
# with a **302 to `/en/publications/pages/notfound.aspx`** — a soft block
|
|
604
|
+
# that is indistinguishable from "no such document" unless you look. That
|
|
605
|
+
# second form silently emptied all 14 report series of one run. So a
|
|
606
|
+
# notfound landing is treated as a retryable failure like any 5xx, and
|
|
607
|
+
# only a persistent one raises.
|
|
608
|
+
#
|
|
609
|
+
# @param url [String]
|
|
610
|
+
# @return [Mechanize::Page]
|
|
611
|
+
# @raise [Relaton::RequestError] after RETRIES attempts
|
|
612
|
+
def get(url)
|
|
613
|
+
attempt = 0
|
|
614
|
+
begin
|
|
615
|
+
attempt += 1
|
|
616
|
+
# A crawl the governor has abandoned must stop *now*: grinding out the
|
|
617
|
+
# remaining hours against a WAF ban is what put the job over the 6 h
|
|
618
|
+
# Actions cap in the first place.
|
|
619
|
+
raise Relaton::RequestError, "ITU-R crawl abandoned: rate-limited by www.itu.int" if @governor.exhausted?
|
|
620
|
+
|
|
621
|
+
@governor.wait # the pool-wide cooldown, if any worker opened one
|
|
622
|
+
@pacer.wait # the shared minimum gap between request starts
|
|
623
|
+
page = agent.get url
|
|
624
|
+
raise SoftBlock.new(page), "redirected to notfound" if notfound? page
|
|
625
|
+
|
|
626
|
+
@governor.succeeded!
|
|
627
|
+
page
|
|
628
|
+
rescue Mechanize::ResponseCodeError, SocketError, Timeout::Error, Errno::ECONNRESET,
|
|
629
|
+
EOFError, Net::ProtocolError, OpenSSL::SSL::SSLError => e
|
|
630
|
+
# A rate limit is a *pool* event and a transport error is not, so they
|
|
631
|
+
# get different treatment. Four workers each running their own 5/10/15 s
|
|
632
|
+
# ladder against a WAF ban keeps the limiter engaged; one shared
|
|
633
|
+
# cooldown on the ban's own minute scale is what outlasts it.
|
|
634
|
+
if Governor.throttle?(e)
|
|
635
|
+
gave_up = @governor.throttled!(retry_after: Governor.retry_after(e))
|
|
636
|
+
Util.warn "ITU-R crawl: #{url} throttled (#{e.message}); " \
|
|
637
|
+
"pool cooldown round #{@governor.throttle_rounds}"
|
|
638
|
+
retry if !gave_up && attempt <= RETRIES
|
|
639
|
+
elsif attempt <= RETRIES
|
|
640
|
+
Util.warn "ITU-R crawl: #{url} failed (#{e.message}); retry #{attempt}/#{RETRIES} in #{backoff attempt}s"
|
|
641
|
+
sleep backoff(attempt)
|
|
642
|
+
retry
|
|
643
|
+
end
|
|
644
|
+
raise Relaton::RequestError, "Could not access #{url}: #{e.message}"
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# @param page [Mechanize::Page]
|
|
649
|
+
# @return [Boolean]
|
|
650
|
+
def notfound?(page)
|
|
651
|
+
page.uri.to_s.include? NOTFOUND
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Linear backoff — the block is a rate limit, so the point is to wait out
|
|
655
|
+
# a window, not to hammer a dead host.
|
|
656
|
+
#
|
|
657
|
+
# @param attempt [Integer]
|
|
658
|
+
# @return [Integer] seconds
|
|
659
|
+
def backoff(attempt)
|
|
660
|
+
attempt * RETRY_BACKOFF
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|