|
|
@@ -1,10 +1,17 @@
|
|
|
package space.anyi.cleaner;
|
|
|
|
|
|
+import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
|
|
|
+import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
|
|
import org.jsoup.Jsoup;
|
|
|
import org.jsoup.nodes.Document;
|
|
|
import org.jsoup.nodes.Element;
|
|
|
import org.jsoup.select.Elements;
|
|
|
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
|
@@ -13,14 +20,23 @@ import java.util.regex.Matcher;
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
/**
|
|
|
- * 详情页清洗与记者/编辑解析工具(静态方法)。
|
|
|
+ * 详情页清洗、PDF 生成与记者/编辑解析工具(静态方法)。
|
|
|
*
|
|
|
* <p>清洗:严格按需求移除 meta/script、id 为 hidden-box 的元素,并在 div.container
|
|
|
* 的直接子 div 中仅保留 .article-title、.not-exist-media-leader、.article-content。
|
|
|
+ * 生成:清洗后 HTML 经 openhtmltopdf 渲染为 PDF,思源宋体挂到页面 CSS 实际使用的
|
|
|
+ * 字体族名下保证中文有字形可渲染。
|
|
|
* 解析:从清洗后正文纯文本按子句正则提取文字/图片记者、通讯员与编辑。</p>
|
|
|
*/
|
|
|
public class DetailCleaner {
|
|
|
|
|
|
+ /** 思源宋体(常规/粗体)文件路径,用于 PDF 中文字形渲染 */
|
|
|
+ private static final String SERIF_REGULAR = "/usr/share/fonts/yangyi/SourceHanSerifCN-Regular.ttf";
|
|
|
+ private static final String SERIF_BOLD = "/usr/share/fonts/yangyi/SourceHanSerifCN-Bold.ttf";
|
|
|
+ /** 思源宋体挂载的字体族名(覆盖页面 CSS 可能用到的中文族) */
|
|
|
+ private static final String[] SERIF_FAMILIES = {"CJK", "SimHei", "Microsoft YaHei", "微软雅黑",
|
|
|
+ "FZBiaoYaSong-GBK_YS", "sans-serif", "serif", "Arial"};
|
|
|
+
|
|
|
/** 名称段捕获的终止标记:后续子句前缀或行尾 */
|
|
|
private static final String STOP = "(?=文、|文/|图、|图/|视频/|通讯员[::]?|(?:广州日报)?新花城编辑[::]|编辑[::]|实习生[::]|$)";
|
|
|
/** 记者子句:文、图/…记者:X 同时计入图文,文/ 只计文字,图/ 只计图片 */
|
|
|
@@ -72,6 +88,55 @@ public class DetailCleaner {
|
|
|
return result.html();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将详情页 HTML 渲染为 PDF。
|
|
|
+ *
|
|
|
+ * <p>内部做防御性清洗(移除 video/script、head 仅留样式表、body 仅留
|
|
|
+ * 文章三个区块),并在 html 上追加 font-family 兜底。页面 CSS 未给 body 声明
|
|
|
+ * font-family,PDF 引擎需显式继承;思源宋体注册到页面 CSS 可能用到的字体族名下,
|
|
|
+ * 保证中文有字形可渲染。</p>
|
|
|
+ *
|
|
|
+ * @param html 详情页 HTML(通常为 {@link #clean(String)} 的产物)
|
|
|
+ * @param outFile PDF 输出路径(父目录不存在时自动创建)
|
|
|
+ * @throws IOException 写入 PDF 失败
|
|
|
+ */
|
|
|
+ public static void html2pdf(String html, Path outFile) throws IOException {
|
|
|
+ Document doc = Jsoup.parse(html);
|
|
|
+ doc.select("video, script").remove();
|
|
|
+ doc.head().children().not("link[rel='stylesheet']").remove();
|
|
|
+ doc.body().children().not("div.article-title, div.not-exist-media-leader, div.article-content").remove();
|
|
|
+
|
|
|
+ Element htmlEl = doc.selectFirst("html");
|
|
|
+ htmlEl.attr("style", htmlEl.attr("style") + ";font-family:'Microsoft YaHei',sans-serif");
|
|
|
+
|
|
|
+ if (outFile.toAbsolutePath().getParent() != null) {
|
|
|
+ Files.createDirectories(outFile.toAbsolutePath().getParent());
|
|
|
+ }
|
|
|
+ try (OutputStream os = Files.newOutputStream(outFile)) {
|
|
|
+ PdfRendererBuilder builder = new PdfRendererBuilder();
|
|
|
+ File regular = new File(SERIF_REGULAR);
|
|
|
+ File bold = new File(SERIF_BOLD);
|
|
|
+ for (String family : SERIF_FAMILIES) {
|
|
|
+ builder.useFont(regular, family, 400, BaseRendererBuilder.FontStyle.NORMAL, true);
|
|
|
+ builder.useFont(bold, family, 700, BaseRendererBuilder.FontStyle.NORMAL, true);
|
|
|
+ }
|
|
|
+ doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
|
|
|
+ // jsoup XML 语法会把 原样输出,而 openhtmltopdf 的 XML 解析器不认该实体,
|
|
|
+ // 需还原为 Unicode 不间断空格
|
|
|
+ builder.withHtmlContent(doc.html().replace(" ", "\u00A0"), "https://www.gz-cmc.com/");
|
|
|
+ builder.toStream(os);
|
|
|
+ builder.run();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 标题清洗,标题中可能包含html标签,需要去除
|
|
|
+ * @param title
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public static String titleClean(String title){
|
|
|
+ return Jsoup.parse(title).text();
|
|
|
+ }
|
|
|
/**
|
|
|
* 取清洗后正文(.article-content)的纯文本。
|
|
|
*
|