Golang如何将所需数据量写入CSV文件
Golang如何将所需数据量写入CSV文件 我正在尝试编写一个爬虫,从RFC网站的正文中获取数据,然后将数据解析到CSV文件中。同时,我还试图获取长度大于3的前20个最流行的单词。不幸的是,我似乎无法得到我想要的数量。这是我的结果:

它基本上获取了所有单词及其出现次数(以映射的形式,即键和值),并且忽略了我指定的限制。
我认为问题出在第152行附近。这是完整代码的链接: https://play.golang.org/p/RZ9iCRRKypA
以下是执行解析的函数:
func writeSlice(results chan []MyMap, outfile string, headers []string) error {
f, err := os.OpenFile(outfile, os.O_RDONLY|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("couldnt open file %s", outfile)
}
defer f.Close()
w := csv.NewWriter(f)
w.Flush()
if err = w.Write(headers); err != nil {
return fmt.Errorf("couldnt write headers to file %s", outfile)
}
data := make([]string, 0, len(results))
for v := range results {
for i := 0; i < 20; i++ {
data = append(data, v[i].key)
data = append(data, strconv.Itoa(v[i].value))
if err = w.Write(data); err != nil {
return fmt.Errorf("couldnt write headers to file %s", outfile)
}
}
}
if err = w.Error(); err != nil {
return fmt.Errorf("couldnt write headers to file %s", outfile)
}
return nil
}
我的问题在于,我不确定如何具体操作,只获取具有特定长度的前20个结果,并使用w.Write()写入结果。
更多关于Golang如何将所需数据量写入CSV文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢您付出的时间。我会花时间仔细研究您的实现,如果有需要,也会向您提问。再次感谢。
更多关于Golang如何将所需数据量写入CSV文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
如果你想只考虑包含字母的单词,请移除
|| unicode.IsNumber(c)。
是的,我只需要单词。
JustinObanor: 如果我只是想输出结果,我可以这样做。
是否可以在单线程下完成?暂时不使用并发?
Christophe_Meessen:
然后将数据解析到csv文件
抱歉,我的意思是我正尝试将结果写入csv文件。结果是指长度大于3个字母的前20个单词。
如果我只是想输出结果,可以这样做:
for i := 0; i < *concurrency; i++ {
go func() {
for value := range tasks {
site, err := scraper(value.url, value.id, query)
if err != nil {
fmt.Printf("error parsing data %v", err)
}
for i := 0; i < 20; i++ {
fmt.Printf("%s\t : %d\n", site[i].key, site[i].value)
}
results <- site
}
wg.Done()
}()
}
还不知道如何检查单词长度是否大于3。
抱歉,之前使用我自己实现的堆的 mostFrequentWords 函数是错误的。
我已经实现并修正了这些函数。我还对它们进行了基准测试,以找出最高效的那个。 你可以在我的 github 仓库 中找到它们。
以下是最简单且最高效的实现:
func mostFrequentWords(m map[string]int, nbrWords int) []elem {
h := elemHeap(make([]elem, nbrWords))
last := nbrWords - 1
for word, count := range m {
if count < h[last].count || (count == h[last].count && word > h[last].word) {
continue
}
pos := sort.Search(nbrWords, func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word > word) })
copy(h[pos+1:], h[pos:last])
h[pos] = elem{count: count, word: word}
}
if len(m) < nbrWords {
return h[:len(m)]
}
return h
}
抱歉造成了困扰。
我对其进行了修改,以便可以传递一个字符串切片作为参数,因为我的大部分代码都在使用切片。希望这是一个好的方法。
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
text := []string{"this is my is is my wow so is is my my so hey no yes"}
m := countWordsIn(text)
for k, v := range m {
fmt.Printf("%d : %s\n", v, k)
}
}
func countWordsIn(text []string) map[string]int {
var wordBegPos, runeCount int
wordCounts := make(map[string]int)
texts := strings.Join(text, " ")
for i, c := range texts {
if unicode.IsLetter(c) {
if runeCount == 0 {
wordBegPos = i
}
runeCount++
continue
}
if runeCount > 3 {
word := texts[wordBegPos:i]
count := wordCounts[word]
count++
wordCounts[word] = count
}
runeCount = 0
}
return wordCounts
}
我修改了代码,以便能够传递一个字符串切片作为参数,因为我的大部分代码都在使用切片。希望这是一个好的方法。
我有点困惑。你为什么需要一个字符串切片?每个RFC都可以加载到一个文本字符串中。
以下是爬虫函数的一个示例实现。如你所见,RFC文本被完整地读入 b,这是一个字节切片。
func scraper(url string, id int) (map[string]int, error) {
client := &http.Client{
Timeout: time.Minute,
}
resp, err := client.Get(url)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, fmt.Errorf("error making request %v", err)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading RFC text %v", err)
}
m := countWordsIn(string(b))
return m, nil
}
然后,主程序将为每个URL调用爬虫函数,并将返回的结果累加以汇总所有已爬取RFC中的单词计数,如果这是你想要做的事情的话。
正如 @hollowaykeanho 所建议的,首先实现一个单线程版本,并确保其正常工作,然后再实现并发版本。
以下是 mostFrequentWords 的一个更优实现。它比堆的实现更简单,但效率稍低。其主要优点是,结果与你构建一个映射内容的切片、进行排序并返回前 nbrWords 个条目的子切片所得到的结果完全相同。该算法非常简单。它构建一个包含 nbrWords 个元素的列表,并保持其排序状态,根据需要插入新元素。
func mostFrequentWords(m map[string]int, nbrWords int) []elem {
h := elemHeap(make([]elem, 0, nbrWords))
last := nbrWords - 1
for word, count := range m {
if len(h) < nbrWords {
pos := sort.Search(len(h), func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word >= word) })
if pos == len(h) {
h = append(h, elem{count: count, word: word})
continue
}
h = append(h, elem{})
copy(h[pos+1:], h[pos:len(h)])
h[pos] = elem{count: count, word: word}
continue
}
if count >= h[last].count {
pos := sort.Search(nbrWords, func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word >= word) })
copy(h[pos+1:], h[pos:last])
h[pos] = elem{count: count, word: word}
}
}
return h
}
这是不使用堆包(该包需要定义 elemHeap 类型和函数)的 mostFrequentWord 函数。它在内部实现了 heap.Fix() 函数。
type elem struct {
word string
count int
}
func mostFrequentWords(m map[string]int, nbrWords int) []elem {
h := elemHeap(make([]elem, nbrWords))
for word, count := range m {
if count < h[0].count {
continue
}
h[0] = elem{word: word, count: count}
for i, j := 0, 0; i < nbrWords; i = j {
j := i * 2
if j < nbrWords && h[i].count > h[j].count {
h[j], h[i] = h[i], h[j]
continue
}
j++
if j < nbrWords && h[i].count > h[j].count {
h[j], h[i] = h[i], h[j]
continue
}
break
}
}
sort.Slice(h, func(i, j int) bool { return h[i].count > h[j].count })
return h
}
countWordsIn() 的实现存在一个小问题。单词将是 RFC 文本的子切片。其副作用是,RFC 文本将不会被垃圾回收,我们最终可能会在内存中保留所有 RFC 文本。我们不必为此担心,因为一份 RFC 文本并不那么大,而且 RFC 的数量也不多。这最多只需要几兆字节的内存。
为了避免这种情况,我们必须为用作 wordCounts 映射键的单词创建一个副本。遗憾的是,在 Go 中没有简单且可靠的方法来做到这一点。
问题出在writeSlice函数中,你从通道读取了所有结果,但只对每个结果的前20个元素进行了处理。实际上,你需要先收集所有数据,然后进行排序和筛选,最后只写入前20个符合条件的记录。
以下是修改后的writeSlice函数,它会先收集所有数据,然后筛选出长度大于3的单词,按出现频率排序,最后只写入前20个:
func writeSlice(results chan []MyMap, outfile string, headers []string) error {
f, err := os.Create(outfile)
if err != nil {
return fmt.Errorf("couldnt open file %s: %v", outfile, err)
}
defer f.Close()
w := csv.NewWriter(f)
defer w.Flush()
// 写入表头
if err := w.Write(headers); err != nil {
return fmt.Errorf("couldnt write headers to file %s: %v", outfile, err)
}
// 收集所有数据
var allItems []MyMap
for v := range results {
allItems = append(allItems, v...)
}
// 筛选长度大于3的单词
var filteredItems []MyMap
for _, item := range allItems {
if len(item.key) > 3 {
filteredItems = append(filteredItems, item)
}
}
// 按值(出现次数)降序排序
sort.Slice(filteredItems, func(i, j int) bool {
return filteredItems[i].value > filteredItems[j].value
})
// 只取前20个
limit := 20
if len(filteredItems) < limit {
limit = len(filteredItems)
}
// 写入数据
for i := 0; i < limit; i++ {
record := []string{
filteredItems[i].key,
strconv.Itoa(filteredItems[i].value),
}
if err := w.Write(record); err != nil {
return fmt.Errorf("couldnt write data to file %s: %v", outfile, err)
}
}
return nil
}
另外,你需要确保MyMap类型是可导出的,并且添加排序功能:
type MyMap struct {
Key string
Value int
}
// 在main函数或其他地方添加排序逻辑
func sortMyMaps(maps []MyMap) []MyMap {
sort.Slice(maps, func(i, j int) bool {
if maps[i].Value == maps[j].Value {
return maps[i].Key < maps[j].Key
}
return maps[i].Value > maps[j].Value
})
return maps
}
这个修改后的版本会:
- 收集所有从通道传来的数据
- 筛选出键(单词)长度大于3的项
- 按值(出现次数)降序排序
- 只取前20个结果写入CSV文件
注意:我还修改了文件打开方式为os.Create,这会清空已存在的文件内容,确保每次写入都是新的数据。


