Java抓图程序的实现(改进版)
主要难点:
1.并发线程的控制 采用了JDK5的UTIL包里的concurrent子包
2.去重
3.序列化
运行方法:java -Xms128M -Xmx512M -jar JavaCrawler.jar http://foxhq.com/ C:/a.log 0 D:/pic D:/url.tmp D:/img.tmp
SimpleBloomFilter.java
package com.hengking.crawl; import java.io.Serializable; import java.util.BitSet; public class SimpleBloomFilter implements Serializable { /** * */ private static final long serialVersionUID = 1L; private final int DEFAULT_SIZE = 2 << 24; private final int[] seeds = new int[] { 7, 11, 13, 31, 37, 61, }; private BitSet bits = new BitSet(DEFAULT_SIZE); private SimpleHash[] func = new SimpleHash[seeds.length]; // public void main(String[] args) { // String value = "stone2083@yahoo.cn"; // SimpleBloomFilter filter = new SimpleBloomFilter(); // System.out.println(filter.contains(value)); // filter.add(value); // System.out.println(filter.contains(value)); // } public SimpleBloomFilter() { for (int i = 0; i < seeds.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]); } } public void add(String value) { for (SimpleHash f : func) { bits.set(f.hash(value), true); } } public boolean contains(String value) { if (value == null) { return false; } boolean ret = true; for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; } public class SimpleHash implements Serializable { private int cap; private int seed; public SimpleHash(int cap, int seed) { this.cap = cap; this.seed = seed; } public int hash(String value) { int result = 0; int len = value.length(); for (int i = 0; i < len; i++) { result = seed * result + value.charAt(i); } return (cap - 1) & result; } } @Override public String toString() { // TODO Auto-generated method stub return super.toString(); } }
UtilSeriz.java
package com.hengking.crawl; import java.io.*; public class UtilSeriz { /** *将对象序列化到磁盘文件中 *@param *@throwsException */ public static void writeObject(Object o,String strPath) throws Exception{ File f=new File(strPath); if(f.exists()){ f.delete(); } FileOutputStream os=new FileOutputStream(f); //ObjectOutputStream 核心类 ObjectOutputStream oos=new ObjectOutputStream(os); oos.writeObject(o); oos.close(); os.close(); } /** *反序列化,将磁盘文件转化为对象 *@paramf *@return *@throwsException */ public static Object readObject(String strPath) throws Exception{ File f=new File(strPath); if(!f.exists()) { return null; } InputStream is=new FileInputStream(f); //ObjectOutputStream 核心类 ObjectInputStream ois=new ObjectInputStream(is); return ois.readObject(); } }
SearchCrawler.java
package com.hengking.crawl; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import com.hengking.crawl.po.PoCalSearch; import com.hengking.crawl.po.PoDownload; /*** * 说明:抓图工具 * @author 君望永远 * */ public class SearchCrawler implements Runnable{ /* disallowListCache缓存robot不允许搜索的URL。 Robot协议在Web站点的根目录下设置一个robots.txt文件, *规定站点上的哪些页面是限制搜索的。 搜索程序应该在搜索过程中跳过这些区域,下面是robots.txt的一个例子: # robots.txt for http://somehost.com/ User-agent: * Disallow: /cgi-bin/ Disallow: /registration # /Disallow robots on registration page Disallow: /login */ public static SimpleBloomFilter filterUrl; public static SimpleBloomFilter filterImg; private HashMap< String,ArrayList< String>> disallowListCache = new HashMap< String,ArrayList< String>>(); ArrayList< String> errorList= new ArrayList< String>();//错误信息 ArrayList< String> result=new ArrayList< String>(); //搜索到的结果 String startUrl;//开始搜索的起点 LinkedHashSet<String> toCrawlList = new LinkedHashSet<String>(); boolean caseSensitive=false;//是否区分大小写 boolean limitHost=false;//是否在限制的主机内搜索 private static String outdir; private static String seroutdir; private static String seroutdirimg; private boolean blnFlag=false; private static PoCalSearch ps=null; private static PoDownload pd=null; //300个图片分析线程 private static ExecutorService execImg; final Semaphore sempImg = new Semaphore(300); //30个网页分析线程 private static ExecutorService execPage; final Semaphore sempPage = new Semaphore(30); private ArrayList<ParsePage> arrPar=new ArrayList<ParsePage>(); //记录抓图结果 private static BufferedWriter bw = null; public SearchCrawler(String startUrl) { this.startUrl=startUrl; } public ArrayList< String> getResult(){ return result; } public void run(){//启动搜索线程 new Thread(new TimeWrite2File()).start(); blnFlag=true; crawl(startUrl,limitHost,caseSensitive); } //检测URL格式 private URL verifyUrl(String url) { // 只处理HTTP URLs. if (!url.toLowerCase().startsWith("http://")) return null; URL verifiedUrl = null; try { verifiedUrl = new URL(url); } catch (Exception e) { return null; } return verifiedUrl; } // 检测robot是否允许访问给出的URL. private boolean isRobotAllowed(URL urlToCheck) { String host = urlToCheck.getHost().toLowerCase();//获取给出RUL的主机 //System.out.println("主机="+host); // 获取主机不允许搜索的URL缓存 ArrayList< String> disallowList =disallowListCache.get(host); // 如果还没有缓存,下载并缓存。 if (disallowList == null) { disallowList = new ArrayList< String>(); try { URL robotsFileUrl =new URL("http://" + host + "/robots.txt"); BufferedReader reader =new BufferedReader(new InputStreamReader(robotsFileUrl.openStream())); // 读robot文件,创建不允许访问的路径列表。 String line; while ((line = reader.readLine()) != null) { if (line.indexOf("Disallow:") == 0) {//是否包含"Disallow:" String disallowPath =line.substring("Disallow:".length());//获取不允许访问路径 // 检查是否有注释。 int commentIndex = disallowPath.indexOf("#"); if (commentIndex != - 1) { disallowPath =disallowPath.substring(0, commentIndex);//去掉注释 } disallowPath = disallowPath.trim(); disallowList.add(disallowPath); } } // 缓存此主机不允许访问的路径。 disallowListCache.put(host, disallowList); } catch (Exception e) { return true; //web站点根目录下没有robots.txt文件,返回真 } } String file = urlToCheck.getFile(); //System.out.println("文件getFile()="+file); for (int i = 0; i < disallowList.size(); i++) { String disallow = disallowList.get(i); if (file.startsWith(disallow)) { return false; } } return true; } private String downloadPage(URL pageUrl) { try { // Open connection to URL for reading. BufferedReader reader = new BufferedReader(new InputStreamReader(pageUrl.openStream())); // Read page into buffer. String line; StringBuffer pageBuffer = new StringBuffer(); while ((line = reader.readLine()) != null) { pageBuffer.append(line); } return pageBuffer.toString(); } catch (Exception e) { e.printStackTrace(); } return null; } // 从URL中去掉"www" private String removeWwwFromUrl(String url) { int index = url.indexOf("://www."); if (index != -1) { return url.substring(0, index + 3) + url.substring(index + 7); } return (url); } // 解析页面并找出链接 private ArrayList< String> retrieveLinks(URL pageUrl, String pageContents, boolean limitHost) { // 用正则表达式编译链接的匹配模式。 Pattern p =Pattern.compile("<a//s+href//s*=//s*/"?(.*?)[/"|>]",Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(pageContents); ArrayList< String> linkList = new ArrayList< String>(); while (m.find()) { String link = m.group(1).trim(); if (link.length() < 1) { continue; } // 跳过链到本页面内链接。 if (link.charAt(0) == '#') { continue; } if (link.indexOf("mailto:") != -1) { continue; } if (link.toLowerCase().indexOf("javascript") != -1) { continue; } if (link.indexOf("://") == -1){ if (link.charAt(0) == '/') {//处理绝对地 link = "http://" + pageUrl.getHost()+":"+pageUrl.getPort()+ link; } else { String file = pageUrl.getFile(); if (file.indexOf('/') == -1) {//处理相对地址 link = "http://" + pageUrl.getHost()+":"+pageUrl.getPort() + "/" + link; } else { String path =file.substring(0, file.lastIndexOf('/') + 1); link = "http://" + pageUrl.getHost() +":"+pageUrl.getPort()+ path + link; } } } int index = link.indexOf('#'); if (index != -1) { link = link.substring(0, index); } link = removeWwwFromUrl(link); URL verifiedLink = verifyUrl(link); if (verifiedLink == null) { continue; } /* 如果限定主机,排除那些不合条件的URL*/ if (limitHost && !pageUrl.getHost().toLowerCase().equals( verifiedLink.getHost().toLowerCase())) { continue; } // 跳过那些已经处理的链接. if(filterUrl.contains(link)) { logEvent("匹配了:"+link); continue; } else { filterUrl.add(link); } linkList.add(link); } return (linkList); } // 解析页面并找出链接 private ArrayList< String> retrieveImgLinks(URL pageUrl, String pageContents, boolean limitHost) { // 用正则表达式编译链接的匹配模式。 Pattern p =Pattern.compile("<img//s+src//s*=//s*/"?(.*?)[/"|>]",Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(pageContents); ArrayList< String> linkList = new ArrayList< String>(); while (m.find()) { String link = m.group(1).trim(); if (link.length() < 1) { continue; } // 跳过链到本页面内链接。 if (link.charAt(0) == '#') { continue; } if (link.indexOf("mailto:") != -1) { continue; } if (link.toLowerCase().indexOf("javascript") != -1) { continue; } if (link.toLowerCase().endsWith("gif")) { continue; } if (link.indexOf("://") == -1) { if (link.charAt(0) == '/') {//处理绝对地 link = "http://" + pageUrl.getHost()+":"+pageUrl.getPort()+ link; } else { String file = pageUrl.getFile(); if (file.indexOf('/') == -1) {//处理相对地址 link = "http://" + pageUrl.getHost()+":"+pageUrl.getPort() + "/" + link; } else { String path =file.substring(0, file.lastIndexOf('/') + 1); link = "http://" + pageUrl.getHost() +":"+pageUrl.getPort()+ path + link; } } } int index = link.indexOf('#'); if (index != -1) { link = link.substring(0, index); } link = removeWwwFromUrl(link); URL verifiedLink = verifyUrl(link); if (verifiedLink == null) { continue; } /* 如果限定主机,排除那些不合条件的URL*/ if (limitHost && !pageUrl.getHost().toLowerCase().equals( verifiedLink.getHost().toLowerCase())) { continue; } // 跳过那些已经处理的链接. // if (crawledList.contains(link)) { // continue; // } if(filterImg.contains(link)) { logEvent("图片匹配了:"+link); continue; } else { filterImg.add(link); } if(link.lastIndexOf(".gif")==-1) { linkList.add(link); } } return (linkList); } //执行实际的搜索操作 public ArrayList< String> crawl(String startUrl,boolean limithost,boolean caseSensitive ) { // 从开始URL中移出www startUrl = removeWwwFromUrl(startUrl); toCrawlList.add(startUrl); int idxPageParse=0; while (toCrawlList.size()>0) { try { idxPageParse++; // Get URL at bottom of the list. String url = toCrawlList.iterator().next(); ps.setIntUrl(ps.getIntUrl()+1); // Remove URL from the to crawl list. toCrawlList.remove(url); int intRetryPage=0; while (sempPage.availablePermits()<=0) { System.out.println("暂时没有空闲的网页分析线程,等待3秒再执行..."); try { intRetryPage++; if(intRetryPage==10) { logEvent("分析网页"+url+"超时"); sempPage.release(); break; } Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } ParsePage tempPageThread=new ParsePage(url); execPage.submit(tempPageThread); logEvent("开启网页分析线程"+idxPageParse); if(idxPageParse==1) { Thread.currentThread().sleep(30000); } }catch(Exception e) { e.printStackTrace(); } } blnFlag=false; logEvent("抓图完成......"); return result; } public static void logEvent(String strLog) { System.out.println( new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒").format(new Date(Calendar.getInstance().getTimeInMillis()))+"=====>"+strLog); } // 主函数 public static void main(String[] args) { if(args.length!=6) { System.out.println("Usage:java SearchCrawler startUrl maxUrl searchString"); return; } @SuppressWarnings("unused") String strLogPath=args[1]; SearchCrawler crawler = new SearchCrawler(args[0]); outdir=args[3]+"/pic"+new SimpleDateFormat("yyyyMMdd").format(new Date(Calendar.getInstance().getTimeInMillis()))+"/"; File f=new File(outdir); if(!f.exists()) { f.mkdir(); } execPage = Executors.newFixedThreadPool(30); execImg = Executors.newFixedThreadPool(300); seroutdir=args[4]; seroutdirimg=args[5]; ps=new PoCalSearch(); pd=new PoDownload(); try { if(UtilSeriz.readObject(seroutdir)!=null) { System.out.println(new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒").format(new Date(Calendar.getInstance().getTimeInMillis()))+"=====>"+"反序列化URL..."); filterUrl=(SimpleBloomFilter)UtilSeriz.readObject(seroutdir); } else { filterUrl=new SimpleBloomFilter(); } if(UtilSeriz.readObject(seroutdir)!=null) { System.out.println(new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒").format(new Date(Calendar.getInstance().getTimeInMillis()))+"=====>"+"反序列化图片..."); filterImg=(SimpleBloomFilter)UtilSeriz.readObject(seroutdirimg); } else { filterImg=new SimpleBloomFilter(); } } catch (Exception e) { e.printStackTrace(); } String strPic=args[3]+"/pic"+new SimpleDateFormat("yyyyMMdd").format(new Date(Calendar.getInstance().getTimeInMillis()))+".log"; try { bw=new BufferedWriter(new FileWriter(strPic,false)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Thread search=new Thread(crawler); System.out.println( new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒").format(new Date(Calendar.getInstance().getTimeInMillis()))+"=====>"+"开始爬图..."); System.out.println("下载了图:"); search.start(); try { search.join(); logEvent("主函数结束"); bw.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 说明:下载图片的线程 * @author binbin0915 * */ public class ImgDownThread implements Runnable,Callable<Long>{ //待下载的URL private String stru; private boolean isStart=true; public ImgDownThread(String strurl) { super(); this.stru = strurl; } @Override public void run() { try { sempImg.acquire(); try{ URL url=new URL(stru); BufferedInputStream in = new BufferedInputStream(url.openStream()); BufferedImage bi=ImageIO.read(url.openStream()); //尺寸要求 if (bi==null|| bi.getWidth()<30 || bi.getHeight()<30 ) { in.close(); return; } String ss=new SimpleDateFormat("yyyyMMddHHmmss").format(new Date(Calendar.getInstance().getTimeInMillis()))+"_"+Math.round(Math.random()*89999999999999L+1000)+stru.substring(stru.lastIndexOf(".")); String s=outdir+ss; FileOutputStream file = new FileOutputStream(new File(s)); int t; while ((t = in.read()) != -1) { file.write(t); } file.close(); if(new File(s).length()<=10*1024) { in.close(); new File(s).delete(); return; } synchronized(bw) { String str=ss+":"+stru; bw.write(str); bw.newLine(); bw.flush(); } logEvent("下载了:"+stru); ps.setIntImg(ps.getIntImg()+1); in.close(); }catch(Exception e){ logEvent("**********************下载图片:"+stru+"超时"); } } catch (Exception e) { e.printStackTrace(); } finally{ sempImg.release(); } } public boolean isStart() { return isStart; } public void setStart(boolean isStart) { this.isStart = isStart; } @Override public Long call() throws Exception { try { sempImg.acquire(); try{ URL url=new URL(stru); BufferedInputStream in = new BufferedInputStream(url.openStream()); BufferedImage bi=ImageIO.read(url.openStream()); //尺寸要求 if (bi==null|| bi.getWidth()<30 || bi.getHeight()<30 ) { in.close(); return 0l; } String ss=new SimpleDateFormat("yyyyMMddHHmmss").format(new Date(Calendar.getInstance().getTimeInMillis()))+"_"+Math.round(Math.random()*89999999999999L+1000)+stru.substring(stru.lastIndexOf(".")); String s=outdir+ss; FileOutputStream file = new FileOutputStream(new File(s)); int t; while ((t = in.read()) != -1) { file.write(t); } file.close(); if(new File(s).length()<=10*1024) { in.close(); new File(s).delete(); return 0l; } logEvent("下载了:"+stru); ps.setIntImg(ps.getIntImg()+1); in.close(); }catch(Exception e){ logEvent("**********************下载图片:"+stru+"超时"); } } catch (Exception e) { e.printStackTrace(); } finally{ sempImg.release(); return 1l; } } } /*** * 序列化已访问的URL * @author binbin0915 * */ public class TimeWrite2File implements Runnable { @Override public void run() { while(blnFlag) { try { synchronized(ps) { logEvent("开始序列化URL"); UtilSeriz.writeObject(filterUrl,seroutdir); logEvent("结束序列化URL"); logEvent("开始序列化图片"); UtilSeriz.writeObject(filterImg,seroutdirimg); logEvent("结束序列化图片"); logEvent("分析了"+ps.getIntUrl()+"个链接"); logEvent("下载了"+ps.getIntImg()+"张图片"); } Thread.sleep(600000); } catch (Exception e) { e.printStackTrace(); } } } } /*** * 分析对应URL网页的线程 * @author Administrator * */ class ParsePage extends Thread { String url; int iCount=0; public int getiCount() { return iCount; } public void setiCount(int iCount) { this.iCount = iCount; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public ParsePage(String url) { this.url=url; } @Override public void run() { try { sempPage.acquire(); // Convert string url to URL object. URL verifiedUrl = verifyUrl(url); // Skip URL if robots are not allowed to access it. if (!isRobotAllowed(verifiedUrl)) { Thread.currentThread().stop(); } // 增加已处理的URL到crawledList String pageContents=""; pageContents = downloadPage(verifiedUrl); logEvent("分析了:"+verifiedUrl); logEvent("待分析URL数:"+toCrawlList.size()+"个"); if (pageContents != null && pageContents.length() > 0) { // 从页面中获取有效的链接 ArrayList< String> links =retrieveLinks(verifiedUrl, pageContents,limitHost); // 从页面中获取有效的链接 ArrayList< String> imglinks =retrieveImgLinks(verifiedUrl, pageContents,limitHost); //添加到图片下载队列 if(toCrawlList.size()<100000) { toCrawlList.addAll(links); } else { logEvent("待分析的网页URL超过100000!!!!跳过......."); } for(int i=0;i<imglinks.size();i++) { if(imglinks.get(i).indexOf("http:")!=-1) { iCount++; filterImg.add(imglinks.get(i)); ps.setIntImg(ps.getIntImg()+1); int intRetryImg=0; while (sempImg.availablePermits() <= 0) { System.out.println("暂时没有空闲的抓图线程,等待3秒再执行..."); try { intRetryImg++; if(intRetryImg==10) { logEvent("抓图"+imglinks.get(i)+"超时"); sempImg.release(); } Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } Thread tempImgThread=new Thread(new ImgDownThread(imglinks.get(i))); execImg.submit(tempImgThread); if((iCount!=1) && (iCount%10==1) ) { try { logEvent("图多休息2秒......"); Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } synchronized(arrPar) { arrPar.remove(this); } } catch(Exception e) { e.printStackTrace(); } finally { sempPage.release(); } } } }
Java抓图程序的实现(改进版)相关推荐
- 自己写的Java抓图程序
公司里要写一个抓图的程序 其实和搜索引擎差不多的原理 下载分析网页遇到<a>标签继续模拟点击进去再分析该网页 遇到<img>就下载该图 难点: 1 URL去重 采用bloomf ...
- java程序ssh置顶_使用shell脚本启动远程(SSH)Java应用程序不会返回本地提示
我见过类似的问题,所有已解决的问题已经解决/不适用. 我在启动Java应用程序的远程计算机中有一个bash脚本.相关的行将是: #!/usr/bin/env bash ... java -cp /fu ...
- java 正则 cpu 100_这六个原因真的可以使Java应用程序的CPU使用率飙升到100%吗?...
点击上方的"代码农户的冥想记录",然后选择"设为明星" 高质量文章,及时交付 问题 1. 无限while循环会导致CPU使用率飙升吗? 2.经常使用Young ...
- java web程序示例_想要建立一些有趣的东西吗? 这是示例Web应用程序创意的列表。...
java web程序示例 Interested in learning JavaScript? Get my ebook at jshandbook.com 有兴趣学习JavaScript吗? 在js ...
- java 用程序代码解释继承_关于初级java程序员笔试题
关于初级java程序员笔试题 Sun 认证Java程序员考试内容涉及Java所有相关知识.编程概念及applet开发技巧.下面是小编整理的关于初级java程序员笔试题,欢迎大家参考! 第一题:判断题 ...
- [Google API](8)构建使用Google API的Java应用程序
Google 搜索引擎建立起了通过 Web 服务接口可用的索引.拼写建议和缓存页面,从而允许所有语言的程序员都能就个人使用存取信息.Google 搜索引擎还提供了 Java API,从而存取数据更为便 ...
- 用JEP 343打包工具,构建自包含、可安装的Java应用程序
OpenJDK社区发布了JEP 343:打包工具的早期访问版本.JEP 343:打包工具,又名jpackage,是打包自包含Java应用程序和Java运行时环境的新工具.这个基于JavaFX java ...
- Java应用程序项目的打包与发行
这里主要是讲解一下怎样将 Java程序打包成独立运行的exe程序包,以下这种方法应该是最佳的解决方案了.NetDuke的EXE程序包了是使用这种方案制作的.在操作步骤上还是比较简单的,而且通用性强. ...
- 面向 Java 开发人员的 Ajax: 构建动态的 Java 应用程序
面向 Java 开发人员的 Ajax: 构建动态的 Java 应用程序 Ajax 为更好的 Web 应用程序铺平了道路 在 Web 应用程序开发中,页面重载循环是最大的一个使用障碍,对于 Java™ ...
- Java高级程序员(5年左右)面试的题目集
Java高级程序员(5年左右)面试的题目集 https://blog.csdn.net/fangqun663775/article/details/73614850?utm_source=blogxg ...
最新文章
- XmlHttp学习笔记
- python配置文件密码管理_python – 可以在django管理员中实现“下次登录时更改密码”类型功能吗?...
- 关闭页面刷新上层页面的几种方式
- Win10修改防火墙入站规则
- 文本显示变量_【RPA课堂】UiPath中的变量、数据类型和组件
- 文件上传java逻辑_Java 文件上传 实例
- 关于Js下拉导航的解释
- 安装虚拟机Centos系统并安装Docker过程记录
- 重构职场竞争力之测试跨界思维
- 使用Elizabeth为您的应用程序生成随机数据
- 使用 Linux下 timerfd 系列 API 创建定时器并使用 epoll 监听
- Tcl 语言 ——语法篇
- 分享110个ASP源码,有一款适合你
- 【MacOS】MacOS 添加虚拟打印机
- 百度披露被黑原委 黑客骗得邮箱
- 使用google.zxing制作条形码和二维码
- 重置微信内置浏览器字体大小
- 身为UI设计师,如何应对失业的恐慌
- mysql查询重名_同名同姓搜索,同名身份证号码数据库
- 推荐系统三十六式(刑无刀)学习笔记(四)
热门文章
- uniapp ios 跳转appstore
- 编译udf小软件(附视频教程)
- 计算机毕业设计项目推荐(源码+论文+PPT)
- DoublyLinkedList
- checking for libzip... configure: error: system libzip must be upgraded to version = 0.11问题解决
- 人工智能数学基础8:两个重要极限及夹逼定理
- 卡瓦莱斯的世界杯往事
- Android View事件分发机制
- a标签的href属性与事件修饰符阻止默认行为
- 分段线性插值法实验报告_试验二插值法(含实验报告格式)-金锄头文库