迭代器的运用——批量获取hitokoto语句
等我生成一份500kb的json文件,看哪位小朋友还敢硬编码。
发布于 2021927|更新于 2021928|遵循 CC BY-NC-SA 4.0 许可

原本🐱忙着睡觉,并不想自己提供一份超大数据文件给小朋友。不过我显然高估了小朋友的读题能力——当我看到有同学重复了10+遍DOM树操作时,我差点在数据结构课上气得跳起来。

总之,感谢这位同学治好了我的低血压,我花了一个小时,粗略地认识了迭代器,顺便写了一个可以批量收集hitokoto语句的小脚本。在🎮的帮助下,增加了处理Status Code 514访问拒绝的部分,由于能够在访问频率过快时自动等待一段时间重启,获取数据所需时间显著下降👍。

Prerequisite

我完成的第一个版本使用了计时器来控制请求的发送。

javascript
复制代码
1const timeId = setInterval(sendRequest, 1000); 2setTimeout(() => clearInterval(timeId), 1000*n);

简化代码如上,通过改变n的数量,以每秒1次的频率向目标接口发送请求,并把data加入到数组中。

很显然这是一个非常不优雅的解决方案,有一种手工计时的味道,以及没有catch可能出现的错误,使程序不太稳定。

在面向StackOverflow编程后,我意识到异步迭代+generator的组合可能比较适合这个使用场景,开始尝试。

Iterable object

这是我第一次见到可迭代对象🤦‍♂️。简而言之,iterable object中包含了我们要迭代的范围,每一轮循环中需要返回的变量,以及循环被推进和判断迭代是否结束的方法next()

下面就是一个例子:

javascript
复制代码
1let range = { 2 from: 1, 3 to: 5,//迭代返回 4 5 [Symbol.iterator]() { 6 this.current = this.from; 7 return this;//要返回的变量 8 }, 9 10 next() { 11 if (this.current <= this.to) { 12 return { done: false, value: this.current++ };//如何推进循环 13 } else { 14 return { done: true };//迭代是否结束 15 } 16 } 17};

现在,这个迭代器就可以使用for...of方法来遍历了。

javascript
复制代码
1for (let num of range) { 2 alert(num); //1 2 3 4 5 3}

Generator

通常来说,一个function只能返回return一次,而generator可以使用yield返回任意次,直到触发了return为止。此时的donetrue

javascript
复制代码
1function* generateSequence() { 2 yield 1; 3 return 2; 4} 5const generator = generateSequence(); 6const one = generator.next();//{ value: 1, done: false } 7const two = generator.next();//{ value: 2, done: true }

可以想象到的是,我们可以通过传参,yield出我们想要的结果。

javascript
复制代码
1function* generateSequence(start, end) { 2 for (let i = start; i <= end; i++) { 3 yield i; 4 } 5} 6for(let value of generateSequence(1, 5)) { 7 alert(value); // 1 2 3 4 5 8}//遍历输出

在这个例子中,已经可以举一反三,利用临时变量的累加来达到批量发送请求的目的了。

迭代!

为什么不直接使用for循环呢?在一般情况下,for循环都是同步进行的。也就是说,当循环100次时,这100次的请求是几乎同步发送的,因为我们没有要求剩下99次请求需要等待第1次请求返回后再依次发送。因此,需要利用for await来进行异步操作。那么,只需要使用generator来生成一个间隔一段时间就发送一次请求的迭代器即可。

javascript
复制代码
1async function* generateSequence(end) { 2 for (let i = 1; i <= end; i++) { 3 const response = await getData(); 4 await new Promise((resolve) => setTimeout(resolve, 100));//每次发送请求需要等待一段时间,否则请求过快会被拒绝 5 yield response; 6 } 7}

这里我返回的是response,一个json变量。最后,只需要在for await中遍历创建好的迭代器,依次将获得的json对象push入事先创建的数组之中就能完成获取。

javascript
复制代码
1let data = []; 2let generator = generateSequence(100);//获取100条语句 3for await (let response of generator) data.push(response);//遍历迭代器并将结果push进数组

加速

完成这样一个循环后,程序已经可以运行了。但是由于服务器和网络等因素的限制,如果请求速度过快(如500ms/次),就有可能出现Status code 514的错误。如果此时不处理这个错误,程序将直接退出。这显然不是一个稳定、快速的脚本,因此我在迭代器中增加了一些错误处理,当请求失败时,等待一段时间再重新进行请求。

javascript
复制代码
1const getData = async () => { 2 try { 3 const response = await axios.get('https://v1.hitokoto.cn', { 4 params: { 5 encode: 'json', 6 }, 7 }); 8 console.log('👍已获取数据!'); 9 return await response.data; 10 } catch (e) { 11 console.log('🙀访问拒绝,等待中...'); 12 await sleep(1000); 13 console.log('🐱重新获取数据...'); 14 return {}; 15 } 16}; 17 18async function* generateSequence(end) { 19 for (let i = 1; i <= end; i++) { 20 let response = {}; 21 while (JSON.stringify(response) === '{}') {//当请求失败时,getData将等待1s后返回{},此时重启请求 22 response = await getData(); 23 } 24 await sleep(200);//等待200ms 25 yield response; 26 } 27}

增加容错之后,相比于第一个版本1s/次的速度,获取数据的效率显著上升。

太棒了!学到许多👍

Comments