写给广大网友: 如果不是为了作业我甚至不会写这篇博客, 这篇博客没有任何技术参考价值, 反而可能给各位造成误导, 请各位慎重参考莫要完全置信!
写给助教: 大多数内容是我学习笔记直接Paste上来的...未必代表我的学习顺序.
request的第一个参数包括请求头, url, 编码方式等信息, 代表请求; 第二个参数代表回调函数, 其中body部分相当于网页中的document对象
//重新包装后柯里化的request函数 let my_req = (callback) => (url) => { let options = { url: url, encoding: null, headers: headers, timeout: 1000 } try { request(options, (error, response, body) => {//request的第一个参数包括请求头, url, 编码方式等信息, 代表请求; 第二个参数代表回调函数, 其中body部分相当于网页中的document对象 try { if (!error && response.statusCode === 200) callback(body); else { console.error("访问 " + url + " 超时, 原因可能是网络不佳或爬虫被屏蔽 " + error + " " + response); fails++, console.log("有" + fails + "次请求中的失败"); } } catch (e) { console.log("回调" + url + " 时出现错误 " + e) } }) } catch (e) { console.error("访问不成功, 可能是非法的url格式, 或回调函数中出现了错误" + e); } }
Js是一种动态语言, 因此它的对象并不是通过声明来固定某种类型, 而是通过调用构造函数生成对象并将它赋值给某个变量.
对象的成员变量(或叫属性)和函数和
只需要调用正则表达式的构造函数或者通过简便写法'/a/'就能
var regexp = new RegExp("a"); var regexp_ = /a/;
正则表达式能够作为字符对象match函数的参数(模式串)使用, match函数的返回值是一个子串, 使得其存在从第0个字符开始的子串能够与模式串匹配.
由于在Leetcode上写过正则表达式那个题, 所以我对两种特殊的模式串'.'和'*'还是比较熟悉的.
bool isMatch(string &s, string &p, int _s = 0, int _p = 0) { if (_p + 1 < p.size() && p[_p + 1] == '*') { if (isMatch(s, p, _s, _p + 2)) return true; for (int i = 1; _s + i - 1 < s.size(); i++) { if (p[_p] != '.' && p[_p] != s[_s + i - 1]) break; if (isMatch(s, p, _s + i, _p + 2)) return true; } return false; } if (_s >= s.size() && _p >= p.size()) return true; if (_s >= s.size() || _p >= p.size()) return false; return (p[_p] == '.' || p[_p] == s[_s]) && (isMatch(s, p, _s + 1, _p + 1)); }
能匹配一类字符的特殊字符: '\d'匹配所有数字和'\w'匹配所有字母; '[abc]'匹配方括号内的任意一个字符.
决定匹配次数的特殊字符: '*'匹配它前面的字符任意次(包括0次); '?'匹配它前面的字符0次或1次; '+'匹配它前面的字符1次或更多次.
决定匹配开始和结束的特殊字符: '^'待匹配串的开头是模式串; '$'它之前的待匹配串的结尾是模式串.
一个网页是新浪主页上的新闻网页, 当网页的url不是.html就是.shtml或者.phtml结尾, 且用https:/作为开头.
var news_reg = /https?:\/\/[\S]*.s?p?html$/;
其中check函数是通过上面的正则表达式实现的, 过于trivial于是不贴chek函数的代码.
const open_all_ahref_with = (open_) => my_req((body) => { try { var html = myIconv.decode(body, my_encoding); var $$ = myCheerio.load(html, { decodeEntities: true }); } catch (e) { console.log("解析含a链接的网页失败" + e) } try { var a_tags, a_tags = $$('a') } catch (e) { console.log("解析网页中的a链接失败" + e) } if (a_tags == undefined) return console.log("没有发现a链接"); a_tags.each((i, elem) => { let href = $$(elem).attr("href"); href = formated_as_whole_url(href); console.log(href); if (check(href)) open_(href); }) })
最基础的用法
匹配某个类型的标签
$('a')
遍历使用forEach, 其参数是回调函数, 函数的参数是(i, elem)的二元组, 函数体内放上与正常for循环该有的代码, 当需要循环中断时, 返回false, 需要继续执行, 返回true. 语法与for非常不同, 但功能上提供了break和continue类似的东西.
获取标签的属性和内容
$('a').text()//第一个a链接中装着的内容 $('a').attr('href')//第一个a链接中的href属性的值
处理新闻的过程非常傻, 基本上完全依赖于网页上实际有啥.
const store_news_with = (store_) => (url) => my_req((body) => { try { var html = myIconv.decode(body, my_encoding); var $$ = myCheerio.load(html, { decodeEntities: true }); } catch (e) { console.log("解析新闻页面失败" + e) } try { var fetch = { title: "", content: "", url: url, keywords: "", publish_time: "", crawl_time: new Date(), source_name: seed, source_encoding: my_encoding, } } catch (e) { console.log("定义当前网页的抓取(fetch)对象失败" + e) } try { var possible_title = $$('title'); possible_title.each((i, elem) => { fetch.title = fetch.title + $$(elem).text() + "\n"; }) } catch (e) { console.log("没能从title标签中捕获标题" + e); try { var possible_title = $$('h1'); possible_title.each((i, elem) => { let text = $$(elem).text(); if (/�/.test(text)) return false; fetch.title = fetch.title + text + "\n"; }) } catch (e) { console.log("没能从h1标签中捕获标题" + e); return; } } try { var possible = $$('p'); possible.each((i, elem) => { let text = $$(elem).text(); if (/�/.test(text)) return false; if (text.length > 20 && !/\|/.test(text) && !/ /.test(text)) fetch.content = fetch.content + text + "\n"; }) } catch (e) { console.log("没能从p标签中捕获内容" + e); return; } try { var possible = $$('meta[name="keywords"]').first(); fetch.keywords = possible.attr("content"); } catch (e) { console.log("没能捕获关键词" + e); return; } try { var possible = $$('meta[property*="time"]').first(); fetch.publish_time = possible.attr("content"); } catch (e) { console.log("没能从meta标签捕获发行时间" + e) } if (fetch.publish_time === "") try { var possible = $$('span[class*="time"]').first(); fetch.publish_time = possible.text(); } catch (e) { console.log("没能从span标签捕获发行时间" + e); return; } var no_undefined = true; if (fetch == undefined) { console.assert("fetch对象为空"); return; } try { for (let i in fetch) if (fetch[i] == "" || fetch[i] == undefined) return console.log("fetch对象中的" + i + "为空") } catch (e) { console.log("fetch对象不能被迭代" + e), no_undefined = false } if (no_undefined) store_(fetch); })(url)
CREATE TABLE `fetches` ( `id_fetches` int(11) NOT NULL AUTO_INCREMENT, `url` varchar(200) DEFAULT NULL, `source_name` varchar(200) DEFAULT NULL, `source_encoding` varchar(45) DEFAULT NULL, `title` varchar(200) DEFAULT NULL, `keywords` varchar(200) DEFAULT NULL, `publish_time` date DEFAULT NULL, `crawl_time` datetime DEFAULT NULL, `content` longtext, `createtime` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id_fetches`), UNIQUE KEY `id_fetches_UNIQUE` (`id_fetches`), UNIQUE KEY `url_UNIQUE` (`url`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
异步函数: 在定义函数时使用的function关键字前或者在定义箭头函数的参数列表前加上async关键字就能将这个函数声明为异步函数.
异步函数中, 各条命令没有执行顺序的限制, 所以它会比非异步的情况更快完成(尤其是在网络请求和渲染异步进行的业务场景中非常必要).
当某条命令依赖于某个变量的值时, 这个变量很可能没有变成程序员所需要的那样. 此时Promise对象提供了一种解决方案.
Promise对象的构造函数的参数是一个函数, 这个函数的入参是两个函数resolve和reject, 函数体内包含了一系列的指令, 这些指令最终产生的结果将作为resolve的入参.
对Promise对象使用await关键字时, 它将返回resolve的入参. 这使得异步编程变得更自然.
就我的观点而言Promise是某种扭曲的产物, 是Js社区中同时存在函数式编程和对象式编程两种范式的不良后果. 事实上, 纯函数式编程中并不需要Promise, 而纯对象式编程不会这样来定义Promise的构造函数.
首先封装连接池(通过node_module中的mysql组件)
const mysql = require('mysql'); var pool_ = mysql.createPool({ host: '127.0.0.1', user: 'root', password: 'root', database: 'crawl' }); module.exports = { pool_ };
然后封装请求指令: 指令中需要先从池中抽取连接, 再通过连接来进行请求, 故写作异步函数时需要用Promise保证其先执行. (实际上此处完全没有必要, 因为它与非异步的效果几乎相同)
const { pool_ } = require('./pool_'); const findUser = () => { return new Promise((resolve, reject) => { try { pool_.getConnection((err, conn) => { if (err) { console.warn("Cannot connect MySql " + err); } else { resolve(conn); return; } }) } catch (e) { reject(undefined); } }) } const sqlquery_ = async (sql, param, callback) => { let connection = await findUser(); return new Promise(() => connection.query(sql, param, callback)).then(connection.release()); } const sqlquery_noparam = async (sql, callback) => { let connection = await findUser(); return new Promise(() => connection.query(sql, callback)).then(connection.release()); } exports.query_ = sqlquery_; exports.query_noparam = sqlquery_noparam;
const store_to_mysql_if_no_repetition = (fetch) => { const fetchAddSql = 'INSERT INTO fetches(url,source_name,source_encoding,title,' + 'keywords,publish_time,crawl_time,content) VALUES(?,?,?,?,?,?,?,?)'; const fetchAddSql_Params = [fetch.url, fetch.source_name, fetch.source_encoding, fetch.title, fetch.keywords, fetch.publish_time, fetch.crawl_time.toFormat("YYYY-MM-DD HH24:MI:SS"), fetch.content ]; sql.query_(fetchAddSql, fetchAddSql_Params, (err, vals, fields) => { if (err) { console.log("存入MySql时: " + err); } else { console.log("存储成功:" + fetch.url); } }) }
首先需要一个表单来向服务器提交数据, 因而有一个<form>标签; 然后搜索数据需要关键字类型(关键词, 标题, 日期, 内容等)就在里面放个<select>标签再在里面套个<option>标签;
搜索按钮用<input type = 'button'>, 搜索框用<input type = 'text'>.
需要实现搜索下拉栏, 它是个无序列表, 所以在下面放个<div>再套个<ul>, 拿到数据之后就放到里面去.
<div class="wrapper"> <img src="Images/DASE.png" class="logo"> <form class="SearchBox"> <div class="MainBox" style="display: block;"> <select class="SearchOptions"> <option value="title">Title</option> <option value="keywords">KeyWords</option> <option value="publish_time">Publish Time</option> <option value="source_name">Source</option> <option value="crawl_time">Crawl Time</option> </select> <input type="text" class="SearchTextBox"> <input type="button" value="Search" class="SearchButton"> </div> <div class="Sublist" style="display: inline-block;"> <ul class="Suggestion"> </ul> </div> </form> </div>
令人欣慰的是: css的选择器和jQuery的选择器语法居然大差不差, 省掉了我很多力气.
伪类选择器的关键字多了个:hover用来判断鼠标悬停事件, 由此就能做出下拉框的动态效果了.
<style> .wrapper { text-align: center; margin-top: 150px; z-index: 0; } .logo { display: inline-block; margin-left: 10px; margin-block-end: 30px; width: 375px; height: auto; } .MainBox { text-align: center; display: block; } .SearchBox { display: block; } .SearchOptions { display: inline-flex-box; vertical-align: top; position: relative; left: 6px; width: 115px; height: 16px; z-index: 10; padding: 12px 16px; font-size: 16px; margin: 0 0 0 0; outline: 0; box-shadow: none; border-radius: 10px 0 0 10px; border: 2px solid #3e0079; background: #fff; color: #222; box-sizing: content-box; text-align: center; } .SearchTextBox { display: inline-flex; vertical-align: top; width: 512px; height: 16px; padding: 12px 16px; margin: 0, 0, 0, 0; font-size: 16px; box-shadow: none; border-radius: 0 0 0 0; border: 2px solid #3e0079; background: #fff; color: #222; box-sizing: content-box; } .SearchButton { display: inline-flexbox; z-index: 100; vertical-align: top; position: relative; left: -6px; cursor: pointer; width: 108px; height: 44px; line-height: 45px; line-height: 44px\9; padding: 0; margin: 0, 0, 0, 0; background: 0 0; background-color: #3e0079; border-radius: 0 10px 10px 0; font-size: 17px; color: #fff; box-shadow: none; font-weight: 400; border: none; outline: 0; } .Suggestion { display: inline-block; position: relative; left: -17px; top: -17px; list-style: none; width: 512px; height: auto; margin: 0, 0, 0, 0; } .SuggestionWord { display: block; width: 512px; height: auto; padding: 12px 16px; margin: 0, 0, 0, 0; font-size: 16px; border: 2px solid #3e0079; border-top: none; background: rgb(253, 250, 255); color: #222; } div.SuggestionWord:hover { background-color: rgb(243, 229, 243); color: #3e0079; } .SuggestionWord:first-child { border-top: 2px solid #3e0079; } .SuggestionWord:last-child { border-radius: 0 0 10px 10px; } a { text-decoration: none; z-index: 100; } </style>
由于某种未知的原因