分类 项目的一些探索和发现 下的文章

受同学所托,要爬一个网站

第一步很自然的,查看网页元素,看渲染的能不能直接抓下来,

但是用上requestcherrio,一查元素,为空?(页面上明明有),把requestbody打印出来,才发现是ajax,应对方法:打开chrome,用Network,终于查看到了请求,复制链接地址,竟然直接浏览器打开就可以获得到json数据...

接下来就是漫长的数据整理工作了以及一直要弄明白的回调地狱的问题

第一关:字符串处理

以为返回的是json,结果一直报错,才发现返回的内容是hxbase_json1({sum: 2660, list: [,…]}),也就是真正的json内容在两个括号里面...

换言之,怎么获得两个指定的符号中间的内容?

第一反应:正则,但基本忘光了(以前就只是生搬硬套的用...),之后搜索中看到有人提到了直接用字符串的方法,str.substring(str.indexOf('('),str.indexOf(')')),这方法好是好,但是:

input: "hxbase_json1({XXXXxx(xxx)xxxx})"
output: ({XXXXxx(xxx

也就是如果中间有括号就不行(弃),如果要用字符串的方法的话,直接提取子串不就可以了!str.slice(13, -1);,主要发现其他请求都是返回的这个形式,所以直接写死也行,好的,第一关成功!

另,就在写总结的时候,发现找到了以前第一次爬网站的时候各种堆砌出来的较通用的正则匹配函数:

// 匹配前后缀,提取中间的内容
function getInnerString(source, prefix, postfix) {
    if (prefix !== "<p>")
        var regexp = new RegExp(encodeReg(prefix) + '.+' + encodeReg(postfix), 'gi');
    else
        var regexp = new RegExp(encodeReg(prefix) + '(?:(?!<\/p>)[\\s\\S])*' + encodeReg(postfix), 'gi');
    var matches = String(source).match(regexp);
    if (matches == null) {
        let now = new Date();
        matches = ['this is failed', now + ''];
    }
    var formatedMatches = matches.map(value => {
        return value
            .replace(prefix, '')
            .replace(postfix, '');
    });
    return formatedMatches;
}
//转义影响正则的字符
function encodeReg(source) {
    return String(source).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1');
}

第二关:字符串转换成json

以为提取出字符串之间的内容就可以了

// 提取中间内容
let str = body.slice(13, -1);
// 把字符串转化为json
let testJson = JSON.parse(str);
...
// 报错
SyntaxError: Unexpected token s in JSON at position 1

参考链接:字符串转换成JSON的三种方式,终于知道为什么,因为必须要遵守json规范,而字符串里面的属性并没有引号...

示例发现一个更神奇的现象,只有外面用单引号,而属性用双引号才可以

let jee = '{"sum":2660}'; // 正确:输出{ sum: 2660 }
let jee = "{'sum':2660}"; // SyntaxError: Unexpected token ' in JSON at position 1
console.log(JSON.parse(jee));

于是这里的解决方法只能用eval了(第一次觉得eval这么亲切!),完美!

第三关:回调地狱

现在整个流程差不多写成这样:

// 部分为伪代码
for(let i= 11; i<=17 ;i++) {
    // 先请求起始url,借此得到所有的页码数
    let first = "xxx";
    Request(first, (error, response, body)=> {
        let str = body.slice(13, -1);
        let testJson = eval("(" + str + ")");
        let sum = testJson.sum;
        // 获取到整个的页码
        let pageNum = Math.floor(sum / 20);
        for(let j=1; j<=pageNum; j++) {
            let url = "xxx"+j;
            Request(url, (error, response, body) => {
                // 获取数据(伪代码)
                list.push(data)
            })
        }
        //写数据
        fs.writeFile(i+"year.json", JSON.stringify(list),
            function (err) {
                if (err) throw err;
                console.log('It\'s saved!');
            }
        );
    })
}

这也就是传说中的回调金字塔(回调地狱),说说我自己遇到的情况,

第一,不一定每次请求都能正确无误,所以常常跑着跑着,突然报错了,然后这样的代码结构简直要命,实际中的我还在其中用了一个urls数组先存下所有的url再遍历发请求,也就是各种嵌套,各种异步,出错出在哪到找不到...

第二,遇到了爬虫(或者异步中)很经典的问题,怎么判断回调结束了?这里的问题就是在fs.writeFile的时候怎么确保list已经获取到了所有的值?(11年有133条url,每条url指向20条数据)

第一种问题,以前有过一次爬博客园的经历,那里是采用Promise化的方式解决的,request库还有promise的版本,promise就保证了要么then回调成功,要么err

/**
 * 第一次爬博客园时的经历
 * users 示例
 {
    raphael5200: {
        username: 'raphael5200',
        defaultPage: 'http://www.cnblogs.com/raphael5200/default.html?page=',
        pages: 'http://www.cnblogs.com/raphael5200/default.html?page=2'
    },
    joyeecheung: {
        username: 'joyeecheung',
        defaultPage: 'http://www.cnblogs.com/joyeecheung/default.html?page=',
        pages: 'http://www.cnblogs.com/joyeecheung/default.html?page=2'
    }
    ...
 }
*/
// 遍历每一个用户
for (let key in users) {
    // 请求每一个用户的主页
    rq(users[key].pages)
      .then((body)=>{
        let titles = [];
        let reads = 0;
        let replys = 0;
        for (let i = 0; i < getPageNum(body); i++) {
          // 请求用户发出来的每一个文章列表页
          rq(users[key].defaultPage + '' + i)
            .then((body) => {
              // 提取当前文章列表页的所有文章标题,存到users[key].articles, 每篇文章的阅读数、评论数相加放置到readNum,replyNum中

              // 写入 当时不知道怎么判断异步结束,直接用了一个比较hack的方法,不断的重写文件(最后能保存下来的就随缘了)
              // fs.writeFile: Asynchronously writes data to a file, replacing the file if it already exists
              fs.writeFile("users.json", JSON.stringify(users),
                  function (err) {
                      if (err) throw err;
                      console.log('It\'s saved!');
                  }
              );
            }).catch (function (err) {
              //console.log(err);
            });
        }
      })
      .catch(function (err) {
          console.log(err);
      });
};
// 写入 还尝试过另一个方法,等待1分钟之后写入...(随缘)
setTimeout((users) => {
fs.writeFile("users.json",JSON.stringify(users),
  function (err) {
    if (err) throw err;
    console.log('It\'s saved!');
  }
);
}, 1000*1*60);

这次实际上是通过,把不必要的循环去掉,直接把10年17年要的数据,就分为8条url,在直接请求这8条(好在只有8条啊)

插播一条小细节: 之前的let str = body.slice(13, -1);,常常报错,大致意思就是body出问题了,undefined或者什么的不能调用slice方法,这样的话就又遇到了一个经典的问题:js怎么判断变量的类型?

参考链接:JavaScript学习总结(六)——JavaScript判断数据类型总结

// 外层加一个判断就避免了
if (typeof body === "string") {
        let str = body.slice(13, -1);

第二种问题是采用的,settInterval计数解决

参考链接:NodeJS的异步编程风格

例如:fs去访问五个txt文件,1.txt...5.TXT

const fs = require('fs');
const list = [];

for (let i = 1; i <= 5; i++ ) {
    fs.readFile('./' + i + '.txt', 'utf-8' ,(err, data) => {
        if (err) throw err;
        list.push(data);
    });
}

// 在后台不断的轮训,当list的长度达到指定值(也就是回调全部结束),此时就可以进行清除定时器
let intervalId = setInterval(() => {
    console.log('waiting!...');
    if(list.length === 5) {
        console.log('SUCCESS');
        // 注意setInterval与clearInterval的配合
        clearInterval(intervalId);
    }
}, 1000);

插话:有点疑惑的是,clearInterval(intervalId)有种自身清除自身的感觉? 已知的,它们都是隶属于WindowOrWorkerGlobalScope的方法 (待填坑..)

但是在参考链接中的方法更佳,使用setInterval会加入新的事件循环? Javascript 单线程模型 Event Loop 机制

//记录总的文件大小,使用一个`count`计数来判断异步结束 
count = filenames.length;
for (i = 0; i < filenames.length; i++) {
  fs.stat("./" + filenames[i], function (err, stats) {
     totalBytes += stats.size;
     count--;
     if (count === 0) {
           console.log(totalBytes);
      }
   });
}
从这个例子中,我们可以学到一点:并发运行的相同异步函数如果协作完成任务,需要添加计数代码判断执行状态,并且把所有异步函数完成后执行的代码放在判断条件的语句块里

第三关: 中文乱码

前面的关卡好不容易闯过了后,兴高采烈的抓到了数据,写到了json里,可是一打开一开,gg,股票名称的字段,全是乱码?,网上提示,直接看一下源网站的编码,浏览器打开一看头部,charset="gb2312",难怪...

参考链接:

request库使用时, gb2312、GBK中文乱码解决方法

nodejs下request模块中文gb2312乱码问题

也就是引用iconv-lite包,对返回的gb2312body进行一下转码,Iconv.decode(body, 'gb2312').toString(),这样就可以了

数据库备份:

参考官方文档:

Upgrading MySQL on Windows,了解自己按哪种方式升级,我的是Upgrading MySQL Using the Windows ZIP Distribution

之后开始数据备份,Database Backup Methods

选择备份的方法, 我用的是Making Backups with mysqldump,去查看mysqldump的用法,mysqldump — A Database Backup Program文档比较多,我直接看的:Using mysqldump for Backups

// 直接使用命令行,报错 `Got error: 1045: Access denied for user 'ODBC'@'localhost' (using password: NO)`
mysqldump --all-databases > dump.sql

在网上找到Mysql备份还原数据库之mysqldump实例及参数详细说明

// 成功
mysqldump -u root -p --all-databases > dump.sql

新版本的安装

选用傻瓜版:msi,安装过程有一个注意事项,因为选的是Custom,但是在后来的界面中一直打开的mysql server都是最新版的8.0,而且安装到某个界面还没有next弹出...,后来才发现在Custom之后的选择界面有filter选项,点开,里面就可以选择各种版本的server了

数据的导入

进入mysql数据库,使用source命令:

source E:\xxx.sql

起因,妹子问的实习的公司连不上网,只有公司电脑连有线才可以,但是她在输ip地址从192.168.101.100192.168.101.254一个个试(已经试到206),还是连不上网怎么办?

其实我对这个连不上网的问题真的好菜啊...,仅有的一次还是利用192.168.1.1(还是什么去了..)登录上路由器,然后路由器密码还是贼简单,登上去了,就改了一个路由器的名字,开心了半天,然后就没有然后了...

回到正题,于是赶快google,知乎搜,还是不知道怎么做,就知道了一个DHCP静态ip的名词.. 以及看到网友推荐了一本书,网络是怎样连接的,mark一下了

接着只好很丢脸的说,我不会啊,但是妹子说的从192.168.101.100192.168.101.254一个个试,却留下了印象,就是,我们会用ping来测试网络是不是畅通,那么有没有一种方法能让自动的ping呢?

也就是今天自己学会的bat脚本:

刚开始搜到这个:shell脚本实现网络连接的检测,这里面的思路就用循环从0到254,依次ping,很好,不过这是shell脚本,也就是在我的电脑上装了git-bash是可以运行的,但是妹子的电脑是没有的,也就是要转化成bat脚本啊,于是坑就来了

bat脚本的格式语法问题:

@echo off ::开头,不显示后续命令行及当前命令行

set var_name = "hello" ::set来定义变量

echo %var_name%   ::echo用于输出,变量取值用%var%

for /l %%i in (1,1,254)  ::递增循环的示例 /l表示带开关,此处的变量是%%i
do (
    :: code here     
)

于是代码初步这样:

@echo off
::Ping网段所有IP
set ip="192.168.0."  
for /l %%i in (1,1,254)
do (
ping -c 2 %ip%%%i |grep -q 'ttl=' && echo "%ip%%%i yes"|| echo "%ip%%%i no"
)
::yes正常,no主机不存在或不正常   
pause

遇到的麻烦:

一闪而过...,应该是语法错误了,直接在cmd中输入ping,根本没有-c的参数,而且grepSearch for PATTERN in each FILE or standard input,都去掉,依然一闪而过...

后来改成for /l %%i in (1,1,254) do ( ping %ip%%%i && echo "%ip%%%i yes"|| echo "%ip%%%i no" ),写成一行可以,但报

错误的参数 1。
""192.168.0." 1 no"

在循环中拼接字符串,ping %ip%%%i 难道不可行?

批处理文件在循环中拼接字符串,为什么结果不对啊问题中发现也有人遇到过,是需要设置环境变量延迟,加上setlocal enabledelayedexpansion,并变成ping !ip!%%i,可还是报同样的错误

难道是%%i是数字类型,而ip是字符串类型根本不行?

在bat批处理脚本中,怎样将for语句中的%%i当作字符串处理?做类似于%str:~1,5%之类的操作?

可以用在循环中在设一个变量等于%%i的方法,即set num= %%i ping !ip!!num!,结果没了错误参数,变成

""192.168.0." 1 yes"

这到底好没好呢...

同时又有提到字符串拼接的批处理用的是set /a num+=1set str=!str! %%i ,再在cmd批处理中set /a和set /p的区别介绍,了解到

/P 命令行开关允许将变量数值设成用户输入的一行输入

/a 是指定一个变量等于一串运算字符

彩蛋是这里面的实例2竟然有ping的示例代码

@echo off
set a=1
:start
echo %a%
ping 172.19.5.%a% -w 1 -n 1|find /i "Lost = 1"&&set c=1||set c=0
if %c%==0 (echo 172.19.5.%a% >>IP.txt)
set /a a=%a%+1
if %a%==255 exit
goto :start

利用goto变成一个do...while的结构,而且巧妙的用172.19.5.%a%避免了之前的字符串拼接,最后还能把ping成功的ip写入txt

自己粘过来试,仍不成功...因为明明在我的电脑上172.19.5.x都是ping不通的,却也全部写入了txt文件

再次查看ping命令:发现-w为等待每次回复的超时时间(毫秒),这里设置为1,-n为要发送的回显请求数,而find,作者是想找到Lost=1的ping不通的,但是我的是中文...,设为丢失=1就好了:

1
数据包:已发送 = 1,已接受 = 0, 丢失 = 1 (100%丢失)

完美!

还有一种方式,之前看到设置超时为1,觉得是超时太断所致,即使是ping通的也会被判断为错误的,就直接改成,去掉find,并需要把判断条件改成%c%==1

ping 192.168.101.%a% -n 1 && set c=1 || set c=0
if %c%==1 (echo 192.168.101.%a% >>IP.txt)

这样也是可行的,但是每次都会输出一大堆的ping的回显消息了

其实过程中还有,各种报ECHO处于关闭状态的错误等,从下午到晚上,也算是第一次知道了shell编程是什么,以及借此好像阴差阳错的懂了一点点linux的强大

最后希望自己慢慢的不要那么菜吧哈哈

还有部分的参考连接:

.bat批处理(三):变量声明、设置、拼接、截取

BAT批处理为何报错echo处于关闭状态?

易百教程-批处理简介