params) {
+ StringBuilder accum = new StringBuilder();
+ for (XValue v : params) {
+ accum.append(v.asString());
+ }
+ return XValue.create(accum.toString());
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Contains.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Contains.java
new file mode 100644
index 000000000..796ef5019
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Contains.java
@@ -0,0 +1,29 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: boolean contains(string, string)
+ *
+ * The contains function returns true if the first argument string contains the second argument string, and otherwise returns false.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class Contains implements Function {
+ @Override
+ public String name() {
+ return "contains";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String first = params.get(0).asString();
+ String second = params.get(1).asString();
+ return XValue.create(first.contains(second));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Count.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Count.java
new file mode 100644
index 000000000..4c53cd52e
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Count.java
@@ -0,0 +1,29 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: number count(node-set)
+ * The count function returns the number of nodes in the argument node-set.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/27.
+ */
+public class Count implements Function {
+ @Override
+ public String name() {
+ return "count";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ if (params == null || params.size() == 0) {
+ return XValue.create(0);
+ }
+ return XValue.create(params.get(0).asElements().size());
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/First.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/First.java
new file mode 100644
index 000000000..cd2670520
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/First.java
@@ -0,0 +1,25 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * first in xpath is 1
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/29.
+ */
+public class First implements Function {
+ @Override
+ public String name() {
+ return "first";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ return XValue.create(1);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/FormatDate.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/FormatDate.java
new file mode 100644
index 000000000..1372b7a03
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/FormatDate.java
@@ -0,0 +1,47 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.exception.XpathParserException;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Function: string format-date(string, string, string)
+ * The format-date function returns Date object
+ * The first parameter is the date and time
+ * The second parameter is the time format of the first parameter.
+ * The third parameter is not required, and is required if the date format of the first parameter requires that the time zone must be specified
+ * For example, format-date("1999/04/01","yyyy/MM/dd") returns Date Object, and format-date("1999/04/01 07:55:23 pm","yyyy/MM/dd hh:mm:ss a",'en') returns Date Object.
+ *
+ * @author github.com/zzldn@163.com
+ * @since 2019/1/22.
+ */
+public class FormatDate implements Function {
+ @Override
+ public String name() {
+ return "format-date";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String value = params.get(0).asString();
+ String patten = params.get(1).asString();
+ try {
+ if (params.size() > 2 && null != params.get(2)) {
+ final Locale locale = Locale.forLanguageTag(params.get(2).asString());
+ final SimpleDateFormat format = new SimpleDateFormat(patten, locale);
+ return XValue.create(format.parse(value));
+ }
+ return XValue.create(FastDateFormat.getInstance(patten).parse(value));
+ } catch (ParseException e) {
+ throw new XpathParserException("date format exception!", e);
+ }
+
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Last.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Last.java
new file mode 100644
index 000000000..6dae66f6b
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Last.java
@@ -0,0 +1,28 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: number last()
+ * The last function returns a number equal to the context size from the expression evaluation context.
+ * e.g.
+ * para[last()] selects the last para child of the context node
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/27.
+ */
+public class Last implements Function {
+ @Override
+ public String name() {
+ return "last";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ return XValue.create(-1);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Not.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Not.java
new file mode 100644
index 000000000..d5af0b500
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Not.java
@@ -0,0 +1,30 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.exception.XpathParserException;
+
+import java.util.List;
+
+/**
+ * bool not(bool)
+ *
+ * @author github.com/hermitmmll
+ * @since 2018/4/3.
+ */
+public class Not implements Function {
+ @Override
+ public String name() {
+ return "not";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ if (params.size() == 1) {
+ return XValue.create(!params.get(0).asBoolean());
+ } else {
+ throw new XpathParserException("error param in not(bool) function.Please check.");
+ }
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Position.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Position.java
new file mode 100644
index 000000000..24cb49259
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/Position.java
@@ -0,0 +1,28 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.util.CommonUtil;
+
+import java.util.List;
+
+/**
+ * The position function returns a number equal to the context position from the expression evaluation context.
+ * e.g.
+ * /child::doc/child::chapter[position()=5]/child::section[position()=2] selects the second section of the fifth chapter of the doc document element
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/2/28.
+ */
+public class Position implements Function {
+ @Override
+ public String name() {
+ return "position";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ return XValue.create(CommonUtil.getElIndexInSameTags(scope.singleEl(), scope.getParent()));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StartsWith.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StartsWith.java
new file mode 100644
index 000000000..f70db13f9
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StartsWith.java
@@ -0,0 +1,29 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: boolean starts-with(string, string)
+ *
+ * The starts-with function returns true if the first argument string starts with the second argument string, and otherwise returns false.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class StartsWith implements Function {
+ @Override
+ public String name() {
+ return "starts-with";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String first = params.get(0).asString();
+ String second = params.get(1).asString();
+ return XValue.create(first.startsWith(second));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StringLength.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StringLength.java
new file mode 100644
index 000000000..169c4f8d2
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/StringLength.java
@@ -0,0 +1,30 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * number string-length(string?)
+ * The string-length returns the number of characters in the string (see [3.6 Strings]). If the argument is
+ * omitted, it defaults to the context node converted to a string, in other words the string-value of the context node.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/27.
+ */
+public class StringLength implements Function {
+ @Override
+ public String name() {
+ return "string-length";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ if (params == null || params.size() == 0) {
+ return XValue.create(0);
+ }
+ return XValue.create(params.get(0).asString().length());
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubString.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubString.java
new file mode 100644
index 000000000..d264bc9cf
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubString.java
@@ -0,0 +1,47 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: string substring(string, number, number?)
+ * https://www.w3.org/TR/1999/REC-xpath-19991116/#function-substring
+ * The substring function returns the substring of the first argument starting at the position specified in
+ * the second argument with length specified in the third argument. For example, substring("12345",2,3) returns "234".
+ * If the third argument is not specified, it returns the substring starting at the position specified in the
+ * second argument and continuing to the end of the string. For example, substring("12345",2) returns "2345".
+ *
+ * substring("12345", 1.5, 2.6) returns "234"
+ * substring("12345", 0 `div` 0, 3) returns ""
+ * substring("12345", 1, 0 `div` 0) returns ""
+ * substring("12345", -42, 1 `div` 0) returns "12345"
+ * substring("12345", -1 `div` 0, 1 `div` 0) returns ""
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+
+public class SubString implements Function {
+ @Override
+ public String name() {
+ return "substring";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ int start = params.get(1).asLong().intValue();
+ start = Math.max(start - 1, 0);
+ if (params.get(2) != null) {
+ int end = params.get(2).asLong().intValue();
+ end = Math.min(start + end, target.length());
+ end = Math.max(end, 0);
+ return XValue.create(StringUtils.substring(target, start, end));
+ }
+ return XValue.create(StringUtils.substring(target, start));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfter.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfter.java
new file mode 100644
index 000000000..d52de57aa
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfter.java
@@ -0,0 +1,32 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: string substring-after(string, string)
+ * The substring-after function returns the substring of the first argument string that follows
+ * the first occurrence of the second argument string in the first argument string, or the empty string if
+ * the first argument string does not contain the second argument string.
+ * For example, substring-after("1999/04/01","/") returns 04/01, and substring-after("1999/04/01","19") returns 99/04/01.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class SubStringAfter implements Function {
+ @Override
+ public String name() {
+ return "substring-after";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ String sep = params.get(1).asString();
+ return XValue.create(StringUtils.substringAfter(target, sep));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfterLast.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfterLast.java
new file mode 100644
index 000000000..ffb28edd1
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringAfterLast.java
@@ -0,0 +1,32 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: string substring-after-last(string, string)
+ * The substring-after function returns the substring of the first argument string that follows
+ * the first occurrence of the second argument string in the first argument string, or the empty string if
+ * the first argument string does not contain the second argument string.
+ * For example, substring-after-last("1999/04/01","/") returns 01.
+ *
+ * @author github.com/zzldnl
+ * @since 2018/3/26.
+ */
+public class SubStringAfterLast implements Function {
+ @Override
+ public String name() {
+ return "substring-after-last";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ String sep = params.get(1).asString();
+ return XValue.create(StringUtils.substringAfterLast(target, sep));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBefore.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBefore.java
new file mode 100644
index 000000000..6db7980a3
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBefore.java
@@ -0,0 +1,33 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: string substring-before(string, string)
+ *
+ * The substring-before function returns the substring of the first argument string that precedes the
+ * first occurrence of the second argument string in the first argument string, or the empty string
+ * if the first argument string does not contain the second argument string.
+ * For example, substring-before("1999/04/01","/") returns 1999.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class SubStringBefore implements Function {
+ @Override
+ public String name() {
+ return "substring-before";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ String sep = params.get(1).asString();
+ return XValue.create(StringUtils.substringBefore(target, sep));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBeforeLast.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBeforeLast.java
new file mode 100644
index 000000000..6bc022104
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringBeforeLast.java
@@ -0,0 +1,33 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * Function: string substring-before-last(string, string)
+ *
+ * The substring-before function returns the substring of the first argument string that precedes the
+ * first occurrence of the second argument string in the first argument string, or the empty string
+ * if the first argument string does not contain the second argument string.
+ * For example, substring-before-last("1999/04/01","/") returns 1999/04.
+ *
+ * @author github.com/zzldnl
+ * @since 2018/3/26.
+ */
+public class SubStringBeforeLast implements Function {
+ @Override
+ public String name() {
+ return "substring-before-last";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ String sep = params.get(1).asString();
+ return XValue.create(StringUtils.substringBeforeLast(target, sep));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringEx.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringEx.java
new file mode 100644
index 000000000..ae1db61f2
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/function/SubStringEx.java
@@ -0,0 +1,44 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.List;
+
+/**
+ * JsoupXpath扩展函数(统一使用 ‘jx’ 域),保持使用习惯java开发者习惯相同,第一个数字为起始索引(且索引从0开始),第二个数字为结束索引
+ *
+ * StringUtils.substring(null, *, *) = null
+ * StringUtils.substring("", * , *) = "";
+ * StringUtils.substring("abc", 0, 2) = "ab"
+ * StringUtils.substring("abc", 2, 0) = ""
+ * StringUtils.substring("abc", 2, 4) = "c"
+ * StringUtils.substring("abc", 2.13, 3.7) = "c"
+ * StringUtils.substring("abc", 4, 6) = ""
+ * StringUtils.substring("abc", 2, 2) = ""
+ * StringUtils.substring("abc", -2, -1) = "b"
+ * StringUtils.substring("abc", -4, 2) = "ab"
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+
+public class SubStringEx implements Function {
+ @Override
+ public String name() {
+ return "substring-ex";
+ }
+
+ @Override
+ public XValue call(Scope scope, List params) {
+ String target = params.get(0).asString();
+ int start = params.get(1).asLong().intValue();
+ if (params.get(2) != null) {
+ int end = params.get(2).asLong().intValue();
+ return XValue.create(StringUtils.substring(target, start, end));
+ }
+ return XValue.create(StringUtils.substring(target, start));
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/AllText.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/AllText.java
new file mode 100644
index 000000000..36582504b
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/AllText.java
@@ -0,0 +1,44 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.jsoup.nodes.Element;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 获取当前节点下以及所有子孙节点中纯文本
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class AllText implements NodeTest {
+ /**
+ * 支持的函数名
+ */
+ @Override
+ public String name() {
+ return "allText";
+ }
+
+ /**
+ * 函数具体逻辑
+ *
+ * @param scope 上下文
+ * @return 计算好的节点
+ */
+ @Override
+ public XValue call(Scope scope) {
+ List res = new LinkedList<>();
+ for (Element e : scope.context()) {
+ if ("script".equals(e.nodeName())) {
+ res.add(e.data());
+ } else {
+ res.add(e.text());
+ }
+ }
+ return XValue.create(res);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Html.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Html.java
new file mode 100644
index 000000000..0bca9fb15
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Html.java
@@ -0,0 +1,31 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.jsoup.nodes.Element;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 获取全部节点的内部的html
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/4/9.
+ */
+public class Html implements NodeTest {
+ @Override
+ public String name() {
+ return "html";
+ }
+
+ @Override
+ public XValue call(Scope scope) {
+ List res = new LinkedList<>();
+ for (Element e : scope.context()) {
+ res.add(e.html());
+ }
+ return XValue.create(res);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Node.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Node.java
new file mode 100644
index 000000000..db6aef02d
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Node.java
@@ -0,0 +1,45 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+/**
+ * 获取当前节点下所有子节点以及独立文本
+ *
+ * @author: github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/4/4.
+ */
+public class Node implements NodeTest {
+ /**
+ * 支持的函数名
+ */
+ @Override
+ public String name() {
+ return "node";
+ }
+
+ /**
+ * 函数具体逻辑
+ *
+ * @param scope 上下文
+ * @return 计算好的节点
+ */
+ @Override
+ public XValue call(Scope scope) {
+ Elements context = new Elements();
+ for (Element el : scope.context()) {
+ context.addAll(el.children());
+ String txt = el.ownText();
+ if (StringUtils.isNotBlank(txt)) {
+ Element et = new Element("");
+ et.appendText(txt);
+ context.add(et);
+ }
+ }
+ return XValue.create(context);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Num.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Num.java
new file mode 100644
index 000000000..9d66fc45f
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Num.java
@@ -0,0 +1,51 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.apache.commons.lang3.StringUtils;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.util.Scanner;
+
+import java.math.BigDecimal;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 提取自由文本中的数字,如果知道节点的自有文本(即非子代节点所包含的文本)中只存在一个数字,如阅读数,评论数,价格等那么直接可以直接提取此数字出来。
+ * 如果有多个数字将提取第一个匹配的连续数字,支持小数,返回double
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class Num implements NodeTest {
+ private static final Pattern numExt = Pattern.compile("\\d*\\.?\\d+");
+
+ /**
+ * 支持的函数名
+ */
+ @Override
+ public String name() {
+ return "num";
+ }
+
+ /**
+ * 函数具体逻辑
+ *
+ * @param scope 上下文
+ * @return 计算好的节点
+ */
+ @Override
+ public XValue call(Scope scope) {
+ NodeTest textFun = Scanner.findNodeTestByName("allText");
+ XValue textVal = textFun.call(scope);
+ String whole = StringUtils.join(textVal.asList(), "");
+ Matcher matcher = numExt.matcher(whole);
+ if (matcher.find()) {
+ String numStr = matcher.group();
+ BigDecimal num = new BigDecimal(numStr);
+ return XValue.create(num.doubleValue());
+ } else {
+ return XValue.create(null);
+ }
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/OuterHtml.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/OuterHtml.java
new file mode 100644
index 000000000..f158ef88c
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/OuterHtml.java
@@ -0,0 +1,31 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.jsoup.nodes.Element;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 获取全部节点的 包含节点本身在内的全部html
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/4/9.
+ */
+public class OuterHtml implements NodeTest {
+ @Override
+ public String name() {
+ return "outerHtml";
+ }
+
+ @Override
+ public XValue call(Scope scope) {
+ List res = new LinkedList<>();
+ for (Element e : scope.context()) {
+ res.add(e.outerHtml());
+ }
+ return XValue.create(res);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Text.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Text.java
new file mode 100644
index 000000000..d52553d15
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/core/node/Text.java
@@ -0,0 +1,108 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.select.Elements;
+import org.jsoup.select.NodeTraversor;
+import org.jsoup.select.NodeVisitor;
+import org.seimicrawler.xpath.core.Constants;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.util.CommonUtil;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/2/28.
+ * `text()`不再简单的返回节点下的所有文本,而是按照标准语义识别出多个文本块,返回文本块列表,如
+ * ```
+ * one two three
+ * ```
+ * - `//text()` 返回 `["one", "two", "three" ]`
+ * - `//text()[2]` 返回 `["three"]`
+ */
+public class Text implements NodeTest {
+ /**
+ * 支持的函数名
+ */
+ @Override
+ public String name() {
+ return "text";
+ }
+
+ /**
+ * 函数具体逻辑
+ *
+ * @param scope 上下文
+ * @return 计算好的节点
+ */
+ @Override
+ public XValue call(Scope scope) {
+ Elements context = scope.context();
+ final Elements res = new Elements();
+ if (context != null && context.size() > 0) {
+ if (scope.isRecursion()) {
+ for (final Element e : context) {
+ final Map indexMap = new HashMap<>();
+ NodeTraversor.traverse(new NodeVisitor() {
+ @Override
+ public void head(Node node, int depth) {
+ if (node instanceof TextNode) {
+ TextNode textNode = (TextNode) node;
+ String key = depth + "_" + textNode.parent().hashCode();
+ Integer index = indexMap.get(key);
+ if (index == null) {
+ index = 1;
+ } else {
+ index += 1;
+ }
+ indexMap.put(key, index);
+ Element data = new Element(Constants.DEF_TEXT_TAG_NAME);
+ data.text(textNode.getWholeText());
+ try {
+ Method parent = Node.class.getDeclaredMethod("setParentNode", Node.class);
+ parent.setAccessible(true);
+ parent.invoke(data, textNode.parent());
+ } catch (Exception e) {
+ //ignore
+ }
+ CommonUtil.setSameTagIndexInSiblings(data, index);
+ res.add(data);
+ }
+ }
+
+ @Override
+ public void tail(Node node, int depth) {
+
+ }
+ }, e);
+ }
+ } else {
+ for (Element e : context) {
+ if ("script".equals(e.nodeName())) {
+ Element data = new Element(Constants.DEF_TEXT_TAG_NAME);
+ data.text(e.data());
+ CommonUtil.setSameTagIndexInSiblings(data, 1);
+ res.add(data);
+ } else {
+ List textNodes = e.textNodes();
+ for (int i = 0; i < textNodes.size(); i++) {
+ TextNode textNode = textNodes.get(i);
+ Element data = new Element(Constants.DEF_TEXT_TAG_NAME);
+ data.text(textNode.getWholeText());
+ CommonUtil.setSameTagIndexInSiblings(data, i + 1);
+ res.add(data);
+ }
+ }
+ }
+ }
+ }
+ return XValue.create(res);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/DoFailOnErrorHandler.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/DoFailOnErrorHandler.java
new file mode 100644
index 000000000..1dfcd670a
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/DoFailOnErrorHandler.java
@@ -0,0 +1,35 @@
+package org.seimicrawler.xpath.exception;
+
+import org.antlr.v4.runtime.DefaultErrorStrategy;
+import org.antlr.v4.runtime.InputMismatchException;
+import org.antlr.v4.runtime.Parser;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.misc.ParseCancellationException;
+
+/**
+ * 如果语法解析异常,直接抛出并终止解析
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/23.
+ */
+public class DoFailOnErrorHandler extends DefaultErrorStrategy {
+
+ @Override
+ public void recover(Parser recognizer, RecognitionException e) {
+ for (ParserRuleContext context = recognizer.getContext(); context != null; context = context.getParent()) {
+ context.exception = e;
+ }
+ throw new ParseCancellationException(e);
+ }
+
+ @Override
+ public Token recoverInline(Parser recognizer) throws RecognitionException {
+ InputMismatchException e = new InputMismatchException(recognizer);
+ for (ParserRuleContext context = recognizer.getContext(); context != null; context = context.getParent()) {
+ context.exception = e;
+ }
+ throw new ParseCancellationException(e);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchAxisException.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchAxisException.java
new file mode 100644
index 000000000..deb40ad7d
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchAxisException.java
@@ -0,0 +1,28 @@
+package org.seimicrawler.xpath.exception;
+/*
+ Copyright 2014 Wang Haomiao
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * 使用不存在的轴语法则抛出此异常
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * Date: 14-3-15
+ */
+public class NoSuchAxisException extends RuntimeException {
+ public NoSuchAxisException(String msg) {
+ super(msg);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchFunctionException.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchFunctionException.java
new file mode 100644
index 000000000..5266905d9
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/NoSuchFunctionException.java
@@ -0,0 +1,26 @@
+package org.seimicrawler.xpath.exception;
+/*
+ Copyright 2014 Wang Haomiao
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * Date: 14-3-16
+ */
+public class NoSuchFunctionException extends RuntimeException {
+ public NoSuchFunctionException(String msg) {
+ super(msg);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathMergeValueException.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathMergeValueException.java
new file mode 100644
index 000000000..e7036050b
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathMergeValueException.java
@@ -0,0 +1,13 @@
+package org.seimicrawler.xpath.exception;
+
+/**
+ * 无法合并多个表达式的解析结果
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2017/12/5.
+ */
+public class XpathMergeValueException extends RuntimeException {
+ public XpathMergeValueException(String message) {
+ super(message);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathParserException.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathParserException.java
new file mode 100644
index 000000000..62f5065a8
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathParserException.java
@@ -0,0 +1,23 @@
+package org.seimicrawler.xpath.exception;
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2017/12/5.
+ */
+public class XpathParserException extends RuntimeException {
+ /**
+ * Constructs a new runtime exception with the specified detail message.
+ * The cause is not initialized, and may subsequently be initialized by a
+ * call to {@link #initCause}.
+ *
+ * @param message the detail message. The detail message is saved for
+ * later retrieval by the {@link #getMessage()} method.
+ */
+ public XpathParserException(String message) {
+ super(message);
+ }
+
+ public XpathParserException(String message, Throwable e) {
+ super(message, e);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathSyntaxErrorException.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathSyntaxErrorException.java
new file mode 100644
index 000000000..f4a83909d
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/exception/XpathSyntaxErrorException.java
@@ -0,0 +1,30 @@
+package org.seimicrawler.xpath.exception;
+/*
+ Copyright 2014 Wang Haomiao
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 14-3-19
+ */
+public class XpathSyntaxErrorException extends RuntimeException {
+ public XpathSyntaxErrorException(String msg) {
+ super(msg);
+ }
+
+ public XpathSyntaxErrorException(String msg, Throwable e) {
+ super(msg, e);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/CommonUtil.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/CommonUtil.java
new file mode 100644
index 000000000..9cebd438c
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/CommonUtil.java
@@ -0,0 +1,121 @@
+package org.seimicrawler.xpath.util;
+/*
+ Copyright 2014 Wang Haomiao
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.seimicrawler.xpath.core.Constants;
+import org.seimicrawler.xpath.core.Scope;
+
+import java.util.Objects;
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * Date: 14-3-15
+ */
+public class CommonUtil {
+
+ /**
+ * 获取同名元素在同胞中的index
+ *
+ * @param e
+ * @return
+ */
+ public static int getElIndexInSameTags(Element e, Scope scope) {
+ Elements chs = e.parent().children();
+ int index = 1;
+ for (Element cur : chs) {
+ if (e.tagName().equals(cur.tagName()) && scope.context().contains(cur)) {
+ if (e.equals(cur)) {
+ break;
+ } else {
+ index += 1;
+ }
+ }
+ }
+ return index;
+ }
+
+
+ /**
+ * 获取同胞中同名元素的数量
+ *
+ * @param e
+ * @return
+ */
+ public static int sameTagElNums(Element e, Scope scope) {
+ Elements context = new Elements();
+ Elements els = e.parent().getElementsByTag(e.tagName());
+ for (Element el : els) {
+ if (scope.context().contains(el)) {
+ context.add(el);
+ }
+ }
+ return context.size();
+ }
+
+ public static int getIndexInContext(Scope scope, Element el) {
+ for (int i = 0; i < scope.context().size(); i++) {
+ Element tmp = scope.context().get(i);
+ if (Objects.equals(tmp, el)) {
+ return i + 1;
+ }
+ }
+ return Integer.MIN_VALUE;
+ }
+
+ public static Elements followingSibling(Element el) {
+ Elements rs = new Elements();
+ Element tmp = el.nextElementSibling();
+ while (tmp != null) {
+ rs.add(tmp);
+ tmp = tmp.nextElementSibling();
+ }
+ if (rs.size() > 0) {
+ return rs;
+ }
+ return null;
+ }
+
+ public static Elements precedingSibling(Element el) {
+ Elements rs = new Elements();
+ Element tmp = el.previousElementSibling();
+ while (tmp != null) {
+ rs.add(tmp);
+ tmp = tmp.previousElementSibling();
+ }
+ if (rs.size() > 0) {
+ return rs;
+ }
+ return null;
+ }
+
+ public static void setSameTagIndexInSiblings(Element ori, int index) {
+ if (ori == null) {
+ return;
+ }
+ ori.attr(Constants.EL_SAME_TAG_INDEX_KEY, String.valueOf(index));
+ }
+
+ public static int getJxSameTagIndexInSiblings(Element ori) {
+ String val = ori.attr(Constants.EL_SAME_TAG_INDEX_KEY);
+ if (StringUtils.isBlank(val)) {
+ return -1;
+ }
+ return Integer.parseInt(val);
+ }
+}
diff --git a/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/Scanner.java b/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/Scanner.java
new file mode 100644
index 000000000..01a999303
--- /dev/null
+++ b/JsoupXpath/src/main/java/org/seimicrawler/xpath/util/Scanner.java
@@ -0,0 +1,141 @@
+package org.seimicrawler.xpath.util;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.seimicrawler.xpath.core.AxisSelector;
+import org.seimicrawler.xpath.core.Function;
+import org.seimicrawler.xpath.core.NodeTest;
+import org.seimicrawler.xpath.core.axis.AncestorOrSelfSelector;
+import org.seimicrawler.xpath.core.axis.AncestorSelector;
+import org.seimicrawler.xpath.core.axis.AttributeSelector;
+import org.seimicrawler.xpath.core.axis.ChildSelector;
+import org.seimicrawler.xpath.core.axis.DescendantOrSelfSelector;
+import org.seimicrawler.xpath.core.axis.DescendantSelector;
+import org.seimicrawler.xpath.core.axis.FollowingSelector;
+import org.seimicrawler.xpath.core.axis.FollowingSiblingOneSelector;
+import org.seimicrawler.xpath.core.axis.FollowingSiblingSelector;
+import org.seimicrawler.xpath.core.axis.ParentSelector;
+import org.seimicrawler.xpath.core.axis.PrecedingSelector;
+import org.seimicrawler.xpath.core.axis.PrecedingSiblingOneSelector;
+import org.seimicrawler.xpath.core.axis.PrecedingSiblingSelector;
+import org.seimicrawler.xpath.core.axis.SelfSelector;
+import org.seimicrawler.xpath.core.function.Concat;
+import org.seimicrawler.xpath.core.function.Contains;
+import org.seimicrawler.xpath.core.function.Count;
+import org.seimicrawler.xpath.core.function.First;
+import org.seimicrawler.xpath.core.function.FormatDate;
+import org.seimicrawler.xpath.core.function.Last;
+import org.seimicrawler.xpath.core.function.Not;
+import org.seimicrawler.xpath.core.function.Position;
+import org.seimicrawler.xpath.core.function.StartsWith;
+import org.seimicrawler.xpath.core.function.StringLength;
+import org.seimicrawler.xpath.core.function.SubString;
+import org.seimicrawler.xpath.core.function.SubStringAfter;
+import org.seimicrawler.xpath.core.function.SubStringAfterLast;
+import org.seimicrawler.xpath.core.function.SubStringBefore;
+import org.seimicrawler.xpath.core.function.SubStringBeforeLast;
+import org.seimicrawler.xpath.core.function.SubStringEx;
+import org.seimicrawler.xpath.core.node.AllText;
+import org.seimicrawler.xpath.core.node.Html;
+import org.seimicrawler.xpath.core.node.Node;
+import org.seimicrawler.xpath.core.node.Num;
+import org.seimicrawler.xpath.core.node.OuterHtml;
+import org.seimicrawler.xpath.core.node.Text;
+import org.seimicrawler.xpath.exception.NoSuchAxisException;
+import org.seimicrawler.xpath.exception.NoSuchFunctionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 考虑更广泛的兼容性,替换掉 FastClasspathScanner,采用手工注册
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/2/28.
+ */
+public class Scanner {
+ private static final Map axisSelectorMap = new HashMap<>();
+ private static final Map nodeTestMap = new HashMap<>();
+ private static final Map functionMap = new HashMap<>();
+ private static final Logger logger = LoggerFactory.getLogger(Scanner.class);
+
+ static {
+ initAxis(AncestorOrSelfSelector.class, AncestorSelector.class, AttributeSelector.class, ChildSelector.class, DescendantOrSelfSelector.class, DescendantSelector.class, FollowingSelector.class, FollowingSiblingOneSelector.class, FollowingSiblingSelector.class, ParentSelector.class, PrecedingSelector.class, PrecedingSiblingOneSelector.class, PrecedingSiblingSelector.class, SelfSelector.class);
+ initFunction(Concat.class, Contains.class, Count.class, First.class, Last.class, Not.class, Position.class, StartsWith.class, StringLength.class, SubString.class, SubStringAfter.class, SubStringBefore.class, SubStringEx.class, FormatDate.class, SubStringAfterLast.class, SubStringBeforeLast.class);
+ initNode(AllText.class, Html.class, Node.class, Num.class, OuterHtml.class, Text.class);
+ }
+
+ public static AxisSelector findSelectorByName(String selectorName) {
+ AxisSelector selector = axisSelectorMap.get(selectorName);
+ if (selector == null) {
+ throw new NoSuchAxisException("not support axis: " + selectorName);
+ }
+ return selector;
+ }
+
+ public static NodeTest findNodeTestByName(String nodeTestName) {
+ NodeTest nodeTest = nodeTestMap.get(nodeTestName);
+ if (nodeTest == null) {
+ throw new NoSuchFunctionException("not support nodeTest: " + nodeTestName);
+ }
+ return nodeTest;
+ }
+
+ public static Function findFunctionByName(String funcName) {
+ Function function = functionMap.get(funcName);
+ if (function == null) {
+ throw new NoSuchFunctionException("not support function: " + funcName);
+ }
+ return function;
+ }
+
+ public static void registerFunction(Class extends Function> func) {
+ Function function;
+ try {
+ function = func.newInstance();
+ functionMap.put(function.name(), function);
+ } catch (Exception e) {
+ logger.info(ExceptionUtils.getRootCauseMessage(e), e);
+ }
+ }
+
+ public static void registerNodeTest(Class extends NodeTest> nodeTestClass) {
+ NodeTest nodeTest;
+ try {
+ nodeTest = nodeTestClass.newInstance();
+ nodeTestMap.put(nodeTest.name(), nodeTest);
+ } catch (Exception e) {
+ logger.info(ExceptionUtils.getRootCauseMessage(e), e);
+ }
+ }
+
+ public static void registerAxisSelector(Class extends AxisSelector> axisSelectorClass) {
+ AxisSelector axisSelector;
+ try {
+ axisSelector = axisSelectorClass.newInstance();
+ axisSelectorMap.put(axisSelector.name(), axisSelector);
+ } catch (Exception e) {
+ logger.info(ExceptionUtils.getRootCauseMessage(e), e);
+ }
+ }
+
+ public static void initAxis(Class extends AxisSelector>... cls) {
+ for (Class extends AxisSelector> axis : cls) {
+ registerAxisSelector(axis);
+ }
+ }
+
+ public static void initFunction(Class extends Function>... cls) {
+ for (Class extends Function> func : cls) {
+ registerFunction(func);
+ }
+ }
+
+ public static void initNode(Class extends NodeTest>... cls) {
+ for (Class extends NodeTest> node : cls) {
+ registerNodeTest(node);
+ }
+ }
+
+}
diff --git a/JsoupXpath/src/main/resources/Xpath.g4 b/JsoupXpath/src/main/resources/Xpath.g4
new file mode 100644
index 000000000..9d439dcab
--- /dev/null
+++ b/JsoupXpath/src/main/resources/Xpath.g4
@@ -0,0 +1,296 @@
+grammar Xpath;
+
+/*
+XPath 1.0 grammar. Should conform to the official spec at
+http://www.w3.org/TR/1999/REC-xpath-19991116. The grammar
+rules have been kept as close as possible to those in the
+spec, but some adjustmewnts were unavoidable. These were
+mainly removing left recursion (spec seems to be based on
+LR), and to deal with the double nature of the '*' token
+(node wildcard and multiplication operator). See also
+section 3.7 in the spec. These rule changes should make
+no difference to the strings accepted by the grammar.
+
+Written by Jan-Willem van den Broek
+Version 1.0
+
+Do with this code as you will.
+*/
+/*
+ Ported to Antlr4 by Tom Everett
+*/
+/**
+操作符扩展:
+ a^=b 字符串a以字符串b开头 a startwith b
+ a*=b a包含b, a contains b
+ a$=b a以b结尾 a endwith b
+ a~=b a的内容符合 正则表达式b
+ a!~b a的内容不符合 正则表达式b
+
+轴扩展:
+ following-sibling-one
+ preceding-sibling-one
+ sibling
+
+NodeTest扩展:
+ num 抽取数字
+ allText 提取节点下全部文本
+ outerHtml 获取全部节点的 包含节点本身在内的全部html
+ html 获取全部节点的内部的html
+
+*/
+
+main : expr
+ ;
+
+locationPath
+ : relativeLocationPath
+ | absoluteLocationPathNoroot
+ ;
+
+absoluteLocationPathNoroot
+ : op=(PATHSEP|ABRPATH) relativeLocationPath
+ ;
+
+relativeLocationPath
+ : step (op=(PATHSEP|ABRPATH) step)*
+ ;
+
+step
+ : axisSpecifier nodeTest predicate*
+ | abbreviatedStep
+ ;
+
+axisSpecifier
+ : AxisName '::'
+ | '@'?
+ ;
+
+nodeTest: nameTest
+ | NodeType '(' ')'
+ | 'processing-instruction' '(' Literal ')'
+ ;
+
+predicate
+ : '[' expr ']'
+ ;
+
+abbreviatedStep
+ : '.'
+ | '..'
+ ;
+
+expr : orExpr
+ ;
+
+primaryExpr
+ : variableReference
+ | '(' expr ')'
+ | Literal
+ | Number
+ | functionCall
+ ;
+
+functionCall
+ : functionName '(' ( expr ( ',' expr )* )? ')'
+ ;
+
+unionExprNoRoot
+ : pathExprNoRoot (op=PIPE unionExprNoRoot)?
+ | PATHSEP PIPE unionExprNoRoot
+ ;
+
+pathExprNoRoot
+ : locationPath
+ | filterExpr (op=(PATHSEP|ABRPATH) relativeLocationPath)?
+ ;
+
+filterExpr
+ : primaryExpr predicate*
+ ;
+
+orExpr : andExpr ('or' andExpr)*
+ ;
+
+andExpr : equalityExpr ('and' equalityExpr)*
+ ;
+
+equalityExpr
+ : relationalExpr (op=(EQUALITY|INEQUALITY) relationalExpr)*
+ ;
+
+relationalExpr
+ : additiveExpr (op=(LESS|MORE_|LESS|GE|START_WITH|END_WITH|CONTAIN_WITH|REGEXP_WITH|REGEXP_NOT_WITH) additiveExpr)*
+ ;
+
+additiveExpr
+ : multiplicativeExpr (op=(PLUS|MINUS) multiplicativeExpr)*
+ ;
+
+multiplicativeExpr
+ : unaryExprNoRoot (op=(MUL|DIVISION|MODULO) multiplicativeExpr)?
+// | '/' (op=('`div`'|'`mod`') multiplicativeExpr)?
+ ;
+
+unaryExprNoRoot
+ : (sign=MINUS)? unionExprNoRoot
+ ;
+
+qName : nCName (':' nCName)?
+ ;
+
+functionName
+ : qName // Does not match nodeType, as per spec.
+ ;
+
+variableReference
+ : '$' qName
+ ;
+
+nameTest: '*'
+ | nCName ':' '*'
+ | qName
+ ;
+
+nCName : NCName
+ | AxisName
+ ;
+
+NodeType: 'comment'
+ | 'text'
+ | 'processing-instruction'
+ | 'node'
+ | 'num' //抽取数字
+ | 'allText' //提取节点下全部文本
+ | 'outerHtml' //获取全部节点的 包含节点本身在内的全部html
+ | 'html' //获取全部节点的内部的html
+ ;
+
+Number : Digits ('.' Digits?)?
+ | '.' Digits
+ ;
+
+fragment
+Digits : ('0'..'9')+
+ ;
+
+AxisName: 'ancestor'
+ | 'ancestor-or-self'
+ | 'attribute'
+ | 'child'
+ | 'descendant'
+ | 'descendant-or-self'
+ | 'following'
+ | 'following-sibling'
+// | 'namespace'
+ | 'parent'
+ | 'preceding'
+ | 'preceding-sibling'
+ | 'self'
+ | 'following-sibling-one'
+ | 'preceding-sibling-one'
+ | 'sibling'
+ ;
+
+
+ PATHSEP
+ :'/';
+ ABRPATH
+ : '//';
+ LPAR
+ : '(';
+ RPAR
+ : ')';
+ LBRAC
+ : '[';
+ RBRAC
+ : ']';
+ MINUS
+ : '-';
+ PLUS
+ : '+';
+ DOT
+ : '.';
+ MUL
+ : '*';
+ DIVISION
+ : '`div`';
+ MODULO
+ : '`mod`';
+ DOTDOT
+ : '..';
+ AT
+ : '@';
+ COMMA
+ : ',';
+ PIPE
+ : '|';
+ LESS
+ : '<';
+ MORE_
+ : '>';
+ LE
+ : '<=';
+ GE
+ : '>=';
+ EQUALITY
+ : '=';
+ INEQUALITY
+ : '!=';
+ START_WITH
+ : '^=';
+ END_WITH
+ : '$=';
+ CONTAIN_WITH
+ : '*=';
+ REGEXP_WITH
+ : '~=';
+ REGEXP_NOT_WITH
+ : '!~';
+ COLON
+ : ':';
+ CC
+ : '::';
+ APOS
+ : '\'';
+ QUOT
+ : '"';
+
+Literal : '"' ~'"'* '"'
+ | '\'' ~'\''* '\''
+ ;
+
+Whitespace
+ : (' '|'\t'|'\n'|'\r')+ ->skip
+ ;
+
+NCName : NCNameStartChar NCNameChar*
+ ;
+
+fragment
+NCNameStartChar
+ : 'A'..'Z'
+ | '_'
+ | 'a'..'z'
+ | '\u00C0'..'\u00D6'
+ | '\u00D8'..'\u00F6'
+ | '\u00F8'..'\u02FF'
+ | '\u0370'..'\u037D'
+ | '\u037F'..'\u1FFF'
+ | '\u200C'..'\u200D'
+ | '\u2070'..'\u218F'
+ | '\u2C00'..'\u2FEF'
+ | '\u3001'..'\uD7FF'
+ | '\uF900'..'\uFDCF'
+ | '\uFDF0'..'\uFFFD'
+// Unfortunately, java escapes can't handle this conveniently,
+// as they're limited to 4 hex digits. TODO.
+// | '\U010000'..'\U0EFFFF'
+ ;
+
+fragment
+NCNameChar
+ : NCNameStartChar | '-' | '.' | '0'..'9'
+ | '\u00B7' | '\u0300'..'\u036F'
+ | '\u203F'..'\u2040'
+ ;
\ No newline at end of file
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/BaseTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/BaseTest.java
new file mode 100644
index 000000000..fc97764c2
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/BaseTest.java
@@ -0,0 +1,12 @@
+package org.seimicrawler.xpath;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author: github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2018/3/26.
+ */
+public class BaseTest {
+ protected Logger logger = LoggerFactory.getLogger(BaseTest.class);
+}
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/JXDocumentTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/JXDocumentTest.java
new file mode 100644
index 000000000..5a578b548
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/JXDocumentTest.java
@@ -0,0 +1,289 @@
+package org.seimicrawler.xpath;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.seimicrawler.xpath.exception.XpathSyntaxErrorException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * JXDocument Tester.
+ *
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @version 1.0
+ */
+@RunWith(DataProviderRunner.class)
+public class JXDocumentTest {
+
+ private JXDocument underTest;
+
+ private JXDocument doubanTest;
+
+ private JXDocument custom;
+ private final ClassLoader loader = getClass().getClassLoader();
+ private final Logger logger = LoggerFactory.getLogger(JXDocumentTest.class);
+
+ @Before
+ public void before() throws Exception {
+ String html = "some body
Two
";
+ underTest = JXDocument.create(html);
+ if (doubanTest == null) {
+ URL t = loader.getResource("d_test.html");
+ assert t != null;
+ File dBook = new File(t.toURI());
+ String context = FileUtils.readFileToString(dBook, StandardCharsets.UTF_8);
+ doubanTest = JXDocument.create(context);
+ }
+ custom = JXDocument.create("性别: 男 ");
+ }
+
+ /**
+ * Method: sel(String xpath)
+ */
+ @Test
+ public void testSel() throws Exception {
+ String xpath = "//script[1]/text()";
+ JXNode res = underTest.selNOne(xpath);
+ Assert.assertNotNull(res);
+ Assert.assertEquals("console.log('aaaaa')", res.asString());
+ }
+
+ @Test
+ public void testNotMatchFilter() throws Exception {
+ String xpath = "//div[contains(@class,'xiao')]/text()";
+ JXNode node = underTest.selNOne(xpath);
+ Assert.assertEquals("Two", node.asString());
+ }
+
+ @Test
+ @DataProvider(value = {
+ "//a/@href",
+ "//div[@class='paginator']/span[@class='next']/a/@href",
+ })
+ public void testXpath(String xpath) throws XpathSyntaxErrorException {
+ logger.info("current xpath: {}", xpath);
+ List rs = doubanTest.selN(xpath);
+ for (JXNode n : rs) {
+ if (!n.isString()) {
+ int index = n.asElement().siblingIndex();
+ logger.info("index = {}", index);
+ }
+ logger.info(n.toString());
+ }
+ }
+
+ /**
+ * d_test.html 来源于 https://book.douban.com/tag/%E4%BA%92%E8%81%94%E7%BD%91
+ *
+ * 为了测试各种可能情况,ul[@class='subject-list']节点以及其下内容被复制了一份出来,并修改部分书名前缀为'T2-'以便区分
+ */
+ @DataProvider
+ public static Object[][] dataOfXpathAndexpect() {
+ return new Object[][]{
+ {"//ul[@class='subject-list']/li[position()<3][last()]/div/h2/allText()", "黑客与画家 : 硅谷创业之父Paul Graham文集T2-黑客与画家 : 硅谷创业之父Paul Graham文集"},
+ {"//ul[@class='subject-list']/li[first()]/div/h2/allText()", "失控 : 全人类的最终命运和结局T2-失控 : 全人类的最终命运和结局"},
+ {"//ul[@class='subject-list']/li[./div/div/span[@class='pl']/num()>(1000+90*(2*50))][last()][1]/div/h2/allText()", "长尾理论长尾理论"},
+ {"//ul[@class='subject-list']/li[self::li/div/div/span[@class='pl']/num()>10000][-1]/div/h2/allText()", "长尾理论长尾理论"},
+ {"//ul[@class='subject-list']/li[contains(self::li/div/div/span[@class='pl']//text(),'14582')]/div/h2//text()", "黑客与画家: 硅谷创业之父Paul Graham文集T2-黑客与画家: 硅谷创业之父Paul Graham文集"},
+ {"//ul[@class='subject-list']/li[contains(./div/div/span[@class='pl']//text(),'14582')]/div/h2//text()", "黑客与画家: 硅谷创业之父Paul Graham文集T2-黑客与画家: 硅谷创业之父Paul Graham文集"},
+ {"//*[@id=\"subject_list\"]/ul/li[2]/div[2]/h2/a//text()", "黑客与画家: 硅谷创业之父Paul Graham文集T2-黑客与画家: 硅谷创业之父Paul Graham文集"},
+ {"//ul[@class]", 3L},
+ {"//a[@id]/@href", "https://www.douban.com/doumail/"},
+ {"//*[@id=\"subject_list\"]/ul[1]/li[8]/div[2]/div[2]/span[3]/num()", "3734.0"},
+ {"//a[@id]/@href | //*[@id=\"subject_list\"]/ul[1]/li[8]/div[2]/div[2]/span[3]/num()", "https://www.douban.com/doumail/3734.0"},
+ };
+ }
+
+ @UseDataProvider("dataOfXpathAndexpect")
+ @Test
+ public void testXpathAndAssert(String xpath, Object expect) throws XpathSyntaxErrorException {
+ logger.info("current xpath: {}", xpath);
+ List rs = doubanTest.selN(xpath);
+ if (expect instanceof String) {
+ String res = StringUtils.join(rs, "");
+ logger.info(res);
+ Assert.assertEquals(expect, res);
+ } else if (expect instanceof Number) {
+ long size = (long) expect;
+ Assert.assertEquals(size, rs.size());
+ }
+ }
+
+ @Test
+ @DataProvider(value = {
+ "//ul[@class='subject-list']/li[position()<3]"
+ })
+ public void testJXNode(String xpath) throws XpathSyntaxErrorException {
+ logger.info("current xpath: {}", xpath);
+ List jxNodeList = doubanTest.selN(xpath);
+ Set expect = new HashSet<>();
+ //第一个 ul 中的
+ expect.add("失控: 全人类的最终命运和结局");
+ expect.add("黑客与画家: 硅谷创业之父Paul Graham文集");
+ //第二个 ul 中的
+ expect.add("T2-失控: 全人类的最终命运和结局");
+ expect.add("T2-黑客与画家: 硅谷创业之父Paul Graham文集");
+
+ Set res = new HashSet<>();
+ for (JXNode node : jxNodeList) {
+ if (!node.isString()) {
+ String currentRes = StringUtils.join(node.sel("/div/h2/a//text()"), "");
+ logger.info(currentRes);
+ res.add(currentRes);
+ }
+ }
+ Assert.assertEquals(expect, res);
+ }
+
+ @Test
+ @DataProvider(value = {
+ "//ul[@class='subject-list']"
+ })
+ public void testRecursionNode(String xpath) throws XpathSyntaxErrorException {
+ logger.info("current xpath: {}", xpath);
+ List jxNodeList = doubanTest.selN(xpath);
+ logger.info("size = {}", jxNodeList.size());
+ // 有两个ul,下面的是为了测试特意复制添加的
+ Assert.assertEquals(2, jxNodeList.size());
+ }
+
+ @Test
+ @DataProvider(value = {
+ "//body/div/div/h1/text()",
+ "/body/div/div/h1/text()"
+ })
+ public void absolutePathTest(String xpath) throws XpathSyntaxErrorException {
+ logger.info("current xpath: {}", xpath);
+ List jxNodeList = doubanTest.selN(xpath);
+ logger.info("size = {},res ={}", jxNodeList.size(), jxNodeList);
+ }
+
+ @Test
+ public void testAs() throws XpathSyntaxErrorException {
+ List jxNodeList = custom.selN("//b[contains(text(),'性别')]/parent::*/text()");
+ Assert.assertEquals("男", StringUtils.join(jxNodeList, ""));
+ for (JXNode jxNode : jxNodeList) {
+ logger.info(jxNode.toString());
+ }
+ }
+
+ /**
+ * fix https://github.com/zhegexiaohuozi/JsoupXpath/issues/33
+ */
+// @Test
+ public void testNotObj() {
+ JXDocument doc = JXDocument.createByUrl("https://www.gxwztv.com/61/61514/");
+// List nodes = doc.selN("//*[@id=\"chapters-list\"]/li[@style]");
+ List nodes = doc.selN("//*[@id=\"chapters-list\"]/li[not(@style)]");
+ for (JXNode node : nodes) {
+ logger.info("r = {}", node);
+ }
+ }
+
+ /**
+ * fix https://github.com/zhegexiaohuozi/JsoupXpath/issues/34
+ */
+ @Test
+ public void testAttrAtRoot() {
+ String content = "\n" +
+ " \n" +
+ " \n" +
+ " 第2章 神奇交流群 \n" +
+ " \n" +
+ "";
+ JXDocument doc = JXDocument.create(content);
+ List nodes = doc.selN("//@href");
+ for (JXNode node : nodes) {
+ logger.info("r = {}", node);
+ }
+ }
+
+ @Test
+ public void testA() {
+ String content = "网页设计师 ";
+ JXDocument doc = JXDocument.create(content);
+ List nodes = doc.selN("//*[text()='网页设计师']");
+ for (JXNode node : nodes) {
+ logger.info("r = {}", node);
+ }
+ }
+
+ /**
+ * fix https://github.com/zhegexiaohuozi/JsoupXpath/issues/52
+ */
+ @Test
+ public void fixTextBehaviorTest() {
+ String html = "分类: 动漫地区: 日本年份: 2010
";
+ JXDocument jxDocument = JXDocument.create(html);
+ List jxNodes = jxDocument.selN("//text()[3]");
+ String actual = StringUtils.join(jxNodes, "");
+ logger.info("actual = {}", actual);
+ Assert.assertEquals("2010", actual);
+ List nodes = jxDocument.selN("//text()");
+ String allText = StringUtils.join(nodes, "");
+ Assert.assertEquals("分类:动漫地区:日本年份:2010", allText);
+ logger.info("all = {}", allText);
+ }
+
+ /**
+ * fix https://github.com/zhegexiaohuozi/JsoupXpath/issues/44
+ */
+ @Test
+ public void fixTextElNoParentTest() {
+ String test = "";
+ JXDocument j = JXDocument.create(test);
+ List l = j.selN("//div[@class='a']//text()[not(ancestor::div[@class='e'])]");
+ Set finalRes = new HashSet<>();
+ for (JXNode i : l) {
+ logger.info("{}", i.toString());
+ finalRes.add(i.asString());
+ }
+ Assert.assertFalse(finalRes.contains("not need"));
+ Assert.assertTrue(finalRes.contains("need"));
+ Assert.assertEquals(4, finalRes.size());
+ }
+
+ /**
+ * fix https://github.com/zhegexiaohuozi/JsoupXpath/issues/53
+ */
+ @Test
+ public void fixIssue53() {
+ String content = " \n" +
+ " \n" +
+ "
\n" +
+ " \n" +
+ "
\n" +
+ "
巡璃 | 短篇 | 连载
\n" +
+ "
这是一位普通老兵的故事,这位老兵没有走上战场,也没有人歌颂他,但他的工作却是面对生与死,他是一名普通的军转干部,没有得到任何荣誉,却仍旧坚守着信仰,永远忠诚。除了他的家人,他的战友,他的故事不被任何人所知,但他的故事正是一代军人、一代军转干部的写照。所以,我来歌颂他,歌颂那一代人。
\n" +
+ "
最新更新 第一次见识到生死 · 2020-02-19
\n" +
+ "
\n" +
+ " \n" +
+ "
\n" +
+ "
4497 总字数
\n" +
+ "
0 总推荐
\n" +
+ "
\n" +
+ "
书籍详情 加入书架
\n" +
+ "
";
+ JXDocument j = JXDocument.create(content);
+ List l = j.selN("//*[text()='总字数']//text()");
+ Assert.assertEquals(2, l.size());
+ Assert.assertEquals("4497", l.get(0).asString());
+ Assert.assertEquals("总字数", l.get(1).asString());
+ }
+
+}
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/DateFormatTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/DateFormatTest.java
new file mode 100644
index 000000000..8b9571d89
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/DateFormatTest.java
@@ -0,0 +1,49 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.junit.Test;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @description: TODO
+ * @create: 2019-01-21 21:07
+ * @author: zzldn@163.com
+ * @since JDK1.8
+ **/
+
+public class DateFormatTest {
+
+ @Test
+ public void defaultTest() {
+ List params = new LinkedList<>();
+ params.add(XValue.create("2019-01-21 19:05:42"));
+ params.add(XValue.create("yyyy-MM-dd HH:mm:ss"));
+ FormatDate formatDate = new FormatDate();
+ XValue value = formatDate.call(null, params);
+ System.out.println(value.asDate());
+ }
+
+ @Test
+ public void defaultTimeTest() {
+ List params = new LinkedList<>();
+ params.add(XValue.create("19:05:42"));
+ params.add(XValue.create("HH:mm:ss"));
+ FormatDate formatDate = new FormatDate();
+ XValue value = formatDate.call(null, params);
+ System.out.println(value.asDate());
+ }
+
+ @Test
+ public void localTest() {
+ List params = new LinkedList<>();
+ params.add(XValue.create("1/21/2019 07:05:42 AM"));
+ params.add(XValue.create("MM/dd/yyyy hh:mm:ss aa"));
+ params.add(XValue.create(Locale.ENGLISH.toString()));
+ FormatDate formatDate = new FormatDate();
+ XValue value = formatDate.call(null, params);
+ System.out.println(value.asDate());
+ }
+}
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/SubStringTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/SubStringTest.java
new file mode 100644
index 000000000..1ca106f86
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/function/SubStringTest.java
@@ -0,0 +1,53 @@
+package org.seimicrawler.xpath.core.function;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.seimicrawler.xpath.core.XValue;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * SubString Tester.
+ *
+ * @author seimimaster@gmail.com
+ * @version 1.0
+ */
+public class SubStringTest {
+
+ /**
+ * substring("12345", 1.5, 2.6) returns "234"
+ * Method: call(Element context, List params)
+ */
+ @Test
+ public void testCall() throws Exception {
+ List params = new LinkedList<>();
+ params.add(XValue.create("12345"));
+ params.add(XValue.create("1.5"));
+ params.add(XValue.create("2.6"));
+ SubString subStringFunc = new SubString();
+ Assert.assertEquals(subStringFunc.call(null, params).asString(), "234");
+ }
+
+ @Test
+ public void testZeroLength() throws Exception {
+ List params = new LinkedList<>();
+ params.add(XValue.create("12345"));
+ params.add(XValue.create("2"));
+ params.add(XValue.create("-6"));
+ SubString subStringFunc = new SubString();
+ Assert.assertEquals(subStringFunc.call(null, params).asString(), "");
+ }
+
+ @Test
+ public void testOneLength() throws Exception {
+ List params = new LinkedList<>();
+ params.add(XValue.create("12345"));
+ params.add(XValue.create("0"));
+ params.add(XValue.create("1"));
+ SubString subStringFunc = new SubString();
+ Assert.assertEquals(subStringFunc.call(null, params).asString(), "1");
+ }
+
+
+}
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/node/NumTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/node/NumTest.java
new file mode 100644
index 000000000..d685fcbac
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/core/node/NumTest.java
@@ -0,0 +1,59 @@
+package org.seimicrawler.xpath.core.node;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.junit.Assert;
+import org.junit.Test;
+import org.seimicrawler.xpath.BaseTest;
+import org.seimicrawler.xpath.core.Scope;
+import org.seimicrawler.xpath.core.XValue;
+
+/**
+ * Num Tester.
+ *
+ * @author seimimaster@gmail.com
+ * @version 1.0
+ */
+public class NumTest extends BaseTest {
+
+ /**
+ * Method: call(Elements context)
+ */
+ @Test
+ public void testCall() throws Exception {
+ Elements context = new Elements();
+ Element el = new Element("V");
+ el.appendText("test 33.69");
+ context.add(el);
+ Num n = new Num();
+ XValue v = n.call(Scope.create(context));
+ logger.info("v = {}", v);
+ Assert.assertEquals(33.69, v.asDouble(), 0.00000000000001);
+ }
+
+ @Test
+ public void testShort() throws Exception {
+ Elements context = new Elements();
+ Element el = new Element("V");
+ el.appendText("test .69");
+ context.add(el);
+ Num n = new Num();
+ XValue v = n.call(Scope.create(context));
+ logger.info("v = {}", v);
+ Assert.assertEquals(0.69, v.asDouble(), 0.00000000000001);
+ }
+
+ @Test
+ public void testOnZero() throws Exception {
+ Elements context = new Elements();
+ Element el = new Element("V");
+ el.appendText("test 69.");
+ context.add(el);
+ Num n = new Num();
+ XValue v = n.call(Scope.create(context));
+ logger.info("v = {}", v);
+ Assert.assertEquals(69, v.asDouble(), 0.00000000000001);
+ }
+
+
+}
diff --git a/JsoupXpath/src/test/java/org/seimicrawler/xpath/expr/ExprTest.java b/JsoupXpath/src/test/java/org/seimicrawler/xpath/expr/ExprTest.java
new file mode 100644
index 000000000..df7bbc832
--- /dev/null
+++ b/JsoupXpath/src/test/java/org/seimicrawler/xpath/expr/ExprTest.java
@@ -0,0 +1,63 @@
+package org.seimicrawler.xpath.expr;
+
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.apache.commons.io.FileUtils;
+import org.jsoup.Jsoup;
+import org.jsoup.select.Elements;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.seimicrawler.xpath.BaseTest;
+import org.seimicrawler.xpath.antlr.XpathLexer;
+import org.seimicrawler.xpath.antlr.XpathParser;
+import org.seimicrawler.xpath.core.XValue;
+import org.seimicrawler.xpath.core.XpathProcessor;
+import org.seimicrawler.xpath.exception.DoFailOnErrorHandler;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author github.com/zhegexiaohuozi seimimaster@gmail.com
+ * @since 2017/12/6.
+ */
+public class ExprTest extends BaseTest {
+
+ private Elements root;
+ private final ClassLoader loader = getClass().getClassLoader();
+
+ @Before
+ public void init() throws Exception {
+ // https://book.douban.com/tag/%E4%BA%92%E8%81%94%E7%BD%91
+ URL t = loader.getResource("d_test.html");
+ assert t != null;
+ File dBook = new File(t.toURI());
+ String context = FileUtils.readFileToString(dBook, StandardCharsets.UTF_8);
+ root = Jsoup.parse(context).children();
+ }
+
+ @Test
+ public void exp() {
+ String xpath = "//a[@id]/@href";
+ CharStream input = CharStreams.fromString(xpath);
+ XpathLexer lexer = new XpathLexer(input);
+ CommonTokenStream tokens = new CommonTokenStream(lexer);
+ XpathParser parser = new XpathParser(tokens);
+ parser.setErrorHandler(new DoFailOnErrorHandler());
+ ParseTree tree = parser.main();
+ XpathProcessor processor = new XpathProcessor(root);
+ XValue value = processor.visit(tree);
+ logger.info("visit res = {}", value);
+ }
+
+ @Test
+ public void roundHalfUp() {
+ int x = new BigDecimal("5.53").setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
+ Assert.assertEquals(6, x);
+ }
+}
\ No newline at end of file
diff --git a/JsoupXpath/src/test/resources/d_test.html b/JsoupXpath/src/test/resources/d_test.html
new file mode 100644
index 000000000..b7becb155
--- /dev/null
+++ b/JsoupXpath/src/test/resources/d_test.html
@@ -0,0 +1,5416 @@
+
+
+
+
+
+ 豆瓣图书标签: 互联网
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
豆瓣图书标签: 互联网
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 凯文·凯利 / 东西文库 / 新星出版社 / 2010-12 / 88.00元
+
+
+
+
+
+
+ 8.7
+
+
+ (12637人评价)
+
+
+
+
+
《失控》,全名为《失控:机器、社会与经济的新生物学》(Out of Control: The New Biology of
+ Machines,
+ Social...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] Paul Graham / 阮一峰 / 人民邮电出版社 / 2011-4 / 49.00元
+
+
+
+
+
+
+ 8.8
+
+
+ (14582人评价)
+
+
+
+
+
本书是硅谷创业之父Paul Graham
+ 的文集,主要介绍黑客即优秀程序员的爱好和动机,讨论黑客成长、黑客对世界的贡献以及编程语言和黑客工作方法等所有对计算...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] Marty Cagan / 七印部落 / 华中科技大学出版社 / 2011-5 / 36.00元
+
+
+
+
+
+
+ 8.5
+
+
+ (4842人评价)
+
+
+
+
+
+ 为什么市场上那么多软件产品无人问津,成功的产品凤毛麟角?怎样发掘有价值的产品?拿什么说服开发团队接受你的产品设计?如何将敏捷方法融入产品开发?过去二十多年,...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克里斯·安德森 / 乔江涛 / 中信出版社 / 2006-12 / 35.00元
+
+
+
+
+
+
+ 7.7
+
+
+ (11063人评价)
+
+
+
+
+
书中阐述,商业和文化的未来不在于传统需求曲线上那个代表“畅销商品”(hits)的头部;
+ 而是那条代表“冷门商品”(misses)经常为人遗忘的长尾。
+ 举例来...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 苏杰 / 电子工业出版社 / 2010年4月 / 45.00元
+
+
+
+
+
+
+ 7.6
+
+
+ (6509人评价)
+
+
+
+
+
+ 这是写给“-1到3岁的产品经理”的书,适合刚入门的产品经理、产品规划师、需求分析师,以及对做产品感兴趣的学生,用户体验、市场运营、技术部门的朋友们,特别是互...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 大卫·柯克帕特里克 / 沈路、梁军、崔筝 / 华文出版社 / 2010-10 / 49.80
+
+
+
+
+
+
+ 7.9
+
+
+ (4618人评价)
+
+
+
+
+
+ 本书作者近距离地采访了与Facebook相关的人士,其中包括Facebook的创始人、员工、投资人、意向投资人以及合作伙伴,加起来超过了130人。这是真切详...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 杰弗里·摩尔(Geoffrey A. Moore) / 赵娅 / 机械工业出版社 / 2009-1 / 36.00元
+
+
+
+
+
+
+ 8.4
+
+
+ (400人评价)
+
+
+
+
+
在真正涉足高科技领域之前,你有必要读一读这本书——在这个节奏飞快、竞争激烈的技术竞技场上,这本书绝对能够帮助你更容易地获得成功。
+ ——威廉姆·劳森 罗盛软件...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jesse James Garrett / 范晓燕 / 机械工业出版社 / 2007年10月 / 25.00
+
+
+
+
+
+
+ 8.2
+
+
+ (3734人评价)
+
+
+
+
+
这不是一本关于“怎样做(How-to)”的书。有很多很多讨论如何建设网站的书,这本不是。
+ 这不是一本关于技术的书。在这里你找不到一行代码。
+ 这不是一本有答案...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 尼古拉斯·卡尔 / 刘纯毅 / 中信出版社 / 2010-12 / 42.00元
+
+
+
+
+
+
+ 7.6
+
+
+ (2135人评价)
+
+
+
+
+
+ 《浅薄:互联网如何毒化了我们的大脑》在我们跟计算机越来越密不可分的过程中,我们越来越多的人生体验通过电脑屏幕上闪烁摇曳、虚无缥缈的符号完成,最大的危险就是我...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 林军 / 中信出版社 / 2009-7 / 59.00
+
+
+
+
+
+
+ 8.2
+
+
+ (2072人评价)
+
+
+
+
+
覆雨翻云的中国网事; 荡气回肠的产业传奇;虚拟世界的真实讲述;万象网络的还原走笔。
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克莱·舍基 / 胡泳、沈满琳 / 中国人民大学出版社 / 2009-5 / 39.80
+
+
+
+
+
+
+ 7.5
+
+
+ (2419人评价)
+
+
+
+
+
+ 一位妇女丢掉了手机,但征召了一群志愿者将其从盗窃者手中夺回。一个旅客在乘坐飞机时领受恶劣服务,她通过自己的博客发动了一场全民运动。在伦敦地铁爆炸案和印度洋海...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jason Fried、Heinemeier David Hansson、Matthew Linderman /
+ 37signals / 2009-11-18 /
+ USD 24.99
+
+
+
+
+
+
+ 9.1
+
+
+ (916人评价)
+
+
+
+
+
Getting Real details the business, design, programming, and
+ marketing
+ principl...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 王坚 / 人民邮电出版社 / 2010-4 / 55.00元
+
+
+
+
+
+
+ 7.9
+
+
+ (3168人评价)
+
+
+
+
+
+ 本书作者一直从事互联网产品的研究和实战,经验丰富,同时作为导师,指导了大量优秀的产品经理,本书的内容也是作者8年来培养产品经理新兵的经验集萃。如果你缺乏培养...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克里斯·安德森 / 蒋旭峰、冯斌、璩静 / 中信出版社 / 2009-9 / 39.00
+
+
+
+
+
+
+ 7.5
+
+
+ (3397人评价)
+
+
+
+
+
在《免费:商业的未来
+ 》这本书,克里斯·安德森认为,新型的“免费”并不是一种左口袋出、右口袋进的营销策略,而是一种把货物和服务的成本压低到零的新型卓越能力。...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 贾森·弗里德、[丹] 戴维·海涅迈尔·汉森 / 李瑜偲 / 中信出版社 / 2010-10 / 36.00元
+
+
+
+
+
+
+ 8.3
+
+
+ (6308人评价)
+
+
+
+
+
大多数的企业管理的书籍都会告诉你:制定商业计划、分析竞争形势、寻找投资人等等。如果你要找的是那样的书,那么把这本书放回书架吧。
+ 这本书呈现的是一种更好、更简...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 卡尔・夏皮罗(Carl Shapiro)、[美] 哈尔・瓦里安(Hal Varian) / 张帆 / 中国人民大学出版社
+ / 2000-6 / 33.00元
+
+
+
+
+
+
+ 9.1
+
+
+ (276人评价)
+
+
+
+
+
+ 本书的目标是,运用网络经济中的经济学知识,从经济研究和作者自己的经验中提取出适合信息相关产业的经理们的知识。本书描述的思想、概念、模型和思考方法会帮助读者作...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 克莱顿•克里斯坦森( Clayton M. Christensen ) / 胡建桥 / 中信出版社 / 2010-6 /
+ 38.00元
+
+
+
+
+
+
+ 8.5
+
+
+ (1799人评价)
+
+
+
+
+
管理类经典图书
+ o 被《福布斯》评为20世纪最具影响的20本商业图书之一
+ o “全球商业书籍奖”获奖图书
+ “颠覆大师”克莱顿•克里斯坦森经典力作。
+ 《金融时...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 尼葛洛庞帝 / 胡泳 等 / 海南出版社 / 1997-2 / 16.80元
+
+
+
+
+
+
+ 7.8
+
+
+ (915人评价)
+
+
+
+
+
+ 《数字化生存》可以说是二十世纪信息技术及理念发展的圣经,此书的流行和传播对上个世纪信息时代的启蒙、发展产生了深远的影响,本书深入浅出地讲解了信息技术的基本概...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 谢家华 / [美] 谢传刚 / 中华工商联合出版社 / 2011-1 / 32.90元
+
+
+
+
+
+
+ 8.3
+
+
+ (2409人评价)
+
+
+
+
+
+ 本书是“美捷步”(Zappos)首席执行官谢家华创造奇迹的心路历程与商业哲学的精华萃取,分享了他在商场与生活中得到的宝贵经验与教训。从儿时创办蚯蚓养殖场到大...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 胡泳 / 广西师范大学出版社 / 2008-9 / 35.00元
+
+
+
+
+
+
+ 8.0
+
+
+ (718人评价)
+
+
+
+
+
+ 本书触及了网络政治学中的一个重大话题——网络空间中的私域与公域。随着科技的进步,在信息时代的开端,公与私的含义和边界都出现了不容忽视的游移。《众声喧哗》主要...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 凯文·凯利 / 东西文库 / 新星出版社 / 2010-12 / 88.00元
+
+
+
+
+
+
+ 8.7
+
+
+ (12637人评价)
+
+
+
+
+
《失控》,全名为《失控:机器、社会与经济的新生物学》(Out of Control: The New Biology of
+ Machines,
+ Social...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 凯文·凯利 / 东西文库 / 新星出版社 / 2010-12 / 88.00元
+
+
+
+
+
+
+ 8.7
+
+
+ (12637人评价)
+
+
+
+
+
《失控》,全名为《失控:机器、社会与经济的新生物学》(Out of Control: The New Biology of
+ Machines,
+ Social...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] Paul Graham / 阮一峰 / 人民邮电出版社 / 2011-4 / 49.00元
+
+
+
+
+
+
+ 8.8
+
+
+ (14582人评价)
+
+
+
+
+
本书是硅谷创业之父Paul Graham
+ 的文集,主要介绍黑客即优秀程序员的爱好和动机,讨论黑客成长、黑客对世界的贡献以及编程语言和黑客工作方法等所有对计算...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] Marty Cagan / 七印部落 / 华中科技大学出版社 / 2011-5 / 36.00元
+
+
+
+
+
+
+ 8.5
+
+
+ (4842人评价)
+
+
+
+
+
+ 为什么市场上那么多软件产品无人问津,成功的产品凤毛麟角?怎样发掘有价值的产品?拿什么说服开发团队接受你的产品设计?如何将敏捷方法融入产品开发?过去二十多年,...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克里斯·安德森 / 乔江涛 / 中信出版社 / 2006-12 / 35.00元
+
+
+
+
+
+
+ 7.7
+
+
+ (11063人评价)
+
+
+
+
+
书中阐述,商业和文化的未来不在于传统需求曲线上那个代表“畅销商品”(hits)的头部;
+ 而是那条代表“冷门商品”(misses)经常为人遗忘的长尾。
+ 举例来...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 苏杰 / 电子工业出版社 / 2010年4月 / 45.00元
+
+
+
+
+
+
+ 7.6
+
+
+ (6509人评价)
+
+
+
+
+
+ 这是写给“-1到3岁的产品经理”的书,适合刚入门的产品经理、产品规划师、需求分析师,以及对做产品感兴趣的学生,用户体验、市场运营、技术部门的朋友们,特别是互...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 大卫·柯克帕特里克 / 沈路、梁军、崔筝 / 华文出版社 / 2010-10 / 49.80
+
+
+
+
+
+
+ 7.9
+
+
+ (4618人评价)
+
+
+
+
+
+ 本书作者近距离地采访了与Facebook相关的人士,其中包括Facebook的创始人、员工、投资人、意向投资人以及合作伙伴,加起来超过了130人。这是真切详...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 杰弗里·摩尔(Geoffrey A. Moore) / 赵娅 / 机械工业出版社 / 2009-1 / 36.00元
+
+
+
+
+
+
+ 8.4
+
+
+ (400人评价)
+
+
+
+
+
在真正涉足高科技领域之前,你有必要读一读这本书——在这个节奏飞快、竞争激烈的技术竞技场上,这本书绝对能够帮助你更容易地获得成功。
+ ——威廉姆·劳森 罗盛软件...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jesse James Garrett / 范晓燕 / 机械工业出版社 / 2007年10月 / 25.00
+
+
+
+
+
+
+ 8.2
+
+
+ (3734人评价)
+
+
+
+
+
这不是一本关于“怎样做(How-to)”的书。有很多很多讨论如何建设网站的书,这本不是。
+ 这不是一本关于技术的书。在这里你找不到一行代码。
+ 这不是一本有答案...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 尼古拉斯·卡尔 / 刘纯毅 / 中信出版社 / 2010-12 / 42.00元
+
+
+
+
+
+
+ 7.6
+
+
+ (2135人评价)
+
+
+
+
+
+ 《浅薄:互联网如何毒化了我们的大脑》在我们跟计算机越来越密不可分的过程中,我们越来越多的人生体验通过电脑屏幕上闪烁摇曳、虚无缥缈的符号完成,最大的危险就是我...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 林军 / 中信出版社 / 2009-7 / 59.00
+
+
+
+
+
+
+ 8.2
+
+
+ (2072人评价)
+
+
+
+
+
覆雨翻云的中国网事; 荡气回肠的产业传奇;虚拟世界的真实讲述;万象网络的还原走笔。
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克莱·舍基 / 胡泳、沈满琳 / 中国人民大学出版社 / 2009-5 / 39.80
+
+
+
+
+
+
+ 7.5
+
+
+ (2419人评价)
+
+
+
+
+
+ 一位妇女丢掉了手机,但征召了一群志愿者将其从盗窃者手中夺回。一个旅客在乘坐飞机时领受恶劣服务,她通过自己的博客发动了一场全民运动。在伦敦地铁爆炸案和印度洋海...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jason Fried、Heinemeier David Hansson、Matthew Linderman /
+ 37signals / 2009-11-18 /
+ USD 24.99
+
+
+
+
+
+
+ 9.1
+
+
+ (916人评价)
+
+
+
+
+
Getting Real details the business, design, programming, and
+ marketing
+ principl...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 王坚 / 人民邮电出版社 / 2010-4 / 55.00元
+
+
+
+
+
+
+ 7.9
+
+
+ (3168人评价)
+
+
+
+
+
+ 本书作者一直从事互联网产品的研究和实战,经验丰富,同时作为导师,指导了大量优秀的产品经理,本书的内容也是作者8年来培养产品经理新兵的经验集萃。如果你缺乏培养...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 克里斯·安德森 / 蒋旭峰、冯斌、璩静 / 中信出版社 / 2009-9 / 39.00
+
+
+
+
+
+
+ 7.5
+
+
+ (3397人评价)
+
+
+
+
+
在《免费:商业的未来
+ 》这本书,克里斯·安德森认为,新型的“免费”并不是一种左口袋出、右口袋进的营销策略,而是一种把货物和服务的成本压低到零的新型卓越能力。...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 贾森·弗里德、[丹] 戴维·海涅迈尔·汉森 / 李瑜偲 / 中信出版社 / 2010-10 / 36.00元
+
+
+
+
+
+
+ 8.3
+
+
+ (6308人评价)
+
+
+
+
+
大多数的企业管理的书籍都会告诉你:制定商业计划、分析竞争形势、寻找投资人等等。如果你要找的是那样的书,那么把这本书放回书架吧。
+ 这本书呈现的是一种更好、更简...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 卡尔・夏皮罗(Carl Shapiro)、[美] 哈尔・瓦里安(Hal Varian) / 张帆 / 中国人民大学出版社
+ / 2000-6 / 33.00元
+
+
+
+
+
+
+ 9.1
+
+
+ (276人评价)
+
+
+
+
+
+ 本书的目标是,运用网络经济中的经济学知识,从经济研究和作者自己的经验中提取出适合信息相关产业的经理们的知识。本书描述的思想、概念、模型和思考方法会帮助读者作...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 克莱顿•克里斯坦森( Clayton M. Christensen ) / 胡建桥 / 中信出版社 / 2010-6 /
+ 38.00元
+
+
+
+
+
+
+ 8.5
+
+
+ (1799人评价)
+
+
+
+
+
管理类经典图书
+ o 被《福布斯》评为20世纪最具影响的20本商业图书之一
+ o “全球商业书籍奖”获奖图书
+ “颠覆大师”克莱顿•克里斯坦森经典力作。
+ 《金融时...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 尼葛洛庞帝 / 胡泳 等 / 海南出版社 / 1997-2 / 16.80元
+
+
+
+
+
+
+ 7.8
+
+
+ (915人评价)
+
+
+
+
+
+ 《数字化生存》可以说是二十世纪信息技术及理念发展的圣经,此书的流行和传播对上个世纪信息时代的启蒙、发展产生了深远的影响,本书深入浅出地讲解了信息技术的基本概...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [美] 谢家华 / [美] 谢传刚 / 中华工商联合出版社 / 2011-1 / 32.90元
+
+
+
+
+
+
+ 8.3
+
+
+ (2409人评价)
+
+
+
+
+
+ 本书是“美捷步”(Zappos)首席执行官谢家华创造奇迹的心路历程与商业哲学的精华萃取,分享了他在商场与生活中得到的宝贵经验与教训。从儿时创办蚯蚓养殖场到大...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 胡泳 / 广西师范大学出版社 / 2008-9 / 35.00元
+
+
+
+
+
+
+ 8.0
+
+
+ (718人评价)
+
+
+
+
+
+ 本书触及了网络政治学中的一个重大话题——网络空间中的私域与公域。随着科技的进步,在信息时代的开端,公与私的含义和边界都出现了不容忽视的游移。《众声喧哗》主要...
+
+
+
+
+
+
+
+
+
+
+ 想读
+
+
+
+
+ 在读
+
+
+
+
+ 读过
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <前页
+
+
+
+
1
+
+
2
+
+
+
3
+
+
+
4
+
+
+
5
+
+
+
6
+
+
+
7
+
+
+
8
+
+
+
9
+
+
...
+
+
96
+
+
97
+
+
+
+ 后页>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 相关的标签
+ · · · · · ·
+
+
+
+
+
+
+
+
+
+
+
+
+
+ > 浏览全部图书标签
+
+
+
+
+ "互联网"相关豆列
+ · · · · · ·
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 767a79371..182d3bd01 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -123,6 +123,7 @@ kapt {
dependencies {
implementation project(path: ':epublib')
+ implementation project(path: ':JsoupXpath')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
testImplementation 'junit:junit:4.13.2'
@@ -177,9 +178,6 @@ dependencies {
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
//规则相关
- implementation 'org.jsoup:jsoup:1.14.1'
- //noinspection GradleDependency
- implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
implementation 'com.jayway.jsonpath:json-path:2.6.0'
//JS rhino
diff --git a/settings.gradle b/settings.gradle
index 6e5b9d8ee..a489efdc2 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app',':epublib'
+include ':app',':JsoupXpath',':epublib'