Go এর জন্য CSV বয়লারপ্লেট · Felix Geisendörfer

প্রকাশিত:
tl; dr: Go-তে CSV ফাইল পড়ার এবং লেখার জন্য এখানে আমার বয়লারপ্লেট। আমি আশা করি আপনি এটি দরকারী পাবেন.
প্রতি একবারে, আমাকে Go-তে CSV ফাইলগুলি পড়তে এবং/বা লিখতে হবে। তত্ত্বে এটা সহজ। একজনকে শুধুমাত্র স্ট্যান্ডার্ড লাইব্রেরি থেকে এনকোডিং/সিএসভি অন্তর্ভুক্ত করতে হবে এবং একটি গো স্ট্রাকট এবং CSV রেকর্ডগুলির মধ্যে ম্যাপ করতে কিছুটা বয়লারপ্লেট লিখতে হবে।
যাইহোক, ফলস্বরূপ কোডটি প্রায়ই বিভিন্ন কারণে আমাকে অসন্তুষ্ট করে রেখেছে:
- আমি চাকাটি পুনরায় উদ্ভাবন করতে অনেক বেশি সময় ব্যয় করি, কোড গঠনের বিভিন্ন উপায় নিয়ে খেলা করি।
- কোডটি বজায় রাখা বিরক্তিকর, যেমন আউটপুট কলামের ক্রম পরিবর্তন করার জন্য প্রায়শই কমপক্ষে 4টি জায়গায় পরিবর্তনের প্রয়োজন হয়: কাঠামোর সংজ্ঞা, শিরোনাম লেখা, রেকর্ড লেখা, এবং রেকর্ড পড়া।
- খারাপ CSV ইনপুটের ক্ষেত্রে ফলাফলটি খুব শক্তিশালী নয় এবং অনুপস্থিত, অপ্রত্যাশিত বা ভুল কলাম বা হেডারের মতো সমস্যাগুলি পরিচালনা করে না।
- ত্রুটি বার্তাগুলি প্রায়শই সারি নম্বর, কলামের নাম ইত্যাদির মতো গুরুত্বপূর্ণ তথ্য হারিয়ে ফেলে।
- আমি কীভাবে এর জন্য ভাল পরীক্ষা লিখব যা ভবিষ্যতে বজায় রাখা সহজ হবে?
এটি মোকাবেলা করার একটি উপায় হল একটি উচ্চ স্তরের লাইব্রেরি ব্যবহার করা, এবং আপনার অবশ্যই এটি বিবেচনা করা উচিত। কিন্তু বাস্তবতা প্রায়ই একটি কুৎসিত জগাখিচুড়ি. আপনি হয়তো দেখতে পাচ্ছেন যে আপনার পছন্দের লাইব্রেরি একটি নির্দিষ্ট প্রান্তের ক্ষেত্রে পরিচালনা করে না, যেমন একাধিক হেডার সারি উপস্থিত থাকা (আমার ব্যাঙ্ক 6টি হেডার লাইন রাখতে পছন্দ করে)। অথবা আপনি যে ত্রুটি বার্তাগুলি পাচ্ছেন তা আপনার ব্যবহারকারীদের জন্য যথেষ্ট সহায়ক নয়৷ অথবা ত্রুটি হ্যান্ডলিং খুব কঠোর. অথবা প্রতিফলন ভিত্তিক ম্যাপিং খুব জাদু. অথবা কর্মক্ষমতা sucks. আমি চালিয়ে যেতে পারি, কিন্তু আপনি বুঝতে পেরেছেন – CSV এর সাথে ডিল করা বাজে এবং এটিকে একটি বিন্যাস বলা একটি বিন্যাসের ধারণার জন্য কিছুটা অপমানজনক। আমার দৃষ্টিকোণ থেকে, এই সমস্যাটির জন্য একটি লাইব্রেরি লেখা একটি ভয়ঙ্করভাবে অতিরিক্ত প্রকৌশলী জগাখিচুড়ি তৈরি না করে বা অনেক প্রান্তের ক্ষেত্রে উপেক্ষা করা ছাড়াই সম্ভব নয়।
তাই শেষ পর্যন্ত মনে হচ্ছে আপনাকে দুটি সাবঅপ্টিমাল বিকল্পের মধ্যে বেছে নিতে হবে। আপনি একগুচ্ছ লাইব্রেরি ব্যবহার করে দেখতে পারেন এবং আপনার প্রয়োজনের সবচেয়ে কাছের একটি ব্যবহার এবং/অথবা কাঁটাচামচ করতে পারেন। অথবা আপনি আপনার নিজের বয়লারপ্লেটটি মিলিয়নতম বার রোল করতে পারেন, শুধুমাত্র কিছুক্ষণ পরেই ভয়ঙ্কর বিরক্তির সাথে এটির দিকে ফিরে তাকাতে পারেন।
আপনার পছন্দ সম্ভবত আপনার ইনপুট এর পাগলামি উপর নির্ভর করা উচিত, এবং আমি আপনার জন্য এটি করতে সক্ষম হবে না. কিন্তু আমি সিদ্ধান্ত নিয়েছি যে আমি ভবিষ্যতে আমার জন্য বয়লারপ্লেট রুটটিকে কিছুটা কম বিরক্তিকর করতে চাই। এটি সম্পন্ন করার জন্য আমি একটি বয়লারপ্লেট টেমপ্লেট তৈরি করেছি যা আমি আমার ভবিষ্যতের প্রয়োজনের জন্য অনুলিপি এবং পেস্ট করব, প্রয়োজন অনুসারে এটি সামঞ্জস্য করব। ব্লগ পোস্টের বাকি অংশের জন্য আমি এটি ডিজাইন করার জন্য আমার চিন্তা প্রক্রিয়ার মাধ্যমে আপনাকে হাঁটতে চাই।
শুরু করার জন্য, আমি আমার উদাহরণ হিসাবে বাস্তব জগতের CSV ব্যবহার করার সিদ্ধান্ত নিয়েছি, যার মধ্যে সাধারণ কুশ্রীতার বিট রয়েছে যা একজনকে মোকাবেলা করতে হয়। আমি দ্রুত cars.csv নামে একটি ফাইল খুঁজে পেয়েছি যা দেখতে এইরকম:
Car;MPG;Cylinders;Displacement;Horsepower;Weight;Acceleration;Model;Origin
STRING;DOUBLE;INT;DOUBLE;DOUBLE;DOUBLE;DOUBLE;INT;CAT
Chevrolet Chevelle Malibu;18.0;8;307.0;130.0;3504.;12.0;70;US
Buick Skylark 320;15.0;8;350.0;165.0;3693.;11.5;70;US
Plymouth Satellite;18.0;8;318.0;150.0;3436.;11.0;70;US
...
আপনি দেখতে পাচ্ছেন, এখানে ইতিমধ্যে দুটি কুৎসিত বিট পরীক্ষা করা হয়েছে: একটি বিভাজক হিসাবে কমার পরিবর্তে সেমিকোলন, এবং একটি দ্বিতীয় শিরোনাম লাইন যা যেকোনো কারণেই টাইপ তথ্য ধারণ করে বলে মনে হয়। দুর্দান্ত নয়, ভয়ানক নয়, তবে সেখানে যা আছে তার জন্য বেশ সাধারণ।
আমার বয়লারপ্লেটের লক্ষ্য হল ফাইল ফরম্যাটটি এমনভাবে পড়া এবং লেখা যা শুরুতে বর্ণিত পাঁচটি সমস্যার সমাধান করে। এটি একটি অত্যধিক পরিমাণ বিমূর্ততা ছাড়া তা করা উচিত, এবং অবশ্যই কোন প্রতিফলন, তবুও যতটা সম্ভব মার্জিতভাবে। নীচে দেখানো হিসাবে মেমরিতে ডেটা ধরে রাখার জন্য একটি মধ্যবর্তী গো স্ট্রাকটও থাকা উচিত।
type Car struct {
Car string
MPG float64
Cylinders int64
Displacement float64
Horsepower float64
Weight float64
Acceleration float64
Model int64
Origin string
}
সাধারণত আমার প্রথম নিরীহ প্রচেষ্টা হল কিছু কোড লেখা যা হেডার এবং স্ট্রাকট ম্যাপিংকে এমনভাবে সংজ্ঞায়িত করে যা CSV এনকোডিং/ডিকোডিং থেকে আলাদা। এখানে একটি উদাহরণ:
var carHeaders = ()string{
"Car",
"MPG",
"Cylinders",
// remaining columns ...
}
func (c *Car) UnmarshalRecord(record ()string) error {
if got, want := len(record), len(carHeaders); got != want {
return fmt.Errorf("bad column number: got=%d want=%d", got, want)
}
c.Car = record(0)
if c.MPG, err = strconv.ParseFloat(record(1), 64); err != nil {
return fmt.Errorf("column=%q: %w", carHeaders(1), err)
} else if c.Cylinders, err = strconv.ParseInt(record(2), 10, 64); err != nil {
return fmt.Errorf("column=%q: %w", carHeaders(2), err)
}
// remaining columns ...
return nil
}
func (c *Car) MarshalRecord() (()string, error) {
return ()string{
c.Car,
fmt.Sprintf("%f", c.MPG),
fmt.Sprintf("%d", c.Cylinders),
// remaining columns ...
}, nil
}
এটি কাজ করে, কিন্তু যেকোনও কলাম যোগ করা, অপসারণ করা বা পুনর্বিন্যাস করার জন্য আমাদের 4 থেকে 5টি ভিন্ন জায়গায় আমাদের কোড পরিবর্তন করতে হবে এবং আমি শুরুতে উল্লেখ করেছি, আমরা এটি এড়াতে চাই। সুতরাং আসুন কিছু বিমূর্ততা চেষ্টা করি যা আমাদের কলামগুলিকে ঘোষণামূলকভাবে নির্দিষ্ট করতে দেয় এবং কীভাবে সেগুলি মার্শাল/আনমার্শাল করতে হয়:
type carColumn struct {
Name string
Type string
UnmarshalValue func(*Car, string) error
MarshalValue func(*Car) (string, error)
}
var carColumns = ()carColumn{
{
"Car",
"STRING",
func(c *Car, val string) error {
c.Car = val
return nil
},
func(c *Car) (string, error) {
return c.Car, nil
},
},
{
"MPG",
"DOUBLE",
func(c *Car, val string) (err error) {
c.MPG, err = strconv.ParseFloat(val, 64)
return
},
func(c *Car) (string, error) {
return fmt.Sprintf("%f", c.MPG), nil
},
},
{
"Cylinders",
"INT",
func(c *Car, val string) (err error) {
c.Cylinders, err = strconv.ParseInt(val, 10, 64)
return
},
func(c *Car) (string, error) {
return fmt.Sprintf("%d", c.Cylinders), nil
},
},
// remaining columns ...
}
এটি অবশ্যই একটি বিট শব্দপূর্ণ, কিন্তু এটি আমাদের সমস্যার সমাধান করে। আমাদের কলামগুলিকে পুনরায় সাজানো এখন একটি একক কাট এবং পেস্ট অপারেশন, এবং একটি কলাম যোগ করা বা সরানোও অনেক সহজ হয়েছে৷ এই পদ্ধতির এমনকি আমাদের একটি সুবিধাজনক উপায় দেয় Type CSV ফাইলের দ্বিতীয় লাইনে তথ্য পাওয়া যায়, তাই পরবর্তীতে আমাদের নিজস্ব CSV ফাইল লেখার সময় আমরা সহজেই এই ব্যঙ্গের নকল করতে পারি।
আমরা এখনও আমাদের আপডেট করতে হবে UnmarshalRecord এবং MarshalRecord আগে থেকে ভাল খবর হল যে আমাদের এই কোডটি আবার সংশোধন করতে হবে এমন সম্ভাবনা নেই:
func (c *Car) UnmarshalRecord(record ()string) error {
if got, want := len(record), len(carColumns); got != want {
return fmt.Errorf("bad number of columns: got=%d want=%d", got, want)
}
for i, col := range carColumns {
if err := col.UnmarshalValue(c, record(i)); err != nil {
return fmt.Errorf("column=%q: %w", col.Name, err)
}
}
return nil
}
func (c *Car) MarshalRecord() (()string, error) {
record := make(()string, len(carColumns))
for i, col := range carColumns {
val, err := col.MarshalValue(c)
if err != nil {
return nil, err
}
record(i) = val
}
return record, nil
}
এখন CSV ডিকোডিং নিজেই বাস্তবায়ন করার সময়। আমার বয়লারপ্লেট এর মাধ্যমে এটি প্রয়োগ করে ReadCarsCSV ফাংশন যা নীচে দেখা যাবে।
const carComma = ';'
func ReadCarsCSV(r io.Reader) (()*Car, error) {
cr := csv.NewReader(r)
cr.Comma = carComma
records, err := cr.ReadAll()
if err != nil {
return nil, err
}
cars := make(()*Car, 0, len(records))
for i, record := range records {
if got, want := len(record), len(carColumns); got != want {
return nil, fmt.Errorf("row=%d: bad number of columns: got=%d want=%d", i+1, got, want)
}
switch i {
case 0:
for i, got := range record {
if want := carColumns(i).Name; got != want {
return nil, fmt.Errorf("unexpected header column %d: got=%q want=%q", i, got, want)
}
}
case 1:
for i, got := range record {
if want := carColumns(i).Type; got != want {
return nil, fmt.Errorf("unexpected type column %d: got=%q want=%q", i, got, want)
}
}
default:
car := &Car{}
if err := car.UnmarshalRecord(record); err != nil {
return nil, fmt.Errorf("row=%d: %w", i+1, err)
}
cars = append(cars, car)
}
}
return cars, nil
}
উপরের কোডটি প্রদত্ত থেকে পড়া সমস্ত গাড়ি ফেরত দেওয়ার প্রধান কাজটি সম্পন্ন করে io.Readerকিন্তু এটি কলামের সংখ্যা, সেইসাথে প্রথম দুটি সারিতে পাওয়া হেডার এবং টাইপ নামগুলিকেও যাচাই করে। কোনো একটি রেকর্ডে কিছু ভুল হলে সারি নম্বরের পাশাপাশি আপত্তিকর কলামের নাম উভয় সহ ত্রুটি বার্তাগুলিও ভাল হওয়া উচিত।
এটি পরিবর্তন করাও সহজ হওয়া উচিত। উদাহরণস্বরূপ, যদি আপনি পড়ার সময় দ্বিতীয় শিরোনাম লাইনটি উপেক্ষা করতে চান তবে কোডটি সরিয়ে ফেলুন। অথবা সমস্ত শিরোনামগুলির একটি নির্দিষ্ট অবস্থানের প্রয়োজনের পরিবর্তে, আপনি ইনপুট থেকে তাদের নামগুলির সাথে নামের সাথে মিল করে গতিশীলভাবে তাদের অবস্থান আবিষ্কার করতে পারেন carColumns টুকরো টুকরো টুকরো টুকরো টুকরো টুকরো টুকরো টুকরো টুকরো টুকরো করে রেকর্ড করুন
অথবা আপনি সিদ্ধান্ত নিতে পারেন যে আপনার মেমরির ব্যবহার এবং GC চাপ কমাতে একটি স্ট্রিমিং ইন্টারফেস প্রয়োজন:
type CarReader struct {
r io.Reader
// ...
}
func (r *CarReader) Read(c *Car) error {
// ...
}
সম্ভাবনাগুলি অন্তহীন, এবং আপনি আপনার লাইব্রেরির পছন্দ দ্বারা নিজেকে এক কোণে ব্যাক পাবেন না।
পরবর্তীতে আমাদের ডেটাকে আবার CSV-এ রূপান্তর করা হচ্ছে। আমার সমাধান এই মত দেখায়:
func WriteCarsCSV(w io.Writer, cars ()*Car) error {
cw := csv.NewWriter(w)
cw.Comma = carComma
header := make(()string, len(carColumns))
types := make(()string, len(carColumns))
for i, col := range carColumns {
header(i) = col.Name
types(i) = col.Type
}
cw.Write(header)
cw.Write(types)
for _, car := range cars {
record, err := car.MarshalRecord()
if err != nil {
return err
}
cw.Write(record)
}
cw.Flush()
return cw.Error()
}
আপনি দেখতে পাচ্ছেন, আমরা অদ্ভুত দ্বিতীয় হেডার লাইনটি লেখার যত্ন নিই। আমরা প্রত্যেকের জন্য ত্রুটি পরীক্ষা না করে কিছু কোড সংরক্ষণ করি cw.Write() কল করুন এবং ফিরে এসে তাদের পরিচালনা করুন cw.Error() পরিবর্তে শেষে.
এখন আমাদের পরীক্ষার কথা ভাবা উচিত। পরীক্ষার ক্ষেত্রে আমি 80/20 নিয়মের একজন বড় অনুরাগী, তাই নীচে একটি পরীক্ষা রয়েছে যা খুব দক্ষতার সাথে সুখী পথকে কভার করে।
func TestCSVReadWriteCycle(t *testing.T) {
in := strings.TrimSpace(`
Car;MPG;Cylinders;Displacement;Horsepower;Weight;Acceleration;Model;Origin
STRING;DOUBLE;INT;DOUBLE;DOUBLE;DOUBLE;DOUBLE;INT;CAT
Chevrolet Chevelle Malibu;18.0;8;307.0;130.0;3504.;12.0;70;US
Buick Skylark 320;15.0;8;350.0;165.0;3693.;11.5;70;US
Plymouth Satellite;18.0;8;318.0;150.0;3436.;11.0;70;US
`)
wantOut := strings.TrimSpace(`
Car;MPG;Cylinders;Displacement;Horsepower;Weight;Acceleration;Model;Origin
STRING;DOUBLE;INT;DOUBLE;DOUBLE;DOUBLE;DOUBLE;INT;CAT
Chevrolet Chevelle Malibu;18.000000;8;307.000000;130.000000;3504.000000;12.000000;70;US
Buick Skylark 320;15.000000;8;350.000000;165.000000;3693.000000;11.500000;70;US
Plymouth Satellite;18.000000;8;318.000000;150.000000;3436.000000;11.000000;70;US
`)
for i := 0; i < 2; i++ {
cars, err := ReadCarsCSV(strings.NewReader(in))
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
if err := WriteCarsCSV(buf, cars); err != nil {
t.Fatal(err)
}
gotOut := strings.TrimSpace(buf.String())
if gotOut != wantOut {
t.Fatalf("\ngot:\n%s\nwant:\n%s", gotOut, wantOut)
}
in = gotOut
}
}
পরীক্ষাটি প্রথমে প্রদান করা হয় in CSV, এবং নিশ্চিত করে যে এটিকে আবার CSV-এ রূপান্তরিত করা হয় wantOut যা একটু ভিন্ন কারণ Go আমাদের ফ্লোটিং পয়েন্ট নম্বরগুলিকে ফর্ম্যাট করে। তারপরে পরীক্ষাটি প্রথম পুনরাবৃত্তি থেকে আউটপুটকে দ্বিতীয়টির জন্য ইনপুট হিসাবে বিবেচনা করে। এটি নিশ্চিত করে যে আমাদের বাস্তবায়ন মূল বিন্যাস পড়তে পারে, সেইসাথে এটি নিজেই তৈরি করা সামান্য ভিন্ন আউটপুট।
অবশ্যই আমরা মূল ফাইল থেকে ফ্লোট ফরম্যাটিং কুইর্কগুলি পুনরুত্পাদন করার লক্ষ্য রাখতে পারি। প্রথম নজরে দেখে মনে হচ্ছে সমস্ত ফ্লোটগুলি পিরিয়ডের পরে এক অঙ্ক দিয়ে ফর্ম্যাট করা হয়েছে৷ যাইহোক, একটি ঘনিষ্ঠ চেহারা প্রকাশ করে যে Weight কলামের একটি পিরিয়ড আছে, কিন্তু নিচের অঙ্কটি এড়িয়ে যায়, যেমন 3504.. আমি এই খরগোশের গর্তে না যাওয়ার সিদ্ধান্ত নিয়েছি, কিন্তু এটা পরিষ্কার হওয়া উচিত যে আমাদের বয়লারপ্লেট পদ্ধতি আমাদেরকে এইরকম CSV ব্যঙ্গগুলির সাথে মোকাবিলা করার জন্য একটি দুর্দান্ত অবস্থানে রাখে৷
আপনি CSV পার্স করার বিষয়ে কতটা গুরুতর তার উপর নির্ভর করে, আপনি সম্ভবত কয়েকটি ত্রুটির ক্ষেত্রেও পরীক্ষা করতে চান এবং হয়ত এতে কিছু অস্পষ্টতাও ফেলতে চান। কিন্তু আমি সিদ্ধান্ত নিয়েছি যে উপরের পরীক্ষাটি আমার বয়লারপ্লেটের জন্য যথেষ্ট ভালো, তাই আপনাকে এই অংশটি নিজেই করতে হবে।
ঠিক আছে, এটাই। আমি আপনার চিন্তা শুনতে চাই, বিশেষ করে যদি আপনার কাছে এটি আরও উন্নত করার জন্য ধারণা থাকে। অথবা আপনি যদি একগুচ্ছ বয়লারপ্লেট কপি ও পেস্ট করার ধারণার ভালো বিকল্প দেখতে পান তাহলে আমাকে জানান। আমি এটি পছন্দ করি এমনটা নয়, তবে যতদূর আমি বলতে পারি এটি এই ক্ষেত্রে গো-এর সাথে কাজ করার জন্য একটি মিষ্টি জায়গা হিট করে।
ধন্যবাদ থর্স্টেন বল এবং টমাস সেনার্ট এই পোস্টটি পর্যালোচনা করার জন্য এবং অনেক মূল্যবান পরামর্শ দেওয়ার জন্য।
— ফেলিক্স গেইসেনডোরফার
আরএসএস বা ই-মেইলের মাধ্যমে এই ব্লগে সদস্যতা নিন বা এর মাধ্যমে আমার কাছ থেকে ছোট আপডেট পান টুইটার.
