`
珊瑚成长日记
  • 浏览: 20709 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

PDF数据解析

    博客分类:
  • java
 
阅读更多

 系统中有一个读取PDF表单的数据,然后将数据按字段解析出来,存储到数据库的功能。实现思路,大致是先获取PDF的流,把数据导入到xml中,然后逐行读取xml的数据。具体的实现代码有点挫...

package com.rb.common.pdf;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;

import com.itextpdf.text.DocumentException;
import com.rb.owk.commons.lang.base.orm.BusinessException;

/**
 * 解析PDF报表T-Q-1,提取报表数据
 * 
 * @author HO274509
 * 
 */
public class PdfTQ1 {
	private PdfTQ1() {
	}

	private final static Log log = LogFactory.getLog(PdfTQ1.class);
	// 要解析的PDF
	public static final String RESOURCE = "/pdf/T-Q-1.pdf";// classpath相对路径
	// 要填充PDF的XML数据来源
	public static final String XMLDATA = "T-Q-1.xml";
	// 填充之前的PDF
	public static final String SOURCE = "T-Q-1.pdf";
	// 填充之后的PDF
	public static final String RESULT = "T-Q-1_fill.pdf";

	/**
	 * 打印pdf数据
	 * 
	 * @throws TransformerException
	 * @throws TransformerFactoryConfigurationError
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 * @throws IOException
	 */
	public static void printPdfData() throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		/**
		 * 获取xml格式数据
		 */
		InputStream in = PdfTQ4.class.getResourceAsStream(RESOURCE);
//		 另一种获取方法  PdfUtil.getXFAData(RESOURCE, file);
		ByteArrayOutputStream os = PdfUtil.getXFAData(in);
		ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
		/**
		 * 使用dom4j解析xml,获取数据
		 */

		SAXReader xmlReader = new SAXReader();
		org.dom4j.Document document = null;
		try {
			// document = xmlReader.read(xml);
			document = xmlReader.read(is);
		} catch (org.dom4j.DocumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			log.error("log4j解析出错");
		}
		// 根节点
		Element root = document.getRootElement();
		// 表单根节点
		Element element = root.element("T_Q_1");
		// 填报人相关信息
		log.info("-----填报人信息-----");
		log.info(String.format("填报部门:%s", element.element("FILLIN_DEPT")
				.getText()));
		log.info(String.format("填报人:%s", element.element("FILLIN_PERSON")
				.getText()));
		log.info(String.format("联系电话:%s", element.element("TELEPHONE")
				.getText()));
		log.info(String.format("责任人:%s", element.element("RES_PERSON")
				.getText()));

		log.info("-----重要信息系统的停止服务情况-----");
		// 表单TQ1001数据根节点:停止服务性质为非预期停止服务的系统停止服务情况
		Iterator<Element> it_TQ1001 = element.elementIterator("TQ1001");
		int index = 1;
		while (it_TQ1001.hasNext()) {// 打印非预期停止服务的系统停止服务情况
			Element TQ1001 = it_TQ1001.next();
			log.info("停止服务性质:非预期停止服务");
			log.info(String.format("序号:%s", index));
			log
					.info(String.format("信息系统:%s", TQ1001.element("COL1")
							.getText()));
			if (StringUtils.isNotEmpty(TQ1001.elementText("COL2"))) {
				log.info(String.format("备注:%s", TQ1001.elementText("COL2")));
			}
			log.info(String.format("停止服务原因:%s", TQ1001.elementText("COL3")));
			if (StringUtils.isNotEmpty(TQ1001.elementText("COL4"))) {
				log.info(String.format("备注:%s", TQ1001.elementText("COL4")));
			}
			log.info(String.format("事件等级:%s", TQ1001.elementText("COL5")));
			log.info(String.format("起始时间:%s", TQ1001.elementText("COL6")));
			log.info(String.format("结束时间:%s", TQ1001.elementText("COL7")));
			log.info(String.format("影响范围:%s", TQ1001.elementText("COL8")));
			if (StringUtils.isNotEmpty(TQ1001.elementText("COL9"))) {
				log.info(String.format("个数:%s", TQ1001.elementText("COL9")));
			}
			log.info(String.format("描述:%s", TQ1001.elementText("COL10")));
			index++;
		}
		// 表单TQ1003数据根节点:停止服务性质为预期停止服务的系统停止服务情况
		Iterator<Element> it_TQ1003 = element.elementIterator("TQ1003");
		index = 1;
		while (it_TQ1003.hasNext()) {// 打印停止服务的系统停止服务情况
			Element TQ1002 = it_TQ1003.next();
			log.info("停止服务性质:预期停止服务");
			log.info(String.format("序号:%s", index));
			log
					.info(String.format("信息系统:%s", TQ1002.element("COL1")
							.getText()));
			if (StringUtils.isNotEmpty(TQ1002.elementText("COL7"))) {
				log.info(String.format("备注:%s", TQ1002.elementText("COL7")));
			}
			log.info(String.format("停止服务原因:%s", TQ1002.elementText("COL2")));
			if (StringUtils.isNotEmpty(TQ1002.elementText("COL8"))) {
				log.info(String.format("备注:%s", TQ1002.elementText("COL8")));
			}
			log.info(String.format("起始时间:%s", TQ1002.elementText("COL3")));
			log.info(String.format("结束时间:%s", TQ1002.elementText("COL4")));
			log.info(String.format("影响范围:%s", TQ1002.elementText("COL10")));
			if (StringUtils.isNotEmpty(TQ1002.elementText("COL9"))) {
				log.info(String.format("个数:%s", TQ1002.elementText("COL9")));
			}
			log.info(String.format("描述:%s", TQ1002.elementText("COL6")));
			index++;
		}
		// 表单TQ1006数据根节点:核心业务系统重要性能指标
		Element TQ1006 = element.element("TQ1006");
		log.info("-----核心系统重要性能指标-----");
		log.info(String.format("系统可用率-->数量:%s,备注:%s", TQ1006.element("COL1")
				.getText(), TQ1006.element("COL19").getText()));
		log.info(String.format("批处理的平均批处理用时-->数量:%s,备注:%s", TQ1006.element(
				"COL2").getText(), TQ1006.element("COL20").getText()));
		log.info(String.format("CPU平均使用率-->数量:%s,备注:%s", TQ1006.element("COL3")
				.getText(), TQ1006.element("COL21").getText()));
		log.info(String.format("CPU高峰使用率-->数量:%s,备注:%s", TQ1006.element("COL4")
				.getText(), TQ1006.element("COL22").getText()));
		log.info(String.format("内存平均使用率-->数量:%s,备注:%s", TQ1006.element("COL5")
				.getText(), TQ1006.element("COL23").getText()));
		log.info(String.format("磁盘空间占有率峰值-->数量:%s,备注:%s", TQ1006
				.element("COL8").getText(), TQ1006.element("COL26").getText()));
		log.info(String.format("处理能力---"));
		log.info(String.format("日均交易笔数-->数量:%s,备注:%s", TQ1006.element("COL9")
				.getText(), TQ1006.element("COL27").getText()));
		log.info(String.format("日交易笔数峰值-->数量:%s,备注:%s", TQ1006.element("COL10")
				.getText(), TQ1006.element("COL28").getText()));
		log.info(String.format("系统可承载的最大交易并发数-->数量:%s,备注:%s", TQ1006.element(
				"COL11").getText(), TQ1006.element("COL29").getText()));
		log.info(String.format("交易成功率-->数量:%s,备注:%s", TQ1006.element("COL12")
				.getText(), TQ1006.element("COL30").getText()));
		log.info(String.format("账户数及变动---"));
		log.info(String.format("公司账户及增减---"));
		log
				.info(String.format("公司账户:%s,同比:%s,环比:%s,备注:%s", TQ1006
						.element("COL13").getText(), TQ1006.element("COL14")
						.getText(), TQ1006.element("COL15").getText(), TQ1006
						.element("COL31").getText()));
		log.info(String.format("个人账户及增减---"));
		log
				.info(String.format("个人账户:%s,同比:%s,环比:%s,备注:%s", TQ1006
						.element("COL16").getText(), TQ1006.element("COL17")
						.getText(), TQ1006.element("COL18").getText(), TQ1006
						.element("COL32").getText()));
		// 表单TQ1007数据根节点:
		Element TQ1007 = element.element("TQ1007");
		log.info("-----核心网络系统运行情况-----");
		log.info(String.format("业务时段平均带宽占用情况(生产中心或中心机房到一级分支机构):%s,备注:%s",
				TQ1007.elementText("COL1"), TQ1007.elementText("COL15")));
		log.info(String.format("业务时段平均带宽占用情况(互联网出口):%s,备注:%s", TQ1007
				.elementText("COL2"), TQ1007.elementText("COL16")));
		log.info("------网上银行系统运行情况-----");
		log.info(String.format("日均交易笔数 数量:%s,备注:%s",
				TQ1007.elementText("COL3"), TQ1007.elementText("COL17")));
		log.info(String.format("日交易笔数峰 数量:%s,备注:%s",
				TQ1007.elementText("COL4"), TQ1007.elementText("COL18")));
		log.info(String.format("系统可承载的最大交易并发 数量:%s,备注:%s", TQ1007
				.elementText("COL5"), TQ1007.elementText("COL19")));
		log.info(String.format("平均在线并发用户数 数量:%s,备注:%s", TQ1007
				.elementText("COL6"), TQ1007.elementText("COL20")));
		log.info(String.format("最大在线并发用户数 数量:%s,备注:%s", TQ1007
				.elementText("COL7"), TQ1007.elementText("COL21")));
		log.info(String.format("系统可承载的最大在线并发数 数量:%s,备注:%s", TQ1007
				.elementText("COL8"), TQ1007.elementText("COL22")));
		log.info("-----银行卡系统运行情况-----");
		log.info(String.format("日均交易笔数 数量:%s,备注:%s",
				TQ1007.elementText("COL9"), TQ1007.elementText("COL23")));
		log.info(String.format("日交易笔数峰数 数量:%s,备注:%s", TQ1007
				.elementText("COL10"), TQ1007.elementText("COL24")));
		log.info(String.format("系统可承载的最大交易并发数 数量:%s,备注:%s", TQ1007
				.elementText("COL11"), TQ1007.elementText("COL25")));
		log.info("-----第三方存管系统运行情况-----");
		log.info(String.format("日均交易笔数 数量:%s,备注:%s", TQ1007
				.elementText("COL12"), TQ1007.elementText("COL26")));
		log.info(String.format("日交易笔数峰数 数量:%s,备注:%s", TQ1007
				.elementText("COL13"), TQ1007.elementText("COL27")));
		log.info(String.format("系统可承载的最大交易并发数 数量:%s,备注:%s", TQ1007
				.elementText("COL14"), TQ1007.elementText("COL28")));
		log.info("-----数据中心(中心机房)外部异常情况-----");
		log.info(String.format("数据中心市电中断次数 数量:%s,备注:%s", TQ1007
				.elementText("COL29"), TQ1007.elementText("COL30")));
		log.info(String.format("数据中心由于外部原因导致网络通讯中断次数 数量:%s,备注:%s", TQ1007
				.elementText("COL31"), TQ1007.elementText("COL32")));
	}

	public static Map<String, Object> getTq1Data() throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		Map<String, Object> tq1Map = new HashMap<String, Object>();
		/**
		 * 获取xml格式数据
		 */
		InputStream in = PdfTQ4.class.getResourceAsStream(RESOURCE);
		ByteArrayOutputStream os = PdfUtil.getXFAData(in);
		ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
		/**
		 * 使用dom4j解析xml,获取数据
		 */

		SAXReader xmlReader = new SAXReader();
		org.dom4j.Document document = null;
		try {
			// document = xmlReader.read(xml);
			document = xmlReader.read(is);
		} catch (org.dom4j.DocumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			log.error("log4j解析出错");
		}
		// PDF报表在adobe life cycle表单设计里的结构是:pdf报表标识符-》多个表单-》表单内嵌套有数据节点或者子表单
		// PDF报表的XML数据结构(基本一致,就这四层):ROOT-》报表标识节点-》各个表单根节点=》表单内部数据节点
		// 根节点
		Element root = document.getRootElement();
		// 报表根节点
		Element element = root.element("T_Q_1");
		// 填报人相关信息
		tq1Map.put("ISEMPTY", element.elementText("isempty"));
		tq1Map.put("FILLINDEPT", element.elementText("FILLIN_DEPT"));
		tq1Map.put("FILLINPERSON", element.elementText("FILLIN_PERSON"));
		tq1Map.put("TELEPHONE", element.elementText("TELEPHONE"));
		tq1Map.put("RESPERSON", element.elementText("RES_PERSON"));
		List<Map<String, Object>> tq1001List = new ArrayList<Map<String,Object>>();
		Iterator<Element> it_TQ1001 = element.elementIterator("TQ1001");
		int i=1;
		while (it_TQ1001.hasNext()) {
			Element tq1001 = it_TQ1001.next();
			Iterator<Element> it = tq1001.elementIterator();
			Map<String , Object> tq1001Map = new HashMap<String, Object>();
			tq1001Map.put("INDEX", i++);
			while (it.hasNext()) {
				Element tmp = it.next();
				log.info(tmp.getName() + ":::::::" + tmp.getTextTrim());
				tq1001Map.put(tmp.getName(), tmp.getTextTrim());
			}
			if(StringUtils.isEmpty((String)tq1001Map.get("COL9"))){
				tq1001Map.put("COL9", null);
			}
			tq1001List.add(tq1001Map);
		}
		tq1Map.put("tq1001List", tq1001List);
		List<Map<String, Object>> tq1003List = new ArrayList<Map<String,Object>>();
		Iterator<Element> it_TQ1003 = element.elementIterator("TQ1003");
		i=1;
		while (it_TQ1003.hasNext()) {
			Element tq1003 = it_TQ1003.next();
			Iterator<Element> it = tq1003.elementIterator();
			Map<String , Object> tq1003Map = new HashMap<String, Object>();
			tq1003Map.put("INDEX", i++);
			while (it.hasNext()) {
				Element tmp = it.next();
				log.info(tmp.getName() + ":::::::" + tmp.getTextTrim());
				tq1003Map.put(tmp.getName(), tmp.getTextTrim());
			}
			if(StringUtils.isEmpty((String)tq1003Map.get("COL9"))){
				tq1003Map.put("COL9", null);
			}
			tq1003List.add(tq1003Map);
		}
		tq1Map.put("tq1003List", tq1003List);
		Element TQ1006 = element.element("TQ1006");
		Iterator<Element> it_TQ1006 = TQ1006.elementIterator();
		while (it_TQ1006.hasNext()) {
			Element tmp = it_TQ1006.next();
			log.info(tmp.getName() + ":::::::" + tmp.getTextTrim());
			tq1Map.put(tmp.getName(), tmp.getTextTrim());
		}
		Element TQ1007 = element.element("TQ1007");
		Iterator<Element> it_TQ1007 = TQ1007.elementIterator();
		while (it_TQ1007.hasNext()) {
			Element tmp = it_TQ1007.next();
			log.info(tmp.getName()+"TQ1007" + ":::::::" + tmp.getTextTrim());
			tq1Map.put(tmp.getName()+"TQ1007", tmp.getTextTrim());
		}

		return tq1Map;
	}

	/**
	 * 填充成新的pdf之后,会提示pdf文档自创建后被修改,无法再使用扩展功能,这个不知道为什么
	 * 
	 * @throws IOException
	 * @throws DocumentException
	 */
	public static void fillPdf() throws IOException, DocumentException {
		PdfUtil.manipulatePdf(RESOURCE, XMLDATA, RESULT);
	}

	public static void main(String[] args) throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException,
			DocumentException, BusinessException {
		// 提取pdf数据,打印
		printPdfData();
		// // 以XML形式导出pdf数据
		File file = new File(XMLDATA);
		PdfUtil.getXFAData(RESOURCE, file);
		// 根据模板和数据生成PDF
		fillPdf();

	}
}

 调用的pdfutil代码如下

 

package com.rb.common.pdf;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.XfaForm;

/**
 * pdf工具类 
 * 依赖ITEXT
 * @author HO274509
 *
 */
public class PdfUtil {

	/**
	 * Reads the data from a PDF containing an XFA form.
	 * 解析基于XML Forms Architecture的pdf,导出xml形式的表单数据写入到文件中
	 * @param src
	 *            the original PDF
	 * @param dest
	 *            the data in XML format
	 * @throws IOException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws TransformerFactoryConfigurationError
	 * @throws TransformerException
	 */
	public static void getXFAData(String src, File file) throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		FileOutputStream os = new FileOutputStream(file);
		PdfReader reader = new PdfReader(src);
		XfaForm xfa = new XfaForm(reader);
		Node node = xfa.getDatasetsNode();
		NodeList list = node.getChildNodes();
		for (int i = 0; i < list.getLength(); i++) {
			if ("data".equals(list.item(i).getLocalName())) {
				node = list.item(i);
				break;
			}
		}
		Transformer tf = TransformerFactory.newInstance().newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		tf.transform(new DOMSource(node), new StreamResult(os));
		reader.close();
	}
	
	/**
	 * Reads the data from a PDF containing an XFA form.
	 * 解析基于XML Forms Architecture的pdf,导出xml形式的表单数据写入到文件中
	 * @param src
	 *            the original PDF
	 * @throws IOException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws TransformerFactoryConfigurationError
	 * @throws TransformerException
	 */
	public static ByteArrayOutputStream getXFAData(InputStream is) throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		PdfReader reader = new PdfReader(is);
		XfaForm xfa = new XfaForm(reader);
		Node node = xfa.getDatasetsNode();
		NodeList list = node.getChildNodes();
		for (int i = 0; i < list.getLength(); i++) {
			if ("data".equals(list.item(i).getLocalName())) {
				node = list.item(i);
				break;
			}
		}
		Transformer tf = TransformerFactory.newInstance().newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		tf.transform(new DOMSource(node), new StreamResult(os));
		reader.close();
		return os;
	}
	/**
	 * Reads the data from a PDF containing an XFA form.
	 * 解析基于XML Forms Architecture的pdf,导出xml形式的表单数据写入到文件中
	 * @param src
	 *            the original PDF
	 * @param dest
	 *            the data in XML format
	 * @throws IOException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws TransformerFactoryConfigurationError
	 * @throws TransformerException
	 */
	public static void getXFAData(InputStream is, File file) throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		FileOutputStream os = new FileOutputStream(file);
		PdfReader reader = new PdfReader(is);
		XfaForm xfa = new XfaForm(reader);
		Node node = xfa.getDatasetsNode();
		NodeList list = node.getChildNodes();
		for (int i = 0; i < list.getLength(); i++) {
			if ("data".equals(list.item(i).getLocalName())) {
				node = list.item(i);
				break;
			}
		}
		Transformer tf = TransformerFactory.newInstance().newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		tf.transform(new DOMSource(node), new StreamResult(os));
		reader.close();
	}
	
	/**
	 * Checks if a PDF containing an interactive form uses AcroForm technology,
	 * XFA technology, or both. Also lists the field names.
	 * 
	 * 不支持adobe designer设计的xfa格式的pdf
	 * @param src
	 *            the original PDF
	 * @param dest
	 *            a text file containing form info.
	 * @throws IOException
	 */
	public void getFieldnames(String src, String dest) throws IOException {
		PrintStream out = new PrintStream(new FileOutputStream(dest));
		PdfReader reader = new PdfReader(src);
		AcroFields form = reader.getAcroFields();
		XfaForm xfa = form.getXfa();
		out.println(xfa.isXfaPresent() ? "XFA form" : "AcroForm");
		Set<String> fields = form.getFields().keySet();
		for (String key : fields) {
			out.println(key);
		}
		out.flush();
		out.close();
	}
	/**
	 * Reads the XML that makes up an XFA form.
	 * 
	 * @param src
	 *            the original PDF file
	 * @param dest
	 *            the resulting XML file
	 * @throws IOException
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws TransformerFactoryConfigurationError
	 * @throws TransformerException
	 */
	public void readXfa(String src, String dest) throws IOException,
			ParserConfigurationException, SAXException,
			TransformerFactoryConfigurationError, TransformerException {
		FileOutputStream os = new FileOutputStream(dest);
		PdfReader reader = new PdfReader(src);
		XfaForm xfa = new XfaForm(reader);
		Document doc = xfa.getDomDocument();
		Transformer tf = TransformerFactory.newInstance().newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		tf.transform(new DOMSource(doc), new StreamResult(os));
		reader.close();
	}
	
    /**
     * Manipulates a PDF file src with the file dest as result
     * @param src the original PDF
     * @param xml the XML data that needs to be added to the XFA form
     * @param dest the resulting PDF
     * @throws IOException
     * @throws DocumentException
     */
    public static void manipulatePdf(String src, String xml, String dest)
        throws IOException, DocumentException {
        PdfReader reader = new PdfReader(src);
        PdfStamper stamper = new PdfStamper(reader,
                new FileOutputStream(dest), '\0', true);
        AcroFields form = stamper.getAcroFields();
        XfaForm xfa = form.getXfa();
        xfa.fillXfaForm(new FileInputStream(xml));
        stamper.close();
    }
	
}

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics