Merge pull request #462 from gedoor/master

优化URL拼接
pull/481/head
Antecer 4 years ago committed by GitHub
commit 3f25d16cf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 354
      app/src/main/assets/help/regex.md
  2. 5
      app/src/main/assets/updateLog.md
  3. 3
      app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt
  4. 119
      app/src/main/java/io/legado/app/help/ContentHelp.kt
  5. 2
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  6. 3
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  7. 26
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  8. 6
      app/src/main/java/io/legado/app/model/rss/Rss.kt
  9. 2
      app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt
  10. 2
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  11. 4
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  12. 2
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  13. 2
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  14. 13
      app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
  15. 16
      app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt
  16. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  17. 23
      app/src/main/java/io/legado/app/utils/NetworkUtils.kt
  18. 19
      app/src/main/res/layout/activity_replace_edit.xml

@ -0,0 +1,354 @@
## 正则表达式学习
- [基本匹配](#1-基本匹配)
- [元字符](#2-元字符)
- [英文句号](#21-英文句号)
- [字符集](#22-字符集)
- [否定字符集](#221-否定字符集)
- [重复](#23-重复)
- [星号](#231-星号)
- [加号](#232-加号)
- [问号](#233-问号)
- [花括号](#24-花括号)
- [字符组](#25-字符组)
- [分支结构](#26-分支结构)
- [转义特殊字符](#27-转义特殊字符)
- [定位符](#28-定位符)
- [插入符号](#281-插入符号)
- [美元符号](#282-美元符号)
- [简写字符集](#3-简写字符集)
- [断言](#4-断言)
- [正向先行断言](#41-正向先行断言)
- [负向先行断言](#42-负向先行断言)
- [正向后行断言](#43-正向后行断言)
- [负向后行断言](#44-负向后行断言)
- [标记](#5-标记)
- [不区分大小写](#51-不区分大小写)
- [全局搜索](#52-全局搜索)
- [多行匹配](#53-多行匹配)
- [常用正则表达式](#常用正则表达式)
## 1. 基本匹配
正则表达式只是我们用于在文本中检索字母和数字的模式。例如正则表达式 `cat`,表示: 字母 `c` 后面跟着一个字母 `a`,再后面跟着一个字母 `t`
<pre>
"cat" => The <a href="#learn-regex"><strong>cat</strong></a> sat on the mat
</pre>
正则表达式 `123` 会匹配字符串 "123"。通过将正则表达式中的每个字符逐个与要匹配的字符串中的每个字符进行比较,来完成正则匹配。
正则表达式通常区分大小写,因此正则表达式 `Cat` 与字符串 "cat" 不匹配。
<pre>
"Cat" => The cat sat on the <a href="#learn-regex"><strong>Cat</strong></a>
</pre>
## 2. 元字符
元字符是正则表达式的基本组成元素。元字符在这里跟它通常表达的意思不一样,而是以某种特殊的含义去解释。有些元字符写在方括号内的时候有特殊含义。
元字符如下:
|元字符|描述|
|:----:|----|
|.|匹配除换行符以外的任意字符。|
|[ ]|字符类,匹配方括号中包含的任意字符。|
|[^ ]|否定字符类。匹配方括号中不包含的任意字符|
|*|匹配前面的子表达式零次或多次|
|+|匹配前面的子表达式一次或多次|
|?|匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。|
|{n,m}|花括号,匹配前面字符至少 n 次,但是不超过 m 次。|
|(xyz)|字符组,按照确切的顺序匹配字符xyz。|
|&#124;|分支结构,匹配符号之前的字符或后面的字符。|
|&#92;|转义符,它可以还原元字符原来的含义,允许你匹配保留字符 <code>[ ] ( ) { } . * + ? ^ $ \ &#124;</code>|
|^|匹配行的开始|
|$|匹配行的结束|
## 2.1 英文句号
英文句号 `.` 是元字符的最简单的例子。元字符 `.` 可以匹配任意单个字符。它不会匹配换行符和新行的字符。例如正则表达式 `.ar`,表示: 任意字符后面跟着一个字母 `a`
再后面跟着一个字母 `r`
<pre>
".ar" => The <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
## 2.2 字符集
字符集也称为字符类。方括号被用于指定字符集。使用字符集内的连字符来指定字符范围。方括号内的字符范围的顺序并不重要。
例如正则表达式 `[Tt]he`,表示: 大写 `T` 或小写 `t` ,后跟字母 `h`,再后跟字母 `e`
<pre>
"[Tt]he" => <a href="#learn-regex"><strong>The</strong></a> car parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
然而,字符集中的英文句号表示它字面的含义。正则表达式 `ar[.]`,表示小写字母 `a`,后面跟着一个字母 `r`,再后面跟着一个英文句号 `.` 字符。
<pre>
"ar[.]" => A garage is a good place to park a c<a href="#learn-regex"><strong>ar.</strong></a>
</pre>
### 2.2.1 否定字符集
一般来说插入字符 `^` 表示一个字符串的开始,但是当它在方括号内出现时,它会取消字符集。例如正则表达式 `[^c]ar`,表示: 除了字母 `c` 以外的任意字符,后面跟着字符 `a`
再后面跟着一个字母 `r`
<pre>
"[^c]ar" => The car <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
## 2.3 重复
以下元字符 `+`,`*` 或 `?` 用于指定子模式可以出现多少次。这些元字符在不同情况下的作用不同。
### 2.3.1 星号
该符号 `*` 表示匹配上一个匹配规则的零次或多次。正则表达式 `a*` 表示小写字母 `a` 可以重复零次或者多次。但是它如果出现在字符集或者字符类之后,它表示整个字符集的重复。
例如正则表达式 `[a-z]*`,表示: 一行中可以包含任意数量的小写字母。
<pre>
"[a-z]*" => T<a href="#learn-regex"><strong>he</strong></a> <a href="#learn-regex"><strong>car</strong></a> <a href="#learn-regex"><strong>parked</strong></a> <a href="#learn-regex"><strong>in</strong></a> <a href="#learn-regex"><strong>the</strong></a> <a href="#learn-regex"><strong>garage</strong></a> #21.
</pre>
`*` 符号可以与元符号 `.` 用在一起,用来匹配任意字符串 `.*`。该 `*` 符号可以与空格符 `\s` 一起使用,用来匹配一串空格字符。
例如正则表达式 `\s*cat\s*`,表示: 零个或多个空格,后面跟小写字母 `c`,再后面跟小写字母 `a`,再再后面跟小写字母 `t`,后面再跟零个或多个空格。
<pre>
"\s*cat\s*" => The fat<a href="#learn-regex"><strong> cat </strong></a>sat on the <a href="#learn-regex"><strong>cat</strong></a>.
</pre>
### 2.3.2 加号
该符号 `+` 匹配上一个字符的一次或多次。例如正则表达式 `c.+t`,表示: 一个小写字母 `c`,后跟任意数量的字符,后跟小写字母 `t`
<pre>
"c.+t" => The fat <a href="#learn-regex"><strong>cat sat on the mat</strong></a>.
</pre>
### 2.3.3 问号
在正则表达式中,元字符 `?` 用来表示前一个字符是可选的。该符号匹配前一个字符的零次或一次。
例如正则表达式 `[T]?he`,表示: 可选的大写字母 `T`,后面跟小写字母 `h`,后跟小写字母 `e`
<pre>
"[T]he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.
</pre>
<pre>
"[T]?he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in t<a href="#learn-regex"><strong>he</strong></a> garage.
</pre>
## 2.4 花括号
在正则表达式中花括号(也被称为量词 ?)用于指定字符或一组字符可以重复的次数。例如正则表达式 `[0-9]{2,3}`,表示: 匹配至少2位数字但不超过3位(0到9范围内的字符)。
<pre>
"[0-9]{2,3}" => The number was 9.<a href="#learn-regex"><strong>999</strong></a>7 but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
我们可以省略第二个数字。例如正则表达式 `[0-9]{2,}`,表示: 匹配2个或更多个数字。如果我们也删除逗号,则正则表达式 `[0-9]{2}`,表示: 匹配正好为2位数的数字。
<pre>
"[0-9]{2,}" => The number was 9.<a href="#learn-regex"><strong>9997</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
<pre>
"[0-9]{2}" => The number was 9.<a href="#learn-regex"><strong>99</strong></a><a href="#learn-regex"><strong>97</strong></a> but we rounded it off to <a href="#learn-regex"><strong>10</strong></a>.0.
</pre>
## 2.5 字符组
字符组是一组写在圆括号内的子模式 `(...)`。正如我们在正则表达式中讨论的那样,如果我们把一个量词放在一个字符之后,它会重复前一个字符。
但是,如果我们把量词放在一个字符组之后,它会重复整个字符组。
例如正则表达式 `(ab)*` 表示匹配零个或多个的字符串 "ab"。我们还可以在字符组中使用元字符 `|`。例如正则表达式 `(c|g|p)ar`,表示: 小写字母 `c`、`g` 或 `p` 后面跟字母 `a`,后跟字母 `r`
<pre>
"(c|g|p)ar" => The <a href="#learn-regex"><strong>car</strong></a> is <a href="#learn-regex"><strong>par</strong></a>ked in the <a href="#learn-regex"><strong>gar</strong></a>age.
</pre>
## 2.6 分支结构
在正则表达式中垂直条 `|` 用来定义分支结构,分支结构就像多个表达式之间的条件。现在你可能认为这个字符集和分支机构的工作方式一样。
但是字符集和分支结构巨大的区别是字符集只在字符级别上有作用,然而分支结构在表达式级别上依然可以使用。
例如正则表达式 `(T|t)he|car`,表示: 大写字母 `T` 或小写字母 `t`,后面跟小写字母 `h`,后跟小写字母 `e` 或小写字母 `c`,后跟小写字母 `a`,后跟小写字母 `r`
<pre>
"(T|t)he|car" => <a href="#learn-regex"><strong>The</strong></a> <a href="#learn-regex"><strong>car</strong></a> is parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
## 2.7 转义特殊字符
正则表达式中使用反斜杠 `\` 来转义下一个字符。这将允许你使用保留字符来作为匹配字符 `{ } [ ] / \ + * . $ ^ | ?`。在特殊字符前面加 `\`,就可以使用它来做匹配字符。
例如正则表达式 `.` 是用来匹配除了换行符以外的任意字符。现在要在输入字符串中匹配 `.` 字符,正则表达式 `(f|c|m)at\.?`,表示: 小写字母 `f`、`c` 或者 `m` 后跟小写字母 `a`,后跟小写字母 `t`,后跟可选的 `.` 字符。
<pre>
"(f|c|m)at\.?" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> sat on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
## 2.8 定位符
在正则表达式中,为了检查匹配符号是否是起始符号或结尾符号,我们使用定位符。
定位符有两种类型: 第一种类型是 `^` 检查匹配字符是否是起始字符,第二种类型是 `$`,它检查匹配字符是否是输入字符串的最后一个字符。
### 2.8.1 插入符号
插入符号 `^` 符号用于检查匹配字符是否是输入字符串的第一个字符。如果我们使用正则表达式 `^a` (如果a是起始符号)匹配字符串 `abc`,它会匹配到 `a`
但是如果我们使用正则表达式 `^b`,它是匹配不到任何东西的,因为在字符串 `abc` 中 "b" 不是起始字符。
让我们来看看另一个正则表达式 `^(T|t)he`,这表示: 大写字母 `T` 或小写字母 `t` 是输入字符串的起始符号,后面跟着小写字母 `h`,后跟小写字母 `e`
<pre>
"(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in <a href="#learn-regex"><strong>the</strong></a> garage.
</pre>
<pre>
"^(T|t)he" => <a href="#learn-regex"><strong>The</strong></a> car is parked in the garage.
</pre>
### 2.8.2 美元符号
美元 `$` 符号用于检查匹配字符是否是输入字符串的最后一个字符。例如正则表达式 `(at\.)$`,表示: 小写字母 `a`,后跟小写字母 `t`,后跟一个 `.` 字符,且这个匹配器必须是字符串的结尾。
<pre>
"(at\.)" => The fat c<a href="#learn-regex"><strong>at.</strong></a> s<a href="#learn-regex"><strong>at.</strong></a> on the m<a href="#learn-regex"><strong>at.</strong></a>
</pre>
<pre>
"(at\.)$" => The fat cat sat on the m<a href="#learn-regex"><strong>at.</strong></a>
</pre>
## 3. 简写字符集
正则表达式为常用的字符集和常用的正则表达式提供了简写。简写字符集如下:
|简写|描述|
|:----:|----|
|.|匹配除换行符以外的任意字符|
|\w|匹配所有字母和数字的字符: `[a-zA-Z0-9_]`|
|\W|匹配非字母和数字的字符: `[^\w]`|
|\d|匹配数字: `[0-9]`|
|\D|匹配非数字: `[^\d]`|
|\s|匹配空格符: `[\t\n\f\r\p{Z}]`|
|\S|匹配非空格符: `[^\s]`|
## 4. 断言
后行断言和先行断言有时候被称为断言,它们是特殊类型的 ***非捕获组*** (用于匹配模式,但不包括在匹配列表中)。当我们在一种特定模式之前或者之后有这种模式时,会优先使用断言。
例如我们想获取输入字符串 `$4.44 and $10.88` 中带有前缀 `$` 的所有数字。我们可以使用这个正则表达式 `(?<=\$)[0-9\.]*`,表示: 获取包含 `.` 字符且前缀为 `$` 的所有数字。
以下是正则表达式中使用的断言:
|符号|描述|
|:----:|----|
|?=|正向先行断言|
|?!|负向先行断言|
|?<=|正向后行断言|
|?<!|负向后行断言|
### 4.1 正向先行断言
正向先行断言认为第一部分的表达式必须是先行断言表达式。返回的匹配结果仅包含与第一部分表达式匹配的文本。
要在一个括号内定义一个正向先行断言,在括号中问号和等号是这样使用的 `(?=...)`。先行断言表达式写在括号中的等号后面。
例如正则表达式 `(T|t)he(?=\sfat)`,表示: 匹配大写字母 `T` 或小写字母 `t`,后面跟字母 `h`,后跟字母 `e`
在括号中,我们定义了正向先行断言,它会引导正则表达式引擎匹配 `The``the` 后面跟着 `fat`
<pre>
"(T|t)he(?=\sfat)" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.
</pre>
### 4.2 负向先行断言
当我们需要从输入字符串中获取不匹配表达式的内容时,使用负向先行断言。负向先行断言的定义跟我们定义的正向先行断言一样,
唯一的区别是不是等号 `=`,我们使用否定符号 `!`,例如 `(?!...)`
我们来看看下面的正则表达式 `(T|t)he(?!\sfat)`,表示: 从输入字符串中获取全部 `The` 或者 `the` 且不匹配 `fat` 前面加上一个空格字符。
<pre>
"(T|t)he(?!\sfat)" => The fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.
</pre>
### 4.3 正向后行断言
正向后行断言是用于获取在特定模式之前的所有匹配内容。正向后行断言表示为 `(?<=...)`。例如正则表达式 `(?<=(T|t)he\s)(fat|mat)`,表示: 从输入字符串中获取在单词 `The``the` 之后的所有 `fat``mat` 单词。
<pre>
"(?<=(T|t)he\s)(fat|mat)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the <a href="#learn-regex"><strong>mat</strong></a>.
</pre>
### 4.4 负向后行断言
负向后行断言是用于获取不在特定模式之前的所有匹配的内容。负向后行断言表示为 `(?<!...)`。例如正则表达式 `(?<!(T|t)he\s)(cat)`,表示: 在输入字符中获取所有不在 `The``the` 之后的所有单词 `cat`
<pre>
"(?&lt;!(T|t)he\s)(cat)" => The cat sat on <a href="#learn-regex"><strong>cat</strong></a>.
</pre>
## 5. 标记
标记也称为修饰符,因为它会修改正则表达式的输出。这些标志可以以任意顺序或组合使用,并且是正则表达式的一部分。
|标记|描述|
|:----:|----|
|i|不区分大小写: 将匹配设置为不区分大小写。|
|g|全局搜索: 搜索整个输入字符串中的所有匹配。|
|m|多行匹配: 会匹配输入字符串每一行。|
### 5.1 不区分大小写
`i` 修饰符用于执行不区分大小写匹配。例如正则表达式 `/The/gi`,表示: 大写字母 `T`,后跟小写字母 `h`,后跟字母 `e`
但是在正则匹配结束时 `i` 标记会告诉正则表达式引擎忽略这种情况。正如你所看到的,我们还使用了 `g` 标记,因为我们要在整个输入字符串中搜索匹配。
<pre>
"The" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on the mat.
</pre>
<pre>
"/The/gi" => <a href="#learn-regex"><strong>The</strong></a> fat cat sat on <a href="#learn-regex"><strong>the</strong></a> mat.
</pre>
### 5.2 全局搜索
`g` 修饰符用于执行全局匹配 (会查找所有匹配,不会在查找到第一个匹配时就停止)。
例如正则表达式 `/.(at)/g`,表示: 除换行符之外的任意字符,后跟小写字母 `a`,后跟小写字母 `t`
因为我们在正则表达式的末尾使用了 `g` 标记,它会从整个输入字符串中找到每个匹配项。
<pre>
".(at)" => The <a href="#learn-regex"><strong>fat</strong></a> cat sat on the mat.
</pre>
<pre>
"/.(at)/g" => The <a href="#learn-regex"><strong>fat</strong></a> <a href="#learn-regex"><strong>cat</strong></a> <a href="#learn-regex"><strong>sat</strong></a> on the <a href="#learn-regex"><strong>mat</strong></a>.
</pre>
### 5.3 多行匹配
`m` 修饰符被用来执行多行的匹配。正如我们前面讨论过的 `(^, $)`,使用定位符来检查匹配字符是输入字符串开始或者结束。但是我们希望每一行都使用定位符,所以我们就使用 `m` 修饰符。
例如正则表达式 `/at(.)?$/gm`,表示: 小写字母 `a`,后跟小写字母 `t`,匹配除了换行符以外任意字符零次或一次。而且因为 `m` 标记,现在正则表达式引擎匹配字符串中每一行的末尾。
<pre>
"/.at(.)?$/" => The fat
cat sat
on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
<pre>
"/.at(.)?$/gm" => The <a href="#learn-regex"><strong>fat</strong></a>
cat <a href="#learn-regex"><strong>sat</strong></a>
on the <a href="#learn-regex"><strong>mat.</strong></a>
</pre>
## 常用正则表达式
* **正整数**: `^\d+$`
* **负整数**: `^-\d+$`
* **电话号码**: `^+?[\d\s]{3,}$`
* **电话代码**: `^+?[\d\s]+(?[\d\s]{10,}$`
* **整数**: `^-?\d+$`
* **用户名**: `^[\w\d_.]{4,16}$`
* **字母数字字符**: `^[a-zA-Z0-9]*$`
* **带空格的字母数字字符**: `^[a-zA-Z0-9 ]*$`
* **密码**: `^(?=^.{6,}$)((?=.*[A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z]))^.*$`
* **电子邮件**: `^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})*$`
* **IPv4 地址**: `^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))*$`
* **小写字母**: `^([a-z])*$`
* **大写字母**: `^([A-Z])*$`
* **网址**: `^(((http|https|ftp):\/\/)?([[a-zA-Z0-9]\-\.])+(\.)([[a-zA-Z0-9]]){2,4}([[a-zA-Z0-9]\/+=%&_\.~?\-]*))*$`
* **VISA 信用卡号码**: `^(4[0-9]{12}(?:[0-9]{3})?)*$`
* **日期 (MM/DD/YYYY)**: `^(0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)?[0-9]{2}$`
* **日期 (YYYY/MM/DD)**: `^(19|20)?[0-9]{2}[- /.](0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])$`
* **万事达信用卡号码**: `^(5[1-5][0-9]{14})*$`

@ -3,9 +3,12 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/11/03**
**2020/11/06**
* 详情页菜单添加拷贝URL
* 解决一些书名太长缓存报错的bug
* 添加备份搜索记录
* 替换编辑界面添加正则学习教程
* 自动分段优化 by [tumuyan](https://github.com/tumuyan)
**2020/11/01**
* 导入本地添加智能扫描,菜单-智能扫描,扫描当前文件夹包括子文件夹下所有文件

@ -8,6 +8,9 @@ import io.legado.app.data.entities.SearchKeyword
@Dao
interface SearchKeywordDao {
@get:Query("SELECT * FROM search_keywords")
val all: List<SearchKeyword>
@Query("SELECT * FROM search_keywords ORDER BY usage DESC")
fun liveDataByUsage(): LiveData<List<SearchKeyword>>

@ -1,6 +1,7 @@
package io.legado.app.help
import java.util.*
import java.util.regex.Pattern
import kotlin.math.max
import kotlin.math.min
@ -30,6 +31,7 @@ object ContentHelp {
} else {
content1
}
val dict = makeDict(content2)
var p = content2
.replace("&quot;".toRegex(), "")
.replace("[::]['\"‘”“]+".toRegex(), ":“")
@ -68,9 +70,7 @@ object ContentHelp {
buffer = StringBuffer((content1.length * 1.15).toInt())
for (s in p) {
buffer.append("\n")
buffer.append(
findNewLines(s)
)
buffer.append(findNewLines(s, dict))
}
buffer = reduceLength(buffer)
content1 = ("$chapterName\n\n" + buffer.toString() // 处理章节头部空格和换行
@ -178,18 +178,18 @@ object ContentHelp {
}
// 对内容重新划分段落.输入参数str已经使用换行符预分割
private fun findNewLines(str: String): String {
private fun findNewLines(str: String, dict: List<String>): String {
val string = StringBuffer(str)
// 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。
val arrayQuote: MutableList<Int> = ArrayList()
// 标记插入换行符的位置,int为插入位置(str的char下标)
var insN = ArrayList<Int>()
// mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。
// 长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间
// 0未知,正数引号内,负数引号外。
// 如果相邻的两个标记都为+1,那么需要增加1个引号。
// 引号内不进行断句
//mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。
//长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间
//0未知,正数引号内,负数引号外。
//如果相邻的两个标记都为+1,那么需要增加1个引号。
//引号内不进行断句
val mod = IntArray(str.length)
var waitClose = false
for (i in str.indices) {
@ -201,7 +201,16 @@ object ContentHelp {
if (size > 0) {
val quotePre = arrayQuote[size - 1]
if (i - quotePre == 2) {
if (match(",、,/", str[i - 1])) {
var remove = false
if (waitClose) {
if (match(",,、/", str[i - 1])) {
// 考虑出现“和”这种特殊情况
remove = true
}
} else if (match(",,、/和与或", str[i - 1])) {
remove = true
}
if (remove) {
string.setCharAt(i, '“')
string.setCharAt(i - 2, '”')
arrayQuote.removeAt(size - 1)
@ -232,8 +241,10 @@ object ContentHelp {
}
}
}
// if(char_b2=='.' || char_b2=='。')
if (match(MARK_SENTENCES_END_P, charB2)) insN.add(p - 1) else {
//if(char_b2=='.' || char_b2=='。')
if (match(MARK_SENTENCES_END_P, charB2)) {
insN.add(p - 1)
} else if (!match("", charB2)) {
val lastEnd = seekLast(str, MARK_SENTENCES_END, i, lastQuote)
if (lastEnd > 0) insN.add(lastEnd) else insN.add(lastQuote)
}
@ -258,16 +269,15 @@ object ContentHelp {
val size = arrayQuote.size
// 标记循环状态,此位置前的引号是否已经配对
//标记循环状态,此位置前的引号是否已经配对
var opend = false
if (size > 0) {
// 第1次遍历array_quote,令其元素的值不为0
//第1次遍历array_quote,令其元素的值不为0
for (i in 0 until size) {
if (mod[i] > 0) {
opend = true
} else if (mod[i] < 0) {
// 连续2个反引号表明存在冲突,强制把前一个设为正引号
//连续2个反引号表明存在冲突,强制把前一个设为正引号
if (!opend) {
if (i > 0) mod[i] = 3
}
@ -280,7 +290,7 @@ object ContentHelp {
// 修正,断尾必须封闭引号
if (opend) {
if (arrayQuote[size - 1] - string.length > -3) {
// if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){
//if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){
if (size > 1) mod[size - 2] = 4
// 0<=i<size,故无需判断size>=1
mod[size - 1] = -4
@ -318,9 +328,39 @@ object ContentHelp {
}
}
// 第3次循环,匹配并插入换行。
// "xxxx" xxxx。\n xxx“xxxx”
// 未实现
//第3次循环,匹配并插入换行。
//"xxxx" xxxx。\n xxx“xxxx”
//未实现
// 使用字典验证ins_n , 避免插入不必要的换行。
// 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题
val insN1 = ArrayList<Int>()
for (i in insN) {
if (match("\"'”“", string[i])) {
val start: Int = seekLast(
str,
"\"'”“",
i - 1,
i - WORD_MAX_LENGTH
)
if (start > 0) {
val word = str.substring(start + 1, i)
if (dict.contains(word)) {
//System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word);
//引号内如果是字典词条,后方不插入换行符(前方不需要优化)
continue
} else {
//System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word);
if (match("的地得", str[start])) {
//xx的“xx”,后方不插入换行符(前方不需要优化)
continue
}
}
}
}
insN1.add(i)
}
insN = insN1
// 随机在句末插入换行符
insN = ArrayList(HashSet(insN))
@ -471,6 +511,34 @@ object ContentHelp {
return buffer.toString()
}
/**
* 从字符串提取引号包围,且不止出现一次的内容为字典
*
* @param str
* @return 词条列表
*/
private fun makeDict(str: String): List<String> {
// 引号中间不包含任何标点
val patten = Pattern.compile(
"""
(?<=["'”“])([^
\p{P}]{1,${WORD_MAX_LENGTH}})(?=["'”“])
""".trimIndent()
)
//Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\"'”“]{1,16})(?=[\"'”“])");
val matcher = patten.matcher(str)
val cache: MutableList<String> = ArrayList()
val dict: MutableList<String> = ArrayList()
while (matcher.find()) {
val word = matcher.group()
if (cache.contains(word)) {
if (!dict.contains(word)) dict.add(word)
} else cache.add(word)
}
return dict
}
/**
* 计算匹配到字典的每个字符的位置
*
@ -510,14 +578,14 @@ object ContentHelp {
*
* @param str 数据字符串
* @param key 字典字符串
* @param from 从哪个字符开始匹配默认0
* @param to 匹配到哪个字符不包含此字符默认匹配到最末位
* @param from 从哪个字符开始匹配默认最末位
* @param to 匹配到哪个字符不包含此字符默认0
* @return 位置正向计算)
*/
private fun seekLast(str: String, key: String, from: Int, to: Int): Int {
if (str.length - from < 1) return -1
var i = 0
if (from > i) i = from
var i = str.lastIndex
if (from < i && i > 0) i = from
var t = 0
if (to > 0) t = to
var c: Char
@ -619,6 +687,9 @@ object ContentHelp {
private const val MARK_QUOTATION = "\"“”"
private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex()
// 限制字典的长度
private const val WORD_MAX_LENGTH = 16
private fun match(rule: String, chr: Char): Boolean {
return rule.indexOf(chr) != -1
}

@ -33,6 +33,7 @@ object Backup {
"rssStar.json",
"replaceRule.json",
"readRecord.json",
"searchHistory.json",
DefaultData.txtTocRuleFileName,
DefaultData.httpTtsFileName,
ReadBookConfig.configFileName,
@ -64,6 +65,7 @@ object Backup {
writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath)
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath)
writeListToJson(App.db.searchKeywordDao().all, "searchHistory.json", backupPath)
writeListToJson(App.db.txtTocRule().all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(App.db.httpTTSDao().all, DefaultData.httpTtsFileName, backupPath)
GSON.toJson(ReadBookConfig.configList).let {

@ -129,6 +129,9 @@ object Restore {
fileToListT<ReplaceRule>(path, "replaceRule.json")?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray())
}
fileToListT<SearchKeyword>(path, "searchHistory.json")?.let {
App.db.searchKeywordDao().insert(*it.toTypedArray())
}
fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
}

@ -10,6 +10,7 @@ import io.legado.app.help.JsExtensions
import io.legado.app.utils.*
import org.jsoup.nodes.Entities
import org.mozilla.javascript.NativeObject
import java.net.URL
import java.util.*
import java.util.regex.Pattern
import javax.script.SimpleBindings
@ -25,6 +26,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
var chapter: BookChapter? = null
private var content: Any? = null
private var baseUrl: String? = null
private var baseURL: URL? = null
private var isJSON: Boolean = false
private var isRegex: Boolean = false
@ -37,18 +39,28 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
private var objectChangedJP = false
@Throws(Exception::class)
@JvmOverloads
fun setContent(content: Any?, baseUrl: String? = this.baseUrl): AnalyzeRule {
fun setContent(content: Any?): AnalyzeRule {
if (content == null) throw AssertionError("Content cannot be null")
isJSON = content.toString().isJson()
this.content = content
this.baseUrl = baseUrl
objectChangedXP = true
objectChangedJS = true
objectChangedJP = true
return this
}
fun setBaseUrl(baseUrl: String?): AnalyzeRule {
this.baseUrl = baseUrl
baseUrl?.let {
try {
baseURL = URL(baseUrl.substringBefore(","))
} catch (e: Exception) {
e.printStackTrace()
}
}
return this
}
/**
* 获取XPath解析类
*/
@ -157,7 +169,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
val urlList = ArrayList<String>()
if (result is List<*>) {
for (url in result as List<*>) {
val absoluteURL = NetworkUtils.getAbsoluteURL(baseUrl, url.toString())
val absoluteURL = NetworkUtils.getAbsoluteURL(baseURL, url.toString())
if (!absoluteURL.isNullOrEmpty() && !urlList.contains(absoluteURL)) {
urlList.add(absoluteURL)
}
@ -220,7 +232,11 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
result.toString()
}
if (isUrl) {
return NetworkUtils.getAbsoluteURL(baseUrl, str) ?: ""
return if (str.isBlank()) {
baseUrl ?: ""
} else {
NetworkUtils.getAbsoluteURL(baseURL, str) ?: ""
}
}
return str
}

@ -45,10 +45,8 @@ object Rss {
).getResponseAwait(rssArticle.origin)
.body
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(
body,
NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)
)
analyzeRule.setContent(body)
.setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link))
analyzeRule.getString(ruleContent)
}
}

@ -38,7 +38,7 @@ object RssParserByRule {
} else {
val articleList = mutableListOf<RssArticle>()
val analyzeRule = AnalyzeRule()
analyzeRule.setContent(body, sortUrl)
analyzeRule.setContent(body).setBaseUrl(sortUrl)
var reverse = false
if (ruleArticles.startsWith("-")) {
reverse = true

@ -210,7 +210,7 @@ object BookChapterList {
log: Boolean = false
): ChapterData<List<String>> {
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
val chapterList = arrayListOf<BookChapter>()
val nextUrlList = arrayListOf<String>()
val nextTocRule = tocRule.nextTocUrl

@ -97,7 +97,7 @@ object BookContent {
val replaceRegex = bookSource.ruleContent?.replaceRegex
if (!replaceRegex.isNullOrEmpty()) {
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(contentStr, baseUrl)
analyzeRule.setContent(contentStr).setBaseUrl(baseUrl)
analyzeRule.chapter = bookChapter
contentStr = analyzeRule.getString(replaceRegex)
}
@ -119,7 +119,7 @@ object BookContent {
printLog: Boolean = true
): ContentData<List<String>> {
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
val nextUrlList = arrayListOf<String>()
analyzeRule.chapter = chapter
val nextUrlRule = contentRule.nextContentUrl

@ -27,7 +27,7 @@ object BookInfo {
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val infoRule = bookSource.getBookInfoRule()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
infoRule.init?.let {
if (it.isNotEmpty()) {
Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则")

@ -38,7 +38,7 @@ object BookList {
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}")
if (!scope.isActive) throw CancellationException()
val analyzeRule = AnalyzeRule(variableBook)
analyzeRule.setContent(body, baseUrl)
analyzeRule.setContent(body).setBaseUrl(baseUrl)
bookSource.bookUrlPattern?.let {
if (baseUrl.matches(it.toRegex())) {
Debug.log(bookSource.bookSourceUrl, "≡链接为详情页")

@ -16,10 +16,12 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.getViewModel
import io.legado.app.utils.postEvent
import kotlinx.android.synthetic.main.activity_replace_edit.*
import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.toast
import kotlin.math.abs
@ -58,10 +60,13 @@ class ReplaceEditActivity :
override fun onActivityCreated(savedInstanceState: Bundle?) {
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this)
viewModel.replaceRuleData.observe(this, {
viewModel.initData(intent) {
upReplaceView(it)
})
viewModel.initData(intent)
}
iv_help.onClick {
val mdText = String(assets.open("help/regex.md").readBytes())
TextDialog.show(supportFragmentManager, mdText, TextDialog.MD)
}
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@ -96,7 +101,7 @@ class ReplaceEditActivity :
}
private fun getReplaceRule(): ReplaceRule {
val replaceRule: ReplaceRule = viewModel.replaceRuleData.value ?: ReplaceRule()
val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule()
replaceRule.name = et_name.text.toString()
replaceRule.group = et_group.text.toString()
replaceRule.pattern = et_replace_rule.text.toString()

@ -2,35 +2,33 @@ package io.legado.app.ui.replace.edit
import android.app.Application
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.ReplaceRule
class ReplaceEditViewModel(application: Application) : BaseViewModel(application) {
val replaceRuleData = MutableLiveData<ReplaceRule>()
var replaceRule: ReplaceRule? = null
fun initData(intent: Intent) {
fun initData(intent: Intent, finally: (replaceRule: ReplaceRule) -> Unit) {
execute {
replaceRuleData.value ?: let {
val id = intent.getLongExtra("id", -1)
if (id > 0) {
App.db.replaceRuleDao().findById(id)?.let {
replaceRuleData.postValue(it)
}
replaceRule = App.db.replaceRuleDao().findById(id)
} else {
val pattern = intent.getStringExtra("pattern") ?: ""
val isRegex = intent.getBooleanExtra("isRegex", false)
val scope = intent.getStringExtra("scope")
val rule = ReplaceRule(
replaceRule = ReplaceRule(
name = pattern,
pattern = pattern,
isRegex = isRegex,
scope = scope
)
replaceRuleData.postValue(rule)
}
}.onFinally {
replaceRule?.let {
finally(it)
}
}
}

@ -48,6 +48,8 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
viewModel.initData(intent)
}
@Suppress("DEPRECATION")
@SuppressLint("SwitchIntDef")
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.orientation) {

@ -90,6 +90,22 @@ object NetworkUtils {
return relativeUrl
}
/**
* 获取绝对地址
*/
fun getAbsoluteURL(baseURL: URL?, relativePath: String): String? {
if (baseURL == null) return relativePath
var relativeUrl = relativePath
try {
val parseUrl = URL(baseURL, relativePath)
relativeUrl = parseUrl.toString()
return relativeUrl
} catch (e: Exception) {
e.printStackTrace()
}
return relativeUrl
}
fun getBaseUrl(url: String?): String? {
if (url == null || !url.startsWith("http")) return null
val index = url.indexOf("/", 9)
@ -99,11 +115,10 @@ object NetworkUtils {
}
fun getSubDomain(url: String?): String {
var baseUrl = getBaseUrl(url)
if (baseUrl == null) return ""
val baseUrl = getBaseUrl(url) ?: return ""
return if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) {
baseUrl.substring(baseUrl.lastIndexOf("/")+1)
} else baseUrl.substring(baseUrl.indexOf(".")+1)
baseUrl.substring(baseUrl.lastIndexOf("/") + 1)
} else baseUrl.substring(baseUrl.indexOf(".") + 1)
}
/**

@ -62,12 +62,29 @@
android:layout_height="wrap_content" />
</io.legado.app.ui.widget.text.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<io.legado.app.lib.theme.view.ATECheckBox
android:id="@+id/cb_use_regex"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/use_regex" />
<ImageView
android:id="@+id/iv_help"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_help"
app:tint="@color/secondaryText"
android:contentDescription="@string/help" />
</LinearLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:id="@+id/til_replace_to"
android:layout_width="match_parent"

Loading…
Cancel
Save