diff --git a/modules/src/main/java/org/archive/modules/extractor/ExtractorHTML.java b/modules/src/main/java/org/archive/modules/extractor/ExtractorHTML.java index ebcaf47a..22f75d94 100644 --- a/modules/src/main/java/org/archive/modules/extractor/ExtractorHTML.java +++ b/modules/src/main/java/org/archive/modules/extractor/ExtractorHTML.java @@ -28,8 +28,10 @@ import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.httpclient.URIException; +import org.apache.commons.lang.StringUtils; import org.archive.io.ReplayCharSequence; import org.archive.modules.CoreAttributeConstants; import org.archive.modules.CrawlMetadata; @@ -43,1000 +45,999 @@ import org.archive.util.UriUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import com.google.common.base.Strings; + +import au.id.jericho.lib.html.Element; + /** - * Basic link-extraction, from an HTML content-body, - * using regular expressions. + * Basic link-extraction, from an HTML content-body, using regular expressions. * - * NOTE: This processor may open a ReplayCharSequence from the - * CrawlURI's Recorder, without closing that ReplayCharSequence, to allow - * reuse by later processors in sequence. In the usual (Heritrix) case, a - * call after all processing to the Recorder's endReplays() method ensures - * timely close of any reused ReplayCharSequences. Reuse of this processor - * elsewhere should ensure a similar cleanup call to Recorder.endReplays() - * occurs. + * NOTE: This processor may open a ReplayCharSequence from the CrawlURI's + * Recorder, without closing that ReplayCharSequence, to allow reuse by later + * processors in sequence. In the usual (Heritrix) case, a call after all + * processing to the Recorder's endReplays() method ensures timely close of any + * reused ReplayCharSequences. Reuse of this processor elsewhere should ensure a + * similar cleanup call to Recorder.endReplays() occurs. * - * TODO: Compare against extractors based on HTML parsing libraries for + * TODO: Compare against extractors based on HTML parsing libraries for * accuracy, completeness, and speed. * * @author gojomo */ public class ExtractorHTML extends ContentExtractor implements InitializingBean { - @SuppressWarnings("unused") - private static final long serialVersionUID = 2L; + @SuppressWarnings("unused") + private static final long serialVersionUID = 2L; - private static Logger logger = - Logger.getLogger(ExtractorHTML.class.getName()); + private static Logger logger = Logger.getLogger(ExtractorHTML.class.getName()); - private final static String MAX_ELEMENT_REPLACE = "MAX_ELEMENT"; - - private final static String MAX_ATTR_NAME_REPLACE = "MAX_ATTR_NAME"; - - private final static String MAX_ATTR_VAL_REPLACE = "MAX_ATTR_VAL"; + private final static String MAX_ELEMENT_REPLACE = "MAX_ELEMENT"; - public final static String A_META_ROBOTS = "meta-robots"; - - public final static String A_FORM_OFFSETS = "form-offsets"; - - { - setMaxElementLength(64); - } - public int getMaxElementLength() { - return (Integer) kp.get("maxElementLength"); - } - public void setMaxElementLength(int max) { - kp.put("maxElementLength",max); - } - - - /** - * Relevant tag extractor. - * - *
- * This pattern extracts either: - *
- *- * groups: - *
- *- * HER-1998 - Modified part 8 to allow conditional html comments. - * Conditional HTML comment example: - * "<!--[if expression]> HTML <![endif]-->" - *
- * - *- * This technique is commonly used to reference CSS & JavaScript that - * are designed to deal with the quirks of a specific version of Internet - * Explorer. There is another syntax for conditional comments which already - * gets parsed by the regex since it doesn't start with "<!--" Ex. - * <!if expression> HTML <!endif> - *
- * - *- * https://en.wikipedia.org/wiki/Conditional_Comments - *
- */ - // version w/ less unnecessary backtracking - static final String RELEVANT_TAG_EXTRACTOR = - "(?is)<(?:((script[^>]*+)>.*?]*+)>.*?]*+)" + // 5, 6, 7 - "|(!--(?!\\[if|>).*?--))>"; // 8 + private final static String MAX_ATTR_NAME_REPLACE = "MAX_ATTR_NAME"; -// version w/ problems with unclosed script tags -// static final String RELEVANT_TAG_EXTRACTOR = -// "(?is)<(?:((script.*?)>.*?.*?"; + private final static String MAX_ATTR_VAL_REPLACE = "MAX_ATTR_VAL"; + public final static String A_META_ROBOTS = "meta-robots"; - -// // this pattern extracts 'href' or 'src' attributes from -// // any open-tag innards matched by the above -// static Pattern RELEVANT_ATTRIBUTE_EXTRACTOR = Pattern.compile( -// "(?is)(\\w+)(?:\\s+|(?:\\s.*?\\s))(?:(href)|(src))\\s*=(?:(?:\\s*\"(.+?)\")|(?:\\s*'(.+?)')|(\\S+))"); -// -// // this pattern extracts 'robots' attributes -// static Pattern ROBOTS_ATTRIBUTE_EXTRACTOR = Pattern.compile( -// "(?is)(\\w+)\\s+.*?(?:(robots))\\s*=(?:(?:\\s*\"(.+)\")|(?:\\s*'(.+)')|(\\S+))"); + public final static String A_FORM_OFFSETS = "form-offsets"; - { - setMaxAttributeNameLength(64); // 64 chars - } + { + setMaxElementLength(64); + } - public int getMaxAttributeNameLength() { - return (Integer) kp.get("maxAttributeNameLength"); - } + public int getMaxElementLength() { + return (Integer) kp.get("maxElementLength"); + } - public void setMaxAttributeNameLength(int max) { - kp.put("maxAttributeNameLength", max); - } + public void setMaxElementLength(int max) { + kp.put("maxElementLength", max); + } + /** + * Relevant tag extractor. + * + *+ * This pattern extracts either: + *
+ *+ * groups: + *
+ *+ * HER-1998 - Modified part 8 to allow conditional html comments. + * Conditional HTML comment example: "<!--[if expression]> HTML + * <![endif]-->" + *
+ * + *+ * This technique is commonly used to reference CSS & JavaScript that + * are designed to deal with the quirks of a specific version of Internet + * Explorer. There is another syntax for conditional comments which already + * gets parsed by the regex since it doesn't start with "<!--" Ex. + * <!if expression> HTML <!endif> + *
+ * + *+ * https://en.wikipedia.org/wiki/Conditional_Comments + *
+ */ + // version w/ less unnecessary backtracking + static final String RELEVANT_TAG_EXTRACTOR = "(?is)<(?:((script[^>]*+)>.*?]*+)>.*?]*+)" + // 5, + // 6, + // 7 + "|(!--(?!\\[if|>).*?--))>"; // 8 - { - setMaxAttributeValLength(2048); // 2K - } + // version w/ problems with unclosed script tags + // static final String RELEVANT_TAG_EXTRACTOR = + // "(?is)<(?:((script.*?)>.*?.*?"; - public int getMaxAttributeValLength() { - return (Integer) kp.get("maxAttributeValLength"); - } + // // this pattern extracts 'href' or 'src' attributes from + // // any open-tag innards matched by the above + // static Pattern RELEVANT_ATTRIBUTE_EXTRACTOR = Pattern.compile( + // "(?is)(\\w+)(?:\\s+|(?:\\s.*?\\s))(?:(href)|(src))\\s*=(?:(?:\\s*\"(.+?)\")|(?:\\s*'(.+?)')|(\\S+))"); + // + // // this pattern extracts 'robots' attributes + // static Pattern ROBOTS_ATTRIBUTE_EXTRACTOR = Pattern.compile( + // "(?is)(\\w+)\\s+.*?(?:(robots))\\s*=(?:(?:\\s*\"(.+)\")|(?:\\s*'(.+)')|(\\S+))"); - public void setMaxAttributeValLength(int max) { - kp.put("maxAttributeValLength", max); - } - - // TODO: perhaps cut to near MAX_URI_LENGTH - - // this pattern extracts attributes from any open-tag innards - // matched by the above. attributes known to be URIs of various - // sorts are matched specially - static final String EACH_ATTRIBUTE_EXTRACTOR = - "(?is)\\s?((href)|(action)|(on\\w*)" // 1, 2, 3, 4 - +"|((?:src)|(?:srcset)|(?:lowsrc)|(?:background)|(?:cite)" // ... - +"|(?:longdesc)|(?:usemap)|(?:profile)|(?:datasrc))" // 5 - +"|(codebase)|((?:classid)|(?:data))|(archive)|(code)" // 6, 7, 8, 9 - +"|(value)|(style)|(method)" // 10, 11, 12 - +"|([-\\w]{1,"+MAX_ATTR_NAME_REPLACE+"}))" // 13 - +"\\s*=\\s*" - +"(?:(?:\"(.{0,"+MAX_ATTR_VAL_REPLACE+"}?)(?:\"|$))" // 14 - +"|(?:'(.{0,"+MAX_ATTR_VAL_REPLACE+"}?)(?:'|$))" // 15 - +"|(\\S{1,"+MAX_ATTR_VAL_REPLACE+"}))"; // 16 - // groups: - // 1: attribute name - // 2: HREF - single URI relative to doc base, or occasionally javascript: - // 3: ACTION - single URI relative to doc base, or occasionally javascript: - // 4: ON[WHATEVER] - script handler - // 5: SRC,SRCSET,LOWSRC,BACKGROUND,CITE,LONGDESC,USEMAP,PROFILE, or DATASRC - // single URI relative to doc base - // 6: CODEBASE - a single URI relative to doc base, affecting other - // attributes - // 7: CLASSID, DATA - a single URI relative to CODEBASE (if supplied) - // 8: ARCHIVE - one or more space-delimited URIs relative to CODEBASE - // (if supplied) - // 9: CODE - a single URI relative to the CODEBASE (is specified). - // 10: VALUE - often includes a uri path on forms - // 11: STYLE - inline attribute style info - // 12: METHOD - form GET/POST - // 13: any other attribute - // 14: double-quote delimited attr value - // 15: single-quote delimited attr value - // 16: space-delimited attr value + { + setMaxAttributeNameLength(64); // 64 chars + } - - static final String WHITESPACE = "\\s"; - static final String CLASSEXT =".class"; - static final String APPLET = "applet"; - static final String BASE = "base"; - static final String LINK = "link"; - static final String FRAME = "frame"; - static final String IFRAME = "iframe"; + public int getMaxAttributeNameLength() { + return (Integer) kp.get("maxAttributeNameLength"); + } - - /** - * If true, FRAME/IFRAME SRC-links are treated as embedded resources (like - * IMG, 'E' hop-type), otherwise they are treated as navigational links. - * Default is true. - */ - { - setTreatFramesAsEmbedLinks(true); - } - public boolean getTreatFramesAsEmbedLinks() { - return (Boolean) kp.get("treatFramesAsEmbedLinks"); - } - public void setTreatFramesAsEmbedLinks(boolean asEmbeds) { - kp.put("treatFramesAsEmbedLinks",asEmbeds); - } - - /** - * If true, URIs appearing as the ACTION attribute in HTML FORMs are - * ignored. Default is false. - */ - { - setIgnoreFormActionUrls(false); - } - public boolean getIgnoreFormActionUrls() { - return (Boolean) kp.get("ignoreFormActionUrls"); - } - public void setIgnoreFormActionUrls(boolean ignoreActions) { - kp.put("ignoreFormActionUrls",ignoreActions); - } + public void setMaxAttributeNameLength(int max) { + kp.put("maxAttributeNameLength", max); + } - /** - * If true, only ACTION URIs with a METHOD of GET (explicit or implied) - * are extracted. Default is true. - */ - { - setExtractOnlyFormGets(true); - } - public boolean getExtractOnlyFormGets() { - return (Boolean) kp.get("extractOnlyFormGets"); - } - public void setExtractOnlyFormGets(boolean onlyGets) { - kp.put("extractOnlyFormGets",onlyGets); - } - - /** - * If true, in-page Javascript is scanned for strings that - * appear likely to be URIs. This typically finds both valid - * and invalid URIs, and attempts to fetch the invalid URIs - * sometimes generates webmaster concerns over odd crawler - * behavior. Default is true. - */ - { - setExtractJavascript(true); - } - public boolean getExtractJavascript() { - return (Boolean) kp.get("extractJavascript"); - } - public void setExtractJavascript(boolean extractJavascript) { - kp.put("extractJavascript",extractJavascript); - } + { + setMaxAttributeValLength(2048); // 2K + } - /** - * If true, strings that look like URIs found in unusual places (such as - * form VALUE attributes) will be extracted. This typically finds both valid - * and invalid URIs, and attempts to fetch the invalid URIs sometimes - * generate webmaster concerns over odd crawler behavior. Default is true. - */ - { - setExtractValueAttributes(true); - } - public boolean getExtractValueAttributes() { - return (Boolean) kp.get("extractValueAttributes"); - } - public void setExtractValueAttributes(boolean extractValueAttributes) { - kp.put("extractValueAttributes",extractValueAttributes); - } + public int getMaxAttributeValLength() { + return (Integer) kp.get("maxAttributeValLength"); + } - /** - * If true, URIs which end in typical non-HTML extensions (such as .gif) - * will not be scanned as if it were HTML. Default is true. - */ - { - setIgnoreUnexpectedHtml(true); - } - public boolean getIgnoreUnexpectedHtml() { - return (Boolean) kp.get("ignoreUnexpectedHtml"); - } - public void setIgnoreUnexpectedHtml(boolean ignoreUnexpectedHtml) { - kp.put("ignoreUnexpectedHtml",ignoreUnexpectedHtml); - } - - /** - * CrawlMetadata provides the robots honoring policy to use when - * considering a robots META tag. - */ - protected CrawlMetadata metadata; - public CrawlMetadata getMetadata() { - return metadata; - } - @Autowired - public void setMetadata(CrawlMetadata provider) { - this.metadata = provider; - } - - /** - * Javascript extractor to use to process inline javascript. Autowired if - * available. If null, links will not be extracted from inline javascript. - */ - transient protected ExtractorJS extractorJS; - public ExtractorJS getExtractorJS() { - return extractorJS; - } - @Autowired - public void setExtractorJS(ExtractorJS extractorJS) { - this.extractorJS = extractorJS; - } - - // TODO: convert to Strings - private String relevantTagPattern; - private String eachAttributePattern; - - public ExtractorHTML() { - } + public void setMaxAttributeValLength(int max) { + kp.put("maxAttributeValLength", max); + } - public void afterPropertiesSet() { - String regex = RELEVANT_TAG_EXTRACTOR; - regex = regex.replace(MAX_ELEMENT_REPLACE, - Integer.toString(getMaxElementLength())); - this.relevantTagPattern = regex; - - regex = EACH_ATTRIBUTE_EXTRACTOR; - regex = regex.replace(MAX_ATTR_NAME_REPLACE, - Integer.toString(getMaxAttributeNameLength())); - regex = regex.replace(MAX_ATTR_VAL_REPLACE, - Integer.toString(getMaxAttributeValLength())); - this.eachAttributePattern = regex; - } - + // TODO: perhaps cut to near MAX_URI_LENGTH - protected void processGeneralTag(CrawlURI curi, CharSequence element, - CharSequence cs) { + // this pattern extracts attributes from any open-tag innards + // matched by the above. attributes known to be URIs of various + // sorts are matched specially + static final String EACH_ATTRIBUTE_EXTRACTOR = "(?is)\\s?((href)|(action)|(on\\w*)" // 1, + // 2, + // 3, + // 4 + + "|((?:src)|(?:srcset)|(?:lowsrc)|(?:background)|(?:cite)" // ... + + "|(?:longdesc)|(?:usemap)|(?:profile)|(?:datasrc)" // ... + + "|(?:data-src)|(?:data-srcset)|(?:data-original)|(?:data-original-set))" // 5 + + "|(codebase)|((?:classid)|(?:data))|(archive)|(code)" // 6, 7, 8, + // 9 + + "|(value)|(style)|(method)" // 10, 11, 12 + + "|([-\\w]{1," + MAX_ATTR_NAME_REPLACE + "}))" // 13 + + "\\s*=\\s*" + "(?:(?:\"(.{0," + MAX_ATTR_VAL_REPLACE + "}?)(?:\"|$))" // 14 + + "|(?:'(.{0," + MAX_ATTR_VAL_REPLACE + "}?)(?:'|$))" // 15 + + "|(\\S{1," + MAX_ATTR_VAL_REPLACE + "}))"; // 16 + // groups: + // 1: attribute name + // 2: HREF - single URI relative to doc base, or occasionally javascript: + // 3: ACTION - single URI relative to doc base, or occasionally javascript: + // 4: ON[WHATEVER] - script handler + // 5: SRC,SRCSET,LOWSRC,BACKGROUND,CITE,LONGDESC,USEMAP,PROFILE, or + // DATA-SRC, DATA-ORIGINAL single URI relative to doc base + // DATA-SRCSET, DATA-ORIGINAL-SET multi URI relative to doc base + // 6: CODEBASE - a single URI relative to doc base, affecting other + // attributes + // 7: CLASSID, DATA - a single URI relative to CODEBASE (if supplied) + // 8: ARCHIVE - one or more space-delimited URIs relative to CODEBASE + // (if supplied) + // 9: CODE - a single URI relative to the CODEBASE (is specified). + // 10: VALUE - often includes a uri path on forms + // 11: STYLE - inline attribute style info + // 12: METHOD - form GET/POST + // 13: any other attribute + // 14: double-quote delimited attr value + // 15: single-quote delimited attr value + // 16: space-delimited attr value - Matcher attr = TextUtils.getMatcher(eachAttributePattern,cs); + static final String WHITESPACE = "\\s"; + static final String CLASSEXT = ".class"; + static final String APPLET = "applet"; + static final String BASE = "base"; + static final String LINK = "link"; + static final String FRAME = "frame"; + static final String IFRAME = "iframe"; - // Just in case it's an OBJECT or APPLET tag - String codebase = null; - ArrayList